Compare commits
788 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -1722,3 +1722,346 @@
|
||||
[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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2019 Cloudron UG
|
||||
Copyright (c) 2020 Cloudron UG
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
|
||||
|
||||
@@ -48,18 +48,8 @@ the dashboard, database addons, graph container, base image etc. Cloudron also r
|
||||
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
|
||||
## Support
|
||||
|
||||
* [Documentation](https://cloudron.io/documentation/)
|
||||
|
||||
## Related repos
|
||||
|
||||
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
|
||||
the containers in the Cloudron.
|
||||
|
||||
## Community
|
||||
|
||||
* [Chat](https://chat.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,8 +29,6 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
|
||||
|
||||
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
|
||||
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
|
||||
ubuntu_version=$(lsb_release -rs)
|
||||
ubuntu_codename=$(lsb_release -cs)
|
||||
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
|
||||
apt-get -y install \
|
||||
acl \
|
||||
@@ -44,7 +44,6 @@ apt-get -y install \
|
||||
linux-generic \
|
||||
logrotate \
|
||||
mysql-server-5.7 \
|
||||
nginx-full \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
resolvconf \
|
||||
@@ -54,6 +53,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-1~${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
|
||||
|
||||
@@ -62,10 +67,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"
|
||||
|
||||
@@ -111,7 +116,7 @@ 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
|
||||
@@ -123,6 +128,13 @@ timedatectl set-ntp 1
|
||||
# mysql follows the system timezone
|
||||
timedatectl set-timezone UTC
|
||||
|
||||
echo "==> Adding sshd configuration warning"
|
||||
sed -e '/Port 22/ i # NOTE: Cloudron only supports moving SSH to port 202. See https://cloudron.io/documentation/security/#securing-ssh-access' -i /etc/ssh/sshd_config
|
||||
|
||||
# https://bugs.launchpad.net/ubuntu/+source/base-files/+bug/1701068
|
||||
echo "==> Disabling motd news"
|
||||
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/motd-news
|
||||
|
||||
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
|
||||
systemctl stop bind9 || true
|
||||
systemctl disable bind9 || true
|
||||
|
||||
@@ -2,57 +2,60 @@
|
||||
|
||||
'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'),
|
||||
constants = require('./src/constants.js'),
|
||||
dockerProxy = require('./src/dockerproxy.js'),
|
||||
fs = require('fs'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
server = require('./src/server.js');
|
||||
paths = require('./src/paths.js'),
|
||||
server = require('./src/server.js'),
|
||||
util = require('util');
|
||||
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
console.log(` Cloudron ${constants.VERSION} `);
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
const NOOP_CALLBACK = function () { };
|
||||
|
||||
function setupLogging(callback) {
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
fs.open(paths.BOX_LOG_FILE, 'a', function (error, fd) {
|
||||
if (error) return callback(error);
|
||||
|
||||
require('debug').log = function (...args) {
|
||||
fs.appendFileSync(fd, util.format(...args) + '\n');
|
||||
};
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
async.series([
|
||||
setupLogging,
|
||||
server.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);
|
||||
|
||||
const debug = require('debug')('box:box'); // require this here so that logging handler is already setup
|
||||
|
||||
process.on('SIGINT', function () {
|
||||
debug('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 () {
|
||||
debug('Received SIGTERM. Shutting down.');
|
||||
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 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) {
|
||||
|
||||
@@ -9,7 +9,7 @@ exports.up = function(db, callback) {
|
||||
if (!mailbox.membersJson) return iteratorDone();
|
||||
|
||||
let members = JSON.parse(mailbox.membersJson);
|
||||
members = members.map((m) => m.indexOf('@') === -1 ? `${m}@${mailbox.domain}` : m); // only because we don't do things in a xction
|
||||
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);
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
+32
-23
@@ -21,19 +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(
|
||||
@@ -52,18 +56,9 @@ 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,
|
||||
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),
|
||||
@@ -78,19 +73,24 @@ CREATE TABLE IF NOT EXISTS apps(
|
||||
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
|
||||
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,
|
||||
bindsJson TEXT, // bind mounts
|
||||
servicesConfigJson TEXT, // app services configuration
|
||||
|
||||
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
|
||||
FOREIGN KEY(taskId) REFERENCES tasks(id),
|
||||
PRIMARY KEY(id));
|
||||
|
||||
@@ -102,13 +102,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,
|
||||
@@ -130,8 +123,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 */
|
||||
@@ -155,7 +150,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))
|
||||
|
||||
@@ -188,12 +182,15 @@ 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 */
|
||||
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(
|
||||
@@ -225,6 +222,18 @@ 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)
|
||||
);
|
||||
|
||||
Generated
+1426
-1769
File diff suppressed because it is too large
Load Diff
+30
-42
@@ -17,80 +17,68 @@
|
||||
"@google-cloud/dns": "^1.1.0",
|
||||
"@google-cloud/storage": "^2.5.0",
|
||||
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
|
||||
"async": "^2.6.2",
|
||||
"aws-sdk": "^2.476.0",
|
||||
"async": "^2.6.3",
|
||||
"aws-sdk": "^2.685.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"cloudron-manifestformat": "^3.0.0",
|
||||
"cloudron-manifestformat": "^5.5.0",
|
||||
"connect": "^3.7.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^1.2.1",
|
||||
"connect-lastmile": "^2.0.0",
|
||||
"connect-timeout": "^1.9.0",
|
||||
"cookie-parser": "^1.4.4",
|
||||
"cookie-session": "^1.3.3",
|
||||
"cron": "^1.7.1",
|
||||
"csurf": "^1.10.0",
|
||||
"db-migrate": "^0.11.6",
|
||||
"cookie-session": "^1.4.0",
|
||||
"cron": "^1.8.2",
|
||||
"db-migrate": "^0.11.11",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^4.1.1",
|
||||
"dockerode": "^2.5.8",
|
||||
"ejs": "^2.6.1",
|
||||
"ejs-cli": "^2.0.1",
|
||||
"ejs-cli": "^2.2.0",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.16.2",
|
||||
"js-yaml": "^3.13.1",
|
||||
"js-yaml": "^3.14.0",
|
||||
"json": "^9.0.6",
|
||||
"ldapjs": "^1.0.2",
|
||||
"lodash": "^4.17.11",
|
||||
"lodash": "^4.17.15",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mime": "^2.4.4",
|
||||
"moment-timezone": "^0.5.25",
|
||||
"morgan": "^1.9.1",
|
||||
"mime": "^2.4.6",
|
||||
"moment": "^2.26.0",
|
||||
"moment-timezone": "^0.5.31",
|
||||
"morgan": "^1.10.0",
|
||||
"multiparty": "^4.2.1",
|
||||
"mysql": "^2.17.1",
|
||||
"nodemailer": "^6.2.1",
|
||||
"mysql": "^2.18.1",
|
||||
"nodemailer": "^6.4.6",
|
||||
"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.3.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"qrcode": "^1.3.3",
|
||||
"readdirp": "^3.0.2",
|
||||
"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",
|
||||
"safetydance": "^1.1.1",
|
||||
"semver": "^6.1.1",
|
||||
"session-file-store": "^1.3.1",
|
||||
"showdown": "^1.9.0",
|
||||
"showdown": "^1.9.1",
|
||||
"speakeasy": "^2.0.0",
|
||||
"split": "^1.0.1",
|
||||
"superagent": "^5.0.9",
|
||||
"supererror": "^0.7.2",
|
||||
"superagent": "^5.2.2",
|
||||
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
|
||||
"tar-stream": "^2.1.0",
|
||||
"tar-stream": "^2.1.2",
|
||||
"tldjs": "^2.3.1",
|
||||
"underscore": "^1.9.1",
|
||||
"uuid": "^3.3.2",
|
||||
"valid-url": "^1.0.9",
|
||||
"underscore": "^1.10.2",
|
||||
"uuid": "^3.4.0",
|
||||
"validator": "^11.0.0",
|
||||
"ws": "^7.0.0",
|
||||
"xml2js": "^0.4.19"
|
||||
"ws": "^7.3.0",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"expect.js": "*",
|
||||
"hock": "^1.3.3",
|
||||
"js2xmlparser": "^4.0.0",
|
||||
"hock": "^1.4.1",
|
||||
"js2xmlparser": "^4.0.1",
|
||||
"mocha": "^6.1.4",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^10.0.6",
|
||||
"node-sass": "^4.12.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"recursive-readdir": "^2.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -22,7 +22,7 @@ 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 boxdata/profileicons boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
|
||||
mkdir -p platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
|
||||
|
||||
# put cert
|
||||
|
||||
+10
-69
@@ -41,16 +41,14 @@ 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
|
||||
@@ -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;;
|
||||
@@ -91,47 +88,6 @@ 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
|
||||
readonly AVAILABLE_PROVIDERS="azure, caas, cloudscale, contabo, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, interox, lightsail, linode, netcup, ovh, rosehosting, scaleway, skysilk, time4vps, upcloud, vultr or generic"
|
||||
if [[ -z "${provider}" ]]; then
|
||||
echo "--provider is required ($AVAILABLE_PROVIDERS)"
|
||||
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}" != "gce" && \
|
||||
"${provider}" != "hetzner" && \
|
||||
"${provider}" != "interox" && \
|
||||
"${provider}" != "interox-image" && \
|
||||
"${provider}" != "lightsail" && \
|
||||
"${provider}" != "linode" && \
|
||||
"${provider}" != "linode-stackscript" && \
|
||||
"${provider}" != "netcup" && \
|
||||
"${provider}" != "netcup-image" && \
|
||||
"${provider}" != "ovh" && \
|
||||
"${provider}" != "rosehosting" && \
|
||||
"${provider}" != "scaleway" && \
|
||||
"${provider}" != "skysilk" && \
|
||||
"${provider}" != "skysilk-image" && \
|
||||
"${provider}" != "time4vps" && \
|
||||
"${provider}" != "time4vps-image" && \
|
||||
"${provider}" != "upcloud" && \
|
||||
"${provider}" != "upcloud-image" && \
|
||||
"${provider}" != "vultr" && \
|
||||
"${provider}" != "generic" \
|
||||
]]; then
|
||||
echo "--provider must be one of: $AVAILABLE_PROVIDERS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "##############################################"
|
||||
echo " Cloudron Setup (${requestedVersion:-latest})"
|
||||
@@ -150,12 +106,6 @@ 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}"
|
||||
@@ -195,49 +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
|
||||
# this file is used >= 4.2
|
||||
echo "${provider}" > /etc/cloudron/PROVIDER
|
||||
|
||||
# this file is unused <= 4.2 and exists to make legacy installations work. the start script will remove this file anyway
|
||||
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
|
||||
{
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"provider": "${provider}"
|
||||
}
|
||||
CONF_END
|
||||
|
||||
[[ -n "${license}" ]] && echo -n "$license" > /etc/cloudron/LICENSE
|
||||
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# only needed for >= 4.2
|
||||
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('api_server_origin', '${apiServerOrigin}');" 2>/dev/null
|
||||
mysql -uroot -ppassword -e "REPLACE INTO box.settings (name, value) VALUES ('web_server_origin', '${webServerOrigin}');" 2>/dev/null
|
||||
|
||||
echo -n "=> Waiting for cloudron to be ready (this takes some time) ..."
|
||||
while true; do
|
||||
echo -n "."
|
||||
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}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,7 +13,7 @@ HELP_MESSAGE="
|
||||
This script collects diagnostic information to help debug server related issues
|
||||
|
||||
Options:
|
||||
--admin-login Login as administrator
|
||||
--owner-login Login as owner
|
||||
--enable-ssh Enable SSH access for the Cloudron support team
|
||||
--help Show this message
|
||||
"
|
||||
@@ -26,7 +26,7 @@ fi
|
||||
|
||||
enableSSH="false"
|
||||
|
||||
args=$(getopt -o "" -l "help,enable-ssh,admin-login" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "help,enable-ssh,admin-login,owner-login" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
@@ -34,10 +34,15 @@ while true; do
|
||||
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
|
||||
--enable-ssh) enableSSH="true"; shift;;
|
||||
--admin-login)
|
||||
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE admin=1 LIMIT 1" 2>/dev/null)
|
||||
# fall through
|
||||
;&
|
||||
--owner-login)
|
||||
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE role='owner' LIMIT 1" 2>/dev/null)
|
||||
admin_password=$(pwgen -1s 12)
|
||||
printf '{"%s":"%s"}\n' "${admin_username}" "${admin_password}" > /tmp/cloudron_ghost.json
|
||||
echo "Login as ${admin_username} / ${admin_password} . Remove /tmp/cloudron_ghost.json when done."
|
||||
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} . Remove ${ghost_file} when done."
|
||||
exit 0
|
||||
;;
|
||||
--) break;;
|
||||
@@ -80,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
|
||||
@@ -107,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/PROVIDER); then
|
||||
if [[ -d /home/ubuntu ]]; then
|
||||
ssh_user="ubuntu"
|
||||
keys_file="/home/ubuntu/.ssh/authorized_keys"
|
||||
else
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+27
-17
@@ -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,6 +23,8 @@ 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
|
||||
@@ -56,13 +57,22 @@ 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-1~${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
|
||||
|
||||
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 +119,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"
|
||||
|
||||
+32
-7
@@ -20,6 +20,11 @@ readonly ubuntu_version=$(lsb_release -rs)
|
||||
|
||||
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
|
||||
|
||||
# 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
|
||||
systemctl enable apparmor
|
||||
@@ -47,13 +52,16 @@ 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}/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
|
||||
|
||||
# ensure backups folder exists and is writeable
|
||||
mkdir -p /var/backups
|
||||
@@ -77,23 +85,28 @@ 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
|
||||
|
||||
echo "==> Adding systemd services"
|
||||
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
|
||||
systemctl disable cloudron.target || true
|
||||
rm -f /etc/systemd/system/cloudron.target
|
||||
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable unbound
|
||||
systemctl enable cloudron-syslog
|
||||
systemctl enable cloudron.target
|
||||
systemctl enable box
|
||||
systemctl enable cloudron-firewall
|
||||
|
||||
# update firewall rules
|
||||
@@ -113,7 +126,7 @@ 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"
|
||||
systemctl restart collectd
|
||||
@@ -142,8 +155,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,9 +188,11 @@ readonly mysql_root_password="password"
|
||||
mysqladmin -u root -ppassword password password # reset default root password
|
||||
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}"
|
||||
if ! BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up; then
|
||||
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
|
||||
@@ -189,6 +211,9 @@ fi
|
||||
echo "==> Cleaning up stale redis directories"
|
||||
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} +
|
||||
|
||||
echo "==> Cleaning up old logs"
|
||||
rm -f /home/yellowtent/platformdata/logs/*/*.log.* || true
|
||||
|
||||
echo "==> Changing ownership"
|
||||
# be careful of what is chown'ed here. subdirs like mysql,redis etc are owned by the containers and will stop working if perms change
|
||||
chown -R "${USER}" /etc/cloudron
|
||||
@@ -204,7 +229,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
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@ 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
|
||||
|
||||
# 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
|
||||
iptables -t filter -A CLOUDRON -p udp --sport 53 -j ACCEPT
|
||||
|
||||
@@ -3,10 +3,17 @@
|
||||
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 '\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"
|
||||
else
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,10 +3,12 @@ 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):
|
||||
cmd = 'timeout 1800 du -Dsb "{}"'.format(pathinfo['dir'])
|
||||
# -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'])
|
||||
|
||||
@@ -26,6 +28,7 @@ def parseSize(size):
|
||||
|
||||
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')
|
||||
|
||||
@@ -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)'
|
||||
@@ -9,6 +9,8 @@
|
||||
/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
|
||||
@@ -16,6 +18,7 @@
|
||||
missingok
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
# this truncates the original log file and not the rotated one
|
||||
copytruncate
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -50,3 +50,12 @@ 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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
server:
|
||||
interface: 0.0.0.0
|
||||
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
|
||||
|
||||
+8
-119
@@ -1,140 +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'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
debug = require('debug')('box:accesscontrol'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
users = require('./users.js'),
|
||||
_ = 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 BoxError(BoxError.BAD_FIELD, 'Empty scope not allowed', { field: 'scope' });
|
||||
|
||||
// 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 BoxError(BoxError.BAD_FIELD, 'Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '), { field: 'scope' });
|
||||
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 BoxError(BoxError.NOT_FOUND, '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 === BoxError.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 === BoxError.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);
|
||||
|
||||
if (!user.active) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
if (!user.active) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
|
||||
scopesForUser(user, function (error, userScopes) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
|
||||
callback(null, user, { authorizedScopes }); // ends up in req.authInfo
|
||||
});
|
||||
callback(null, user);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+521
-261
File diff suppressed because it is too large
Load Diff
+21
-10
@@ -40,10 +40,10 @@ var assert = require('assert'),
|
||||
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain',
|
||||
'apps.accessRestrictionJson', 'apps.memoryLimit',
|
||||
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson',
|
||||
'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
|
||||
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson', 'apps.bindsJson',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
|
||||
'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(',');
|
||||
@@ -94,6 +94,14 @@ function postProcess(result) {
|
||||
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;
|
||||
|
||||
assert(result.bindsJson === null || typeof result.bindsJson === 'string');
|
||||
result.binds = safe.JSON.parse(result.bindsJson) || {};
|
||||
delete result.bindsJson;
|
||||
|
||||
result.alternateDomains = result.alternateDomains || [];
|
||||
result.alternateDomains.forEach(function (d) {
|
||||
delete d.appId;
|
||||
@@ -242,6 +250,7 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
|
||||
const accessRestriction = data.accessRestriction || null;
|
||||
const accessRestrictionJson = JSON.stringify(accessRestriction);
|
||||
const memoryLimit = data.memoryLimit || 0;
|
||||
const cpuShares = data.cpuShares || 512;
|
||||
const installationState = data.installationState;
|
||||
const runState = data.runState;
|
||||
const sso = 'sso' in data ? data.sso : null;
|
||||
@@ -250,16 +259,17 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
|
||||
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, runState, accessRestrictionJson, memoryLimit, '
|
||||
+ 'sso, debugModeJson, mailboxName, label, tagsJson, reverseProxyConfigJson) '
|
||||
+ ' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, runState, accessRestrictionJson, memoryLimit,
|
||||
sso, debugModeJson, mailboxName, label, tagsJson, reverseProxyConfigJson ]
|
||||
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({
|
||||
@@ -347,12 +357,13 @@ 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 apps WHERE id = ?', args: [ id ] }
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (results[3].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
if (results[4].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -424,7 +435,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
|
||||
var fields = [ ], values = [ ];
|
||||
for (var p in app) {
|
||||
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig') {
|
||||
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig' || p === 'binds') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
|
||||
|
||||
+7
-13
@@ -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) {
|
||||
@@ -73,7 +73,6 @@ function checkAppHealth(app, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (app.installationState !== apps.ISTATE_INSTALLED || app.runState !== apps.RSTATE_RUNNING) {
|
||||
debugApp(app, 'skipped. istate:%s rstate:%s', app.installationState, app.runState);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
@@ -103,10 +102,8 @@ function checkAppHealth(app, callback) {
|
||||
.timeout(HEALTHCHECK_INTERVAL)
|
||||
.end(function (error, res) {
|
||||
if (error && !error.response) {
|
||||
debugApp(app, 'not alive (network error): %s', error.message);
|
||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
|
||||
debugApp(app, 'not alive : %s', error || res.status);
|
||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||
} else {
|
||||
setHealth(app, apps.HEALTH_HEALTHY, callback);
|
||||
@@ -180,17 +177,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 === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; })
|
||||
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
|
||||
|
||||
debug('apps alive: [%s]', alive);
|
||||
debug(`app health: ${alive.length} alive / ${allApps.length - alive.length} dead.` + (error ? ` ${error.reason}` : ''));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -205,7 +199,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();
|
||||
});
|
||||
|
||||
+932
-805
File diff suppressed because it is too large
Load Diff
+140
-146
@@ -1,21 +1,24 @@
|
||||
'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,
|
||||
|
||||
@@ -27,21 +30,47 @@ var apps = require('./apps.js'),
|
||||
async = require('async'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
custom = require('./custom.js'),
|
||||
debug = require('debug')('box:appstore'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
groups = require('./groups.js'),
|
||||
mail = require('./mail.js'),
|
||||
os = require('os'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
// 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
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
function getFeatures() {
|
||||
return gFeatures;
|
||||
}
|
||||
|
||||
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');
|
||||
@@ -89,13 +118,32 @@ function registerUser(email, password, callback) {
|
||||
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 BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 409) return callback(new BoxError(BoxError.ALREADY_EXISTS));
|
||||
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');
|
||||
|
||||
@@ -110,7 +158,11 @@ function getSubscription(callback) {
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -174,112 +226,8 @@ function unpurchaseApp(appId, data, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function sendAliveStatus(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
let allSettings, allDomains, mailDomains, loginEvents, userCount, groupCount;
|
||||
|
||||
async.series([
|
||||
function (callback) {
|
||||
settings.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
allSettings = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
domains.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
allDomains = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
mail.getDomains(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
mailDomains = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
eventlog.getAllPaged([ eventlog.ACTION_USER_LOGIN ], null, 1, 1, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
loginEvents = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
users.count(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
userCount = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
groups.count(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
groupCount = 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; })))
|
||||
},
|
||||
userCount: userCount,
|
||||
groupCount: groupCount,
|
||||
appAutoupdatePattern: allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY],
|
||||
boxAutoupdatePattern: allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY],
|
||||
timeZone: allSettings[settings.TIME_ZONE_KEY],
|
||||
sysinfoProvider: allSettings[settings.SYSINFO_CONFIG_KEY].provider
|
||||
};
|
||||
|
||||
var data = {
|
||||
version: constants.VERSION,
|
||||
adminFqdn: settings.adminFqdn(),
|
||||
provider: settings.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 = `${settings.apiServerOrigin()}/api/v1/alive`;
|
||||
superagent.post(url).send(data).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
|
||||
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
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('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) {
|
||||
@@ -287,7 +235,13 @@ function getBoxUpdate(callback) {
|
||||
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
|
||||
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION }).timeout(10 * 1000).end(function (error, result) {
|
||||
const query = {
|
||||
accessToken: token,
|
||||
boxVersion: constants.VERSION,
|
||||
automatic: options.automatic
|
||||
};
|
||||
|
||||
superagent.get(url).query(query).timeout(10 * 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));
|
||||
@@ -313,16 +267,24 @@ function getBoxUpdate(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
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 = `${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: constants.VERSION, appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
|
||||
superagent.get(url).query(query).timeout(10 * 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));
|
||||
@@ -340,7 +302,9 @@ function getAppUpdate(app, callback) {
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -354,7 +318,7 @@ function registerCloudron(data, callback) {
|
||||
|
||||
superagent.post(url).send(data).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, `Unable to register cloudron: ${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 BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
|
||||
@@ -375,15 +339,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 BoxError(BoxError.CONFLICT));
|
||||
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}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -398,7 +377,7 @@ function registerWithLoginCredentials(options, callback) {
|
||||
}
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (token) return callback(new BoxError(BoxError.CONFLICT));
|
||||
if (token) return callback(new BoxError(BoxError.CONFLICT, 'Cloudron is already registered'));
|
||||
|
||||
maybeSignup(function (error) {
|
||||
if (error) return callback(error);
|
||||
@@ -406,19 +385,20 @@ function registerWithLoginCredentials(options, callback) {
|
||||
login(options.email, options.password, options.totpToken || '', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
registerCloudron({ domain: settings.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) {
|
||||
@@ -430,12 +410,12 @@ function createTicket(info, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
collectAppInfoIfNeeded(function (error, result) {
|
||||
if (error) console.error('Unable to get app info', error);
|
||||
if (error) return callback(error);
|
||||
if (result) info.app = result;
|
||||
|
||||
let url = settings.apiServerOrigin() + '/api/v1/ticket';
|
||||
|
||||
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 BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
@@ -443,7 +423,9 @@ function createTicket(info, callback) {
|
||||
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)));
|
||||
|
||||
callback(null);
|
||||
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
|
||||
|
||||
callback(null, { message: `An email for sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -466,7 +448,13 @@ function getApps(callback) {
|
||||
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)));
|
||||
|
||||
callback(null, result.body.apps);
|
||||
settings.getAppstoreListingConfig(function (error, listingConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const filteredApps = result.body.apps.filter(app => isAppAllowed(app.id, listingConfig));
|
||||
|
||||
callback(null, filteredApps);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -477,20 +465,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 = `${settings.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 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)));
|
||||
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(10 * 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+139
-146
@@ -17,8 +17,6 @@ exports = module.exports = {
|
||||
_waitForDnsPropagation: waitForDnsPropagation
|
||||
};
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
@@ -27,6 +25,7 @@ var addons = require('./addons.js'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
backups = require('./backups.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
collectd = require('./collectd.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:apptask'),
|
||||
df = require('@sindresorhus/df'),
|
||||
@@ -36,7 +35,6 @@ var addons = require('./addons.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
mkdirp = require('mkdirp'),
|
||||
net = require('net'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
@@ -45,14 +43,14 @@ var addons = require('./addons.js'),
|
||||
rimraf = require('rimraf'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
sftp = require('./sftp.js'),
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }),
|
||||
CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh'),
|
||||
const COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd/app.ejs', { encoding: 'utf8' }),
|
||||
MV_VOLUME_CMD = path.join(__dirname, 'scripts/mvvolume.sh'),
|
||||
LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }),
|
||||
CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh');
|
||||
@@ -64,11 +62,13 @@ function debugApp(app) {
|
||||
}
|
||||
|
||||
function makeTaskError(error, app) {
|
||||
let boxError = error instanceof BoxError ? error : new BoxError(BoxError.UNKNOWN_ERROR, error.message); // until we port everything to BoxError
|
||||
assert.strictEqual(typeof error, 'object');
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
|
||||
// track a few variables which helps 'repair' restart the task (see also scheduleTask in apps.js)
|
||||
boxError.details.taskId = app.taskId;
|
||||
boxError.details.installationState = app.installationState;
|
||||
return boxError.toPlainObject();
|
||||
error.details.taskId = app.taskId;
|
||||
error.details.installationState = app.installationState;
|
||||
return error.toPlainObject();
|
||||
}
|
||||
|
||||
// updates the app object and the database
|
||||
@@ -140,7 +140,15 @@ function createContainer(app, callback) {
|
||||
docker.createContainer(app, function (error, container) {
|
||||
if (error) return callback(error);
|
||||
|
||||
updateApp(app, { containerId: container.id }, callback);
|
||||
updateApp(app, { containerId: container.id }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// re-generate configs that rely on container id
|
||||
async.series([
|
||||
addLogrotateConfig.bind(null, app),
|
||||
addCollectdProfile.bind(null, app)
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,11 +159,14 @@ function deleteContainers(app, options, callback) {
|
||||
|
||||
debugApp(app, 'deleting app containers (app, scheduler)');
|
||||
|
||||
docker.deleteContainers(app.id, options, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
updateApp(app, { containerId: null }, callback);
|
||||
});
|
||||
async.series([
|
||||
// remove configs that rely on container id
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
docker.stopContainers.bind(null, app.id),
|
||||
docker.deleteContainers.bind(null, app.id, options),
|
||||
updateApp.bind(null, app, { containerId: null })
|
||||
], callback);
|
||||
}
|
||||
|
||||
function createAppDir(app, callback) {
|
||||
@@ -163,7 +174,7 @@ function createAppDir(app, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const appDir = path.join(paths.APPS_DATA_DIR, app.id);
|
||||
mkdirp(appDir, function (error) {
|
||||
fs.mkdir(appDir, { recursive: true }, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error creating directory: ${error.message}`, { appDir }));
|
||||
|
||||
callback(null);
|
||||
@@ -216,29 +227,14 @@ function addCollectdProfile(app, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var collectdConf = ejs.render(COLLECTD_CONFIG_EJS, { appId: app.id, containerId: app.containerId, appDataDir: apps.getDataDir(app, app.dataDir) });
|
||||
fs.writeFile(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), collectdConf, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error writing collectd config: ${error.message}`));
|
||||
|
||||
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', app.id ], {}, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.COLLECTD_ERROR, 'Could not add collectd config'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
collectd.addProfile(app.id, collectdConf, callback);
|
||||
}
|
||||
|
||||
function removeCollectdProfile(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, app.id + '.conf'), function (error) {
|
||||
if (error && error.code !== 'ENOENT') debugApp(app, 'Error removing collectd profile', error);
|
||||
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', app.id ], {}, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.COLLECTD_ERROR, 'Could not remove collectd config'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
collectd.removeProfile(app.id, callback);
|
||||
}
|
||||
|
||||
function addLogrotateConfig(app, callback) {
|
||||
@@ -433,12 +429,12 @@ function waitForDnsPropagation(app, callback) {
|
||||
sysinfo.getServerIp(function (error, ip) {
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Error getting public IP: ${error.message}`));
|
||||
|
||||
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
|
||||
domains.waitForDnsRecord(app.location, app.domain, 'A', ip, { times: 240 }, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: app.location, domain: app.domain }));
|
||||
|
||||
// now wait for alternateDomains, if any
|
||||
async.eachSeries(app.alternateDomains, function (domain, iteratorCallback) {
|
||||
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { interval: 5000, times: 240 }, function (error) {
|
||||
domains.waitForDnsRecord(domain.subdomain, domain.domain, 'A', ip, { times: 240 }, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.DNS_ERROR, `DNS Record is not synced yet: ${error.message}`, { ip: ip, subdomain: domain.subdomain, domain: domain.domain }));
|
||||
|
||||
iteratorCallback();
|
||||
@@ -448,16 +444,18 @@ function waitForDnsPropagation(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function moveDataDir(app, sourceDir, callback) {
|
||||
function moveDataDir(app, targetDir, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(sourceDir === null || typeof sourceDir === 'string');
|
||||
assert(targetDir === null || typeof targetDir === 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let resolvedSourceDir = apps.getDataDir(app, sourceDir);
|
||||
let resolvedTargetDir = apps.getDataDir(app, app.dataDir);
|
||||
let resolvedSourceDir = apps.getDataDir(app, app.dataDir);
|
||||
let resolvedTargetDir = apps.getDataDir(app, targetDir);
|
||||
|
||||
debug(`moveDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`);
|
||||
|
||||
if (resolvedSourceDir === resolvedTargetDir) return callback();
|
||||
|
||||
shell.sudo('moveDataDir', [ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {}, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Error migrating data directory: ${error.message}`));
|
||||
|
||||
@@ -492,17 +490,6 @@ function startApp(app, callback){
|
||||
docker.startContainer(app.id, callback);
|
||||
}
|
||||
|
||||
// Ordering is based on the following rationale:
|
||||
// - configure nginx, icon, oauth
|
||||
// - register subdomain.
|
||||
// at this point, the user can visit the site and the above nginx config can show some install screen.
|
||||
// the icon can be displayed in this nginx page and oauth proxy means the page can be protected
|
||||
// - download image
|
||||
// - setup volumes
|
||||
// - setup addons (requires the above volume)
|
||||
// - setup the container (requires image, volumes, addons)
|
||||
// - setup collectd (requires container id)
|
||||
// restore is also handled here since restore is just an install with some oldConfig to clean up
|
||||
function install(app, args, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof args, 'object');
|
||||
@@ -511,6 +498,7 @@ function install(app, args, progressCallback, callback) {
|
||||
|
||||
const restoreConfig = args.restoreConfig; // has to be set when restoring
|
||||
const overwriteDns = args.overwriteDns;
|
||||
const oldManifest = args.oldManifest;
|
||||
|
||||
async.series([
|
||||
// this protects against the theoretical possibility of an app being marked for install/restore from
|
||||
@@ -520,29 +508,29 @@ function install(app, args, progressCallback, callback) {
|
||||
// teardown for re-installs
|
||||
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
|
||||
unconfigureReverseProxy.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
docker.stopContainers.bind(null, app.id),
|
||||
deleteContainers.bind(null, app, { managedOnly: true }),
|
||||
function teardownAddons(next) {
|
||||
// when restoring, app does not require these addons anymore. remove carefully to preserve the db passwords
|
||||
let addonsToRemove;
|
||||
if (restoreConfig && restoreConfig.oldManifest) { // oldManifest is null for clone
|
||||
addonsToRemove = _.omit(restoreConfig.oldManifest.addons, Object.keys(app.manifest.addons));
|
||||
if (oldManifest) {
|
||||
addonsToRemove = _.omit(oldManifest.addons, Object.keys(app.manifest.addons));
|
||||
} else {
|
||||
addonsToRemove = app.manifest.addons;
|
||||
}
|
||||
|
||||
addons.teardownAddons(app, addonsToRemove, next);
|
||||
},
|
||||
deleteAppDir.bind(null, app, { removeDirectory: false }), // do not remove any symlinked appdata dir
|
||||
|
||||
function deleteAppDirIfNeeded(done) {
|
||||
if (restoreConfig && !restoreConfig.backupId) return done(); // in-place import should not delete data dir
|
||||
|
||||
deleteAppDir(app, { removeDirectory: false }, done); // do not remove any symlinked appdata dir
|
||||
},
|
||||
|
||||
function deleteImageIfChanged(done) {
|
||||
if (!restoreConfig || !restoreConfig.oldManifest) return done();
|
||||
if (!oldManifest || oldManifest.dockerImage === app.manifest.dockerImage) return done();
|
||||
|
||||
if (restoreConfig.oldManifest.dockerImage === app.manifest.dockerImage) return done();
|
||||
|
||||
docker.deleteImage(restoreConfig.oldManifest, done);
|
||||
docker.deleteImage(oldManifest, done);
|
||||
},
|
||||
|
||||
reserveHttpPort.bind(null, app),
|
||||
@@ -565,14 +553,22 @@ function install(app, args, progressCallback, callback) {
|
||||
progressCallback.bind(null, { percent: 60, message: 'Setting up addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
], next);
|
||||
} else if (!restoreConfig.backupId) { // in-place import
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 60, message: 'Importing addons in-place' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
addons.clearAddons.bind(null, app, _.omit(app.manifest.addons, 'localstorage')),
|
||||
addons.restoreAddons.bind(null, app, app.manifest.addons),
|
||||
], next);
|
||||
} else {
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 65, message: 'Download backup and restoring addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
addons.clearAddons.bind(null, app, app.manifest.addons),
|
||||
backups.restoreApp.bind(null, app, app.manifest.addons, restoreConfig, (progress) => {
|
||||
progressCallback({ percent: 65, message: `Restore - ${progress.message}` });
|
||||
})
|
||||
backups.downloadApp.bind(null, app, restoreConfig, (progress) => {
|
||||
progressCallback({ percent: 65, message: progress.message });
|
||||
}),
|
||||
addons.restoreAddons.bind(null, app, app.manifest.addons)
|
||||
], next);
|
||||
}
|
||||
},
|
||||
@@ -580,14 +576,11 @@ function install(app, args, progressCallback, callback) {
|
||||
progressCallback.bind(null, { percent: 70, message: 'Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 75, message: 'Setting up logrotate config' }),
|
||||
addLogrotateConfig.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 80, message: 'Setting up collectd profile' }),
|
||||
addCollectdProfile.bind(null, app),
|
||||
|
||||
startApp.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 80, message: 'Configuring file manager' }),
|
||||
sftp.rebuild.bind(null, {}),
|
||||
|
||||
progressCallback.bind(null, { percent: 85, message: 'Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
|
||||
@@ -622,8 +615,8 @@ function backup(app, args, progressCallback, callback) {
|
||||
], function seriesDone(error) {
|
||||
if (error) {
|
||||
debugApp(app, 'error backing up app: %s', error);
|
||||
// return to installed state intentionally
|
||||
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: error.toPlainObject ? error.toPlainObject() : error.message }, callback.bind(null, error));
|
||||
// return to installed state intentionally. the error is stashed only in the task and not the app (the UI shows error state otherwise)
|
||||
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null }, callback.bind(null, makeTaskError(error, app)));
|
||||
}
|
||||
callback(null);
|
||||
});
|
||||
@@ -637,7 +630,6 @@ function create(app, args, progressCallback, callback) {
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
|
||||
docker.stopContainers.bind(null, app.id),
|
||||
deleteContainers.bind(null, app, { managedOnly: true }),
|
||||
|
||||
// FIXME: re-setup addons only because sendmail addon to re-inject env vars on mailboxName change
|
||||
@@ -673,7 +665,6 @@ function changeLocation(app, args, progressCallback, callback) {
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
|
||||
unconfigureReverseProxy.bind(null, app),
|
||||
docker.stopContainers.bind(null, app.id),
|
||||
deleteContainers.bind(null, app, { managedOnly: true }),
|
||||
function (next) {
|
||||
let obsoleteDomains = oldConfig.alternateDomains.filter(function (o) {
|
||||
@@ -709,7 +700,7 @@ function changeLocation(app, args, progressCallback, callback) {
|
||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
|
||||
], function seriesDone(error) {
|
||||
if (error) {
|
||||
debugApp(app, 'error reconfiguring : %s', error);
|
||||
debugApp(app, 'error changing location : %s', error);
|
||||
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
|
||||
}
|
||||
callback(null);
|
||||
@@ -722,12 +713,11 @@ function migrateDataDir(app, args, progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let oldDataDir = args.oldDataDir;
|
||||
assert(oldDataDir === null || typeof oldDataDir === 'string');
|
||||
let newDataDir = args.newDataDir;
|
||||
assert(newDataDir === null || typeof newDataDir === 'string');
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
|
||||
docker.stopContainers.bind(null, app.id),
|
||||
deleteContainers.bind(null, app, { managedOnly: true }),
|
||||
|
||||
progressCallback.bind(null, { percent: 45, message: 'Ensuring app data directory' }),
|
||||
@@ -735,72 +725,48 @@ function migrateDataDir(app, args, progressCallback, callback) {
|
||||
|
||||
// re-setup addons since this creates the localStorage volume
|
||||
progressCallback.bind(null, { percent: 50, message: 'Setting up addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
addons.setupAddons.bind(null, _.extend({}, app, { dataDir: newDataDir }), app.manifest.addons),
|
||||
|
||||
// migrate dataDir
|
||||
function (next) {
|
||||
const dataDirChanged = oldDataDir !== app.dataDir;
|
||||
progressCallback.bind(null, { percent: 60, message: 'Moving data dir' }),
|
||||
moveDataDir.bind(null, app, newDataDir),
|
||||
|
||||
if (!dataDirChanged) return next();
|
||||
|
||||
moveDataDir(app, oldDataDir, next);
|
||||
},
|
||||
|
||||
progressCallback.bind(null, { percent: 60, message: 'Creating container' }),
|
||||
progressCallback.bind(null, { percent: 90, message: 'Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
startApp.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
|
||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, dataDir: newDataDir })
|
||||
], function seriesDone(error) {
|
||||
if (error) {
|
||||
debugApp(app, 'error reconfiguring : %s', error);
|
||||
debugApp(app, 'error migrating data dir : %s', error);
|
||||
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
|
||||
}
|
||||
callback(null);
|
||||
|
||||
// We do this after the app has the new data commited to the database
|
||||
sftp.rebuild({}, function (error) {
|
||||
if (error) debug('migrateDataDir: failed to rebuild sftp addon:', error);
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// configure is called for an infra update and repair
|
||||
// configure is called for an infra update and repair to re-create container, reverseproxy config. it's all "local"
|
||||
function configure(app, args, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof args, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const oldConfig = args.oldConfig || null;
|
||||
const overwriteDns = args.overwriteDns;
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 10, message: 'Cleaning up old install' }),
|
||||
unconfigureReverseProxy.bind(null, app),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
docker.stopContainers.bind(null, app.id),
|
||||
deleteContainers.bind(null, app, { managedOnly: true }),
|
||||
function (next) {
|
||||
if (!oldConfig) return next();
|
||||
|
||||
let obsoleteDomains = oldConfig.alternateDomains.filter(function (o) {
|
||||
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
|
||||
});
|
||||
|
||||
if (oldConfig.fqdn !== app.fqdn) obsoleteDomains.push({ subdomain: oldConfig.location, domain: oldConfig.domain });
|
||||
|
||||
if (obsoleteDomains.length === 0) return next();
|
||||
|
||||
unregisterSubdomains(app, obsoleteDomains, next);
|
||||
},
|
||||
|
||||
reserveHttpPort.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 20, message: 'Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
|
||||
registerSubdomains.bind(null, app, overwriteDns),
|
||||
|
||||
progressCallback.bind(null, { percent: 40, message: 'Downloading image' }),
|
||||
downloadImage.bind(null, app.manifest),
|
||||
|
||||
@@ -814,16 +780,10 @@ function configure(app, args, progressCallback, callback) {
|
||||
progressCallback.bind(null, { percent: 60, message: 'Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 65, message: 'Setting up logrotate config' }),
|
||||
addLogrotateConfig.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 70, message: 'Add collectd profile' }),
|
||||
addCollectdProfile.bind(null, app),
|
||||
|
||||
startApp.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 80, message: 'Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
progressCallback.bind(null, { percent: 80, message: 'Configuring file manager' }),
|
||||
sftp.rebuild.bind(null, {}),
|
||||
|
||||
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
|
||||
configureReverseProxy.bind(null, app),
|
||||
@@ -835,7 +795,8 @@ function configure(app, args, progressCallback, callback) {
|
||||
debugApp(app, 'error reconfiguring : %s', error);
|
||||
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
|
||||
}
|
||||
callback(null);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -856,7 +817,7 @@ function update(app, args, progressCallback, callback) {
|
||||
async.series([
|
||||
// this protects against the theoretical possibility of an app being marked for update from
|
||||
// a previous version of box code
|
||||
progressCallback.bind(null, { percent: 0, message: 'Verify manifest' }),
|
||||
progressCallback.bind(null, { percent: 5, message: 'Verify manifest' }),
|
||||
verifyManifest.bind(null, updateConfig.manifest),
|
||||
|
||||
function (next) {
|
||||
@@ -882,7 +843,6 @@ function update(app, args, progressCallback, callback) {
|
||||
// note: we cleanup first and then backup. this is done so that the app is not running should backup fail
|
||||
// we cannot easily 'recover' from backup failures because we have to revert manfest and portBindings
|
||||
progressCallback.bind(null, { percent: 35, message: 'Cleaning up old install' }),
|
||||
docker.stopContainers.bind(null, app.id),
|
||||
deleteContainers.bind(null, app, { managedOnly: true }),
|
||||
function deleteImageIfChanged(done) {
|
||||
if (app.manifest.dockerImage === updateConfig.manifest.dockerImage) return done();
|
||||
@@ -903,7 +863,7 @@ function update(app, args, progressCallback, callback) {
|
||||
if (newTcpPorts[portName] || newUdpPorts[portName]) return callback(null); // port still in use
|
||||
|
||||
appdb.delPortBinding(currentPorts[portName], apps.PORT_TYPE_TCP, function (error) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) console.error('Portbinding does not exist in database.');
|
||||
if (error && error.reason === BoxError.NOT_FOUND) debug('update: portbinding does not exist in database', error);
|
||||
else if (error) return next(error);
|
||||
|
||||
// also delete from app object for further processing (the db is updated in the next step)
|
||||
@@ -919,14 +879,17 @@ function update(app, args, progressCallback, callback) {
|
||||
progressCallback.bind(null, { percent: 45, message: 'Downloading icon' }),
|
||||
downloadIcon.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 70, message: 'Updating addons' }),
|
||||
progressCallback.bind(null, { percent: 60, message: 'Updating addons' }),
|
||||
addons.setupAddons.bind(null, app, updateConfig.manifest.addons),
|
||||
|
||||
progressCallback.bind(null, { percent: 80, message: 'Creating container' }),
|
||||
progressCallback.bind(null, { percent: 70, message: 'Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
startApp.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 80, message: 'Configuring file manager' }),
|
||||
sftp.rebuild.bind(null, {}),
|
||||
|
||||
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null, updateTime: new Date() })
|
||||
], function seriesDone(error) {
|
||||
@@ -949,9 +912,16 @@ function start(app, args, progressCallback, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 20, message: 'Starting container' }),
|
||||
progressCallback.bind(null, { percent: 10, message: 'Starting app services' }),
|
||||
addons.startAppServices.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 35, message: 'Starting container' }),
|
||||
docker.startContainer.bind(null, app.id),
|
||||
|
||||
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
|
||||
progressCallback.bind(null, { percent: 60, message: 'Configuring reverse proxy' }),
|
||||
configureReverseProxy.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
|
||||
], function seriesDone(error) {
|
||||
@@ -973,6 +943,30 @@ function stop(app, args, progressCallback, callback) {
|
||||
progressCallback.bind(null, { percent: 20, message: 'Stopping container' }),
|
||||
docker.stopContainers.bind(null, app.id),
|
||||
|
||||
progressCallback.bind(null, { percent: 50, message: 'Stopping app services' }),
|
||||
addons.stopAppServices.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
|
||||
], function seriesDone(error) {
|
||||
if (error) {
|
||||
debugApp(app, 'error starting app: %s', error);
|
||||
return updateApp(app, { installationState: apps.ISTATE_ERROR, error: makeTaskError(error, app) }, callback.bind(null, error));
|
||||
}
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function restart(app, args, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof args, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 20, message: 'Restarting container' }),
|
||||
docker.restartContainer.bind(null, app.id),
|
||||
|
||||
progressCallback.bind(null, { percent: 100, message: 'Done' }),
|
||||
updateApp.bind(null, app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null })
|
||||
], function seriesDone(error) {
|
||||
@@ -991,36 +985,31 @@ function uninstall(app, args, progressCallback, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 0, message: 'Remove collectd profile' }),
|
||||
removeCollectdProfile.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 5, message: 'Remove logrotate config' }),
|
||||
removeLogrotateConfig.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 10, message: 'Stopping app' }),
|
||||
docker.stopContainers.bind(null, app.id),
|
||||
|
||||
progressCallback.bind(null, { percent: 20, message: 'Deleting container' }),
|
||||
unconfigureReverseProxy.bind(null, app),
|
||||
deleteContainers.bind(null, app, {}),
|
||||
|
||||
progressCallback.bind(null, { percent: 30, message: 'Teardown addons' }),
|
||||
addons.teardownAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
progressCallback.bind(null, { percent: 40, message: 'Deleting app data directory' }),
|
||||
progressCallback.bind(null, { percent: 40, message: 'Cleanup file manager' }),
|
||||
function (callback) {
|
||||
if (!app.dataDir) return callback();
|
||||
sftp.rebuild({ ignoredApps: [ app.id ] }, callback);
|
||||
},
|
||||
|
||||
progressCallback.bind(null, { percent: 50, message: 'Deleting app data directory' }),
|
||||
deleteAppDir.bind(null, app, { removeDirectory: true }),
|
||||
|
||||
progressCallback.bind(null, { percent: 50, message: 'Deleting image' }),
|
||||
progressCallback.bind(null, { percent: 60, message: 'Deleting image' }),
|
||||
docker.deleteImage.bind(null, app.manifest),
|
||||
|
||||
progressCallback.bind(null, { percent: 60, message: 'Unregistering domains' }),
|
||||
progressCallback.bind(null, { percent: 70, message: 'Unregistering domains' }),
|
||||
unregisterSubdomains.bind(null, app, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains)),
|
||||
|
||||
progressCallback.bind(null, { percent: 70, message: 'Cleanup icon' }),
|
||||
progressCallback.bind(null, { percent: 80, message: 'Cleanup icon' }),
|
||||
removeIcon.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 80, message: 'Unconfiguring reverse proxy' }),
|
||||
unconfigureReverseProxy.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 90, message: 'Cleanup logs' }),
|
||||
cleanupLogs.bind(null, app),
|
||||
|
||||
@@ -1072,9 +1061,13 @@ function run(appId, args, progressCallback, callback) {
|
||||
return start(app, args, progressCallback, callback);
|
||||
case apps.ISTATE_PENDING_STOP:
|
||||
return stop(app, args, progressCallback, callback);
|
||||
case apps.ISTATE_PENDING_RESTART:
|
||||
return restart(app, args, progressCallback, callback);
|
||||
case apps.ISTATE_INSTALLED: // can only happen when we have a bug in our code while testing/development
|
||||
return updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }, callback);
|
||||
default:
|
||||
debugApp(app, 'apptask launched with invalid command');
|
||||
return callback(new Error('Unknown install command in apptask:' + app.installationState));
|
||||
return callback(new BoxError(BoxError.INTERNAL_ERROR, 'Unknown install command in apptask:' + app.installationState));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
debug = require('debug')('box:apptaskmanager'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
@@ -42,12 +43,12 @@ function scheduleTask(appId, taskId, callback) {
|
||||
if (!gInitialized) initializeSync();
|
||||
|
||||
if (appId in gActiveTasks) {
|
||||
return callback(new Error(`Task for %s is already active: ${appId}`));
|
||||
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: 0, message: 'Waiting for other app tasks to complete' }, NOOP_CALLBACK);
|
||||
tasks.update(taskId, { percent: 1, message: 'Waiting for other app tasks to complete' }, NOOP_CALLBACK);
|
||||
gPendingTasks.push({ appId, taskId, callback });
|
||||
return;
|
||||
}
|
||||
@@ -56,7 +57,7 @@ function scheduleTask(appId, taskId, callback) {
|
||||
|
||||
if (lockError) {
|
||||
debug(`Could not get lock. ${lockError.message}, queueing task id ${taskId}`);
|
||||
tasks.update(taskId, { percent: 0, message: waitText(lockError.operation) }, NOOP_CALLBACK);
|
||||
tasks.update(taskId, { percent: 1, message: waitText(lockError.operation) }, NOOP_CALLBACK);
|
||||
gPendingTasks.push({ appId, taskId, callback });
|
||||
return;
|
||||
}
|
||||
@@ -67,7 +68,8 @@ function scheduleTask(appId, taskId, callback) {
|
||||
|
||||
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
|
||||
|
||||
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */ }, function (error, result) {
|
||||
// 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];
|
||||
|
||||
@@ -5,6 +5,7 @@ exports = module.exports = {
|
||||
HEALTH_MONITOR: { userId: null, username: 'healthmonitor' },
|
||||
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'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
database = require('./database.js');
|
||||
|
||||
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 BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Authcode 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 BoxError(BoxError.ALREADY_EXISTS));
|
||||
if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_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 BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Authcode 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 BoxError(BoxError.DATABASE_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 BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
+25
-30
@@ -6,27 +6,20 @@ var assert = require('assert'),
|
||||
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,15 +31,15 @@ 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) {
|
||||
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,7 +49,7 @@ 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');
|
||||
@@ -71,15 +64,14 @@ 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) {
|
||||
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); });
|
||||
@@ -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,8 +111,8 @@ 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 BoxError(BoxError.ALREADY_EXISTS));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
+550
-272
File diff suppressed because it is too large
Load Diff
+7
-1
@@ -33,6 +33,7 @@ function BoxError(reason, errorOrMessage, details) {
|
||||
}
|
||||
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';
|
||||
@@ -44,6 +45,7 @@ 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';
|
||||
@@ -58,9 +60,10 @@ 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.UNKNOWN_ERROR = 'Unknown Error'; // only used for porting
|
||||
|
||||
BoxError.prototype.toPlainObject = function () {
|
||||
return _.extend({}, { message: this.message, reason: this.reason }, this.details);
|
||||
@@ -75,6 +78,8 @@ BoxError.toHttpError = function (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:
|
||||
@@ -86,6 +91,7 @@ BoxError.toHttpError = function (error) {
|
||||
case BoxError.FS_ERROR:
|
||||
case BoxError.MAIL_ERROR:
|
||||
case BoxError.DOCKER_ERROR:
|
||||
case BoxError.ADDONS_ERROR:
|
||||
return new HttpError(424, error);
|
||||
case BoxError.DATABASE_ERROR:
|
||||
case BoxError.INTERNAL_ERROR:
|
||||
|
||||
+45
-42
@@ -9,8 +9,8 @@ var assert = require('assert'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('../paths.js'),
|
||||
request = require('request'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
@@ -41,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, '');
|
||||
@@ -96,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);
|
||||
|
||||
@@ -113,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');
|
||||
@@ -134,7 +138,7 @@ Acme2.prototype.updateContact = function (registrationUri, callback) {
|
||||
|
||||
const that = this;
|
||||
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when updating contact: ${error.message}`));
|
||||
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}`);
|
||||
@@ -154,7 +158,7 @@ Acme2.prototype.registerUser = function (callback) {
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when registering user: ${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 BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
@@ -180,7 +184,7 @@ 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 BoxError(BoxError.NETWORK_ERROR, `Network error when creating new order: ${error.message}`));
|
||||
if (error) return callback(error);
|
||||
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending signed request: ${result.body.detail}`));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
@@ -201,14 +205,15 @@ 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 BoxError(BoxError.NETWORK_ERROR, `Network error waiting for order: ${error.message}`)); // network error
|
||||
return retryCallback(error);
|
||||
}
|
||||
if (result.statusCode !== 200) {
|
||||
debug('waitForOrder: invalid response code getting uri %s', result.statusCode);
|
||||
@@ -253,7 +258,7 @@ Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
|
||||
};
|
||||
|
||||
this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when notifying challenge: ${error.message}`));
|
||||
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();
|
||||
@@ -265,21 +270,22 @@ 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 BoxError(BoxError.NETWORK_ERROR, `Network error waiting for challenge: ${error.message}`));
|
||||
return retryCallback(error);
|
||||
}
|
||||
if (result.statusCode !== 200) {
|
||||
debug('waitForChallenge: invalid response code getting uri %s', 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 BoxError(BoxError.TRY_AGAIN));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
@@ -305,7 +311,7 @@ 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 BoxError(BoxError.NETWORK_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 BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
@@ -326,7 +332,7 @@ 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');
|
||||
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));
|
||||
|
||||
@@ -348,20 +354,17 @@ Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
const that = this;
|
||||
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
debug('downloadCertificate: downloading certificate');
|
||||
|
||||
superagent.get(certUrl).buffer().parse(function (res, done) {
|
||||
var data = [ ];
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
}).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error when downloading certificate: ${error.message}`));
|
||||
if (result.statusCode === 202) return retryCallback(new BoxError(BoxError.TRY_AGAIN, 'Retry'));
|
||||
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 fullChainPem = result.text;
|
||||
const fullChainPem = result.body; // buffer
|
||||
|
||||
const certName = hostname.replace('*.', '_.');
|
||||
var certificateFile = path.join(outdir, `${certName}.cert`);
|
||||
@@ -449,7 +452,7 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
|
||||
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
|
||||
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { times: 200 }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, challenge);
|
||||
@@ -488,8 +491,8 @@ Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl,
|
||||
debug(`prepareChallenge: http: ${this.performHttpAuthorization}`);
|
||||
|
||||
const that = this;
|
||||
superagent.get(authorizationUrl).timeout(30 * 1000).end(function (error, response) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when preparing challenge: ${error.message}`));
|
||||
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;
|
||||
@@ -569,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(new BoxError(BoxError.NETWORK_ERROR, `Network error getting directory: ${error.message}`));
|
||||
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;
|
||||
|
||||
|
||||
@@ -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, '', '');
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate,
|
||||
|
||||
// testing
|
||||
_name: 'fallback'
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:cert/fallback.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, '', '');
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// -------------------------------------------
|
||||
// This file just describes the interface
|
||||
//
|
||||
// New backends can start from here
|
||||
// -------------------------------------------
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate
|
||||
};
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
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');
|
||||
|
||||
return callback(new Error('Not implemented'));
|
||||
}
|
||||
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getAll: getAll,
|
||||
getAllWithTokenCount: getAllWithTokenCount,
|
||||
getAllWithTokenCountByIdentifier: getAllWithTokenCountByIdentifier,
|
||||
add: add,
|
||||
del: del,
|
||||
getByAppId: getByAppId,
|
||||
getByAppIdAndType: getByAppIdAndType,
|
||||
|
||||
upsert: upsert,
|
||||
|
||||
delByAppId: delByAppId,
|
||||
delByAppIdAndType: delByAppIdAndType,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
database = require('./database.js');
|
||||
|
||||
var CLIENTS_FIELDS = [ 'id', 'appId', 'type', 'clientSecret', 'redirectURI', 'scope' ].join(',');
|
||||
var CLIENTS_FIELDS_PREFIXED = [ 'clients.id', 'clients.appId', 'clients.type', 'clients.clientSecret', 'clients.redirectURI', 'clients.scope' ].join(',');
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE 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, `Client not found: ${id}`));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients ORDER BY appId', function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function getAllWithTokenCount(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId GROUP BY clients.id', [], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function getAllWithTokenCountByIdentifier(identifier, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + CLIENTS_FIELDS_PREFIXED + ',COUNT(tokens.clientId) AS tokenCount FROM clients LEFT OUTER JOIN tokens ON clients.id=tokens.clientId WHERE tokens.identifier=? GROUP BY clients.id', [ identifier ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function getByAppId(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE appId = ? LIMIT 1', [ appId ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getByAppIdAndType(appId, type, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE appId = ? AND type = ? LIMIT 1', [ appId, type ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appId, type, clientSecret, redirectURI, scope, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof clientSecret, 'string');
|
||||
assert.strictEqual(typeof redirectURI, 'string');
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [ id, appId, type, clientSecret, redirectURI, scope ];
|
||||
|
||||
database.query('INSERT INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (?, ?, ?, ?, ?, ?)', data, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
|
||||
if (error || result.affectedRows === 0) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(id, appId, type, clientSecret, redirectURI, scope, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof clientSecret, 'string');
|
||||
assert.strictEqual(typeof redirectURI, 'string');
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = [ id, appId, type, clientSecret, redirectURI, scope ];
|
||||
|
||||
database.query('REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (?, ?, ?, ?, ?, ?)', data, function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
|
||||
if (error || result.affectedRows === 0) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM clients WHERE id = ?', [ id ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, `Client not found: ${id}`));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function delByAppId(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM clients WHERE appId=?', [ appId ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function delByAppIdAndType(appId, type, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM clients WHERE appId=? AND type=?', [ appId, type ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM clients', function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
-329
@@ -1,329 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
get: get,
|
||||
del: del,
|
||||
getAll: getAll,
|
||||
getByAppIdAndType: getByAppIdAndType,
|
||||
getTokensByUserId: getTokensByUserId,
|
||||
delTokensByUserId: delTokensByUserId,
|
||||
delByAppIdAndType: delByAppIdAndType,
|
||||
addTokenByUserId: addTokenByUserId,
|
||||
delToken: delToken,
|
||||
|
||||
issueDeveloperToken: issueDeveloperToken,
|
||||
|
||||
addDefaultClients: addDefaultClients,
|
||||
|
||||
removeTokenPrivateFields: removeTokenPrivateFields,
|
||||
|
||||
// client ids. we categorize them so we can have different restrictions based on the client
|
||||
ID_WEBADMIN: 'cid-webadmin', // dashboard oauth
|
||||
ID_SDK: 'cid-sdk', // created by user via dashboard
|
||||
ID_CLI: 'cid-cli', // created via cli tool
|
||||
|
||||
// client type enums
|
||||
TYPE_EXTERNAL: 'external',
|
||||
TYPE_BUILT_IN: 'built-in',
|
||||
TYPE_OAUTH: 'addon-oauth',
|
||||
TYPE_PROXY: 'addon-proxy'
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:clients'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
hat = require('./hat.js'),
|
||||
accesscontrol = require('./accesscontrol.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
users = require('./users.js'),
|
||||
uuid = require('uuid'),
|
||||
_ = require('underscore');
|
||||
|
||||
function validateClientName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
if (name.length < 1) return new BoxError(BoxError.BAD_FIELD, 'name must be atleast 1 character', { field: 'name' });
|
||||
if (name.length > 128) return new BoxError(BoxError.BAD_FIELD, 'name too long', { field: 'name' });
|
||||
|
||||
if (/[^a-zA-Z0-9-]/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'name can only contain alphanumerals and dash', { field: 'name' });
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateTokenName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
if (name.length > 64) return new BoxError(BoxError.BAD_FIELD, 'name too long', { field: 'name' });
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function add(appId, type, redirectURI, scope, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof redirectURI, 'string');
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = accesscontrol.validateScopeString(scope);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateClientName(appId);
|
||||
if (error) return callback(error);
|
||||
|
||||
var id = 'cid-' + uuid.v4();
|
||||
var clientSecret = hat(8 * 128);
|
||||
|
||||
clientdb.add(id, appId, type, clientSecret, redirectURI, scope, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var client = {
|
||||
id: id,
|
||||
appId: appId,
|
||||
type: type,
|
||||
clientSecret: clientSecret,
|
||||
redirectURI: redirectURI,
|
||||
scope: scope
|
||||
};
|
||||
|
||||
callback(null, client);
|
||||
});
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.get(id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function del(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.del(id, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.getAll(function (error, results) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, []);
|
||||
if (error) return callback(error);
|
||||
|
||||
var tmp = [];
|
||||
async.each(results, function (record, callback) {
|
||||
if (record.type === exports.TYPE_EXTERNAL || record.type === exports.TYPE_BUILT_IN) {
|
||||
// the appId in this case holds the name
|
||||
record.name = record.appId;
|
||||
|
||||
tmp.push(record);
|
||||
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
apps.get(record.appId, function (error, result) {
|
||||
if (error) {
|
||||
console.error('Failed to get app details for oauth client', record.appId, error);
|
||||
return callback(null); // ignore error so we continue listing clients
|
||||
}
|
||||
|
||||
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
|
||||
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
|
||||
|
||||
record.domain = result.fqdn;
|
||||
|
||||
tmp.push(record);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
callback(null, tmp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getByAppIdAndType(appId, type, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.getByAppIdAndType(appId, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getTokensByUserId(clientId, userId, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.getByIdentifierAndClientId(userId, clientId, function (error, result) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) {
|
||||
// this can mean either that there are no tokens or the clientId is actually unknown
|
||||
get(clientId, function (error/*, result*/) {
|
||||
if (error) return callback(error);
|
||||
callback(null, []);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (error) return callback(error);
|
||||
callback(null, result || []);
|
||||
});
|
||||
}
|
||||
|
||||
function delTokensByUserId(clientId, userId, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.delByIdentifierAndClientId(userId, clientId, function (error) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) {
|
||||
// this can mean either that there are no tokens or the clientId is actually unknown
|
||||
get(clientId, function (error/*, result*/) {
|
||||
if (error) return callback(error);
|
||||
callback(null);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (error) return callback(error);
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function delByAppIdAndType(appId, type, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getByAppIdAndType(appId, type, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tokendb.delByClientId(result.id, function (error) {
|
||||
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
|
||||
|
||||
clientdb.delByAppIdAndType(appId, type, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof expiresAt, 'number');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const name = options.name || '';
|
||||
let error = validateTokenName(name);
|
||||
if (error) return callback(error);
|
||||
|
||||
get(clientId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
users.get(userId, function (error, user) {
|
||||
if (error) return callback(error);
|
||||
|
||||
accesscontrol.scopesForUser(user, function (error, userScopes) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const scope = accesscontrol.canonicalScopeString(result.scope);
|
||||
const authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(','));
|
||||
|
||||
const token = {
|
||||
id: 'tid-' + uuid.v4(),
|
||||
accessToken: hat(8 * 32),
|
||||
identifier: userId,
|
||||
clientId: result.id,
|
||||
expires: expiresAt,
|
||||
scope: authorizedScopes.join(','),
|
||||
name: name
|
||||
};
|
||||
|
||||
tokendb.add(token, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, {
|
||||
accessToken: token.accessToken,
|
||||
tokenScopes: authorizedScopes,
|
||||
identifier: userId,
|
||||
clientId: result.id,
|
||||
expires: expiresAt
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function issueDeveloperToken(userObject, auditSource, callback) {
|
||||
assert.strictEqual(typeof userObject, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
|
||||
addTokenByUserId(exports.ID_CLI, userObject.id, expiresAt, {}, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { userId: userObject.id, user: users.removePrivateFields(userObject) });
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function delToken(clientId, tokenId, callback) {
|
||||
assert.strictEqual(typeof clientId, 'string');
|
||||
assert.strictEqual(typeof tokenId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
get(clientId, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tokendb.del(tokenId, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addDefaultClients(origin, callback) {
|
||||
assert.strictEqual(typeof origin, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Adding default clients');
|
||||
|
||||
// The domain might have changed, therefor we have to update the record
|
||||
// id, appId, type, clientSecret, redirectURI, scope
|
||||
async.series([
|
||||
clientdb.upsert.bind(null, exports.ID_WEBADMIN, 'Settings', 'built-in', 'secret-webadmin', origin, '*'),
|
||||
clientdb.upsert.bind(null, exports.ID_SDK, 'SDK', 'built-in', 'secret-sdk', origin, '*'),
|
||||
clientdb.upsert.bind(null, exports.ID_CLI, 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*')
|
||||
], callback);
|
||||
}
|
||||
|
||||
function removeTokenPrivateFields(token) {
|
||||
return _.pick(token, 'id', 'identifier', 'clientId', 'scope', 'expires', 'name');
|
||||
}
|
||||
+42
-34
@@ -18,26 +18,25 @@ exports = module.exports = {
|
||||
|
||||
setupDashboard: setupDashboard,
|
||||
|
||||
runSystemChecks: runSystemChecks,
|
||||
runSystemChecks: runSystemChecks
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
var addons = require('./addons.js'),
|
||||
apps = require('./apps.js'),
|
||||
appstore = require('./appstore.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
backups = require('./backups.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
clients = require('./clients.js'),
|
||||
constants = require('./constants.js'),
|
||||
cron = require('./cron.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
domains = require('./domains.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
custom = require('./custom.js'),
|
||||
fs = require('fs'),
|
||||
mail = require('./mail.js'),
|
||||
notifications = require('./notifications.js'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
platform = require('./platform.js'),
|
||||
@@ -67,7 +66,7 @@ function uninitialize(callback) {
|
||||
|
||||
async.series([
|
||||
cron.stopJobs,
|
||||
platform.stop
|
||||
platform.stopAllTasks
|
||||
], callback);
|
||||
}
|
||||
|
||||
@@ -79,7 +78,13 @@ function onActivated(callback) {
|
||||
// 2. the restore code path can run without sudo (since mail/ is non-root)
|
||||
async.series([
|
||||
platform.start,
|
||||
cron.startJobs
|
||||
cron.startJobs,
|
||||
function checkBackupConfiguration(callback) {
|
||||
backups.checkConfiguration(function (error, message) {
|
||||
if (error) return callback(error);
|
||||
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, callback);
|
||||
});
|
||||
}
|
||||
], callback);
|
||||
}
|
||||
|
||||
@@ -104,9 +109,18 @@ function notifyUpdate(callback) {
|
||||
|
||||
// each of these tasks can fail. we will add some routes to fix/re-run them
|
||||
function runStartupTasks() {
|
||||
// stop all the systemd tasks
|
||||
platform.stopAllTasks(NOOP_CALLBACK);
|
||||
|
||||
// configure nginx to be reachable by IP
|
||||
reverseProxy.writeDefaultConfig(NOOP_CALLBACK);
|
||||
|
||||
// this configures collectd to collect backup storage metrics if filesystem is used. This is also triggerd when the settings change with the rest api
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return debug('runStartupTasks: failed to get backup config.', error);
|
||||
backups.configureCollectd(backupConfig, NOOP_CALLBACK);
|
||||
});
|
||||
|
||||
// always generate webadmin config since we have no versioning mechanism for the ejs
|
||||
if (settings.adminDomain()) reverseProxy.writeAdminConfig(settings.adminDomain(), NOOP_CALLBACK);
|
||||
|
||||
@@ -134,16 +148,21 @@ function getConfig(callback) {
|
||||
mailFqdn: settings.mailFqdn(),
|
||||
version: constants.VERSION,
|
||||
isDemo: settings.isDemo(),
|
||||
memory: os.totalmem(),
|
||||
provider: settings.provider(),
|
||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
|
||||
uiSpec: custom.uiSpec()
|
||||
footer: allSettings[settings.FOOTER_KEY] || constants.FOOTER,
|
||||
features: appstore.getFeatures(),
|
||||
profileLocked: allSettings[settings.DIRECTORY_CONFIG_KEY].lockUserProfiles,
|
||||
mandatory2FA: allSettings[settings.DIRECTORY_CONFIG_KEY].mandatory2FA
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reboot(callback) {
|
||||
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
|
||||
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', '', function (error) {
|
||||
if (error) debug('reboot: failed to clear reboot notification.', error);
|
||||
|
||||
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function isRebootRequired(callback) {
|
||||
@@ -154,26 +173,13 @@ function isRebootRequired(callback) {
|
||||
}
|
||||
|
||||
// called from cron.js
|
||||
function runSystemChecks() {
|
||||
async.parallel([
|
||||
checkBackupConfiguration,
|
||||
checkMailStatus,
|
||||
checkRebootRequired
|
||||
], function (error) {
|
||||
debug('runSystemChecks: done', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkBackupConfiguration(callback) {
|
||||
function runSystemChecks(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Checking backup configuration');
|
||||
|
||||
backups.checkConfiguration(function (error, message) {
|
||||
if (error) return callback(error);
|
||||
|
||||
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, callback);
|
||||
});
|
||||
async.parallel([
|
||||
checkMailStatus,
|
||||
checkRebootRequired
|
||||
], callback);
|
||||
}
|
||||
|
||||
function checkMailStatus(callback) {
|
||||
@@ -196,7 +202,7 @@ function checkRebootRequired(callback) {
|
||||
isRebootRequired(function (error, rebootRequired) {
|
||||
if (error) return callback(error);
|
||||
|
||||
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', rebootRequired ? 'To finish security updates, a [reboot](/#/system) is necessary.' : '', callback);
|
||||
notifications.alert(notifications.ALERT_REBOOT, 'Reboot Required', rebootRequired ? 'To finish ubuntu security updates, a reboot is necessary.' : '', callback);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -253,6 +259,8 @@ function prepareDashboardDomain(domain, auditSource, callback) {
|
||||
|
||||
debug(`prepareDashboardDomain: ${domain}`);
|
||||
|
||||
if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -291,10 +299,7 @@ function setDashboardDomain(domain, auditSource, callback) {
|
||||
|
||||
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
|
||||
|
||||
async.series([
|
||||
(done) => settings.setAdmin(domain, fqdn, done),
|
||||
(done) => clients.addDefaultClients(settings.adminOrigin(), done)
|
||||
], function (error) {
|
||||
settings.setAdmin(domain, fqdn, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
|
||||
@@ -313,10 +318,13 @@ function setDashboardAndMailDomain(domain, auditSource, callback) {
|
||||
|
||||
debug(`setDashboardAndMailDomain: ${domain}`);
|
||||
|
||||
if (settings.isDemo()) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
|
||||
|
||||
setDashboardDomain(domain, auditSource, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mail.onMailFqdnChanged(NOOP_CALLBACK); // this will update dns and re-configure mail server
|
||||
addons.restartService('turn', NOOP_CALLBACK); // to update the realm variable
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
addProfile,
|
||||
removeProfile
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
debug = require('debug')('collectd'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
shell = require('./shell.js');
|
||||
|
||||
const CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh');
|
||||
|
||||
function addProfile(name, profile, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof profile, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const configFilePath = path.join(paths.COLLECTD_APPCONFIG_DIR, `${name}.conf`);
|
||||
|
||||
// skip restarting collectd if the profile already exists with the same contents
|
||||
const currentProfile = safe.fs.readFileSync(configFilePath, 'utf8') || '';
|
||||
if (currentProfile === profile) return callback(null);
|
||||
|
||||
fs.writeFile(configFilePath, profile, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error writing collectd config: ${error.message}`));
|
||||
|
||||
shell.sudo('addCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'add', name ], {}, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.COLLECTD_ERROR, 'Could not add collectd config'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function removeProfile(name, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
fs.unlink(path.join(paths.COLLECTD_APPCONFIG_DIR, `${name}.conf`), function (error) {
|
||||
if (error && error.code !== 'ENOENT') debug('Error removing collectd profile', error);
|
||||
|
||||
shell.sudo('removeCollectdProfile', [ CONFIGURE_COLLECTD_CMD, 'remove', name ], {}, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.COLLECTD_ERROR, 'Could not remove collectd config'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Plugin python>
|
||||
<Module du>
|
||||
<Path>
|
||||
Instance "cloudron-backup"
|
||||
Dir "<%= backupDir %>"
|
||||
</Path>
|
||||
</Module>
|
||||
</Plugin>
|
||||
|
||||
+8
-5
@@ -32,21 +32,24 @@ exports = module.exports = {
|
||||
|
||||
NGINX_DEFAULT_CONFIG_FILE_NAME: 'default.conf',
|
||||
|
||||
GHOST_USER_FILE: '/tmp/cloudron_ghost.json',
|
||||
|
||||
DEFAULT_TOKEN_EXPIRATION: 7 * 24 * 60 * 60 * 1000, // 1 week
|
||||
DEFAULT_TOKEN_EXPIRATION: 365 * 24 * 60 * 60 * 1000, // 1 year
|
||||
|
||||
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024), // see also client.js
|
||||
|
||||
DEMO_USERNAME: 'cloudron',
|
||||
DEMO_BLACKLISTED_APPS: [ 'com.github.cloudtorrent' ],
|
||||
|
||||
AUTOUPDATE_PATTERN_NEVER: 'never',
|
||||
|
||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
|
||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8), // also used in dashboard client.js
|
||||
|
||||
CLOUDRON: CLOUDRON,
|
||||
TEST: TEST,
|
||||
|
||||
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '4.2.0-test'
|
||||
SUPPORT_EMAIL: 'support@cloudron.io',
|
||||
|
||||
FOOTER: '© 2020 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)',
|
||||
|
||||
VERSION: process.env.BOX_ENV === 'cloudron' ? fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim() : '5.1.1-test'
|
||||
};
|
||||
|
||||
|
||||
+114
-127
@@ -1,34 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
// IMPORTANT: These patterns are together because they spin tasks which acquire a lock
|
||||
// If the patterns overlap all the time, then the task may not ever get a chance to run!
|
||||
// If you change this change dashboard patterns in settings.html
|
||||
const DEFAULT_CLEANUP_BACKUPS_PATTERN = '00 30 1,3,5,23 * * *',
|
||||
DEFAULT_BOX_AUTOUPDATE_PATTERN = '00 00 1,3,5,23 * * *',
|
||||
DEFAULT_APP_AUTOUPDATE_PATTERN = '00 15 1,3,5,23 * * *';
|
||||
|
||||
exports = module.exports = {
|
||||
startJobs: startJobs,
|
||||
startJobs,
|
||||
|
||||
stopJobs: stopJobs,
|
||||
stopJobs,
|
||||
|
||||
handleSettingsChanged: handleSettingsChanged
|
||||
handleSettingsChanged,
|
||||
|
||||
DEFAULT_BOX_AUTOUPDATE_PATTERN,
|
||||
DEFAULT_APP_AUTOUPDATE_PATTERN
|
||||
};
|
||||
|
||||
var appHealthMonitor = require('./apphealthmonitor.js'),
|
||||
apps = require('./apps.js'),
|
||||
appstore = require('./appstore.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
backups = require('./backups.js'),
|
||||
cloudron = require('./cloudron.js'),
|
||||
constants = require('./constants.js'),
|
||||
CronJob = require('cron').CronJob,
|
||||
debug = require('debug')('box:cron'),
|
||||
disks = require('./disks.js'),
|
||||
dyndns = require('./dyndns.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
janitor = require('./janitor.js'),
|
||||
scheduler = require('./scheduler.js'),
|
||||
settings = require('./settings.js'),
|
||||
system = require('./system.js'),
|
||||
updater = require('./updater.js'),
|
||||
updateChecker = require('./updatechecker.js');
|
||||
|
||||
var gJobs = {
|
||||
alive: null, // send periodic stats
|
||||
appAutoUpdater: null,
|
||||
boxAutoUpdater: null,
|
||||
appUpdateChecker: null,
|
||||
@@ -59,150 +68,130 @@ var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
function startJobs(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var randomHourMinute = Math.floor(60*Math.random());
|
||||
gJobs.alive = new CronJob({
|
||||
cronTime: '00 ' + randomHourMinute + ' * * * *', // every hour on a random minute
|
||||
onTick: appstore.sendAliveStatus,
|
||||
const randomMinute = Math.floor(60*Math.random());
|
||||
gJobs.systemChecks = new CronJob({
|
||||
cronTime: '00 30 2 * * *', // once a day. if you change this interval, change the notification messages with correct duration
|
||||
onTick: () => cloudron.runSystemChecks(NOOP_CALLBACK),
|
||||
start: true
|
||||
});
|
||||
|
||||
gJobs.diskSpaceChecker = new CronJob({
|
||||
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
|
||||
onTick: () => system.checkDiskSpace(NOOP_CALLBACK),
|
||||
start: true
|
||||
});
|
||||
|
||||
gJobs.boxUpdateCheckerJob = new CronJob({
|
||||
cronTime: '00 ' + randomMinute + ' 1,3,5,21,23 * * *', // 5 times
|
||||
onTick: () => updateChecker.checkBoxUpdates({ automatic: true }, NOOP_CALLBACK),
|
||||
start: true
|
||||
});
|
||||
|
||||
// this is run separately from the update itself so that the user can disable automatic updates but can still get a notification
|
||||
gJobs.appUpdateChecker = new CronJob({
|
||||
cronTime: '00 ' + randomMinute + ' 2,4,6,20,22 * * *', // 5 times
|
||||
onTick: () => updateChecker.checkAppUpdates({ automatic: true }, NOOP_CALLBACK),
|
||||
start: true
|
||||
});
|
||||
|
||||
gJobs.cleanupTokens = new CronJob({
|
||||
cronTime: '00 */30 * * * *', // every 30 minutes
|
||||
onTick: janitor.cleanupTokens,
|
||||
start: true
|
||||
});
|
||||
|
||||
gJobs.cleanupBackups = new CronJob({
|
||||
cronTime: DEFAULT_CLEANUP_BACKUPS_PATTERN,
|
||||
onTick: backups.startCleanupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true
|
||||
});
|
||||
|
||||
gJobs.cleanupEventlog = new CronJob({
|
||||
cronTime: '00 */30 * * * *', // every 30 minutes
|
||||
onTick: eventlog.cleanup,
|
||||
start: true
|
||||
});
|
||||
|
||||
gJobs.dockerVolumeCleaner = new CronJob({
|
||||
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||
onTick: janitor.cleanupDockerVolumes,
|
||||
start: true
|
||||
});
|
||||
|
||||
gJobs.schedulerSync = new CronJob({
|
||||
cronTime: constants.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
|
||||
onTick: scheduler.sync,
|
||||
start: true
|
||||
});
|
||||
|
||||
gJobs.certificateRenew = new CronJob({
|
||||
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||
onTick: cloudron.renewCerts.bind(null, {}, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true
|
||||
});
|
||||
|
||||
gJobs.appHealthMonitor = new CronJob({
|
||||
cronTime: '*/10 * * * * *', // every 10 seconds
|
||||
onTick: appHealthMonitor.run.bind(null, 10, NOOP_CALLBACK),
|
||||
start: true
|
||||
});
|
||||
|
||||
settings.getAll(function (error, allSettings) {
|
||||
if (error) return callback(error);
|
||||
|
||||
recreateJobs(allSettings[settings.TIME_ZONE_KEY]);
|
||||
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY]);
|
||||
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY]);
|
||||
const tz = allSettings[settings.TIME_ZONE_KEY];
|
||||
backupConfigChanged(allSettings[settings.BACKUP_CONFIG_KEY], tz);
|
||||
appAutoupdatePatternChanged(allSettings[settings.APP_AUTOUPDATE_PATTERN_KEY], tz);
|
||||
boxAutoupdatePatternChanged(allSettings[settings.BOX_AUTOUPDATE_PATTERN_KEY], tz);
|
||||
dynamicDnsChanged(allSettings[settings.DYNAMIC_DNS_KEY]);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function handleSettingsChanged(key, value) {
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
|
||||
// value is a variant
|
||||
|
||||
switch (key) {
|
||||
case settings.TIME_ZONE_KEY: recreateJobs(value); break;
|
||||
case settings.APP_AUTOUPDATE_PATTERN_KEY: appAutoupdatePatternChanged(value); break;
|
||||
case settings.BOX_AUTOUPDATE_PATTERN_KEY: boxAutoupdatePatternChanged(value); break;
|
||||
case settings.DYNAMIC_DNS_KEY: dynamicDnsChanged(value); break;
|
||||
default: break;
|
||||
case settings.TIME_ZONE_KEY:
|
||||
case settings.BACKUP_CONFIG_KEY:
|
||||
case settings.APP_AUTOUPDATE_PATTERN_KEY:
|
||||
case settings.BOX_AUTOUPDATE_PATTERN_KEY:
|
||||
case settings.DYNAMIC_DNS_KEY:
|
||||
debug('handleSettingsChanged: recreating all jobs');
|
||||
async.series([
|
||||
stopJobs,
|
||||
startJobs
|
||||
], NOOP_CALLBACK);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function recreateJobs(tz) {
|
||||
function backupConfigChanged(value, tz) {
|
||||
assert.strictEqual(typeof value, 'object');
|
||||
assert.strictEqual(typeof tz, 'string');
|
||||
|
||||
debug('Creating jobs with timezone %s', tz);
|
||||
debug(`backupConfigChanged: schedule ${value.schedulePattern} (${tz})`);
|
||||
|
||||
if (gJobs.backup) gJobs.backup.stop();
|
||||
|
||||
gJobs.backup = new CronJob({
|
||||
cronTime: '00 00 */6 * * *', // check every 6 hours
|
||||
onTick: backups.ensureBackup.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.systemChecks) gJobs.systemChecks.stop();
|
||||
gJobs.systemChecks = new CronJob({
|
||||
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
|
||||
onTick: cloudron.runSystemChecks,
|
||||
start: true,
|
||||
runOnInit: true, // run system check immediately
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.diskSpaceChecker) gJobs.diskSpaceChecker.stop();
|
||||
gJobs.diskSpaceChecker = new CronJob({
|
||||
cronTime: '00 30 * * * *', // every 30 minutes. if you change this interval, change the notification messages with correct duration
|
||||
onTick: () => disks.checkDiskSpace(NOOP_CALLBACK),
|
||||
start: true,
|
||||
runOnInit: true, // run system check immediately
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
// randomized pattern per cloudron every hour
|
||||
var randomMinute = Math.floor(60*Math.random());
|
||||
|
||||
if (gJobs.boxUpdateCheckerJob) gJobs.boxUpdateCheckerJob.stop();
|
||||
gJobs.boxUpdateCheckerJob = new CronJob({
|
||||
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
|
||||
onTick: () => updateChecker.checkBoxUpdates(NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.appUpdateChecker) gJobs.appUpdateChecker.stop();
|
||||
gJobs.appUpdateChecker = new CronJob({
|
||||
cronTime: '00 ' + randomMinute + ' * * * *', // once an hour
|
||||
onTick: () => updateChecker.checkAppUpdates(NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.cleanupTokens) gJobs.cleanupTokens.stop();
|
||||
gJobs.cleanupTokens = new CronJob({
|
||||
cronTime: '00 */30 * * * *', // every 30 minutes
|
||||
onTick: janitor.cleanupTokens,
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.cleanupBackups) gJobs.cleanupBackups.stop();
|
||||
gJobs.cleanupBackups = new CronJob({
|
||||
cronTime: '00 45 */6 * * *', // every 6 hours. try not to overlap with ensureBackup job
|
||||
onTick: backups.startCleanupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.cleanupEventlog) gJobs.cleanupEventlog.stop();
|
||||
gJobs.cleanupEventlog = new CronJob({
|
||||
cronTime: '00 */30 * * * *', // every 30 minutes
|
||||
onTick: eventlog.cleanup,
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.dockerVolumeCleaner) gJobs.dockerVolumeCleaner.stop();
|
||||
gJobs.dockerVolumeCleaner = new CronJob({
|
||||
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||
onTick: janitor.cleanupDockerVolumes,
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.schedulerSync) gJobs.schedulerSync.stop();
|
||||
gJobs.schedulerSync = new CronJob({
|
||||
cronTime: constants.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
|
||||
onTick: scheduler.sync,
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.certificateRenew) gJobs.certificateRenew.stop();
|
||||
gJobs.certificateRenew = new CronJob({
|
||||
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||
onTick: cloudron.renewCerts.bind(null, {}, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
|
||||
if (gJobs.appHealthMonitor) gJobs.appHealthMonitor.stop();
|
||||
gJobs.appHealthMonitor = new CronJob({
|
||||
cronTime: '*/10 * * * * *', // every 10 seconds
|
||||
onTick: appHealthMonitor.run.bind(null, 10, NOOP_CALLBACK),
|
||||
cronTime: value.schedulePattern,
|
||||
onTick: backups.startBackupTask.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: tz
|
||||
});
|
||||
}
|
||||
|
||||
function boxAutoupdatePatternChanged(pattern) {
|
||||
function boxAutoupdatePatternChanged(pattern, tz) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
assert(gJobs.boxUpdateCheckerJob);
|
||||
assert.strictEqual(typeof tz, 'string');
|
||||
|
||||
debug('Box auto update pattern changed to %s', pattern);
|
||||
debug(`boxAutoupdatePatternChanged: pattern - ${pattern} (${tz})`);
|
||||
|
||||
if (gJobs.boxAutoUpdater) gJobs.boxAutoUpdater.stop();
|
||||
|
||||
@@ -220,15 +209,15 @@ function boxAutoupdatePatternChanged(pattern) {
|
||||
}
|
||||
},
|
||||
start: true,
|
||||
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
|
||||
timeZone: tz
|
||||
});
|
||||
}
|
||||
|
||||
function appAutoupdatePatternChanged(pattern) {
|
||||
function appAutoupdatePatternChanged(pattern, tz) {
|
||||
assert.strictEqual(typeof pattern, 'string');
|
||||
assert(gJobs.boxUpdateCheckerJob);
|
||||
assert.strictEqual(typeof tz, 'string');
|
||||
|
||||
debug('Apps auto update pattern changed to %s', pattern);
|
||||
debug(`appAutoupdatePatternChanged: pattern ${pattern} (${tz})`);
|
||||
|
||||
if (gJobs.appAutoUpdater) gJobs.appAutoUpdater.stop();
|
||||
|
||||
@@ -246,13 +235,12 @@ function appAutoupdatePatternChanged(pattern) {
|
||||
}
|
||||
},
|
||||
start: true,
|
||||
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
|
||||
timeZone: tz
|
||||
});
|
||||
}
|
||||
|
||||
function dynamicDnsChanged(enabled) {
|
||||
assert.strictEqual(typeof enabled, 'boolean');
|
||||
assert(gJobs.boxUpdateCheckerJob);
|
||||
|
||||
debug('Dynamic DNS setting changed to %s', enabled);
|
||||
|
||||
@@ -260,8 +248,7 @@ function dynamicDnsChanged(enabled) {
|
||||
gJobs.dynamicDns = new CronJob({
|
||||
cronTime: '5 * * * * *', // we only update the records if the ip has changed.
|
||||
onTick: dyndns.sync.bind(null, auditSource.CRON, NOOP_CALLBACK),
|
||||
start: true,
|
||||
timeZone: gJobs.boxUpdateCheckerJob.cronTime.zone // hack
|
||||
start: true
|
||||
});
|
||||
} else {
|
||||
if (gJobs.dynamicDns) gJobs.dynamicDns.stop();
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
let debug = require('debug')('box:custom'),
|
||||
lodash = require('lodash'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
yaml = require('js-yaml');
|
||||
|
||||
exports = module.exports = {
|
||||
uiSpec: uiSpec,
|
||||
spec: spec
|
||||
};
|
||||
|
||||
const DEFAULT_SPEC = {
|
||||
appstore: {
|
||||
blacklist: [],
|
||||
whitelist: null // null imples, not set. this is an object and not an array
|
||||
},
|
||||
backups: {
|
||||
configurable: true
|
||||
},
|
||||
domains: {
|
||||
dynamicDns: true,
|
||||
changeDashboardDomain: true
|
||||
},
|
||||
subscription: {
|
||||
configurable: true
|
||||
},
|
||||
support: {
|
||||
email: 'support@cloudron.io',
|
||||
remoteSupport: true,
|
||||
ticketFormBody:
|
||||
'Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).\n\n'
|
||||
+ '* [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)\n'
|
||||
+ '* [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)\n'
|
||||
+ '* [Forum](https://forum.cloudron.io/)\n\n',
|
||||
submitTickets: true
|
||||
},
|
||||
alerts: {
|
||||
email: '',
|
||||
notifyCloudronAdmins: true
|
||||
},
|
||||
footer: {
|
||||
body: '© 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
|
||||
}
|
||||
};
|
||||
|
||||
const gSpec = (function () {
|
||||
try {
|
||||
if (!safe.fs.existsSync(paths.CUSTOM_FILE)) return DEFAULT_SPEC;
|
||||
const c = yaml.safeLoad(safe.fs.readFileSync(paths.CUSTOM_FILE, 'utf8'));
|
||||
return lodash.merge({}, DEFAULT_SPEC, c);
|
||||
} catch (e) {
|
||||
debug(`Error loading features file from ${paths.CUSTOM_FILE} : ${e.message}`);
|
||||
return DEFAULT_SPEC;
|
||||
}
|
||||
})();
|
||||
|
||||
// flags sent to the UI. this is separate because we have values that are secret to the backend
|
||||
function uiSpec() {
|
||||
return gSpec;
|
||||
}
|
||||
|
||||
function spec() {
|
||||
return gSpec;
|
||||
}
|
||||
+44
-101
@@ -14,14 +14,15 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
child_process = require('child_process'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:database'),
|
||||
mysql = require('mysql'),
|
||||
once = require('once'),
|
||||
util = require('util');
|
||||
|
||||
var gConnectionPool = null,
|
||||
gDefaultConnection = null;
|
||||
var gConnectionPool = null;
|
||||
|
||||
const gDatabase = {
|
||||
hostname: '127.0.0.1',
|
||||
@@ -41,59 +42,37 @@ function initialize(callback) {
|
||||
gDatabase.hostname = require('child_process').execSync('docker inspect -f "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}" mysql-server').toString().trim();
|
||||
}
|
||||
|
||||
// https://github.com/mysqljs/mysql#pool-options
|
||||
gConnectionPool = mysql.createPool({
|
||||
connectionLimit: 5, // this has to be > 1 since we store one connection as 'default'. the rest for transactions
|
||||
connectionLimit: 5,
|
||||
host: gDatabase.hostname,
|
||||
user: gDatabase.username,
|
||||
password: gDatabase.password,
|
||||
port: gDatabase.port,
|
||||
database: gDatabase.name,
|
||||
multipleStatements: false,
|
||||
waitForConnections: true, // getConnection() will wait until a connection is avaiable
|
||||
ssl: false,
|
||||
timezone: 'Z' // mysql follows the SYSTEM timezone. on Cloudron, this is UTC
|
||||
});
|
||||
|
||||
gConnectionPool.on('connection', function (connection) {
|
||||
// connection objects are re-used. so we have to attach to the event here (once) to prevent crash
|
||||
// note the pool also has an 'acquire' event but that is called whenever we do a getConnection()
|
||||
connection.on('error', (error) => debug(`Connection ${connection.threadId} error: ${error.message} ${error.code}`));
|
||||
|
||||
connection.query('USE ' + gDatabase.name);
|
||||
connection.query('SET SESSION sql_mode = \'strict_all_tables\'');
|
||||
});
|
||||
|
||||
reconnect(callback);
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
if (gConnectionPool) {
|
||||
gConnectionPool.end(callback);
|
||||
gConnectionPool = null;
|
||||
} else {
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
if (!gConnectionPool) return callback(null);
|
||||
|
||||
function reconnect(callback) {
|
||||
callback = callback ? once(callback) : function () {};
|
||||
|
||||
gConnectionPool.getConnection(function (error, connection) {
|
||||
if (error) {
|
||||
console.error('Unable to reestablish connection to database. Try again in a bit.', error.message);
|
||||
return setTimeout(reconnect.bind(null, callback), 1000);
|
||||
}
|
||||
|
||||
connection.on('error', function (error) {
|
||||
// by design, we catch all normal errors by providing callbacks.
|
||||
// this function should be invoked only when we have no callbacks pending and we have a fatal error
|
||||
assert(error.fatal, 'Non-fatal error on connection object');
|
||||
|
||||
console.error('Unhandled mysql connection error.', error);
|
||||
|
||||
// This is most likely an issue an can cause double callbacks from reconnect()
|
||||
setTimeout(reconnect.bind(null, callback), 1000);
|
||||
});
|
||||
|
||||
gDefaultConnection = connection;
|
||||
|
||||
callback(null);
|
||||
});
|
||||
gConnectionPool.end(callback);
|
||||
gConnectionPool = null;
|
||||
}
|
||||
|
||||
function clear(callback) {
|
||||
@@ -103,86 +82,46 @@ function clear(callback) {
|
||||
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name,
|
||||
gDatabase.hostname, gDatabase.username, gDatabase.password, gDatabase.name);
|
||||
|
||||
async.series([
|
||||
child_process.exec.bind(null, cmd),
|
||||
require('./clients.js').addDefaultClients.bind(null, 'https://admin-localhost')
|
||||
], callback);
|
||||
}
|
||||
|
||||
function beginTransaction(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gConnectionPool === null) return callback(new Error('No database connection pool.'));
|
||||
|
||||
gConnectionPool.getConnection(function (error, connection) {
|
||||
if (error) {
|
||||
console.error('Unable to get connection to database. Try again in a bit.', error.message);
|
||||
return setTimeout(beginTransaction.bind(null, callback), 1000);
|
||||
}
|
||||
|
||||
connection.beginTransaction(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
return callback(null, connection);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function rollback(connection, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
connection.rollback(function (error) {
|
||||
if (error) console.error(error); // can this happen?
|
||||
|
||||
connection.release();
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: if commit fails, is it supposed to return an error ?
|
||||
function commit(connection, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
connection.commit(function (error) {
|
||||
if (error) return rollback(connection, callback);
|
||||
|
||||
connection.release();
|
||||
return callback(null);
|
||||
});
|
||||
child_process.exec(cmd, callback);
|
||||
}
|
||||
|
||||
function query() {
|
||||
var args = Array.prototype.slice.call(arguments);
|
||||
var callback = args[args.length - 1];
|
||||
const args = Array.prototype.slice.call(arguments);
|
||||
const callback = args[args.length - 1];
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (gDefaultConnection === null) return callback(new Error('No connection to database'));
|
||||
if (constants.TEST && !gConnectionPool) return callback(new BoxError(BoxError.DATABASE_ERROR, 'database.js not initialized'));
|
||||
|
||||
args[args.length -1 ] = function (error, result) {
|
||||
if (error && error.fatal) {
|
||||
gDefaultConnection = null;
|
||||
setTimeout(reconnect, 1000);
|
||||
}
|
||||
|
||||
callback(error, result);
|
||||
};
|
||||
|
||||
gDefaultConnection.query.apply(gDefaultConnection, args);
|
||||
gConnectionPool.query.apply(gConnectionPool, args); // this is same as getConnection/query/release
|
||||
}
|
||||
|
||||
function transaction(queries, callback) {
|
||||
assert(util.isArray(queries));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
beginTransaction(function (error, conn) {
|
||||
callback = once(callback);
|
||||
|
||||
gConnectionPool.getConnection(function (error, connection) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.mapSeries(queries, function iterator(query, done) {
|
||||
conn.query(query.query, query.args, done);
|
||||
}, function seriesDone(error, results) {
|
||||
if (error) return rollback(conn, callback.bind(null, error));
|
||||
const releaseConnection = (error) => { connection.release(); callback(error); };
|
||||
|
||||
commit(conn, callback.bind(null, null, results));
|
||||
connection.beginTransaction(function (error) {
|
||||
if (error) return releaseConnection(error);
|
||||
|
||||
async.mapSeries(queries, function iterator(query, done) {
|
||||
connection.query(query.query, query.args, done);
|
||||
}, function seriesDone(error, results) {
|
||||
if (error) return connection.rollback(() => releaseConnection(error));
|
||||
|
||||
connection.commit(function (error) {
|
||||
if (error) return connection.rollback(() => releaseConnection(error));
|
||||
|
||||
connection.release();
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -203,7 +142,11 @@ function exportToFile(file, callback) {
|
||||
assert.strictEqual(typeof file, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
|
||||
// latest mysqldump enables column stats by default which is not present in MySQL 5.7 server
|
||||
// this option must not be set in production cloudrons which still use the old mysqldump
|
||||
const disableColStats = (constants.TEST && require('fs').readFileSync('/etc/lsb-release', 'utf-8').includes('20.04')) ? '--column-statistics=0' : '';
|
||||
|
||||
var cmd = `/usr/bin/mysqldump -h "${gDatabase.hostname}" -u root -p${gDatabase.password} ${disableColStats} --single-transaction --routines --triggers ${gDatabase.name} > "${file}"`;
|
||||
|
||||
child_process.exec(cmd, callback);
|
||||
}
|
||||
|
||||
-176
@@ -1,176 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
removePrivateFields: removePrivateFields,
|
||||
injectPrivateFields: injectPrivateFields,
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
wait: wait,
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
debug = require('debug')('box:dns/caas'),
|
||||
domains = require('../domains.js'),
|
||||
settings = require('../settings.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
function formatError(response) {
|
||||
return util.format('Caas DNS error [%s] %j', response.statusCode, response.body);
|
||||
}
|
||||
|
||||
function getFqdn(location, domain) {
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
|
||||
return (location === '') ? domain : location + '-' + domain;
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
|
||||
// do not return the 'key'. in caas, this is private
|
||||
delete domainObject.fallbackCertificate.key;
|
||||
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function upsert(domainObject, location, type, values, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dnsConfig = domainObject.config;
|
||||
|
||||
let fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
|
||||
|
||||
debug('add: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
values: values
|
||||
};
|
||||
|
||||
superagent
|
||||
.post(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
|
||||
.query({ token: dnsConfig.token })
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode === 420) return callback(new BoxError(BoxError.BUSY));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function get(domainObject, location, type, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dnsConfig = domainObject.config;
|
||||
const fqdn = location !== '' && type === 'TXT' ? location + '.' + domainObject.domain : getFqdn(location, domainObject.domain);
|
||||
|
||||
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', domainObject.domain, location, type, fqdn);
|
||||
|
||||
superagent
|
||||
.get(settings.apiServerOrigin() + '/api/v1/caas/domains/' + fqdn)
|
||||
.query({ token: dnsConfig.token, type: type })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null, result.body.values);
|
||||
});
|
||||
}
|
||||
|
||||
function del(domainObject, location, type, values, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dnsConfig = domainObject.config;
|
||||
debug('del: %s for zone %s of type %s with values %j', location, domainObject.domain, type, values);
|
||||
|
||||
var data = {
|
||||
type: type,
|
||||
values: values
|
||||
};
|
||||
|
||||
superagent
|
||||
.del(settings.apiServerOrigin() + '/api/v1/caas/domains/' + getFqdn(location, domainObject.domain))
|
||||
.query({ token: dnsConfig.token })
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
|
||||
if (result.statusCode === 420) return callback(new BoxError(BoxError.BUSY));
|
||||
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function wait(domainObject, location, type, value, options, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
|
||||
}
|
||||
|
||||
function verifyDnsConfig(domainObject, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dnsConfig = domainObject.config;
|
||||
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
|
||||
|
||||
const ip = '127.0.0.1';
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
hyphenatedSubdomains: true // this will ensure we always use them, regardless of passed-in configs
|
||||
};
|
||||
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added');
|
||||
|
||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
});
|
||||
}
|
||||
+41
-27
@@ -13,6 +13,7 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
constants = require('../constants.js'),
|
||||
debug = require('debug')('box:dns/cloudflare'),
|
||||
dns = require('../native-dns.js'),
|
||||
domains = require('../domains.js'),
|
||||
@@ -25,12 +26,12 @@ var assert = require('assert'),
|
||||
var CLOUDFLARE_ENDPOINT = 'https://api.cloudflare.com/client/v4';
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function translateRequestError(result, callback) {
|
||||
@@ -39,24 +40,43 @@ function translateRequestError(result, callback) {
|
||||
|
||||
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, util.format('%s %j', result.statusCode, 'API does not exist')));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.BAD_FIELD, result.body.message));
|
||||
if ((result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) && result.body.errors.length > 0) {
|
||||
let error = result.body.errors[0];
|
||||
let message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
|
||||
if (result.statusCode === 400 || result.statusCode === 401 || result.statusCode === 403) {
|
||||
let message = 'Unknown error';
|
||||
if (typeof result.body.error === 'string') {
|
||||
message = `message: ${result.body.error} statusCode: ${result.statusCode}`;
|
||||
} else if (Array.isArray(result.body.errors) && result.body.errors.length > 0) {
|
||||
let error = result.body.errors[0];
|
||||
message = `message: ${error.message} statusCode: ${result.statusCode} code:${error.code}`;
|
||||
}
|
||||
return callback(new BoxError(BoxError.ACCESS_DENIED, message));
|
||||
}
|
||||
|
||||
callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
}
|
||||
|
||||
function createRequest(method, url, dnsConfig) {
|
||||
assert.strictEqual(typeof method, 'string');
|
||||
assert.strictEqual(typeof url, 'string');
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
|
||||
let request = superagent(method, url)
|
||||
.timeout(30 * 1000);
|
||||
|
||||
if (dnsConfig.tokenType === 'GlobalApiKey') {
|
||||
request.set('X-Auth-Key', dnsConfig.token).set('X-Auth-Email', dnsConfig.email);
|
||||
} else {
|
||||
request.set('Authorization', 'Bearer ' + dnsConfig.token);
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
function getZoneByName(dnsConfig, zoneName, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
superagent.get(CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active')
|
||||
.set('X-Auth-Key', dnsConfig.token)
|
||||
.set('X-Auth-Email', dnsConfig.email)
|
||||
.timeout(30 * 1000)
|
||||
createRequest('GET', CLOUDFLARE_ENDPOINT + '/zones?name=' + zoneName + '&status=active', dnsConfig)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
@@ -74,11 +94,8 @@ function getDnsRecords(dnsConfig, zoneId, fqdn, type, callback) {
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
superagent.get(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
|
||||
.set('X-Auth-Key',dnsConfig.token)
|
||||
.set('X-Auth-Email',dnsConfig.email)
|
||||
createRequest('GET', CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records', dnsConfig)
|
||||
.query({ type: type, name: fqdn })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
@@ -132,11 +149,8 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
if (i >= dnsRecords.length) { // create a new record
|
||||
debug(`upsert: Adding new record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: false`);
|
||||
|
||||
superagent.post(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records')
|
||||
.set('X-Auth-Key', dnsConfig.token)
|
||||
.set('X-Auth-Email', dnsConfig.email)
|
||||
createRequest('POST', CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records', dnsConfig)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return iteratorCallback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, iteratorCallback);
|
||||
@@ -148,11 +162,8 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
|
||||
debug(`upsert: Updating existing record fqdn: ${fqdn}, zoneName: ${zoneName} proxied: ${data.proxied}`);
|
||||
|
||||
superagent.put(CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records/' + dnsRecords[i].id)
|
||||
.set('X-Auth-Key', dnsConfig.token)
|
||||
.set('X-Auth-Email', dnsConfig.email)
|
||||
createRequest('PUT', CLOUDFLARE_ENDPOINT + '/zones/' + zoneId + '/dns_records/' + dnsRecords[i].id, dnsConfig)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
++i; // increment, as we have consumed the record
|
||||
|
||||
@@ -217,10 +228,7 @@ function del(domainObject, location, type, values, callback) {
|
||||
if (tmp.length === 0) return callback(null);
|
||||
|
||||
async.eachSeries(tmp, function (record, callback) {
|
||||
superagent.del(CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id)
|
||||
.set('X-Auth-Key', dnsConfig.token)
|
||||
.set('X-Auth-Email', dnsConfig.email)
|
||||
.timeout(30 * 1000)
|
||||
createRequest('DELETE', CLOUDFLARE_ENDPOINT + '/zones/'+ zoneId + '/dns_records/' + record.id, dnsConfig)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (result.statusCode !== 200 || result.body.success !== true) return translateRequestError(result, callback);
|
||||
@@ -277,14 +285,20 @@ function verifyDnsConfig(domainObject, callback) {
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName;
|
||||
|
||||
// token can be api token or global api key
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
|
||||
if (!dnsConfig.email || typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
|
||||
if (dnsConfig.tokenType !== 'GlobalApiKey' && dnsConfig.tokenType !== 'ApiToken') return callback(new BoxError(BoxError.BAD_FIELD, 'tokenType is required', { field: 'tokenType' }));
|
||||
|
||||
if (dnsConfig.tokenType === 'GlobalApiKey') {
|
||||
if (typeof dnsConfig.email !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'email must be a non-empty string', { field: 'email' }));
|
||||
}
|
||||
|
||||
const ip = '127.0.0.1';
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token,
|
||||
email: dnsConfig.email
|
||||
tokenType: dnsConfig.tokenType,
|
||||
email: dnsConfig.email || null
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
@@ -13,6 +13,7 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
constants = require('../constants.js'),
|
||||
debug = require('debug')('box:dns/digitalocean'),
|
||||
dns = require('../native-dns.js'),
|
||||
domains = require('../domains.js'),
|
||||
@@ -28,12 +29,12 @@ function formatError(response) {
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function getInternal(dnsConfig, zoneName, name, type, callback) {
|
||||
|
||||
+3
-2
@@ -12,6 +12,7 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
constants = require('../constants.js'),
|
||||
debug = require('debug')('box:dns/gandi'),
|
||||
dns = require('../native-dns.js'),
|
||||
domains = require('../domains.js'),
|
||||
@@ -26,12 +27,12 @@ function formatError(response) {
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function upsert(domainObject, location, type, values, callback) {
|
||||
|
||||
+3
-2
@@ -12,6 +12,7 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
constants = require('../constants.js'),
|
||||
debug = require('debug')('box:dns/gcdns'),
|
||||
dns = require('../native-dns.js'),
|
||||
domains = require('../domains.js'),
|
||||
@@ -21,12 +22,12 @@ var assert = require('assert'),
|
||||
_ = require('underscore');
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.credentials.private_key = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.credentials.private_key = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.credentials.private_key === domains.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
|
||||
if (newConfig.credentials.private_key === constants.SECRET_PLACEHOLDER && currentConfig.credentials) newConfig.credentials.private_key = currentConfig.credentials.private_key;
|
||||
}
|
||||
|
||||
function getDnsCredentials(dnsConfig) {
|
||||
|
||||
+4
-3
@@ -12,6 +12,7 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
constants = require('../constants.js'),
|
||||
debug = require('debug')('box:dns/godaddy'),
|
||||
dns = require('../native-dns.js'),
|
||||
domains = require('../domains.js'),
|
||||
@@ -32,12 +33,12 @@ function formatError(response) {
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.apiSecret = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.apiSecret = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.apiSecret === domains.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
|
||||
if (newConfig.apiSecret === constants.SECRET_PLACEHOLDER) newConfig.apiSecret = currentConfig.apiSecret;
|
||||
}
|
||||
|
||||
function upsert(domainObject, location, type, values, callback) {
|
||||
@@ -126,7 +127,7 @@ function del(domainObject, location, type, values, callback) {
|
||||
|
||||
debug(`get: ${name} in zone ${zoneName} of type ${type} with values ${JSON.stringify(values)}`);
|
||||
|
||||
if (type !== 'A' && type !== 'TXT') return callback(new BoxError(BoxError.EXTERNAL_ERROR, new Error('Record deletion is not supported by GoDaddy API')));
|
||||
if (type !== 'A' && type !== 'TXT') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Record deletion is not supported by GoDaddy API'));
|
||||
|
||||
// check if the record exists at all so that we don't insert the "Dead" record for no reason
|
||||
get(domainObject, location, type, function (error, values) {
|
||||
|
||||
@@ -17,15 +17,17 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
util = require('util');
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
// in-place removal of tokens and api keys with domains.SECRET_PLACEHOLDER
|
||||
// in-place removal of tokens and api keys with constants.SECRET_PLACEHOLDER
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
// in-place injection of tokens and api keys which came in with domains.SECRET_PLACEHOLDER
|
||||
// in-place injection of tokens and api keys which came in with constants.SECRET_PLACEHOLDER
|
||||
}
|
||||
|
||||
function upsert(domainObject, location, type, values, callback) {
|
||||
@@ -37,7 +39,7 @@ function upsert(domainObject, location, type, values, callback) {
|
||||
|
||||
// Result: none
|
||||
|
||||
callback(new Error('not implemented'));
|
||||
callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'upsert is not implemented'));
|
||||
}
|
||||
|
||||
function get(domainObject, location, type, callback) {
|
||||
@@ -48,7 +50,7 @@ function get(domainObject, location, type, callback) {
|
||||
|
||||
// Result: Array of matching DNS records in string format
|
||||
|
||||
callback(new Error('not implemented'));
|
||||
callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'get is not implemented'));
|
||||
}
|
||||
|
||||
function del(domainObject, location, type, values, callback) {
|
||||
@@ -60,7 +62,7 @@ function del(domainObject, location, type, values, callback) {
|
||||
|
||||
// Result: none
|
||||
|
||||
callback(new Error('not implemented'));
|
||||
callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'del is not implemented'));
|
||||
}
|
||||
|
||||
function wait(domainObject, location, type, value, options, callback) {
|
||||
@@ -80,5 +82,5 @@ function verifyDnsConfig(domainObject, callback) {
|
||||
|
||||
// Result: dnsConfig object
|
||||
|
||||
callback(new Error('not implemented'));
|
||||
callback(new BoxError(BoxError.NOT_IMPLEMENTED, 'verifyDnsConfig is not implemented'));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,310 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
removePrivateFields: removePrivateFields,
|
||||
injectPrivateFields: injectPrivateFields,
|
||||
upsert: upsert,
|
||||
get: get,
|
||||
del: del,
|
||||
wait: wait,
|
||||
verifyDnsConfig: verifyDnsConfig
|
||||
};
|
||||
|
||||
let async = require('async'),
|
||||
assert = require('assert'),
|
||||
constants = require('../constants.js'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
debug = require('debug')('box:dns/linode'),
|
||||
dns = require('../native-dns.js'),
|
||||
domains = require('../domains.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
waitForDns = require('./waitfordns.js');
|
||||
|
||||
const LINODE_ENDPOINT = 'https://api.linode.com/v4';
|
||||
|
||||
function formatError(response) {
|
||||
return util.format('Linode DNS error [%s] %j', response.statusCode, response.body);
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function getZoneId(dnsConfig, zoneName, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// returns 100 at a time
|
||||
superagent.get(`${LINODE_ENDPOINT}/domains`)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.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.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
if (!Array.isArray(result.body.data)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response'));
|
||||
|
||||
const zone = result.body.data.find(d => d.domain === zoneName);
|
||||
|
||||
if (!zone || !zone.id) return callback(new BoxError(BoxError.NOT_FOUND, 'Zone not found'));
|
||||
|
||||
debug(`getZoneId: zone id of ${zoneName} is ${zone.id}`);
|
||||
|
||||
callback(null, zone.id);
|
||||
});
|
||||
}
|
||||
|
||||
function getZoneRecords(dnsConfig, zoneName, name, type, callback) {
|
||||
assert.strictEqual(typeof dnsConfig, 'object');
|
||||
assert.strictEqual(typeof zoneName, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`getInternal: getting dns records of ${zoneName} with ${name} and type ${type}`);
|
||||
|
||||
getZoneId(dnsConfig, zoneName, function (error, zoneId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let page = 0, more = false;
|
||||
let records = [];
|
||||
|
||||
async.doWhilst(function (iteratorDone) {
|
||||
const url = `${LINODE_ENDPOINT}/domains/${zoneId}/records?page=${++page}`;
|
||||
|
||||
superagent.get(url)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return iteratorDone(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 404) return iteratorDone(new BoxError(BoxError.NOT_FOUND, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return iteratorDone(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return iteratorDone(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
records = records.concat(result.body.data.filter(function (record) {
|
||||
return (record.type === type && record.name === name);
|
||||
}));
|
||||
|
||||
more = result.body.page !== result.body.pages;
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, function () { return more; }, function (error) {
|
||||
debug('getZoneRecords:', error, JSON.stringify(records));
|
||||
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, { zoneId, records });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function get(domainObject, location, type, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { records }) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var tmp = records.map(function (record) { return record.target; });
|
||||
|
||||
debug('get: %j', tmp);
|
||||
|
||||
return callback(null, tmp);
|
||||
});
|
||||
}
|
||||
|
||||
function upsert(domainObject, location, type, values, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
debug('upsert: %s for zone %s of type %s with values %j', name, zoneName, type, values);
|
||||
|
||||
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let i = 0, recordIds = []; // used to track available records to update instead of create
|
||||
|
||||
async.eachSeries(values, function (value, iteratorCallback) {
|
||||
let data = {
|
||||
type: type,
|
||||
ttl_sec: 300 // lowest
|
||||
};
|
||||
|
||||
if (type === 'MX') {
|
||||
data.priority = parseInt(value.split(' ')[0], 10);
|
||||
data.target = value.split(' ')[1];
|
||||
} else if (type === 'TXT') {
|
||||
data.target = value.replace(/^"(.*)"$/, '$1'); // strip any double quotes
|
||||
} else {
|
||||
data.target = value;
|
||||
}
|
||||
|
||||
if (i >= records.length) {
|
||||
data.name = name; // only set for new records
|
||||
|
||||
superagent.post(`${LINODE_ENDPOINT}/domains/${zoneId}/records`)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
recordIds.push(result.body.id);
|
||||
|
||||
return iteratorCallback(null);
|
||||
});
|
||||
} else {
|
||||
superagent.put(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${records[i].id}`)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.send(data)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
// increment, as we have consumed the record
|
||||
++i;
|
||||
|
||||
if (error && !error.response) return iteratorCallback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 400) return iteratorCallback(new BoxError(BoxError.BAD_FIELD, formatError(result)));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return iteratorCallback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return iteratorCallback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
recordIds.push(result.body.id);
|
||||
|
||||
return iteratorCallback(null);
|
||||
});
|
||||
}
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('upsert: completed with recordIds:%j', recordIds);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function del(domainObject, location, type, values, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert(util.isArray(values));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName,
|
||||
name = domains.getName(domainObject, location, type) || '';
|
||||
|
||||
getZoneRecords(dnsConfig, zoneName, name, type, function (error, { zoneId, records }) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (records.length === 0) return callback(null);
|
||||
|
||||
var tmp = records.filter(function (record) { return values.some(function (value) { return value === record.target; }); });
|
||||
|
||||
debug('del: %j', tmp);
|
||||
|
||||
if (tmp.length === 0) return callback(null);
|
||||
|
||||
// FIXME we only handle the first one currently
|
||||
|
||||
superagent.del(`${LINODE_ENDPOINT}/domains/${zoneId}/records/${tmp[0].id}`)
|
||||
.set('Authorization', 'Bearer ' + dnsConfig.token)
|
||||
.timeout(30 * 1000)
|
||||
.retry(5)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(null);
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.ACCESS_DENIED, formatError(result)));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, formatError(result)));
|
||||
|
||||
debug('del: done');
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function wait(domainObject, location, type, value, options, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof type, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert(options && typeof options === 'object'); // { interval: 5000, times: 50000 }
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const fqdn = domains.fqdn(location, domainObject);
|
||||
|
||||
waitForDns(fqdn, domainObject.zoneName, type, value, options, callback);
|
||||
}
|
||||
|
||||
function verifyDnsConfig(domainObject, callback) {
|
||||
assert.strictEqual(typeof domainObject, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const dnsConfig = domainObject.config,
|
||||
zoneName = domainObject.zoneName;
|
||||
|
||||
if (!dnsConfig.token || typeof dnsConfig.token !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'token must be a non-empty string', { field: 'token' }));
|
||||
|
||||
const ip = '127.0.0.1';
|
||||
|
||||
var credentials = {
|
||||
token: dnsConfig.token
|
||||
};
|
||||
|
||||
if (process.env.BOX_ENV === 'test') return callback(null, credentials); // this shouldn't be here
|
||||
|
||||
dns.resolve(zoneName, 'NS', { timeout: 5000 }, function (error, nameservers) {
|
||||
if (error && error.code === 'ENOTFOUND') return callback(new BoxError(BoxError.BAD_FIELD, 'Unable to resolve nameservers for this domain', { field: 'nameservers' }));
|
||||
if (error || !nameservers) return callback(new BoxError(BoxError.BAD_FIELD, error ? error.message : 'Unable to get nameservers', { field: 'nameservers' }));
|
||||
|
||||
if (nameservers.map(function (n) { return n.toLowerCase(); }).indexOf('ns1.linode.com') === -1) {
|
||||
debug('verifyDnsConfig: %j does not contains DO NS', nameservers);
|
||||
return callback(new BoxError(BoxError.BAD_FIELD, 'Domain nameservers are not set to Linode', { field: 'nameservers' }));
|
||||
}
|
||||
|
||||
const location = 'cloudrontestdns';
|
||||
|
||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added');
|
||||
|
||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
callback(null, credentials);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -12,6 +12,7 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
constants = require('../constants.js'),
|
||||
debug = require('debug')('box:dns/namecheap'),
|
||||
dns = require('../native-dns.js'),
|
||||
domains = require('../domains.js'),
|
||||
@@ -25,12 +26,12 @@ var assert = require('assert'),
|
||||
const ENDPOINT = 'https://api.namecheap.com/xml.response';
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function getQuery(dnsConfig, callback) {
|
||||
|
||||
+11
-2
@@ -12,6 +12,7 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
constants = require('../constants.js'),
|
||||
debug = require('debug')('box:dns/namecom'),
|
||||
dns = require('../native-dns.js'),
|
||||
domains = require('../domains.js'),
|
||||
@@ -27,12 +28,12 @@ function formatError(response) {
|
||||
}
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.token = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.token = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.token === domains.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
if (newConfig.token === constants.SECRET_PLACEHOLDER) newConfig.token = currentConfig.token;
|
||||
}
|
||||
|
||||
function addRecord(dnsConfig, zoneName, name, type, values, callback) {
|
||||
@@ -54,6 +55,10 @@ function addRecord(dnsConfig, zoneName, name, type, values, callback) {
|
||||
if (type === 'MX') {
|
||||
data.priority = parseInt(values[0].split(' ')[0], 10);
|
||||
data.answer = values[0].split(' ')[1];
|
||||
} else if (type === 'TXT') {
|
||||
// we have to strip the quoting for some odd reason for name.com! If you change that also change updateRecord
|
||||
let tmp = values[0];
|
||||
data.answer = tmp.indexOf('"') === 0 && tmp.lastIndexOf('"') === tmp.length-1 ? tmp.slice(1, tmp.length-1) : tmp;
|
||||
} else {
|
||||
data.answer = values[0];
|
||||
}
|
||||
@@ -91,6 +96,10 @@ function updateRecord(dnsConfig, zoneName, recordId, name, type, values, callbac
|
||||
if (type === 'MX') {
|
||||
data.priority = parseInt(values[0].split(' ')[0], 10);
|
||||
data.answer = values[0].split(' ')[1];
|
||||
} else if (type === 'TXT') {
|
||||
// we have to strip the quoting for some odd reason for name.com! If you change that also change addRecord
|
||||
let tmp = values[0];
|
||||
data.answer = tmp.indexOf('"') === 0 && tmp.lastIndexOf('"') === tmp.length-1 ? tmp.slice(1, tmp.length-1) : tmp;
|
||||
} else {
|
||||
data.answer = values[0];
|
||||
}
|
||||
|
||||
+6
-4
@@ -13,6 +13,7 @@ exports = module.exports = {
|
||||
var assert = require('assert'),
|
||||
AWS = require('aws-sdk'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
constants = require('../constants.js'),
|
||||
debug = require('debug')('box:dns/route53'),
|
||||
dns = require('../native-dns.js'),
|
||||
domains = require('../domains.js'),
|
||||
@@ -21,12 +22,12 @@ var assert = require('assert'),
|
||||
_ = require('underscore');
|
||||
|
||||
function removePrivateFields(domainObject) {
|
||||
domainObject.config.secretAccessKey = domains.SECRET_PLACEHOLDER;
|
||||
domainObject.config.secretAccessKey = constants.SECRET_PLACEHOLDER;
|
||||
return domainObject;
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.secretAccessKey === domains.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
|
||||
if (newConfig.secretAccessKey === constants.SECRET_PLACEHOLDER) newConfig.secretAccessKey = currentConfig.secretAccessKey;
|
||||
}
|
||||
|
||||
function getDnsCredentials(dnsConfig) {
|
||||
@@ -280,13 +281,14 @@ function verifyDnsConfig(domainObject, callback) {
|
||||
}
|
||||
|
||||
const location = 'cloudrontestdns';
|
||||
const newDomainObject = Object.assign({ }, domainObject, { config: credentials });
|
||||
|
||||
upsert(domainObject, location, 'A', [ ip ], function (error) {
|
||||
upsert(newDomainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record added');
|
||||
|
||||
del(domainObject, location, 'A', [ ip ], function (error) {
|
||||
del(newDomainObject, location, 'A', [ ip ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('verifyDnsConfig: Test A record removed again');
|
||||
|
||||
+121
-119
@@ -1,21 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
connection: connectionInstance(),
|
||||
|
||||
testRegistryConfig: testRegistryConfig,
|
||||
setRegistryConfig: setRegistryConfig,
|
||||
injectPrivateFields: injectPrivateFields,
|
||||
removePrivateFields: removePrivateFields,
|
||||
|
||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8),
|
||||
|
||||
ping: ping,
|
||||
|
||||
info: info,
|
||||
downloadImage: downloadImage,
|
||||
createContainer: createContainer,
|
||||
startContainer: startContainer,
|
||||
restartContainer: restartContainer,
|
||||
stopContainer: stopContainer,
|
||||
stopContainerByName: stopContainer,
|
||||
stopContainers: stopContainers,
|
||||
@@ -27,6 +24,7 @@ exports = module.exports = {
|
||||
getContainerIdByIp: getContainerIdByIp,
|
||||
inspect: inspect,
|
||||
inspectByName: inspect,
|
||||
execContainer: execContainer,
|
||||
getEvents: getEvents,
|
||||
memoryUsage: memoryUsage,
|
||||
createVolume: createVolume,
|
||||
@@ -34,20 +32,14 @@ exports = module.exports = {
|
||||
clearVolume: clearVolume
|
||||
};
|
||||
|
||||
// timeout is optional
|
||||
function connectionInstance(timeout) {
|
||||
var Docker = require('dockerode');
|
||||
var docker = new Docker({ socketPath: '/var/run/docker.sock', timeout: timeout });
|
||||
return docker;
|
||||
}
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
async = require('async'),
|
||||
assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
child_process = require('child_process'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:docker.js'),
|
||||
debug = require('debug')('box:docker'),
|
||||
Docker = require('dockerode'),
|
||||
path = require('path'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
@@ -58,18 +50,14 @@ var addons = require('./addons.js'),
|
||||
const CLEARVOLUME_CMD = path.join(__dirname, 'scripts/clearvolume.sh'),
|
||||
MKDIRVOLUME_CMD = path.join(__dirname, 'scripts/mkdirvolume.sh');
|
||||
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
|
||||
debug(app.fqdn + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||
}
|
||||
const DOCKER_SOCKET_PATH = '/var/run/docker.sock';
|
||||
const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH });
|
||||
|
||||
function testRegistryConfig(auth, callback) {
|
||||
assert.strictEqual(typeof auth, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
docker.checkAuth(auth, function (error /*, data */) { // this returns a 500 even for auth errors
|
||||
gConnection.checkAuth(auth, function (error /*, data */) { // this returns a 500 even for auth errors
|
||||
if (error) return callback(new BoxError(BoxError.BAD_FIELD, error, { field: 'serverAddress' }));
|
||||
|
||||
callback();
|
||||
@@ -77,13 +65,13 @@ function testRegistryConfig(auth, callback) {
|
||||
}
|
||||
|
||||
function injectPrivateFields(newConfig, currentConfig) {
|
||||
if (newConfig.password === exports.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
|
||||
if (newConfig.password === constants.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password;
|
||||
}
|
||||
|
||||
function removePrivateFields(registryConfig) {
|
||||
assert.strictEqual(typeof registryConfig, 'object');
|
||||
|
||||
if (registryConfig.password) registryConfig.password = exports.SECRET_PLACEHOLDER;
|
||||
if (registryConfig.password) registryConfig.password = constants.SECRET_PLACEHOLDER;
|
||||
|
||||
return registryConfig;
|
||||
}
|
||||
@@ -108,19 +96,21 @@ function ping(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// do not let the request linger
|
||||
var docker = connectionInstance(1000);
|
||||
const connection = new Docker({ socketPath: DOCKER_SOCKET_PATH, timeout: 1000 });
|
||||
|
||||
docker.ping(function (error, result) {
|
||||
connection.ping(function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
if (result !== 'OK') return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to ping the docker daemon'));
|
||||
if (Buffer.isBuffer(result) && result.toString('utf8') === 'OK') return callback(null); // sometimes it returns buffer
|
||||
if (result === 'OK') return callback(null);
|
||||
|
||||
callback(null);
|
||||
callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to ping the docker daemon'));
|
||||
});
|
||||
}
|
||||
|
||||
function getRegistryConfig(image, callback) {
|
||||
// https://github.com/docker/distribution/blob/release/2.7/reference/normalize.go#L62
|
||||
const parts = image.split('/');
|
||||
if (parts.length === 2) return callback(null, null); // public docker registry
|
||||
if (parts.length === 1 || (parts[0].match(/[.:]/) === null)) return callback(null, null); // public docker registry
|
||||
|
||||
settings.getRegistryConfig(function (error, registryConfig) {
|
||||
if (error) return callback(error);
|
||||
@@ -139,36 +129,35 @@ function getRegistryConfig(image, callback) {
|
||||
}
|
||||
|
||||
function pullImage(manifest, callback) {
|
||||
var docker = exports.connection;
|
||||
|
||||
getRegistryConfig(manifest.dockerImage, function (error, authConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug(`pullImage: will pull ${manifest.dockerImage}. auth: ${authConfig ? 'yes' : 'no'}`);
|
||||
|
||||
docker.pull(manifest.dockerImage, { authconfig: authConfig }, function (error, stream) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to pull image. Please check the network or if the image needs authentication. statusCode: ' + error.statusCode));
|
||||
gConnection.pull(manifest.dockerImage, { authconfig: authConfig }, function (error, stream) {
|
||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, `Unable to pull image ${manifest.dockerImage}. message: ${error.message} statusCode: ${error.statusCode}`));
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, `Unable to pull image ${manifest.dockerImage}. Please check the network or if the image needs authentication. statusCode: ${error.statusCode}`));
|
||||
|
||||
// https://github.com/dotcloud/docker/issues/1074 says each status message
|
||||
// is emitted as a chunk
|
||||
stream.on('data', function (chunk) {
|
||||
var data = safe.JSON.parse(chunk) || { };
|
||||
debug('pullImage %s: %j', manifest.id, data);
|
||||
debug('pullImage: %j', data);
|
||||
|
||||
// The data.status here is useless because this is per layer as opposed to per image
|
||||
if (!data.status && data.error) {
|
||||
debug('pullImage error %s: %s', manifest.id, data.errorDetail.message);
|
||||
debug('pullImage error %s: %s', manifest.dockerImage, data.errorDetail.message);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('end', function () {
|
||||
debug('downloaded image %s of %s successfully', manifest.dockerImage, manifest.id);
|
||||
debug('downloaded image %s', manifest.dockerImage);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
stream.on('error', function (error) {
|
||||
debug('error pulling image %s of %s: %j', manifest.dockerImage, manifest.id, error);
|
||||
debug('error pulling image %s: %j', manifest.dockerImage, error);
|
||||
|
||||
callback(new BoxError(BoxError.DOCKER_ERROR, error.message));
|
||||
});
|
||||
@@ -180,21 +169,30 @@ function downloadImage(manifest, callback) {
|
||||
assert.strictEqual(typeof manifest, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('downloadImage %s %s', manifest.id, manifest.dockerImage);
|
||||
debug('downloadImage %s', manifest.dockerImage);
|
||||
|
||||
var attempt = 1;
|
||||
|
||||
async.retry({ times: 10, interval: 15000 }, function (retryCallback) {
|
||||
debug('Downloading image %s %s. attempt: %s', manifest.id, manifest.dockerImage, attempt++);
|
||||
async.retry({ times: 10, interval: 5000, errorFilter: e => e.reason !== BoxError.NOT_FOUND }, function (retryCallback) {
|
||||
debug('Downloading image %s. attempt: %s', manifest.dockerImage, attempt++);
|
||||
|
||||
pullImage(manifest, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
retryCallback(error);
|
||||
});
|
||||
pullImage(manifest, retryCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function getBindsSync(app) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
|
||||
let binds = [];
|
||||
|
||||
for (let name of Object.keys(app.binds)) {
|
||||
const bind = app.binds[name];
|
||||
binds.push(`${bind.hostPath}:/media/${name}:${bind.readOnly ? 'ro' : 'rw'}`);
|
||||
}
|
||||
|
||||
return binds;
|
||||
}
|
||||
|
||||
function createSubcontainer(app, name, cmd, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
@@ -202,8 +200,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var docker = exports.connection,
|
||||
isAppContainer = !cmd; // non app-containers are like scheduler and exec (terminal) containers
|
||||
let isAppContainer = !cmd; // non app-containers are like scheduler and exec (terminal) containers
|
||||
|
||||
var manifest = app.manifest;
|
||||
var exposedPorts = {}, dockerPortBindings = { };
|
||||
@@ -285,6 +282,7 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
},
|
||||
HostConfig: {
|
||||
Mounts: addons.getMountsSync(app, app.manifest.addons),
|
||||
Binds: getBindsSync(app), // ideally, we have to use 'Mounts' but we have to create volumes then
|
||||
LogConfig: {
|
||||
Type: 'syslog',
|
||||
Config: {
|
||||
@@ -299,15 +297,17 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
PublishAllPorts: false,
|
||||
ReadonlyRootfs: app.debugMode ? !!app.debugMode.readonlyRootfs : true,
|
||||
RestartPolicy: {
|
||||
'Name': isAppContainer ? 'always' : 'no',
|
||||
'Name': isAppContainer ? 'unless-stopped' : 'no',
|
||||
'MaximumRetryCount': 0
|
||||
},
|
||||
CpuShares: 512, // relative to 1024 for system processes
|
||||
CpuShares: app.cpuShares,
|
||||
VolumesFrom: isAppContainer ? null : [ app.containerId + ':rw' ],
|
||||
NetworkMode: 'cloudron', // user defined bridge network
|
||||
Dns: ['172.18.0.1'], // use internal dns
|
||||
DnsSearch: ['.'], // use internal dns
|
||||
SecurityOpt: [ 'apparmor=docker-cloudron-app' ]
|
||||
SecurityOpt: [ 'apparmor=docker-cloudron-app' ],
|
||||
CapAdd: [],
|
||||
CapDrop: []
|
||||
},
|
||||
NetworkingConfig: {
|
||||
EndpointsConfig: {
|
||||
@@ -319,17 +319,15 @@ function createSubcontainer(app, name, cmd, options, callback) {
|
||||
};
|
||||
|
||||
var capabilities = manifest.capabilities || [];
|
||||
if (capabilities.includes('net_admin')) {
|
||||
containerOptions.HostConfig.CapAdd = [
|
||||
'NET_ADMIN'
|
||||
];
|
||||
}
|
||||
|
||||
// https://docs-stage.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
|
||||
if (capabilities.includes('net_admin')) containerOptions.HostConfig.CapAdd.push('NET_ADMIN', 'NET_RAW');
|
||||
if (capabilities.includes('mlock')) containerOptions.HostConfig.CapAdd.push('IPC_LOCK'); // mlock prevents swapping
|
||||
if (!capabilities.includes('ping')) containerOptions.HostConfig.CapDrop.push('NET_RAW'); // NET_RAW is included by default by Docker
|
||||
|
||||
containerOptions = _.extend(containerOptions, options);
|
||||
|
||||
debugApp(app, 'Creating container for %s', app.manifest.dockerImage);
|
||||
|
||||
docker.createContainer(containerOptions, function (error, container) {
|
||||
gConnection.createContainer(containerOptions, function (error, container) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
callback(null, container);
|
||||
@@ -345,15 +343,27 @@ function startContainer(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var docker = exports.connection;
|
||||
|
||||
var container = docker.getContainer(containerId);
|
||||
debug('Starting container %s', containerId);
|
||||
var container = gConnection.getContainer(containerId);
|
||||
|
||||
container.start(function (error) {
|
||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error && error.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, error)); // e.g start.sh is not executable
|
||||
if (error && error.statusCode !== 304) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
if (error && error.statusCode !== 304) return callback(new BoxError(BoxError.DOCKER_ERROR, error)); // 304 means already started
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function restartContainer(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = gConnection.getContainer(containerId);
|
||||
|
||||
container.restart(function (error) {
|
||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error && error.statusCode === 400) return callback(new BoxError(BoxError.BAD_FIELD, error)); // e.g start.sh is not executable
|
||||
if (error && error.statusCode !== 204) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -368,9 +378,7 @@ function stopContainer(containerId, callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var docker = exports.connection;
|
||||
var container = docker.getContainer(containerId);
|
||||
debug('Stopping container %s', containerId);
|
||||
var container = gConnection.getContainer(containerId);
|
||||
|
||||
var options = {
|
||||
t: 10 // wait for 10 seconds before killing it
|
||||
@@ -379,13 +387,9 @@ function stopContainer(containerId, callback) {
|
||||
container.stop(options, function (error) {
|
||||
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error stopping container:' + error.message));
|
||||
|
||||
debug('Waiting for container ' + containerId);
|
||||
|
||||
container.wait(function (error, data) {
|
||||
container.wait(function (error/*, data */) {
|
||||
if (error && (error.statusCode !== 304 && error.statusCode !== 404)) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error waiting on container:' + error.message));
|
||||
|
||||
debug('Container %s stopped with status code [%s]', containerId, data ? String(data.StatusCode) : '');
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
@@ -395,12 +399,9 @@ function deleteContainer(containerId, callback) {
|
||||
assert(!containerId || typeof containerId === 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('deleting container %s', containerId);
|
||||
|
||||
if (containerId === null) return callback(null);
|
||||
|
||||
var docker = exports.connection;
|
||||
var container = docker.getContainer(containerId);
|
||||
var container = gConnection.getContainer(containerId);
|
||||
|
||||
var removeOptions = {
|
||||
force: true, // kill container if it's running
|
||||
@@ -424,14 +425,10 @@ function deleteContainers(appId, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var docker = exports.connection;
|
||||
|
||||
debug('deleting containers of %s', appId);
|
||||
|
||||
let labels = [ 'appId=' + appId ];
|
||||
if (options.managedOnly) labels.push('isCloudronManaged=true');
|
||||
|
||||
docker.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
|
||||
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: labels }) }, function (error, containers) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
async.eachSeries(containers, function (container, iteratorDone) {
|
||||
@@ -444,11 +441,7 @@ function stopContainers(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var docker = exports.connection;
|
||||
|
||||
debug('stopping containers of %s', appId);
|
||||
|
||||
docker.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
|
||||
gConnection.listContainers({ all: 1, filters: JSON.stringify({ label: [ 'appId=' + appId ] }) }, function (error, containers) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
async.eachSeries(containers, function (container, iteratorDone) {
|
||||
@@ -464,8 +457,6 @@ function deleteImage(manifest, callback) {
|
||||
var dockerImage = manifest ? manifest.dockerImage : null;
|
||||
if (!dockerImage) return callback(null);
|
||||
|
||||
var docker = exports.connection;
|
||||
|
||||
var removeOptions = {
|
||||
force: false, // might be shared with another instance of this app
|
||||
noprune: false // delete untagged parents
|
||||
@@ -474,7 +465,7 @@ function deleteImage(manifest, callback) {
|
||||
// registry v1 used to pull down all *tags*. this meant that deleting image by tag was not enough (since that
|
||||
// just removes the tag). we used to remove the image by id. this is not required anymore because aliases are
|
||||
// not created anymore after https://github.com/docker/docker/pull/10571
|
||||
docker.getImage(dockerImage).remove(removeOptions, function (error) {
|
||||
gConnection.getImage(dockerImage).remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode === 400) return callback(null); // invalid image format. this can happen if user installed with a bad --docker-image
|
||||
if (error && error.statusCode === 404) return callback(null); // not found
|
||||
if (error && error.statusCode === 409) return callback(null); // another container using the image
|
||||
@@ -492,10 +483,8 @@ function getContainerIdByIp(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var docker = exports.connection;
|
||||
|
||||
docker.getNetwork('cloudron').inspect(function (error, bridge) {
|
||||
if (error && error.statusCode === 404) return callback(new Error('Unable to find the cloudron network'));
|
||||
gConnection.getNetwork('cloudron').inspect(function (error, bridge) {
|
||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Unable to find the cloudron network'));
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
var containerId;
|
||||
@@ -505,7 +494,7 @@ function getContainerIdByIp(ip, callback) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!containerId) return callback(new Error('No container with that ip'));
|
||||
if (!containerId) return callback(new BoxError(BoxError.DOCKER_ERROR, 'No container with that ip'));
|
||||
|
||||
callback(null, containerId);
|
||||
});
|
||||
@@ -515,23 +504,48 @@ function inspect(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = exports.connection.getContainer(containerId);
|
||||
var container = gConnection.getContainer(containerId);
|
||||
|
||||
container.inspect(function (error, result) {
|
||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND, `Unable to find container ${containerId}`));
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function execContainer(containerId, options, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = gConnection.getContainer(containerId);
|
||||
|
||||
container.exec(options.execOptions, function (error, exec) {
|
||||
if (error && error.statusCode === 409) return callback(new BoxError(BoxError.BAD_STATE, error.message)); // container restarting/not running
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
exec.start(options.startOptions, function(error, stream /* in hijacked mode, this is a net.socket */) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
if (options.rows && options.columns) {
|
||||
// there is a race where resizing too early results in a 404 "no such exec"
|
||||
// https://git.cloudron.io/cloudron/box/issues/549
|
||||
setTimeout(function () {
|
||||
exec.resize({ h: options.rows, w: options.columns }, function (error) { if (error) debug('Error resizing console', error); });
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
callback(null, stream);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getEvents(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
|
||||
docker.getEvents(options, function (error, stream) {
|
||||
gConnection.getEvents(options, function (error, stream) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
callback(null, stream);
|
||||
@@ -542,7 +556,7 @@ function memoryUsage(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var container = exports.connection.getContainer(containerId);
|
||||
var container = gConnection.getContainer(containerId);
|
||||
|
||||
container.stats({ stream: false }, function (error, result) {
|
||||
if (error && error.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
@@ -552,14 +566,12 @@ function memoryUsage(containerId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function createVolume(app, name, volumeDataDir, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
function createVolume(name, volumeDataDir, labels, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof volumeDataDir, 'string');
|
||||
assert.strictEqual(typeof labels, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
|
||||
const volumeOptions = {
|
||||
Name: name,
|
||||
Driver: 'local',
|
||||
@@ -568,17 +580,14 @@ function createVolume(app, name, volumeDataDir, callback) {
|
||||
device: volumeDataDir,
|
||||
o: 'bind'
|
||||
},
|
||||
Labels: {
|
||||
'fqdn': app.fqdn,
|
||||
'appId': app.id
|
||||
},
|
||||
Labels: labels
|
||||
};
|
||||
|
||||
// requires sudo because the path can be outside appsdata
|
||||
shell.sudo('createVolume', [ MKDIRVOLUME_CMD, volumeDataDir ], {}, function (error) {
|
||||
if (error) return callback(new Error(`Error creating app data dir: ${error.message}`));
|
||||
if (error) return callback(new BoxError(BoxError.FS_ERROR, `Error creating app data dir: ${error.message}`));
|
||||
|
||||
docker.createVolume(volumeOptions, function (error) {
|
||||
gConnection.createVolume(volumeOptions, function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
|
||||
callback();
|
||||
@@ -586,14 +595,12 @@ function createVolume(app, name, volumeDataDir, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function clearVolume(app, name, options, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
function clearVolume(name, options, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
let volume = docker.getVolume(name);
|
||||
let volume = gConnection.getVolume(name);
|
||||
volume.inspect(function (error, v) {
|
||||
if (error && error.statusCode === 404) return callback();
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, error));
|
||||
@@ -608,16 +615,13 @@ function clearVolume(app, name, options, callback) {
|
||||
}
|
||||
|
||||
// this only removes the volume and not the data
|
||||
function removeVolume(app, name, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
function removeVolume(name, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
|
||||
let volume = docker.getVolume(name);
|
||||
let volume = gConnection.getVolume(name);
|
||||
volume.remove(function (error) {
|
||||
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume of ${app.id} ${error.message}`));
|
||||
if (error && error.statusCode !== 404) return callback(new BoxError(BoxError.DOCKER_ERROR, `removeVolume: Error removing volume: ${error.message}`));
|
||||
|
||||
callback();
|
||||
});
|
||||
@@ -626,9 +630,7 @@ function removeVolume(app, name, callback) {
|
||||
function info(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let docker = exports.connection;
|
||||
|
||||
docker.info(function (error, result) {
|
||||
gConnection.info(function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DOCKER_ERROR, 'Error connecting to docker'));
|
||||
|
||||
callback(null, result);
|
||||
|
||||
+19
-17
@@ -23,11 +23,6 @@ var apps = require('./apps.js'),
|
||||
var gHttpServer = null;
|
||||
|
||||
function authorizeApp(req, res, next) {
|
||||
// TODO add here some authorization
|
||||
// - block apps not using the docker addon
|
||||
// - block calls regarding platform containers
|
||||
// - only allow managing and inspection of containers belonging to the app
|
||||
|
||||
// make the tests pass for now
|
||||
if (constants.TEST) {
|
||||
req.app = { id: 'testappid' };
|
||||
@@ -60,10 +55,12 @@ function attachDockerRequest(req, res, next) {
|
||||
// Force node to send out the headers, this is required for the /container/wait api to make the docker cli proceed
|
||||
res.write(' ');
|
||||
|
||||
dockerResponse.on('error', function (error) { console.error('dockerResponse error:', error); });
|
||||
dockerResponse.on('error', function (error) { debug('dockerResponse error:', error); });
|
||||
dockerResponse.pipe(res, { end: true });
|
||||
});
|
||||
|
||||
req.dockerRequest.on('error', () => {}); // abort() throws
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -74,22 +71,21 @@ function containersCreate(req, res, next) {
|
||||
safe.set(req.body, 'Labels', _.extend({ }, safe.query(req.body, 'Labels'), { appId: req.app.id, isCloudronManaged: String(false) })); // overwrite the app id to track containers of an app
|
||||
safe.set(req.body, 'HostConfig.LogConfig', { Type: 'syslog', Config: { 'tag': req.app.id, 'syslog-address': 'udp://127.0.0.1:2514', 'syslog-format': 'rfc5424' }});
|
||||
|
||||
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data'),
|
||||
dockerDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'docker');
|
||||
const appDataDir = path.join(paths.APPS_DATA_DIR, req.app.id, 'data');
|
||||
|
||||
debug('Original volume binds:', req.body.HostConfig.Binds);
|
||||
debug('Original bind mounts:', req.body.HostConfig.Binds);
|
||||
|
||||
let binds = [];
|
||||
for (let bind of (req.body.HostConfig.Binds || [])) {
|
||||
if (bind.startsWith(appDataDir)) binds.push(bind); // eclipse will inspect docker to find out the host folders and pass that to child containers
|
||||
else if (bind.startsWith('/app/data')) binds.push(bind.replace(new RegExp('^/app/data'), appDataDir));
|
||||
else binds.push(`${dockerDataDir}/${bind}`);
|
||||
if (!bind.startsWith('/app/data/')) {
|
||||
req.dockerRequest.abort();
|
||||
return next(new HttpError(400, 'Binds must be under /app/data/'));
|
||||
}
|
||||
|
||||
binds.push(bind.replace(new RegExp('^/app/data/'), appDataDir + '/'));
|
||||
}
|
||||
|
||||
// cleanup the paths from potential double slashes
|
||||
binds = binds.map(function (bind) { return bind.replace(/\/+/g, '/'); });
|
||||
|
||||
debug('Rewritten volume binds:', binds);
|
||||
debug('Rewritten bind mounts:', binds);
|
||||
safe.set(req.body, 'HostConfig.Binds', binds);
|
||||
|
||||
let plainBody = JSON.stringify(req.body);
|
||||
@@ -117,6 +113,9 @@ function start(callback) {
|
||||
assert(gHttpServer === null, 'Already started');
|
||||
|
||||
let json = middleware.json({ strict: true });
|
||||
|
||||
// we protect container create as the app/admin can otherwise mount random paths (like the ghost file)
|
||||
// protected other paths is done by preventing install/exec access of apps using docker addon
|
||||
let router = new express.Router();
|
||||
router.post('/:version/containers/create', containersCreate);
|
||||
|
||||
@@ -137,7 +136,10 @@ function start(callback) {
|
||||
.use(middleware.lastMile());
|
||||
|
||||
gHttpServer = http.createServer(proxyServer);
|
||||
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '0.0.0.0', callback);
|
||||
gHttpServer.listen(constants.DOCKER_PROXY_PORT, '172.18.0.1', callback);
|
||||
|
||||
// Overwrite the default 2min request timeout. This is required for large builds for example
|
||||
gHttpServer.setTimeout(60 * 60 * 1000);
|
||||
|
||||
debug(`startDockerProxy: started proxy on port ${constants.DOCKER_PROXY_PORT}`);
|
||||
|
||||
|
||||
+29
-14
@@ -16,7 +16,7 @@ var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
safe = require('safetydance');
|
||||
|
||||
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson', 'locked' ].join(',');
|
||||
var DOMAINS_FIELDS = [ 'domain', 'zoneName', 'provider', 'configJson', 'tlsConfigJson' ].join(',');
|
||||
|
||||
function postProcess(data) {
|
||||
data.config = safe.JSON.parse(data.configJson);
|
||||
@@ -24,8 +24,6 @@ function postProcess(data) {
|
||||
delete data.configJson;
|
||||
delete data.tlsConfigJson;
|
||||
|
||||
data.locked = !!data.locked; // make it bool
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -53,17 +51,22 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(name, domain, callback) {
|
||||
function add(name, data, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof domain, 'object');
|
||||
assert.strictEqual(typeof domain.zoneName, 'string');
|
||||
assert.strictEqual(typeof domain.provider, 'string');
|
||||
assert.strictEqual(typeof domain.config, 'object');
|
||||
assert.strictEqual(typeof domain.tlsConfig, 'object');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof data.zoneName, 'string');
|
||||
assert.strictEqual(typeof data.provider, 'string');
|
||||
assert.strictEqual(typeof data.config, 'object');
|
||||
assert.strictEqual(typeof data.tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', [ name, domain.zoneName, domain.provider, JSON.stringify(domain.config), JSON.stringify(domain.tlsConfig) ], function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
|
||||
let queries = [
|
||||
{ query: 'INSERT INTO domains (domain, zoneName, provider, configJson, tlsConfigJson) VALUES (?, ?, ?, ?, ?)', args: [ name, data.zoneName, data.provider, JSON.stringify(data.config), JSON.stringify(data.tlsConfig) ] },
|
||||
{ query: 'INSERT INTO mail (domain, dkimSelector) VALUES (?, ?)', args: [ name, data.dkimSelector || 'cloudron' ] },
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, 'Domain already exists'));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
@@ -102,10 +105,22 @@ function del(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM domains WHERE domain=?', [ domain ], function (error, result) {
|
||||
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') return callback(new BoxError(BoxError.CONFLICT));
|
||||
let queries = [
|
||||
{ query: 'DELETE FROM mail WHERE domain = ?', args: [ domain ] },
|
||||
{ query: 'DELETE FROM domains WHERE domain = ?', args: [ domain ] },
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') {
|
||||
if (error.message.indexOf('apps_mailDomain_constraint') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by an app or the mailbox of an app. Check the domains of apps and the Email section of each app.'));
|
||||
if (error.message.indexOf('subdomains') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more app(s).'));
|
||||
if (error.message.indexOf('mail') !== -1) return callback(new BoxError(BoxError.CONFLICT, 'Domain is in use by one or more mailboxes. Delete them first in the Email view.'));
|
||||
|
||||
return callback(new BoxError(BoxError.CONFLICT, error.message));
|
||||
}
|
||||
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
|
||||
if (results[1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Domain not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
+26
-15
@@ -28,9 +28,7 @@ module.exports = exports = {
|
||||
|
||||
checkDnsRecords: checkDnsRecords,
|
||||
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
|
||||
SECRET_PLACEHOLDER: String.fromCharCode(0x25CF).repeat(8)
|
||||
prepareDashboardDomain: prepareDashboardDomain
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
@@ -40,6 +38,7 @@ var assert = require('assert'),
|
||||
debug = require('debug')('box:domains'),
|
||||
domaindb = require('./domaindb.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
mail = require('./mail.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
@@ -48,18 +47,20 @@ var assert = require('assert'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
// choose which subdomain backend we use for test purpose we use route53
|
||||
function api(provider) {
|
||||
assert.strictEqual(typeof provider, 'string');
|
||||
|
||||
switch (provider) {
|
||||
case 'caas': return require('./dns/caas.js');
|
||||
case 'cloudflare': return require('./dns/cloudflare.js');
|
||||
case 'route53': return require('./dns/route53.js');
|
||||
case 'gcdns': return require('./dns/gcdns.js');
|
||||
case 'digitalocean': return require('./dns/digitalocean.js');
|
||||
case 'gandi': return require('./dns/gandi.js');
|
||||
case 'godaddy': return require('./dns/godaddy.js');
|
||||
case 'linode': return require('./dns/linode.js');
|
||||
case 'namecom': return require('./dns/namecom.js');
|
||||
case 'namecheap': return require('./dns/namecheap.js');
|
||||
case 'noop': return require('./dns/noop.js');
|
||||
@@ -86,9 +87,9 @@ function verifyDnsConfig(dnsConfig, domain, zoneName, provider, callback) {
|
||||
|
||||
const domainObject = { config: dnsConfig, domain: domain, zoneName: zoneName };
|
||||
api(provider).verifyDnsConfig(domainObject, function (error, result) {
|
||||
if (error && error.reason === BoxError.ACCESS_DENIED) return callback(new BoxError(BoxError.BAD_FIELD, 'Incorrect configuration. Access denied'));
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.BAD_FIELD, 'Zone not found'));
|
||||
if (error && error.reason === BoxError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.BAD_FIELD, 'Configuration error: ' + error.message));
|
||||
if (error && error.reason === BoxError.ACCESS_DENIED) return callback(new BoxError(BoxError.BAD_FIELD, `Access denied: ${error.message}`));
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.BAD_FIELD, `Zone not found: ${error.message}`));
|
||||
if (error && error.reason === BoxError.EXTERNAL_ERROR) return callback(new BoxError(BoxError.BAD_FIELD, `Configuration error: ${error.message}`));
|
||||
if (error) return callback(error);
|
||||
|
||||
result.hyphenatedSubdomains = !!dnsConfig.hyphenatedSubdomains;
|
||||
@@ -147,10 +148,9 @@ function validateTlsConfig(tlsConfig, dnsProvider) {
|
||||
case 'letsencrypt-prod':
|
||||
case 'letsencrypt-staging':
|
||||
case 'fallback':
|
||||
case 'caas':
|
||||
break;
|
||||
default:
|
||||
return new BoxError(BoxError.BAD_FIELD, 'tlsConfig.provider must be caas, fallback, letsencrypt-prod/staging', { field: 'tlsProvider' });
|
||||
return new BoxError(BoxError.BAD_FIELD, 'tlsConfig.provider must be fallback, letsencrypt-prod/staging', { field: 'tlsProvider' });
|
||||
}
|
||||
|
||||
if (tlsConfig.wildcard) {
|
||||
@@ -170,7 +170,7 @@ function add(domain, data, auditSource, callback) {
|
||||
assert.strictEqual(typeof data.tlsConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
|
||||
let { zoneName, provider, config, fallbackCertificate, tlsConfig, dkimSelector } = data;
|
||||
|
||||
if (!tld.isValid(domain)) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
|
||||
if (domain.endsWith('.')) return callback(new BoxError(BoxError.BAD_FIELD, 'Invalid domain', { field: 'domain' }));
|
||||
@@ -193,10 +193,12 @@ function add(domain, data, auditSource, callback) {
|
||||
let error = validateTlsConfig(tlsConfig, provider);
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!dkimSelector) dkimSelector = 'cloudron-' + settings.adminDomain().replace(/\./g, '');
|
||||
|
||||
verifyDnsConfig(config, domain, zoneName, provider, function (error, sanitizedConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
domaindb.add(domain, { zoneName: zoneName, provider: provider, config: sanitizedConfig, tlsConfig: tlsConfig }, function (error) {
|
||||
domaindb.add(domain, { zoneName, provider, config: sanitizedConfig, tlsConfig, dkimSelector }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
reverseProxy.setFallbackCertificate(domain, fallbackCertificate, function (error) {
|
||||
@@ -204,6 +206,8 @@ function add(domain, data, auditSource, callback) {
|
||||
|
||||
eventlog.add(eventlog.ACTION_DOMAIN_ADD, auditSource, { domain, zoneName, provider });
|
||||
|
||||
mail.onDomainAdded(domain, NOOP_CALLBACK);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
@@ -215,14 +219,14 @@ function get(domain, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domaindb.get(domain, function (error, result) {
|
||||
// TODO try to find subdomain entries maybe based on zoneNames or so
|
||||
if (error) return callback(error);
|
||||
|
||||
reverseProxy.getFallbackCertificate(domain, function (_, bundle) { // never returns an error
|
||||
var cert = safe.fs.readFileSync(bundle.certFilePath, 'utf-8');
|
||||
var key = safe.fs.readFileSync(bundle.keyFilePath, 'utf-8');
|
||||
|
||||
if (!cert || !key) return callback(new BoxError(BoxError.FS_ERROR, 'unable to read certificates from disk'));
|
||||
// do not error here. otherwise, there is no way to fix things up from the UI
|
||||
if (!cert || !key) debug(`Unable to read fallback certificates of ${domain} from disk`);
|
||||
|
||||
result.fallbackCertificate = { cert: cert, key: key };
|
||||
|
||||
@@ -253,6 +257,8 @@ function update(domain, data, auditSource, callback) {
|
||||
|
||||
let { zoneName, provider, config, fallbackCertificate, tlsConfig } = data;
|
||||
|
||||
if (settings.isDemo() && (domain === settings.adminDomain())) return callback(new BoxError(BoxError.CONFLICT, 'Not allowed in demo mode'));
|
||||
|
||||
domaindb.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
@@ -311,6 +317,8 @@ function del(domain, auditSource, callback) {
|
||||
|
||||
eventlog.add(eventlog.ACTION_DOMAIN_REMOVE, auditSource, { domain });
|
||||
|
||||
mail.onDomainRemoved(domain, NOOP_CALLBACK);
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
@@ -434,19 +442,22 @@ function waitForDnsRecord(location, domain, type, value, options, callback) {
|
||||
get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// linode DNS takes ~15mins
|
||||
if (!options.interval) options.interval = domainObject.provider === 'linode' ? 20000 : 5000;
|
||||
|
||||
api(domainObject.provider).wait(domainObject, location, type, value, options, callback);
|
||||
});
|
||||
}
|
||||
|
||||
// removes all fields that are strictly private and should never be returned by API calls
|
||||
function removePrivateFields(domain) {
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate', 'locked');
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'config', 'tlsConfig', 'fallbackCertificate');
|
||||
return api(result.provider).removePrivateFields(result);
|
||||
}
|
||||
|
||||
// removes all fields that are not accessible by a normal user
|
||||
function removeRestrictedFields(domain) {
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider', 'locked');
|
||||
var result = _.pick(domain, 'domain', 'zoneName', 'provider');
|
||||
|
||||
// always ensure config object
|
||||
result.config = { hyphenatedSubdomains: !!domain.config.hyphenatedSubdomains };
|
||||
|
||||
+11
-5
@@ -7,7 +7,7 @@ exports = module.exports = {
|
||||
getByCreationTime: getByCreationTime,
|
||||
cleanup: cleanup,
|
||||
|
||||
// keep in sync with webadmin index.js filter and CLI tool
|
||||
// keep in sync with webadmin index.js filter
|
||||
ACTION_ACTIVATE: 'cloudron.activate',
|
||||
ACTION_APP_CLONE: 'app.clone',
|
||||
ACTION_APP_CONFIGURE: 'app.configure',
|
||||
@@ -21,6 +21,9 @@ exports = module.exports = {
|
||||
ACTION_APP_OOM: 'app.oom',
|
||||
ACTION_APP_UP: 'app.up',
|
||||
ACTION_APP_DOWN: 'app.down',
|
||||
ACTION_APP_START: 'app.start',
|
||||
ACTION_APP_STOP: 'app.stop',
|
||||
ACTION_APP_RESTART: 'app.restart',
|
||||
|
||||
ACTION_BACKUP_FINISH: 'backup.finish',
|
||||
ACTION_BACKUP_START: 'backup.start',
|
||||
@@ -40,8 +43,10 @@ exports = module.exports = {
|
||||
ACTION_MAIL_DISABLED: 'mail.disabled',
|
||||
ACTION_MAIL_MAILBOX_ADD: 'mail.box.add',
|
||||
ACTION_MAIL_MAILBOX_REMOVE: 'mail.box.remove',
|
||||
ACTION_MAIL_MAILBOX_UPDATE: 'mail.box.update',
|
||||
ACTION_MAIL_LIST_ADD: 'mail.list.add',
|
||||
ACTION_MAIL_LIST_REMOVE: 'mail.list.remove',
|
||||
ACTION_MAIL_LIST_UPDATE: 'mail.list.update',
|
||||
|
||||
ACTION_PROVISION: 'cloudron.provision',
|
||||
ACTION_RESTORE: 'cloudron.restore', // unused
|
||||
@@ -57,6 +62,9 @@ exports = module.exports = {
|
||||
|
||||
ACTION_DYNDNS_UPDATE: 'dyndns.update',
|
||||
|
||||
ACTION_SUPPORT_TICKET: 'support.ticket',
|
||||
ACTION_SUPPORT_SSH: 'support.ssh',
|
||||
|
||||
ACTION_PROCESS_CRASH: 'system.crash'
|
||||
};
|
||||
|
||||
@@ -82,11 +90,9 @@ function add(action, source, data, callback) {
|
||||
api(uuid.v4(), action, source, data, function (error, id) {
|
||||
if (error) return callback(error);
|
||||
|
||||
notifications.onEvent(id, action, source, data, function (error) {
|
||||
if (error) return callback(error);
|
||||
callback(null, { id: id });
|
||||
|
||||
callback(null, { id: id });
|
||||
});
|
||||
notifications.onEvent(id, action, source, data, NOOP_CALLBACK);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+401
-69
@@ -1,7 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
search: search,
|
||||
verifyPassword: verifyPassword,
|
||||
createAndVerifyUserIfNotExist: createAndVerifyUserIfNotExist,
|
||||
|
||||
testConfig: testConfig,
|
||||
startSyncer: startSyncer,
|
||||
@@ -18,7 +20,9 @@ var assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
constants = require('./constants.js'),
|
||||
debug = require('debug')('box:externalldap'),
|
||||
groups = require('./groups.js'),
|
||||
ldap = require('ldapjs'),
|
||||
once = require('once'),
|
||||
settings = require('./settings.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
users = require('./users.js');
|
||||
@@ -33,39 +37,115 @@ function removePrivateFields(ldapConfig) {
|
||||
return ldapConfig;
|
||||
}
|
||||
|
||||
function translateUser(ldapConfig, ldapUser) {
|
||||
assert.strictEqual(typeof ldapConfig, 'object');
|
||||
|
||||
return {
|
||||
username: ldapUser[ldapConfig.usernameField],
|
||||
email: ldapUser.mail || ldapUser.mailPrimaryAddress,
|
||||
displayName: ldapUser.cn // user.giveName + ' ' + user.sn
|
||||
};
|
||||
}
|
||||
|
||||
function validUserRequirements(user) {
|
||||
if (!user.username || !user.email || !user.displayName) {
|
||||
debug(`[Invalid LDAP user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// performs service bind if required
|
||||
function getClient(externalLdapConfig, callback) {
|
||||
function getClient(externalLdapConfig, doBindAuth, callback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof doBindAuth, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// ensure we only callback once since we also have to listen to client.error events
|
||||
callback = once(callback);
|
||||
|
||||
// basic validation to not crash
|
||||
try { ldap.parseDN(externalLdapConfig.baseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid baseDn')); }
|
||||
try { ldap.parseFilter(externalLdapConfig.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
|
||||
|
||||
var config = {
|
||||
url: externalLdapConfig.url,
|
||||
tlsOptions: {
|
||||
rejectUnauthorized: externalLdapConfig.acceptSelfSignedCerts ? false : true
|
||||
}
|
||||
};
|
||||
|
||||
var client;
|
||||
try {
|
||||
client = ldap.createClient({ url: externalLdapConfig.url });
|
||||
client = ldap.createClient(config);
|
||||
} catch (e) {
|
||||
if (e instanceof ldap.ProtocolError) return callback(new BoxError(BoxError.BAD_FIELD, 'url protocol is invalid'));
|
||||
return callback(new BoxError(BoxError.INTERNAL_ERROR, e));
|
||||
}
|
||||
|
||||
if (!externalLdapConfig.bindDn) return callback(null, client);
|
||||
// ensure we don't just crash
|
||||
client.on('error', function (error) {
|
||||
callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
});
|
||||
|
||||
// skip bind auth if none exist or if not wanted
|
||||
if (!externalLdapConfig.bindDn || !doBindAuth) return callback(null, client);
|
||||
|
||||
client.bind(externalLdapConfig.bindDn, externalLdapConfig.bindPassword, function (error) {
|
||||
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback(null, client, externalLdapConfig);
|
||||
callback(null, client);
|
||||
});
|
||||
}
|
||||
|
||||
function ldapSearch(externalLdapConfig, options, callback) {
|
||||
function ldapGetByDN(externalLdapConfig, dn, callback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof dn, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getClient(externalLdapConfig, true, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let searchOptions = {
|
||||
paged: true,
|
||||
scope: 'sub' // We may have to make this configurable
|
||||
};
|
||||
|
||||
debug(`Get object at ${dn}`);
|
||||
|
||||
// basic validation to not crash
|
||||
try { ldap.parseDN(dn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid DN')); }
|
||||
|
||||
client.search(dn, searchOptions, function (error, result) {
|
||||
if (error instanceof ldap.NoSuchObjectError) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
|
||||
let ldapObjects = [];
|
||||
|
||||
result.on('searchEntry', entry => ldapObjects.push(entry.object));
|
||||
result.on('error', error => callback(new BoxError(BoxError.EXTERNAL_ERROR, error)));
|
||||
|
||||
result.on('end', function (result) {
|
||||
client.unbind();
|
||||
|
||||
if (result.status !== 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
|
||||
if (ldapObjects.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
|
||||
callback(null, ldapObjects[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO support search by email
|
||||
function ldapUserSearch(externalLdapConfig, options, callback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getClient(externalLdapConfig, function (error, client) {
|
||||
getClient(externalLdapConfig, true, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let searchOptions = {
|
||||
@@ -101,6 +181,48 @@ function ldapSearch(externalLdapConfig, options, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function ldapGroupSearch(externalLdapConfig, options, callback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getClient(externalLdapConfig, true, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let searchOptions = {
|
||||
paged: true,
|
||||
scope: 'sub' // We may have to make this configurable
|
||||
};
|
||||
|
||||
if (externalLdapConfig.groupFilter) searchOptions.filter = ldap.parseFilter(externalLdapConfig.groupFilter);
|
||||
|
||||
if (options.filter) { // https://github.com/ldapjs/node-ldapjs/blob/master/docs/filters.md
|
||||
let extraFilter = ldap.parseFilter(options.filter);
|
||||
searchOptions.filter = new ldap.AndFilter({ filters: [ extraFilter, searchOptions.filter ] });
|
||||
}
|
||||
|
||||
debug(`Listing groups at ${externalLdapConfig.groupBaseDn} with filter ${searchOptions.filter.toString()}`);
|
||||
|
||||
client.search(externalLdapConfig.groupBaseDn, searchOptions, function (error, result) {
|
||||
if (error instanceof ldap.NoSuchObjectError) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
|
||||
let ldapGroups = [];
|
||||
|
||||
result.on('searchEntry', entry => ldapGroups.push(entry.object));
|
||||
result.on('error', error => callback(new BoxError(BoxError.EXTERNAL_ERROR, error)));
|
||||
|
||||
result.on('end', function (result) {
|
||||
client.unbind();
|
||||
|
||||
if (result.status !== 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Server returned status ' + result.status));
|
||||
|
||||
callback(null, ldapGroups);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function testConfig(config, callback) {
|
||||
assert.strictEqual(typeof config, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -118,7 +240,20 @@ function testConfig(config, callback) {
|
||||
if (!config.filter) return callback(new BoxError(BoxError.BAD_FIELD, 'filter must not be empty'));
|
||||
try { ldap.parseFilter(config.filter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid filter')); }
|
||||
|
||||
getClient(config, function (error, client) {
|
||||
if ('syncGroups' in config && typeof config.syncGroups !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'syncGroups must be a boolean'));
|
||||
if ('acceptSelfSignedCerts' in config && typeof config.acceptSelfSignedCerts !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'acceptSelfSignedCerts must be a boolean'));
|
||||
|
||||
if (config.syncGroups) {
|
||||
if (!config.groupBaseDn) return callback(new BoxError(BoxError.BAD_FIELD, 'groupBaseDn must not be empty'));
|
||||
try { ldap.parseDN(config.groupBaseDn); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid groupBaseDn')); }
|
||||
|
||||
if (!config.groupFilter) return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
|
||||
try { ldap.parseFilter(config.groupFilter); } catch (e) { return callback(new BoxError(BoxError.BAD_FIELD, 'invalid groupFilter')); }
|
||||
|
||||
if (!config.groupnameField || typeof config.groupnameField !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'groupFilter must not be empty'));
|
||||
}
|
||||
|
||||
getClient(config, true, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var opts = {
|
||||
@@ -136,6 +271,58 @@ function testConfig(config, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function search(identifier, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
|
||||
if (error) return callback(error);
|
||||
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||
|
||||
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// translate ldap properties to ours
|
||||
let users = ldapUsers.map(function (u) { return translateUser(externalLdapConfig, u); });
|
||||
|
||||
callback(null, users);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createAndVerifyUserIfNotExist(identifier, password, callback) {
|
||||
assert.strictEqual(typeof identifier, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getExternalLdapConfig(function (error, externalLdapConfig) {
|
||||
if (error) return callback(error);
|
||||
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||
if (!externalLdapConfig.autoCreate) return callback(new BoxError(BoxError.BAD_STATE, 'auto create not enabled'));
|
||||
|
||||
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${identifier}` }, function (error, ldapUsers) {
|
||||
if (error) return callback(error);
|
||||
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
|
||||
|
||||
let user = translateUser(externalLdapConfig, ldapUsers[0]);
|
||||
if (!validUserRequirements(user)) return callback(new BoxError(BoxError.BAD_FIELD));
|
||||
|
||||
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_AUTO_CREATE, function (error, user) {
|
||||
if (error) {
|
||||
debug(`createAndVerifyUserIfNotExist: Failed to auto create user ${user.username}`, error);
|
||||
return callback(new BoxError(BoxError.INTERNAL_ERROR));
|
||||
}
|
||||
|
||||
verifyPassword(user, password, function (error) {
|
||||
if (error) return callback(error);
|
||||
callback(null, user);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function verifyPassword(user, password, callback) {
|
||||
assert.strictEqual(typeof user, 'object');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
@@ -145,19 +332,20 @@ function verifyPassword(user, password, callback) {
|
||||
if (error) return callback(error);
|
||||
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||
|
||||
ldapSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) {
|
||||
ldapUserSearch(externalLdapConfig, { filter: `${externalLdapConfig.usernameField}=${user.username}` }, function (error, ldapUsers) {
|
||||
if (error) return callback(error);
|
||||
if (ldapUsers.length === 0) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (ldapUsers.length > 1) return callback(new BoxError(BoxError.CONFLICT));
|
||||
|
||||
const userDn = ldapUsers[0].dn;
|
||||
let client = ldap.createClient({ url: externalLdapConfig.url });
|
||||
getClient(externalLdapConfig, false, function (error, client) {
|
||||
if (error) return callback(error);
|
||||
|
||||
client.bind(userDn, password, function (error) {
|
||||
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
client.bind(ldapUsers[0].dn, password, function (error) {
|
||||
if (error instanceof ldap.InvalidCredentialsError) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (error) return callback(new BoxError(BoxError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
callback(null, translateUser(externalLdapConfig, ldapUsers[0]));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -182,6 +370,197 @@ function startSyncer(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function syncUsers(externalLdapConfig, progressCallback, callback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
ldapUserSearch(externalLdapConfig, {}, function (error, ldapUsers) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug(`Found ${ldapUsers.length} users`);
|
||||
|
||||
let percent = 10;
|
||||
let step = 30/(ldapUsers.length+1); // ensure no divide by 0
|
||||
|
||||
// we ignore all errors here and just log them for now
|
||||
async.eachSeries(ldapUsers, function (user, iteratorCallback) {
|
||||
user = translateUser(externalLdapConfig, user);
|
||||
|
||||
if (!validUserRequirements(user)) return iteratorCallback();
|
||||
|
||||
percent += step;
|
||||
progressCallback({ percent, message: `Syncing... ${user.username}` });
|
||||
|
||||
users.getByUsername(user.username, function (error, result) {
|
||||
if (error && error.reason !== BoxError.NOT_FOUND) return iteratorCallback(error);
|
||||
|
||||
if (!result) {
|
||||
debug(`[adding user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
||||
|
||||
users.create(user.username, null /* password */, user.email, user.displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
|
||||
if (error) debug('syncUsers: Failed to create user', user, error.message);
|
||||
iteratorCallback();
|
||||
});
|
||||
} else if (result.source !== 'ldap') {
|
||||
debug(`[conflicting user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
||||
|
||||
iteratorCallback();
|
||||
} else if (result.email !== user.email || result.displayName !== user.displayName) {
|
||||
debug(`[updating user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
||||
|
||||
users.update(result, { email: user.email, fallbackEmail: user.email, displayName: user.displayName }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
|
||||
if (error) debug('Failed to update user', user, error);
|
||||
|
||||
iteratorCallback();
|
||||
});
|
||||
} else {
|
||||
// user known and up-to-date
|
||||
debug(`[up-to-date user] username=${user.username} email=${user.email} displayName=${user.displayName}`);
|
||||
|
||||
iteratorCallback();
|
||||
}
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function syncGroups(externalLdapConfig, progressCallback, callback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!externalLdapConfig.syncGroups) {
|
||||
debug('Group sync is disabled');
|
||||
progressCallback({ percent: 70, message: 'Skipping group sync...' });
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
ldapGroupSearch(externalLdapConfig, {}, function (error, ldapGroups) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug(`Found ${ldapGroups.length} groups`);
|
||||
|
||||
let percent = 40;
|
||||
let step = 30/(ldapGroups.length+1); // ensure no divide by 0
|
||||
|
||||
// we ignore all non internal errors here and just log them for now
|
||||
async.eachSeries(ldapGroups, function (ldapGroup, iteratorCallback) {
|
||||
var groupName = ldapGroup[externalLdapConfig.groupnameField];
|
||||
if (!groupName) return iteratorCallback();
|
||||
// some servers return empty array for unknown properties :-/
|
||||
if (typeof groupName !== 'string') return iteratorCallback();
|
||||
|
||||
// groups are lowercase
|
||||
groupName = groupName.toLowerCase();
|
||||
|
||||
percent += step;
|
||||
progressCallback({ percent, message: `Syncing... ${groupName}` });
|
||||
|
||||
groups.getByName(groupName, function (error, result) {
|
||||
if (error && error.reason !== BoxError.NOT_FOUND) return iteratorCallback(error);
|
||||
|
||||
if (!result) {
|
||||
debug(`[adding group] groupname=${groupName}`);
|
||||
|
||||
groups.create(groupName, 'ldap', function (error) {
|
||||
if (error) debug('syncGroups: Failed to create group', groupName, error);
|
||||
iteratorCallback();
|
||||
});
|
||||
} else {
|
||||
debug(`[up-to-date group] groupname=${groupName}`);
|
||||
|
||||
iteratorCallback();
|
||||
}
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('sync: ldap sync is done', error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncGroupUsers(externalLdapConfig, progressCallback, callback) {
|
||||
assert.strictEqual(typeof externalLdapConfig, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!externalLdapConfig.syncGroups) {
|
||||
debug('Group users sync is disabled');
|
||||
progressCallback({ percent: 99, message: 'Skipping group users sync...' });
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
groups.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var ldapGroups = result.filter(function (g) { return g.source === 'ldap'; });
|
||||
debug(`Found ${ldapGroups.length} groups to sync users`);
|
||||
|
||||
async.eachSeries(ldapGroups, function (group, iteratorCallback) {
|
||||
debug(`Sync users for group ${group.name}`);
|
||||
|
||||
ldapGroupSearch(externalLdapConfig, {}, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
if (!result || result.length === 0) {
|
||||
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
|
||||
return callback();
|
||||
}
|
||||
|
||||
// since our group names are lowercase we cannot use potentially case matching ldap filters
|
||||
let found = result.find(function (r) {
|
||||
if (!r[externalLdapConfig.groupnameField]) return false;
|
||||
return r[externalLdapConfig.groupnameField].toLowerCase() === group.name;
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
debug(`syncGroupUsers: Unable to find group ${group.name} ignoring for now.`);
|
||||
return callback();
|
||||
}
|
||||
|
||||
var ldapGroupMembers = found.member || found.uniqueMember || [];
|
||||
|
||||
// if only one entry is in the group ldap returns a string, not an array!
|
||||
if (typeof ldapGroupMembers === 'string') ldapGroupMembers = [ ldapGroupMembers ];
|
||||
|
||||
debug(`Group ${group.name} has ${ldapGroupMembers.length} members.`);
|
||||
|
||||
async.eachSeries(ldapGroupMembers, function (memberDn, iteratorCallback) {
|
||||
ldapGetByDN(externalLdapConfig, memberDn, function (error, result) {
|
||||
if (error) {
|
||||
debug(`Failed to get ${memberDn}:`, error);
|
||||
return iteratorCallback();
|
||||
}
|
||||
|
||||
debug(`Found member object at ${memberDn} adding to group ${group.name}`);
|
||||
|
||||
const username = result[externalLdapConfig.usernameField];
|
||||
if (!username) return iteratorCallback();
|
||||
|
||||
users.getByUsername(username, function (error, result) {
|
||||
if (error) {
|
||||
debug(`syncGroupUsers: Failed to get user by username ${username}`, error);
|
||||
return iteratorCallback();
|
||||
}
|
||||
|
||||
groups.addMember(group.id, result.id, function (error) {
|
||||
if (error && error.reason !== BoxError.ALREADY_EXISTS) debug('syncGroupUsers: Failed to add member', error);
|
||||
iteratorCallback();
|
||||
});
|
||||
});
|
||||
});
|
||||
}, function (error) {
|
||||
if (error) debug('syncGroupUsers: ', error);
|
||||
iteratorCallback();
|
||||
});
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function sync(progressCallback, callback) {
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -192,65 +571,18 @@ function sync(progressCallback, callback) {
|
||||
if (error) return callback(error);
|
||||
if (externalLdapConfig.provider === 'noop') return callback(new BoxError(BoxError.BAD_STATE, 'not enabled'));
|
||||
|
||||
ldapSearch(externalLdapConfig, {}, function (error, ldapUsers) {
|
||||
async.series([
|
||||
syncUsers.bind(null, externalLdapConfig, progressCallback),
|
||||
syncGroups.bind(null, externalLdapConfig, progressCallback),
|
||||
syncGroupUsers.bind(null, externalLdapConfig, progressCallback)
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug(`Found ${ldapUsers.length} users`);
|
||||
let percent = 10;
|
||||
let step = 90/(ldapUsers.length+1); // ensure no divide by 0
|
||||
progressCallback({ percent: 100, message: 'Done' });
|
||||
|
||||
// we ignore all errors here and just log them for now
|
||||
async.eachSeries(ldapUsers, function (user, iteratorCallback) {
|
||||
const delayedCallback = (error) => setTimeout(iteratorCallback, 40000);
|
||||
debug('sync: ldap sync is done', error);
|
||||
|
||||
const username = user[externalLdapConfig.usernameField];
|
||||
const email = user.mail;
|
||||
const displayName = user.cn; // user.giveName + ' ' + user.sn
|
||||
|
||||
if (!username || !email || !displayName) {
|
||||
debug(`[empty username/email/displayName] username=${username} email=${email} displayName=${displayName} usernameField=${externalLdapConfig.usernameField}`);
|
||||
return delayedCallback();
|
||||
}
|
||||
|
||||
percent += step;
|
||||
progressCallback({ percent, message: `Syncing... ${username}` });
|
||||
|
||||
users.getByUsername(username, function (error, result) {
|
||||
if (error && error.reason !== BoxError.NOT_FOUND) {
|
||||
debug(`Could not find user with username ${username}: ${error.message}`);
|
||||
return delayedCallback();
|
||||
}
|
||||
|
||||
if (error) {
|
||||
debug(`[adding user] username=${username} email=${email} displayName=${displayName}`);
|
||||
|
||||
users.create(username, null /* password */, email, displayName, { source: 'ldap' }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
|
||||
if (error) console.error('Failed to create user', user, error);
|
||||
delayedCallback();
|
||||
});
|
||||
} else if (result.source !== 'ldap') {
|
||||
debug(`[conflicting user] username=${username} email=${email} displayName=${displayName}`);
|
||||
|
||||
delayedCallback();
|
||||
} else if (result.email !== email || result.displayName !== displayName) {
|
||||
debug(`[updating user] username=${username} email=${email} displayName=${displayName}`);
|
||||
|
||||
users.update(result.id, { email: email, fallbackEmail: email, displayName: displayName }, auditSource.EXTERNAL_LDAP_TASK, function (error) {
|
||||
if (error) debug('Failed to update user', user, error);
|
||||
|
||||
delayedCallback();
|
||||
});
|
||||
} else {
|
||||
// user known and up-to-date
|
||||
debug(`[up-to-date user] username=${username} email=${email} displayName=${displayName}`);
|
||||
|
||||
delayedCallback();
|
||||
}
|
||||
});
|
||||
}, function (error) {
|
||||
debug('sync: ldap sync is done', error);
|
||||
callback(error);
|
||||
});
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+7
-2
@@ -5,6 +5,7 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
infra = require('./infra_version.js'),
|
||||
paths = require('./paths.js'),
|
||||
shell = require('./shell.js');
|
||||
@@ -26,7 +27,7 @@ function startGraphite(existingInfra, callback) {
|
||||
--log-opt syslog-address=udp://127.0.0.1:2514 \
|
||||
--log-opt syslog-format=rfc5424 \
|
||||
--log-opt tag=graphite \
|
||||
-m 75m \
|
||||
-m 150m \
|
||||
--memory-swap 150m \
|
||||
--dns 172.18.0.1 \
|
||||
--dns-search=. \
|
||||
@@ -37,5 +38,9 @@ function startGraphite(existingInfra, callback) {
|
||||
--label isCloudronManaged=true \
|
||||
--read-only -v /tmp -v /run "${tag}"`;
|
||||
|
||||
shell.exec('startGraphite', cmd, callback);
|
||||
async.series([
|
||||
shell.exec.bind(null, 'stopGraphite', 'docker stop graphite || true'),
|
||||
shell.exec.bind(null, 'removeGraphite', 'docker rm -f graphite || true'),
|
||||
shell.exec.bind(null, 'startGraphite', cmd)
|
||||
], callback);
|
||||
}
|
||||
|
||||
+19
-20
@@ -2,6 +2,7 @@
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getByName: getByName,
|
||||
getWithMembers: getWithMembers,
|
||||
getAll: getAll,
|
||||
getAllWithMembers: getAllWithMembers,
|
||||
@@ -19,8 +20,6 @@ exports = module.exports = {
|
||||
getMembership: getMembership,
|
||||
setMembership: setMembership,
|
||||
|
||||
getGroups: getGroups,
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
@@ -28,7 +27,7 @@ var assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
database = require('./database.js');
|
||||
|
||||
var GROUPS_FIELDS = [ 'id', 'name' ].join(',');
|
||||
var GROUPS_FIELDS = [ 'id', 'name', 'source' ].join(',');
|
||||
|
||||
function get(groupId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
@@ -42,6 +41,18 @@ function get(groupId, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByName(name, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups WHERE name = ?', [ name ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getWithMembers(groupId, callback) {
|
||||
assert.strictEqual(typeof groupId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -63,7 +74,7 @@ function getWithMembers(groupId, callback) {
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups', function (error, results) {
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' FROM userGroups ORDER BY name', function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
@@ -73,9 +84,8 @@ function getAll(callback) {
|
||||
function getAllWithMembers(callback) {
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ',GROUP_CONCAT(groupMembers.userId) AS userIds ' +
|
||||
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
|
||||
' GROUP BY userGroups.id', function (error, results) {
|
||||
' GROUP BY userGroups.id ORDER BY name', function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Group not found'));
|
||||
|
||||
results.forEach(function (result) { result.userIds = result.userIds ? result.userIds.split(',') : [ ]; });
|
||||
|
||||
@@ -83,12 +93,13 @@ function getAllWithMembers(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, name, callback) {
|
||||
function add(id, name, source, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof source, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO userGroups (id, name) VALUES (?, ?)', [ id, name ], function (error, result) {
|
||||
database.query('INSERT INTO userGroups (id, name, source) VALUES (?, ?, ?)', [ id, name, source ], function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error));
|
||||
if (error || result.affectedRows !== 1) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
@@ -260,15 +271,3 @@ function isMember(groupId, userId, callback) {
|
||||
callback(null, result.length !== 0);
|
||||
});
|
||||
}
|
||||
|
||||
function getGroups(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + GROUPS_FIELDS + ' ' +
|
||||
' FROM userGroups INNER JOIN groupMembers ON userGroups.id = groupMembers.groupId AND groupMembers.userId = ?', [ userId ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
+26
-15
@@ -4,6 +4,7 @@ exports = module.exports = {
|
||||
create: create,
|
||||
remove: remove,
|
||||
get: get,
|
||||
getByName: getByName,
|
||||
update: update,
|
||||
getWithMembers: getWithMembers,
|
||||
getAll: getAll,
|
||||
@@ -15,8 +16,6 @@ exports = module.exports = {
|
||||
removeMember: removeMember,
|
||||
isMember: isMember,
|
||||
|
||||
getGroups: getGroups,
|
||||
|
||||
setMembership: setMembership,
|
||||
getMembership: getMembership,
|
||||
|
||||
@@ -45,8 +44,17 @@ function validateGroupname(name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function create(name, callback) {
|
||||
function validateGroupSource(source) {
|
||||
assert.strictEqual(typeof source, 'string');
|
||||
|
||||
if (source !== '' && source !== 'ldap') return new BoxError(BoxError.BAD_FIELD, 'source must be "" or "ldap"', { field: source });
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function create(name, source, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof source, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// we store names in lowercase
|
||||
@@ -55,8 +63,11 @@ function create(name, callback) {
|
||||
var error = validateGroupname(name);
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateGroupSource(source);
|
||||
if (error) return callback(error);
|
||||
|
||||
var id = 'gid-' + uuid.v4();
|
||||
groupdb.add(id, name, function (error) {
|
||||
groupdb.add(id, name, source, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, { id: id, name: name });
|
||||
@@ -85,6 +96,17 @@ function get(id, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getByName(name, callback) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getByName(name, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
return callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function getWithMembers(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -217,17 +239,6 @@ function update(groupId, data, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getGroups(userId, callback) {
|
||||
assert.strictEqual(typeof userId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
groupdb.getGroups(userId, function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function count(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user