Compare commits
1336 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4aae663b2e | ||
|
|
da00bce4b7 | ||
|
|
0067766284 | ||
|
|
bb0b5550e0 | ||
|
|
1db1f3faf4 | ||
|
|
9650a55c85 | ||
|
|
9451bcd38b | ||
|
|
aa7dbdd1fa | ||
|
|
ac18fb47b4 | ||
|
|
91a229305d | ||
|
|
70b0da9e38 | ||
|
|
4275114d28 | ||
|
|
83872a0a1d | ||
|
|
4d4aad084c | ||
|
|
8994a12117 | ||
|
|
28b6a340f0 | ||
|
|
1724607433 | ||
|
|
39864fbbb9 | ||
|
|
94dcec9df1 | ||
|
|
10ca889de0 | ||
|
|
cfcc210f9c | ||
|
|
38e5d2286e | ||
|
|
149e176cfd | ||
|
|
3a19ab6866 | ||
|
|
aa71a734b9 | ||
|
|
d81ee7d99a | ||
|
|
2946657889 | ||
|
|
fc6f91157d | ||
|
|
315d721174 | ||
|
|
ed7f2e7bb5 | ||
|
|
53cb9b1f7a | ||
|
|
cccdf68cec | ||
|
|
f04654022a | ||
|
|
2b92310d24 | ||
|
|
c21155f07b | ||
|
|
baded52c96 | ||
|
|
476f348693 | ||
|
|
dd58c174a8 | ||
|
|
376e070b72 | ||
|
|
f0e0372127 | ||
|
|
5e2c655ccb | ||
|
|
4a158c559e | ||
|
|
03a59cd500 | ||
|
|
b71ab187ff | ||
|
|
bbed7c1d8a | ||
|
|
c496d994c0 | ||
|
|
7a6a170451 | ||
|
|
5a6b261ba2 | ||
|
|
70fbcf8ce4 | ||
|
|
93712c0f03 | ||
|
|
e78abe2fab | ||
|
|
e190076f1a | ||
|
|
4a85207dba | ||
|
|
b0e80de9ec | ||
|
|
a546914796 | ||
|
|
3af6012779 | ||
|
|
5b51f73be4 | ||
|
|
d74537868a | ||
|
|
2056ede942 | ||
|
|
f2d366c35d | ||
|
|
0bb2da8a04 | ||
|
|
38607048ee | ||
|
|
9c413ffe3d | ||
|
|
14e1cb5ad6 | ||
|
|
aaf93cb772 | ||
|
|
8f08c52103 | ||
|
|
9ccd82ce4e | ||
|
|
013669e872 | ||
|
|
9ebdeca3ad | ||
|
|
8823487bc1 | ||
|
|
c4dffa393b | ||
|
|
a5c4b5d8a1 | ||
|
|
2f58092af2 | ||
|
|
1f7877e0e5 | ||
|
|
a304c7f4a5 | ||
|
|
601fc9a202 | ||
|
|
32e00bdf47 | ||
|
|
83fa83a709 | ||
|
|
895ccdb549 | ||
|
|
fd8741be16 | ||
|
|
3206afcd7c | ||
|
|
ab2d246945 | ||
|
|
41ec22e8c3 | ||
|
|
af54142997 | ||
|
|
c8c4f99849 | ||
|
|
48c52533c4 | ||
|
|
1a98d6d2bd | ||
|
|
615198cd36 | ||
|
|
664b3ab958 | ||
|
|
dac677df06 | ||
|
|
fd2087d7e4 | ||
|
|
d5087ff0c2 | ||
|
|
1d0ad3cb47 | ||
|
|
30c3acaed9 | ||
|
|
afd938abdf | ||
|
|
38ca8926af | ||
|
|
283f1aac21 | ||
|
|
8ba1f3914c | ||
|
|
a262b08887 | ||
|
|
925408ffcd | ||
|
|
04d4375297 | ||
|
|
691b15363a | ||
|
|
caadb1d418 | ||
|
|
382ae7424d | ||
|
|
6073d2ba7e | ||
|
|
6ecbd4a0fd | ||
|
|
92c43e58c7 | ||
|
|
dc91abb800 | ||
|
|
e19ab45e81 | ||
|
|
72daaa9ff0 | ||
|
|
8106fa3b7d | ||
|
|
282040ed1b | ||
|
|
bcd04715c0 | ||
|
|
14b2fa55c3 | ||
|
|
04e103a32d | ||
|
|
0b0c02e421 | ||
|
|
196a5cfb42 | ||
|
|
fc408b8288 | ||
|
|
e2c342f242 | ||
|
|
19fcabd32b | ||
|
|
a842d77b6d | ||
|
|
ef68cb70c0 | ||
|
|
adfb506af4 | ||
|
|
1d188297f9 | ||
|
|
141a32315f | ||
|
|
8f7b224846 | ||
|
|
4610e05ca1 | ||
|
|
cc4407a438 | ||
|
|
5d9568eb91 | ||
|
|
a9f52ba305 | ||
|
|
9f9575f46a | ||
|
|
47a598a494 | ||
|
|
d294dea84d | ||
|
|
304fe45ee8 | ||
|
|
0edb673dc6 | ||
|
|
cd1b46848e | ||
|
|
6bd87485c6 | ||
|
|
d5952fafc3 | ||
|
|
7660e90d51 | ||
|
|
4d482d11ee | ||
|
|
a14dbbe77a | ||
|
|
0d535d2d5c | ||
|
|
7b24239d38 | ||
|
|
10d7c47576 | ||
|
|
025eb18411 | ||
|
|
24db6630ee | ||
|
|
0930683366 | ||
|
|
67bdf47ef6 | ||
|
|
de869b90ee | ||
|
|
9e2f52caef | ||
|
|
b06432824c | ||
|
|
07642f0c56 | ||
|
|
f17899d804 | ||
|
|
88cd857f97 | ||
|
|
195fb198dd | ||
|
|
ad2219dd43 | ||
|
|
55eb999821 | ||
|
|
aedc8e8087 | ||
|
|
de7d27cd08 | ||
|
|
e4c7985e10 | ||
|
|
fbcfa647ef | ||
|
|
953c65788c | ||
|
|
b6473bc8f0 | ||
|
|
a5cdd6087a | ||
|
|
24ffe5ec26 | ||
|
|
c2f8da5507 | ||
|
|
dbf3d3abd7 | ||
|
|
9ee4692215 | ||
|
|
126f5e761b | ||
|
|
6874792670 | ||
|
|
6b3b4eb8b3 | ||
|
|
d67598ab7e | ||
|
|
d8fd6be832 | ||
|
|
a5dc65bda7 | ||
|
|
6c8be9a47a | ||
|
|
1a5fc894d6 | ||
|
|
7f324793b5 | ||
|
|
0735353ab4 | ||
|
|
6ff2c5f757 | ||
|
|
29ab352846 | ||
|
|
4a6f36bc0e | ||
|
|
0ef0c77305 | ||
|
|
05c331172a | ||
|
|
2414b44b6d | ||
|
|
ca53449141 | ||
|
|
9342b2f0e3 | ||
|
|
d15aa68bd7 | ||
|
|
624e34d02d | ||
|
|
af683b5fa4 | ||
|
|
f9c6c0102e | ||
|
|
f71fbce249 | ||
|
|
a184012205 | ||
|
|
3bf50af09a | ||
|
|
29c513df78 | ||
|
|
d2e03c009a | ||
|
|
a541c0e048 | ||
|
|
ead832ac73 | ||
|
|
370485eee6 | ||
|
|
f3165c4e3b | ||
|
|
a8187216af | ||
|
|
cf79e7f1ec | ||
|
|
353369c1e9 | ||
|
|
6507d95b98 | ||
|
|
294413b798 | ||
|
|
51fd959e9d | ||
|
|
8ddc72704e | ||
|
|
d1f9ae3df8 | ||
|
|
28dee54a39 | ||
|
|
ff5702efc3 | ||
|
|
663e0952fc | ||
|
|
8a17e13ec4 | ||
|
|
a8436f8784 | ||
|
|
93313abf33 | ||
|
|
246956fd0e | ||
|
|
b2fe43184c | ||
|
|
7bdeaca75b | ||
|
|
e905c1edbe | ||
|
|
88f24afae6 | ||
|
|
33fb093aeb | ||
|
|
ac6c9e9b15 | ||
|
|
df5a333f30 | ||
|
|
65290e52f7 | ||
|
|
9683bb6408 | ||
|
|
e5209a1392 | ||
|
|
56707ac86a | ||
|
|
64a4b712cc | ||
|
|
3ccd527c8b | ||
|
|
85d37233a2 | ||
|
|
eff9d378e5 | ||
|
|
0f9a5c6b9a | ||
|
|
a20bcbd570 | ||
|
|
583c544cae | ||
|
|
f55300eba5 | ||
|
|
a68ddcbbc4 | ||
|
|
0723b7d672 | ||
|
|
f5ed17e3d8 | ||
|
|
5ecf457a35 | ||
|
|
79a7e5d4a1 | ||
|
|
7d157b9343 | ||
|
|
67ccb180c9 | ||
|
|
822964116f | ||
|
|
360c3112ef | ||
|
|
f2fba18860 | ||
|
|
cae9921159 | ||
|
|
f497d5d309 | ||
|
|
51a165dc7a | ||
|
|
9d4082356b | ||
|
|
3b8bc47ee5 | ||
|
|
78752fde7a | ||
|
|
c6fd922fcd | ||
|
|
e90a211820 | ||
|
|
8529485837 | ||
|
|
6810d823f5 | ||
|
|
3e62f1913a | ||
|
|
d23662c464 | ||
|
|
922c1ea317 | ||
|
|
258d81d7e9 | ||
|
|
1363e02603 | ||
|
|
ccc65127f1 | ||
|
|
3b38bb5d33 | ||
|
|
59c51c5747 | ||
|
|
ca17afc734 | ||
|
|
0b537fe163 | ||
|
|
2a32bf3fc7 | ||
|
|
57c4d47657 | ||
|
|
0371fe19ab | ||
|
|
3de8fd5d92 | ||
|
|
ce86cb892d | ||
|
|
9789ae3374 | ||
|
|
e508893dcc | ||
|
|
699f04c9ff | ||
|
|
89c82fb001 | ||
|
|
b7fed04c12 | ||
|
|
0ec5714271 | ||
|
|
5e483e4f3a | ||
|
|
84374b955e | ||
|
|
3a25c8da9f | ||
|
|
5a5983cf96 | ||
|
|
71c44a4c44 | ||
|
|
41053d6857 | ||
|
|
4287642308 | ||
|
|
3934e59bd3 | ||
|
|
9080e5c3ab | ||
|
|
3d5599cdd9 | ||
|
|
138d01e755 | ||
|
|
213ce114e3 | ||
|
|
ad8b9cfc9f | ||
|
|
de400dd652 | ||
|
|
6218ee30a7 | ||
|
|
976f072ef4 | ||
|
|
f4762be58b | ||
|
|
1b92ce08aa | ||
|
|
1d3d8288a9 | ||
|
|
eec54e93bf | ||
|
|
77b965cada | ||
|
|
bcc9eda66c | ||
|
|
3a0b9d7b3b | ||
|
|
e511b70d8f | ||
|
|
25cc60e648 | ||
|
|
d1e05dcb6f | ||
|
|
8cfd859711 | ||
|
|
7b3b826f87 | ||
|
|
195c9bd81f | ||
|
|
a8928d26d1 | ||
|
|
ef287d4436 | ||
|
|
6ae1de6989 | ||
|
|
9c810ce837 | ||
|
|
ba913bb949 | ||
|
|
58487b729a | ||
|
|
bf73cbaf97 | ||
|
|
1db868bf9c | ||
|
|
d331597bff | ||
|
|
71648d92ae | ||
|
|
735485b539 | ||
|
|
09c8248e31 | ||
|
|
c0b0029935 | ||
|
|
64af278f39 | ||
|
|
57dabbfc69 | ||
|
|
279f7a80c5 | ||
|
|
b66fdb10f2 | ||
|
|
84c1703c1a | ||
|
|
f324d50cef | ||
|
|
93a1e6fca8 | ||
|
|
4d55783ed8 | ||
|
|
aad50fb5b2 | ||
|
|
fb4ba5855b | ||
|
|
fbe5f42536 | ||
|
|
7663360ce6 | ||
|
|
0a3aad0205 | ||
|
|
cde42e5f92 | ||
|
|
fd965072c5 | ||
|
|
d703d1cd13 | ||
|
|
bd9c664b1a | ||
|
|
ae94ff1432 | ||
|
|
b64acb412e | ||
|
|
cbc5ec7d89 | ||
|
|
5401dc9e18 | ||
|
|
9b37597ac8 | ||
|
|
784c8b2bd2 | ||
|
|
2388fe5047 | ||
|
|
064eff0ac1 | ||
|
|
b5c933494a | ||
|
|
8c0bd97064 | ||
|
|
2ca9534715 | ||
|
|
641704a741 | ||
|
|
82d88d375e | ||
|
|
751caa7b3b | ||
|
|
7e16128b11 | ||
|
|
008fa09877 | ||
|
|
045963afe5 | ||
|
|
b799df3626 | ||
|
|
772df6f9af | ||
|
|
72cb383f2c | ||
|
|
625dc7c49b | ||
|
|
86916a94de | ||
|
|
71666a028b | ||
|
|
01e6301332 | ||
|
|
13050f7bff | ||
|
|
bedcd6fccf | ||
|
|
df8a71cd8b | ||
|
|
a113ece22b | ||
|
|
a63c2cfdf2 | ||
|
|
8f78a9dcde | ||
|
|
02eb362f37 | ||
|
|
f79263a92a | ||
|
|
cd95da6d35 | ||
|
|
5ab2c9afaa | ||
|
|
e77201099d | ||
|
|
30a4c00f35 | ||
|
|
e68db4ce57 | ||
|
|
b5a83ab902 | ||
|
|
2c9efea733 | ||
|
|
9615dc1458 | ||
|
|
f50a8482c3 | ||
|
|
cd3dc00f2f | ||
|
|
65eae30a48 | ||
|
|
fa4392df09 | ||
|
|
f8d6fd80d5 | ||
|
|
88ed545830 | ||
|
|
4388f6e87c | ||
|
|
6157364e20 | ||
|
|
96999e399d | ||
|
|
6a3df679fa | ||
|
|
03e49c59e2 | ||
|
|
b525b6e4fa | ||
|
|
5541b89cf7 | ||
|
|
aaeed5d18b | ||
|
|
d6c3c8a294 | ||
|
|
d337fc6d47 | ||
|
|
2d897d8537 | ||
|
|
12b101e04f | ||
|
|
d69af56c90 | ||
|
|
0cac5610c8 | ||
|
|
d0afcf6628 | ||
|
|
37fa27d54f | ||
|
|
be4fed2c19 | ||
|
|
47d02d8c4f | ||
|
|
4881d8e3a1 | ||
|
|
cc618abf58 | ||
|
|
546e381325 | ||
|
|
9d1bb29a00 | ||
|
|
876d0d5873 | ||
|
|
2aa5c387c7 | ||
|
|
9ca8e49a4e | ||
|
|
6ceed03f6b | ||
|
|
4836b16030 | ||
|
|
f9f44b18ad | ||
|
|
d4f5b7ca34 | ||
|
|
9b57329f56 | ||
|
|
0064ac5ead | ||
|
|
f2489c0845 | ||
|
|
dca345b135 | ||
|
|
645c1b9151 | ||
|
|
678fca6704 | ||
|
|
b74fae3762 | ||
|
|
2817ea833a | ||
|
|
b7ed6d8463 | ||
|
|
005c33dbb5 | ||
|
|
4176317250 | ||
|
|
bbd562f711 | ||
|
|
a19505a708 | ||
|
|
1eed16bc97 | ||
|
|
d9f88985fe | ||
|
|
a57e33e8d1 | ||
|
|
b4552ddb5f | ||
|
|
1da2450b10 | ||
|
|
9536b42244 | ||
|
|
dd75cdb37e | ||
|
|
3b3e537797 | ||
|
|
0f9168052a | ||
|
|
eb47476c83 | ||
|
|
7b04817874 | ||
|
|
c7a7456ec9 | ||
|
|
e422dd1198 | ||
|
|
a75928d805 | ||
|
|
fb2c5a85b6 | ||
|
|
4de2e381ff | ||
|
|
4da8c8d6db | ||
|
|
3c565defca | ||
|
|
191be658d5 | ||
|
|
1f209d0fb4 | ||
|
|
ba91e1dfb2 | ||
|
|
6766884cd8 | ||
|
|
b075140e76 | ||
|
|
aa8586d273 | ||
|
|
9b2a3d23b2 | ||
|
|
6a43a4bd20 | ||
|
|
8c78889e88 | ||
|
|
873159b793 | ||
|
|
b5823d3210 | ||
|
|
cd99c22f64 | ||
|
|
baa5122fcb | ||
|
|
5447aa7c80 | ||
|
|
933918ea27 | ||
|
|
cbbcdc5df1 | ||
|
|
4dfa7b132d | ||
|
|
fb5bfaa2bd | ||
|
|
20e206fa43 | ||
|
|
467fa59023 | ||
|
|
166c06c628 | ||
|
|
5ff3c8961c | ||
|
|
08f33f0e78 | ||
|
|
0c5a637203 | ||
|
|
e3b4fdb6b1 | ||
|
|
e730a6e282 | ||
|
|
722808a0e4 | ||
|
|
eae33161c1 | ||
|
|
f14df141f7 | ||
|
|
f7a4330cd1 | ||
|
|
23474c9752 | ||
|
|
fc08f9823e | ||
|
|
639bddb4b7 | ||
|
|
f87b32fc7b | ||
|
|
468ad6d578 | ||
|
|
8b5c7d3d87 | ||
|
|
e791084793 | ||
|
|
316a1ae2c5 | ||
|
|
71beca68dc | ||
|
|
aae79db27a | ||
|
|
6f188da2a6 | ||
|
|
9ae4ce82a7 | ||
|
|
5adfa722d4 | ||
|
|
c26dda7cc9 | ||
|
|
b7440ee516 | ||
|
|
e4b06b16a9 | ||
|
|
491af5bd9a | ||
|
|
9b67ab9713 | ||
|
|
f0a62600af | ||
|
|
dd5dfd98b7 | ||
|
|
d5ec38c4db | ||
|
|
f945463dbe | ||
|
|
cf9439fb3b | ||
|
|
6901847c49 | ||
|
|
c54c25c35e | ||
|
|
5728bce6bc | ||
|
|
d752403ed6 | ||
|
|
a48c08bd23 | ||
|
|
e46bbe8546 | ||
|
|
f5c8f18980 | ||
|
|
2d2270a337 | ||
|
|
d315c53ff8 | ||
|
|
d36b06acf7 | ||
|
|
2299af1dba | ||
|
|
e25ccc5e9a | ||
|
|
3ea6610923 | ||
|
|
2d50f10fd6 | ||
|
|
81d0637483 | ||
|
|
6c4df5abf0 | ||
|
|
2eb0b5eedd | ||
|
|
0e00492f54 | ||
|
|
b84a62eb5d | ||
|
|
c41ed95afe | ||
|
|
fe07013383 | ||
|
|
4f9cb9a8a1 | ||
|
|
ec5129d25b | ||
|
|
6a781c62ec | ||
|
|
c01ee83cd7 | ||
|
|
cc591e399d | ||
|
|
7462c703f3 | ||
|
|
879a6b4202 | ||
|
|
0ae8dc1040 | ||
|
|
242548b36a | ||
|
|
252aedda25 | ||
|
|
3507269321 | ||
|
|
9a5dce33db | ||
|
|
c4101a62ed | ||
|
|
f52037f305 | ||
|
|
03bd67c4e7 | ||
|
|
1eef239392 | ||
|
|
d1e14ed691 | ||
|
|
60a787ce3d | ||
|
|
f96bc6d5f4 | ||
|
|
5d439d9e79 | ||
|
|
1453178693 | ||
|
|
510121bf54 | ||
|
|
2d607b394c | ||
|
|
bd12b0e441 | ||
|
|
738b4e60fa | ||
|
|
1ae2f55c04 | ||
|
|
2ebdf9673d | ||
|
|
0427d790e5 | ||
|
|
90add7cf47 | ||
|
|
26b1f8dfdb | ||
|
|
ba29889f54 | ||
|
|
9d2284add7 | ||
|
|
dd44edde0a | ||
|
|
885e90e810 | ||
|
|
9cdf5dd0f3 | ||
|
|
df6e3eb1e6 | ||
|
|
05026771e1 | ||
|
|
7039108438 | ||
|
|
02ee13cfb2 | ||
|
|
096e244252 | ||
|
|
bf5b7294a0 | ||
|
|
a5da266643 | ||
|
|
cf7bb49e15 | ||
|
|
208b732bda | ||
|
|
c73d93b8bd | ||
|
|
98a96eae2b | ||
|
|
2f9fe30c9d | ||
|
|
aeee8afc02 | ||
|
|
e85f0a4f52 | ||
|
|
da98649667 | ||
|
|
5ac08cc06b | ||
|
|
da72597dd3 | ||
|
|
1f1c94de70 | ||
|
|
60b3fceea6 | ||
|
|
5073809486 | ||
|
|
debd779cfd | ||
|
|
6b9454100e | ||
|
|
779ad24542 | ||
|
|
b94dbf5fa3 | ||
|
|
45c49c9757 | ||
|
|
91288c96b1 | ||
|
|
f8e22a0730 | ||
|
|
114b45882a | ||
|
|
b1b6f70118 | ||
|
|
648d42dfe4 | ||
|
|
99f989c384 | ||
|
|
2112c7d096 | ||
|
|
ac63d00c93 | ||
|
|
e04871f79f | ||
|
|
182c162dc4 | ||
|
|
822b38cc89 | ||
|
|
d564003c87 | ||
|
|
1b307632ab | ||
|
|
aa747cea85 | ||
|
|
f4a322478d | ||
|
|
d2882433a5 | ||
|
|
a94b175805 | ||
|
|
37d81da806 | ||
|
|
d089444441 | ||
|
|
b0d65a1bae | ||
|
|
16288cf277 | ||
|
|
7ddbabf781 | ||
|
|
fe35f4497b | ||
|
|
625463f6ab | ||
|
|
ff632b6816 | ||
|
|
fbc666f178 | ||
|
|
d89bbdd50c | ||
|
|
96f9aa39b2 | ||
|
|
7330814d0f | ||
|
|
312efdcd94 | ||
|
|
5db78ae359 | ||
|
|
97967e60e8 | ||
|
|
9106b5d182 | ||
|
|
74bdb6cb9d | ||
|
|
0a44d426fa | ||
|
|
e1718c4e8d | ||
|
|
f511a610b5 | ||
|
|
4d5715188d | ||
|
|
2ea21be5bd | ||
|
|
5bb0419699 | ||
|
|
a8131eed71 | ||
|
|
ed09c06ba4 | ||
|
|
3c59a0ff31 | ||
|
|
a6d24b3e48 | ||
|
|
060135eecb | ||
|
|
ef296c24fe | ||
|
|
707aaf25ec | ||
|
|
7edeb0c358 | ||
|
|
e516af14b2 | ||
|
|
4086f2671d | ||
|
|
23c4550430 | ||
|
|
31d25cd6be | ||
|
|
07b3c7a245 | ||
|
|
a00b7281a7 | ||
|
|
ddeee0c970 | ||
|
|
8aad71efd0 | ||
|
|
2028f6b984 | ||
|
|
bff4999d27 | ||
|
|
d429015f83 | ||
|
|
e2628e2d43 | ||
|
|
05dcbee7e3 | ||
|
|
a81919262e | ||
|
|
b14b5f141b | ||
|
|
1259d11173 | ||
|
|
0a7b132be8 | ||
|
|
ed9210eede | ||
|
|
9ee6aa54c6 | ||
|
|
7cfc455cd3 | ||
|
|
a481ceac8c | ||
|
|
8c7eff4e24 | ||
|
|
c6c584ff74 | ||
|
|
ba50eb121d | ||
|
|
aa8ebbd7ea | ||
|
|
64bc9c6dbe | ||
|
|
bba9963b7c | ||
|
|
6ea2aa4a54 | ||
|
|
3c3f81365b | ||
|
|
3adeed381b | ||
|
|
0f5b7278b8 | ||
|
|
f94ff49fb9 | ||
|
|
d512a9c30d | ||
|
|
0c5113ed5b | ||
|
|
2469f4cdff | ||
|
|
9c53bfb7fb | ||
|
|
8b8144588d | ||
|
|
77553da4c1 | ||
|
|
cbcf943691 | ||
|
|
725a19e5b5 | ||
|
|
f9115f902a | ||
|
|
e4faf26d74 | ||
|
|
1c96fbb533 | ||
|
|
3dc163c33d | ||
|
|
edae94cf2e | ||
|
|
d1ff8e9d6b | ||
|
|
70743bd285 | ||
|
|
493f1505f0 | ||
|
|
007e3b5eef | ||
|
|
d9bf6c0933 | ||
|
|
324344d118 | ||
|
|
5cb71e9443 | ||
|
|
cca19f00c5 | ||
|
|
6648f41f3d | ||
|
|
c1e6b47fd6 | ||
|
|
0f103ccce1 | ||
|
|
bc6e652293 | ||
|
|
85b4f2dbdd | ||
|
|
d47b83a63b | ||
|
|
b2e9fa7e0d | ||
|
|
a9fb444622 | ||
|
|
33ba22a021 | ||
|
|
57de0282cd | ||
|
|
8568fd26d8 | ||
|
|
84f41e08cf | ||
|
|
a96da20536 | ||
|
|
5199a9342e | ||
|
|
893ecec0fa | ||
|
|
e3da6419f5 | ||
|
|
0750d2ba50 | ||
|
|
f1fcb65fbe | ||
|
|
215aa65d5a | ||
|
|
85f67c13da | ||
|
|
6dcc478aeb | ||
|
|
3f2496db6f | ||
|
|
612f79f9e0 | ||
|
|
90fb1cd735 | ||
|
|
7c24d9c6c6 | ||
|
|
60f1b2356a | ||
|
|
0b8f21508f | ||
|
|
ae128c0fa4 | ||
|
|
1b4ec9ecf9 | ||
|
|
b0ce0b61d6 | ||
|
|
e1ffdaddfa | ||
|
|
af8344f482 | ||
|
|
7dc2596b3b | ||
|
|
0109956fc2 | ||
|
|
945fe3f3ec | ||
|
|
9c868135f3 | ||
|
|
5be288023b | ||
|
|
a03f97186c | ||
|
|
0aab891980 | ||
|
|
5268d3f57d | ||
|
|
129cbb5beb | ||
|
|
2601d2945d | ||
|
|
e3829eb24b | ||
|
|
f6cb1a0863 | ||
|
|
4f964101a0 | ||
|
|
f6dcba025f | ||
|
|
d6ec65d456 | ||
|
|
65d8074a07 | ||
|
|
e3af61ca4a | ||
|
|
a58f1268f0 | ||
|
|
41eacc4bc5 | ||
|
|
aabb9dee13 | ||
|
|
c855d75f35 | ||
|
|
8f5cdcf439 | ||
|
|
984559427e | ||
|
|
89494ced41 | ||
|
|
ef764c2393 | ||
|
|
8624e2260d | ||
|
|
aa011f4add | ||
|
|
3df61c9ab8 | ||
|
|
a4516776d6 | ||
|
|
54d0ade997 | ||
|
|
3557fcd129 | ||
|
|
330b4a613c | ||
|
|
7ba3412aae | ||
|
|
6f60495d4d | ||
|
|
0b2eb8fb9e | ||
|
|
48af17e052 | ||
|
|
b7b1055530 | ||
|
|
e7029c0afd | ||
|
|
cba3674ac0 | ||
|
|
865a549885 | ||
|
|
50dcf827a5 | ||
|
|
f5fb582f83 | ||
|
|
dbba502f83 | ||
|
|
aae49f16a2 | ||
|
|
45d5f8c74d | ||
|
|
6cfd64e536 | ||
|
|
c5cc404b3e | ||
|
|
42cbcc6ce3 | ||
|
|
812bdcd462 | ||
|
|
f275409ee8 | ||
|
|
8994ac3727 | ||
|
|
7c5ff5e4d5 | ||
|
|
c5e84d5469 | ||
|
|
c143450dc6 | ||
|
|
07b95c2c4b | ||
|
|
c30734f7f3 | ||
|
|
91f506c17b | ||
|
|
7a17695ad5 | ||
|
|
f5076c87d4 | ||
|
|
a47d6e1f3a | ||
|
|
f6ff1abb00 | ||
|
|
386aaf6470 | ||
|
|
2b3c4cf0ff | ||
|
|
b602e921d0 | ||
|
|
2fc3cdc2a2 | ||
|
|
e2cadbfc30 | ||
|
|
3ffa935da7 | ||
|
|
5f539e331a | ||
|
|
356d0fabda | ||
|
|
122ec75cb6 | ||
|
|
a3a48e1a49 | ||
|
|
4ede765e1f | ||
|
|
4fa181b346 | ||
|
|
4f76d91ae9 | ||
|
|
20d1759fa5 | ||
|
|
433e783ede | ||
|
|
47f47d916d | ||
|
|
b31ac7d1fd | ||
|
|
ea47fb7305 | ||
|
|
82170f8f1b | ||
|
|
acb2655f58 | ||
|
|
b1464517e6 | ||
|
|
151e6351f6 | ||
|
|
154f768281 | ||
|
|
90c857e8fc | ||
|
|
7a3efa2631 | ||
|
|
38cc767f27 | ||
|
|
e1a718c78f | ||
|
|
32a4450e5e | ||
|
|
fca3f606d2 | ||
|
|
4a0a934a76 | ||
|
|
f7c406bec9 | ||
|
|
f4807a6354 | ||
|
|
0960008b7b | ||
|
|
04a1aa38b4 | ||
|
|
f84622efa1 | ||
|
|
f6c4614275 | ||
|
|
7d36533524 | ||
|
|
5cd3df4869 | ||
|
|
b0480f48f3 | ||
|
|
2e820c343a | ||
|
|
ce927a2247 | ||
|
|
ae810d59e9 | ||
|
|
1438ee52a1 | ||
|
|
de4b3e55fa | ||
|
|
d2cd78c5cb | ||
|
|
d000719fa2 | ||
|
|
efea4ed615 | ||
|
|
67a931c4b8 | ||
|
|
bdcc5c0629 | ||
|
|
d113cfc0ba | ||
|
|
4a3ab50878 | ||
|
|
b39261c8cf | ||
|
|
7efb57c8da | ||
|
|
90c24cf356 | ||
|
|
54abada561 | ||
|
|
f1922660be | ||
|
|
795e3c57da | ||
|
|
3f201464a5 | ||
|
|
8ac0be6bb5 | ||
|
|
130805e7bd | ||
|
|
b8c7357fea | ||
|
|
819f8e338f | ||
|
|
9569e46ff8 | ||
|
|
b7baab2d0f | ||
|
|
e2d284797d | ||
|
|
a3ac343fe2 | ||
|
|
dadde96e41 | ||
|
|
99475c51e8 | ||
|
|
cc9b4e26b5 | ||
|
|
32f232d3c0 | ||
|
|
235047ad0b | ||
|
|
228f75de0b | ||
|
|
2f89e7e2b4 | ||
|
|
437f39deb3 | ||
|
|
59582f16c4 | ||
|
|
af9e3e38ce | ||
|
|
d992702b87 | ||
|
|
6a9fe1128f | ||
|
|
573da29a4d | ||
|
|
00cff1a728 | ||
|
|
9bdeff0a39 | ||
|
|
a1f263c048 | ||
|
|
346eac389c | ||
|
|
f52c16b209 | ||
|
|
4faf880aa4 | ||
|
|
f417a49b34 | ||
|
|
66fd713d12 | ||
|
|
2e7630f97e | ||
|
|
3f10524532 | ||
|
|
51f9826918 | ||
|
|
f5bb76333b | ||
|
|
4947faa5ca | ||
|
|
101dc3a93c | ||
|
|
bd3ee0fa24 | ||
|
|
2c52668a74 | ||
|
|
03edd8c96b | ||
|
|
37dfa41e01 | ||
|
|
ea8a3d798e | ||
|
|
1df94fd84d | ||
|
|
5af957dc9c | ||
|
|
21073c627e | ||
|
|
66cdba9c1a | ||
|
|
56d3b38ce6 | ||
|
|
15d0275045 | ||
|
|
991c1a0137 | ||
|
|
7d549dbbd5 | ||
|
|
e27c5583bb | ||
|
|
650c49637f | ||
|
|
eb5dcf1c3e | ||
|
|
ed2b61b709 | ||
|
|
41466a3018 | ||
|
|
2e130ef99d | ||
|
|
a96fb39a82 | ||
|
|
c9923c8d4b | ||
|
|
74b0ff338b | ||
|
|
dcaccc2d7a | ||
|
|
d60714e4e6 | ||
|
|
d513d5d887 | ||
|
|
386566fd4b | ||
|
|
3357ca76fe | ||
|
|
a183ce13ee | ||
|
|
e9d0ed8e1e | ||
|
|
66f66fd14f | ||
|
|
b49d30b477 | ||
|
|
73d83ec57e | ||
|
|
efb39fb24b | ||
|
|
73623f2e92 | ||
|
|
fbcc4cfa50 | ||
|
|
474a3548e0 | ||
|
|
2cdf68379b | ||
|
|
cc8509f8eb | ||
|
|
a520c1b1cb | ||
|
|
75fc2cbcfb | ||
|
|
b8bb69f730 | ||
|
|
b46d3e74d6 | ||
|
|
77a1613107 | ||
|
|
62fab7b09f | ||
|
|
5d87352b28 | ||
|
|
ff60f5a381 | ||
|
|
7f666d9369 | ||
|
|
442f16dbd0 | ||
|
|
2dcab77ed1 | ||
|
|
13be04a169 | ||
|
|
e3767c3a54 | ||
|
|
ce957c8dd5 | ||
|
|
0606b2994c | ||
|
|
33acccbaaa | ||
|
|
1e097abe86 | ||
|
|
e51705c41d | ||
|
|
7eafa661fe | ||
|
|
2fe323e587 | ||
|
|
4e608d04dc | ||
|
|
531d314e25 | ||
|
|
1ab23d2902 | ||
|
|
b3496e1354 | ||
|
|
2efa0aaca4 | ||
|
|
ef9aeb0772 | ||
|
|
924a0136eb | ||
|
|
c382fc375e | ||
|
|
2544acddfa | ||
|
|
58072892d6 | ||
|
|
85a897c78c | ||
|
|
6adf5772d8 | ||
|
|
f98e3b1960 | ||
|
|
671a967e35 | ||
|
|
950ef0074f | ||
|
|
5515324fd4 | ||
|
|
e72622ed4f | ||
|
|
e821733a58 | ||
|
|
a03c0e4475 | ||
|
|
3203821546 | ||
|
|
16f3cee5c5 | ||
|
|
57afb46cbd | ||
|
|
91dde5147a | ||
|
|
d0692f7379 | ||
|
|
e360658c6e | ||
|
|
e7dc77e6de | ||
|
|
e240a8b58f | ||
|
|
38d4f2c27b | ||
|
|
552e2a036c | ||
|
|
2d4b978032 | ||
|
|
36e00f0c84 | ||
|
|
ef64b2b945 | ||
|
|
f6cd33ae24 | ||
|
|
dd109f149f | ||
|
|
5b62d63463 | ||
|
|
3fec599c0c | ||
|
|
e30ea9f143 | ||
|
|
7cb0c31c59 | ||
|
|
b00a7e3cbb | ||
|
|
e63446ffa2 | ||
|
|
580da19bc2 | ||
|
|
936f456cec | ||
|
|
5d6a02f73c | ||
|
|
b345195ea9 | ||
|
|
3e6b66751c | ||
|
|
f78571e46d | ||
|
|
f52000958c | ||
|
|
5ac9c6ce02 | ||
|
|
1110a67483 | ||
|
|
57bb1280f8 | ||
|
|
25c000599f | ||
|
|
86f45e2769 | ||
|
|
7110240e73 | ||
|
|
1da37b66d8 | ||
|
|
f1975d8f2b | ||
|
|
f407ce734a | ||
|
|
f813cfa8db | ||
|
|
d5880cb953 | ||
|
|
95da9744c1 | ||
|
|
85c3e45cde | ||
|
|
520a396ded | ||
|
|
13ad611c96 | ||
|
|
85f58d9681 | ||
|
|
c1de62acef | ||
|
|
7e47e36773 | ||
|
|
00b6217cab | ||
|
|
acc2b5a1a3 | ||
|
|
b06feaa36b | ||
|
|
89cf8a455a | ||
|
|
710046a94f | ||
|
|
b366b0fa6a | ||
|
|
f9e7a8207a | ||
|
|
6178bf3d4b | ||
|
|
f3b979f112 | ||
|
|
9faae96d61 | ||
|
|
2135fe5dd0 | ||
|
|
007a8d248d | ||
|
|
58d4a3455b | ||
|
|
8e3c14f245 | ||
|
|
91af2495a6 | ||
|
|
7d7df5247b | ||
|
|
f99450d264 | ||
|
|
d3eeb5f48a | ||
|
|
1e8a02f91a | ||
|
|
97c3bd8b8e | ||
|
|
09ce27d74b | ||
|
|
2447e91a9f | ||
|
|
e6d881b75d | ||
|
|
36f963dce8 | ||
|
|
1b15d28212 | ||
|
|
4e0c15e102 | ||
|
|
c9e40f59de | ||
|
|
38cf31885c | ||
|
|
4420470242 | ||
|
|
9b05786615 | ||
|
|
725b2c81ee | ||
|
|
661965f2e0 | ||
|
|
7e0ef60305 | ||
|
|
2ac0fe21c6 | ||
|
|
b997f2329d | ||
|
|
23ee758ac9 | ||
|
|
9ea12e71f0 | ||
|
|
d3594c2dd6 | ||
|
|
6ee4b0da27 | ||
|
|
3e66feb514 | ||
|
|
cd91a5ef64 | ||
|
|
cf89609633 | ||
|
|
67c24c1282 | ||
|
|
7d3df3c55f | ||
|
|
dfe5cec46f | ||
|
|
17c881da47 | ||
|
|
6e30c4917c | ||
|
|
c6d4f0d2f0 | ||
|
|
b32128bebf | ||
|
|
a3f3d86908 | ||
|
|
b29c82087a | ||
|
|
657beda7c9 | ||
|
|
b4f5ecb304 | ||
|
|
3dabad5e91 | ||
|
|
890b46836b | ||
|
|
835b3224c6 | ||
|
|
f8d27f3139 | ||
|
|
33f263ebb9 | ||
|
|
027925c0ba | ||
|
|
17c4819d41 | ||
|
|
017d19a8c8 | ||
|
|
46b6e319f5 | ||
|
|
8f087e1c30 | ||
|
|
c3fc0e83a8 | ||
|
|
7ed0ef7b37 | ||
|
|
46ede3d60d | ||
|
|
7a63fd4711 | ||
|
|
65f573b773 | ||
|
|
afa2fe8177 | ||
|
|
ad72a8a929 | ||
|
|
a7b00bad63 | ||
|
|
85fd74135c | ||
|
|
970ccf1ddb | ||
|
|
b237eb03f6 | ||
|
|
a569294f86 | ||
|
|
16f85a23d2 | ||
|
|
fcee8aa5f3 | ||
|
|
d85eabce02 | ||
|
|
de23d1aa03 | ||
|
|
1766bc6ee3 | ||
|
|
c1801d6e71 | ||
|
|
64844045ca | ||
|
|
e90da46967 | ||
|
|
d10957d6df | ||
|
|
50dc90d7ae | ||
|
|
663bedfe39 | ||
|
|
ce9834757e | ||
|
|
cc932328ff | ||
|
|
4ebe143a98 | ||
|
|
82aff74fc2 | ||
|
|
6adc099455 | ||
|
|
35efc8c650 | ||
|
|
3f63d79905 | ||
|
|
00096f4dcd | ||
|
|
c3e0d9086e | ||
|
|
f1dfe3c7e8 | ||
|
|
6f96ff790f | ||
|
|
ccb218f243 | ||
|
|
9ac194bbea | ||
|
|
0191907ce2 | ||
|
|
e80069625b | ||
|
|
0e156b9376 | ||
|
|
a8f1b0241e | ||
|
|
6715cf23d7 | ||
|
|
82a173f7d8 | ||
|
|
857504c409 | ||
|
|
4b4586c1e5 | ||
|
|
6679fe47df | ||
|
|
e7a98025a2 | ||
|
|
2870f24bec | ||
|
|
037440034b | ||
|
|
15cc1f92e3 | ||
|
|
00c6ad675e | ||
|
|
655a740b0c | ||
|
|
028852740d | ||
|
|
c8000fdf90 | ||
|
|
995e56d7e4 | ||
|
|
c20d3b62b0 | ||
|
|
c537dfabb2 | ||
|
|
11b5304cb9 | ||
|
|
fd8abbe2ab | ||
|
|
25d871860d | ||
|
|
d1911be28c | ||
|
|
938ca6402c | ||
|
|
0aaecf6e46 | ||
|
|
b06d84984b | ||
|
|
51b50688e4 | ||
|
|
066d7ab972 | ||
|
|
e092074d77 | ||
|
|
83bdcb8cc4 | ||
|
|
f80f40cbcd | ||
|
|
4b93b31c3d | ||
|
|
4d050725b7 | ||
|
|
57597bd103 | ||
|
|
fb52c2b684 | ||
|
|
de547df9bd | ||
|
|
a05342eaa0 | ||
|
|
fb931b7a3a | ||
|
|
d1c07b6d30 | ||
|
|
7f0ad2afa0 | ||
|
|
d8e0639db4 | ||
|
|
4d91351845 | ||
|
|
d3f08ef580 | ||
|
|
5e11a9c8ed | ||
|
|
957e1a7708 | ||
|
|
7c86ed9783 | ||
|
|
799b588693 | ||
|
|
596f4c01a4 | ||
|
|
f155de0f17 | ||
|
|
476ba1ad69 | ||
|
|
ac4aa4bd3d | ||
|
|
237f2c5112 | ||
|
|
cbc6785eb5 | ||
|
|
26c4cdbf17 | ||
|
|
fb78f31891 | ||
|
|
2b6bf8d195 | ||
|
|
2854462e0e | ||
|
|
b4e4b11ab3 | ||
|
|
7c5a258af3 | ||
|
|
12aa8ac0ad | ||
|
|
58d8f688e5 | ||
|
|
7efb9e817e | ||
|
|
5145ea3530 | ||
|
|
2f6933102c | ||
|
|
25ef5ab636 | ||
|
|
4ae12ac10b | ||
|
|
bfffde5f89 | ||
|
|
aa7ec53257 | ||
|
|
1f41e6dc0f | ||
|
|
1fbbaa82ab | ||
|
|
68b1d1dde1 | ||
|
|
d773cb4873 | ||
|
|
d3c7616120 | ||
|
|
6a92af3db3 | ||
|
|
763e14f55d | ||
|
|
4f57d97fff | ||
|
|
3153fb5cbe | ||
|
|
c9e96cd97a | ||
|
|
c41042635f | ||
|
|
141b2d2691 | ||
|
|
e71e8043cf | ||
|
|
49d80dbfc4 | ||
|
|
8d6eca2349 | ||
|
|
13d0491759 | ||
|
|
37e2d78d6a | ||
|
|
6745221e0f | ||
|
|
18bbe70364 | ||
|
|
eec8d4bdac | ||
|
|
86029c1068 | ||
|
|
0ae9be4de9 | ||
|
|
57e3180737 | ||
|
|
a84cdc3d09 | ||
|
|
a5f35f39fe | ||
|
|
3427db3983 | ||
|
|
e3878fa381 | ||
|
|
e1ded9f7b5 | ||
|
|
1981493398 | ||
|
|
dece7319cc | ||
|
|
5c4e163709 | ||
|
|
d1acc6c466 | ||
|
|
f879d6f529 | ||
|
|
1ac38d4921 | ||
|
|
4818e9a8e4 | ||
|
|
c4ed471d1c | ||
|
|
83c0b2986a | ||
|
|
b8cddf559a | ||
|
|
4ba9f80d44 | ||
|
|
d1d3309e91 | ||
|
|
84cffe8888 | ||
|
|
3929b3ca0a | ||
|
|
d649a470f9 | ||
|
|
db330b23cb | ||
|
|
cda649884e | ||
|
|
45053205db | ||
|
|
3f1533896e | ||
|
|
e20dfe1b26 | ||
|
|
946d9db296 | ||
|
|
6dc2e1aa14 | ||
|
|
001749564d | ||
|
|
ffcba4646c | ||
|
|
01d0c8eb9c | ||
|
|
0cf40bd207 | ||
|
|
4a283e9f35 | ||
|
|
5ab37bcf7e | ||
|
|
9151965cd6 | ||
|
|
c5cd71f9e3 | ||
|
|
602b335c0e | ||
|
|
837c8b85c2 | ||
|
|
7d16396e72 | ||
|
|
66d3d07148 | ||
|
|
b5c1161caa | ||
|
|
b0420889ad | ||
|
|
527819d886 | ||
|
|
1ad0cff28e | ||
|
|
783ec03ac9 | ||
|
|
6cd395d494 | ||
|
|
681079e01c | ||
|
|
aabbc43769 | ||
|
|
2692f6ef4e | ||
|
|
887cbb0b22 | ||
|
|
ca4fdc1be8 | ||
|
|
93199c7f5b | ||
|
|
4c6566f42f | ||
|
|
c38f7d7f93 | ||
|
|
da85cea329 | ||
|
|
d5c70a2b11 | ||
|
|
fe355b4bac | ||
|
|
a7dee6be51 | ||
|
|
2817dc0603 | ||
|
|
6f36c72e88 | ||
|
|
45e806c455 | ||
|
|
bbdd76dd37 | ||
|
|
09921e86c0 | ||
|
|
d6e4b64103 | ||
|
|
9dd3e4537a | ||
|
|
a5f31e8724 | ||
|
|
72ac00b69a | ||
|
|
ae5722a7d4 | ||
|
|
4e3192d450 | ||
|
|
ccca3aca04 | ||
|
|
e4dd5d6434 | ||
|
|
9a77fb6306 | ||
|
|
3ec5c713bf | ||
|
|
837fc27e94 | ||
|
|
9ad6025310 | ||
|
|
d765e4c619 | ||
|
|
f5217236d6 | ||
|
|
8f8d099faf | ||
|
|
16660e083f | ||
|
|
4e35020a1c | ||
|
|
111e0bcb5f | ||
|
|
d7f9a547fc | ||
|
|
6a64f24e98 | ||
|
|
37d7be93b5 | ||
|
|
9c809aa6e1 | ||
|
|
7ab9f3fa2f | ||
|
|
ffeb484a10 | ||
|
|
2ffb32ae60 | ||
|
|
905bb92bad | ||
|
|
3926efd153 | ||
|
|
c5e5bb90e3 | ||
|
|
cea543cba5 | ||
|
|
a8b489624d | ||
|
|
49d3bddb62 | ||
|
|
c0ff3cbd22 | ||
|
|
1de97d6967 | ||
|
|
a44a82083e | ||
|
|
d57681ff21 | ||
|
|
e3de2f81d3 | ||
|
|
e8c5f8164c | ||
|
|
c07e215148 | ||
|
|
4bb676fb5c | ||
|
|
dbdf86edfc | ||
|
|
2c8e6330ce | ||
|
|
1b563854a7 | ||
|
|
80b890101b | ||
|
|
c3696469ff | ||
|
|
3e08e7c653 | ||
|
|
53e39f571c | ||
|
|
c992853cca | ||
|
|
85e17b570b | ||
|
|
30eccfb54b | ||
|
|
3623831390 | ||
|
|
d0a3d00492 | ||
|
|
0b6fbfd910 | ||
|
|
8cfb27fdcd | ||
|
|
841ab54565 | ||
|
|
a2e9254343 | ||
|
|
43cb03a292 | ||
|
|
f2fca33309 | ||
|
|
14d26fe064 | ||
|
|
9cc968e790 | ||
|
|
831e22b4ff | ||
|
|
6774514bd2 | ||
|
|
f543b98764 | ||
|
|
2e94600afe | ||
|
|
9295ce783a | ||
|
|
134f8a28bf | ||
|
|
ab5e4e998c | ||
|
|
a98551f99c | ||
|
|
42fe84152a | ||
|
|
8a3d212bd4 | ||
|
|
af51ddc347 | ||
|
|
b582e549c2 | ||
|
|
5efbccd974 | ||
|
|
82f5cd6075 | ||
|
|
0d8820c247 | ||
|
|
37c6a96a3a | ||
|
|
c53b54bda3 | ||
|
|
808753ad3a | ||
|
|
f919570cea | ||
|
|
9acf49a99e | ||
|
|
239883d01f | ||
|
|
e3cee37527 | ||
|
|
8fd0461c62 | ||
|
|
4d2b5c83ca | ||
|
|
bc314c1119 | ||
|
|
d01749a2c2 | ||
|
|
b46154676a | ||
|
|
fd2d60dca3 | ||
|
|
ed17bdc7c3 | ||
|
|
ac05399cda | ||
|
|
1af5c6a418 | ||
|
|
e2bb668fe4 | ||
|
|
d255466417 | ||
|
|
5509406395 | ||
|
|
97333474c4 | ||
|
|
38928d63d6 | ||
|
|
05c64dcbf2 | ||
|
|
e39b081567 | ||
|
|
62174658cf | ||
|
|
3d26e8a666 | ||
|
|
3d337640ef |
509
CHANGES
509
CHANGES
@@ -1719,3 +1719,512 @@
|
||||
* List unstable apps by default
|
||||
* Fix crash when listing mailboxes
|
||||
|
||||
[4.3.2]
|
||||
* Update manifestformat module
|
||||
|
||||
[4.3.3]
|
||||
* Fix bug where stopped containers got started on server restart
|
||||
* Fix external LDAP UI and syncing
|
||||
* Fix timeout being too low in docker proxy
|
||||
* Make manifest.id optional for custom apps
|
||||
* Fix registry detection in private images
|
||||
* Make mailbox domain configurable for apps
|
||||
|
||||
[4.3.4]
|
||||
* Do not error if fallback certs went missing
|
||||
* Add 'New Apps' section to Appstore view
|
||||
* Fix issue where graphs of some apps were not appearing
|
||||
|
||||
[4.4.0]
|
||||
* Show swap in graphs
|
||||
* Make avatars customizable
|
||||
* Hide access tokens from logs
|
||||
* Add missing '@' sign for email address in app mailbox
|
||||
* Add app fqdn to backup progress message
|
||||
* import: add option to import app in-place
|
||||
* import: add option to import app from arbitrary backup config
|
||||
* Show download progress for rsync backups
|
||||
* Fix various repair workflows
|
||||
* acme2: Implement post-as-get
|
||||
|
||||
[4.4.1]
|
||||
* ami: fix AWS provider validation
|
||||
|
||||
[4.4.2]
|
||||
* Fix crash when reporting that DKIM is not setup correctly
|
||||
* Stopped apps cannot be updated or auto-updated
|
||||
* eventlog: track support ticket creation and remote support status
|
||||
|
||||
[4.4.3]
|
||||
* Add restart button in recovery section
|
||||
* Fix issue where memory usage was not computed correctly
|
||||
* cloudflare: support API tokens
|
||||
|
||||
[4.4.4]
|
||||
* Fix bug where restart button in terminal was not working
|
||||
* Add search field in apps view
|
||||
* Make app view tags and domain filter persistent
|
||||
* Add timezone UI
|
||||
|
||||
[4.4.5]
|
||||
* Fix user listing regression in group edit dialog
|
||||
* Do not show error page for 503
|
||||
* Add mail list and mail box update events
|
||||
* Certs of stopped apps are not renewed anymore
|
||||
* Fix broken memory sliders in the services UI
|
||||
* Set CPU Shares
|
||||
* Update nodejs to 12.14.1
|
||||
* Update MySQL addon packet size to 64M
|
||||
|
||||
[5.0.0]
|
||||
* Show backup disk usage in graphs
|
||||
* Add per-user app passwords
|
||||
* Make app not responding page customizable
|
||||
* Make footer customizable
|
||||
* Add UI to import backups
|
||||
* Display timestamps in browser timezone in the UI
|
||||
* Mail eventlog and usage
|
||||
* Add user roles - owner, admin, user manager and user
|
||||
* Setup logrotate configs for collectd since upstream does not set it up
|
||||
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
|
||||
* linode: add object storage backend
|
||||
* restore: carefully replace backup config
|
||||
* spam: add default corpus and global db
|
||||
|
||||
[5.0.1]
|
||||
* Show backup disk usage in graphs
|
||||
* Add per-user app passwords
|
||||
* Make app not responding page customizable
|
||||
* Make footer customizable
|
||||
* Add UI to import backups
|
||||
* Display timestamps in browser timezone in the UI
|
||||
* Mail eventlog and usage
|
||||
* Add user roles - owner, admin, user manager and user
|
||||
* Setup logrotate configs for collectd since upstream does not set it up
|
||||
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
|
||||
* linode: add object storage backend
|
||||
* restore: carefully replace backup config
|
||||
* spam: add default corpus and global db
|
||||
|
||||
[5.0.2]
|
||||
* Show backup disk usage in graphs
|
||||
* Add per-user app passwords
|
||||
* Make app not responding page customizable
|
||||
* Make footer customizable
|
||||
* Add UI to import backups
|
||||
* Display timestamps in browser timezone in the UI
|
||||
* Mail eventlog and usage
|
||||
* Add user roles - owner, admin, user manager and user
|
||||
* Setup logrotate configs for collectd since upstream does not set it up
|
||||
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
|
||||
* linode: add object storage backend
|
||||
* restore: carefully replace backup config
|
||||
* spam: per mailbox bayes db and training
|
||||
|
||||
[5.0.3]
|
||||
* Show backup disk usage in graphs
|
||||
* Add per-user app passwords
|
||||
* Make app not responding page customizable
|
||||
* Make footer customizable
|
||||
* Add UI to import backups
|
||||
* Display timestamps in browser timezone in the UI
|
||||
* Mail eventlog and usage
|
||||
* Add user roles - owner, admin, user manager and user
|
||||
* Setup logrotate configs for collectd since upstream does not set it up
|
||||
* mail: Add X-Envelope-To and X-Envelope-From headers for incoming mails
|
||||
* linode: add object storage backend
|
||||
* restore: carefully replace backup config
|
||||
* spam: per mailbox bayes db and training
|
||||
|
||||
[5.0.4]
|
||||
* Fix potential previlige escalation because of ghost file
|
||||
* linode: dns backend
|
||||
* make branding routes owner only
|
||||
* add branding API
|
||||
* Add app start/stop/restart events
|
||||
* Use the primary email for LE account
|
||||
* make mail eventlog more descriptive
|
||||
|
||||
[5.0.5]
|
||||
* Fix bug where incoming mail from dynamic hostnames was rejected
|
||||
* Increase token expiry
|
||||
* Fix bug in tag UI where tag removal did not work
|
||||
|
||||
[5.0.6]
|
||||
* Make mail eventlog only visible to owners
|
||||
* Make app password work with sftp
|
||||
|
||||
[5.1.0]
|
||||
* Add turn addon
|
||||
* Fix disk usage display
|
||||
* Drop support for TLSv1 and TLSv1.1
|
||||
* Make cert validation work for ECC certs
|
||||
* Add type filter to mail eventlog
|
||||
* mail: Fix listing of mailboxes and aliases in the UI
|
||||
* branding: fix login page title
|
||||
* Only a Cloudron owner can install/update/exec apps with the docker addon
|
||||
* security: reset tokens are only valid for a day
|
||||
* mail: fix eventlog db perms
|
||||
* Fix various bugs in the disk graphs
|
||||
|
||||
[5.1.1]
|
||||
* Add turn addon
|
||||
* Fix disk usage display
|
||||
* Drop support for TLSv1 and TLSv1.1
|
||||
* Make cert validation work for ECC certs
|
||||
* Add type filter to mail eventlog
|
||||
* mail: Fix listing of mailboxes and aliases in the UI
|
||||
* branding: fix login page title
|
||||
* Only a Cloudron owner can install/update/exec apps with the docker addon
|
||||
* security: reset tokens are only valid for a day
|
||||
* mail: fix eventlog db perms
|
||||
* Fix various bugs in the disk graphs
|
||||
* Fix collectd installation
|
||||
* graphs: sort disk contents by usage
|
||||
* backups: show apps that are not automatically backed up in backup view
|
||||
|
||||
[5.1.2]
|
||||
* Add turn addon
|
||||
* Fix disk usage display
|
||||
* Drop support for TLSv1 and TLSv1.1
|
||||
* Make cert validation work for ECC certs
|
||||
* Add type filter to mail eventlog
|
||||
* mail: Fix listing of mailboxes and aliases in the UI
|
||||
* branding: fix login page title
|
||||
* Only a Cloudron owner can install/update/exec apps with the docker addon
|
||||
* security: reset tokens are only valid for a day
|
||||
* mail: fix eventlog db perms
|
||||
* Fix various bugs in the disk graphs
|
||||
* Fix collectd installation
|
||||
* graphs: sort disk contents by usage
|
||||
* backups: show apps that are not automatically backed up in backup view
|
||||
* turn: deny local address peers https://www.rtcsec.com/2020/04/01-slack-webrtc-turn-compromise/
|
||||
|
||||
[5.1.3]
|
||||
* Fix crash with misconfigured reverse proxy
|
||||
* Fix issue where invitation links are not working anymore
|
||||
|
||||
[5.1.4]
|
||||
* Add support for custom .well-known documents to be served
|
||||
* Add ECDHE-RSA-AES128-SHA256 to cipher list
|
||||
* Fix GPG signature verification
|
||||
|
||||
[5.1.5]
|
||||
* Check for .well-known routes upstream as fallback. This broke nextcloud's caldav/carddav
|
||||
|
||||
[5.2.0]
|
||||
* acme: request ECC certs
|
||||
* less-strict DKIM check to allow users to set a stronger DKIM key
|
||||
* Add members only flag to mailing list
|
||||
* oauth: add backward compat layer for backup and uninstall
|
||||
* fix bug in disk usage sorting
|
||||
* mail: aliases can be across domains
|
||||
* mail: allow an external MX to be set
|
||||
* Add UI to download backup config as JSON (and import it)
|
||||
* Ensure stopped apps are getting backed up
|
||||
* Add OVH Object Storage backend
|
||||
* Add per-app redis status and configuration to Services
|
||||
* spam: large emails were not scanned
|
||||
* mail relay: fix delivery event log
|
||||
* manual update check always gets the latest updates
|
||||
* graphs: fix issue where large number of apps would crash the box code (query param limit exceeded)
|
||||
* backups: fix various security issues in encypted backups (thanks @mehdi)
|
||||
* graphs: add app graphs
|
||||
* older encrypted backups cannot be used in this version
|
||||
* Add backup listing UI
|
||||
* stopping an app will stop dependent services
|
||||
* Add new wasabi s3 storage region us-east-2
|
||||
* mail: Fix bug where SRS translation was done on the main domain instead of mailing list domain
|
||||
* backups: add retention policy
|
||||
* Drop `NET_RAW` caps from container preventing sniffing of network traffic
|
||||
|
||||
[5.2.1]
|
||||
* Fix app disk graphs
|
||||
* restart apps on addon container change
|
||||
|
||||
[5.2.2]
|
||||
* regression: import UI
|
||||
* Mbps -> MBps
|
||||
* Remove verbose logs
|
||||
* Set dmode in tar extract
|
||||
* mail: fix crash in audit logs
|
||||
* import: fix crash because encryption is unset
|
||||
* create redis with the correct label
|
||||
|
||||
[5.2.3]
|
||||
* Do not restart stopped apps
|
||||
|
||||
[5.2.4]
|
||||
* mail: enable/disable incoming mail was showing an error
|
||||
* Do not trigger backup of stopped apps. Instead, we will just retain it's existing backups
|
||||
based on retention policy
|
||||
* remove broken disk graphs
|
||||
* fix OVH backups
|
||||
|
||||
[5.3.0]
|
||||
* better nginx config for higher loads
|
||||
* backups: add CIFS storage provider
|
||||
* backups: add SSHFS storage provider
|
||||
* backups: add NFS storage provider
|
||||
* s3: use vhost style
|
||||
* Fix crash when redis config was set
|
||||
* Update schedule was unselected in the UI
|
||||
* cloudron-setup: --provider is now optional
|
||||
* show warning for unstable updates
|
||||
* add forumUrl to app manifest
|
||||
* postgresql: add unaccent extension for peertube
|
||||
* mail: Add Auto-Submitted header to NDRs
|
||||
* backups: ensure that the latest backup of installed apps is always preserved
|
||||
* add nginx logs
|
||||
* mail: make authentication case insensitive
|
||||
* Fix timeout issues in postgresql and mysql addon
|
||||
* Do not count stopped apps for memory use
|
||||
* LDAP group synchronization
|
||||
|
||||
[5.3.1]
|
||||
* better nginx config for higher loads
|
||||
* backups: add CIFS storage provider
|
||||
* backups: add SSHFS storage provider
|
||||
* backups: add NFS storage provider
|
||||
* s3: use vhost style
|
||||
* Fix crash when redis config was set
|
||||
* Update schedule was unselected in the UI
|
||||
* cloudron-setup: --provider is now optional
|
||||
* show warning for unstable updates
|
||||
* add forumUrl to app manifest
|
||||
* postgresql: add unaccent extension for peertube
|
||||
* mail: Add Auto-Submitted header to NDRs
|
||||
* backups: ensure that the latest backup of installed apps is always preserved
|
||||
* add nginx logs
|
||||
* mail: make authentication case insensitive
|
||||
* Fix timeout issues in postgresql and mysql addon
|
||||
* Do not count stopped apps for memory use
|
||||
* LDAP group synchronization
|
||||
|
||||
[5.3.2]
|
||||
* Do not install sshfs package
|
||||
* 'provider' is not required anymore in various API calls
|
||||
* redis: Set maxmemory and maxmemory-policy
|
||||
* Add mlock capability to manifest (for vault app)
|
||||
|
||||
[5.3.3]
|
||||
* Fix issue where some postinstall messages where causing angular to infinite loop
|
||||
|
||||
[5.3.4]
|
||||
* Fix issue in database error handling
|
||||
|
||||
[5.4.0]
|
||||
* Update nginx to 1.18 for various security fixes
|
||||
* Add ping capability (for statping app)
|
||||
* Fix bug where aliases were displayed incorrectly in SOGo
|
||||
* Add univention as LDAP provider
|
||||
* Bump max_connection for postgres addon to 200
|
||||
* mail: Add pagination to mailing list API
|
||||
* Allow admin to lock email and display name of users
|
||||
* Allow admin to ensure all users have 2FA setup
|
||||
* ami: fix regression where we didn't send provider as part of get status call
|
||||
* nginx: hide version
|
||||
* backups: add b2 provider
|
||||
* Add filemanager webinterface
|
||||
* Add darkmode
|
||||
* Add note that password reset and invite links expire in 24 hours
|
||||
|
||||
[5.4.1]
|
||||
* Update nginx to 1.18 for various security fixes
|
||||
* Add ping capability (for statping app)
|
||||
* Fix bug where aliases were displayed incorrectly in SOGo
|
||||
* Add univention as LDAP provider
|
||||
* Bump max_connection for postgres addon to 200
|
||||
* mail: Add pagination to mailing list API
|
||||
* Allow admin to lock email and display name of users
|
||||
* Allow admin to ensure all users have 2FA setup
|
||||
* ami: fix regression where we didn't send provider as part of get status call
|
||||
* nginx: hide version
|
||||
* backups: add b2 provider
|
||||
* Add filemanager webinterface
|
||||
* Add darkmode
|
||||
* Add note that password reset and invite links expire in 24 hours
|
||||
|
||||
[5.5.0]
|
||||
* postgresql: update to PostgreSQL 11
|
||||
* postgresql: add citext extension to whitelist for loomio
|
||||
* postgresql: add btree_gist,postgres_fdw,pg_stat_statements,plpgsql extensions for gitlab
|
||||
* SFTP/Filebrowser: fix access of external data directories
|
||||
* Fix contrast issues in dark mode
|
||||
* Add option to delete mailbox data when mailbox is delete
|
||||
* Allow days/hours of backups and updates to be configurable
|
||||
* backup cleaner: fix issue where referenced backups where not counted against time periods
|
||||
* route53: fix issue where verification failed if user had more than 100 zones
|
||||
* rework task workers to run them in a separate cgroup
|
||||
* backups: now much faster thanks to reworking of task worker
|
||||
* When custom fallback cert is set, make sure it's used over LE certs
|
||||
* mongodb: update to MongoDB 4.0.19
|
||||
* List groups ordered by name
|
||||
* Invite links are now valid for a week
|
||||
* Update release GPG key
|
||||
* Add pre-defined variables ($CLOUDRON_APPID) for better post install messages
|
||||
* filemanager: show folder first
|
||||
|
||||
[5.6.0]
|
||||
* Remove IP nginx configuration that redirects to dashboard after activation
|
||||
* dashboard: looks for search string in app title as well
|
||||
* Add vaapi caps for transcoding
|
||||
* Fix issue where the long mongodb database names where causing app indices of rocket.chat to overflow (> 127)
|
||||
* Do not resize swap if swap file exists. This means that users can now control how swap is allocated on their own.
|
||||
* SFTP: fix issue where parallel rebuilds would cause an error
|
||||
* backups: make part size configurable
|
||||
* mail: set max email size
|
||||
* mail: allow mail server location to be set
|
||||
* spamassassin: custom configs and wl/bl
|
||||
* Do not automatically update to unstable release
|
||||
* scheduler: reduce container churn
|
||||
* mail: add API to set banner
|
||||
* Fix bug where systemd 237 ignores --nice value in systemd-run
|
||||
* postgresql: enable uuid-ossp extension
|
||||
* firewall: add blocklist
|
||||
* HTTP URLs now redirect directly to the HTTPS of the final domain
|
||||
* linode: Add singapore region
|
||||
* ovh: add sydney region
|
||||
* s3: makes multi-part copies in parallel
|
||||
|
||||
[5.6.1]
|
||||
* Blocklists are now stored in a text file instead of json
|
||||
* regenerate nginx configs
|
||||
|
||||
[5.6.2]
|
||||
* Update docker to 19.03.12
|
||||
* Fix sorting of user listing in the UI
|
||||
* namecheap: fix crash when server returns invalid response
|
||||
* unlink ghost file automatically on successful login
|
||||
* Bump mysql addon connection limit to 200
|
||||
* Fix install issue where `/dev/dri` may not be present
|
||||
* import: when importing filesystem backups, the input box is a path
|
||||
* firewall: fix race condition where blocklist was not added in correct position in the FORWARD chain
|
||||
* services: fix issue where services where scaled up/down too fast
|
||||
* turn: realm variable was not updated properly on dashboard change
|
||||
* nginx: add splash pages for IP based browser access
|
||||
* Give services panel a separate top-level view
|
||||
* Add app state filter
|
||||
* gcs: copy concurrency was not used
|
||||
* Mention why an app update cannot be applied and provide shortcut to start the app if stopped
|
||||
* Remove version from footer into the setting view
|
||||
* Give services panel a separate top-level view
|
||||
* postgresql: set collation order explicity when creating database to C.UTF-8 (for confluence)
|
||||
* rsync: fix error while goes missing when syncing
|
||||
* Pre-select app domain by default in the redirection drop down
|
||||
* robots: preseve leading and trailing whitespaces/newlines
|
||||
|
||||
[5.6.3]
|
||||
* Fix postgres locale issue
|
||||
|
||||
[6.0.0]
|
||||
* Focal support
|
||||
* Reduce duration of self-signed certs to 800 days
|
||||
* Better backup config filename when downloading
|
||||
* branding: footer can have template variables like %YEAR% and %VERSION%
|
||||
* sftp: secure the API with a token
|
||||
* filemanager: Add extract context menu item
|
||||
* Do not download docker images if present locally
|
||||
* sftp: disable access to non-admins by default
|
||||
* postgresql: whitelist pgcrypto extension for loomio
|
||||
* filemanager: Add new file creation action and collapse new and upload actions
|
||||
* rsync: add warning to remove lifecycle rules
|
||||
* Add volume management
|
||||
* backups: adjust node's heap size based on memory limit
|
||||
* s3: diasble per-chunk timeout
|
||||
* logs: more descriptive log file names on download
|
||||
* collectd: remove collectd config when app stopped (and add it back when started)
|
||||
* Apps can optionally request an authwall to be installed in front of them
|
||||
* mailbox can now owned by a group
|
||||
* linode: enable dns provider in setup view
|
||||
* dns: apps can now use the dns port
|
||||
* httpPaths: allow apps to specify forwarding from custom paths to container ports (for OLS)
|
||||
* add elasticemail smtp relay option
|
||||
* mail: add option to fts using solr
|
||||
* mail: change the namespace separator of new installations to /
|
||||
* mail: enable acl
|
||||
* Disable THP
|
||||
* filemanager: allow download dirs as zip files
|
||||
* aws: add china region
|
||||
* security: fix issue where apps could send with any username (but valid password)
|
||||
* i18n support
|
||||
|
||||
[6.0.1]
|
||||
* app: add export route
|
||||
* mail: on location change, fix lock up when one or more domains have invalid credentials
|
||||
* mail: fix crash because of write after timeout closure
|
||||
* scaleway: fix installation issue where THP is not enabled in kernel
|
||||
|
||||
[6.1.0]
|
||||
* mail: update haraka to 2.8.27. this fixes zero-length queue file crash
|
||||
* update: set/unset appStoreId from the update route
|
||||
* proxyauth: Do not follow redirects
|
||||
* proxyauth: add 2FA
|
||||
* appstore: add category translations
|
||||
* appstore: add media category
|
||||
* prepend the version to assets when sourcing to avoid cache hits on update
|
||||
* filemanger: list volumes of the app
|
||||
* Display upload size and size progress
|
||||
* nfs: chown the backups for hardlinks to work
|
||||
* remove user add/remove/role change email notifications
|
||||
* persist update indicator across restarts
|
||||
* cloudron-setup: add --generate-setup-token
|
||||
* dashboard: pass accessToken query param to automatically login
|
||||
* wellknown: add a way to set well known docs
|
||||
* oom: notification mails have links to dashboard
|
||||
* collectd: do not install xorg* packages
|
||||
* apptask: backup/restore tasks now use the backup memory limit configuration
|
||||
* eventlog: add logout event
|
||||
* mailbox: include alias in mailbox search
|
||||
* proxyAuth: add path exclusion
|
||||
* turn: fix for CVE-2020-26262
|
||||
* app password: fix regression where apps are not listed anymore in the UI
|
||||
* Support for multiDomain apps (domain aliases)
|
||||
* netcup: add dns provider
|
||||
* Container swap size is now dynamically determined based on system RAM/swap ratio
|
||||
|
||||
[6.1.1]
|
||||
* Fix bug where platform does not start if memory limits could not be applied
|
||||
|
||||
[6.1.2]
|
||||
* App disk usage was not shown in graphs
|
||||
* Email autoconfig
|
||||
* Fix SOGo login
|
||||
|
||||
[6.2.0]
|
||||
* ovh: object storage URL has changed from s3 to storage subdomain
|
||||
* ionos: add profit bricks object storage
|
||||
* update node to 14.15.4
|
||||
* update docker to 20.10.3
|
||||
* new base image 3.0.0
|
||||
* postgresql updated to 12.5
|
||||
* redis updated to 5.0.7
|
||||
* dovecot updated to 2.3.7
|
||||
* proxyAuth: fix docker UA detection
|
||||
* registry config: add UI to disable it
|
||||
* update solr to 8.8.1
|
||||
* firewall: fix issue where script errored when having more than 15 wl/bl ports
|
||||
* If groups are used, do not allow app installation without choosing the access settings
|
||||
* tls addon
|
||||
* Do not overwrite existing DMARC record
|
||||
* Sync dns records
|
||||
* Dry run restore
|
||||
* linode: show cloudron is installing when user SSHs
|
||||
* mysql: disable bin logs
|
||||
* Show cancel task button if task is still running after 2 minutes
|
||||
* filemanager: fix various bugs involving file names with spaces
|
||||
* Change Referrer-policy default to 'same-origin'
|
||||
* rsync: preserve and restore symlinks
|
||||
* Clean up backups function now removes missing backups
|
||||
|
||||
[6.2.1]
|
||||
* Avoid updown notifications on full restore
|
||||
* Add retries to downloader logic in installer
|
||||
|
||||
[6.2.2]
|
||||
* Fix ENOBUFS issue with backups when collecting fs metadata
|
||||
|
||||
[6.2.3]
|
||||
* Fix addon crashes with missing databases
|
||||
* Update mail container for LMTP cert fix
|
||||
* Fix services view showing yellow icon
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,5 +1,5 @@
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2019 Cloudron UG
|
||||
Copyright (c) 2020 Cloudron UG
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
|
||||
|
||||
34
README.md
34
README.md
@@ -29,9 +29,9 @@ anyone to effortlessly host web applications on their server on their own terms.
|
||||
* Trivially migrate to another server keeping your apps and data (for example, switch your
|
||||
infrastructure provider or move to a bigger server).
|
||||
|
||||
* Comprehensive [REST API](https://cloudron.io/documentation/developer/api/).
|
||||
* Comprehensive [REST API](https://docs.cloudron.io/api/).
|
||||
|
||||
* [CLI](https://cloudron.io/documentation/cli/) to configure apps.
|
||||
* [CLI](https://docs.cloudron.io/custom-apps/cli/) to configure apps.
|
||||
|
||||
* Alerts, audit logs, graphs, dns management ... and much more
|
||||
|
||||
@@ -41,25 +41,37 @@ Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudr
|
||||
|
||||
## Installing
|
||||
|
||||
[Install script](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
[Install script](https://docs.cloudron.io/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
|
||||
**Note:** This repo is a small part of what gets installed on your server - there is
|
||||
the dashboard, database addons, graph container, base image etc. Cloudron also relies
|
||||
on external services such as the App Store for apps to be installed. As such, don't
|
||||
clone this repo and npm install and expect something to work.
|
||||
|
||||
## Documentation
|
||||
## Development
|
||||
|
||||
* [Documentation](https://cloudron.io/documentation/)
|
||||
This is the backend code of Cloudron. The frontend code is [here](https://git.cloudron.io/cloudron/dashboard).
|
||||
|
||||
## Related repos
|
||||
The way to develop is to first install a full instance of Cloudron in a VM. Then you can use the [hotfix](https://git.cloudron.io/cloudron/cloudron-machine)
|
||||
tool to patch the VM with the latest code.
|
||||
|
||||
The [base image repo](https://git.cloudron.io/cloudron/docker-base-image) is the parent image of all
|
||||
the containers in the Cloudron.
|
||||
```
|
||||
SSH_PASSPHRASE=sshkeypassword cloudron-machine hotfix --cloudron my.example.com --release 6.0.0 --ssh-key keyname
|
||||
```
|
||||
|
||||
## Community
|
||||
## License
|
||||
|
||||
* [Chat](https://chat.cloudron.io)
|
||||
Please note that the Cloudron code is under a source-available license. This is not the same as an
|
||||
open source license but ensures the code is available for introspection (and hacking!).
|
||||
|
||||
## Contributions
|
||||
|
||||
Just to give some heads up, we are a bit restrictive in merging changes. We are a small team and
|
||||
would like to keep our maintenance burden low. It might be best to discuss features first in the [forum](https://forum.cloudron.io),
|
||||
to also figure out how many other people will use it to justify maintenance for a feature.
|
||||
|
||||
## Support
|
||||
|
||||
* [Documentation](https://docs.cloudron.io/)
|
||||
* [Forum](https://forum.cloudron.io/)
|
||||
* [Support](mailto:support@cloudron.io)
|
||||
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
assertNotEmpty() {
|
||||
: "${!1:? "$1 is not set."}"
|
||||
}
|
||||
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
|
||||
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
|
||||
|
||||
INSTANCE_TYPE="t2.micro"
|
||||
BLOCK_DEVICE="DeviceName=/dev/sda1,Ebs={VolumeSize=20,DeleteOnTermination=true,VolumeType=gp2}"
|
||||
SSH_KEY_NAME="id_rsa_yellowtent"
|
||||
|
||||
revision=$(git rev-parse HEAD)
|
||||
ami_name=""
|
||||
server_id=""
|
||||
server_ip=""
|
||||
destroy_server="yes"
|
||||
deploy_env="prod"
|
||||
image_id=""
|
||||
|
||||
args=$(getopt -o "" -l "revision:,name:,no-destroy,env:,region:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--env) deploy_env="$2"; shift 2;;
|
||||
--revision) revision="$2"; shift 2;;
|
||||
--name) ami_name="$2"; shift 2;;
|
||||
--no-destroy) destroy_server="no"; shift 2;;
|
||||
--region)
|
||||
case "$2" in
|
||||
"us-east-1")
|
||||
image_id="ami-6edd3078"
|
||||
security_group="sg-a5e17fd9"
|
||||
subnet_id="subnet-b8fbc0f1"
|
||||
;;
|
||||
"eu-central-1")
|
||||
image_id="ami-5aee2235"
|
||||
security_group="sg-19f5a770" # everything open on eu-central-1
|
||||
subnet_id=""
|
||||
;;
|
||||
*)
|
||||
echo "Unknown aws region $2"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
export AWS_DEFAULT_REGION="$2" # used by the aws cli tool
|
||||
shift 2
|
||||
;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
# TODO fix this
|
||||
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY}"
|
||||
export AWS_SECRET_ACCESS_KEY="${AWS_ACCESS_SECRET}"
|
||||
|
||||
readonly ssh_keys="${HOME}/.ssh/id_rsa_yellowtent"
|
||||
readonly SSH="ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
|
||||
if [[ ! -f "${ssh_keys}" ]]; then
|
||||
echo "caas ssh key is missing at ${ssh_keys} (pick it up from secrets repo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${image_id}" ]]; then
|
||||
echo "--region is required (us-east-1 or eu-central-1)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function get_pretty_revision() {
|
||||
local git_rev="$1"
|
||||
local sha1=$(git rev-parse --short "${git_rev}" 2>/dev/null)
|
||||
|
||||
echo "${sha1}"
|
||||
}
|
||||
|
||||
function wait_for_ssh() {
|
||||
echo "=> Waiting for ssh connection"
|
||||
while true; do
|
||||
echo -n "."
|
||||
|
||||
if $SSH ubuntu@${server_ip} echo "hello"; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 5
|
||||
done
|
||||
}
|
||||
|
||||
now=$(date "+%Y-%m-%d-%H%M%S")
|
||||
pretty_revision=$(get_pretty_revision "${revision}")
|
||||
|
||||
if [[ -z "${ami_name}" ]]; then
|
||||
ami_name="box-${deploy_env}-${pretty_revision}-${now}"
|
||||
fi
|
||||
|
||||
echo "=> Create EC2 instance"
|
||||
id=$(aws ec2 run-instances --image-id "${image_id}" --instance-type "${INSTANCE_TYPE}" --security-group-ids "${security_group}" --block-device-mappings "${BLOCK_DEVICE}" --key-name "${SSH_KEY_NAME}" --subnet-id "${subnet_id}" --associate-public-ip-address \
|
||||
| $JSON Instances \
|
||||
| $JSON 0.InstanceId)
|
||||
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "Instance created ID $id"
|
||||
|
||||
echo "=> Waiting for instance to get a public IP"
|
||||
while true; do
|
||||
server_ip=$(aws ec2 describe-instances --instance-ids ${id} \
|
||||
| $JSON Reservations.0.Instances \
|
||||
| $JSON 0.PublicIpAddress)
|
||||
|
||||
if [[ ! -z "${server_ip}" ]]; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Got public IP ${server_ip}"
|
||||
|
||||
wait_for_ssh
|
||||
|
||||
echo "=> Fetching cloudron-setup"
|
||||
while true; do
|
||||
|
||||
if $SSH ubuntu@${server_ip} wget "https://cloudron.io/cloudron-setup" -O "cloudron-setup"; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
echo "=> Running cloudron-setup"
|
||||
$SSH ubuntu@${server_ip} sudo /bin/bash "cloudron-setup" --env "${deploy_env}" --provider "ami" --skip-reboot
|
||||
|
||||
wait_for_ssh
|
||||
|
||||
echo "=> Removing ssh key"
|
||||
$SSH ubuntu@${server_ip} sudo rm /home/ubuntu/.ssh/authorized_keys /root/.ssh/authorized_keys
|
||||
|
||||
echo "=> Creating AMI"
|
||||
image_id=$(aws ec2 create-image --instance-id "${id}" --name "${ami_name}" | $JSON ImageId)
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "Creating AMI with Id ${image_id}"
|
||||
|
||||
echo "=> Waiting for AMI to be created"
|
||||
while true; do
|
||||
state=$(aws ec2 describe-images --image-ids ${image_id} \
|
||||
| $JSON Images \
|
||||
| $JSON 0.State)
|
||||
|
||||
if [[ "${state}" == "available" ]]; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [[ "${destroy_server}" == "yes" ]]; then
|
||||
echo "=> Deleting EC2 instance"
|
||||
|
||||
while true; do
|
||||
state=$(aws ec2 terminate-instances --instance-id "${id}" \
|
||||
| $JSON TerminatingInstances \
|
||||
| $JSON 0.CurrentState.Name)
|
||||
|
||||
if [[ "${state}" == "shutting-down" ]]; then
|
||||
echo ""
|
||||
break
|
||||
fi
|
||||
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Done."
|
||||
echo ""
|
||||
echo "New AMI is: ${image_id}"
|
||||
echo ""
|
||||
@@ -1,261 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ -z "${DIGITAL_OCEAN_TOKEN}" ]]; then
|
||||
echo "Script requires DIGITAL_OCEAN_TOKEN env to be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z "${JSON}" ]]; then
|
||||
echo "Script requires JSON env to be set to path of JSON binary"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly CURL="curl --retry 5 -s -u ${DIGITAL_OCEAN_TOKEN}:"
|
||||
|
||||
function debug() {
|
||||
echo "$@" >&2
|
||||
}
|
||||
|
||||
function get_ssh_key_id() {
|
||||
id=$($CURL "https://api.digitalocean.com/v2/account/keys" \
|
||||
| $JSON ssh_keys \
|
||||
| $JSON -c "this.name === \"$1\"" \
|
||||
| $JSON 0.id)
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "$id"
|
||||
}
|
||||
|
||||
function create_droplet() {
|
||||
local ssh_key_id="$1"
|
||||
local box_name="$2"
|
||||
|
||||
local image_region="sfo2"
|
||||
local ubuntu_image_slug="ubuntu-16-04-x64"
|
||||
local box_size="1gb"
|
||||
|
||||
local data="{\"name\":\"${box_name}\",\"size\":\"${box_size}\",\"region\":\"${image_region}\",\"image\":\"${ubuntu_image_slug}\",\"ssh_keys\":[ \"${ssh_key_id}\" ],\"backups\":false}"
|
||||
|
||||
id=$($CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets" | $JSON droplet.id)
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "$id"
|
||||
}
|
||||
|
||||
function get_droplet_ip() {
|
||||
local droplet_id="$1"
|
||||
ip=$($CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}" | $JSON "droplet.networks.v4[0].ip_address")
|
||||
[[ -z "$ip" ]] && exit 1
|
||||
echo "$ip"
|
||||
}
|
||||
|
||||
function get_droplet_id() {
|
||||
local droplet_name="$1"
|
||||
id=$($CURL "https://api.digitalocean.com/v2/droplets?per_page=200" | $JSON "droplets" | $JSON -c "this.name === '${droplet_name}'" | $JSON "[0].id")
|
||||
[[ -z "$id" ]] && exit 1
|
||||
echo "$id"
|
||||
}
|
||||
|
||||
function power_off_droplet() {
|
||||
local droplet_id="$1"
|
||||
local data='{"type":"power_off"}'
|
||||
local response=$($CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions")
|
||||
local event_id=`echo "${response}" | $JSON action.id`
|
||||
|
||||
if [[ -z "${event_id}" ]]; then
|
||||
debug "Got no event id, assuming already powered off."
|
||||
debug "Response: ${response}"
|
||||
return
|
||||
fi
|
||||
|
||||
debug "Powered off droplet. Event id: ${event_id}"
|
||||
debug -n "Waiting for droplet to power off"
|
||||
|
||||
while true; do
|
||||
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
|
||||
if [[ "${event_status}" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
debug -n "."
|
||||
sleep 10
|
||||
done
|
||||
debug ""
|
||||
}
|
||||
|
||||
function power_on_droplet() {
|
||||
local droplet_id="$1"
|
||||
local data='{"type":"power_on"}'
|
||||
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions" | $JSON action.id`
|
||||
|
||||
debug "Powered on droplet. Event id: ${event_id}"
|
||||
|
||||
if [[ -z "${event_id}" ]]; then
|
||||
debug "Got no event id, assuming already powered on"
|
||||
return
|
||||
fi
|
||||
|
||||
debug -n "Waiting for droplet to power on"
|
||||
|
||||
while true; do
|
||||
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
|
||||
if [[ "${event_status}" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
debug -n "."
|
||||
sleep 10
|
||||
done
|
||||
debug ""
|
||||
}
|
||||
|
||||
function get_image_id() {
|
||||
local snapshot_name="$1"
|
||||
local image_id=""
|
||||
|
||||
if ! response=$($CURL "https://api.digitalocean.com/v2/images?per_page=200"); then
|
||||
echo "Failed to get image listing. ${response}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! image_id=$(echo "$response" \
|
||||
| $JSON images \
|
||||
| $JSON -c "this.name === \"${snapshot_name}\"" 0.id); then
|
||||
echo "Failed to parse curl response: ${response}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -z "${image_id}" ]]; then
|
||||
echo "Failed to get image id of ${snapshot_name}. reponse: ${response}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "${image_id}"
|
||||
}
|
||||
|
||||
function snapshot_droplet() {
|
||||
local droplet_id="$1"
|
||||
local snapshot_name="$2"
|
||||
local data="{\"type\":\"snapshot\",\"name\":\"${snapshot_name}\"}"
|
||||
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions" | $JSON action.id`
|
||||
|
||||
debug "Droplet snapshotted as ${snapshot_name}. Event id: ${event_id}"
|
||||
debug -n "Waiting for snapshot to complete"
|
||||
|
||||
while true; do
|
||||
if ! response=$($CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}"); then
|
||||
echo "Could not get action status. ${response}"
|
||||
continue
|
||||
fi
|
||||
if ! event_status=$(echo "${response}" | $JSON action.status); then
|
||||
echo "Could not parse action.status from response. ${response}"
|
||||
continue
|
||||
fi
|
||||
if [[ "${event_status}" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
debug -n "."
|
||||
sleep 10
|
||||
done
|
||||
debug "! done"
|
||||
|
||||
if ! image_id=$(get_image_id "${snapshot_name}"); then
|
||||
return 1
|
||||
fi
|
||||
echo "${image_id}"
|
||||
}
|
||||
|
||||
function destroy_droplet() {
|
||||
local droplet_id="$1"
|
||||
# TODO: check for 204 status
|
||||
$CURL -X DELETE "https://api.digitalocean.com/v2/droplets/${droplet_id}"
|
||||
debug "Droplet destroyed"
|
||||
debug ""
|
||||
}
|
||||
|
||||
function transfer_image() {
|
||||
local image_id="$1"
|
||||
local region_slug="$2"
|
||||
local data="{\"type\":\"transfer\",\"region\":\"${region_slug}\"}"
|
||||
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/images/${image_id}/actions" | $JSON action.id`
|
||||
echo "${event_id}"
|
||||
}
|
||||
|
||||
function wait_for_image_event() {
|
||||
local image_id="$1"
|
||||
local event_id="$2"
|
||||
|
||||
debug -n "Waiting for ${event_id}"
|
||||
|
||||
while true; do
|
||||
local event_status=`$CURL "https://api.digitalocean.com/v2/images/${image_id}/actions/${event_id}" | $JSON action.status`
|
||||
if [[ "${event_status}" == "completed" ]]; then
|
||||
break
|
||||
fi
|
||||
debug -n "."
|
||||
sleep 10
|
||||
done
|
||||
debug ""
|
||||
}
|
||||
|
||||
function transfer_image_to_all_regions() {
|
||||
local image_id="$1"
|
||||
|
||||
xfer_events=()
|
||||
image_regions=(ams2) ## sfo1 is where the image is created
|
||||
for image_region in ${image_regions[@]}; do
|
||||
xfer_event=$(transfer_image ${image_id} ${image_region})
|
||||
echo "Image transfer to ${image_region} initiated. Event id: ${xfer_event}"
|
||||
xfer_events+=("${xfer_event}")
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "Image transfer initiated, but they will take some time to get transferred."
|
||||
|
||||
for xfer_event in ${xfer_events[@]}; do
|
||||
$vps wait_for_image_event "${image_id}" "${xfer_event}"
|
||||
done
|
||||
}
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
debug "<command> <params...>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case $1 in
|
||||
get_ssh_key_id)
|
||||
get_ssh_key_id "${@:2}"
|
||||
;;
|
||||
|
||||
create)
|
||||
create_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
get_id)
|
||||
get_droplet_id "${@:2}"
|
||||
;;
|
||||
|
||||
get_ip)
|
||||
get_droplet_ip "${@:2}"
|
||||
;;
|
||||
|
||||
power_on)
|
||||
power_on_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
power_off)
|
||||
power_off_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
snapshot)
|
||||
snapshot_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
destroy)
|
||||
destroy_droplet "${@:2}"
|
||||
;;
|
||||
|
||||
transfer_image_to_all_regions)
|
||||
transfer_image_to_all_regions "${@:2}"
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown command $1"
|
||||
exit 1
|
||||
esac
|
||||
@@ -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,11 +29,12 @@ 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 \
|
||||
mysql_package=$([[ "${ubuntu_version}" == "20.04" ]] && echo "mysql-server-8.0" || echo "mysql-server-5.7")
|
||||
apt-get -y install --no-install-recommends \
|
||||
acl \
|
||||
apparmor \
|
||||
build-essential \
|
||||
cifs-utils \
|
||||
cron \
|
||||
@@ -39,12 +42,12 @@ apt-get -y install \
|
||||
debconf-utils \
|
||||
dmsetup \
|
||||
$gpg_package \
|
||||
ipset \
|
||||
iptables \
|
||||
libpython2.7 \
|
||||
linux-generic \
|
||||
logrotate \
|
||||
mysql-server-5.7 \
|
||||
nginx-full \
|
||||
$mysql_package \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
resolvconf \
|
||||
@@ -52,21 +55,29 @@ apt-get -y install \
|
||||
tzdata \
|
||||
unattended-upgrades \
|
||||
unbound \
|
||||
unzip \
|
||||
xfsprogs
|
||||
|
||||
echo "==> installing nginx for xenial for TLSv3 support"
|
||||
curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-2~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/nginx.deb
|
||||
rm /tmp/nginx.deb
|
||||
|
||||
# on some providers like scaleway the sudo file is changed and we want to keep the old one
|
||||
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
|
||||
apt-get -o Dpkg::Options::="--force-confold" install -y --no-install-recommends sudo
|
||||
|
||||
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
|
||||
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
|
||||
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
|
||||
|
||||
echo "==> Installing node.js"
|
||||
mkdir -p /usr/local/node-10.15.1
|
||||
curl -sL https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.15.1
|
||||
ln -sf /usr/local/node-10.15.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.15.1/bin/npm /usr/bin/npm
|
||||
apt-get install -y python # Install python which is required for npm rebuild
|
||||
readonly node_version=14.15.4
|
||||
mkdir -p /usr/local/node-${node_version}
|
||||
curl -sL https://nodejs.org/dist/v${node_version}/node-v${node_version}-linux-x64.tar.gz | tar zxf - --strip-components=1 -C /usr/local/node-${node_version}
|
||||
ln -sf /usr/local/node-${node_version}/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-${node_version}/bin/npm /usr/bin/npm
|
||||
apt-get install -y --no-install-recommends python # Install python which is required for npm rebuild
|
||||
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
|
||||
|
||||
# https://docs.docker.com/engine/installation/linux/ubuntulinux/
|
||||
@@ -77,9 +88,10 @@ mkdir -p /etc/systemd/system/docker.service.d
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
|
||||
# there are 3 packages for docker - containerd, CLI and the daemon
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
readonly docker_version=20.10.3
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.4.3-1_amd64.deb" -o /tmp/containerd.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
@@ -92,7 +104,7 @@ fi
|
||||
|
||||
# do not upgrade grub because it might prompt user and break this script
|
||||
echo "==> Enable memory accounting"
|
||||
apt-get -y --no-upgrade install grub2-common
|
||||
apt-get -y --no-upgrade --no-install-recommends install grub2-common
|
||||
sed -e 's/^GRUB_CMDLINE_LINUX="\(.*\)"$/GRUB_CMDLINE_LINUX="\1 cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
|
||||
update-grub
|
||||
|
||||
@@ -111,19 +123,37 @@ for image in ${images}; do
|
||||
done
|
||||
|
||||
echo "==> Install collectd"
|
||||
if ! apt-get install -y collectd collectd-utils; then
|
||||
# without this, libnotify4 will install gnome-shell
|
||||
apt-get install -y libnotify4 --no-install-recommends
|
||||
if ! apt-get install -y --no-install-recommends libcurl3-gnutls collectd collectd-utils; then
|
||||
# FQDNLookup is true in default debian config. The box code has a custom collectd.conf that fixes this
|
||||
echo "Failed to install collectd. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
|
||||
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
|
||||
fi
|
||||
# https://bugs.launchpad.net/ubuntu/+source/collectd/+bug/1872281
|
||||
[[ "${ubuntu_version}" == "20.04" ]] && echo -e "\nLD_PRELOAD=/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so" >> /etc/default/collectd
|
||||
|
||||
# some hosts like atlantic install ntp which conflicts with timedatectl. https://serverfault.com/questions/1024770/ubuntu-20-04-time-sync-problems-and-possibly-incorrect-status-information
|
||||
echo "==> Configuring host"
|
||||
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||
if systemctl is-active ntp; then
|
||||
systemctl stop ntp
|
||||
apt purge -y ntp
|
||||
fi
|
||||
timedatectl set-ntp 1
|
||||
# mysql follows the system timezone
|
||||
timedatectl set-timezone UTC
|
||||
|
||||
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
|
||||
echo "==> Adding sshd configuration warning"
|
||||
sed -e '/Port 22/ i # NOTE: Cloudron only supports moving SSH to port 202. See https://docs.cloudron.io/security/#securing-ssh-access' -i /etc/ssh/sshd_config
|
||||
|
||||
# https://bugs.launchpad.net/ubuntu/+source/base-files/+bug/1701068
|
||||
echo "==> Disabling motd news"
|
||||
if [ -f "/etc/default/motd-news" ]; then
|
||||
sed -i 's/^ENABLED=.*/ENABLED=0/' /etc/default/motd-news
|
||||
fi
|
||||
|
||||
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed)
|
||||
systemctl stop bind9 || true
|
||||
systemctl disable bind9 || true
|
||||
|
||||
@@ -135,7 +165,7 @@ systemctl disable dnsmasq || true
|
||||
systemctl stop postfix || true
|
||||
systemctl disable postfix || true
|
||||
|
||||
# on ubuntu 18.04, this is the default. this requires resolvconf for DNS to work further after the disable
|
||||
# on ubuntu 18.04 and 20.04, this is the default. this requires resolvconf for DNS to work further after the disable
|
||||
systemctl stop systemd-resolved || true
|
||||
systemctl disable systemd-resolved || true
|
||||
|
||||
@@ -144,4 +174,3 @@ systemctl disable systemd-resolved || true
|
||||
ip6=$([[ -s /proc/net/if_inet6 ]] && echo "yes" || echo "no")
|
||||
echo -e "server:\n\tinterface: 127.0.0.1\n\tdo-ip6: ${ip6}" > /etc/unbound/unbound.conf.d/cloudron-network.conf
|
||||
systemctl restart unbound
|
||||
|
||||
|
||||
88
box.js
88
box.js
@@ -2,57 +2,65 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
// prefix all output with a timestamp
|
||||
// debug() already prefixes and uses process.stderr NOT console.*
|
||||
['log', 'info', 'warn', 'debug', 'error'].forEach(function (log) {
|
||||
var orig = console[log];
|
||||
console[log] = function () {
|
||||
orig.apply(console, [new Date().toISOString()].concat(Array.prototype.slice.call(arguments)));
|
||||
};
|
||||
});
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
let async = require('async'),
|
||||
constants = require('./src/constants.js'),
|
||||
dockerProxy = require('./src/dockerproxy.js'),
|
||||
fs = require('fs'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
paths = require('./src/paths.js'),
|
||||
proxyAuth = require('./src/proxyauth.js'),
|
||||
server = require('./src/server.js');
|
||||
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
console.log(` Cloudron ${constants.VERSION} `);
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
const NOOP_CALLBACK = function () { };
|
||||
|
||||
function setupLogging(callback) {
|
||||
if (process.env.BOX_ENV === 'test') return callback();
|
||||
|
||||
var logfileStream = fs.createWriteStream(paths.BOX_LOG_FILE, { flags:'a' });
|
||||
process.stdout.write = process.stderr.write = logfileStream.write.bind(logfileStream);
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
async.series([
|
||||
server.start,
|
||||
setupLogging,
|
||||
server.start, // do this first since it also inits the database
|
||||
proxyAuth.start,
|
||||
ldap.start,
|
||||
dockerProxy.start
|
||||
], function (error) {
|
||||
if (error) {
|
||||
console.error('Error starting server', error);
|
||||
console.log('Error starting server', error);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('Cloudron is up and running');
|
||||
});
|
||||
|
||||
var NOOP_CALLBACK = function () { };
|
||||
|
||||
process.on('SIGINT', function () {
|
||||
console.log('Received SIGINT. Shutting down.');
|
||||
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
console.log('Received SIGTERM. Shutting down.');
|
||||
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
|
||||
// require those here so that logging handler is already setup
|
||||
require('supererror');
|
||||
const debug = require('debug')('box:box');
|
||||
|
||||
process.on('SIGINT', function () {
|
||||
debug('Received SIGINT. Shutting down.');
|
||||
|
||||
proxyAuth.stop(NOOP_CALLBACK);
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
debug('Received SIGTERM. Shutting down.');
|
||||
|
||||
proxyAuth.stop(NOOP_CALLBACK);
|
||||
server.stop(NOOP_CALLBACK);
|
||||
ldap.stop(NOOP_CALLBACK);
|
||||
dockerProxy.stop(NOOP_CALLBACK);
|
||||
setTimeout(process.exit.bind(process), 3000);
|
||||
});
|
||||
|
||||
process.on('uncaughtException', function (error) {
|
||||
console.error((error && error.stack) ? error.stack : error);
|
||||
setTimeout(process.exit.bind(process, 1), 3000);
|
||||
});
|
||||
|
||||
console.log(`Cloudron is up and running. Logs are at ${paths.BOX_LOG_FILE}`); // this goes to journalctl
|
||||
});
|
||||
|
||||
@@ -12,8 +12,6 @@ exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
console.dir(results);
|
||||
|
||||
async.eachSeries(results, function (r, next) {
|
||||
db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next);
|
||||
}, done);
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
crypto = require('crypto'),
|
||||
fs = require('fs'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
tldjs = require('tldjs');
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM apps, subdomains WHERE apps.id=subdomains.appId AND type="primary"', function (error, apps) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
27
migrations/20191115053638-appdb-add-mailboxDomain.js
Normal file
27
migrations/20191115053638-appdb-add-mailboxDomain.js
Normal file
@@ -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();
|
||||
};
|
||||
15
migrations/20200129052822-apps-add-cpuShares.js
Normal file
15
migrations/20200129052822-apps-add-cpuShares.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
26
migrations/20200131232251-appPasswords-create-table.js
Normal file
26
migrations/20200131232251-appPasswords-create-table.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
22
migrations/20200205172146-remove-authcodedb.js
Normal file
22
migrations/20200205172146-remove-authcodedb.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
24
migrations/20200206154808-remove-clients.js
Normal file
24
migrations/20200206154808-remove-clients.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
17
migrations/20200214051201-domains-drop-locked.js
Normal file
17
migrations/20200214051201-domains-drop-locked.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
40
migrations/20200221211850-users-add-role.js
Normal file
40
migrations/20200221211850-users-add-role.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
28
migrations/20200331164304-apps-alter-mailboxDomain.js
Normal file
28
migrations/20200331164304-apps-alter-mailboxDomain.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
17
migrations/20200417235435-mailboxes-add-membersOnly.js
Normal file
17
migrations/20200417235435-mailboxes-add-membersOnly.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
28
migrations/20200420013715-mailboxes-add-aliasDomain.js
Normal file
28
migrations/20200420013715-mailboxes-add-aliasDomain.js
Normal file
@@ -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);
|
||||
};
|
||||
15
migrations/20200427044800-apps-add-servicesConfigJson.js
Normal file
15
migrations/20200427044800-apps-add-servicesConfigJson.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
15
migrations/20200430035310-apps-add-bindsJson.js
Normal file
15
migrations/20200430035310-apps-add-bindsJson.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
35
migrations/20200512172301-settings-backup-encryption.js
Normal file
35
migrations/20200512172301-settings-backup-encryption.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
24
migrations/20200514045746-backups-add-encryptionVersion.js
Normal file
24
migrations/20200514045746-backups-add-encryptionVersion.js
Normal file
@@ -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();
|
||||
};
|
||||
18
migrations/20200528003052-settings-backup-s3-path-style.js
Normal file
18
migrations/20200528003052-settings-backup-s3-path-style.js
Normal file
@@ -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();
|
||||
};
|
||||
17
migrations/20200604111448-userGroups-add-source.js
Normal file
17
migrations/20200604111448-userGroups-add-source.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
38
migrations/20200614181936-backups-add-identifier.js
Normal file
38
migrations/20200614181936-backups-add-identifier.js
Normal file
@@ -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);
|
||||
});
|
||||
};
|
||||
16
migrations/20200709224148-users-rename-modifiedAt-to-ts.js
Normal file
16
migrations/20200709224148-users-rename-modifiedAt-to-ts.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN ts TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP', function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('ALTER TABLE users DROP COLUMN modifiedAt', callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN ts', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
var backupConfig = JSON.parse(results[0].value);
|
||||
if (backupConfig.intervalSecs === 6 * 60 * 60) { // every 6 hours
|
||||
backupConfig.schedulePattern = '00 00 5,11,17,23 * * *';
|
||||
} else if (backupConfig.intervalSecs === 12 * 60 * 60) { // every 12 hours
|
||||
backupConfig.schedulePattern = '00 00 5,17 * * *';
|
||||
} else if (backupConfig.intervalSecs === 24 * 60 * 60) { // every day
|
||||
backupConfig.schedulePattern = '00 00 23 * * *';
|
||||
} else if (backupConfig.intervalSecs === 3 * 24 * 60 * 60) { // every 3 days (based on day)
|
||||
backupConfig.schedulePattern = '00 00 23 * * 1,3,5';
|
||||
} else if (backupConfig.intervalSecs === 7 * 24 * 60 * 60) { // every week (saturday)
|
||||
backupConfig.schedulePattern = '00 00 23 * * 6';
|
||||
} else { // default to everyday
|
||||
backupConfig.schedulePattern = '00 00 23 * * *';
|
||||
}
|
||||
|
||||
delete backupConfig.intervalSecs;
|
||||
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="admin_domain"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
const adminDomain = results[0].value;
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'INSERT INTO settings (name, value) VALUES (?, ?)', [ 'mail_domain', adminDomain ]),
|
||||
db.runSql.bind(db, 'INSERT INTO settings (name, value) VALUES (?, ?)', [ 'mail_fqdn', `my.${adminDomain}` ])
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name="mail_domain"'),
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name="mail_fqdn"'),
|
||||
], callback);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('SELECT * FROM settings WHERE name=?', ['app_autoupdate_pattern'], function (error, results) {
|
||||
if (error || results.length === 0) return callback(error); // will use defaults from box code
|
||||
|
||||
var updatePattern = results[0].value; // use app auto update patter for the box as well
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name=? OR name=?', ['app_autoupdate_pattern', 'box_autoupdate_pattern']),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', ['autoupdate_pattern', updatePattern]),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
15
migrations/20200824004859-mail-add-bannerJson.js
Normal file
15
migrations/20200824004859-mail-add-bannerJson.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mail ADD COLUMN bannerJson TEXT', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mail DROP COLUMN bannerJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
27
migrations/20200914172345-split-firewall-config.js
Normal file
27
migrations/20200914172345-split-firewall-config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
const OLD_FIREWALL_CONFIG_JSON = '/home/yellowtent/boxdata/firewall-config.json';
|
||||
const PORTS_FILE = '/home/yellowtent/boxdata/firewall/ports.json';
|
||||
const BLOCKLIST_FILE = '/home/yellowtent/boxdata/firewall/blocklist.txt';
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
exports.up = function (db, callback) {
|
||||
if (!fs.existsSync(OLD_FIREWALL_CONFIG_JSON)) return callback();
|
||||
|
||||
try {
|
||||
const dataJson = fs.readFileSync(OLD_FIREWALL_CONFIG_JSON, 'utf8');
|
||||
const data = JSON.parse(dataJson);
|
||||
fs.writeFileSync(BLOCKLIST_FILE, data.blocklist.join('\n') + '\n', 'utf8');
|
||||
fs.writeFileSync(PORTS_FILE, JSON.stringify({ allowed_tcp_ports: data.allowed_tcp_ports }, null, 4), 'utf8');
|
||||
fs.unlinkSync(OLD_FIREWALL_CONFIG_JSON);
|
||||
} catch (error) {
|
||||
console.log('Error migrating old firewall config', error);
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
exports.down = function (db, callback) {
|
||||
callback();
|
||||
};
|
||||
40
migrations/20201028052525-volumes-create-table.js
Normal file
40
migrations/20201028052525-volumes-create-table.js
Normal file
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd1 = 'CREATE TABLE volumes(' +
|
||||
'id VARCHAR(128) NOT NULL UNIQUE,' +
|
||||
'name VARCHAR(256) NOT NULL UNIQUE,' +
|
||||
'hostPath VARCHAR(1024) NOT NULL UNIQUE,' +
|
||||
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
|
||||
'PRIMARY KEY (id)) CHARACTER SET utf8 COLLATE utf8_bin';
|
||||
|
||||
var cmd2 = 'CREATE TABLE appMounts(' +
|
||||
'appId VARCHAR(128) NOT NULL,' +
|
||||
'volumeId VARCHAR(128) NOT NULL,' +
|
||||
'readOnly BOOLEAN DEFAULT 1,' +
|
||||
'UNIQUE KEY appMounts_appId_volumeId (appId, volumeId),' +
|
||||
'FOREIGN KEY(appId) REFERENCES apps(id),' +
|
||||
'FOREIGN KEY(volumeId) REFERENCES volumes(id)) CHARACTER SET utf8 COLLATE utf8_bin;';
|
||||
|
||||
db.runSql(cmd1, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql(cmd2, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN bindsJson', callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE appMounts', function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('DROP TABLE volumes', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
16
migrations/20201110174504-apps-add-proxyAuth.js
Normal file
16
migrations/20201110174504-apps-add-proxyAuth.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN proxyAuth BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN proxyAuth', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
18
migrations/20201113071728-mailboxes-add-ownerType.js
Normal file
18
migrations/20201113071728-mailboxes-add-ownerType.js
Normal file
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes ADD COLUMN ownerType VARCHAR(16)'),
|
||||
db.runSql.bind(db, 'UPDATE mailboxes SET ownerType=?', [ 'user' ]),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY ownerType VARCHAR(16) NOT NULL'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mailboxes DROP COLUMN ownerType', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
13
migrations/20201119072309-apps-remove-httpPort.js
Normal file
13
migrations/20201119072309-apps-remove-httpPort.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN httpPort')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
29
migrations/20201120212726-apps-add-containerIp.js
Normal file
29
migrations/20201120212726-apps-add-containerIp.js
Normal file
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async'),
|
||||
iputils = require('../src/iputils.js');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN containerIp VARCHAR(16) UNIQUE', function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
let baseIp = iputils.intFromIp('172.18.16.0');
|
||||
|
||||
db.all('SELECT * FROM apps', function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
const nextIp = iputils.ipFromInt(++baseIp);
|
||||
db.runSql('UPDATE apps SET containerIp=? WHERE id=?', [ nextIp, app.id ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN containerIp', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
21
migrations/20201204081504-settings-sftp-requireAdmin.js
Normal file
21
migrations/20201204081504-settings-sftp-requireAdmin.js
Normal file
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM settings WHERE name=?', ['platform_config'], function (error, results) {
|
||||
let value;
|
||||
if (error || results.length === 0) {
|
||||
value = { sftp: { requireAdmin: true } };
|
||||
} else {
|
||||
value = JSON.parse(results[0].value);
|
||||
if (!value.sftp) value.sftp = {};
|
||||
value.sftp.requireAdmin = true;
|
||||
}
|
||||
|
||||
// existing installations may not even have the key. so use REPLACE instead of UPDATE
|
||||
db.runSql('REPLACE INTO settings (name, value) VALUES (?, ?)', [ 'platform_config', JSON.stringify(value) ], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
18
migrations/20201222182945-groupMembers-unique-members.js
Normal file
18
migrations/20201222182945-groupMembers-unique-members.js
Normal file
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'CREATE TABLE groupMembers_copy(groupId VARCHAR(128) NOT NULL, userId VARCHAR(128) NOT NULL, FOREIGN KEY(groupId) REFERENCES userGroups(id), FOREIGN KEY(userId) REFERENCES users(id), UNIQUE (groupId, userId)) CHARACTER SET utf8 COLLATE utf8_bin'), // In mysql CREATE TABLE.. LIKE does not copy indexes
|
||||
db.runSql.bind(db, 'INSERT INTO groupMembers_copy SELECT * FROM groupMembers GROUP BY groupId, userId'),
|
||||
db.runSql.bind(db, 'DROP TABLE groupMembers'),
|
||||
db.runSql.bind(db, 'ALTER TABLE groupMembers_copy RENAME TO groupMembers')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE groupMembers DROP INDEX groupMembers_member'),
|
||||
], callback);
|
||||
};
|
||||
51
migrations/20201223014453-domains-add-wellKnownJson.js
Normal file
51
migrations/20201223014453-domains-add-wellKnownJson.js
Normal file
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async'),
|
||||
safe = require('safetydance');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE domains ADD COLUMN wellKnownJson TEXT', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// keep the paths around, so that we don't need to trigger a re-configure. the old nginx config will use the paths
|
||||
// the new one will proxy calls to the box code
|
||||
const WELLKNOWN_DIR = '/home/yellowtent/boxdata/well-known';
|
||||
const output = safe.child_process.execSync('find . -type f -printf "%P\n"', { cwd: WELLKNOWN_DIR, encoding: 'utf8' });
|
||||
if (!output) return callback();
|
||||
const paths = output.trim().split('\n');
|
||||
if (paths.length === 0) return callback(); // user didn't configure any well-known
|
||||
|
||||
let wellKnown = {};
|
||||
for (let path of paths) {
|
||||
const fqdn = path.split('/', 1)[0];
|
||||
const loc = path.slice(fqdn.length+1);
|
||||
const doc = safe.fs.readFileSync(`${WELLKNOWN_DIR}/${path}`, { encoding: 'utf8' });
|
||||
if (!doc) continue;
|
||||
|
||||
wellKnown[fqdn] = {};
|
||||
wellKnown[fqdn][loc] = doc;
|
||||
}
|
||||
|
||||
console.log('Migrating well-known', JSON.stringify(wellKnown, null, 4));
|
||||
|
||||
async.eachSeries(Object.keys(wellKnown), function (fqdn, iteratorDone) {
|
||||
db.runSql('UPDATE domains SET wellKnownJson=? WHERE domain=?', [ JSON.stringify(wellKnown[fqdn]), fqdn ], function (error, result) {
|
||||
if (error) {
|
||||
console.error(error); // maybe the domain does not exist anymore
|
||||
} else if (result.affectedRows === 0) {
|
||||
console.log(`Could not migrate wellknown as domain ${fqdn} is missing`);
|
||||
}
|
||||
iteratorDone();
|
||||
});
|
||||
}, function (error) {
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE domains DROP COLUMN wellKnownJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM settings WHERE name=?', ['platform_config'], function (error, results) {
|
||||
if (error || results.length === 0) return callback(null);
|
||||
|
||||
let value = JSON.parse(results[0].value);
|
||||
|
||||
for (const serviceName of Object.keys(value)) {
|
||||
const service = value[serviceName];
|
||||
if (!service.memorySwap) continue;
|
||||
service.memoryLimit = service.memorySwap;
|
||||
delete service.memorySwap;
|
||||
delete service.memory;
|
||||
}
|
||||
|
||||
db.runSql('UPDATE settings SET value=? WHERE name=?', [ JSON.stringify(value), 'platform_config' ], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
28
migrations/20210120025750-apps-servicesConfig-memoryLimit.js
Normal file
28
migrations/20210120025750-apps-servicesConfig-memoryLimit.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM apps', function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
if (!app.servicesConfigJson) return iteratorDone();
|
||||
|
||||
let servicesConfig = JSON.parse(app.servicesConfigJson);
|
||||
for (const serviceName of Object.keys(servicesConfig)) {
|
||||
const service = servicesConfig[serviceName];
|
||||
if (!service.memorySwap) continue;
|
||||
service.memoryLimit = service.memorySwap;
|
||||
delete service.memorySwap;
|
||||
delete service.memory;
|
||||
}
|
||||
|
||||
db.runSql('UPDATE apps SET servicesConfigJson=? WHERE id=?', [ JSON.stringify(servicesConfig), app.id ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('UPDATE settings SET name=? WHERE name=?', [ 'services_config', 'platform_config' ], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('UPDATE settings SET name=? WHERE name=?', [ 'platform_config', 'services_config' ], callback);
|
||||
};
|
||||
10
migrations/20210204181904-settings-change-ovh-url.js
Normal file
10
migrations/20210204181904-settings-change-ovh-url.js
Normal file
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
/* this contained an invalid migration of OVH URLs from s3 subdomain to storage subdomain. See https://forum.cloudron.io/topic/4584/issue-with-backups-listings-and-saving-backup-config-in-6-2 */
|
||||
callback();
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="registry_config"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
var registryConfig = JSON.parse(results[0].value);
|
||||
if (!registryConfig.provider) registryConfig.provider = 'other';
|
||||
|
||||
db.runSql('UPDATE settings SET value=? WHERE name="registry_config"', [ JSON.stringify(registryConfig) ], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -21,26 +21,31 @@ 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(
|
||||
groupId VARCHAR(128) NOT NULL,
|
||||
userId VARCHAR(128) NOT NULL,
|
||||
FOREIGN KEY(groupId) REFERENCES userGroups(id),
|
||||
FOREIGN KEY(userId) REFERENCES users(id));
|
||||
FOREIGN KEY(userId) REFERENCES users(id),
|
||||
UNIQUE (groupId, userId));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
@@ -52,45 +57,38 @@ 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),
|
||||
healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app last responded
|
||||
containerId VARCHAR(128),
|
||||
manifestJson TEXT,
|
||||
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
|
||||
location VARCHAR(128) NOT NULL,
|
||||
domain VARCHAR(128) NOT NULL,
|
||||
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
|
||||
updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
|
||||
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, // when this db record was updated (useful for UI caching)
|
||||
memoryLimit BIGINT DEFAULT 0,
|
||||
cpuShares INTEGER DEFAULT 512,
|
||||
xFrameOptions VARCHAR(512),
|
||||
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
|
||||
debugModeJson TEXT, // options for development mode
|
||||
reverseProxyConfigJson TEXT, // { robotsTxt, csp }
|
||||
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
|
||||
enableAutomaticUpdate BOOLEAN DEFAULT 1,
|
||||
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
|
||||
mailboxName VARCHAR(128), // mailbox of this app
|
||||
mailboxDomain VARCHAR(128), // mailbox domain of this apps
|
||||
label VARCHAR(128), // display name
|
||||
tagsJson VARCHAR(2048), // array of tags
|
||||
dataDir VARCHAR(256) UNIQUE,
|
||||
taskId INTEGER, // current task
|
||||
errorJson TEXT,
|
||||
servicesConfigJson TEXT, // app services configuration
|
||||
containerIp VARCHAR(16) UNIQUE, // this is not-null because of ip allocation fails, user can 'repair'
|
||||
|
||||
FOREIGN KEY(mailboxDomain) REFERENCES domains(domain),
|
||||
FOREIGN KEY(taskId) REFERENCES tasks(id),
|
||||
PRIMARY KEY(id));
|
||||
|
||||
@@ -102,13 +100,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 +121,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 +148,7 @@ CREATE TABLE IF NOT EXISTS domains(
|
||||
provider VARCHAR(16) NOT NULL,
|
||||
configJson TEXT, /* JSON containing the dns backend provider config */
|
||||
tlsConfigJson TEXT, /* JSON containing the tls provider config */
|
||||
locked BOOLEAN,
|
||||
wellKnownJson TEXT, /* JSON containing well known docs for this domain */
|
||||
|
||||
PRIMARY KEY (domain))
|
||||
|
||||
@@ -169,6 +162,7 @@ CREATE TABLE IF NOT EXISTS mail(
|
||||
mailFromValidation BOOLEAN DEFAULT 1,
|
||||
catchAllJson TEXT,
|
||||
relayJson TEXT,
|
||||
bannerJson TEXT,
|
||||
|
||||
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
|
||||
|
||||
@@ -188,12 +182,16 @@ 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 */
|
||||
ownerType VARCHAR(16) NOT NULL,
|
||||
aliasName VARCHAR(128), /* the target name type is an alias */
|
||||
aliasDomain VARCHAR(128), /* the target domain */
|
||||
membersJson TEXT, /* members of a group. fully qualified */
|
||||
membersOnly BOOLEAN DEFAULT false,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
domain VARCHAR(128),
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES mail(domain),
|
||||
FOREIGN KEY(aliasDomain) REFERENCES mail(domain),
|
||||
UNIQUE (name, domain));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subdomains(
|
||||
@@ -225,8 +223,36 @@ CREATE TABLE IF NOT EXISTS notifications(
|
||||
message TEXT,
|
||||
acknowledged BOOLEAN DEFAULT false,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY appPasswords_name_appId_identifier (name, userId, identifier),
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appPasswords(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
userId VARCHAR(128) NOT NULL,
|
||||
identifier VARCHAR(128) NOT NULL, // resourceId: app id or mail or webadmin
|
||||
hashedPassword VARCHAR(1024) NOT NULL,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY(userId) REFERENCES users(id),
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS volumes(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
name VARCHAR(256) NOT NULL UNIQUE,
|
||||
hostPath VARCHAR(1024) NOT NULL UNIQUE,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appMounts(
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
volumeId VARCHAR(128) NOT NULL,
|
||||
readOnly BOOLEAN DEFAULT 1,
|
||||
UNIQUE KEY appMounts_appId_volumeId (appId, volumeId),
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
FOREIGN KEY(volumeId) REFERENCES volumes(id));
|
||||
|
||||
CHARACTER SET utf8 COLLATE utf8_bin;
|
||||
|
||||
4455
package-lock.json
generated
4455
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
110
package.json
110
package.json
@@ -10,87 +10,77 @@
|
||||
"type": "git",
|
||||
"url": "https://git.cloudron.io/cloudron/box.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0 <=4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/dns": "^1.1.0",
|
||||
"@google-cloud/storage": "^2.5.0",
|
||||
"@google-cloud/dns": "^2.1.0",
|
||||
"@google-cloud/storage": "^5.8.0",
|
||||
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
|
||||
"async": "^2.6.2",
|
||||
"aws-sdk": "^2.476.0",
|
||||
"async": "^3.2.0",
|
||||
"aws-sdk": "^2.850.0",
|
||||
"basic-auth": "^2.0.1",
|
||||
"body-parser": "^1.19.0",
|
||||
"cloudron-manifestformat": "^2.15.0",
|
||||
"cloudron-manifestformat": "^5.10.1",
|
||||
"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",
|
||||
"db-migrate-mysql": "^1.1.10",
|
||||
"debug": "^4.1.1",
|
||||
"dockerode": "^2.5.8",
|
||||
"ejs": "^2.6.1",
|
||||
"ejs-cli": "^2.0.1",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cookie-session": "^1.4.0",
|
||||
"cron": "^1.8.2",
|
||||
"db-migrate": "^0.11.12",
|
||||
"db-migrate-mysql": "^2.1.2",
|
||||
"debug": "^4.3.1",
|
||||
"dockerode": "^3.2.1",
|
||||
"ejs": "^3.1.6",
|
||||
"ejs-cli": "^2.2.1",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.16.2",
|
||||
"js-yaml": "^3.13.1",
|
||||
"json": "^9.0.6",
|
||||
"ldapjs": "^1.0.2",
|
||||
"lodash": "^4.17.11",
|
||||
"ipaddr.js": "^2.0.0",
|
||||
"js-yaml": "^4.0.0",
|
||||
"json": "^10.0.0",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"ldapjs": "^2.2.4",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mime": "^2.4.4",
|
||||
"moment-timezone": "^0.5.25",
|
||||
"morgan": "^1.9.1",
|
||||
"multiparty": "^4.2.1",
|
||||
"mysql": "^2.17.1",
|
||||
"nodemailer": "^6.2.1",
|
||||
"mime": "^2.5.2",
|
||||
"moment": "^2.29.1",
|
||||
"moment-timezone": "^0.5.33",
|
||||
"morgan": "^1.10.0",
|
||||
"multiparty": "^4.2.2",
|
||||
"mustache-express": "^1.3.0",
|
||||
"mysql": "^2.18.1",
|
||||
"nodemailer": "^6.4.18",
|
||||
"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",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"qrcode": "^1.3.3",
|
||||
"readdirp": "^3.0.2",
|
||||
"request": "^2.88.0",
|
||||
"rimraf": "^2.6.3",
|
||||
"qrcode": "^1.4.4",
|
||||
"readdirp": "^3.5.0",
|
||||
"request": "^2.88.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"s3-block-read-stream": "^0.5.0",
|
||||
"safetydance": "^0.7.1",
|
||||
"semver": "^6.1.1",
|
||||
"session-file-store": "^1.3.1",
|
||||
"showdown": "^1.9.0",
|
||||
"safetydance": "^1.1.1",
|
||||
"semver": "^7.3.4",
|
||||
"showdown": "^1.9.1",
|
||||
"speakeasy": "^2.0.0",
|
||||
"split": "^1.0.1",
|
||||
"superagent": "^5.0.9",
|
||||
"superagent": "^6.1.0",
|
||||
"supererror": "^0.7.2",
|
||||
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
|
||||
"tar-stream": "^2.1.0",
|
||||
"tar-stream": "^2.2.0",
|
||||
"tldjs": "^2.3.1",
|
||||
"underscore": "^1.9.1",
|
||||
"uuid": "^3.3.2",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^11.0.0",
|
||||
"ws": "^7.0.0",
|
||||
"xml2js": "^0.4.19"
|
||||
"underscore": "^1.12.0",
|
||||
"uuid": "^8.3.2",
|
||||
"validator": "^13.5.2",
|
||||
"ws": "^7.4.3",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
"expect.js": "*",
|
||||
"hock": "^1.3.3",
|
||||
"js2xmlparser": "^4.0.0",
|
||||
"mocha": "^6.1.4",
|
||||
"hock": "^1.4.1",
|
||||
"js2xmlparser": "^4.0.1",
|
||||
"mocha": "^8.3.0",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^10.0.6",
|
||||
"node-sass": "^4.12.0",
|
||||
"nock": "^13.0.7",
|
||||
"node-sass": "^5.0.0",
|
||||
"recursive-readdir": "^2.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
20
runTests
20
runTests
@@ -2,11 +2,11 @@
|
||||
|
||||
set -eu
|
||||
|
||||
readonly SOURCE_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly source_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly DATA_DIR="${HOME}/.cloudron_test"
|
||||
readonly DEFAULT_TESTS="./src/test/*-test.js ./src/routes/test/*-test.js"
|
||||
|
||||
! "${SOURCE_dir}/src/test/checkInstall" && exit 1
|
||||
! "${source_dir}/src/test/checkInstall" && exit 1
|
||||
|
||||
# cleanup old data dirs some of those docker container data requires sudo to be removed
|
||||
echo "=> Provide root password to purge any leftover data in ${DATA_DIR} and load apparmor profile:"
|
||||
@@ -22,19 +22,27 @@ fi
|
||||
mkdir -p ${DATA_DIR}
|
||||
cd ${DATA_DIR}
|
||||
mkdir -p appsdata
|
||||
mkdir -p boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
|
||||
mkdir -p platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
|
||||
mkdir -p boxdata/profileicons boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com boxdata/sftp/ssh
|
||||
mkdir -p platformdata/addons/mail/banner platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
|
||||
sudo mkdir -p /mnt/cloudron-test-music /media/cloudron-test-music # volume test
|
||||
|
||||
# translations
|
||||
mkdir -p box/dashboard/dist/translation
|
||||
cp -r ${source_dir}/../dashboard/dist/translation/* box/dashboard/dist/translation
|
||||
|
||||
# put cert
|
||||
echo "=> Generating a localhost selfsigned cert"
|
||||
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.localhost"))
|
||||
|
||||
# generate legacy key format for sftp
|
||||
ssh-keygen -m PEM -t rsa -f boxdata/sftp/ssh/ssh_host_rsa_key -q -N ""
|
||||
|
||||
# clear out any containers
|
||||
echo "=> Delete all docker containers first"
|
||||
docker ps -qa | xargs --no-run-if-empty docker rm -f
|
||||
|
||||
# create docker network (while the infra code does this, most tests skip infra setup)
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
|
||||
|
||||
# create the same mysql server version to test with
|
||||
OUT=`docker inspect mysql-server` || true
|
||||
@@ -59,7 +67,7 @@ echo "=> Ensure database"
|
||||
mysql -h"${MYSQL_IP}" -uroot -ppassword -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
|
||||
echo "=> Run database migrations"
|
||||
cd "${SOURCE_dir}"
|
||||
cd "${source_dir}"
|
||||
BOX_ENV=test DATABASE_URL=mysql://root:password@${MYSQL_IP}/box node_modules/.bin/db-migrate up
|
||||
|
||||
echo "=> Run tests with mocha"
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
function exitHandler() {
|
||||
rm -f /etc/update-motd.d/91-cloudron-install-in-progress
|
||||
}
|
||||
|
||||
trap exitHandler EXIT
|
||||
|
||||
# change this to a hash when we make a upgrade release
|
||||
readonly LOG_FILE="/var/log/cloudron-setup.log"
|
||||
readonly MINIMUM_DISK_SIZE_GB="18" # this is the size of "/" and required to fit in docker images 18 is a safe bet for different reporting on 20GB min
|
||||
@@ -41,35 +47,39 @@ if systemctl -q is-active box; then
|
||||
fi
|
||||
|
||||
initBaseImage="true"
|
||||
# provisioning data
|
||||
provider=""
|
||||
provider="generic"
|
||||
requestedVersion=""
|
||||
installServerOrigin="https://api.cloudron.io"
|
||||
apiServerOrigin="https://api.cloudron.io"
|
||||
webServerOrigin="https://cloudron.io"
|
||||
sourceTarballUrl=""
|
||||
rebootServer="true"
|
||||
license=""
|
||||
setupToken=""
|
||||
|
||||
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,generate-setup-token" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
|
||||
--help) echo "See https://docs.cloudron.io/installation/ on how to install Cloudron"; exit 0;;
|
||||
--provider) provider="$2"; shift 2;;
|
||||
--version) requestedVersion="$2"; shift 2;;
|
||||
--env)
|
||||
if [[ "$2" == "dev" ]]; then
|
||||
apiServerOrigin="https://api.dev.cloudron.io"
|
||||
webServerOrigin="https://dev.cloudron.io"
|
||||
installServerOrigin="https://api.dev.cloudron.io"
|
||||
elif [[ "$2" == "staging" ]]; then
|
||||
apiServerOrigin="https://api.staging.cloudron.io"
|
||||
webServerOrigin="https://staging.cloudron.io"
|
||||
installServerOrigin="https://api.staging.cloudron.io"
|
||||
elif [[ "$2" == "unstable" ]]; then
|
||||
installServerOrigin="https://api.dev.cloudron.io"
|
||||
fi
|
||||
shift 2;;
|
||||
--license) license="$2"; shift 2;;
|
||||
--skip-baseimage-init) initBaseImage="false"; shift;;
|
||||
--skip-reboot) rebootServer="false"; shift;;
|
||||
--generate-setup-token) setupToken="$(openssl rand -hex 10)"; shift;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
@@ -83,55 +93,34 @@ fi
|
||||
|
||||
# Only --help works with mismatched ubuntu
|
||||
ubuntu_version=$(lsb_release -rs)
|
||||
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04 or 18.04" > /dev/stderr
|
||||
if [[ "${ubuntu_version}" != "16.04" && "${ubuntu_version}" != "18.04" && "${ubuntu_version}" != "20.04" ]]; then
|
||||
echo "Cloudron requires Ubuntu 16.04, 18.04 or 20.04" > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Install MOTD file for stack script style installations. this is removed by the trap exit handler. Heredoc quotes prevents parameter expansion
|
||||
cat > /etc/update-motd.d/91-cloudron-install-in-progress <<'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
printf "**********************************************************************\n\n"
|
||||
|
||||
printf "\t\t\tWELCOME TO CLOUDRON\n"
|
||||
printf "\t\t\t-------------------\n"
|
||||
|
||||
printf '\n\e[1;32m%-6s\e[m\n\n' "Cloudron is installing. Run 'tail -f /var/log/cloudron-setup.log' to view progress."
|
||||
|
||||
printf "Cloudron overview - https://docs.cloudron.io/ \n"
|
||||
printf "Cloudron setup - https://docs.cloudron.io/installation/#setup \n"
|
||||
|
||||
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
|
||||
|
||||
printf "**********************************************************************\n"
|
||||
EOF
|
||||
chmod +x /etc/update-motd.d/91-cloudron-install-in-progress
|
||||
|
||||
# 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})"
|
||||
@@ -144,32 +133,20 @@ echo " Join us at https://forum.cloudron.io for any questions."
|
||||
echo ""
|
||||
|
||||
if [[ "${initBaseImage}" == "true" ]]; then
|
||||
echo "=> Installing software-properties-common"
|
||||
if ! apt-get install -y software-properties-common &>> "${LOG_FILE}"; then
|
||||
echo "Could not install software-properties-common (for add-apt-repository below). See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Ensure required apt sources"
|
||||
if ! add-apt-repository universe &>> "${LOG_FILE}"; then
|
||||
echo "Could not add required apt sources (for nginx-full). See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Updating apt and installing script dependencies"
|
||||
if ! apt-get update &>> "${LOG_FILE}"; then
|
||||
echo "Could not update package repositories. See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
|
||||
if ! DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y install --no-install-recommends curl python3 ubuntu-standard software-properties-common -y &>> "${LOG_FILE}"; then
|
||||
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=> Checking version"
|
||||
if ! releaseJson=$($curl -s "${apiServerOrigin}/api/v1/releases?boxVersion=${requestedVersion}"); then
|
||||
if ! releaseJson=$($curl -s "${installServerOrigin}/api/v1/releases?boxVersion=${requestedVersion}"); then
|
||||
echo "Failed to get release information"
|
||||
exit 1
|
||||
fi
|
||||
@@ -195,49 +172,46 @@ 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
|
||||
[[ ! -z "${setupToken}" ]] && echo "${setupToken}" > /etc/cloudron/SETUP_TOKEN
|
||||
|
||||
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
|
||||
if [[ -z "${setupToken}" ]]; then
|
||||
url="https://${ip}"
|
||||
else
|
||||
url="https://${ip}/?setupToken=${setupToken}"
|
||||
fi
|
||||
echo -e "\n\n${GREEN}After reboot, visit ${url} 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
|
||||
@@ -245,7 +219,7 @@ if [[ "${rebootServer}" == "true" ]]; then
|
||||
read -p "The server has to be rebooted to apply all the settings. Reboot now ? [Y/n] " yn
|
||||
yn=${yn:-y}
|
||||
case $yn in
|
||||
[Yy]* ) systemctl reboot;;
|
||||
[Yy]* ) exitHandler; systemctl reboot;;
|
||||
* ) exit;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -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' AND username IS NOT NULL ORDER BY createdAt 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} . This password may only be used once. ${ghost_file} will be automatically removed after use."
|
||||
exit 0
|
||||
;;
|
||||
--) break;;
|
||||
@@ -52,7 +57,7 @@ if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
|
||||
echo ""
|
||||
df -h
|
||||
echo ""
|
||||
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/troubleshooting/#recovery-after-disk-full"
|
||||
echo "To recover from a full disk, follow the guide at https://docs.cloudron.io/troubleshooting/#recovery-after-disk-full"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -68,6 +73,9 @@ echo -n "Generating Cloudron Support stats..."
|
||||
# clear file
|
||||
rm -rf $OUT
|
||||
|
||||
echo -e $LINE"DASHBOARD DOMAIN"$LINE >> $OUT
|
||||
mysql -NB -uroot -ppassword -e "SELECT value FROM box.settings WHERE name='admin_fqdn'" &>> $OUT 2>/dev/null || true
|
||||
|
||||
echo -e $LINE"PROVIDER"$LINE >> $OUT
|
||||
cat /etc/cloudron/PROVIDER &>> $OUT || true
|
||||
|
||||
@@ -80,21 +88,21 @@ 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
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
echo -e $LINE"Interface Info"$LINE >> $OUT
|
||||
ip addr &>> $OUT
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
@@ -107,7 +115,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
|
||||
|
||||
31
scripts/cloudron-translation-update
Executable file
31
scripts/cloudron-translation-update
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
# This script downloads new translation data from weblate at https://translate.cloudron.io
|
||||
|
||||
OUT="/home/yellowtent/box/dashboard/dist/translation"
|
||||
|
||||
# We require root
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root. Run with sudo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=> Downloading new translation files..."
|
||||
curl https://translate.cloudron.io/download/cloudron/dashboard/?format=zip -o /tmp/lang.zip
|
||||
|
||||
echo "=> Unpacking..."
|
||||
unzip -jo /tmp/lang.zip -d $OUT
|
||||
chown -R yellowtent:yellowtent $OUT
|
||||
# unzip put very restrictive permissions
|
||||
chmod ua+r $OUT/*
|
||||
|
||||
echo "=> Cleanup..."
|
||||
rm /tmp/lang.zip
|
||||
|
||||
echo "=> Done"
|
||||
|
||||
echo ""
|
||||
echo "Reload the dashboard to see the new translations"
|
||||
echo ""
|
||||
@@ -41,8 +41,8 @@ if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v10.15.1" ]]; then
|
||||
echo "This script requires node 10.15.1"
|
||||
if [[ "$(node --version)" != "v14.15.4" ]]; then
|
||||
echo "This script requires node 14.15.4"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -11,9 +11,12 @@ if [[ ${EUID} -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
readonly USER=yellowtent
|
||||
readonly BOX_SRC_DIR=/home/${USER}/box
|
||||
readonly BASE_DATA_DIR=/home/${USER}
|
||||
function log() {
|
||||
echo -e "$(date +'%Y-%m-%dT%H:%M:%S')" "==> installer: $1"
|
||||
}
|
||||
|
||||
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)"
|
||||
@@ -22,47 +25,60 @@ readonly box_src_tmp_dir="$(realpath ${script_dir}/..)"
|
||||
readonly ubuntu_version=$(lsb_release -rs)
|
||||
readonly ubuntu_codename=$(lsb_release -cs)
|
||||
|
||||
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
|
||||
readonly is_update=$(systemctl is-active -q box && echo "yes" || echo "no")
|
||||
|
||||
echo "==> installer: updating docker"
|
||||
log "Updating from $(cat $box_src_dir/VERSION) to $(cat $box_src_tmp_dir/VERSION)"
|
||||
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
|
||||
log "updating docker"
|
||||
|
||||
readonly docker_version=20.10.3
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "${docker_version}" ]]; then
|
||||
# there are 3 packages for docker - containerd, CLI and the daemon
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.4.3-1_amd64.deb" -o /tmp/containerd.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_${docker_version}~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
|
||||
echo "==> installer: Waiting for all dpkg tasks to finish..."
|
||||
log "Waiting for all dpkg tasks to finish..."
|
||||
while fuser /var/lib/dpkg/lock; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
while ! dpkg --force-confold --configure -a; do
|
||||
echo "==> installer: Failed to fix packages. Retry"
|
||||
log "Failed to fix packages. Retry"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# the latest docker might need newer packages
|
||||
while ! apt update -y; do
|
||||
echo "==> installer: Failed to update packages. Retry"
|
||||
log "Failed to update packages. Retry"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
while ! apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb; do
|
||||
echo "==> installer: Failed to install docker. Retry"
|
||||
log "Failed to install docker. Retry"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.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
|
||||
readonly nginx_version=$(nginx -v 2>&1)
|
||||
if [[ "${nginx_version}" != *"1.18."* ]]; then
|
||||
log "installing nginx 1.18"
|
||||
$curl -sL http://nginx.org/packages/ubuntu/pool/nginx/n/nginx/nginx_1.18.0-2~${ubuntu_codename}_amd64.deb -o /tmp/nginx.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" --force-yes /tmp/nginx.deb
|
||||
rm /tmp/nginx.deb
|
||||
fi
|
||||
|
||||
log "updating node"
|
||||
readonly node_version=14.15.4
|
||||
if [[ "$(node --version)" != "v${node_version}" ]]; then
|
||||
mkdir -p /usr/local/node-${node_version}
|
||||
$curl -sL https://nodejs.org/dist/v${node_version}/node-v${node_version}-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-${node_version}
|
||||
ln -sf /usr/local/node-${node_version}/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-${node_version}/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-10.18.1
|
||||
fi
|
||||
|
||||
# this is here (and not in updater.js) because rebuild requires the above node
|
||||
@@ -73,31 +89,31 @@ for try in `seq 1 10`; do
|
||||
# however by default npm drops privileges for npm rebuild
|
||||
# https://docs.npmjs.com/misc/config#unsafe-perm
|
||||
if cd "${box_src_tmp_dir}" && npm rebuild --unsafe-perm; then break; fi
|
||||
echo "==> installer: Failed to rebuild, trying again"
|
||||
log "Failed to rebuild, trying again"
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [[ ${try} -eq 10 ]]; then
|
||||
echo "==> installer: npm rebuild failed, giving up"
|
||||
log "npm rebuild failed, giving up"
|
||||
exit 4
|
||||
fi
|
||||
|
||||
echo "==> installer: downloading new addon images"
|
||||
log "downloading new addon images"
|
||||
images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); console.log(i.baseImages.map(function (x) { return x.tag; }).join(' '), Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
|
||||
|
||||
echo -e "\tPulling docker images: ${images}"
|
||||
log "\tPulling docker images: ${images}"
|
||||
for image in ${images}; do
|
||||
if ! docker pull "${image}"; then # this pulls the image using the sha256
|
||||
echo "==> installer: Could not pull ${image}"
|
||||
exit 5
|
||||
fi
|
||||
if ! docker pull "${image%@sha256:*}"; then # this will tag the image for readability
|
||||
echo "==> installer: Could not pull ${image%@sha256:*}"
|
||||
exit 6
|
||||
fi
|
||||
while ! docker pull "${image}"; do # this pulls the image using the sha256
|
||||
log "Could not pull ${image}"
|
||||
sleep 5
|
||||
done
|
||||
while ! docker pull "${image%@sha256:*}"; do # this will tag the image for readability
|
||||
log "Could not pull ${image%@sha256:*}"
|
||||
sleep 5
|
||||
done
|
||||
done
|
||||
|
||||
echo "==> installer: update cloudron-syslog"
|
||||
log "update cloudron-syslog"
|
||||
CLOUDRON_SYSLOG_DIR=/usr/local/cloudron-syslog
|
||||
CLOUDRON_SYSLOG="${CLOUDRON_SYSLOG_DIR}/bin/cloudron-syslog"
|
||||
CLOUDRON_SYSLOG_VERSION="1.0.3"
|
||||
@@ -105,26 +121,26 @@ while [[ ! -f "${CLOUDRON_SYSLOG}" || "$(${CLOUDRON_SYSLOG} --version)" != ${CLO
|
||||
rm -rf "${CLOUDRON_SYSLOG_DIR}"
|
||||
mkdir -p "${CLOUDRON_SYSLOG_DIR}"
|
||||
if npm install --unsafe-perm -g --prefix "${CLOUDRON_SYSLOG_DIR}" cloudron-syslog@${CLOUDRON_SYSLOG_VERSION}; then break; fi
|
||||
echo "===> installer: Failed to install cloudron-syslog, trying again"
|
||||
log "Failed to install cloudron-syslog, trying again"
|
||||
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
|
||||
log "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}"
|
||||
log "switching the box code"
|
||||
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"
|
||||
log "calling box setup script"
|
||||
"${box_src_dir}/setup/start.sh"
|
||||
|
||||
125
setup/start.sh
125
setup/start.sh
@@ -5,7 +5,11 @@ set -eu -o pipefail
|
||||
# This script is run after the box code is switched. This means that this script
|
||||
# should pretty much always succeed. No network logic/download code here.
|
||||
|
||||
echo "==> Cloudron Start"
|
||||
function log() {
|
||||
echo -e "$(date +'%Y-%m-%dT%H:%M:%S')" "==> start: $1"
|
||||
}
|
||||
|
||||
log "Cloudron Start"
|
||||
|
||||
readonly USER="yellowtent"
|
||||
readonly HOME_DIR="/home/${USER}"
|
||||
@@ -19,27 +23,34 @@ readonly json="$(realpath ${script_dir}/../node_modules/.bin/json)"
|
||||
readonly ubuntu_version=$(lsb_release -rs)
|
||||
|
||||
cp -f "${script_dir}/../scripts/cloudron-support" /usr/bin/cloudron-support
|
||||
cp -f "${script_dir}/../scripts/cloudron-translation-update" /usr/bin/cloudron-translation-update
|
||||
|
||||
echo "==> Configuring docker"
|
||||
# this needs to match the cloudron/base:2.0.0 gid
|
||||
if ! getent group media; then
|
||||
addgroup --gid 500 --system media
|
||||
fi
|
||||
|
||||
log "Configuring docker"
|
||||
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
systemctl enable apparmor
|
||||
systemctl restart apparmor
|
||||
|
||||
usermod ${USER} -a -G docker
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
# unbound (which starts after box code) relies on this interface to exist. dockerproxy also relies on this.
|
||||
docker network create --subnet=172.18.0.0/16 --ip-range=172.18.0.0/20 cloudron || true
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}"
|
||||
mkdir -p "${APPS_DATA_DIR}"
|
||||
|
||||
# keep these in sync with paths.js
|
||||
echo "==> Ensuring directories"
|
||||
log "Ensuring directories"
|
||||
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/graphite"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mysql"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/postgresql"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/mongodb"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/redis"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/addons/mail/banner"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/acme"
|
||||
@@ -47,19 +58,24 @@ mkdir -p "${PLATFORM_DATA_DIR}/backup"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \
|
||||
"${PLATFORM_DATA_DIR}/logs/updater" \
|
||||
"${PLATFORM_DATA_DIR}/logs/tasks" \
|
||||
"${PLATFORM_DATA_DIR}/logs/crash"
|
||||
"${PLATFORM_DATA_DIR}/logs/crash" \
|
||||
"${PLATFORM_DATA_DIR}/logs/collectd"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/update"
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}/appicons"
|
||||
mkdir -p "${BOX_DATA_DIR}/firewall"
|
||||
mkdir -p "${BOX_DATA_DIR}/profileicons"
|
||||
mkdir -p "${BOX_DATA_DIR}/certs"
|
||||
mkdir -p "${BOX_DATA_DIR}/acme" # acme keys
|
||||
mkdir -p "${BOX_DATA_DIR}/mail/dkim"
|
||||
mkdir -p "${BOX_DATA_DIR}/well-known" # .well-known documents
|
||||
mkdir -p "${BOX_DATA_DIR}/sftp/ssh" # sftp keys
|
||||
|
||||
# ensure backups folder exists and is writeable
|
||||
mkdir -p /var/backups
|
||||
chmod 777 /var/backups
|
||||
|
||||
echo "==> Configuring journald"
|
||||
log "Configuring journald"
|
||||
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
|
||||
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
|
||||
-i /etc/systemd/journald.conf
|
||||
@@ -77,24 +93,28 @@ systemctl daemon-reload
|
||||
systemctl restart systemd-journald
|
||||
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
||||
|
||||
echo "==> Setting up unbound"
|
||||
# Give user access to nginx logs (uses adm group)
|
||||
usermod -a -G adm ${USER}
|
||||
|
||||
log "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"
|
||||
log "Adding systemd services"
|
||||
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
|
||||
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now cloudron-syslog
|
||||
systemctl enable unbound
|
||||
systemctl enable cloudron-syslog
|
||||
systemctl enable cloudron.target
|
||||
systemctl enable box
|
||||
systemctl enable cloudron-firewall
|
||||
systemctl enable --now cloudron-disable-thp
|
||||
|
||||
# update firewall rules
|
||||
systemctl restart cloudron-firewall
|
||||
@@ -108,17 +128,23 @@ systemctl restart unbound
|
||||
# ensure cloudron-syslog runs
|
||||
systemctl restart cloudron-syslog
|
||||
|
||||
echo "==> Configuring sudoers"
|
||||
log "Configuring sudoers"
|
||||
rm -f /etc/sudoers.d/${USER}
|
||||
cp "${script_dir}/start/sudoers" /etc/sudoers.d/${USER}
|
||||
|
||||
echo "==> Configuring collectd"
|
||||
rm -rf /etc/collectd
|
||||
log "Configuring collectd"
|
||||
rm -rf /etc/collectd /var/log/collectd.log
|
||||
ln -sfF "${PLATFORM_DATA_DIR}/collectd" /etc/collectd
|
||||
cp "${script_dir}/start/collectd/collectd.conf" "${PLATFORM_DATA_DIR}/collectd/collectd.conf"
|
||||
if [[ "${ubuntu_version}" == "20.04" ]]; then
|
||||
# https://bugs.launchpad.net/ubuntu/+source/collectd/+bug/1872281
|
||||
if ! grep -q LD_PRELOAD /etc/default/collectd; then
|
||||
echo -e "\nLD_PRELOAD=/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/libpython3.8.so" >> /etc/default/collectd
|
||||
fi
|
||||
fi
|
||||
systemctl restart collectd
|
||||
|
||||
echo "==> Configuring logrotate"
|
||||
log "Configuring logrotate"
|
||||
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
|
||||
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
|
||||
fi
|
||||
@@ -128,10 +154,10 @@ cp "${script_dir}/start/logrotate/"* "${PLATFORM_DATA_DIR}/logrotate.d/"
|
||||
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
|
||||
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/"
|
||||
|
||||
echo "==> Adding motd message for admins"
|
||||
log "Adding motd message for admins"
|
||||
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
|
||||
|
||||
echo "==> Configuring nginx"
|
||||
log "Configuring nginx"
|
||||
# link nginx config to system config
|
||||
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
|
||||
ln -s "${PLATFORM_DATA_DIR}/nginx" /etc/nginx
|
||||
@@ -142,8 +168,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
|
||||
@@ -152,44 +185,64 @@ if [[ ! -f /etc/mysql/mysql.cnf ]] || ! diff -q "${script_dir}/start/mysql.cnf"
|
||||
cp "${script_dir}/start/mysql.cnf" /etc/mysql/mysql.cnf
|
||||
while true; do
|
||||
if ! systemctl list-jobs | grep mysql; then break; fi
|
||||
echo "Waiting for mysql jobs..."
|
||||
log "Waiting for mysql jobs..."
|
||||
sleep 1
|
||||
done
|
||||
while true; do
|
||||
if systemctl restart mysql; then break; fi
|
||||
echo "Restarting MySql again after sometime since this fails randomly"
|
||||
log "Stopping mysql"
|
||||
systemctl stop mysql
|
||||
while mysqladmin ping 2>/dev/null; do
|
||||
log "Waiting for mysql to stop..."
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
systemctl start mysql
|
||||
fi
|
||||
|
||||
# the start/stop of mysql is separate to make sure it got reloaded with latest config and it's up and running before we start the new box code
|
||||
# when using 'system restart mysql', it seems to restart much later and the box code loses connection during platform startup (dangerous!)
|
||||
log "Starting mysql"
|
||||
systemctl start mysql
|
||||
while ! mysqladmin ping 2>/dev/null; do
|
||||
log "Waiting for mysql to start..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
readonly mysql_root_password="password"
|
||||
mysqladmin -u root -ppassword password password # reset default root password
|
||||
if [[ "${ubuntu_version}" == "20.04" ]]; then
|
||||
# mysql 8 added a new caching_sha2_password scheme which mysqljs does not support
|
||||
mysql -u root -p${mysql_root_password} -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '${mysql_root_password}';"
|
||||
fi
|
||||
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
|
||||
echo "==> Migrating data"
|
||||
# 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
|
||||
log "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
|
||||
echo "DB migration failed"
|
||||
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
|
||||
log "DB migration failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f /etc/cloudron/cloudron.conf
|
||||
|
||||
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
|
||||
echo "==> Generating dhparams (takes forever)"
|
||||
log "Generating dhparams (takes forever)"
|
||||
openssl dhparam -out "${BOX_DATA_DIR}/dhparams.pem" 2048
|
||||
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
|
||||
else
|
||||
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
|
||||
fi
|
||||
|
||||
# old installations used to create appdata/<app>/redis which is now part of old backups and prevents restore
|
||||
echo "==> Cleaning up stale redis directories"
|
||||
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} +
|
||||
if [[ ! -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" ]]; then
|
||||
# the key format in Ubuntu 20 changed, so we create keys in legacy format. for older ubuntu, just re-use the host keys
|
||||
# see https://github.com/proftpd/proftpd/issues/793
|
||||
if [[ "${ubuntu_version}" == "20.04" ]]; then
|
||||
ssh-keygen -m PEM -t rsa -f "${BOX_DATA_DIR}/sftp/ssh/ssh_host_rsa_key" -q -N ""
|
||||
else
|
||||
cp /etc/ssh/ssh_host_rsa_key* ${BOX_DATA_DIR}/sftp/ssh
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "==> Changing ownership"
|
||||
log "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
|
||||
chown "${USER}:${USER}" -R "${PLATFORM_DATA_DIR}/nginx" "${PLATFORM_DATA_DIR}/collectd" "${PLATFORM_DATA_DIR}/addons" "${PLATFORM_DATA_DIR}/acme" "${PLATFORM_DATA_DIR}/backup" "${PLATFORM_DATA_DIR}/logs" "${PLATFORM_DATA_DIR}/update"
|
||||
@@ -203,9 +256,9 @@ find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail"
|
||||
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
|
||||
log "Starting Cloudron"
|
||||
systemctl start box
|
||||
|
||||
sleep 2 # give systemd sometime to start the processes
|
||||
|
||||
echo "==> Almost done"
|
||||
log "Almost done"
|
||||
|
||||
14
setup/start/cloudron-disable-thp.sh
Executable file
14
setup/start/cloudron-disable-thp.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
echo "==> Disabling THP"
|
||||
|
||||
# https://docs.couchbase.com/server/current/install/thp-disable.html
|
||||
if [[ -d /sys/kernel/mm/transparent_hugepage ]]; then
|
||||
echo "never" > /sys/kernel/mm/transparent_hugepage/enabled
|
||||
echo "never" > /sys/kernel/mm/transparent_hugepage/defrag
|
||||
else
|
||||
echo "==> kernel does not have THP"
|
||||
fi
|
||||
|
||||
@@ -6,11 +6,40 @@ echo "==> Setting up firewall"
|
||||
iptables -t filter -N CLOUDRON || true
|
||||
iptables -t filter -F CLOUDRON # empty any existing rules
|
||||
|
||||
# NOTE: keep these in sync with src/apps.js validatePortBindings
|
||||
# allow ssh, http, https, ping, dns
|
||||
iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
# ssh is allowed alternately on port 202
|
||||
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT
|
||||
# first setup any user IP block lists
|
||||
ipset create cloudron_blocklist hash:net || true
|
||||
/home/yellowtent/box/src/scripts/setblocklist.sh
|
||||
|
||||
iptables -t filter -A CLOUDRON -m set --match-set cloudron_blocklist src -j DROP
|
||||
# the DOCKER-USER chain is not cleared on docker restart
|
||||
if ! iptables -t filter -C DOCKER-USER -m set --match-set cloudron_blocklist src -j DROP; then
|
||||
iptables -t filter -I DOCKER-USER 1 -m set --match-set cloudron_blocklist src -j DROP
|
||||
fi
|
||||
|
||||
# allow related and establisted connections
|
||||
iptables -t filter -A CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443 -j ACCEPT # 202 is the alternate ssh port
|
||||
|
||||
# whitelist any user ports. we used to use --dports but it has a 15 port limit (XT_MULTI_PORTS)
|
||||
ports_json="/home/yellowtent/boxdata/firewall/ports.json"
|
||||
if allowed_tcp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_tcp_ports.join(','))" 2>/dev/null); then
|
||||
IFS=',' arr=(${allowed_tcp_ports})
|
||||
for p in "${arr[@]}"; do
|
||||
iptables -A CLOUDRON -p tcp -m tcp --dport "${p}" -j ACCEPT
|
||||
done
|
||||
fi
|
||||
|
||||
if allowed_udp_ports=$(node -e "console.log(JSON.parse(fs.readFileSync('${ports_json}', 'utf8')).allowed_udp_ports.join(','))" 2>/dev/null); then
|
||||
IFS=',' arr=(${allowed_udp_ports})
|
||||
for p in "${arr[@]}"; do
|
||||
iptables -A CLOUDRON -p udp -m udp --dport "${p}" -j ACCEPT
|
||||
done
|
||||
fi
|
||||
|
||||
# turn and stun service
|
||||
iptables -t filter -A CLOUDRON -p tcp -m multiport --dports 3478,5349 -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 3478,5349 -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p udp -m multiport --dports 50000:51000 -j ACCEPT
|
||||
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-reply -j ACCEPT
|
||||
@@ -47,8 +76,6 @@ for port in 22 202; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --dport ${port} -m state --state NEW -m recent --update --name "public-${port}" --seconds 10 --hitcount 5 -j CLOUDRON_RATELIMIT_LOG
|
||||
done
|
||||
|
||||
# TODO: move docker platform rules to platform.js so it can be specialized to rate limit only when destination is the mail container
|
||||
|
||||
# docker translates (dnat) 25, 587, 993, 4190 in the PREROUTING step
|
||||
for port in 2525 4190 9993; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --syn ! -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 50 -j CLOUDRON_RATELIMIT_LOG
|
||||
@@ -64,12 +91,12 @@ for port in 3306 5432 6379 27017; do
|
||||
iptables -A CLOUDRON_RATELIMIT -p tcp --syn -s 172.18.0.0/16 -d 172.18.0.0/16 --dport ${port} -m connlimit --connlimit-above 5000 -j CLOUDRON_RATELIMIT_LOG
|
||||
done
|
||||
|
||||
# For ssh, http, https
|
||||
if ! iptables -t filter -C INPUT -j CLOUDRON_RATELIMIT 2>/dev/null; then
|
||||
iptables -t filter -I INPUT 1 -j CLOUDRON_RATELIMIT
|
||||
fi
|
||||
|
||||
# For smtp, imap etc routed via docker/nat
|
||||
# Workaroud issue where Docker insists on adding itself first in FORWARD table
|
||||
# Workaround issue where Docker insists on adding itself first in FORWARD table
|
||||
iptables -D FORWARD -j CLOUDRON_RATELIMIT || true
|
||||
iptables -I FORWARD 1 -j CLOUDRON_RATELIMIT
|
||||
|
||||
echo "==> Setting up firewall done"
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
[[ -f /etc/update-motd.d/91-cloudron-install-in-progress ]] && exit
|
||||
|
||||
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
|
||||
|
||||
if [[ ! -f /etc/cloudron/SETUP_TOKEN ]]; then
|
||||
url="https://${ip}"
|
||||
else
|
||||
setupToken="$(cat /etc/cloudron/SETUP_TOKEN)"
|
||||
url="https://${ip}/?setupToken=${setupToken}"
|
||||
fi
|
||||
|
||||
printf "\t\t\tWELCOME TO CLOUDRON\n"
|
||||
printf "\t\t\t-------------------\n"
|
||||
|
||||
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit https://<IP> on your browser and accept the self-signed certificate to finish setup."
|
||||
printf "Cloudron overview - https://cloudron.io/documentation/ \n"
|
||||
printf "Cloudron setup - https://cloudron.io/documentation/installation/#setup \n"
|
||||
printf '\n\e[1;32m%-6s\e[m\n\n' "Visit ${url} on your browser and accept the self-signed certificate to finish setup."
|
||||
printf "Cloudron overview - https://docs.cloudron.io/ \n"
|
||||
printf "Cloudron setup - https://docs.cloudron.io/installation/#setup \n"
|
||||
else
|
||||
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
|
||||
printf "\t\t\t-----------------------\n"
|
||||
@@ -16,7 +32,7 @@ else
|
||||
printf "Cloudron relies on and may break your installation. Ubuntu security updates\n"
|
||||
printf "are automatically installed on this server every night.\n"
|
||||
printf "\n"
|
||||
printf "Read more at https://cloudron.io/documentation/security/#os-updates\n"
|
||||
printf "Read more at https://docs.cloudron.io/security/#os-updates\n"
|
||||
fi
|
||||
|
||||
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
|
||||
|
||||
@@ -4,6 +4,11 @@ set -eu -o pipefail
|
||||
|
||||
readonly APPS_SWAP_FILE="/apps.swap"
|
||||
|
||||
if [[ -f "${APPS_SWAP_FILE}" ]]; then
|
||||
echo "Swap file already exists at /apps.swap . Skipping"
|
||||
exit
|
||||
fi
|
||||
|
||||
# all sizes are in mb
|
||||
readonly physical_memory=$(LC_ALL=C free -m | awk '/Mem:/ { print $2 }')
|
||||
readonly swap_size=$((${physical_memory} > 4096 ? 4096 : ${physical_memory})) # min(RAM, 4GB) if you change this, fix enoughResourcesAvailable() in client.js
|
||||
|
||||
@@ -57,7 +57,7 @@ LoadPlugin logfile
|
||||
|
||||
<Plugin logfile>
|
||||
LogLevel "info"
|
||||
File "/var/log/collectd.log"
|
||||
File "/home/yellowtent/platformdata/logs/collectd/collectd.log"
|
||||
Timestamp true
|
||||
PrintSeverity false
|
||||
</Plugin>
|
||||
@@ -121,7 +121,7 @@ LoadPlugin memory
|
||||
#LoadPlugin netlink
|
||||
#LoadPlugin network
|
||||
#LoadPlugin nfs
|
||||
LoadPlugin nginx
|
||||
#LoadPlugin nginx
|
||||
#LoadPlugin notify_desktop
|
||||
#LoadPlugin notify_email
|
||||
#LoadPlugin ntpd
|
||||
@@ -149,7 +149,7 @@ LoadPlugin nginx
|
||||
#LoadPlugin statsd
|
||||
LoadPlugin swap
|
||||
#LoadPlugin table
|
||||
LoadPlugin tail
|
||||
#LoadPlugin tail
|
||||
#LoadPlugin tail_csv
|
||||
#LoadPlugin tcpconns
|
||||
#LoadPlugin teamspeak2
|
||||
@@ -197,42 +197,11 @@ LoadPlugin write_graphite
|
||||
IgnoreSelected false
|
||||
</Plugin>
|
||||
|
||||
<Plugin nginx>
|
||||
URL "http://127.0.0.1/nginx_status"
|
||||
</Plugin>
|
||||
|
||||
<Plugin swap>
|
||||
ReportByDevice false
|
||||
ReportBytes true
|
||||
</Plugin>
|
||||
|
||||
<Plugin "tail">
|
||||
<File "/var/log/nginx/error.log">
|
||||
Instance "nginx"
|
||||
<Match>
|
||||
Regex ".*"
|
||||
DSType "CounterInc"
|
||||
Type counter
|
||||
Instance "errors"
|
||||
</Match>
|
||||
</File>
|
||||
<File "/var/log/nginx/access.log">
|
||||
Instance "nginx"
|
||||
<Match>
|
||||
Regex ".*"
|
||||
DSType "CounterInc"
|
||||
Type counter
|
||||
Instance "requests"
|
||||
</Match>
|
||||
<Match>
|
||||
Regex " \".*\" [0-9]+ [0-9]+ ([0-9]+)"
|
||||
DSType GaugeAverage
|
||||
Type delay
|
||||
Instance "response"
|
||||
</Match>
|
||||
</File>
|
||||
</Plugin>
|
||||
|
||||
<Plugin python>
|
||||
# https://blog.dbrgn.ch/2017/3/10/write-a-collectd-python-plugin/
|
||||
ModulePath "/home/yellowtent/box/setup/start/collectd/"
|
||||
|
||||
@@ -6,7 +6,7 @@ disks = []
|
||||
|
||||
def init():
|
||||
global disks
|
||||
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).splitlines()]
|
||||
lines = [s.split() for s in subprocess.check_output(["df", "--type=ext4", "--output=source,target,size,used,avail"]).decode('utf-8').splitlines()]
|
||||
disks = lines[1:] # strip header
|
||||
collectd.info('custom df plugin initialized with %s' % disks)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
performance_schema=OFF
|
||||
max_connections=50
|
||||
# on ec2, without this we get a sporadic connection drop when doing the initial migration
|
||||
max_allowed_packet=32M
|
||||
max_allowed_packet=64M
|
||||
|
||||
# https://mathiasbynens.be/notes/mysql-utf8mb4
|
||||
character-set-server = utf8mb4
|
||||
@@ -15,6 +15,9 @@ collation-server = utf8mb4_unicode_ci
|
||||
# set timezone to UTC
|
||||
default_time_zone='+00:00'
|
||||
|
||||
# disable bin logs. they are only useful in replication mode
|
||||
skip-log-bin
|
||||
|
||||
[mysqldump]
|
||||
quick
|
||||
quote-names
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
user www-data;
|
||||
|
||||
worker_processes 1;
|
||||
# detect based on available CPU cores
|
||||
worker_processes auto;
|
||||
|
||||
# this is 4096 by default. See /proc/<PID>/limits and /etc/security/limits.conf
|
||||
# usually twice the worker_connections (one for uptsream, one for downstream)
|
||||
# see also LimitNOFILE=16384 in systemd drop-in
|
||||
worker_rlimit_nofile 8192;
|
||||
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
# a single worker has these many simultaneous connections max
|
||||
worker_connections 4096;
|
||||
}
|
||||
|
||||
http {
|
||||
@@ -36,23 +43,5 @@ http {
|
||||
# zones for rate limiting
|
||||
limit_req_zone $binary_remote_addr zone=admin_login:10m rate=10r/s; # 10 request a second
|
||||
|
||||
|
||||
# default http server that returns 404 for any domain we are not listening on
|
||||
server {
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
server_name does_not_match_anything;
|
||||
|
||||
# acme challenges (for app installation and re-configure when the vhost config does not exist)
|
||||
location /.well-known/acme-challenge/ {
|
||||
default_type text/plain;
|
||||
alias /home/yellowtent/platformdata/acme/;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
|
||||
include applications/*.conf;
|
||||
}
|
||||
|
||||
@@ -25,9 +25,6 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurecollec
|
||||
Defaults!/home/yellowtent/box/src/scripts/collectlogs.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/retire.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/update.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/update.sh
|
||||
|
||||
@@ -50,3 +47,15 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.s
|
||||
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmmailbox.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmmailbox.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/starttask.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/starttask.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/stoptask.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/stoptask.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/setblocklist.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/setblocklist.sh
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
[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
|
||||
|
||||
15
setup/start/systemd/cloudron-disable-thp.service
Normal file
15
setup/start/systemd/cloudron-disable-thp.service
Normal file
@@ -0,0 +1,15 @@
|
||||
# https://docs.mongodb.com/manual/tutorial/transparent-huge-pages/
|
||||
[Unit]
|
||||
Description=Disable Transparent Huge Pages (THP)
|
||||
DefaultDependencies=no
|
||||
After=sysinit.target local-fs.target
|
||||
Before=docker.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart="/home/yellowtent/box/setup/start/cloudron-disable-thp.sh"
|
||||
RemainAfterExit=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=basic.target
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
[Unit]
|
||||
Description=Cloudron Smartserver
|
||||
Documentation=https://cloudron.io/documentation.html
|
||||
StopWhenUnneeded=true
|
||||
Requires=box.service
|
||||
After=box.service
|
||||
# AllowIsolate=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[Unit]
|
||||
Description=Unbound DNS Resolver
|
||||
After=network.target
|
||||
After=network.target docker.service
|
||||
|
||||
[Service]
|
||||
PIDFile=/run/unbound.pid
|
||||
|
||||
13
setup/start/unbound.conf
Normal file
13
setup/start/unbound.conf
Normal file
@@ -0,0 +1,13 @@
|
||||
server:
|
||||
port: 53
|
||||
interface: 127.0.0.1
|
||||
interface: 172.18.0.1
|
||||
do-ip6: no
|
||||
access-control: 127.0.0.1 allow
|
||||
access-control: 172.18.0.1/16 allow
|
||||
cache-max-negative-ttl: 30
|
||||
cache-max-ttl: 300
|
||||
# enable below for logging to journalctl -u unbound
|
||||
# verbosity: 5
|
||||
# log-queries: yes
|
||||
|
||||
@@ -4,4 +4,4 @@ set -eu -o pipefail
|
||||
|
||||
echo "Stopping cloudron"
|
||||
|
||||
systemctl stop cloudron.target
|
||||
systemctl stop box
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
245
src/appdb.js
245
src/appdb.js
@@ -1,32 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
get: get,
|
||||
getByHttpPort: getByHttpPort,
|
||||
getByContainerId: getByContainerId,
|
||||
add: add,
|
||||
exists: exists,
|
||||
del: del,
|
||||
update: update,
|
||||
getAll: getAll,
|
||||
getPortBindings: getPortBindings,
|
||||
delPortBinding: delPortBinding,
|
||||
get,
|
||||
add,
|
||||
exists,
|
||||
del,
|
||||
update,
|
||||
getAll,
|
||||
getPortBindings,
|
||||
delPortBinding,
|
||||
|
||||
setAddonConfig: setAddonConfig,
|
||||
getAddonConfig: getAddonConfig,
|
||||
getAddonConfigByAppId: getAddonConfigByAppId,
|
||||
getAddonConfigByName: getAddonConfigByName,
|
||||
unsetAddonConfig: unsetAddonConfig,
|
||||
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
|
||||
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
|
||||
setAddonConfig,
|
||||
getAddonConfig,
|
||||
getAddonConfigByAppId,
|
||||
getAddonConfigByName,
|
||||
unsetAddonConfig,
|
||||
unsetAddonConfigByAppId,
|
||||
getAppIdByAddonConfigValue,
|
||||
getByIpAddress,
|
||||
|
||||
setHealth: setHealth,
|
||||
setTask: setTask,
|
||||
getAppStoreIds: getAppStoreIds,
|
||||
setHealth,
|
||||
setTask,
|
||||
getAppStoreIds,
|
||||
|
||||
// subdomain table types
|
||||
SUBDOMAIN_TYPE_PRIMARY: 'primary',
|
||||
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
|
||||
SUBDOMAIN_TYPE_ALIAS: 'alias',
|
||||
|
||||
_clear: clear
|
||||
};
|
||||
@@ -38,17 +38,14 @@ var assert = require('assert'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
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.sso', 'apps.debugModeJson', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.enableAutomaticUpdate',
|
||||
const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
|
||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.accessRestrictionJson', 'apps.memoryLimit', 'apps.cpuShares',
|
||||
'apps.label', 'apps.tagsJson', 'apps.taskId', 'apps.reverseProxyConfigJson', 'apps.servicesConfigJson',
|
||||
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableAutomaticUpdate',
|
||||
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
const SUBDOMAIN_FIELDS = [ 'appId', 'domain', 'subdomain', 'type' ].join(',');
|
||||
const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
function postProcess(result) {
|
||||
assert.strictEqual(typeof result, 'object');
|
||||
@@ -89,16 +86,33 @@ function postProcess(result) {
|
||||
result.sso = !!result.sso; // make it bool
|
||||
result.enableBackup = !!result.enableBackup; // make it bool
|
||||
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
|
||||
result.proxyAuth = !!result.proxyAuth;
|
||||
|
||||
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
|
||||
result.debugMode = safe.JSON.parse(result.debugModeJson);
|
||||
delete result.debugModeJson;
|
||||
|
||||
result.alternateDomains = result.alternateDomains || [];
|
||||
result.alternateDomains.forEach(function (d) {
|
||||
delete d.appId;
|
||||
delete d.type;
|
||||
});
|
||||
assert(result.servicesConfigJson === null || typeof result.servicesConfigJson === 'string');
|
||||
result.servicesConfig = safe.JSON.parse(result.servicesConfigJson) || {};
|
||||
delete result.servicesConfigJson;
|
||||
|
||||
let subdomains = JSON.parse(result.subdomains), domains = JSON.parse(result.domains), subdomainTypes = JSON.parse(result.subdomainTypes);
|
||||
delete result.subdomains;
|
||||
delete result.domains;
|
||||
delete result.subdomainTypes;
|
||||
|
||||
result.alternateDomains = [];
|
||||
result.aliasDomains = [];
|
||||
for (let i = 0; i < subdomainTypes.length; i++) {
|
||||
if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_PRIMARY) {
|
||||
result.location = subdomains[i];
|
||||
result.domain = domains[i];
|
||||
} else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_REDIRECT) {
|
||||
result.alternateDomains.push({ domain: domains[i], subdomain: subdomains[i] });
|
||||
} else if (subdomainTypes[i] === exports.SUBDOMAIN_TYPE_ALIAS) {
|
||||
result.aliasDomains.push({ domain: domains[i], subdomain: subdomains[i] });
|
||||
}
|
||||
}
|
||||
|
||||
let envNames = JSON.parse(result.envNames), envValues = JSON.parse(result.envValues);
|
||||
delete result.envNames;
|
||||
@@ -108,119 +122,67 @@ function postProcess(result) {
|
||||
if (envNames[i]) result.env[envNames[i]] = envValues[i];
|
||||
}
|
||||
|
||||
let volumeIds = JSON.parse(result.volumeIds);
|
||||
delete result.volumeIds;
|
||||
let volumeReadOnlys = JSON.parse(result.volumeReadOnlys);
|
||||
delete result.volumeReadOnlys;
|
||||
|
||||
result.mounts = volumeIds[0] === null ? [] : volumeIds.map((v, idx) => { return { volumeId: v, readOnly: !!volumeReadOnlys[idx] }; }); // NOTE: volumeIds is [null] when volumes of an app is empty
|
||||
|
||||
result.error = safe.JSON.parse(result.errorJson);
|
||||
delete result.errorJson;
|
||||
|
||||
result.taskId = result.taskId ? String(result.taskId) : null;
|
||||
}
|
||||
|
||||
// each query simply join apps table with another table by id. we then join the full result together
|
||||
const PB_QUERY = 'SELECT id, GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes FROM apps LEFT JOIN appPortBindings ON apps.id = appPortBindings.appId GROUP BY apps.id';
|
||||
const ENV_QUERY = 'SELECT id, JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues FROM apps LEFT JOIN appEnvVars ON apps.id = appEnvVars.appId GROUP BY apps.id';
|
||||
const SUBDOMAIN_QUERY = 'SELECT id, JSON_ARRAYAGG(subdomains.subdomain) AS subdomains, JSON_ARRAYAGG(subdomains.domain) AS domains, JSON_ARRAYAGG(subdomains.type) AS subdomainTypes FROM apps LEFT JOIN subdomains ON apps.id = subdomains.appId GROUP BY apps.id';
|
||||
const MOUNTS_QUERY = 'SELECT id, JSON_ARRAYAGG(appMounts.volumeId) AS volumeIds, JSON_ARRAYAGG(appMounts.readOnly) AS volumeReadOnlys FROM apps LEFT JOIN appMounts ON apps.id = appMounts.appId GROUP BY apps.id';
|
||||
const APPS_QUERY = `SELECT ${APPS_FIELDS_PREFIXED}, hostPorts, environmentVariables, portTypes, envNames, envValues, subdomains, domains, subdomainTypes, volumeIds, volumeReadOnlys FROM apps`
|
||||
+ ` LEFT JOIN (${PB_QUERY}) AS q1 on q1.id = apps.id`
|
||||
+ ` LEFT JOIN (${ENV_QUERY}) AS q2 on q2.id = apps.id`
|
||||
+ ` LEFT JOIN (${SUBDOMAIN_QUERY}) AS q3 on q3.id = apps.id`
|
||||
+ ` LEFT JOIN (${MOUNTS_QUERY}) AS q4 on q4.id = apps.id`;
|
||||
|
||||
function get(id, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes, '
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
|
||||
database.query(`${APPS_QUERY} WHERE apps.id = ?`, [ id ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
postProcess(result[0]);
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getByHttpPort(httpPort, callback) {
|
||||
assert.strictEqual(typeof httpPort, 'number');
|
||||
function getByIpAddress(ip, callback) {
|
||||
assert.strictEqual(typeof ip, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
|
||||
database.query(`${APPS_QUERY} WHERE apps.containerIp = ?`, [ ip ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
postProcess(result[0]);
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getByContainerId(containerId, callback) {
|
||||
assert.strictEqual(typeof containerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
postProcess(result[0]);
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
callback(null, result[0]);
|
||||
});
|
||||
}
|
||||
|
||||
function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables, GROUP_CONCAT(appPortBindings.type) AS portTypes,'
|
||||
+ 'JSON_ARRAYAGG(appEnvVars.name) AS envNames, JSON_ARRAYAGG(appEnvVars.value) AS envValues'
|
||||
+ ' FROM apps'
|
||||
+ ' LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId'
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
|
||||
database.query(`${APPS_QUERY} ORDER BY apps.id`, [ ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
results.forEach(postProcess);
|
||||
|
||||
alternateDomains.forEach(function (d) {
|
||||
var domain = results.find(function (a) { return d.appId === a.id; });
|
||||
if (!domain) return;
|
||||
|
||||
domain.alternateDomains = domain.alternateDomains || [];
|
||||
domain.alternateDomains.push(d);
|
||||
});
|
||||
|
||||
results.forEach(postProcess);
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,6 +204,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 +213,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({
|
||||
@@ -290,6 +254,15 @@ function add(id, appStoreId, manifest, location, domain, portBindings, data, cal
|
||||
});
|
||||
}
|
||||
|
||||
if (data.aliasDomains) {
|
||||
data.aliasDomains.forEach(function (d) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)',
|
||||
args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_ALIAS ]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'no such domain'));
|
||||
@@ -347,12 +320,14 @@ function del(id, callback) {
|
||||
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appPasswords WHERE identifier = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (results[3].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
if (results[5].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -385,6 +360,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
|
||||
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
|
||||
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
|
||||
assert(!('aliasDomains' in app) || Array.isArray(app.aliasDomains));
|
||||
assert(!('tags' in app) || Array.isArray(app.tags));
|
||||
assert(!('env' in app) || typeof app.env === 'object');
|
||||
|
||||
@@ -412,22 +388,35 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
}
|
||||
|
||||
if ('location' in app && 'domain' in app) { // must be updated together as they are unique together
|
||||
queries.push({ query: 'UPDATE subdomains SET subdomain = ?, domain = ? WHERE appId = ? AND type = ?', args: [ app.location, app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ]}); // all locations of an app must be updated together
|
||||
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, app.domain, app.location, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
|
||||
if ('alternateDomains' in app) {
|
||||
app.alternateDomains.forEach(function (d) {
|
||||
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
|
||||
});
|
||||
}
|
||||
|
||||
if ('aliasDomains' in app) {
|
||||
app.aliasDomains.forEach(function (d) {
|
||||
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_ALIAS ]});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ('alternateDomains' in app) {
|
||||
queries.push({ query: 'DELETE FROM subdomains WHERE appId = ? AND type = ?', args: [ id, exports.SUBDOMAIN_TYPE_REDIRECT ]});
|
||||
app.alternateDomains.forEach(function (d) {
|
||||
queries.push({ query: 'INSERT INTO subdomains (appId, domain, subdomain, type) VALUES (?, ?, ?, ?)', args: [ id, d.domain, d.subdomain, exports.SUBDOMAIN_TYPE_REDIRECT ]});
|
||||
if ('mounts' in app) {
|
||||
queries.push({ query: 'DELETE FROM appMounts WHERE appId = ?', args: [ id ]});
|
||||
app.mounts.forEach(function (m) {
|
||||
queries.push({ query: 'INSERT INTO appMounts (appId, volumeId, readOnly) VALUES (?, ?, ?)', args: [ id, m.volumeId, m.readOnly ]});
|
||||
});
|
||||
}
|
||||
|
||||
var fields = [ ], values = [ ];
|
||||
for (var p in app) {
|
||||
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig') {
|
||||
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig' || p === 'servicesConfig') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'aliasDomains' && p !== 'env' && p !== 'mounts') {
|
||||
fields.push(p + ' = ?');
|
||||
values.push(app[p]);
|
||||
}
|
||||
|
||||
@@ -10,49 +10,48 @@ var appdb = require('./appdb.js'),
|
||||
docker = require('./docker.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
superagent = require('superagent');
|
||||
|
||||
exports = module.exports = {
|
||||
run: run
|
||||
run
|
||||
};
|
||||
|
||||
const HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
|
||||
const UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
|
||||
const UNHEALTHY_THRESHOLD = 20 * 60 * 1000; // 20 minutes
|
||||
|
||||
const OOM_EVENT_LIMIT = 60 * 60 * 1000; // 60 minutes
|
||||
let gStartTime = null; // time when apphealthmonitor was started
|
||||
let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5 minutes ago
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function setHealth(app, health, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof health, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let now = new Date(), healthTime = app.healthTime, curHealth = app.health;
|
||||
// app starts out with null health
|
||||
// if it became healthy, we update immediately. this is required for ui to say "running" etc
|
||||
// if it became unhealthy/error/dead, wait for a threshold before updating db
|
||||
|
||||
const now = new Date(), lastHealth = app.health;
|
||||
let healthTime = gStartTime > app.healthTime ? gStartTime : app.healthTime; // on box restart, clamp value to start time
|
||||
|
||||
if (health === apps.HEALTH_HEALTHY) {
|
||||
healthTime = now;
|
||||
if (curHealth && curHealth !== apps.HEALTH_HEALTHY) { // app starts out with null health
|
||||
debugApp(app, 'app switched from %s to healthy', curHealth);
|
||||
if (lastHealth && lastHealth !== apps.HEALTH_HEALTHY) { // app starts out with null health
|
||||
debug(`setHealth: ${app.id} (${app.fqdn}) switched from ${lastHealth} to healthy`);
|
||||
|
||||
// do not send mails for dev apps
|
||||
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_UP, auditSource.HEALTH_MONITOR, { app: app });
|
||||
}
|
||||
} else if (Math.abs(now - healthTime) > UNHEALTHY_THRESHOLD) {
|
||||
if (curHealth === apps.HEALTH_HEALTHY) {
|
||||
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
|
||||
if (lastHealth === apps.HEALTH_HEALTHY) {
|
||||
debug(`setHealth: marking ${app.id} (${app.fqdn}) as unhealthy since not seen for more than ${UNHEALTHY_THRESHOLD/(60 * 1000)} minutes`);
|
||||
|
||||
// do not send mails for dev apps
|
||||
if (!app.debugMode) eventlog.add(eventlog.ACTION_APP_DOWN, auditSource.HEALTH_MONITOR, { app: app });
|
||||
}
|
||||
} else {
|
||||
debugApp(app, 'waiting for %s seconds to update the app health', (UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000);
|
||||
debug(`setHealth: ${app.id} (${app.fqdn}) waiting for ${(UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000} to update health`);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
@@ -61,6 +60,7 @@ function setHealth(app, health, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
app.health = health;
|
||||
app.healthTime = healthTime;
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -73,28 +73,19 @@ 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);
|
||||
}
|
||||
|
||||
const manifest = app.manifest;
|
||||
|
||||
docker.inspect(app.containerId, function (error, data) {
|
||||
if (error || !data || !data.State) {
|
||||
debugApp(app, 'Error inspecting container');
|
||||
return setHealth(app, apps.HEALTH_ERROR, callback);
|
||||
}
|
||||
|
||||
if (data.State.Running !== true) {
|
||||
debugApp(app, 'exited');
|
||||
return setHealth(app, apps.HEALTH_DEAD, callback);
|
||||
}
|
||||
if (error || !data || !data.State) return setHealth(app, apps.HEALTH_ERROR, callback);
|
||||
if (data.State.Running !== true) return setHealth(app, apps.HEALTH_DEAD, callback);
|
||||
|
||||
// non-appstore apps may not have healthCheckPath
|
||||
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
|
||||
|
||||
// poll through docker network instead of nginx to bypass any potential oauth proxy
|
||||
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
|
||||
const healthCheckUrl = `http://${app.containerIp}:${manifest.httpPort}${manifest.healthCheckPath}`;
|
||||
superagent
|
||||
.get(healthCheckUrl)
|
||||
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
|
||||
@@ -103,10 +94,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);
|
||||
} else if (res.statusCode >= 403) { // 2xx and 3xx are ok. even 401 and 403 are ok for now (for WP sites)
|
||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||
} else {
|
||||
setHealth(app, apps.HEALTH_HEALTHY, callback);
|
||||
@@ -129,7 +118,7 @@ function getContainerInfo(containerId, callback) {
|
||||
|
||||
/*
|
||||
OOM can be tested using stress tool like so:
|
||||
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
|
||||
docker run -ti -m 100M cloudron/base:2.0.0 /bin/bash
|
||||
apt-get update && apt-get install stress
|
||||
stress --vm 1 --vm-bytes 200M --vm-hang 0
|
||||
*/
|
||||
@@ -180,17 +169,14 @@ function processDockerEvents(intervalSecs, callback) {
|
||||
function processApp(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
apps.getAll(function (error, result) {
|
||||
apps.getAll(function (error, allApps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.each(result, checkAppHealth, function (error) {
|
||||
if (error) console.error(error);
|
||||
async.each(allApps, checkAppHealth, function (error) {
|
||||
const alive = allApps
|
||||
.filter(function (a) { return a.installationState === apps.ISTATE_INSTALLED && a.runState === apps.RSTATE_RUNNING && a.health === apps.HEALTH_HEALTHY; });
|
||||
|
||||
var alive = result
|
||||
.filter(function (a) { return a.installationState === 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);
|
||||
});
|
||||
@@ -201,11 +187,13 @@ function run(intervalSecs, callback) {
|
||||
assert.strictEqual(typeof intervalSecs, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!gStartTime) gStartTime = new Date();
|
||||
|
||||
async.series([
|
||||
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();
|
||||
});
|
||||
|
||||
1969
src/apps.js
1969
src/apps.js
File diff suppressed because it is too large
Load Diff
361
src/appstore.js
361
src/appstore.js
@@ -1,25 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getApps: getApps,
|
||||
getApp: getApp,
|
||||
getAppVersion: getAppVersion,
|
||||
getFeatures,
|
||||
|
||||
registerWithLoginCredentials: registerWithLoginCredentials,
|
||||
registerWithLicense: registerWithLicense,
|
||||
getApps,
|
||||
getApp,
|
||||
getAppVersion,
|
||||
|
||||
purchaseApp: purchaseApp,
|
||||
unpurchaseApp: unpurchaseApp,
|
||||
trackBeginSetup,
|
||||
trackFinishedSetup,
|
||||
|
||||
getSubscription: getSubscription,
|
||||
isFreePlan: isFreePlan,
|
||||
registerWithLoginCredentials,
|
||||
|
||||
sendAliveStatus: sendAliveStatus,
|
||||
purchaseApp,
|
||||
unpurchaseApp,
|
||||
|
||||
getAppUpdate: getAppUpdate,
|
||||
getBoxUpdate: getBoxUpdate,
|
||||
getUserToken,
|
||||
getSubscription,
|
||||
isFreePlan,
|
||||
|
||||
createTicket: createTicket
|
||||
getAppUpdate,
|
||||
getBoxUpdate,
|
||||
|
||||
createTicket
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
@@ -27,21 +30,51 @@ 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'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
users = require('./users.js'),
|
||||
support = require('./support.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,
|
||||
userGroups: false,
|
||||
userRoles: false,
|
||||
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 +122,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 +162,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 +230,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,17 +239,23 @@ 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(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode === 204) return callback(null); // no update
|
||||
if (result.statusCode === 204) return callback(null, null); // no update
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
var updateInfo = result.body;
|
||||
|
||||
if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) {
|
||||
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
|
||||
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Update version invalid or is a downgrade: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
|
||||
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
|
||||
@@ -313,16 +271,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(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
@@ -340,7 +306,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 +322,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 +343,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 +381,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 +389,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) {
|
||||
@@ -426,24 +410,55 @@ function createTicket(info, callback) {
|
||||
apps.get(info.appId, callback);
|
||||
}
|
||||
|
||||
function enableSshIfNeeded(callback) {
|
||||
if (!info.enableSshSupport) return callback();
|
||||
|
||||
support.enableRemoteSupport(true, auditSource, function (error) {
|
||||
// ensure we can at least get the ticket through
|
||||
if (error) debug('Unable to enable SSH support.', error);
|
||||
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
collectAppInfoIfNeeded(function (error, result) {
|
||||
if (error) console.error('Unable to get app info', error);
|
||||
if (result) info.app = result;
|
||||
enableSshIfNeeded(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let url = settings.apiServerOrigin() + '/api/v1/ticket';
|
||||
collectAppInfoIfNeeded(function (error, app) {
|
||||
if (error) return callback(error);
|
||||
if (app) info.app = app;
|
||||
|
||||
info.supportEmail = custom.spec().support.email; // destination address for tickets
|
||||
info.supportEmail = constants.SUPPORT_EMAIL; // destination address for tickets
|
||||
|
||||
superagent.post(url).query({ accessToken: token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
var req = superagent.post(`${settings.apiServerOrigin()}/api/v1/ticket`)
|
||||
.query({ accessToken: token })
|
||||
.timeout(30 * 1000);
|
||||
|
||||
callback(null);
|
||||
// either send as JSON through body or as multipart, depending on attachments
|
||||
if (info.app) {
|
||||
req.field('infoJSON', JSON.stringify(info));
|
||||
|
||||
apps.getLocalLogfilePaths(info.app).forEach(function (filePath) {
|
||||
var logs = safe.child_process.execSync(`tail --lines=1000 ${filePath}`);
|
||||
if (logs) req.attach(path.basename(filePath), logs, path.basename(filePath));
|
||||
});
|
||||
} else {
|
||||
req.send(info);
|
||||
}
|
||||
|
||||
req.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
eventlog.add(eventlog.ACTION_SUPPORT_TICKET, auditSource, info);
|
||||
|
||||
callback(null, { message: `An email was sent to ${constants.SUPPORT_EMAIL}. We will get back shortly!` });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -459,14 +474,20 @@ function getApps(callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/apps`;
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(10 * 1000).end(function (error, result) {
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, unstable: unstable }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
|
||||
if (!result.body.apps) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
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 +498,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(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null, result.body);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
482
src/apptask.js
482
src/apptask.js
@@ -3,30 +3,25 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
run: run,
|
||||
run,
|
||||
|
||||
// exported for testing
|
||||
_reserveHttpPort: reserveHttpPort,
|
||||
_configureReverseProxy: configureReverseProxy,
|
||||
_unconfigureReverseProxy: unconfigureReverseProxy,
|
||||
_createAppDir: createAppDir,
|
||||
_deleteAppDir: deleteAppDir,
|
||||
_verifyManifest: verifyManifest,
|
||||
_registerSubdomains: registerSubdomains,
|
||||
_unregisterSubdomains: unregisterSubdomains,
|
||||
_waitForDnsPropagation: waitForDnsPropagation
|
||||
};
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
var addons = require('./addons.js'),
|
||||
appdb = require('./appdb.js'),
|
||||
const appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
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'),
|
||||
@@ -35,15 +30,15 @@ var addons = require('./addons.js'),
|
||||
ejs = require('ejs'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
fs = require('fs'),
|
||||
iputils = require('./iputils.js'),
|
||||
manifestFormat = require('cloudron-manifestformat'),
|
||||
mkdirp = require('mkdirp'),
|
||||
net = require('net'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
rimraf = require('rimraf'),
|
||||
safe = require('safetydance'),
|
||||
services = require('./services.js'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
@@ -51,8 +46,7 @@ var addons = require('./addons.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 +58,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
|
||||
@@ -90,22 +86,16 @@ function updateApp(app, values, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function reserveHttpPort(app, callback) {
|
||||
function allocateContainerIp(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
let server = net.createServer();
|
||||
server.listen(0, function () {
|
||||
let port = server.address().port;
|
||||
|
||||
updateApp(app, { httpPort: port }, function (error) {
|
||||
server.close(function (/* closeError */) {
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Failed to allocate http port ${port}: ${error.message}`));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
async.retry({ times: 10 }, function (retryCallback) {
|
||||
const iprange = iputils.intFromIp('172.18.20.255') - iputils.intFromIp('172.18.16.1');
|
||||
let rnd = Math.floor(Math.random() * iprange);
|
||||
const containerIp = iputils.ipFromInt(iputils.intFromIp('172.18.16.1') + rnd);
|
||||
updateApp(app, { containerIp }, retryCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
function configureReverseProxy(app, callback) {
|
||||
@@ -140,7 +130,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 +149,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 +164,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 +217,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) {
|
||||
@@ -345,82 +331,6 @@ function removeIcon(app, callback) {
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function registerSubdomains(app, overwrite, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof overwrite, 'boolean');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
sysinfo.getServerIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const allDomains = [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains);
|
||||
|
||||
debug(`registerSubdomain: Will register ${JSON.stringify(allDomains)}`);
|
||||
|
||||
async.eachSeries(allDomains, function (domain, iteratorDone) {
|
||||
async.retry({ times: 200, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Registering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
|
||||
|
||||
// get the current record before updating it
|
||||
domains.getDnsRecords(domain.subdomain, domain.domain, 'A', function (error, values) {
|
||||
if (error && error.reason === BoxError.EXTERNAL_ERROR) return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
|
||||
if (error && error.reason === BoxError.ACCESS_DENIED) return retryCallback(null, new BoxError(BoxError.ACCESS_DENIED, error.message, { domain }));
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, new BoxError(BoxError.NOT_FOUND, error.message, { domain }));
|
||||
if (error) return retryCallback(null, new BoxError(BoxError.EXTERNAL_ERROR, error.message, domain)); // give up for other errors
|
||||
|
||||
if (values.length !== 0 && values[0] === ip) return retryCallback(null); // up-to-date
|
||||
|
||||
// refuse to update any existing DNS record for custom domains that we did not create
|
||||
if (values.length !== 0 && !overwrite) return retryCallback(null, new BoxError(BoxError.ALREADY_EXISTS, 'DNS Record already exists', { domain }));
|
||||
|
||||
domains.upsertDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
|
||||
if (error && (error.reason === BoxError.BUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
|
||||
debug('registerSubdomains: Upsert error. Will retry.', error.message);
|
||||
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
|
||||
}
|
||||
|
||||
retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, domain) : null);
|
||||
});
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result) return iteratorDone(error || result);
|
||||
|
||||
iteratorDone(null);
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterSubdomains(app, allDomains, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert(Array.isArray(allDomains));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
sysinfo.getServerIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(allDomains, function (domain, iteratorDone) {
|
||||
async.retry({ times: 30, interval: 5000 }, function (retryCallback) {
|
||||
debugApp(app, 'Unregistering subdomain: %s%s', domain.subdomain ? (domain.subdomain + '.') : '', domain.domain);
|
||||
|
||||
domains.removeDnsRecords(domain.subdomain, domain.domain, 'A', [ ip ], function (error) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return retryCallback(null, null);
|
||||
if (error && (error.reason === BoxError.SBUSY || error.reason === BoxError.EXTERNAL_ERROR)) {
|
||||
debug('registerSubdomains: Remove error. Will retry.', error.message);
|
||||
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain })); // try again
|
||||
}
|
||||
|
||||
retryCallback(null, error ? new BoxError(BoxError.EXTERNAL_ERROR, error.message, { domain }) : null);
|
||||
});
|
||||
}, function (error, result) {
|
||||
if (error || result) return iteratorDone(error || result);
|
||||
|
||||
iteratorDone();
|
||||
});
|
||||
}, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForDnsPropagation(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -433,12 +343,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) {
|
||||
// now wait for alternateDomains and aliasDomains, if any
|
||||
async.eachSeries(app.alternateDomains.concat(app.aliasDomains), function (domain, iteratorCallback) {
|
||||
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,15 +358,17 @@ 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}`);
|
||||
debugApp(app, `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}`));
|
||||
@@ -487,22 +399,13 @@ function downloadImage(manifest, callback) {
|
||||
}
|
||||
|
||||
function startApp(app, callback){
|
||||
debugApp(app, 'startApp: starting container');
|
||||
|
||||
if (app.runState === apps.RSTATE_STOPPED) return 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 +414,8 @@ function install(app, args, progressCallback, callback) {
|
||||
|
||||
const restoreConfig = args.restoreConfig; // has to be set when restoring
|
||||
const overwriteDns = args.overwriteDns;
|
||||
const skipDnsSetup = args.skipDnsSetup;
|
||||
const oldManifest = args.oldManifest;
|
||||
|
||||
async.series([
|
||||
// this protects against the theoretical possibility of an app being marked for install/restore from
|
||||
@@ -520,38 +425,46 @@ 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);
|
||||
services.teardownAddons(app, addonsToRemove, next);
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
deleteAppDir.bind(null, app, { removeDirectory: false }), // 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),
|
||||
// allocating container ip here, lets the users "repair" an app if allocation fails at appdb.add time
|
||||
allocateContainerIp.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),
|
||||
function setupDnsIfNeeded(done) {
|
||||
if (skipDnsSetup) return done();
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
|
||||
|
||||
domains.registerLocations.bind(null, [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains), { overwriteDns }, progressCallback)
|
||||
], done);
|
||||
},
|
||||
|
||||
progressCallback.bind(null, { percent: 40, message: 'Downloading image' }),
|
||||
downloadImage.bind(null, app.manifest),
|
||||
@@ -563,16 +476,24 @@ function install(app, args, progressCallback, callback) {
|
||||
if (!restoreConfig) {
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 60, message: 'Setting up addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
services.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' }),
|
||||
services.setupAddons.bind(null, app, app.manifest.addons),
|
||||
services.clearAddons.bind(null, app, _.omit(app.manifest.addons, 'localstorage')),
|
||||
services.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}` });
|
||||
})
|
||||
services.setupAddons.bind(null, app, app.manifest.addons),
|
||||
services.clearAddons.bind(null, app, app.manifest.addons),
|
||||
backups.downloadApp.bind(null, app, restoreConfig, (progress) => {
|
||||
progressCallback({ percent: 65, message: progress.message });
|
||||
}),
|
||||
services.restoreAddons.bind(null, app, app.manifest.addons)
|
||||
], next);
|
||||
}
|
||||
},
|
||||
@@ -580,16 +501,16 @@ 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: 85, message: 'Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
function waitForDns(done) {
|
||||
if (skipDnsSetup) return done();
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 85, message: 'Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
], done);
|
||||
},
|
||||
|
||||
progressCallback.bind(null, { percent: 95, message: 'Configuring reverse proxy' }),
|
||||
configureReverseProxy.bind(null, app),
|
||||
@@ -613,7 +534,7 @@ function backup(app, args, progressCallback, callback) {
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 10, message: 'Backing up' }),
|
||||
backups.backupApp.bind(null, app, { /* options */ }, (progress) => {
|
||||
backups.backupApp.bind(null, app, { snapshotOnly: !!args.snapshotOnly }, (progress) => {
|
||||
progressCallback({ percent: 30, message: progress.message });
|
||||
}),
|
||||
|
||||
@@ -622,8 +543,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,12 +558,11 @@ 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
|
||||
progressCallback.bind(null, { percent: 30, message: 'Setting up addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
services.setupAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
progressCallback.bind(null, { percent: 60, message: 'Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
@@ -668,39 +588,57 @@ function changeLocation(app, args, progressCallback, callback) {
|
||||
|
||||
const oldConfig = args.oldConfig;
|
||||
const locationChanged = oldConfig.fqdn !== app.fqdn;
|
||||
const skipDnsSetup = args.skipDnsSetup;
|
||||
const overwriteDns = args.overwriteDns;
|
||||
|
||||
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) {
|
||||
return !app.alternateDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
|
||||
});
|
||||
|
||||
if (oldConfig.aliasDomains) {
|
||||
obsoleteDomains = obsoleteDomains.concat(oldConfig.aliasDomains.filter(function (o) {
|
||||
return !app.aliasDomains.some(function (n) { return n.subdomain === o.subdomain && n.domain === o.domain; });
|
||||
}));
|
||||
}
|
||||
|
||||
if (locationChanged) obsoleteDomains.push({ subdomain: oldConfig.location, domain: oldConfig.domain });
|
||||
|
||||
if (obsoleteDomains.length === 0) return next();
|
||||
|
||||
unregisterSubdomains(app, obsoleteDomains, next);
|
||||
domains.unregisterLocations(obsoleteDomains, progressCallback, next);
|
||||
},
|
||||
|
||||
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
|
||||
registerSubdomains.bind(null, app, overwriteDns),
|
||||
function setupDnsIfNeeded(done) {
|
||||
if (skipDnsSetup) return done();
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 30, message: 'Registering subdomains' }),
|
||||
domains.registerLocations.bind(null, [ { subdomain: app.location, domain: app.domain }].concat(app.alternateDomains).concat(app.aliasDomains), { overwriteDns }, progressCallback)
|
||||
], done);
|
||||
},
|
||||
|
||||
// re-setup addons since they rely on the app's fqdn (e.g oauth)
|
||||
progressCallback.bind(null, { percent: 50, message: 'Setting up addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
services.setupAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
progressCallback.bind(null, { percent: 60, message: 'Creating container' }),
|
||||
createContainer.bind(null, app),
|
||||
|
||||
startApp.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 80, message: 'Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
function waitForDns(done) {
|
||||
if (skipDnsSetup) return done();
|
||||
|
||||
async.series([
|
||||
progressCallback.bind(null, { percent: 80, message: 'Waiting for DNS propagation' }),
|
||||
exports._waitForDnsPropagation.bind(null, app),
|
||||
], done);
|
||||
},
|
||||
|
||||
progressCallback.bind(null, { percent: 90, message: 'Configuring reverse proxy' }),
|
||||
configureReverseProxy.bind(null, app),
|
||||
@@ -709,7 +647,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 +660,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 +672,43 @@ 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),
|
||||
services.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);
|
||||
|
||||
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),
|
||||
|
||||
@@ -809,22 +717,13 @@ function configure(app, args, progressCallback, callback) {
|
||||
|
||||
// re-setup addons since they rely on the app's fqdn (e.g oauth)
|
||||
progressCallback.bind(null, { percent: 50, message: 'Setting up addons' }),
|
||||
addons.setupAddons.bind(null, app, app.manifest.addons),
|
||||
services.setupAddons.bind(null, app, app.manifest.addons),
|
||||
|
||||
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: 90, message: 'Configuring reverse proxy' }),
|
||||
configureReverseProxy.bind(null, app),
|
||||
|
||||
@@ -835,11 +734,11 @@ 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();
|
||||
});
|
||||
}
|
||||
|
||||
// nginx configuration is skipped because app.httpPort is expected to be available
|
||||
function update(app, args, progressCallback, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof args, 'object');
|
||||
@@ -851,12 +750,15 @@ function update(app, args, progressCallback, callback) {
|
||||
|
||||
// app does not want these addons anymore
|
||||
// FIXME: this does not handle option changes (like multipleDatabases)
|
||||
var unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
|
||||
const unusedAddons = _.omit(app.manifest.addons, Object.keys(updateConfig.manifest.addons));
|
||||
const httpPathsChanged = app.manifest.httpPaths !== updateConfig.manifest.httpPaths;
|
||||
const httpPortChanged = app.manifest.httpPort !== updateConfig.manifest.httpPort;
|
||||
const proxyAuthChanged = !_.isEqual(safe.query(app.manifest, 'addons.proxyAuth'), safe.query(updateConfig.manifest, 'addons.proxyAuth'));
|
||||
|
||||
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 +784,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();
|
||||
@@ -891,7 +792,7 @@ function update(app, args, progressCallback, callback) {
|
||||
},
|
||||
|
||||
// only delete unused addons after backup
|
||||
addons.teardownAddons.bind(null, app, unusedAddons),
|
||||
services.teardownAddons.bind(null, app, unusedAddons),
|
||||
|
||||
// free unused ports
|
||||
function (next) {
|
||||
@@ -903,7 +804,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) debugApp(app, '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 +820,21 @@ 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' }),
|
||||
addons.setupAddons.bind(null, app, updateConfig.manifest.addons),
|
||||
progressCallback.bind(null, { percent: 60, message: 'Updating addons' }),
|
||||
services.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: 90, message: 'Configuring reverse proxy' }),
|
||||
function (next) {
|
||||
if (!httpPathsChanged && !proxyAuthChanged && !httpPortChanged) return next();
|
||||
|
||||
configureReverseProxy(app, next);
|
||||
},
|
||||
|
||||
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 +857,19 @@ 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' }),
|
||||
services.startAppServices.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 35, message: 'Starting container' }),
|
||||
docker.startContainer.bind(null, app.id),
|
||||
|
||||
progressCallback.bind(null, { percent: 60, message: 'Adding collectd profile' }),
|
||||
addCollectdProfile.bind(null, app),
|
||||
|
||||
// stopped apps do not renew certs. currently, we don't do DNS to not overwrite existing user settings
|
||||
progressCallback.bind(null, { percent: 80, 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 +891,33 @@ 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' }),
|
||||
services.stopAppServices.bind(null, app),
|
||||
|
||||
progressCallback.bind(null, { percent: 80, message: 'Removing collectd profile' }),
|
||||
removeCollectdProfile.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 +936,27 @@ 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),
|
||||
services.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' }),
|
||||
|
||||
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' }),
|
||||
unregisterSubdomains.bind(null, app, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains)),
|
||||
progressCallback.bind(null, { percent: 70, message: 'Unregistering domains' }),
|
||||
domains.unregisterLocations.bind(null, [ { subdomain: app.location, domain: app.domain } ].concat(app.alternateDomains).concat(app.aliasDomains), progressCallback),
|
||||
|
||||
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 +1008,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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
scheduleTask: scheduleTask
|
||||
scheduleTask
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
debug = require('debug')('box:apptaskmanager'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
safe = require('safetydance'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
scheduler = require('./scheduler.js'),
|
||||
services = require('./services.js'),
|
||||
tasks = require('./tasks.js');
|
||||
|
||||
let gActiveTasks = { }; // indexed by app id
|
||||
@@ -34,21 +37,22 @@ function initializeSync() {
|
||||
}
|
||||
|
||||
// callback is called when task is finished
|
||||
function scheduleTask(appId, taskId, callback) {
|
||||
function scheduleTask(appId, taskId, options, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof taskId, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
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);
|
||||
gPendingTasks.push({ appId, taskId, callback });
|
||||
tasks.update(taskId, { percent: 1, message: 'Waiting for other app tasks to complete' }, NOOP_CALLBACK);
|
||||
gPendingTasks.push({ appId, taskId, options, callback });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,8 +60,8 @@ 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);
|
||||
gPendingTasks.push({ appId, taskId, callback });
|
||||
tasks.update(taskId, { percent: 1, message: waitText(lockError.operation) }, NOOP_CALLBACK);
|
||||
gPendingTasks.push({ appId, taskId, options, callback });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,11 +71,17 @@ 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) {
|
||||
scheduler.suspendJobs(appId);
|
||||
|
||||
tasks.startTask(taskId, Object.assign(options, { logFile }), function (error, result) {
|
||||
callback(error, result);
|
||||
|
||||
delete gActiveTasks[appId];
|
||||
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
|
||||
|
||||
// post app task hooks
|
||||
services.rebuildService('sftp', error => { if (error) debug('Unable to rebuild sftp:', error); });
|
||||
scheduler.resumeJobs(appId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,6 +91,6 @@ function startNextTask() {
|
||||
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
|
||||
|
||||
const t = gPendingTasks.shift();
|
||||
scheduleTask(t.appId, t.taskId, t.callback);
|
||||
scheduleTask(t.appId, t.taskId, t.options, t.callback);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
29
src/autoconfig.xml.ejs
Normal file
29
src/autoconfig.xml.ejs
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<clientConfig version="1.1">
|
||||
<emailProvider id="<%= domain %>">
|
||||
<domain><%= domain %></domain>
|
||||
<displayName>Cloudron Mail</displayName>
|
||||
<displayShortName>Cloudron</displayShortName>
|
||||
<incomingServer type="imap">
|
||||
<hostname><%= mailFqdn %></hostname>
|
||||
<port>993</port>
|
||||
<socketType>SSL</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
</incomingServer>
|
||||
<outgoingServer type="smtp">
|
||||
<hostname><%= mailFqdn %></hostname>
|
||||
<port>587</port>
|
||||
<socketType>STARTTLS</socketType>
|
||||
<authentication>password-cleartext</authentication>
|
||||
<username>%EMAILADDRESS%</username>
|
||||
<addThisServer>true</addThisServer>
|
||||
</outgoingServer>
|
||||
|
||||
<documentation url="http://cloudron.io/email/#autodiscover">
|
||||
<descr lang="en">Cloudron Email</descr>
|
||||
</documentation>
|
||||
|
||||
</emailProvider>
|
||||
</clientConfig>
|
||||
|
||||
@@ -6,27 +6,21 @@ 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,
|
||||
list,
|
||||
|
||||
_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 +32,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 +50,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 +65,29 @@ 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); });
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function list(page, perPage, callback) {
|
||||
assert(typeof page === 'number' && page > 0);
|
||||
assert(typeof perPage === 'number' && perPage > 0);
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups ORDER BY creationTime DESC LIMIT ?,?',
|
||||
[ (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 +114,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 +127,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));
|
||||
|
||||
1008
src/backups.js
1008
src/backups.js
File diff suppressed because it is too large
Load Diff
@@ -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,10 +45,12 @@ BoxError.DATABASE_ERROR = 'Database Error';
|
||||
BoxError.DNS_ERROR = 'DNS Error';
|
||||
BoxError.DOCKER_ERROR = 'Docker Error';
|
||||
BoxError.EXTERNAL_ERROR = 'External Error'; // use this for external API errors
|
||||
BoxError.FEATURE_DISABLED = 'Feature Disabled';
|
||||
BoxError.FS_ERROR = 'FileSystem Error';
|
||||
BoxError.INACTIVE = 'Inactive';
|
||||
BoxError.INTERNAL_ERROR = 'Internal Error';
|
||||
BoxError.INVALID_CREDENTIALS = 'Invalid Credentials';
|
||||
BoxError.IPTABLES_ERROR = 'IPTables Error';
|
||||
BoxError.LICENSE_ERROR = 'License Error';
|
||||
BoxError.LOGROTATE_ERROR = 'Logrotate Error';
|
||||
BoxError.MAIL_ERROR = 'Mail Error';
|
||||
@@ -58,9 +61,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 +79,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 +92,8 @@ BoxError.toHttpError = function (error) {
|
||||
case BoxError.FS_ERROR:
|
||||
case BoxError.MAIL_ERROR:
|
||||
case BoxError.DOCKER_ERROR:
|
||||
case BoxError.ADDONS_ERROR:
|
||||
case BoxError.IPTABLES_ERROR:
|
||||
return new HttpError(424, error);
|
||||
case BoxError.DATABASE_ERROR:
|
||||
case BoxError.INTERNAL_ERROR:
|
||||
|
||||
18
src/branding.js
Normal file
18
src/branding.js
Normal file
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
renderFooter
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
constants = require('./constants.js');
|
||||
|
||||
function renderFooter(footer) {
|
||||
assert.strictEqual(typeof footer, 'string');
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
return footer.replace(/%YEAR%/g, year)
|
||||
.replace(/%VERSION%/g, constants.VERSION);
|
||||
}
|
||||
|
||||
@@ -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,8 +138,8 @@ Acme2.prototype.updateContact = function (registrationUri, callback) {
|
||||
|
||||
const that = this;
|
||||
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when updating contact: ${error.message}`));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to update contact. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`));
|
||||
|
||||
debug(`updateContact: contact of user updated to ${that.email}`);
|
||||
|
||||
@@ -154,9 +158,9 @@ Acme2.prototype.registerUser = function (callback) {
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new 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)));
|
||||
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to register new account. Expecting 200 or 201, got ${result.statusCode} ${JSON.stringify(result.body)}`));
|
||||
|
||||
debug(`registerUser: user registered keyid: ${result.headers.location}`);
|
||||
|
||||
@@ -180,9 +184,9 @@ 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 (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)));
|
||||
if (error) return callback(error);
|
||||
if (result.statusCode === 403) return callback(new BoxError(BoxError.ACCESS_DENIED, `Forbidden sending new order: ${result.body.detail}`));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to send new order. Expecting 201, got ${result.statusCode} ${JSON.stringify(result.body)}`));
|
||||
|
||||
debug('newOrder: created order %s %j', domain, result.body);
|
||||
|
||||
@@ -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,8 +258,8 @@ 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 (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)));
|
||||
if (error) return callback(error);
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to notify challenge. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`));
|
||||
|
||||
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,9 +311,9 @@ Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, cal
|
||||
debug('signCertificate: sending sign request');
|
||||
|
||||
this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new 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)));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Failed to sign certificate. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -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'));
|
||||
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)));
|
||||
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, `Failed to get cert. Expecting 200, got ${result.statusCode} ${JSON.stringify(result.body)}`));
|
||||
|
||||
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;
|
||||
|
||||
@@ -583,34 +586,34 @@ Acme2.prototype.getDirectory = function (callback) {
|
||||
});
|
||||
};
|
||||
|
||||
Acme2.prototype.getCertificate = function (hostname, domain, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
Acme2.prototype.getCertificate = function (vhost, domain, callback) {
|
||||
assert.strictEqual(typeof vhost, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`getCertificate: start acme flow for ${hostname} from ${this.caDirectory}`);
|
||||
debug(`getCertificate: start acme flow for ${vhost} from ${this.caDirectory}`);
|
||||
|
||||
if (hostname !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
|
||||
hostname = domains.makeWildcard(hostname);
|
||||
debug(`getCertificate: will get wildcard cert for ${hostname}`);
|
||||
if (vhost !== domain && this.wildcard) { // bare domain is not part of wildcard SAN
|
||||
vhost = domains.makeWildcard(vhost);
|
||||
debug(`getCertificate: will get wildcard cert for ${vhost}`);
|
||||
}
|
||||
|
||||
const that = this;
|
||||
this.getDirectory(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
that.acmeFlow(hostname, domain, function (error) {
|
||||
that.acmeFlow(vhost, domain, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
const certName = hostname.replace('*.', '_.');
|
||||
const certName = vhost.replace('*.', '_.');
|
||||
callback(null, path.join(outdir, `${certName}.cert`), path.join(outdir, `${certName}.key`));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function getCertificate(hostname, domain, options, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
function getCertificate(vhost, domain, options, callback) {
|
||||
assert.strictEqual(typeof vhost, 'string'); // this can also be a wildcard domain (for alias domains)
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -620,6 +623,6 @@ function getCertificate(hostname, domain, options, callback) {
|
||||
debug(`getCertificate: attempt ${attempt++}`);
|
||||
|
||||
let acme = new Acme2(options || { });
|
||||
acme.getCertificate(hostname, domain, retryCallback);
|
||||
acme.getCertificate(vhost, domain, retryCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate,
|
||||
|
||||
// testing
|
||||
_name: 'caas'
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
debug = require('debug')('box:cert/caas.js');
|
||||
|
||||
function getCertificate(hostname, domain, options, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getCertificate: using fallback certificate', hostname);
|
||||
|
||||
return callback(null, '', '');
|
||||
}
|
||||
@@ -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
src/clientdb.js
179
src/clientdb.js
@@ -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
src/clients.js
329
src/clients.js
@@ -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');
|
||||
}
|
||||
215
src/cloudron.js
215
src/cloudron.js
@@ -1,52 +1,54 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
getConfig: getConfig,
|
||||
getLogs: getLogs,
|
||||
initialize,
|
||||
uninitialize,
|
||||
getConfig,
|
||||
getLogs,
|
||||
|
||||
reboot: reboot,
|
||||
isRebootRequired: isRebootRequired,
|
||||
reboot,
|
||||
isRebootRequired,
|
||||
|
||||
onActivated: onActivated,
|
||||
onActivated,
|
||||
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
setDashboardDomain: setDashboardDomain,
|
||||
setDashboardAndMailDomain: setDashboardAndMailDomain,
|
||||
renewCerts: renewCerts,
|
||||
setupDnsAndCert,
|
||||
|
||||
setupDashboard: setupDashboard,
|
||||
prepareDashboardDomain,
|
||||
setDashboardDomain,
|
||||
updateDashboardDomain,
|
||||
renewCerts,
|
||||
syncDnsRecords,
|
||||
|
||||
runSystemChecks: runSystemChecks,
|
||||
runSystemChecks
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
const 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'),
|
||||
branding = require('./branding.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'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
services = require('./services.js'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
sysinfo = require('./sysinfo.js'),
|
||||
tasks = require('./tasks.js'),
|
||||
users = require('./users.js');
|
||||
|
||||
@@ -67,19 +69,29 @@ function uninitialize(callback) {
|
||||
|
||||
async.series([
|
||||
cron.stopJobs,
|
||||
platform.stop
|
||||
platform.stopAllTasks
|
||||
], callback);
|
||||
}
|
||||
|
||||
function onActivated(callback) {
|
||||
function onActivated(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// Starting the platform after a user is available means:
|
||||
// 1. mail bounces can now be sent to the cloudron owner
|
||||
// 2. the restore code path can run without sudo (since mail/ is non-root)
|
||||
async.series([
|
||||
platform.start,
|
||||
cron.startJobs
|
||||
platform.start.bind(null, options),
|
||||
cron.startJobs,
|
||||
function checkBackupConfiguration(done) {
|
||||
backups.checkConfiguration(function (error, message) {
|
||||
if (error) return done(error);
|
||||
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, done);
|
||||
});
|
||||
},
|
||||
// disable responding to api calls via IP to not leak domain info. this is carefully placed as the last item, so it buys
|
||||
// the UI some time to query the dashboard domain in the restore code path
|
||||
(done) => setTimeout(() => reverseProxy.writeDefaultConfig({ activated :true }, done), 30000)
|
||||
], callback);
|
||||
}
|
||||
|
||||
@@ -104,18 +116,49 @@ function notifyUpdate(callback) {
|
||||
|
||||
// each of these tasks can fail. we will add some routes to fix/re-run them
|
||||
function runStartupTasks() {
|
||||
// configure nginx to be reachable by IP
|
||||
reverseProxy.writeDefaultConfig(NOOP_CALLBACK);
|
||||
const tasks = [
|
||||
// stop all the systemd tasks
|
||||
platform.stopAllTasks,
|
||||
|
||||
// always generate webadmin config since we have no versioning mechanism for the ejs
|
||||
if (settings.adminDomain()) reverseProxy.writeAdminConfig(settings.adminDomain(), 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
|
||||
function (callback) {
|
||||
settings.getBackupConfig(function (error, backupConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// check activation state and start the platform
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return debug(error);
|
||||
if (!activated) return debug('initialize: not activated yet'); // not activated
|
||||
backups.configureCollectd(backupConfig, callback);
|
||||
});
|
||||
},
|
||||
|
||||
onActivated(NOOP_CALLBACK);
|
||||
// always generate webadmin config since we have no versioning mechanism for the ejs
|
||||
function (callback) {
|
||||
if (!settings.adminDomain()) return callback();
|
||||
|
||||
reverseProxy.writeDashboardConfig(settings.adminDomain(), callback);
|
||||
},
|
||||
|
||||
// check activation state and start the platform
|
||||
function (callback) {
|
||||
users.isActivated(function (error, activated) {
|
||||
if (error) return callback(error);
|
||||
|
||||
// configure nginx to be reachable by IP when not activated. for the moment, the IP based redirect exists even after domain is setup
|
||||
// just in case user forgot or some network error happenned in the middle (then browser refresh takes you to activation page)
|
||||
// we remove the config as a simple security measure to not expose IP <-> domain
|
||||
if (!activated) {
|
||||
debug('runStartupTasks: not activated. generating IP based redirection config');
|
||||
return reverseProxy.writeDefaultConfig({ activated: false }, callback);
|
||||
}
|
||||
|
||||
onActivated({}, callback);
|
||||
});
|
||||
}
|
||||
];
|
||||
|
||||
// we used to run tasks in parallel but simultaneous nginx reloads was causing issues
|
||||
async.series(async.reflectAll(tasks), function (error, results) {
|
||||
results.forEach((result, idx) => {
|
||||
if (result.error) debug(`Startup task at index ${idx} failed: ${result.error.message}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -134,16 +177,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: branding.renderFooter(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 +202,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 +231,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 +288,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);
|
||||
|
||||
@@ -264,7 +301,7 @@ function prepareDashboardDomain(domain, auditSource, callback) {
|
||||
const conflict = result.filter(app => app.fqdn === fqdn);
|
||||
if (conflict.length) return callback(new BoxError(BoxError.BAD_STATE, 'Dashboard location conflicts with an existing app'));
|
||||
|
||||
tasks.add(tasks.TASK_PREPARE_DASHBOARD_DOMAIN, [ domain, auditSource ], function (error, taskId) {
|
||||
tasks.add(tasks.TASK_SETUP_DNS_AND_CERT, [ constants.ADMIN_LOCATION, domain, auditSource ], function (error, taskId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tasks.startTask(taskId, {}, NOOP_CALLBACK);
|
||||
@@ -286,15 +323,12 @@ function setDashboardDomain(domain, auditSource, callback) {
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
reverseProxy.writeAdminConfig(domain, function (error) {
|
||||
reverseProxy.writeDashboardConfig(domain, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
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.setAdminLocation(domain, fqdn, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
|
||||
@@ -306,33 +340,24 @@ function setDashboardDomain(domain, auditSource, callback) {
|
||||
}
|
||||
|
||||
// call this only post activation because it will restart mail server
|
||||
function setDashboardAndMailDomain(domain, auditSource, callback) {
|
||||
function updateDashboardDomain(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`setDashboardAndMailDomain: ${domain}`);
|
||||
debug(`updateDashboardDomain: ${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
|
||||
services.rebuildService('turn', NOOP_CALLBACK); // to update the realm variable
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function setupDashboard(auditSource, progressCallback, callback) {
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
domains.prepareDashboardDomain.bind(null, settings.adminDomain(), auditSource, progressCallback),
|
||||
setDashboardDomain.bind(null, settings.adminDomain(), auditSource)
|
||||
], callback);
|
||||
}
|
||||
|
||||
function renewCerts(options, auditSource, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
@@ -346,3 +371,47 @@ function renewCerts(options, auditSource, callback) {
|
||||
callback(null, taskId);
|
||||
});
|
||||
}
|
||||
|
||||
function setupDnsAndCert(subdomain, domain, auditSource, progressCallback, callback) {
|
||||
assert.strictEqual(typeof subdomain, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof progressCallback, 'function');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const adminFqdn = domains.fqdn(subdomain, domainObject);
|
||||
|
||||
sysinfo.getServerIp(function (error, ip) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.series([
|
||||
(done) => { progressCallback({ message: `Updating DNS of ${adminFqdn}` }); done(); },
|
||||
domains.upsertDnsRecords.bind(null, subdomain, domain, 'A', [ ip ]),
|
||||
(done) => { progressCallback({ message: `Waiting for DNS of ${adminFqdn}` }); done(); },
|
||||
domains.waitForDnsRecord.bind(null, subdomain, domain, 'A', ip, { interval: 30000, times: 50000 }),
|
||||
(done) => { progressCallback({ message: `Getting certificate of ${adminFqdn}` }); done(); },
|
||||
reverseProxy.ensureCertificate.bind(null, domains.fqdn(subdomain, domainObject), domain, auditSource)
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function syncDnsRecords(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tasks.add(tasks.TASK_SYNC_DNS_RECORDS, [ options ], function (error, taskId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tasks.startTask(taskId, {}, NOOP_CALLBACK);
|
||||
|
||||
callback(null, taskId);
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user