Compare commits
1319 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a887e19d46 | ||
|
|
2ab941660e | ||
|
|
a75769071c | ||
|
|
7f2af067cf | ||
|
|
88454e7d6c | ||
|
|
5c920fd200 | ||
|
|
ab650c7a95 | ||
|
|
1e776bbbe0 | ||
|
|
cd0294129f | ||
|
|
d1c6e786c2 | ||
|
|
58d66b5293 | ||
|
|
1942a7ecf4 | ||
|
|
22c2add55e | ||
|
|
60c5cccfc2 | ||
|
|
b4874ec1f4 | ||
|
|
d7b326bf2b | ||
|
|
b9d8b5f973 | ||
|
|
64fd6e0dac | ||
|
|
868103e7e4 | ||
|
|
3354cb8ebe | ||
|
|
4fc012dea0 | ||
|
|
947cb786d6 | ||
|
|
689f2791ba | ||
|
|
a5ec5b0ed9 | ||
|
|
8e5916b785 | ||
|
|
563f846eba | ||
|
|
7781ea3205 | ||
|
|
2f5ece8f1d | ||
|
|
ec46dab754 | ||
|
|
d5d27d512c | ||
|
|
0a695190c4 | ||
|
|
59deca76a1 | ||
|
|
a829ab44f1 | ||
|
|
82a7befb92 | ||
|
|
331d0ee717 | ||
|
|
addafa529f | ||
|
|
8232d471a3 | ||
|
|
813454ca82 | ||
|
|
7d987d7c79 | ||
|
|
7a25187bee | ||
|
|
f97cbb5fd5 | ||
|
|
12d233c5f9 | ||
|
|
09fce1978e | ||
|
|
8ed2f98d1d | ||
|
|
13262d014b | ||
|
|
ade1187fc8 | ||
|
|
2404e79928 | ||
|
|
d68ed91b17 | ||
|
|
1a21423401 | ||
|
|
a478134759 | ||
|
|
c639746211 | ||
|
|
7a96e4858a | ||
|
|
02339d503c | ||
|
|
c3a5360a88 | ||
|
|
ad9097d212 | ||
|
|
6e57f8cc03 | ||
|
|
d6365ff27f | ||
|
|
4793eb9ef5 | ||
|
|
03175aa8de | ||
|
|
bc3169deb3 | ||
|
|
9b4d43075e | ||
|
|
d2c12297dc | ||
|
|
1a8496d61e | ||
|
|
a017af41c5 | ||
|
|
ec216d9828 | ||
|
|
bce1efb77c | ||
|
|
b078d37f37 | ||
|
|
8d944f74c0 | ||
|
|
dc10b8a07f | ||
|
|
7b9f741522 | ||
|
|
51cb3b0ba8 | ||
|
|
4db4834c90 | ||
|
|
e1f0d12251 | ||
|
|
e2388b7d88 | ||
|
|
d0e6b6bfe4 | ||
|
|
b6f2c94464 | ||
|
|
8cdddef077 | ||
|
|
e82ac5ecc5 | ||
|
|
db6c07f86a | ||
|
|
2df642000d | ||
|
|
11d80cec7d | ||
|
|
8c9ce30d29 | ||
|
|
df142994a8 | ||
|
|
2d115d3d0f | ||
|
|
1b594d3e50 | ||
|
|
332f2e7c10 | ||
|
|
a7614cef2e | ||
|
|
9842b6d4a1 | ||
|
|
88818a1ec2 | ||
|
|
812f5cce99 | ||
|
|
fdf7da9111 | ||
|
|
ed9e1772ea | ||
|
|
657a2cac2f | ||
|
|
d15aa2744d | ||
|
|
29ab3e91b3 | ||
|
|
f6377fd1c6 | ||
|
|
122a987d61 | ||
|
|
4610e78d91 | ||
|
|
351bd46cb7 | ||
|
|
8878bc4bf9 | ||
|
|
61b6bee946 | ||
|
|
9997cbddb8 | ||
|
|
7115498f32 | ||
|
|
0f05c243aa | ||
|
|
9c12f1fe15 | ||
|
|
7383cc4e90 | ||
|
|
6466b47ada | ||
|
|
1856fc05d9 | ||
|
|
a19662bdfa | ||
|
|
488763fc42 | ||
|
|
7cbe60a484 | ||
|
|
ded9a6e377 | ||
|
|
ea205363a0 | ||
|
|
ad13445c93 | ||
|
|
eb5c2ed30b | ||
|
|
bd3080a6b3 | ||
|
|
be5290c5ca | ||
|
|
43fd207164 | ||
|
|
34c53694a0 | ||
|
|
927f8483ce | ||
|
|
a19205e3ad | ||
|
|
49e5c60422 | ||
|
|
57b623ee44 | ||
|
|
0c904af927 | ||
|
|
9cd025972c | ||
|
|
21111eccc4 | ||
|
|
917079f341 | ||
|
|
4d6d768be1 | ||
|
|
c54cd992ca | ||
|
|
d5ec599dd1 | ||
|
|
0542ab16d4 | ||
|
|
7e75ef7685 | ||
|
|
f296265461 | ||
|
|
fb4eade215 | ||
|
|
8b3e85907c | ||
|
|
ca4876649d | ||
|
|
7ebc2abe5d | ||
|
|
37e132319b | ||
|
|
b2728118e9 | ||
|
|
c428f649aa | ||
|
|
7baf979a59 | ||
|
|
ccecaca047 | ||
|
|
c7ee684f25 | ||
|
|
52156c9a35 | ||
|
|
4fba216af9 | ||
|
|
1d00c788d1 | ||
|
|
d891d39587 | ||
|
|
cfde6e31ad | ||
|
|
243772d1f5 | ||
|
|
1c36b8eaf7 | ||
|
|
120fa4924a | ||
|
|
c3c9c2f39a | ||
|
|
fc90829ba2 | ||
|
|
ce9224c690 | ||
|
|
18a2107247 | ||
|
|
f13d05dad7 | ||
|
|
86586444a9 | ||
|
|
4e47d0595d | ||
|
|
45e85e4d53 | ||
|
|
a3420f885d | ||
|
|
a266fe13d0 | ||
|
|
44aba5d6e1 | ||
|
|
3fe5307ae3 | ||
|
|
d03fb0e71f | ||
|
|
d9723b72e4 | ||
|
|
6ba61f1bda | ||
|
|
d1df647ddd | ||
|
|
95c4a1f90c | ||
|
|
e00325e694 | ||
|
|
85c13cae58 | ||
|
|
00fd9e5b7f | ||
|
|
dde81ee847 | ||
|
|
c46fc96500 | ||
|
|
1914a9a703 | ||
|
|
1a061e4446 | ||
|
|
29ce80cebe | ||
|
|
4b6ac538ac | ||
|
|
70b9000b0e | ||
|
|
24dcb1b79c | ||
|
|
384915883f | ||
|
|
4cfc75f1d1 | ||
|
|
c49cbb524d | ||
|
|
b401c3d930 | ||
|
|
890a7cfb37 | ||
|
|
70a1ef1af3 | ||
|
|
38a0cdc0be | ||
|
|
93344a5a4a | ||
|
|
9f792fc04b | ||
|
|
7cb95faacb | ||
|
|
bf122f0f56 | ||
|
|
78e9446a05 | ||
|
|
138e1595fa | ||
|
|
37b02ad36a | ||
|
|
02f0055594 | ||
|
|
ec1f0f9320 | ||
|
|
bfe6389f62 | ||
|
|
30db3e8973 | ||
|
|
5b67f2cf29 | ||
|
|
a007b74b1c | ||
|
|
a89482d4fa | ||
|
|
0cd4f133aa | ||
|
|
e5ba4ff973 | ||
|
|
ce133b997d | ||
|
|
217632354f | ||
|
|
9841351190 | ||
|
|
f3341f4b7f | ||
|
|
ff1f448860 | ||
|
|
37f28746fc | ||
|
|
9a22ba3af7 | ||
|
|
2942da78de | ||
|
|
89ff6be971 | ||
|
|
be0d7bcce1 | ||
|
|
851b257678 | ||
|
|
579eacb644 | ||
|
|
f52c5b584e | ||
|
|
8980c18deb | ||
|
|
b05a9ce064 | ||
|
|
1974314c1f | ||
|
|
2bde023d4d | ||
|
|
3a10003246 | ||
|
|
1b08710b7e | ||
|
|
101d09eeb3 | ||
|
|
00f949f156 | ||
|
|
adbe46d369 | ||
|
|
3198926cd6 | ||
|
|
957a6a20fe | ||
|
|
94f75bb0d7 | ||
|
|
0f442755e5 | ||
|
|
cd2e782d48 | ||
|
|
e97606ca87 | ||
|
|
00ada80230 | ||
|
|
34db98c489 | ||
|
|
110695355c | ||
|
|
021fb4bb94 | ||
|
|
dea033e4b0 | ||
|
|
7dfe40739e | ||
|
|
9f0d1b515c | ||
|
|
2691d46d50 | ||
|
|
78c8f1de71 | ||
|
|
d27ee4bfbc | ||
|
|
cc5daa428d | ||
|
|
3e2189aeed | ||
|
|
79f9963792 | ||
|
|
6f53723169 | ||
|
|
d8cb100fc0 | ||
|
|
5f9b2f1159 | ||
|
|
801ca7eda1 | ||
|
|
45a2d3745c | ||
|
|
551fe4d846 | ||
|
|
791981c2f2 | ||
|
|
a18a620847 | ||
|
|
99e63ffc3f | ||
|
|
e10a6d9de5 | ||
|
|
147f16571a | ||
|
|
bd1fbc4a05 | ||
|
|
0843f78ec8 | ||
|
|
9769fbfcf2 | ||
|
|
7e73197eb9 | ||
|
|
e3964fd710 | ||
|
|
e66961b814 | ||
|
|
4176e5a98e | ||
|
|
45cf8a62d1 | ||
|
|
b1380819ba | ||
|
|
57fa457596 | ||
|
|
de1e218ce9 | ||
|
|
e117ee2bef | ||
|
|
a9e101d9f4 | ||
|
|
a2f8203a42 | ||
|
|
b9ee127775 | ||
|
|
6668bb3e8a | ||
|
|
5fd129e509 | ||
|
|
d59c1f53b9 | ||
|
|
d2f38c1abc | ||
|
|
c0a1db6941 | ||
|
|
fc10b4a79b | ||
|
|
9da2117e99 | ||
|
|
7e030b149b | ||
|
|
bd23abd265 | ||
|
|
dd0fb8292c | ||
|
|
b4cbf63519 | ||
|
|
4fd04fa349 | ||
|
|
c22cdb8d81 | ||
|
|
eb963b2eb4 | ||
|
|
7d299908c9 | ||
|
|
2585282f86 | ||
|
|
f25d5b3304 | ||
|
|
6e878faa8b | ||
|
|
15a6cbe62b | ||
|
|
76b0b214ec | ||
|
|
f5c643c960 | ||
|
|
ca8e0613fb | ||
|
|
0c9334d0d2 | ||
|
|
712dc97e9b | ||
|
|
4df48c97ec | ||
|
|
fe3ea53cda | ||
|
|
d385c80882 | ||
|
|
b823213c94 | ||
|
|
4b86311ab9 | ||
|
|
b9efa8f445 | ||
|
|
f8db12346d | ||
|
|
4d3948f81f | ||
|
|
5431d50206 | ||
|
|
6db078c26a | ||
|
|
f61e9c7f27 | ||
|
|
567d92ce00 | ||
|
|
7a6d26c5da | ||
|
|
046ac85177 | ||
|
|
f0fd088247 | ||
|
|
5ec0d1e691 | ||
|
|
9391a934c3 | ||
|
|
bb62e6a318 | ||
|
|
0da6539c48 | ||
|
|
9cf833dab2 | ||
|
|
ed57260fcf | ||
|
|
c98f625c4c | ||
|
|
f3008064e4 | ||
|
|
1faee00764 | ||
|
|
a40505e2ee | ||
|
|
484202b4c6 | ||
|
|
6a7fc17c60 | ||
|
|
05d3897ae2 | ||
|
|
9f1210202a | ||
|
|
be6b172d6f | ||
|
|
fef9e0a5c1 | ||
|
|
b84b033bf3 | ||
|
|
b30ff1f55a | ||
|
|
c6be0b290b | ||
|
|
33cfd7a629 | ||
|
|
5952a5c69d | ||
|
|
20de563925 | ||
|
|
7da80b4c62 | ||
|
|
15d765be6d | ||
|
|
bfe2f116a7 | ||
|
|
f535b3de2f | ||
|
|
e560c18b57 | ||
|
|
aecb99b6a3 | ||
|
|
7da17f8190 | ||
|
|
1964270a4f | ||
|
|
f45b61d95c | ||
|
|
ff11c38169 | ||
|
|
3e67067431 | ||
|
|
824f00d1e8 | ||
|
|
96d19f59a4 | ||
|
|
42c6fe50d2 | ||
|
|
9242f7095a | ||
|
|
99c9fbc38f | ||
|
|
0d31207ad7 | ||
|
|
8af7dbc35a | ||
|
|
d0a373cb15 | ||
|
|
3dc87bbca8 | ||
|
|
a55c399585 | ||
|
|
f74aa24dd2 | ||
|
|
1aa7eb4478 | ||
|
|
0c7002ba59 | ||
|
|
fd6dd1ea18 | ||
|
|
aa74d5cd82 | ||
|
|
8fc10a0bdd | ||
|
|
809ed0f0dc | ||
|
|
b8a4e1c4a3 | ||
|
|
d9e45f732b | ||
|
|
ca025b36f7 | ||
|
|
bfb719d35e | ||
|
|
2a1b61107f | ||
|
|
969cee7c90 | ||
|
|
7a3f579d3e | ||
|
|
288d5efa88 | ||
|
|
7be821963c | ||
|
|
a236f8992a | ||
|
|
a5c2257f39 | ||
|
|
9d3b4ba816 | ||
|
|
43bf0767f1 | ||
|
|
b301e5b151 | ||
|
|
2b484c0382 | ||
|
|
f40ab4e2d5 | ||
|
|
c0a27380e9 | ||
|
|
0d7a3f43c4 | ||
|
|
8195e439f3 | ||
|
|
b5edbf716c | ||
|
|
466265fde1 | ||
|
|
40033e09cd | ||
|
|
573663412c | ||
|
|
17599417f7 | ||
|
|
0ece6d8b0e | ||
|
|
e0ac0393fe | ||
|
|
6d38b3255c | ||
|
|
477ff424d6 | ||
|
|
a843104348 | ||
|
|
0f4bc0981a | ||
|
|
07f6351465 | ||
|
|
1b26e86365 | ||
|
|
94b4bf94c0 | ||
|
|
d5de05b633 | ||
|
|
0ab6cad048 | ||
|
|
9833ad548b | ||
|
|
aa1ba3b226 | ||
|
|
3774d4de28 | ||
|
|
e4961726bc | ||
|
|
77cf7d0da6 | ||
|
|
a993e0b228 | ||
|
|
43671a9fd6 | ||
|
|
49cfd1e9b7 | ||
|
|
58d4a4f54f | ||
|
|
e4e328ba6a | ||
|
|
fd6bc955ff | ||
|
|
511a18e0ed | ||
|
|
e29d224a92 | ||
|
|
bb48ffb01f | ||
|
|
31fd3411f7 | ||
|
|
a737d2675e | ||
|
|
fd462659cd | ||
|
|
cb10d0d465 | ||
|
|
61f1c4884c | ||
|
|
2cd00de6e3 | ||
|
|
d3c5d53eae | ||
|
|
6dfafae342 | ||
|
|
2f861c3309 | ||
|
|
af388f0f16 | ||
|
|
c36cc86c5f | ||
|
|
02f195b25c | ||
|
|
18623fd9b7 | ||
|
|
9b74bb73aa | ||
|
|
ee9636b496 | ||
|
|
5c2cbd7840 | ||
|
|
7fbac6cc17 | ||
|
|
9e7e9d66bf | ||
|
|
7fe66aa7fa | ||
|
|
2dda0efe83 | ||
|
|
59620ca473 | ||
|
|
12eae1eff2 | ||
|
|
b03bf87b7d | ||
|
|
c32718b164 | ||
|
|
a6ea12fedc | ||
|
|
2d260eb0d5 | ||
|
|
d7dd069ae0 | ||
|
|
6a77a58489 | ||
|
|
c30ac5f927 | ||
|
|
437f7ef890 | ||
|
|
1f7347e8de | ||
|
|
96f59d7cfe | ||
|
|
d55f65c7c9 | ||
|
|
9a0d5b918f | ||
|
|
3553fbc7b6 | ||
|
|
55d53f13d9 | ||
|
|
27369a650c | ||
|
|
913f0d5d97 | ||
|
|
ada63ec697 | ||
|
|
117f06e971 | ||
|
|
9f03a9a6e2 | ||
|
|
ce406c7088 | ||
|
|
e7127df30d | ||
|
|
10e2817257 | ||
|
|
337a47c62b | ||
|
|
14bdac20ef | ||
|
|
88e2b3f9aa | ||
|
|
22d731f06d | ||
|
|
e3d288ef7d | ||
|
|
455f597543 | ||
|
|
8c9e626920 | ||
|
|
5a000c1ff4 | ||
|
|
ddf634bfb2 | ||
|
|
89d3b8cc6a | ||
|
|
49af6d09a2 | ||
|
|
e5b0cac284 | ||
|
|
6f33900f85 | ||
|
|
514823af7d | ||
|
|
65b058f563 | ||
|
|
7c8560deff | ||
|
|
6bbe2613b4 | ||
|
|
5771478e4b | ||
|
|
e13030bc89 | ||
|
|
0a0ac93a55 | ||
|
|
214fb50e74 | ||
|
|
959f8ee31e | ||
|
|
cb0d75be37 | ||
|
|
11353e9e3a | ||
|
|
8cd5c15c2b | ||
|
|
b86b8b8ee1 | ||
|
|
c5f6e6b028 | ||
|
|
592d8abc58 | ||
|
|
d93068fc62 | ||
|
|
a864af52df | ||
|
|
1eedd4b185 | ||
|
|
9d38edfe95 | ||
|
|
f895ebba73 | ||
|
|
511287b16e | ||
|
|
530e06ec66 | ||
|
|
9cab383b43 | ||
|
|
9785ab82ed | ||
|
|
9d237e7bd6 | ||
|
|
7e9885012d | ||
|
|
1de785d97c | ||
|
|
2bd6566537 | ||
|
|
88fa4cf188 | ||
|
|
b26167481e | ||
|
|
1b6af9bd12 | ||
|
|
0159963cb0 | ||
|
|
996041cabc | ||
|
|
cb0352e33c | ||
|
|
3169f032c8 | ||
|
|
5ff8ee1a8f | ||
|
|
d3f31a3ace | ||
|
|
ac7e7f0db9 | ||
|
|
4c1e967dad | ||
|
|
f3ccd5c074 | ||
|
|
8369c0e2c0 | ||
|
|
122a966e72 | ||
|
|
9c2ff2f862 | ||
|
|
0ba45e746b | ||
|
|
54c06cdabb | ||
|
|
5a2e10317c | ||
|
|
8292d52acf | ||
|
|
7d21470fc7 | ||
|
|
eb0530bcba | ||
|
|
8855092faa | ||
|
|
2e02a3c71e | ||
|
|
5b5303ba7f | ||
|
|
022a54278e | ||
|
|
19b50dc428 | ||
|
|
e7eac003a9 | ||
|
|
cc17c6b2cd | ||
|
|
23d16b07aa | ||
|
|
7ecb3dd771 | ||
|
|
e43f974d34 | ||
|
|
e16cd38722 | ||
|
|
9d2f81d6b9 | ||
|
|
3fe539436b | ||
|
|
76f94eb559 | ||
|
|
7630ef921d | ||
|
|
625127d298 | ||
|
|
f24c4d2805 | ||
|
|
194340afa0 | ||
|
|
fdc9639aba | ||
|
|
f95ec53a85 | ||
|
|
3d425b7030 | ||
|
|
37c6c24e0e | ||
|
|
50bdd7ec7b | ||
|
|
769cb3e251 | ||
|
|
9447c45406 | ||
|
|
66a3962cfe | ||
|
|
d145eacbaf | ||
|
|
ed03ed7bad | ||
|
|
953b463799 | ||
|
|
6d28bb0489 | ||
|
|
c2f464ea75 | ||
|
|
4c56ffc767 | ||
|
|
885aa8833c | ||
|
|
63310c44c0 | ||
|
|
05dd65718f | ||
|
|
05d3f8a667 | ||
|
|
3fa45ea728 | ||
|
|
a7d2098f09 | ||
|
|
e1ecb49d59 | ||
|
|
6facfac4c5 | ||
|
|
97d2494fe3 | ||
|
|
a54be69c96 | ||
|
|
800e25a7a7 | ||
|
|
c1ce2977fa | ||
|
|
7177f82041 | ||
|
|
ecc9415679 | ||
|
|
23280f1635 | ||
|
|
af062631c5 | ||
|
|
99b866736c | ||
|
|
9e74b666fb | ||
|
|
1cc54f9a71 | ||
|
|
a2d8ef7c63 | ||
|
|
0835a6d690 | ||
|
|
67e0a19e13 | ||
|
|
6fa13f1bcc | ||
|
|
25604774b9 | ||
|
|
b0fb989be0 | ||
|
|
61c70b9922 | ||
|
|
8b5bdf4e88 | ||
|
|
84d8d4a745 | ||
|
|
04f93fb90f | ||
|
|
d559af15b8 | ||
|
|
1510e13066 | ||
|
|
d7cb8842d3 | ||
|
|
7596411d70 | ||
|
|
876cef5970 | ||
|
|
1ebc077721 | ||
|
|
f5acdc0c4b | ||
|
|
866321da4d | ||
|
|
58696c5132 | ||
|
|
3d11dfaae3 | ||
|
|
3cc320b1d0 | ||
|
|
2efeab3493 | ||
|
|
a78cab36ab | ||
|
|
41e525e037 | ||
|
|
33d5cb3061 | ||
|
|
0cf523c285 | ||
|
|
9e346e0780 | ||
|
|
9cd25e548f | ||
|
|
d221e40602 | ||
|
|
a6ce5ce796 | ||
|
|
23bfb7b8e6 | ||
|
|
b536c8196b | ||
|
|
8f677177f2 | ||
|
|
bb7cac4855 | ||
|
|
e62fa01ab3 | ||
|
|
5be48c5486 | ||
|
|
6265efd5e7 | ||
|
|
222d2840cf | ||
|
|
0e8fe8cd07 | ||
|
|
f96a8bc269 | ||
|
|
bbced03f3a | ||
|
|
ffe0e6dbbe | ||
|
|
7bb6016f7b | ||
|
|
d5f58eea34 | ||
|
|
204c659af2 | ||
|
|
ae5eb00bd0 | ||
|
|
61c059babe | ||
|
|
a4e4d30d5d | ||
|
|
c4fbfd32ee | ||
|
|
4f5af1f7be | ||
|
|
22efb96f66 | ||
|
|
b7d3014aab | ||
|
|
fc7e72835e | ||
|
|
d824ced70f | ||
|
|
2b9147bbc0 | ||
|
|
1ea9a0af50 | ||
|
|
7825cf06c0 | ||
|
|
20a2f56184 | ||
|
|
22be3ad948 | ||
|
|
dfa5a0f5fe | ||
|
|
b7d046533c | ||
|
|
c74cf59e3b | ||
|
|
92941260a5 | ||
|
|
74fdcb0781 | ||
|
|
6ab38633c2 | ||
|
|
e553ade936 | ||
|
|
b4b3fd9ab6 | ||
|
|
4fc6f5a094 | ||
|
|
0f521e2521 | ||
|
|
088b016083 | ||
|
|
ccb2fcf39b | ||
|
|
f6e82e93e4 | ||
|
|
02126caacd | ||
|
|
8f55565dbf | ||
|
|
3246edd5a8 | ||
|
|
7e7e067b5b | ||
|
|
fd0bdbce7e | ||
|
|
5ba8a8f7da | ||
|
|
a799b0931f | ||
|
|
d895786f55 | ||
|
|
4d89b4a8df | ||
|
|
e544e77fd9 | ||
|
|
7a5a94614e | ||
|
|
c935811e08 | ||
|
|
cd6d97c3eb | ||
|
|
292f0624fb | ||
|
|
9fb0d1f3bf | ||
|
|
d82b722887 | ||
|
|
3bcab14de1 | ||
|
|
3ded411c15 | ||
|
|
706c970314 | ||
|
|
35dfd540b5 | ||
|
|
7c4394d8cb | ||
|
|
69972e6460 | ||
|
|
dd19f11e29 | ||
|
|
b312414957 | ||
|
|
f3910f03ca | ||
|
|
080c667d9c | ||
|
|
2bf0614da1 | ||
|
|
bd5bdfa8b5 | ||
|
|
d1582a435d | ||
|
|
d27aa8821a | ||
|
|
f5f08cf9ff | ||
|
|
91877f7b2d | ||
|
|
7f9b078430 | ||
|
|
e9a01fa0c8 | ||
|
|
1f33674f30 | ||
|
|
badf58db3e | ||
|
|
58c98be9a7 | ||
|
|
71a5555816 | ||
|
|
aee20c0423 | ||
|
|
95faccd8d9 | ||
|
|
85ec4a3de7 | ||
|
|
e79b6ade51 | ||
|
|
79ccfa087d | ||
|
|
9a349eff32 | ||
|
|
e756a442f6 | ||
|
|
332c860e80 | ||
|
|
d934671202 | ||
|
|
747359478d | ||
|
|
f0f18db5a8 | ||
|
|
2f068c66d4 | ||
|
|
fa5415b1eb | ||
|
|
e23fd5e3c5 | ||
|
|
996c838320 | ||
|
|
083b8a6d7b | ||
|
|
58625c82a0 | ||
|
|
74211dd2c9 | ||
|
|
cd4808e5e2 | ||
|
|
e44ad19bda | ||
|
|
e31d413551 | ||
|
|
d170a3e5e1 | ||
|
|
10c3872902 | ||
|
|
a3c658d6b8 | ||
|
|
0c8cf9cc0d | ||
|
|
b92a79eb9d | ||
|
|
ba2c6c1219 | ||
|
|
f0a5cc6116 | ||
|
|
9fbe209c04 | ||
|
|
b8bbd73e07 | ||
|
|
2e40325f2a | ||
|
|
bdbd336db5 | ||
|
|
5c645ea224 | ||
|
|
5c2985cac7 | ||
|
|
a0d1016c01 | ||
|
|
816cf44117 | ||
|
|
1119a62576 | ||
|
|
152cb48340 | ||
|
|
5a4c2a4974 | ||
|
|
9063e8f4a4 | ||
|
|
519552083b | ||
|
|
f6ad66bd30 | ||
|
|
65affb5146 | ||
|
|
38d9378e48 | ||
|
|
cd8011e858 | ||
|
|
e352562c51 | ||
|
|
3dd2a19915 | ||
|
|
384286cf0a | ||
|
|
162281e753 | ||
|
|
9a3603fea3 | ||
|
|
121dc85e40 | ||
|
|
823bad2ace | ||
|
|
c9ed8543c8 | ||
|
|
d752c68790 | ||
|
|
39c0409d42 | ||
|
|
7937738258 | ||
|
|
ea092bda99 | ||
|
|
3b908a5857 | ||
|
|
536e14dfe8 | ||
|
|
edd2c5f779 | ||
|
|
ee76c2c06e | ||
|
|
0190a92c26 | ||
|
|
730da103b1 | ||
|
|
ffe30289ee | ||
|
|
cfe9af5c3c | ||
|
|
9647ce153d | ||
|
|
bec63c1ad0 | ||
|
|
3dec0ff85b | ||
|
|
8868d8e99e | ||
|
|
9a0d9506f8 | ||
|
|
ec57b813da | ||
|
|
57c4d96467 | ||
|
|
ebaf0a08b5 | ||
|
|
abae4b5106 | ||
|
|
81b721be2b | ||
|
|
e9108ae3f8 | ||
|
|
acc7b65649 | ||
|
|
77ed177855 | ||
|
|
71cb5c579d | ||
|
|
f22e4c261a | ||
|
|
4a046ca70e | ||
|
|
62ee3fa0f1 | ||
|
|
ef0403af0e | ||
|
|
044b27967e | ||
|
|
4942a2480b | ||
|
|
cde852f0f9 | ||
|
|
a435e88b25 | ||
|
|
add954db80 | ||
|
|
76ec639fea | ||
|
|
155decfca1 | ||
|
|
3ab131786b | ||
|
|
a273dea755 | ||
|
|
9a6d8e3a33 | ||
|
|
4d9ecef6fd | ||
|
|
4df72a4015 | ||
|
|
c3e2dc4ce8 | ||
|
|
5a1bcd53a3 | ||
|
|
4c8865ed5b | ||
|
|
cb6bd50db0 | ||
|
|
614dbb3674 | ||
|
|
8ed8bb2dad | ||
|
|
a1f6985534 | ||
|
|
6bdf2e38cc | ||
|
|
19ae55ebd4 | ||
|
|
61cef46a95 | ||
|
|
172738f7b7 | ||
|
|
5d0d60d89e | ||
|
|
3cfdc64735 | ||
|
|
b47069ccad | ||
|
|
2051b3921b | ||
|
|
46f46483f8 | ||
|
|
66b02b58b6 | ||
|
|
4428c3d7d8 | ||
|
|
2d4b9786fa | ||
|
|
d2d9c4be6f | ||
|
|
a9d6ac29f1 | ||
|
|
4d50bd5c78 | ||
|
|
fdd651b9cc | ||
|
|
7b56f102cc | ||
|
|
e329360daa | ||
|
|
5e8a431a92 | ||
|
|
cd3f21a92e | ||
|
|
03d3ae3eb4 | ||
|
|
0c350dcf6e | ||
|
|
c6b3d15d72 | ||
|
|
8d7f7cb438 | ||
|
|
b5a4121574 | ||
|
|
916ca87db4 | ||
|
|
bfea97f14e | ||
|
|
f98657aca8 | ||
|
|
45c5e770fa | ||
|
|
f4ce7ecf4b | ||
|
|
8dfe1fe97f | ||
|
|
4bf165efaf | ||
|
|
c7f6ae5be9 | ||
|
|
d83d2d5f4e | ||
|
|
2362b2a5a0 | ||
|
|
fb08a17ec8 | ||
|
|
1bcc2d544a | ||
|
|
6fd1205681 | ||
|
|
da2b00c9cf | ||
|
|
f6213595d1 | ||
|
|
b1b2bd5b97 | ||
|
|
aa19cbbfc7 | ||
|
|
8d39faddc9 | ||
|
|
52714dbcc9 | ||
|
|
be92d3a0bc | ||
|
|
f3189f72fd | ||
|
|
144c1d4e2f | ||
|
|
e5964f9d93 | ||
|
|
ea30cbe117 | ||
|
|
598a9664a7 | ||
|
|
d04628a42d | ||
|
|
7bce63d74e | ||
|
|
452fe9f76d | ||
|
|
7983ff5db2 | ||
|
|
c361ab954d | ||
|
|
a8735a6465 | ||
|
|
76255c0dd4 | ||
|
|
87655ff3cd | ||
|
|
fc7be2ac1a | ||
|
|
e93b95bee8 | ||
|
|
6a18d6918e | ||
|
|
578ce09b5e | ||
|
|
27f6177fc9 | ||
|
|
20c0deeac4 | ||
|
|
f1f8cdb6e9 | ||
|
|
345e4e846c | ||
|
|
6f57b36158 | ||
|
|
0264e10e69 | ||
|
|
067f5bf5a3 | ||
|
|
c81b643cdf | ||
|
|
388ad077d6 | ||
|
|
db93cdd95f | ||
|
|
68304a3fc1 | ||
|
|
13259c114a | ||
|
|
5131ba453d | ||
|
|
8fdc9939cd | ||
|
|
c15449492a | ||
|
|
1cab1e06d9 | ||
|
|
4831926869 | ||
|
|
4fcf25077b | ||
|
|
c32461f322 | ||
|
|
0abe6fc0b4 | ||
|
|
edc3d53f94 | ||
|
|
bb5fbbe746 | ||
|
|
36f3e3fe50 | ||
|
|
65c8000f66 | ||
|
|
2d45f8bc40 | ||
|
|
7a0d4ad508 | ||
|
|
5ae93bb569 | ||
|
|
aa6ca46792 | ||
|
|
e8c11f6e15 | ||
|
|
08bb8e3df9 | ||
|
|
d62bf6812e | ||
|
|
422abc205b | ||
|
|
1269104112 | ||
|
|
97d762f01f | ||
|
|
671b5e29d0 | ||
|
|
c7538a35a2 | ||
|
|
458658a71b | ||
|
|
e348a1d2c5 | ||
|
|
59ff3998bc | ||
|
|
9471dc27e0 | ||
|
|
4b559a58d1 | ||
|
|
5980ab9b69 | ||
|
|
70e5daf8c6 | ||
|
|
92e1553eed | ||
|
|
2236e07722 | ||
|
|
5166cd788b | ||
|
|
de89d41e72 | ||
|
|
3dd5526938 | ||
|
|
a88893b10a | ||
|
|
51d1794e88 | ||
|
|
95e8fc73e6 | ||
|
|
96974ab439 | ||
|
|
127b22d7ce | ||
|
|
ca962e635e | ||
|
|
a70cc97b8e | ||
|
|
79ae75030c | ||
|
|
32f8a52c2b | ||
|
|
d1a1f7004b | ||
|
|
52289568bf | ||
|
|
dada79cf65 | ||
|
|
139a2bac1a | ||
|
|
3e4eaeab35 | ||
|
|
484171dd1b | ||
|
|
1c69b1695a | ||
|
|
7cfba0e176 | ||
|
|
ade2b65a94 | ||
|
|
950a6d4c5d | ||
|
|
19348ef205 | ||
|
|
5662b124e0 | ||
|
|
5c1307f6f2 | ||
|
|
2105b2ecdb | ||
|
|
d05bf9396d | ||
|
|
5b22822ac3 | ||
|
|
e08e1418e5 | ||
|
|
31d0a5c40e | ||
|
|
89446d56e0 | ||
|
|
bbcad40fcf | ||
|
|
70db169976 | ||
|
|
abc867935b | ||
|
|
2bb85dc16c | ||
|
|
00f4bf3d16 | ||
|
|
0cca838db9 | ||
|
|
abc8e1c377 | ||
|
|
de67b6bc0c | ||
|
|
058534af21 | ||
|
|
ce1b621488 | ||
|
|
4434c7862e | ||
|
|
86c4246f75 | ||
|
|
7dc3fb9854 | ||
|
|
71b0226c54 | ||
|
|
a18d5bbe34 | ||
|
|
f1352c6ef0 | ||
|
|
7e6ce1a1ef | ||
|
|
9f5471ee85 | ||
|
|
3bf36d6c93 | ||
|
|
38523835fd | ||
|
|
4cb2a929a5 | ||
|
|
1db14c710b | ||
|
|
13787629b6 | ||
|
|
42c705e362 | ||
|
|
4765e4f83c | ||
|
|
ddffc8a36e | ||
|
|
8aec71845b | ||
|
|
c01864ccf5 | ||
|
|
4f839ae44e | ||
|
|
db6404a7c6 | ||
|
|
93e0acc8e9 | ||
|
|
9fa7a48b86 | ||
|
|
c0b929035f | ||
|
|
7612e38695 | ||
|
|
47329eaebc | ||
|
|
f53a951daf | ||
|
|
2181137181 | ||
|
|
6e925f6b99 | ||
|
|
3b5495bf72 | ||
|
|
3617432113 | ||
|
|
f95beff6d4 | ||
|
|
6d365fde14 | ||
|
|
b16ff33688 | ||
|
|
9d8d0bed38 | ||
|
|
f967116087 | ||
|
|
721352c5aa | ||
|
|
496ba986bf | ||
|
|
101a3b24ce | ||
|
|
201dc570cd | ||
|
|
ff359c477f | ||
|
|
74cb8d9655 | ||
|
|
91d0710e04 | ||
|
|
0cc3f08ae7 | ||
|
|
ac391bfc17 | ||
|
|
e5a04e8d38 | ||
|
|
8cc07e51bf | ||
|
|
4b7090cf7c | ||
|
|
8c8cc035ab | ||
|
|
4b93d30ec0 | ||
|
|
d8ff2488a3 | ||
|
|
b771df88da | ||
|
|
54e237cec8 | ||
|
|
b5c848474b | ||
|
|
dae52089e3 | ||
|
|
4c4f3d04e9 | ||
|
|
e8674487f2 | ||
|
|
e2fadebf64 | ||
|
|
d3331fea7f | ||
|
|
bdcd9e035c | ||
|
|
7f3453ce5c | ||
|
|
ed7a7bc879 | ||
|
|
5a6b8222df | ||
|
|
3262486a96 | ||
|
|
c73b30556f | ||
|
|
2ec89d6a20 | ||
|
|
a0b69df20d | ||
|
|
57aa3de9bb | ||
|
|
38a4c1aede | ||
|
|
fcc77635c2 | ||
|
|
25be1563e1 | ||
|
|
4a9b0e8db6 | ||
|
|
ab35821b59 | ||
|
|
14439ccf77 | ||
|
|
5ddfa989d0 | ||
|
|
a915348b22 | ||
|
|
a7fe35513a | ||
|
|
701024cf80 | ||
|
|
4ecb0d82e7 | ||
|
|
5279be64d0 | ||
|
|
b9c3e85f89 | ||
|
|
8aaa671412 | ||
|
|
873ebddbd0 | ||
|
|
13c628b58b | ||
|
|
3500236d32 | ||
|
|
2f881c0c91 | ||
|
|
9d45e4e0ae | ||
|
|
13fac3072d | ||
|
|
6d8fdb131f | ||
|
|
ee65089eb7 | ||
|
|
40c7d18382 | ||
|
|
3236a9a5b7 | ||
|
|
d0522d7d4f | ||
|
|
aef6b32019 | ||
|
|
11b4c886d7 | ||
|
|
3470252768 | ||
|
|
1a3d5d0bdc | ||
|
|
05f07b1f47 | ||
|
|
898f1dd151 | ||
|
|
17ac6bb1a4 | ||
|
|
f05bed594b | ||
|
|
e63b67b99e | ||
|
|
efbc045c8a | ||
|
|
172d4b7c5e | ||
|
|
8b9177b484 | ||
|
|
2acb065d38 | ||
|
|
0b33b0b6a2 | ||
|
|
0390891280 | ||
|
|
9203534f67 | ||
|
|
e15d11a693 | ||
|
|
c021d3d9ce | ||
|
|
ea3cc9b153 | ||
|
|
3612b64dae | ||
|
|
79f9180f6b | ||
|
|
766ef5f420 | ||
|
|
bdbb9acfd0 | ||
|
|
6bdac3aaec | ||
|
|
14acdbe7d1 | ||
|
|
895280fc79 | ||
|
|
83ae303b31 | ||
|
|
cc81a10dd2 | ||
|
|
6e3600011b | ||
|
|
2b07b5ba3a | ||
|
|
7b64b2a708 | ||
|
|
810f5e7409 | ||
|
|
1affb2517a | ||
|
|
85ea9b3255 | ||
|
|
07e052b865 | ||
|
|
bc0ea740f1 | ||
|
|
841b4aa814 | ||
|
|
9989478b91 | ||
|
|
d3227eceff | ||
|
|
5f71f6987c | ||
|
|
86dbb1bdcf | ||
|
|
77ac8d1e62 | ||
|
|
e62d417324 | ||
|
|
b8f85837fb | ||
|
|
2237d7ef8a | ||
|
|
65210ea91d | ||
|
|
16c1622b1f | ||
|
|
635557ca45 | ||
|
|
b9daa62ece | ||
|
|
808be96de3 | ||
|
|
1e93289f23 | ||
|
|
ccf0f84598 | ||
|
|
3ec4c7501d | ||
|
|
f55034906c | ||
|
|
cbd3c60c5d | ||
|
|
2037fec878 | ||
|
|
772fd1b563 | ||
|
|
d9309cb215 | ||
|
|
433c34e4ce | ||
|
|
68a4769f1e | ||
|
|
248569d0a8 | ||
|
|
5146e39023 | ||
|
|
ecd1d69863 | ||
|
|
06219b0c58 | ||
|
|
0a74bd1718 | ||
|
|
8a5b24afff | ||
|
|
6bdd7f7a57 | ||
|
|
1bb2552384 | ||
|
|
b5b20452cc | ||
|
|
4a34703cd3 | ||
|
|
a8d9b57c47 | ||
|
|
52bbf3be21 | ||
|
|
3bde0666e2 | ||
|
|
b5374a1f90 | ||
|
|
18b8d23148 | ||
|
|
f51b1e1b6b | ||
|
|
ffc4f9d930 | ||
|
|
5680fc839b | ||
|
|
57d435ccf4 | ||
|
|
4b90b8e6d8 | ||
|
|
fc8dcec2bb | ||
|
|
a5245fda65 | ||
|
|
4eec2a6414 | ||
|
|
a536e9fc4b | ||
|
|
a961407379 | ||
|
|
1fd6c363ba | ||
|
|
0a7f1faad1 | ||
|
|
e79d963802 | ||
|
|
1b4bbacd5f | ||
|
|
447c6fbb5f | ||
|
|
78acaccd89 | ||
|
|
bdf9671280 | ||
|
|
357e44284d | ||
|
|
9dced3f596 | ||
|
|
63e3560dd7 | ||
|
|
434525943c | ||
|
|
f0dbf2fc4d | ||
|
|
3137dbec33 | ||
|
|
e71a8fce47 | ||
|
|
ff550e897a | ||
|
|
10034fcbba | ||
|
|
36f8ce453f | ||
|
|
c2e40acb2c | ||
|
|
82b1bb668d | ||
|
|
935a8258a6 | ||
|
|
fa483e5806 | ||
|
|
e0c9658cb9 | ||
|
|
0266a46b32 | ||
|
|
e7294f2950 | ||
|
|
c9f325e75d | ||
|
|
0fa353c2e2 | ||
|
|
c7da090882 | ||
|
|
ee609c8ef0 | ||
|
|
6891ce2bc8 | ||
|
|
94f5adba04 | ||
|
|
b8f843993a | ||
|
|
f9add21899 | ||
|
|
1277da8bfe | ||
|
|
55650fb734 | ||
|
|
d2f4b68c9f | ||
|
|
a76731a991 | ||
|
|
536b8166ce | ||
|
|
d43106b0af | ||
|
|
3688371ce8 | ||
|
|
6d66eb7759 | ||
|
|
8502bf4bfa | ||
|
|
d8225ad653 | ||
|
|
cfb68a0511 | ||
|
|
76677e0aea | ||
|
|
515ee891d3 | ||
|
|
3aea1f3c9d | ||
|
|
8d944f9a4a | ||
|
|
331c8ae247 | ||
|
|
c71a429f61 | ||
|
|
3bad9e523c | ||
|
|
dfa61f1b2d | ||
|
|
6331fa5ced | ||
|
|
707b03b8c8 | ||
|
|
f2f93ed141 | ||
|
|
37e16c7a4c | ||
|
|
41b0c3242e | ||
|
|
48ed051edf | ||
|
|
502642fd25 | ||
|
|
4abe6a7a00 | ||
|
|
3f8fa64b98 | ||
|
|
527ff1b1fb | ||
|
|
804467dce2 | ||
|
|
4d7f308821 | ||
|
|
a5b8418845 | ||
|
|
93d428b8c5 | ||
|
|
7c424ad60c | ||
|
|
5b29a8680d | ||
|
|
8f57c44837 | ||
|
|
b23939127b | ||
|
|
3196322063 | ||
|
|
54c96d98d1 | ||
|
|
f5f92fbb03 | ||
|
|
be0876603c | ||
|
|
7c1ef143f9 | ||
|
|
6d128595e7 | ||
|
|
2f55abfc60 | ||
|
|
f93044ac3b | ||
|
|
7ed422a3c1 | ||
|
|
823b3b8aa8 | ||
|
|
9a701560f4 | ||
|
|
9800154d01 | ||
|
|
4b3f18ccdb | ||
|
|
840d78b2f4 | ||
|
|
b409fd775d | ||
|
|
dbcfb20fab | ||
|
|
12a5965740 | ||
|
|
006ab75433 | ||
|
|
c72ea91743 | ||
|
|
f39ce20580 | ||
|
|
b5c59e6b7d | ||
|
|
b0ecdcc8b6 | ||
|
|
8e1560f412 | ||
|
|
df927eae74 | ||
|
|
30aea047e3 | ||
|
|
cbcadaa449 | ||
|
|
9f4226093b | ||
|
|
fca0e897b2 | ||
|
|
2f729b56fa | ||
|
|
d9f3f64c76 | ||
|
|
e8fa909c2f | ||
|
|
44f6636653 | ||
|
|
148a0d0fc6 | ||
|
|
632ba69663 | ||
|
|
b2465dd2ee | ||
|
|
e56b87766b | ||
|
|
f7ca2e416a | ||
|
|
002f68b0a1 | ||
|
|
aa31be5c5a | ||
|
|
6c0b7017bd | ||
|
|
581774e001 | ||
|
|
3847a6616e | ||
|
|
48fbe28355 | ||
|
|
e3ee5bc1d5 | ||
|
|
a2da9bea58 | ||
|
|
e4512e12c5 | ||
|
|
114f48fb17 | ||
|
|
289e018160 | ||
|
|
cb6699eeed | ||
|
|
802011bb7e | ||
|
|
6cd8e769be | ||
|
|
9f6f67d331 | ||
|
|
161a8fe2bf | ||
|
|
b9c9839bb7 | ||
|
|
76edbee48c | ||
|
|
4142d7a050 | ||
|
|
a0306c69e1 | ||
|
|
31823f6282 | ||
|
|
9b4fffde29 | ||
|
|
cce03e250d | ||
|
|
9b32cad946 | ||
|
|
2877a1057e | ||
|
|
e2debe3c39 | ||
|
|
f54ab11f18 | ||
|
|
b560e281d0 | ||
|
|
3bb4ef5727 | ||
|
|
900c008d20 | ||
|
|
c1183a09a8 | ||
|
|
e04b7b55b0 | ||
|
|
329cc80933 | ||
|
|
a13f0706b4 | ||
|
|
55811de4b8 | ||
|
|
ab456f179e | ||
|
|
f9d5bcd352 | ||
|
|
6a337884b5 | ||
|
|
f953d115da | ||
|
|
88e8fc840f | ||
|
|
d1818e31b0 | ||
|
|
3f4bf647e8 | ||
|
|
725a7e6dec | ||
|
|
e08b210001 | ||
|
|
ec08ccb996 | ||
|
|
b47a146c2b | ||
|
|
14dff27d45 | ||
|
|
305a3c94d0 | ||
|
|
218739a6b5 | ||
|
|
390e69c01c | ||
|
|
4ef274acf0 | ||
|
|
8267279779 | ||
|
|
6d971b9235 | ||
|
|
98dc160886 | ||
|
|
a869c88b43 | ||
|
|
0b86070fe9 | ||
|
|
5c9b6736f0 | ||
|
|
fd4057df94 | ||
|
|
1b1945e1f5 | ||
|
|
ebb053b900 | ||
|
|
3381d9b595 | ||
|
|
d7a11ef394 | ||
|
|
9d40cffabe | ||
|
|
de44c63557 | ||
|
|
ac25477cd7 | ||
|
|
59b86aa090 | ||
|
|
6abd48d480 | ||
|
|
72fc6b8c5a | ||
|
|
fcce4a6853 | ||
|
|
a3b1a2c781 | ||
|
|
a838a1706f | ||
|
|
a24c9fbafb | ||
|
|
ab255e78c5 | ||
|
|
2628678d82 | ||
|
|
eaf9b7f049 | ||
|
|
b8df4d0b79 | ||
|
|
eb315f34dc | ||
|
|
af535757a8 | ||
|
|
600e030c6d | ||
|
|
e86b813551 | ||
|
|
af6653dfeb | ||
|
|
f93e0c868c | ||
|
|
e53aaddc9c | ||
|
|
d3ebb99131 | ||
|
|
82037b70e4 | ||
|
|
323dfb1853 | ||
|
|
59b6d2ef66 | ||
|
|
7b99167d30 | ||
|
|
c829f190df | ||
|
|
f306f334c0 | ||
|
|
3fb8243264 | ||
|
|
b8861b9d78 | ||
|
|
61d98ca4ca | ||
|
|
a3a22604ba | ||
|
|
1766c3f6d6 | ||
|
|
aae259479f | ||
|
|
b640a053e3 | ||
|
|
a3cc17705d | ||
|
|
78ac1d2a12 | ||
|
|
6ec36c9605 | ||
|
|
bd2b03876b | ||
|
|
0c5cb9c98f | ||
|
|
eddcc6fe27 | ||
|
|
953b55439c | ||
|
|
e526890e3e | ||
|
|
71e1e2468b | ||
|
|
fcb7c3e82a | ||
|
|
0d65635bc4 | ||
|
|
4aa76a859f | ||
|
|
003789265c | ||
|
|
1bb3b5e51b | ||
|
|
032ac3ed97 |
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"env": {
|
||||
"es6": true,
|
||||
"node": true
|
||||
"node": true,
|
||||
"es6": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2017
|
||||
"ecmaVersion": 8
|
||||
},
|
||||
"rules": {
|
||||
"indent": [
|
||||
@@ -23,6 +23,7 @@
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
]
|
||||
],
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
289
CHANGES
289
CHANGES
@@ -1425,3 +1425,292 @@
|
||||
* Add UI to toggle dynamic DNS
|
||||
* Add support for hyphenated subdomains
|
||||
|
||||
[3.3.2]
|
||||
* Use new addons with REST APIs
|
||||
* Ubuntu 18.04 LTS support
|
||||
* Custom env vars can be set per application
|
||||
* Add a button to renew certs
|
||||
* Add better support for private builds
|
||||
* cloudflare: Fix crash when using bad email
|
||||
* cloudflare: HTTP proxying works now
|
||||
* add new exoscale-sos regions
|
||||
* Add UI to toggle dynamic DNS
|
||||
* Add support for hyphenated subdomains
|
||||
* Add domain, mail events to eventlog
|
||||
|
||||
[3.3.3]
|
||||
* Use new addons with REST APIs
|
||||
* Ubuntu 18.04 LTS support
|
||||
* Custom env vars can be set per application
|
||||
* Add a button to renew certs
|
||||
* Add better support for private builds
|
||||
* cloudflare: Fix crash when using bad email
|
||||
* cloudflare: HTTP proxying works now
|
||||
* add new exoscale-sos regions
|
||||
* Add UI to toggle dynamic DNS
|
||||
* Add support for hyphenated subdomains
|
||||
* Add domain, mail events to eventlog
|
||||
|
||||
[3.3.4]
|
||||
* Use new addons with REST APIs
|
||||
* Ubuntu 18.04 LTS support
|
||||
* Custom env vars can be set per application
|
||||
* Add a button to renew certs
|
||||
* Add better support for private builds
|
||||
* cloudflare: Fix crash when using bad email
|
||||
* cloudflare: HTTP proxying works now
|
||||
* add new exoscale-sos regions
|
||||
* Add UI to toggle dynamic DNS
|
||||
* Add support for hyphenated subdomains
|
||||
* Add domain, mail events to eventlog
|
||||
|
||||
[3.4.0]
|
||||
* Improve error page
|
||||
* Add system view to manage addons and view their status
|
||||
* Fix iconset regression for account and Cloudron name edits
|
||||
* Add server reboot button and warn if reboot is required for security updates
|
||||
* Backup and update tasks are now cancelable
|
||||
* Move graphite away from port 3000 (reserved by ESXi)
|
||||
* Flexible mailbox management
|
||||
* Automatic updates can be toggled per app
|
||||
|
||||
[3.4.1]
|
||||
* Improve error page
|
||||
* Add system view to manage addons and view their status
|
||||
* Fix iconset regression for account and Cloudron name edits
|
||||
* Add server reboot button and warn if reboot is required for security updates
|
||||
* Backup and update tasks are now cancelable
|
||||
* Move graphite away from port 3000 (reserved by ESXi)
|
||||
* Flexible mailbox management
|
||||
* Automatic updates can be toggled per app
|
||||
|
||||
[3.4.2]
|
||||
* Improve error page
|
||||
* Add system view to manage addons and view their status
|
||||
* Fix iconset regression for account and Cloudron name edits
|
||||
* Add server reboot button and warn if reboot is required for security updates
|
||||
* Backup and update tasks are now cancelable
|
||||
* Move graphite away from port 3000 (reserved by ESXi)
|
||||
* Flexible mailbox management
|
||||
* Automatic updates can be toggled per app
|
||||
|
||||
[3.4.3]
|
||||
* Improve error page
|
||||
* Add system view to manage addons and view their status
|
||||
* Fix iconset regression for account and Cloudron name edits
|
||||
* Add server reboot button and warn if reboot is required for security updates
|
||||
* Backup and update tasks are now cancelable
|
||||
* Move graphite away from port 3000 (reserved by ESXi)
|
||||
* Flexible mailbox management
|
||||
* Automatic updates can be toggled per app
|
||||
* Fix issue where OOM mails are sent out without a rate limit
|
||||
|
||||
[3.5.0]
|
||||
* Add UI to switch dashboard domain
|
||||
* Fix remote support button to not remove misparsed ssh keys
|
||||
* cloudflare: preseve domain proxying status
|
||||
* Fix issue where oom killer might kill the box code or the updater
|
||||
* Add contabo and netcup as supported providers
|
||||
* Allow full logs to be downloaded
|
||||
* Update Haraka to 2.8.22
|
||||
* Log events in the mail container
|
||||
* Fix issue where SpamAssassin and SPF checks were run for outbound email
|
||||
* Improve various eventlog messages
|
||||
* Track dyndns change events
|
||||
* Add new S3 regions - Paris/Stockholm/Osaka
|
||||
* Retry errored downloads during restore
|
||||
* Add user pagination UI
|
||||
* Add namecheap as supported DNS provider
|
||||
|
||||
[3.5.1]
|
||||
* Add dashboard domain change event
|
||||
* Fix issue where notification email were sent from incorrect domain
|
||||
* Alert about configuration issues in the notification UI
|
||||
* Switching dashboard domain now updates MX, SPF records
|
||||
* Mailbox and lists UI is now always visible (but disabled) when incoming email is disabled
|
||||
* Fix issue where long passwords were not accepted
|
||||
* DNS and backup credential secrets are not returned in API calls anymore
|
||||
* Send notification when an app that went down, came back up
|
||||
|
||||
[3.5.2]
|
||||
* Fix encoding of links in plain text email
|
||||
* Hide mail relay password
|
||||
* Do not return API tokens in REST API
|
||||
|
||||
[3.5.3]
|
||||
* Make reboot required check server side
|
||||
* Update node to 10.15.1
|
||||
* Enable gzip compression for large objects
|
||||
* Update docker to 18.09
|
||||
* Add a way to lock specific settings
|
||||
* Add UI to copy app's backup id
|
||||
* Block platform updates based on app manifest constraints
|
||||
* Make crash logs viewable via the dashboard
|
||||
* Fix issue where uploading of filenames with brackets and plus was not working
|
||||
* Add notification for cert renewal and backup failures
|
||||
* Fix issue where mail container was not updated with the latest certificate
|
||||
|
||||
[3.5.4]
|
||||
* Make reboot required check server side
|
||||
* Update node to 10.15.1
|
||||
* Enable gzip compression for large objects
|
||||
* Update docker to 18.09
|
||||
* Add a way to lock specific settings
|
||||
* Add UI to copy app's backup id
|
||||
* Block platform updates based on app manifest constraints
|
||||
* Make crash logs viewable via the dashboard
|
||||
* Fix issue where uploading of filenames with brackets and plus was not working
|
||||
* Add notification for cert renewal and backup failures
|
||||
* Fix issue where mail container was not updated with the latest certificate
|
||||
|
||||
[4.0.0]
|
||||
* (mail) Bump mail_max_userip_connections to 50
|
||||
* Fix issue where DKIM was not setup correctly during a restore
|
||||
* (mysql) Remove any stale lock file on restart
|
||||
* Add a way to disable outbound mail for a domain
|
||||
* Cleanup task logs
|
||||
* Fix issue where dashboard location might conflict with existing app location
|
||||
* Ad graphite to services
|
||||
* Add labels and tags to apps
|
||||
* Ensure MySQL is storing data/time in UTC
|
||||
* Fix bug where the UI redirects to login screen when enabling 2FA with invalid token
|
||||
* Use unbound resolver when resolving NS record of a domain
|
||||
* Various fixes for notifications
|
||||
* Add FTP support for apps
|
||||
* Add app version as part of info dialog
|
||||
* (backup) Do not abort archive if file(s) disappear
|
||||
* Show app upstream version in the info dialog
|
||||
* Add Scaleway ObjectStorage backup backend
|
||||
* Preserve update backups for 3 weeks
|
||||
* Make send test mail functionality work with secondary domain
|
||||
* Add support for an external email relay that does not require authentication
|
||||
* Add option to accept self-signed certs when using external mail relay
|
||||
* Allow publishing and listing community supported apps
|
||||
* Remove spaces support
|
||||
* Features implementation for customization
|
||||
|
||||
[4.0.1]
|
||||
* Make it easier to import email
|
||||
* Give SFTP access only to admins
|
||||
|
||||
[4.0.2]
|
||||
* Fix GCDNS crash
|
||||
* Add option to update without backing up
|
||||
|
||||
[4.0.3]
|
||||
* Fix dashboard issue for non-admins
|
||||
|
||||
[4.1.0]
|
||||
* Remove password requirement for uninstalling apps and users
|
||||
* Hosting provider edition
|
||||
* Enforce limits in mail container
|
||||
* Fix crash when using unauthenticated relay
|
||||
* Fix domain and tag filtering
|
||||
* Customizable app icons
|
||||
* Remove obsolete X-Frame-Options from nginx configs
|
||||
* Give SFTP access based on access restriction
|
||||
|
||||
[4.1.1]
|
||||
* Add UI hint about SFTP access restriction
|
||||
|
||||
[4.1.2]
|
||||
* Accept incoming mail from a private relay
|
||||
* Fix issue where unused addon images were not pruned
|
||||
* Add UI for redirect from multiple domains
|
||||
* Allow apps to be relocated to custom data directory
|
||||
* Make all cloudron env vars have CLOUDRON_ prefix
|
||||
* Update manifest version to 2
|
||||
* Fix issue where DKIM keys were inaccessible
|
||||
* Fix DKIM selector conflict when adding same domain across multiple cloudrons
|
||||
* Fix name.com DNS backend issue for naked domains
|
||||
* Add DigitalOcean Frankfurt (fra1) region for backup storage
|
||||
|
||||
[4.1.3]
|
||||
* Update manifest format package
|
||||
|
||||
[4.1.4]
|
||||
* Add CLOUDRON_ prefix to MySQL addon variables
|
||||
|
||||
[4.1.5]
|
||||
* Make the terminal addon button inject variables based on manifest version
|
||||
* Preserve addon passwords correctly when using v2 manifest
|
||||
* Show error message instead of logging out user when invalid 2FA token is provided
|
||||
* Ensure redis vars are renamed with manifest v2
|
||||
* Add missing Scaleway Object Storage to restore UI
|
||||
* Fix Exoscale endpoints in restore UI
|
||||
* Reset the app icon when showing the configure UI
|
||||
|
||||
[4.1.6]
|
||||
* Fix issue where CLOUDRON_APP_HOSTNAME was incorrectly set
|
||||
* Remove chat link from the footer of login screen
|
||||
* Add support for oplog tailing in mongodb
|
||||
* Fix LDAP not accessible via scheduler containers
|
||||
|
||||
[4.1.7]
|
||||
* Fix issue where login looped when admin bit was removed
|
||||
|
||||
[4.2.0]
|
||||
* Fix issue where tar backups with files > 8GB was corrupt
|
||||
* Add SparkPost as mail relay backend
|
||||
* Add Wasabi storage backend
|
||||
* TOTP tokens are now checked for with +- 60 seconds
|
||||
* IP based restore
|
||||
* Fix issue where task logs were not getting rotated correctly
|
||||
* Add notification for box update
|
||||
* User enable/disable flag
|
||||
* Check disk space before various operations like install, update, backup etc
|
||||
* Collect per app du information
|
||||
* Set Cloudron specific UA for healthchecks
|
||||
* Show message why an app task is 'pending'
|
||||
* Rework app task system so that we can now pass dynamic arguments
|
||||
* Add external LDAP server integration
|
||||
|
||||
[4.2.1]
|
||||
* Rework the app configuration routes & UI
|
||||
* Fine grained eventlog for app configuration
|
||||
* Update Haraka to 2.8.24
|
||||
* Set sieve_max_redirects to 64
|
||||
* SRS support for mail forwarding
|
||||
* Fix issue where sieve responses were not sent via the relay
|
||||
* File based session store
|
||||
* Fix API token error reporting for namecheap backend
|
||||
|
||||
[4.2.2]
|
||||
* Fix typos in migration
|
||||
|
||||
[4.2.3]
|
||||
* Remove flicker of custom icon
|
||||
* Preserve PROVIDER setting from cloudron.conf
|
||||
* Add Skip backup option when updating an app
|
||||
* Fix bug where nginx was not reloaded on cert renewal
|
||||
|
||||
[4.2.4]
|
||||
* Fix demo settings state regression
|
||||
|
||||
[4.2.5]
|
||||
* Fix the demo settins fix
|
||||
|
||||
[4.2.6]
|
||||
* Fix configuration of empty app location (subdomain)
|
||||
|
||||
[4.2.7]
|
||||
* Fix issue where the icon for normal users was displayed incorrectly
|
||||
* Kill stuck backup processes after 12 hours and notify admins
|
||||
* Reconfigure email apps when mail domain is added/removed
|
||||
* Fix crash when only udp ports are defined
|
||||
|
||||
[4.3.0]
|
||||
* Add timeout to kill long running tasks in case they get stuck
|
||||
* email: Auto-subscribe to Spam folder
|
||||
* Allow setting a custom CSP policy
|
||||
* ticket: when email is down, add a field to provide alternate contact email
|
||||
* Re-work app import flow
|
||||
* Add pagination and search to mailbox and mail alias listing
|
||||
* Add UI and workflow to add a private registry
|
||||
* Show external LDAP connector
|
||||
* Network view: Allow IP address detection to be configurable
|
||||
* Add support for custom docker registry
|
||||
* Resolve any lists and aliases in a mailing list
|
||||
* Rename Accounts view to Profile
|
||||
* Add search for groups and user association UI
|
||||
|
||||
|
||||
694
LICENSE
694
LICENSE
@@ -1,661 +1,35 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
The Cloudron Subscription license
|
||||
Copyright (c) 2019 Cloudron UG
|
||||
|
||||
With regard to the Cloudron Software:
|
||||
|
||||
This software and associated documentation files (the "Software") may only be
|
||||
used in production, if you (and any entity that you represent) have agreed to,
|
||||
and are in compliance with, the Cloudron Subscription Terms of Service, available
|
||||
at https://cloudron.io/legal/terms.html (the “Subscription Terms”), or other
|
||||
agreement governing the use of the Software, as agreed by you and Cloudron,
|
||||
and otherwise have a valid Cloudron Subscription. Subject to the foregoing sentence,
|
||||
you are free to modify this Software and publish patches to the Software. You agree
|
||||
that Subscription and/or its licensors (as applicable) retain all right, title and
|
||||
interest in and to all such modifications and/or patches, and all such modifications
|
||||
and/or patches may only be used, copied, modified, displayed, distributed, or otherwise
|
||||
exploited with a valid Cloudron subscription. Notwithstanding the foregoing, you may copy
|
||||
and modify the Software for development and testing purposes, without requiring a
|
||||
subscription. You agree that Cloudron and/or its licensors (as applicable) retain
|
||||
all right, title and interest in and to all such modifications. You are not
|
||||
granted any other rights beyond what is expressly stated herein. Subject to the
|
||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
||||
and/or sell the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
For all third party components incorporated into the Cloudron Software, those
|
||||
components are licensed under the original license provided by the owner of the
|
||||
applicable component.
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
box
|
||||
Copyright (C) 2016,2017,2018 Cloudron UG
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
10
README.md
10
README.md
@@ -37,16 +37,11 @@ anyone to effortlessly host web applications on their server on their own terms.
|
||||
|
||||
## Demo
|
||||
|
||||
Try our demo at https://my-demo.cloudron.me (username: cloudron password: cloudron).
|
||||
Try our demo at https://my.demo.cloudron.io (username: cloudron password: cloudron).
|
||||
|
||||
## Installing
|
||||
|
||||
You can install the Cloudron platform on your own server or get a managed server
|
||||
from cloudron.io. In either case, the Cloudron platform will keep your server and
|
||||
apps up-to-date and secure.
|
||||
|
||||
* [Selfhosting](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
* [Managed Hosting](https://cloudron.io/managed.html)
|
||||
[Install script](https://cloudron.io/documentation/installation/) - [Pricing](https://cloudron.io/pricing.html)
|
||||
|
||||
**Note:** This repo is a small part of what gets installed on your server - there is
|
||||
the dashboard, database addons, graph container, base image etc. Cloudron also relies
|
||||
@@ -64,6 +59,7 @@ the containers in the Cloudron.
|
||||
|
||||
## Community
|
||||
|
||||
* [Chat](https://chat.cloudron.io)
|
||||
* [Forum](https://forum.cloudron.io/)
|
||||
* [Support](mailto:support@cloudron.io)
|
||||
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
assertNotEmpty() {
|
||||
: "${!1:? "$1 is not set."}"
|
||||
}
|
||||
|
||||
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")"/.. && pwd)"
|
||||
export JSON="${SOURCE_DIR}/node_modules/.bin/json"
|
||||
|
||||
revision=$(git rev-parse HEAD)
|
||||
box_name=""
|
||||
server_id=""
|
||||
server_ip=""
|
||||
destroy_server="yes"
|
||||
deploy_env="dev"
|
||||
|
||||
# Only GNU getopt supports long options. OS X comes bundled with the BSD getopt
|
||||
# brew install gnu-getopt to get the GNU getopt on OS X
|
||||
[[ $(uname -s) == "Darwin" ]] && GNU_GETOPT="/usr/local/opt/gnu-getopt/bin/getopt" || GNU_GETOPT="getopt"
|
||||
readonly GNU_GETOPT
|
||||
|
||||
args=$(${GNU_GETOPT} -o "" -l "revision:,regions:,size:,name:,no-destroy,env:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--env) deploy_env="$2"; shift 2;;
|
||||
--revision) revision="$2"; shift 2;;
|
||||
--name) box_name="$2"; destroy_server="no"; shift 2;;
|
||||
--no-destroy) destroy_server="no"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "Creating digitalocean image"
|
||||
if [[ "${deploy_env}" == "staging" ]]; then
|
||||
assertNotEmpty DIGITAL_OCEAN_TOKEN_STAGING
|
||||
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_STAGING}"
|
||||
elif [[ "${deploy_env}" == "dev" ]]; then
|
||||
assertNotEmpty DIGITAL_OCEAN_TOKEN_DEV
|
||||
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_DEV}"
|
||||
elif [[ "${deploy_env}" == "prod" ]]; then
|
||||
assertNotEmpty DIGITAL_OCEAN_TOKEN_PROD
|
||||
export DIGITAL_OCEAN_TOKEN="${DIGITAL_OCEAN_TOKEN_PROD}"
|
||||
else
|
||||
echo "No such env ${deploy_env}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
vps="/bin/bash ${SCRIPT_DIR}/digitalocean.sh"
|
||||
|
||||
readonly ssh_keys="${HOME}/.ssh/id_rsa_caas_${deploy_env}"
|
||||
readonly scp202="scp -P 202 -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
readonly scp22="scp -o ConnectTimeout=10 -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
|
||||
readonly ssh202="ssh -p 202 -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
readonly ssh22="ssh -o IdentitiesOnly=yes -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${ssh_keys}"
|
||||
|
||||
if [[ ! -f "${ssh_keys}" ]]; then
|
||||
echo "caas ssh key is missing at ${ssh_keys} (pick it up from secrets repo)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function get_pretty_revision() {
|
||||
local git_rev="$1"
|
||||
local sha1=$(git rev-parse --short "${git_rev}" 2>/dev/null)
|
||||
|
||||
echo "${sha1}"
|
||||
}
|
||||
|
||||
now=$(date "+%Y-%m-%d-%H%M%S")
|
||||
pretty_revision=$(get_pretty_revision "${revision}")
|
||||
|
||||
if [[ -z "${box_name}" ]]; then
|
||||
# if you change this, change the regexp is appstore/janitor.js
|
||||
box_name="box-${deploy_env}-${pretty_revision}-${now}" # remove slashes
|
||||
|
||||
# create a new server if no name given
|
||||
if ! caas_ssh_key_id=$($vps get_ssh_key_id "caas"); then
|
||||
echo "Could not query caas ssh key"
|
||||
exit 1
|
||||
fi
|
||||
echo "Detected caas ssh key id: ${caas_ssh_key_id}"
|
||||
|
||||
echo "Creating Server with name [${box_name}]"
|
||||
if ! server_id=$($vps create ${caas_ssh_key_id} ${box_name}); then
|
||||
echo "Failed to create server"
|
||||
exit 1
|
||||
fi
|
||||
echo "Created server with id: ${server_id}"
|
||||
|
||||
# If we run scripts overenthusiastically without the wait, setup script randomly fails
|
||||
echo -n "Waiting 120 seconds for server creation"
|
||||
for i in $(seq 1 24); do
|
||||
echo -n "."
|
||||
sleep 5
|
||||
done
|
||||
echo ""
|
||||
else
|
||||
if ! server_id=$($vps get_id "${box_name}"); then
|
||||
echo "Could not determine id from name"
|
||||
exit 1
|
||||
fi
|
||||
echo "Reusing server with id: ${server_id}"
|
||||
|
||||
$vps power_on "${server_id}"
|
||||
fi
|
||||
|
||||
# Query until we get an IP
|
||||
while true; do
|
||||
echo "Trying to get the server IP"
|
||||
if server_ip=$($vps get_ip "${server_id}"); then
|
||||
echo "Server IP : [${server_ip}]"
|
||||
break
|
||||
fi
|
||||
echo "Timedout, trying again in 10 seconds"
|
||||
sleep 10
|
||||
done
|
||||
|
||||
while true; do
|
||||
echo "Trying to copy init script to server"
|
||||
if $scp22 "${SCRIPT_DIR}/initializeBaseUbuntuImage.sh" root@${server_ip}:.; then
|
||||
break
|
||||
fi
|
||||
echo "Timedout, trying again in 30 seconds"
|
||||
sleep 30
|
||||
done
|
||||
|
||||
echo "Copying infra_version.js"
|
||||
$scp22 "${SCRIPT_DIR}/../src/infra_version.js" root@${server_ip}:.
|
||||
|
||||
echo "Copying box source"
|
||||
cd "${SOURCE_DIR}"
|
||||
git archive --format=tar HEAD | $ssh22 "root@${server_ip}" "cat - > /tmp/box.tar.gz"
|
||||
|
||||
echo "Executing init script"
|
||||
if ! $ssh22 "root@${server_ip}" "/bin/bash /root/initializeBaseUbuntuImage.sh caas"; then
|
||||
echo "Init script failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Shutting down server with id : ${server_id}"
|
||||
$ssh22 "root@${server_ip}" "shutdown -f now" || true # shutdown sometimes terminates ssh connection immediately making this command fail
|
||||
|
||||
# wait 10 secs for actual shutdown
|
||||
echo "Waiting for 10 seconds for server to shutdown"
|
||||
sleep 30
|
||||
|
||||
echo "Powering off server"
|
||||
if ! $vps power_off "${server_id}"; then
|
||||
echo "Could not power off server"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
snapshot_name="box-${deploy_env}-${pretty_revision}-${now}"
|
||||
echo "Snapshotting as ${snapshot_name}"
|
||||
if ! image_id=$($vps snapshot "${server_id}" "${snapshot_name}"); then
|
||||
echo "Could not snapshot and get image id"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${destroy_server}" == "yes" ]]; then
|
||||
echo "Destroying server"
|
||||
if ! $vps destroy "${server_id}"; then
|
||||
echo "Could not destroy server"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Skipping server destroy"
|
||||
fi
|
||||
|
||||
echo "Transferring image ${image_id} to other regions"
|
||||
$vps transfer_image_to_all_regions "${image_id}"
|
||||
|
||||
echo "Done."
|
||||
39
baseimage/initializeBaseUbuntuImage.sh
Normal file → Executable file
39
baseimage/initializeBaseUbuntuImage.sh
Normal file → Executable file
@@ -27,50 +27,62 @@ debconf-set-selections <<< 'mysql-server mysql-server/root_password_again passwo
|
||||
|
||||
# this enables automatic security upgrades (https://help.ubuntu.com/community/AutomaticSecurityUpdates)
|
||||
# resolvconf is needed for unbound to work property after disabling systemd-resolved in 18.04
|
||||
ubuntu_version=$(lsb_release -rs)
|
||||
ubuntu_codename=$(lsb_release -cs)
|
||||
gpg_package=$([[ "${ubuntu_version}" == "16.04" ]] && echo "gnupg" || echo "gpg")
|
||||
apt-get -y install \
|
||||
acl \
|
||||
awscli \
|
||||
build-essential \
|
||||
cifs-utils \
|
||||
cron \
|
||||
curl \
|
||||
debconf-utils \
|
||||
dmsetup \
|
||||
$gpg_package \
|
||||
iptables \
|
||||
libpython2.7 \
|
||||
linux-generic \
|
||||
logrotate \
|
||||
mysql-server-5.7 \
|
||||
nginx-full \
|
||||
openssh-server \
|
||||
pwgen \
|
||||
resolvconf \
|
||||
sudo \
|
||||
swaks \
|
||||
tzdata \
|
||||
unattended-upgrades \
|
||||
unbound \
|
||||
xfsprogs
|
||||
|
||||
# on some providers like scaleway the sudo file is changed and we want to keep the old one
|
||||
apt-get -o Dpkg::Options::="--force-confold" install -y sudo
|
||||
|
||||
# this ensures that unattended upgades are enabled, if it was disabled during ubuntu install time (see #346)
|
||||
# debconf-set-selection of unattended-upgrades/enable_auto_updates + dpkg-reconfigure does not work
|
||||
cp /usr/share/unattended-upgrades/20auto-upgrades /etc/apt/apt.conf.d/20auto-upgrades
|
||||
|
||||
echo "==> Installing node.js"
|
||||
mkdir -p /usr/local/node-8.9.3
|
||||
curl -sL https://nodejs.org/dist/v8.9.3/node-v8.9.3-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.9.3
|
||||
ln -sf /usr/local/node-8.9.3/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-8.9.3/bin/npm /usr/bin/npm
|
||||
mkdir -p /usr/local/node-10.15.1
|
||||
curl -sL https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.15.1
|
||||
ln -sf /usr/local/node-10.15.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.15.1/bin/npm /usr/bin/npm
|
||||
apt-get install -y python # Install python which is required for npm rebuild
|
||||
[[ "$(python --version 2>&1)" == "Python 2.7."* ]] || die "Expecting python version to be 2.7.x"
|
||||
|
||||
# https://docs.docker.com/engine/installation/linux/ubuntulinux/
|
||||
echo "==> Installing Docker"
|
||||
|
||||
# create systemd drop-in file
|
||||
# create systemd drop-in file. if you channge options here, be sure to fixup installer.sh as well
|
||||
mkdir -p /etc/systemd/system/docker.service.d
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=overlay2" > /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
|
||||
curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
# there are 3 packages for docker - containerd, CLI and the daemon
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
# apt install with install deps (as opposed to dpkg -i)
|
||||
apt install -y /tmp/docker.deb
|
||||
rm /tmp/docker.deb
|
||||
apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
|
||||
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
|
||||
if [[ "${storage_driver}" != "overlay2" ]]; then
|
||||
@@ -108,6 +120,7 @@ fi
|
||||
echo "==> Configuring host"
|
||||
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||
timedatectl set-ntp 1
|
||||
# mysql follows the system timezone
|
||||
timedatectl set-timezone UTC
|
||||
|
||||
# Disable bind for good measure (on online.net, kimsufi servers these are pre-installed and conflicts with unbound)
|
||||
@@ -126,3 +139,9 @@ systemctl disable postfix || true
|
||||
systemctl stop systemd-resolved || true
|
||||
systemctl disable systemd-resolved || true
|
||||
|
||||
# ubuntu's default config for unbound does not work if ipv6 is disabled. this config is overwritten in start.sh
|
||||
# we need unbound to work as this is required for installer.sh to do any DNS requests
|
||||
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
|
||||
|
||||
|
||||
31
box.js
31
box.js
@@ -2,33 +2,26 @@
|
||||
|
||||
'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 });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs(args) {
|
||||
args[0] = this.namespace + ' ' + args[0];
|
||||
};
|
||||
|
||||
let async = require('async'),
|
||||
config = require('./src/config.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
constants = require('./src/constants.js'),
|
||||
dockerProxy = require('./src/dockerproxy.js'),
|
||||
ldap = require('./src/ldap.js'),
|
||||
server = require('./src/server.js');
|
||||
|
||||
console.log();
|
||||
console.log('==========================================');
|
||||
console.log(' Cloudron will use the following settings ');
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
console.log(' Environment: ', config.CLOUDRON ? 'CLOUDRON' : 'TEST');
|
||||
console.log(' Version: ', config.version());
|
||||
console.log(' Admin Origin: ', config.adminOrigin());
|
||||
console.log(' Appstore API server origin: ', config.apiServerOrigin());
|
||||
console.log(' Appstore Web server origin: ', config.webServerOrigin());
|
||||
console.log(' SysAdmin Port: ', config.get('sysadminPort'));
|
||||
console.log(' LDAP Server Port: ', config.get('ldapPort'));
|
||||
console.log(' Docker Proxy Port: ', config.get('dockerProxyPort'));
|
||||
console.log();
|
||||
console.log(` Cloudron ${constants.VERSION} `);
|
||||
console.log('==========================================');
|
||||
console.log();
|
||||
|
||||
|
||||
@@ -4,19 +4,24 @@
|
||||
|
||||
var database = require('./src/database.js');
|
||||
|
||||
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
|
||||
var crashNotifier = require('./src/crashnotifier.js');
|
||||
|
||||
// This is triggered by systemd with the crashed unit name as argument
|
||||
function main() {
|
||||
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
|
||||
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <unitName>');
|
||||
|
||||
var processName = process.argv[2];
|
||||
console.log('Started crash notifier for', processName);
|
||||
var unitName = process.argv[2];
|
||||
console.log('Started crash notifier for', unitName);
|
||||
|
||||
// mailer needs the db
|
||||
// eventlog api needs the db
|
||||
database.initialize(function (error) {
|
||||
if (error) return console.error('Cannot connect to database. Unable to send crash log.', error);
|
||||
|
||||
sendFailureLogs(processName, { unit: processName });
|
||||
crashNotifier.sendFailureLogs(unitName, function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
process.exit();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = "CREATE TABLE groups(" +
|
||||
var cmd = "CREATE TABLE userGroups(" +
|
||||
"id VARCHAR(128) NOT NULL UNIQUE," +
|
||||
"name VARCHAR(128) NOT NULL UNIQUE," +
|
||||
"PRIMARY KEY(id))";
|
||||
@@ -13,7 +13,7 @@ exports.up = function(db, callback) {
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE groups', function (error) {
|
||||
db.runSql('DROP TABLE userGroups', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ exports.up = function(db, callback) {
|
||||
var cmd = "CREATE TABLE IF NOT EXISTS groupMembers(" +
|
||||
"groupId VARCHAR(128) NOT NULL," +
|
||||
"userId VARCHAR(128) NOT NULL," +
|
||||
"FOREIGN KEY(groupId) REFERENCES groups(id)," +
|
||||
"FOREIGN KEY(groupId) REFERENCES userGroups(id)," +
|
||||
"FOREIGN KEY(userId) REFERENCES users(id));";
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
|
||||
@@ -7,7 +7,7 @@ var ADMIN_GROUP_ID = 'admin'; // see constants.js
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'INSERT INTO groups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
|
||||
db.runSql.bind(db, 'INSERT INTO userGroups (id, name) VALUES (?, ?)', [ ADMIN_GROUP_ID, 'admin' ]),
|
||||
function migrateAdminFlag(done) {
|
||||
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
@@ -10,7 +10,7 @@ exports.up = function(db, callback) {
|
||||
function addGroupMailboxes(done) {
|
||||
console.log('Importing group mailboxes');
|
||||
|
||||
db.all('SELECT id, name FROM groups', function (error, results) {
|
||||
db.all('SELECT id, name FROM userGroups', function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
async.eachSeries(results, function (g, next) {
|
||||
|
||||
@@ -16,7 +16,7 @@ exports.up = function(db, callback) {
|
||||
db.runSql.bind(db, 'ALTER TABLE clients CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE eventlog CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE groupMembers CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE groups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE userGroups CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE migrations CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
db.runSql.bind(db, 'ALTER TABLE settings CONVERT TO CHARACTER SET utf8 COLLATE utf8_bin'),
|
||||
|
||||
@@ -29,7 +29,7 @@ exports.up = function(db, callback) {
|
||||
|
||||
// this will be finally created once we have a domain when we create the owner in user.js
|
||||
const ADMIN_GROUP_ID = 'admin'; // see constants.js
|
||||
db.runSql('DELETE FROM groups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
|
||||
db.runSql('DELETE FROM userGroups WHERE id = ?', [ ADMIN_GROUP_ID ], function (error) {
|
||||
if (error) return done(error);
|
||||
|
||||
db.runSql('DELETE FROM mailboxes WHERE ownerId = ?', [ ADMIN_GROUP_ID ], done);
|
||||
|
||||
@@ -19,8 +19,8 @@ exports.up = function(db, callback) {
|
||||
},
|
||||
function getGroups(done) {
|
||||
db.all('SELECT id, name, GROUP_CONCAT(groupMembers.userId) AS userIds ' +
|
||||
' FROM groups LEFT OUTER JOIN groupMembers ON groups.id = groupMembers.groupId ' +
|
||||
' GROUP BY groups.id', [ ], function (error, results) {
|
||||
' FROM userGroups LEFT OUTER JOIN groupMembers ON userGroups.id = groupMembers.groupId ' +
|
||||
' GROUP BY userGroups.id', [ ], function (error, results) {
|
||||
if (error) return done(error);
|
||||
|
||||
results.forEach(function (result) {
|
||||
|
||||
@@ -18,7 +18,7 @@ exports.up = function(db, callback) {
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'DELETE FROM groupMembers WHERE groupId=?', [ 'admin' ]),
|
||||
db.runSql.bind(db, 'DELETE FROM groups WHERE id=?', [ 'admin' ])
|
||||
db.runSql.bind(db, 'DELETE FROM userGroups WHERE id=?', [ 'admin' ])
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
|
||||
27
migrations/20181116191032-tasks-add-table.js
Normal file
27
migrations/20181116191032-tasks-add-table.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = 'CREATE TABLE tasks(' +
|
||||
'id int NOT NULL AUTO_INCREMENT,' +
|
||||
'type VARCHAR(32) NOT NULL,' +
|
||||
'argsJson TEXT,' +
|
||||
'percent INTEGER DEFAULT 0,' +
|
||||
'message TEXT,' +
|
||||
'errorMessage TEXT,' +
|
||||
'result TEXT,' +
|
||||
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
|
||||
'ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,' +
|
||||
'PRIMARY KEY (id))';
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('DROP TABLE tasks', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
17
migrations/20181203111504-rename-groups-to-userGroups.js
Normal file
17
migrations/20181203111504-rename-groups-to-userGroups.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('SELECT 1 FROM groups LIMIT 1', function (error) {
|
||||
if (error) return callback(); // groups table does not exist
|
||||
|
||||
db.runSql('RENAME TABLE groups TO userGroups', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
// this is a one way renaming since the previous migration steps have been already updated to match the new name
|
||||
callback();
|
||||
};
|
||||
17
migrations/20181203122115-ensure-default-timestamps.js
Normal file
17
migrations/20181203122115-ensure-default-timestamps.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps MODIFY updateTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
|
||||
db.runSql.bind(db, 'ALTER TABLE eventlog MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
|
||||
db.runSql.bind(db, 'ALTER TABLE backups MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes MODIFY creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
28
migrations/20181207042802-apps-add-mailboxName.js
Normal file
28
migrations/20181207042802-apps-add-mailboxName.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 apps ADD COLUMN mailboxName VARCHAR(128)'),
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
|
||||
function migrateMailboxNames(done) {
|
||||
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
|
||||
if (error) return done(error);
|
||||
|
||||
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
|
||||
if (mailbox.ownerType !== 'app') return iteratorDone();
|
||||
|
||||
db.runSql('UPDATE apps SET mailboxName = ? WHERE id = ?', [ mailbox.name, mailbox.ownerId ], iteratorDone);
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
28
migrations/20181207042803-mailboxes-remove-ownerType.js
Normal file
28
migrations/20181207042803-mailboxes-remove-ownerType.js
Normal file
@@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
|
||||
function migrateMailboxNames(done) {
|
||||
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
|
||||
if (error) return done(error);
|
||||
|
||||
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
|
||||
if (mailbox.ownerType !== 'app') return iteratorDone();
|
||||
|
||||
db.runSql('DELETE FROM mailboxes WHERE name = ?', [ mailbox.name ], iteratorDone);
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
|
||||
db.runSql.bind(db, 'COMMIT'),
|
||||
db.runSql.bind(db, 'ALTER TABLE mailboxes DROP COLUMN ownerType')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
16
migrations/20181207165827-apps-add-enableAutomaticUpdate.js
Normal file
16
migrations/20181207165827-apps-add-enableAutomaticUpdate.js
Normal file
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN enableAutomaticUpdate BOOLEAN DEFAULT 1', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN enableAutomaticUpdate', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
27
migrations/20181217140321-notifications-add-table.js
Normal file
27
migrations/20181217140321-notifications-add-table.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = 'CREATE TABLE notifications(' +
|
||||
'id int NOT NULL AUTO_INCREMENT,' +
|
||||
'userId VARCHAR(128) NOT NULL,' +
|
||||
'eventId VARCHAR(128) NOT NULL,' +
|
||||
'title VARCHAR(512) NOT NULL,' +
|
||||
'message TEXT,' +
|
||||
'action VARCHAR(512) NOT NULL,' +
|
||||
'acknowledged BOOLEAN DEFAULT false,' +
|
||||
'creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,' +
|
||||
'FOREIGN KEY(eventId) REFERENCES eventlog(id),' +
|
||||
'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 notifications', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tasks CHANGE result resultJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
db.runSql('DELETE FROM tasks', callback); // empty tasks table since we have bad results format
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tasks CHANGE resultJson result TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
15
migrations/20190114230513-apps-add-dataDir.js
Normal file
15
migrations/20190114230513-apps-add-dataDir.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN dataDir VARCHAR(256) UNIQUE', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN dataDir', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
17
migrations/20190125221223-domains-add-locked.js
Normal file
17
migrations/20190125221223-domains-add-locked.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE domains ADD COLUMN locked BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE domains DROP COLUMN locked', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
// WARNING in the future always give constraints proper names to not rely on automatic ones
|
||||
db.runSql.bind(db, 'ALTER TABLE notifications DROP FOREIGN KEY notifications_ibfk_1'),
|
||||
db.runSql.bind(db, 'ALTER TABLE notifications MODIFY eventId VARCHAR(128)'),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'ALTER TABLE notifications MODIFY eventId VARCHAR(128) NOT NULL'),
|
||||
db.runSql.bind(db, 'ALTER TABLE notifications ADD FOREIGN KEY(eventId) REFERENCES eventlog(id)'),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
'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 !== 'namecheap') return iteratorCallback();
|
||||
|
||||
let config = JSON.parse(domain.configJson);
|
||||
config.token = config.apiKey;
|
||||
delete config.apiKey;
|
||||
|
||||
db.runSql('UPDATE domains SET configJson = ? WHERE domain = ?', [ JSON.stringify(config), domain.domain ], iteratorCallback);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
17
migrations/20190213000125-apps-add-healthTime.js
Normal file
17
migrations/20190213000125-apps-add-healthTime.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var cmd = 'ALTER TABLE apps ADD COLUMN healthTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP';
|
||||
|
||||
db.runSql(cmd, function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN healthTime', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
18
migrations/20190214222607-tokens-ensure-name.js
Normal file
18
migrations/20190214222607-tokens-ensure-name.js
Normal file
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
|
||||
let async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('SELECT * FROM tokens WHERE clientId=?', ['cid-sdk'], function (error, tokens) {
|
||||
if (error) console.error(error);
|
||||
|
||||
async.eachSeries(tokens, function (token, iteratorDone) {
|
||||
if (token.name) return iteratorDone();
|
||||
db.runSql('UPDATE tokens SET name=? WHERE accessToken=?', [ 'Unnamed-' + token.accessToken.slice(0,8), token.accessToken ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
29
migrations/20190215215805-tokens-add-id.js
Normal file
29
migrations/20190215215805-tokens-add-id.js
Normal file
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var uuid = require('uuid');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
|
||||
db.runSql.bind(db, 'ALTER TABLE tokens ADD COLUMN id VARCHAR(128)'),
|
||||
|
||||
function (done) {
|
||||
db.runSql('SELECT * FROM tokens', function (error, tokens) {
|
||||
async.eachSeries(tokens, function (token, iteratorDone) {
|
||||
db.runSql('UPDATE tokens SET id=? WHERE accessToken=?', [ 'tid-'+uuid.v4(), token.accessToken ], iteratorDone);
|
||||
}, done);
|
||||
});
|
||||
},
|
||||
|
||||
db.runSql.bind(db, 'ALTER TABLE tokens MODIFY id VARCHAR(128) NOT NULL UNIQUE'),
|
||||
db.runSql.bind(db, 'COMMIT'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE tokens DROP COLUMN id'),
|
||||
], callback);
|
||||
};
|
||||
17
migrations/20190221215805-settings-add-locked.js
Normal file
17
migrations/20190221215805-settings-add-locked.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE settings ADD COLUMN locked BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE settings DROP COLUMN locked', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
14
migrations/20190228225735-notifications-drop-action.js
Normal file
14
migrations/20190228225735-notifications-drop-action.js
Normal file
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE notifications DROP COLUMN action', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE notifications ADD COLUMN action VARCHAR(512) NOT NULL', callback);
|
||||
};
|
||||
27
migrations/20190303024631-apps-default-mailboxName.js
Normal file
27
migrations/20190303024631-apps-default-mailboxName.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
crypto = require('crypto'),
|
||||
fs = require('fs'),
|
||||
os = require('os'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
tldjs = require('tldjs');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM apps, subdomains WHERE apps.id=subdomains.appId AND type="primary"', function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
if (app.mailboxName) return iteratorDone();
|
||||
|
||||
const mailboxName = (app.subdomain ? app.subdomain : JSON.parse(app.manifestJson).title.toLowerCase().replace(/[^a-zA-Z0-9]/g, '')) + '.app';
|
||||
|
||||
db.runSql('UPDATE apps SET mailboxName=? WHERE id=?', [ mailboxName, app.id ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
15
migrations/20190322144543-apps-add-label.js
Normal file
15
migrations/20190322144543-apps-add-label.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN label VARCHAR(128)', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN label', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
15
migrations/20190322144544-apps-add-tagsJson.js
Normal file
15
migrations/20190322144544-apps-add-tagsJson.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN tagsJson VARCHAR(2048)', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN tagsJson ', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
15
migrations/20190413140945-backups-add-preserveSecs.js
Normal file
15
migrations/20190413140945-backups-add-preserveSecs.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups ADD COLUMN preserveSecs INTEGER DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE backups DROP COLUMN preserveSecs', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
19
migrations/20190426172858-migrate-caas-backupConfig.js
Normal file
19
migrations/20190426172858-migrate-caas-backupConfig.js
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) {
|
||||
if (error || results.length === 0) return callback(error);
|
||||
|
||||
var backupConfig = JSON.parse(results[0].value);
|
||||
if (backupConfig.provider !== 'caas') return callback();
|
||||
|
||||
backupConfig.boxId = backupConfig.prefix; // hack to set the boxId that happens to match the prefix
|
||||
delete backupConfig.fqdn;
|
||||
|
||||
db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [ JSON.stringify(backupConfig) ], callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
51
migrations/20190503170326-settings-get-license.js
Normal file
51
migrations/20190503170326-settings-get-license.js
Normal file
@@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
fs = require('fs'),
|
||||
superagent = require('superagent');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
if (!fs.existsSync('/etc/cloudron/cloudron.conf')) {
|
||||
console.log('Unable to locate cloudron.conf');
|
||||
return callback();
|
||||
}
|
||||
|
||||
const config = JSON.parse(fs.readFileSync('/etc/cloudron/cloudron.conf', 'utf8'));
|
||||
|
||||
db.all('SELECT * FROM settings WHERE name="appstore_config"', function (error, results) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (results.length === 0) {
|
||||
console.log('No appstore config, skipping license migration');
|
||||
return callback();
|
||||
}
|
||||
|
||||
console.log('Downloading license');
|
||||
|
||||
const appstoreConfig = JSON.parse(results[0].value);
|
||||
|
||||
superagent.get(`${config.apiServerOrigin}/api/v1/cloudron_license`)
|
||||
.query({ accessToken: appstoreConfig.token, cloudronId: appstoreConfig.cloudronId, provider: config.provider })
|
||||
.timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new Error('Network error getting license:' + error.message));
|
||||
if (result.statusCode !== 200) return callback(new Error(`Bad status getting license: ${result.status} ${result.text}`));
|
||||
|
||||
if (!result.body.cloudronId || !result.body.licenseKey || !result.body.cloudronToken) return callback(new Error(`Bad response getting license: ${result.text}`));
|
||||
|
||||
console.log('Adding license', result.body);
|
||||
|
||||
async.series([
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'cloudron_id', result.body.cloudronId ]),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'license_key', result.body.licenseKey ]),
|
||||
db.runSql.bind(db, 'INSERT settings (name, value) VALUES(?, ?)', [ 'cloudron_token', result.body.cloudronToken ]),
|
||||
db.runSql.bind(db, 'DELETE FROM settings WHERE name=?', [ 'appstore_config' ]),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
13
migrations/20190505204730-expire-existing-tokens.js
Normal file
13
migrations/20190505204730-expire-existing-tokens.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('UPDATE tokens SET expires=? WHERE clientId=?', [ 1557089270832, 'cid-webadmin' ], function (error) { // force webadmin to get a new token
|
||||
if (error) console.error(error);
|
||||
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
15
migrations/20190507030217-settings-drop-locked.js
Normal file
15
migrations/20190507030217-settings-drop-locked.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE settings DROP COLUMN locked', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE settings ADD COLUMN locked BOOLEAN DEFAULT 0', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
15
migrations/20190610195323-mail-add-dkimSelector.js
Normal file
15
migrations/20190610195323-mail-add-dkimSelector.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mail ADD COLUMN dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron"', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE mail DROP COLUMN dkimSelector', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
14
migrations/20190703031708-apps-remove-ownerId.js
Normal file
14
migrations/20190703031708-apps-remove-ownerId.js
Normal file
@@ -0,0 +1,14 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP FOREIGN KEY apps_owner_constraint'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN ownerId')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
29
migrations/20190725172940-settings-migrate-cloudron-conf.js
Normal file
29
migrations/20190725172940-settings-migrate-cloudron-conf.js
Normal file
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
fs = require('fs');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
if (!fs.existsSync('/etc/cloudron/cloudron.conf')) {
|
||||
console.log('Unable to locate cloudron.conf');
|
||||
return callback();
|
||||
}
|
||||
|
||||
const config = JSON.parse(fs.readFileSync('/etc/cloudron/cloudron.conf', 'utf8'));
|
||||
|
||||
async.series([
|
||||
fs.writeFile.bind(null, '/etc/cloudron/PROVIDER', config.provider, 'utf8'),
|
||||
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||
// we use replace instead of insert because the cloudron-setup adds api/web_server_origin even for legacy setups
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'api_server_origin', config.apiServerOrigin ]),
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'web_server_origin', config.webServerOrigin ]),
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'admin_domain', config.adminDomain ]),
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'admin_fqdn', config.adminFqdn ]),
|
||||
db.runSql.bind(db, 'REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'demo', config.isDemo ]),
|
||||
db.runSql.bind(db, 'COMMIT')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
17
migrations/20190808123531-users-add-active.js
Normal file
17
migrations/20190808123531-users-add-active.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN active BOOLEAN DEFAULT 1', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN active', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
17
migrations/20190826210725-apps-add-taskId.js
Normal file
17
migrations/20190826210725-apps-add-taskId.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps ADD COLUMN taskId INTEGER'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps ADD CONSTRAINT apps_task_constraint FOREIGN KEY(taskId) REFERENCES tasks(id)')
|
||||
], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE app DROP FOREIGN KEY apps_task_constraint'),
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN taskId'),
|
||||
], callback);
|
||||
};
|
||||
12
migrations/20190827221616-apps-drop-task-args.js
Normal file
12
migrations/20190827221616-apps-drop-task-args.js
Normal file
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps DROP updateConfigJson, DROP restoreConfigJson, DROP oldConfigJson', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE installationProgress errorJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps CHANGE errorJson installationProgress TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
17
migrations/20190829153300-users-add-source.js
Normal file
17
migrations/20190829153300-users-add-source.js
Normal file
@@ -0,0 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users ADD COLUMN source VARCHAR(128) DEFAULT ""', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE users DROP COLUMN source', function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
let async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tasks CHANGE errorMessage errorJson TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
|
||||
// convert error messages into json
|
||||
db.all('SELECT id, errorJson FROM apps', function (error, apps) {
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
if (app.errorJson === 'null') return iteratorDone();
|
||||
if (app.errorJson === null) return iteratorDone();
|
||||
|
||||
db.runSql('UPDATE apps SET errorJson = ? WHERE id = ?', [ JSON.stringify({ message: app.errorJson }), app.id ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE tasks CHANGE errorJson errorMessage TEXT', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
// imports mailbox entries for existing users
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM mailboxes', function (error, mailboxes) {
|
||||
async.eachSeries(mailboxes, function (mailbox, iteratorDone) {
|
||||
if (!mailbox.membersJson) return iteratorDone();
|
||||
|
||||
let members = JSON.parse(mailbox.membersJson);
|
||||
members = members.map((m) => m.indexOf('@') === -1 ? `${m}@${mailbox.domain}` : m); // only because we don't do things in a xction
|
||||
|
||||
db.runSql('UPDATE mailboxes SET membersJson=? WHERE name=? AND domain=?', [ JSON.stringify(members), mailbox.name, mailbox.domain ], iteratorDone);
|
||||
}, callback);
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
19
migrations/20190923050431-apps-make-runState-not-null.js
Normal file
19
migrations/20190923050431-apps-make-runState-not-null.js
Normal file
@@ -0,0 +1,19 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('UPDATE apps SET runState=? WHERE runState IS NULL', [ 'running' ], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.runSql('ALTER TABLE apps MODIFY runState VARCHAR(512) NOT NULL', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
db.runSql('ALTER TABLE app MODIFY runState VARCHAR(512)', [], function (error) {
|
||||
if (error) console.error(error);
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
10
migrations/20191002103646-fix-demo-settings-boolean.js
Normal file
10
migrations/20191002103646-fix-demo-settings-boolean.js
Normal file
@@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
// We clear all demo state in the Cloudron...the demo cloudron needs manual intervention afterwards
|
||||
db.runSql('REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'demo', '' ], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
30
migrations/20191014002401-apps-add-reverseProxyConfigJson.js
Normal file
30
migrations/20191014002401-apps-add-reverseProxyConfigJson.js
Normal file
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE apps ADD COLUMN reverseProxyConfigJson TEXT', function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.all('SELECT id, robotsTxt FROM apps', function (error, apps) {
|
||||
if (error) return callback(error);
|
||||
|
||||
async.eachSeries(apps, function (app, iteratorDone) {
|
||||
if (!app.robotsTxt) return iteratorDone();
|
||||
|
||||
db.runSql('UPDATE apps SET reverseProxyConfigJson=? WHERE id=?', [ JSON.stringify({ robotsTxt: JSON.stringify(app.robotsTxt) }), app.id ], iteratorDone);
|
||||
}, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
db.runSql('ALTER TABLE apps DROP COLUMN robotsTxt', callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
async.series([
|
||||
db.runSql.bind(db, 'ALTER TABLE apps DROP COLUMN reverseProxyConfigJson'),
|
||||
], callback);
|
||||
};
|
||||
|
||||
13
migrations/20191029223928-settings-default-sysinfo.js
Normal file
13
migrations/20191029223928-settings-default-sysinfo.js
Normal file
@@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
var fs = require('fs');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
let sysinfoConfig = { provider: 'generic' };
|
||||
|
||||
db.runSql('REPLACE INTO settings (name, value) VALUES(?, ?)', [ 'sysinfo_config', JSON.stringify(sysinfoConfig) ], callback);
|
||||
};
|
||||
|
||||
exports.down = function(db, callback) {
|
||||
callback();
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
#### TEXT - stored offline from table row (use for strings)
|
||||
#### BLOB - stored offline from table row (use for binary data)
|
||||
#### https://dev.mysql.com/doc/refman/5.0/en/storage-requirements.html
|
||||
#### Times are stored in the database in UTC. And precision is seconds
|
||||
|
||||
# The code uses zero dates. Make sure sql_mode does NOT have NO_ZERO_DATE
|
||||
# http://johnemb.blogspot.com/2014/09/adding-or-removing-individual-sql-modes.html
|
||||
@@ -26,10 +27,11 @@ CREATE TABLE IF NOT EXISTS users(
|
||||
twoFactorAuthenticationSecret VARCHAR(128) DEFAULT "",
|
||||
twoFactorAuthenticationEnabled BOOLEAN DEFAULT false,
|
||||
admin BOOLEAN DEFAULT false,
|
||||
source VARCHAR(128) DEFAULT "",
|
||||
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS groups(
|
||||
CREATE TABLE IF NOT EXISTS userGroups(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
name VARCHAR(254) NOT NULL UNIQUE,
|
||||
PRIMARY KEY(id));
|
||||
@@ -37,13 +39,14 @@ CREATE TABLE IF NOT EXISTS groups(
|
||||
CREATE TABLE IF NOT EXISTS groupMembers(
|
||||
groupId VARCHAR(128) NOT NULL,
|
||||
userId VARCHAR(128) NOT NULL,
|
||||
FOREIGN KEY(groupId) REFERENCES groups(id),
|
||||
FOREIGN KEY(groupId) REFERENCES userGroups(id),
|
||||
FOREIGN KEY(userId) REFERENCES users(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
name VARCHAR(64) DEFAULT "", // description
|
||||
accessToken VARCHAR(128) NOT NULL UNIQUE,
|
||||
identifier VARCHAR(128) NOT NULL,
|
||||
identifier VARCHAR(128) NOT NULL, // resourceId: app id or user id
|
||||
clientId VARCHAR(128),
|
||||
scope VARCHAR(512) NOT NULL,
|
||||
expires BIGINT NOT NULL, // FIXME: make this a timestamp
|
||||
@@ -61,34 +64,34 @@ CREATE TABLE IF NOT EXISTS clients(
|
||||
CREATE TABLE IF NOT EXISTS apps(
|
||||
id VARCHAR(128) NOT NULL UNIQUE,
|
||||
appStoreId VARCHAR(128) NOT NULL,
|
||||
installationState VARCHAR(512) NOT NULL,
|
||||
installationProgress TEXT,
|
||||
runState VARCHAR(512),
|
||||
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(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the app was installed
|
||||
updateTime TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP, // when the last app update was done
|
||||
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,
|
||||
xFrameOptions VARCHAR(512),
|
||||
sso BOOLEAN DEFAULT 1, // whether user chose to enable SSO
|
||||
debugModeJson TEXT, // options for development mode
|
||||
robotsTxt TEXT,
|
||||
reverseProxyConfigJson TEXT, // { robotsTxt, csp }
|
||||
enableBackup BOOLEAN DEFAULT 1, // misnomer: controls automatic daily backups
|
||||
enableAutomaticUpdate BOOLEAN DEFAULT 1,
|
||||
mailboxName VARCHAR(128), // mailbox of this app. default allocated as '.app'
|
||||
label VARCHAR(128), // display name
|
||||
tagsJson VARCHAR(2048), // array of tags
|
||||
dataDir VARCHAR(256) UNIQUE,
|
||||
taskId INTEGER, // current task
|
||||
errorJson TEXT,
|
||||
|
||||
// the following fields do not belong here, they can be removed when we use a queue for apptask
|
||||
restoreConfigJson VARCHAR(256), // used to pass backupId to restore from to apptask
|
||||
oldConfigJson TEXT, // used to pass old config to apptask (configure, restore)
|
||||
updateConfigJson TEXT, // used to pass new config to apptask (update)
|
||||
|
||||
ownerId VARCHAR(128),
|
||||
|
||||
FOREIGN KEY(ownerId) REFERENCES users(id),
|
||||
FOREIGN KEY(taskId) REFERENCES tasks(id),
|
||||
PRIMARY KEY(id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS appPortBindings(
|
||||
@@ -126,13 +129,14 @@ CREATE TABLE IF NOT EXISTS appEnvVars(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS backups(
|
||||
id VARCHAR(128) NOT NULL,
|
||||
creationTime TIMESTAMP,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
version VARCHAR(128) NOT NULL, /* app version or box version */
|
||||
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
|
||||
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 */
|
||||
format VARCHAR(16) DEFAULT "tgz",
|
||||
preserveSecs INTEGER DEFAULT 0,
|
||||
|
||||
PRIMARY KEY (id));
|
||||
|
||||
@@ -141,7 +145,7 @@ CREATE TABLE IF NOT EXISTS eventlog(
|
||||
action VARCHAR(128) NOT NULL,
|
||||
source TEXT, /* { userId, username, ip }. userId can be null for cron,sysadmin */
|
||||
data TEXT, /* free flowing json based on action */
|
||||
createdAt TIMESTAMP(2) NOT NULL,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (id));
|
||||
|
||||
@@ -151,6 +155,7 @@ CREATE TABLE IF NOT EXISTS domains(
|
||||
provider VARCHAR(16) NOT NULL,
|
||||
configJson TEXT, /* JSON containing the dns backend provider config */
|
||||
tlsConfigJson TEXT, /* JSON containing the tls provider config */
|
||||
locked BOOLEAN,
|
||||
|
||||
PRIMARY KEY (domain))
|
||||
|
||||
@@ -165,6 +170,8 @@ CREATE TABLE IF NOT EXISTS mail(
|
||||
catchAllJson TEXT,
|
||||
relayJson TEXT,
|
||||
|
||||
dkimSelector VARCHAR(128) NOT NULL DEFAULT "cloudron",
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
PRIMARY KEY(domain))
|
||||
|
||||
@@ -173,15 +180,17 @@ CREATE TABLE IF NOT EXISTS mail(
|
||||
/* Future fields:
|
||||
* accessRestriction - to determine who can access it. So this has foreign keys
|
||||
* quota - per mailbox quota
|
||||
|
||||
NOTE: this table exists only real mailboxes. And has unique constraint to handle
|
||||
conflict with aliases and mailbox names
|
||||
*/
|
||||
CREATE TABLE IF NOT EXISTS mailboxes(
|
||||
name VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(16) NOT NULL, /* 'mailbox', 'alias', 'list' */
|
||||
ownerId VARCHAR(128) NOT NULL, /* app id or user id or group id */
|
||||
ownerType VARCHAR(16) NOT NULL, /* 'app' or 'user' or 'group' */
|
||||
ownerId VARCHAR(128) NOT NULL, /* user id */
|
||||
aliasTarget VARCHAR(128), /* the target name type is an alias */
|
||||
membersJson TEXT, /* members of a group */
|
||||
creationTime TIMESTAMP,
|
||||
membersJson TEXT, /* members of a group. fully qualified */
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
domain VARCHAR(128),
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES mail(domain),
|
||||
@@ -191,10 +200,33 @@ CREATE TABLE IF NOT EXISTS subdomains(
|
||||
appId VARCHAR(128) NOT NULL,
|
||||
domain VARCHAR(128) NOT NULL,
|
||||
subdomain VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(128) NOT NULL,
|
||||
type VARCHAR(128) NOT NULL, /* primary or redirect */
|
||||
|
||||
FOREIGN KEY(domain) REFERENCES domains(domain),
|
||||
FOREIGN KEY(appId) REFERENCES apps(id),
|
||||
UNIQUE (subdomain, domain))
|
||||
UNIQUE (subdomain, domain));
|
||||
|
||||
CHARACTER SET utf8 COLLATE utf8_bin;
|
||||
CREATE TABLE IF NOT EXISTS tasks(
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
type VARCHAR(32) NOT NULL,
|
||||
percent INTEGER DEFAULT 0,
|
||||
message TEXT,
|
||||
errorJson TEXT,
|
||||
resultJson TEXT,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id));
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications(
|
||||
id int NOT NULL AUTO_INCREMENT,
|
||||
userId VARCHAR(128) NOT NULL,
|
||||
eventId VARCHAR(128), // reference to eventlog. can be null
|
||||
title VARCHAR(512) NOT NULL,
|
||||
message TEXT,
|
||||
acknowledged BOOLEAN DEFAULT false,
|
||||
creationTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CHARACTER SET utf8 COLLATE utf8_bin;
|
||||
|
||||
9793
package-lock.json
generated
9793
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
98
package.json
98
package.json
@@ -14,85 +14,87 @@
|
||||
"node": ">=4.0.0 <=4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google-cloud/dns": "^0.7.2",
|
||||
"@google-cloud/storage": "^1.7.0",
|
||||
"@sindresorhus/df": "^2.1.0",
|
||||
"async": "^2.6.1",
|
||||
"aws-sdk": "^2.253.1",
|
||||
"body-parser": "^1.18.3",
|
||||
"cloudron-manifestformat": "^2.14.2",
|
||||
"connect": "^3.6.6",
|
||||
"@google-cloud/dns": "^1.1.0",
|
||||
"@google-cloud/storage": "^2.5.0",
|
||||
"@sindresorhus/df": "git+https://github.com/cloudron-io/df.git#type",
|
||||
"async": "^2.6.2",
|
||||
"aws-sdk": "^2.476.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"cloudron-manifestformat": "^2.15.0",
|
||||
"connect": "^3.7.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "^1.0.2",
|
||||
"connect-lastmile": "^1.2.1",
|
||||
"connect-timeout": "^1.9.0",
|
||||
"cookie-parser": "^1.3.5",
|
||||
"cookie-session": "^1.3.2",
|
||||
"cron": "^1.3.0",
|
||||
"csurf": "^1.6.6",
|
||||
"db-migrate": "^0.11.1",
|
||||
"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": "^3.1.0",
|
||||
"dockerode": "^2.5.5",
|
||||
"debug": "^4.1.1",
|
||||
"dockerode": "^2.5.8",
|
||||
"ejs": "^2.6.1",
|
||||
"ejs-cli": "^2.0.1",
|
||||
"express": "^4.16.3",
|
||||
"express-session": "^1.15.6",
|
||||
"json": "^9.0.3",
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.16.2",
|
||||
"js-yaml": "^3.13.1",
|
||||
"json": "^9.0.6",
|
||||
"ldapjs": "^1.0.2",
|
||||
"lodash": "^4.17.11",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
"mime": "^2.3.1",
|
||||
"moment-timezone": "^0.5.17",
|
||||
"morgan": "^1.9.0",
|
||||
"multiparty": "^4.1.4",
|
||||
"mysql": "^2.15.0",
|
||||
"nodemailer": "^4.6.5",
|
||||
"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",
|
||||
"nodemailer-smtp-transport": "^2.7.4",
|
||||
"oauth2orize": "^1.11.0",
|
||||
"once": "^1.3.2",
|
||||
"once": "^1.4.0",
|
||||
"parse-links": "^0.1.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-http": "^0.3.0",
|
||||
"passport-http-bearer": "^1.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-oauth2-client-password": "^0.1.2",
|
||||
"pretty-bytes": "^5.3.0",
|
||||
"progress-stream": "^2.0.0",
|
||||
"proxy-middleware": "^0.15.0",
|
||||
"qrcode": "^1.2.0",
|
||||
"readdirp": "^2.1.0",
|
||||
"request": "^2.87.0",
|
||||
"rimraf": "^2.6.2",
|
||||
"qrcode": "^1.3.3",
|
||||
"readdirp": "^3.0.2",
|
||||
"request": "^2.88.0",
|
||||
"rimraf": "^2.6.3",
|
||||
"s3-block-read-stream": "^0.5.0",
|
||||
"safetydance": "^0.7.1",
|
||||
"semver": "^5.5.0",
|
||||
"showdown": "^1.8.6",
|
||||
"semver": "^6.1.1",
|
||||
"session-file-store": "^1.3.1",
|
||||
"showdown": "^1.9.0",
|
||||
"speakeasy": "^2.0.0",
|
||||
"split": "^1.0.0",
|
||||
"superagent": "^3.8.3",
|
||||
"split": "^1.0.1",
|
||||
"superagent": "^5.0.9",
|
||||
"supererror": "^0.7.2",
|
||||
"tar-fs": "^1.16.2",
|
||||
"tar-stream": "^1.6.1",
|
||||
"tar-fs": "github:cloudron-io/tar-fs#ignore_stat_error",
|
||||
"tar-stream": "^2.1.0",
|
||||
"tldjs": "^2.3.1",
|
||||
"underscore": "^1.9.1",
|
||||
"uuid": "^3.2.1",
|
||||
"uuid": "^3.3.2",
|
||||
"valid-url": "^1.0.9",
|
||||
"validator": "^10.3.0",
|
||||
"ws": "^5.2.0"
|
||||
"validator": "^11.0.0",
|
||||
"ws": "^7.0.0",
|
||||
"xml2js": "^0.4.19"
|
||||
},
|
||||
"devDependencies": {
|
||||
"expect.js": "*",
|
||||
"hock": "^1.3.2",
|
||||
"istanbul": "*",
|
||||
"js2xmlparser": "^3.0.0",
|
||||
"mocha": "^5.2.0",
|
||||
"hock": "^1.3.3",
|
||||
"js2xmlparser": "^4.0.0",
|
||||
"mocha": "^6.1.4",
|
||||
"mock-aws-s3": "git+https://github.com/cloudron-io/mock-aws-s3.git",
|
||||
"nock": "^9.0.14",
|
||||
"node-sass": "^4.6.1",
|
||||
"nock": "^10.0.6",
|
||||
"node-sass": "^4.12.0",
|
||||
"recursive-readdir": "^2.2.2"
|
||||
},
|
||||
"scripts": {
|
||||
"migrate_local": "DATABASE_URL=mysql://root:@localhost/box node_modules/.bin/db-migrate up",
|
||||
"migrate_test": "BOX_ENV=test DATABASE_URL=mysql://root:@localhost/boxtest node_modules/.bin/db-migrate up",
|
||||
"test": "npm run migrate_test && src/test/setupTest && BOX_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- --exit -R spec ./src/test ./src/routes/test",
|
||||
"test": "./runTests",
|
||||
"postmerge": "/bin/true",
|
||||
"precommit": "/bin/true",
|
||||
"prepush": "npm test",
|
||||
|
||||
71
runTests
Executable file
71
runTests
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu
|
||||
|
||||
readonly SOURCE_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly DATA_DIR="${HOME}/.cloudron_test"
|
||||
readonly DEFAULT_TESTS="./src/test/*-test.js ./src/routes/test/*-test.js"
|
||||
|
||||
! "${SOURCE_dir}/src/test/checkInstall" && exit 1
|
||||
|
||||
# cleanup old data dirs some of those docker container data requires sudo to be removed
|
||||
echo "=> Provide root password to purge any leftover data in ${DATA_DIR} and load apparmor profile:"
|
||||
sudo rm -rf ${DATA_DIR}
|
||||
|
||||
# archlinux does not have apparmor
|
||||
if hash apparmor_parser 2>/dev/null; then
|
||||
echo "=> Loading app armor profile"
|
||||
sudo apparmor_parser --replace --write-cache ./setup/start/docker-cloudron-app.apparmor
|
||||
fi
|
||||
|
||||
# create dir structure
|
||||
mkdir -p ${DATA_DIR}
|
||||
cd ${DATA_DIR}
|
||||
mkdir -p appsdata
|
||||
mkdir -p boxdata/appicons boxdata/mail boxdata/certs boxdata/mail/dkim/localhost boxdata/mail/dkim/foobar.com
|
||||
mkdir -p platformdata/addons/mail platformdata/nginx/cert platformdata/nginx/applications platformdata/collectd/collectd.conf.d platformdata/addons platformdata/logrotate.d platformdata/backup platformdata/logs/tasks
|
||||
|
||||
# put cert
|
||||
echo "=> Generating a localhost selfsigned cert"
|
||||
openssl req -x509 -newkey rsa:2048 -keyout platformdata/nginx/cert/host.key -out platformdata/nginx/cert/host.cert -days 3650 -subj '/CN=localhost' -nodes -config <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:*.localhost"))
|
||||
|
||||
# clear out any containers
|
||||
echo "=> Delete all docker containers first"
|
||||
docker ps -qa | xargs --no-run-if-empty docker rm -f
|
||||
|
||||
# create docker network (while the infra code does this, most tests skip infra setup)
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
|
||||
# create the same mysql server version to test with
|
||||
OUT=`docker inspect mysql-server` || true
|
||||
if [[ "${OUT}" = "[]" ]]; then
|
||||
echo "=> Starting mysql-server..."
|
||||
docker run --name mysql-server -e MYSQL_ROOT_PASSWORD=password -d mysql:5.7
|
||||
else
|
||||
echo "=> mysql-server already running. If you want to start fresh, run 'docker rm --force mysql-server'"
|
||||
fi
|
||||
|
||||
export MYSQL_IP=`docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mysql-server`
|
||||
|
||||
echo "=> Waiting for mysql server to be ready..."
|
||||
while ! mysqladmin ping -h"${MYSQL_IP}" --silent; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "=> Starting cloudron-syslog"
|
||||
cloudron-syslog --logdir "${DATA_DIR}/platformdata/logs/" &
|
||||
|
||||
echo "=> Ensure database"
|
||||
mysql -h"${MYSQL_IP}" -uroot -ppassword -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
|
||||
echo "=> Run database migrations"
|
||||
cd "${SOURCE_dir}"
|
||||
BOX_ENV=test DATABASE_URL=mysql://root:password@${MYSQL_IP}/box node_modules/.bin/db-migrate up
|
||||
|
||||
echo "=> Run tests with mocha"
|
||||
TESTS=${DEFAULT_TESTS}
|
||||
if [[ $# -gt 0 ]]; then
|
||||
TESTS="$*"
|
||||
fi
|
||||
|
||||
BOX_ENV=test ./node_modules/mocha/bin/_mocha --bail --no-timeouts --exit -R spec ${TESTS}
|
||||
@@ -1,85 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 2400"
|
||||
|
||||
ip=""
|
||||
zone=""
|
||||
subdomain=""
|
||||
cloudflare_token=""
|
||||
cloudflare_email=""
|
||||
tls_cert_file=""
|
||||
tls_key_file=""
|
||||
appstore_id=""
|
||||
appstore_token=""
|
||||
|
||||
args=$(getopt -o "" -l "subdomain:,zone:,ip:,cloudflare-token:,cloudflare-email:,tls-cert:,tls-key:,appstore-id:,appstore-token:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--ip) ip="$2"; shift 2;;
|
||||
--subdomain) subdomain="$2"; shift 2;;
|
||||
--zone) zone="$2"; shift 2;;
|
||||
--cloudflare-token) cloudflare_token="$2"; shift 2;;
|
||||
--cloudflare-email) cloudflare_email="$2"; shift 2;;
|
||||
--tls-cert) tls_cert_file="$2"; shift 2;;
|
||||
--tls-key) tls_key_file="$2"; shift 2;;
|
||||
--appstore-id) appstore_id="$2"; shift 2;;
|
||||
--appstore-token) appstore_token="$2"; shift 2;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
function get_status() {
|
||||
key="$1"
|
||||
if status=$($curl -q -f -k "https://${ip}/api/v1/cloudron/status" 2>/dev/null); then
|
||||
currentValue=$(echo "${status}" | python3 -c 'import sys, json; print(json.dumps(json.load(sys.stdin)[sys.argv[1]]))' "${key}")
|
||||
echo "${currentValue}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
function wait_for_status() {
|
||||
key="$1"
|
||||
expectedValue="$2"
|
||||
|
||||
echo "wait_for_status: $key to be $expectedValue"
|
||||
while true; do
|
||||
if currentValue=$(get_status "${key}"); then
|
||||
echo "wait_for_status: $key is current: $currentValue expecting: $expectedValue"
|
||||
if [[ "${currentValue}" == $expectedValue ]]; then
|
||||
break
|
||||
fi
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
}
|
||||
|
||||
echo "=> Waiting for cloudron to be ready"
|
||||
wait_for_status "version" '*'
|
||||
|
||||
echo "Provisioning Cloudron ${subdomain}.${zone}"
|
||||
if [[ -n "${tls_cert_file}" && -n "${tls_key_file}" ]]; then
|
||||
tls_cert=$(cat "${tls_cert_file}" | awk '{printf "%s\\n", $0}')
|
||||
tls_key=$(cat "${tls_key_file}" | awk '{printf "%s\\n", $0}')
|
||||
fallback_cert=$(printf '{ "cert": "%s", "key": "%s", "provider": "fallback", "restricted": true }' "${tls_cert}" "${tls_key}")
|
||||
else
|
||||
fallback_cert=null
|
||||
fi
|
||||
|
||||
setupData=$(printf '{ "dnsConfig": { "domain": "%s", "provider": "cloudflare", "config": { "token": "%s", "email": "%s", "hyphenatedSubdomains": true }, "tlsConfig": { "provider": "fallback" }, "fallbackCertificate": %s }, "autoconf": { "appstoreConfig": { "userId": "%s", "token": "%s" } } }' "${subdomain}.${zone}" "${cloudflare_token}" "${cloudflare_email}" "${fallback_cert}" "${appstore_id}" "${appstore_token}")
|
||||
|
||||
if ! setupResult=$($curl -kq -X POST -H "Content-Type: application/json" -d "${setupData}" https://${ip}/api/v1/cloudron/setup); then
|
||||
echo "Failed to setup with ${setupData}: ${setupResult}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
wait_for_status "webadminStatus" '*"tls": true*'
|
||||
|
||||
echo "Cloudron is ready at https://my-${subdomain}.${zone}"
|
||||
|
||||
@@ -43,22 +43,20 @@ fi
|
||||
initBaseImage="true"
|
||||
# provisioning data
|
||||
provider=""
|
||||
edition=""
|
||||
requestedVersion=""
|
||||
apiServerOrigin="https://api.cloudron.io"
|
||||
webServerOrigin="https://cloudron.io"
|
||||
sourceTarballUrl=""
|
||||
rebootServer="true"
|
||||
baseDataDir=""
|
||||
license=""
|
||||
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,edition:,skip-reboot" -n "$0" -- "$@")
|
||||
args=$(getopt -o "" -l "help,skip-baseimage-init,provider:,version:,env:,skip-reboot,license:" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--help) echo "See https://cloudron.io/documentation/installation/ on how to install Cloudron"; exit 0;;
|
||||
--provider) provider="$2"; shift 2;;
|
||||
--edition) edition="$2"; shift 2;;
|
||||
--version) requestedVersion="$2"; shift 2;;
|
||||
--env)
|
||||
if [[ "$2" == "dev" ]]; then
|
||||
@@ -69,6 +67,7 @@ while true; do
|
||||
webServerOrigin="https://staging.cloudron.io"
|
||||
fi
|
||||
shift 2;;
|
||||
--license) license="$2"; shift 2;;
|
||||
--skip-baseimage-init) initBaseImage="false"; shift;;
|
||||
--skip-reboot) rebootServer="false"; shift;;
|
||||
--) break;;
|
||||
@@ -93,35 +92,43 @@ fi
|
||||
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 (azure, digitalocean, ec2, exoscale, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic)"
|
||||
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}" != "digitalocean" && \
|
||||
"${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: azure, cloudscale.ch, digitalocean, ec2, exoscale, galaxygate, gce, hetzner, lightsail, linode, ovh, rosehosting, scaleway, vultr or generic"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "${baseDataDir}" && ! -d "${baseDataDir}" ]]; then
|
||||
echo "${baseDataDir} does not exist"
|
||||
echo "--provider must be one of: $AVAILABLE_PROVIDERS"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -137,13 +144,25 @@ 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 ! apt-get install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
|
||||
if ! DEBIAN_FRONTEND=noninteractive apt-get -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" -y install curl python3 ubuntu-standard -y &>> "${LOG_FILE}"; then
|
||||
echo "Could not install setup dependencies (curl). See ${LOG_FILE}"
|
||||
exit 1
|
||||
fi
|
||||
@@ -185,43 +204,30 @@ fi
|
||||
|
||||
# NOTE: this install script only supports 3.x and above
|
||||
echo "=> Installing version ${version} (this takes some time) ..."
|
||||
if [[ "${version}" =~ 3\.[0-2]+\.[0-9]+ ]]; then
|
||||
readonly DATA_FILE="/root/cloudron-install-data.json"
|
||||
data=$(cat <<EOF
|
||||
{
|
||||
"provider": "${provider}",
|
||||
"edition": "${edition}",
|
||||
"apiServerOrigin": "${apiServerOrigin}",
|
||||
"webServerOrigin": "${webServerOrigin}",
|
||||
"version": "${version}"
|
||||
}
|
||||
EOF
|
||||
)
|
||||
echo "${data}" > "${DATA_FILE}"
|
||||
mkdir -p /etc/cloudron
|
||||
# this file is used >= 4.2
|
||||
echo "${provider}" > /etc/cloudron/PROVIDER
|
||||
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" --data-file "${DATA_FILE}" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm "${DATA_FILE}"
|
||||
else
|
||||
mkdir -p /etc/cloudron
|
||||
cat > "/etc/cloudron/cloudron.conf" <<CONF_END
|
||||
# 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}",
|
||||
"edition": "${edition}"
|
||||
"provider": "${provider}"
|
||||
}
|
||||
CONF_END
|
||||
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
[[ -n "${license}" ]] && echo -n "$license" > /etc/cloudron/LICENSE
|
||||
|
||||
if ! /bin/bash "${box_src_tmp_dir}/scripts/installer.sh" &>> "${LOG_FILE}"; then
|
||||
echo "Failed to install cloudron. See ${LOG_FILE} for details"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 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 "."
|
||||
@@ -231,10 +237,15 @@ while true; do
|
||||
sleep 10
|
||||
done
|
||||
|
||||
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}"
|
||||
echo -e "\n\n${GREEN}Visit https://<IP> and accept the self-signed certificate to finish setup.${DONE}\n"
|
||||
|
||||
if [[ "${rebootServer}" == "true" ]]; then
|
||||
echo -e "\n${RED}Rebooting this server now to let changes take effect.${DONE}\n"
|
||||
systemctl stop mysql # sometimes mysql ends up having corrupt privilege tables
|
||||
systemctl reboot
|
||||
systemctl stop box mysql # sometimes mysql ends up having corrupt privilege tables
|
||||
|
||||
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;;
|
||||
* ) exit;;
|
||||
esac
|
||||
fi
|
||||
|
||||
@@ -1,13 +1,65 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
# This script collects diagnostic information to help debug server related issues
|
||||
# It also enables SSH access for the cloudron support team
|
||||
|
||||
PASTEBIN="https://paste.cloudron.io"
|
||||
OUT="/tmp/cloudron-support.log"
|
||||
LINE="\n========================================================\n"
|
||||
CLOUDRON_SUPPORT_PUBLIC_KEY="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQVilclYAIu+ioDp/sgzzFz6YU0hPcRYY7ze/LiF/lC7uQqK062O54BFXTvQ3ehtFZCx3bNckjlT2e6gB8Qq07OM66De4/S/g+HJW4TReY2ppSPMVNag0TNGxDzVH8pPHOysAm33LqT2b6L/wEXwC6zWFXhOhHjcMqXvi8Ejaj20H1HVVcf/j8qs5Thkp9nAaFTgQTPu8pgwD8wDeYX1hc9d0PYGesTADvo6HF4hLEoEnefLw7PaStEbzk2fD3j7/g5r5HcgQQXBe74xYZ/1gWOX2pFNuRYOBSEIrNfJEjFJsqk3NR1+ZoMGK7j+AZBR4k0xbrmncQLcQzl6MMDzkp support@cloudron.io"
|
||||
HELP_MESSAGE="
|
||||
This script collects diagnostic information to help debug server related issues
|
||||
|
||||
Options:
|
||||
--admin-login Login as administrator
|
||||
--enable-ssh Enable SSH access for the Cloudron support team
|
||||
--help Show this message
|
||||
"
|
||||
|
||||
# We require root
|
||||
if [[ ${EUID} -ne 0 ]]; then
|
||||
echo "This script should be run as root." > /dev/stderr
|
||||
echo "This script should be run as root. Run with sudo"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
enableSSH="false"
|
||||
|
||||
args=$(getopt -o "" -l "help,enable-ssh,admin-login" -n "$0" -- "$@")
|
||||
eval set -- "${args}"
|
||||
|
||||
while true; do
|
||||
case "$1" in
|
||||
--help) echo -e "${HELP_MESSAGE}"; exit 0;;
|
||||
--enable-ssh) enableSSH="true"; shift;;
|
||||
--admin-login)
|
||||
admin_username=$(mysql -NB -uroot -ppassword -e "SELECT username FROM box.users WHERE admin=1 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."
|
||||
exit 0
|
||||
;;
|
||||
--) break;;
|
||||
*) echo "Unknown option $1"; exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
# check if at least 10mb root partition space is available
|
||||
if [[ "`df --output="avail" / | sed -n 2p`" -lt "10240" ]]; then
|
||||
echo "No more space left on /"
|
||||
echo "This is likely the root case of the issue. Free up some space and also check other partitions below:"
|
||||
echo ""
|
||||
df -h
|
||||
echo ""
|
||||
echo "To recover from a full disk, follow the guide at https://cloudron.io/documentation/troubleshooting/#recovery-after-disk-full"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check for at least 5mb free /tmp space for the log file
|
||||
if [[ "`df --output="avail" /tmp | sed -n 2p`" -lt "5120" ]]; then
|
||||
echo "Not enough space left on /tmp"
|
||||
echo "Free up some space first by deleting files from /tmp"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -16,51 +68,77 @@ echo -n "Generating Cloudron Support stats..."
|
||||
# clear file
|
||||
rm -rf $OUT
|
||||
|
||||
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
|
||||
if [[ $SUDO_USER === "" ]]; then
|
||||
ssh_user="root"
|
||||
ssh_folder="/root/.ssh/"
|
||||
authorized_key_file="${ssh_folder}/authorized_keys"
|
||||
else
|
||||
ssh_user="$SUDO_USER"
|
||||
ssh_folder="/home/$SUDO_USER/.ssh/"
|
||||
authorized_key_file="${ssh_folder}/authorized_keys"
|
||||
fi
|
||||
|
||||
echo -e $LINE"SSH"$LINE >> $OUT
|
||||
echo "Username: ${ssh_user}" >> $OUT
|
||||
echo "Port: ${ssh_port}" >> $OUT
|
||||
|
||||
echo -e $LINE"cloudron.conf"$LINE >> $OUT
|
||||
cat /etc/cloudron/cloudron.conf &>> $OUT
|
||||
echo -e $LINE"PROVIDER"$LINE >> $OUT
|
||||
cat /etc/cloudron/PROVIDER &>> $OUT || true
|
||||
|
||||
echo -e $LINE"Docker container"$LINE >> $OUT
|
||||
docker ps -a &>> $OUT
|
||||
if ! timeout --kill-after 10s 15s docker ps -a &>> $OUT 2>&1; then
|
||||
echo -e "Docker is not responding" >> $OUT
|
||||
fi
|
||||
|
||||
echo -e $LINE"Filesystem stats"$LINE >> $OUT
|
||||
df -h &>> $OUT
|
||||
|
||||
echo -e $LINE"Appsdata stats"$LINE >> $OUT
|
||||
du -hcsL /home/yellowtent/appsdata/* &>> $OUT
|
||||
|
||||
echo -e $LINE"Boxdata stats"$LINE >> $OUT
|
||||
du -hcsL /home/yellowtent/boxdata/* &>> $OUT
|
||||
|
||||
echo -e $LINE"Backup stats (possibly misleading)"$LINE >> $OUT
|
||||
du -hcsL /var/backups/* &>> $OUT
|
||||
|
||||
echo -e $LINE"System daemon status"$LINE >> $OUT
|
||||
systemctl status --lines=100 cloudron.target box mysql unbound cloudron-syslog nginx collectd docker &>> $OUT
|
||||
|
||||
echo -e $LINE"Box logs"$LINE >> $OUT
|
||||
tail -n 100 /home/yellowtent/platformdata/logs/box.log &>> $OUT
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
ip addr &>> $OUT
|
||||
|
||||
echo -e $LINE"Firewall chains"$LINE >> $OUT
|
||||
iptables -L &>> $OUT
|
||||
|
||||
echo "Done"
|
||||
|
||||
if [[ "${enableSSH}" == "true" ]]; then
|
||||
ssh_port=$(cat /etc/ssh/sshd_config | grep "Port " | sed -e "s/.*Port //")
|
||||
permit_root_login=$(grep -q ^PermitRootLogin.*yes /etc/ssh/sshd_config && echo "yes" || echo "no")
|
||||
|
||||
# support.js uses similar logic
|
||||
if $(grep -q "ec2\|lightsail\|ami" /etc/cloudron/PROVIDER); then
|
||||
ssh_user="ubuntu"
|
||||
keys_file="/home/ubuntu/.ssh/authorized_keys"
|
||||
else
|
||||
ssh_user="root"
|
||||
keys_file="/root/.ssh/authorized_keys"
|
||||
fi
|
||||
|
||||
echo -e $LINE"SSH"$LINE >> $OUT
|
||||
echo "Username: ${ssh_user}" >> $OUT
|
||||
echo "Port: ${ssh_port}" >> $OUT
|
||||
echo "PermitRootLogin: ${permit_root_login}" >> $OUT
|
||||
echo "Key file: ${keys_file}" >> $OUT
|
||||
|
||||
echo -n "Enabling ssh access for the Cloudron support team..."
|
||||
mkdir -p $(dirname "${keys_file}") # .ssh does not exist sometimes
|
||||
touch "${keys_file}" # required for concat to work
|
||||
if ! grep -q "${CLOUDRON_SUPPORT_PUBLIC_KEY}" "${keys_file}"; then
|
||||
echo -e "\n${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> "${keys_file}"
|
||||
chmod 600 "${keys_file}"
|
||||
chown "${ssh_user}" "${keys_file}"
|
||||
fi
|
||||
|
||||
echo "Done"
|
||||
fi
|
||||
|
||||
echo -n "Uploading information..."
|
||||
# for some reason not using $(cat $OUT) will not contain newlines!?
|
||||
paste_key=$(curl -X POST ${PASTEBIN}/documents --silent -d "$(cat $OUT)" | python3 -c "import sys, json; print(json.load(sys.stdin)['key'])")
|
||||
echo "Done"
|
||||
|
||||
echo -n "Enabling ssh access for the Cloudron support team..."
|
||||
mkdir -p "${ssh_folder}"
|
||||
echo "${CLOUDRON_SUPPORT_PUBLIC_KEY}" >> ${authorized_key_file}
|
||||
chown -R ${ssh_user} "${ssh_folder}"
|
||||
chmod 600 "${authorized_key_file}"
|
||||
echo "Done"
|
||||
|
||||
echo ""
|
||||
echo "Please send the following link to support@cloudron.io"
|
||||
echo "Please email the following link to support@cloudron.io"
|
||||
echo ""
|
||||
echo "${PASTEBIN}/${paste_key}"
|
||||
|
||||
@@ -41,8 +41,8 @@ if ! $(cd "${SOURCE_DIR}/../dashboard" && git diff --exit-code >/dev/null); then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$(node --version)" != "v8.11.2" ]]; then
|
||||
echo "This script requires node 8.11.2"
|
||||
if [[ "$(node --version)" != "v10.15.1" ]]; then
|
||||
echo "This script requires node 10.15.1"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -19,17 +19,18 @@ readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly box_src_tmp_dir="$(realpath ${script_dir}/..)"
|
||||
|
||||
readonly ubuntu_version=$(lsb_release -rs)
|
||||
readonly ubuntu_codename=$(lsb_release -cs)
|
||||
|
||||
readonly is_update=$(systemctl is-active box && echo "yes" || echo "no")
|
||||
|
||||
echo "==> installer: updating docker"
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
|
||||
$curl -sL https://download.docker.com/linux/ubuntu/dists/xenial/pool/stable/amd64/docker-ce_18.03.1~ce-0~ubuntu_amd64.deb -o /tmp/docker.deb
|
||||
|
||||
# https://download.docker.com/linux/ubuntu/dists/xenial/stable/binary-amd64/Packages
|
||||
if [[ $(sha256sum /tmp/docker.deb | cut -d' ' -f1) != "54f4c9268492a4fd2ec2e6bcc95553855b025f35dcc8b9f60ac34e0aa307279b" ]]; then
|
||||
echo "==> installer: docker binary download is corrupt"
|
||||
exit 5
|
||||
fi
|
||||
if [[ $(docker version --format {{.Client.Version}}) != "18.09.2" ]]; then
|
||||
# there are 3 packages for docker - containerd, CLI and the daemon
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/containerd.io_1.2.2-3_amd64.deb" -o /tmp/containerd.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce-cli_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker-ce-cli.deb
|
||||
$curl -sL "https://download.docker.com/linux/ubuntu/dists/${ubuntu_codename}/pool/stable/amd64/docker-ce_18.09.2~3-0~ubuntu-${ubuntu_codename}_amd64.deb" -o /tmp/docker.deb
|
||||
|
||||
echo "==> installer: Waiting for all dpkg tasks to finish..."
|
||||
while fuser /var/lib/dpkg/lock; do
|
||||
@@ -47,21 +48,21 @@ if [[ $(docker version --format {{.Client.Version}}) != "18.03.1-ce" ]]; then
|
||||
sleep 1
|
||||
done
|
||||
|
||||
while ! apt install -y /tmp/docker.deb; do
|
||||
while ! apt install -y /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb; do
|
||||
echo "==> installer: Failed to install docker. Retry"
|
||||
sleep 1
|
||||
done
|
||||
|
||||
rm /tmp/docker.deb
|
||||
rm /tmp/containerd.deb /tmp/docker-ce-cli.deb /tmp/docker.deb
|
||||
fi
|
||||
|
||||
echo "==> installer: updating node"
|
||||
if [[ "$(node --version)" != "v8.11.2" ]]; then
|
||||
mkdir -p /usr/local/node-8.11.2
|
||||
$curl -sL https://nodejs.org/dist/v8.11.2/node-v8.11.2-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-8.11.2
|
||||
ln -sf /usr/local/node-8.11.2/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-8.11.2/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-6.11.5
|
||||
if [[ "$(node --version)" != "v10.15.1" ]]; then
|
||||
mkdir -p /usr/local/node-10.15.1
|
||||
$curl -sL https://nodejs.org/dist/v10.15.1/node-v10.15.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-10.15.1
|
||||
ln -sf /usr/local/node-10.15.1/bin/node /usr/bin/node
|
||||
ln -sf /usr/local/node-10.15.1/bin/npm /usr/bin/npm
|
||||
rm -rf /usr/local/node-8.11.2 /usr/local/node-8.9.3
|
||||
fi
|
||||
|
||||
# this is here (and not in updater.js) because rebuild requires the above node
|
||||
@@ -86,8 +87,14 @@ images=$(node -e "var i = require('${box_src_tmp_dir}/src/infra_version.js'); co
|
||||
|
||||
echo -e "\tPulling docker images: ${images}"
|
||||
for image in ${images}; do
|
||||
docker pull "${image}" # this pulls the image using the sha256
|
||||
docker pull "${image%@sha256:*}" # this will tag the image for readability
|
||||
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
|
||||
done
|
||||
|
||||
echo "==> installer: update cloudron-syslog"
|
||||
|
||||
@@ -13,10 +13,12 @@ readonly BOX_SRC_DIR="${HOME_DIR}/box"
|
||||
readonly PLATFORM_DATA_DIR="${HOME_DIR}/platformdata" # platform data
|
||||
readonly APPS_DATA_DIR="${HOME_DIR}/appsdata" # app data
|
||||
readonly BOX_DATA_DIR="${HOME_DIR}/boxdata" # box data
|
||||
readonly CONFIG_DIR="${HOME_DIR}/configs"
|
||||
|
||||
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly get_config="$(realpath ${script_dir}/../node_modules/.bin/json) -f /etc/cloudron/cloudron.conf"
|
||||
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
|
||||
|
||||
echo "==> Configuring docker"
|
||||
cp "${script_dir}/start/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||
@@ -24,22 +26,6 @@ systemctl enable apparmor
|
||||
systemctl restart apparmor
|
||||
|
||||
usermod ${USER} -a -G docker
|
||||
# preserve the existing storage driver (user might be using overlay2)
|
||||
storage_driver=$(docker info | grep "Storage Driver" | sed 's/.*: //')
|
||||
[[ -n "${storage_driver}" ]] || storage_driver="overlay2" # if the above command fails
|
||||
|
||||
temp_file=$(mktemp)
|
||||
# create systemd drop-in. some apps do not work with aufs
|
||||
echo -e "[Service]\nExecStart=\nExecStart=/usr/bin/dockerd -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs --storage-driver=${storage_driver}" > "${temp_file}"
|
||||
|
||||
systemctl enable docker
|
||||
# restart docker if options changed
|
||||
if [[ ! -f /etc/systemd/system/docker.service.d/cloudron.conf ]] || ! diff -q /etc/systemd/system/docker.service.d/cloudron.conf "${temp_file}" >/dev/null; then
|
||||
mkdir -p /etc/systemd/system/docker.service.d
|
||||
mv "${temp_file}" /etc/systemd/system/docker.service.d/cloudron.conf
|
||||
systemctl daemon-reload
|
||||
systemctl restart docker
|
||||
fi
|
||||
docker network create --subnet=172.18.0.0/16 cloudron || true
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}"
|
||||
@@ -58,7 +44,10 @@ mkdir -p "${PLATFORM_DATA_DIR}/collectd/collectd.conf.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/acme"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/backup"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/logs/backup" \
|
||||
"${PLATFORM_DATA_DIR}/logs/updater" \
|
||||
"${PLATFORM_DATA_DIR}/logs/tasks" \
|
||||
"${PLATFORM_DATA_DIR}/logs/crash"
|
||||
mkdir -p "${PLATFORM_DATA_DIR}/update"
|
||||
|
||||
mkdir -p "${BOX_DATA_DIR}/appicons"
|
||||
@@ -88,28 +77,19 @@ systemctl daemon-reload
|
||||
systemctl restart systemd-journald
|
||||
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
||||
|
||||
echo "==> Creating config directory"
|
||||
mkdir -p "${CONFIG_DIR}"
|
||||
|
||||
# migration for cloudron.conf file. Can be removed after 3.3
|
||||
if [[ ! -d /etc/cloudron ]]; then
|
||||
echo "==> Migrating existing cloudron.conf to new location"
|
||||
mkdir -p /etc/cloudron
|
||||
cp "${CONFIG_DIR}/cloudron.conf" /etc/cloudron/cloudron.conf
|
||||
fi
|
||||
chown -R "${USER}" /etc/cloudron
|
||||
|
||||
echo "==> Setting up unbound"
|
||||
# DO uses Google nameservers by default. This causes RBL queries to fail (host 2.0.0.127.zen.spamhaus.org)
|
||||
# We do not use dnsmasq because it is not a recursive resolver and defaults to the value in the interfaces file (which is Google DNS!)
|
||||
# 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
|
||||
echo -e "server:\n\tinterface: 0.0.0.0\n\tdo-ip6: yes\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
|
||||
# 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
|
||||
# update the root anchor after a out-of-disk-space situation (see #269)
|
||||
unbound-anchor -a /var/lib/unbound/root.key
|
||||
|
||||
echo "==> Adding systemd services"
|
||||
cp -r "${script_dir}/start/systemd/." /etc/systemd/system/
|
||||
[[ "${ubuntu_version}" == "16.04" ]] && sed -e 's/MemoryMax/MemoryLimit/g' -i /etc/systemd/system/box.service
|
||||
systemctl daemon-reload
|
||||
systemctl enable unbound
|
||||
systemctl enable cloudron-syslog
|
||||
@@ -142,8 +122,11 @@ echo "==> Configuring logrotate"
|
||||
if ! grep -q "^include ${PLATFORM_DATA_DIR}/logrotate.d" /etc/logrotate.conf; then
|
||||
echo -e "\ninclude ${PLATFORM_DATA_DIR}/logrotate.d\n" >> /etc/logrotate.conf
|
||||
fi
|
||||
cp "${script_dir}/start/app-logrotate" "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
|
||||
chown root:root "${PLATFORM_DATA_DIR}/logrotate.d/app-logrotate"
|
||||
rm -f "${PLATFORM_DATA_DIR}/logrotate.d/"*
|
||||
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"
|
||||
cp "${script_dir}/start/cloudron-motd" /etc/update-motd.d/92-cloudron
|
||||
@@ -186,11 +169,13 @@ mysqladmin -u root -ppassword password password # reset default root password
|
||||
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
|
||||
echo "==> Migrating data"
|
||||
sudo -u "${USER}" -H bash <<EOF
|
||||
set -eu
|
||||
cd "${BOX_SRC_DIR}"
|
||||
BOX_ENV=cloudron DATABASE_URL=mysql://root:${mysql_root_password}@127.0.0.1/box "${BOX_SRC_DIR}/node_modules/.bin/db-migrate" up
|
||||
EOF
|
||||
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"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
rm -f /etc/cloudron/cloudron.conf
|
||||
|
||||
if [[ ! -f "${BOX_DATA_DIR}/dhparams.pem" ]]; then
|
||||
echo "==> Generating dhparams (takes forever)"
|
||||
@@ -200,17 +185,18 @@ else
|
||||
cp "${BOX_DATA_DIR}/dhparams.pem" "${PLATFORM_DATA_DIR}/addons/mail/dhparams.pem"
|
||||
fi
|
||||
|
||||
# old installations used to create appdata/<app>/redis which is now part of old backups and prevents restore
|
||||
echo "==> Cleaning up stale redis directories"
|
||||
find "${APPS_DATA_DIR}" -maxdepth 2 -type d -name redis -exec rm -rf {} +
|
||||
|
||||
echo "==> Changing ownership"
|
||||
chown "${USER}:${USER}" -R "${CONFIG_DIR}"
|
||||
# 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"
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}/INFRA_VERSION" 2>/dev/null || true
|
||||
chown "${USER}:${USER}" "${PLATFORM_DATA_DIR}"
|
||||
chown "${USER}:${USER}" "${APPS_DATA_DIR}"
|
||||
|
||||
# logrotate files have to be owned by root, this is here to fixup existing installations where we were resetting the owner to yellowtent
|
||||
chown root:root -R "${PLATFORM_DATA_DIR}/logrotate.d"
|
||||
|
||||
# do not chown the boxdata/mail directory; dovecot gets upset
|
||||
chown "${USER}:${USER}" "${BOX_DATA_DIR}"
|
||||
find "${BOX_DATA_DIR}" -mindepth 1 -maxdepth 1 -not -path "${BOX_DATA_DIR}/mail" -exec chown -R "${USER}:${USER}" {} \;
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
# logrotate config for app logs
|
||||
|
||||
/home/yellowtent/platformdata/logs/*/*.log {
|
||||
# only keep one rotated file, we currently do not send that over the api
|
||||
rotate 1
|
||||
size 10M
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
copytruncate
|
||||
}
|
||||
@@ -9,7 +9,7 @@ iptables -t filter -F CLOUDRON # empty any existing rules
|
||||
# NOTE: keep these in sync with src/apps.js validatePortBindings
|
||||
# allow ssh, http, https, ping, dns
|
||||
iptables -t filter -I CLOUDRON -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||
# caas has ssh on port 202
|
||||
# ssh is allowed alternately on port 202
|
||||
iptables -A CLOUDRON -p tcp -m tcp -m multiport --dports 22,25,80,202,443,587,993,4190 -j ACCEPT
|
||||
|
||||
iptables -t filter -A CLOUDRON -p icmp --icmp-type echo-request -j ACCEPT
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
#!/bin/sh
|
||||
# motd hook to remind admins about updates
|
||||
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
|
||||
printf "\t\t\t-----------------------\n"
|
||||
printf "Please do not run apt upgrade manually as it will update packages that\n"
|
||||
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"
|
||||
#!/bin/bash
|
||||
|
||||
if grep -q "^PasswordAuthentication yes" /etc/ssh/sshd_config; then
|
||||
printf "\nPlease disable password based SSH access to secure your server. Read more at\n"
|
||||
printf "https://cloudron.io/documentation/security/#securing-ssh-access\n"
|
||||
printf "**********************************************************************\n\n"
|
||||
|
||||
if [[ -z "$(ls -A /home/yellowtent/boxdata/mail/dkim)" ]]; then
|
||||
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"
|
||||
else
|
||||
printf "\t\t\tNOTE TO CLOUDRON ADMINS\n"
|
||||
printf "\t\t\t-----------------------\n"
|
||||
printf "Please do not run apt upgrade manually as it will update packages that\n"
|
||||
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"
|
||||
fi
|
||||
|
||||
printf "\nFor help and more information, visit https://forum.cloudron.io\n\n"
|
||||
|
||||
printf "**********************************************************************\n"
|
||||
|
||||
@@ -240,8 +240,23 @@ LoadPlugin write_graphite
|
||||
Interactive false
|
||||
|
||||
Import "df"
|
||||
# <Module df>
|
||||
# </Module>
|
||||
|
||||
Import "du"
|
||||
<Module du>
|
||||
<Path>
|
||||
Instance maildata
|
||||
Dir "/home/yellowtent/boxdata/mail"
|
||||
</Path>
|
||||
<Path>
|
||||
Instance boxdata
|
||||
Dir "/home/yellowtent/boxdata"
|
||||
Exclude "mail"
|
||||
</Path>
|
||||
<Path>
|
||||
Instance platformdata
|
||||
Dir "/home/yellowtent/platformdata"
|
||||
</Path>
|
||||
</Module>
|
||||
</Plugin>
|
||||
|
||||
<Plugin write_graphite>
|
||||
|
||||
@@ -21,6 +21,7 @@ def read():
|
||||
except:
|
||||
continue
|
||||
|
||||
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
|
||||
val = collectd.Values(type='df_complex', plugin='df', plugin_instance=instance)
|
||||
|
||||
free = st.f_bavail * st.f_frsize # bavail is for non-root user. bfree is total
|
||||
|
||||
79
setup/start/collectd/du.py
Normal file
79
setup/start/collectd/du.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import collectd,os,subprocess,sys,re,time
|
||||
|
||||
# https://www.programcreek.com/python/example/106897/collectd.register_read
|
||||
|
||||
PATHS = [] # { name, dir, exclude }
|
||||
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'])
|
||||
if pathinfo['exclude'] != '':
|
||||
cmd += ' --exclude "{}"'.format(pathinfo['exclude'])
|
||||
|
||||
collectd.info('computing size with command: %s' % cmd);
|
||||
try:
|
||||
size = subprocess.check_output(cmd, shell=True).split()[0].decode('utf-8')
|
||||
collectd.info('\tsize of %s is %s (time: %i)' % (pathinfo['dir'], size, int(time.time())))
|
||||
return size
|
||||
except Exception as e:
|
||||
collectd.info('\terror getting the size of %s: %s' % (pathinfo['dir'], str(e)))
|
||||
return 0
|
||||
|
||||
def parseSize(size):
|
||||
units = {"B": 1, "KB": 10**3, "MB": 10**6, "GB": 10**9, "TB": 10**12}
|
||||
number, unit, _ = re.split('([a-zA-Z]+)', size.upper())
|
||||
return int(float(number)*units[unit])
|
||||
|
||||
def dockerSize():
|
||||
# use --format '{{json .}}' to dump the string. '{{if eq .Type "Images"}}{{.Size}}{{end}}' still creates newlines
|
||||
cmd = 'timeout 1800 docker system df --format "{{.Size}}" | head -n1'
|
||||
try:
|
||||
size = subprocess.check_output(cmd, shell=True).strip().decode('utf-8')
|
||||
collectd.info('size of docker images is %s (%s) (time: %i)' % (size, parseSize(size), int(time.time())))
|
||||
return parseSize(size)
|
||||
except Exception as e:
|
||||
collectd.info('error getting docker images size : %s' % str(e))
|
||||
return 0
|
||||
|
||||
# configure is called for each module block. this is called before init
|
||||
def configure(config):
|
||||
global PATHS
|
||||
|
||||
for child in config.children:
|
||||
if child.key != 'Path':
|
||||
collectd.info('du plugin: Unknown config key "%s"' % key)
|
||||
continue
|
||||
|
||||
pathinfo = { 'name': '', 'dir': '', 'exclude': '' }
|
||||
for node in child.children:
|
||||
if node.key == 'Instance':
|
||||
pathinfo['name'] = node.values[0]
|
||||
elif node.key == 'Dir':
|
||||
pathinfo['dir'] = node.values[0]
|
||||
elif node.key == 'Exclude':
|
||||
pathinfo['exclude'] = node.values[0]
|
||||
|
||||
PATHS.append(pathinfo);
|
||||
collectd.info('du plugin: monitoring %s' % pathinfo['dir']);
|
||||
|
||||
def init():
|
||||
global PATHS
|
||||
collectd.info('custom du plugin initialized with %s %s' % (PATHS, sys.version))
|
||||
|
||||
def read():
|
||||
for pathinfo in PATHS:
|
||||
size = du(pathinfo)
|
||||
|
||||
# type comes from https://github.com/collectd/collectd/blob/master/src/types.db
|
||||
val = collectd.Values(type='capacity', plugin='du', plugin_instance=pathinfo['name'])
|
||||
val.dispatch(values=[size], type_instance='usage')
|
||||
|
||||
size = dockerSize()
|
||||
val = collectd.Values(type='capacity', plugin='du', plugin_instance='docker')
|
||||
val.dispatch(values=[size], type_instance='usage')
|
||||
|
||||
|
||||
|
||||
collectd.register_init(init)
|
||||
collectd.register_config(configure)
|
||||
collectd.register_read(read, INTERVAL)
|
||||
40
setup/start/custom.yml
Normal file
40
setup/start/custom.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
# add customizations here
|
||||
# after making changes run "sudo systemctl restart box"
|
||||
|
||||
# appstore:
|
||||
# blacklist:
|
||||
# - io.wekan.cloudronapp
|
||||
# - io.cloudron.openvpn
|
||||
# whitelist:
|
||||
# org.wordpress.cloudronapp: {}
|
||||
# chat.rocket.cloudronapp: {}
|
||||
# com.nextcloud.cloudronapp: {}
|
||||
#
|
||||
# backups:
|
||||
# configurable: true
|
||||
#
|
||||
# domains:
|
||||
# dynamicDns: true
|
||||
# changeDashboardDomain: true
|
||||
#
|
||||
# subscription:
|
||||
# configurable: true
|
||||
#
|
||||
# support:
|
||||
# email: support@cloudron.io
|
||||
# remoteSupport: true
|
||||
#
|
||||
# ticketFormBody: |
|
||||
# Use this form to open support tickets. You can also write directly to [support@cloudron.io](mailto:support@cloudron.io).
|
||||
# * [Knowledge Base & App Docs](https://cloudron.io/documentation/apps/?support_view)
|
||||
# * [Custom App Packaging & API](https://cloudron.io/developer/packaging/?support_view)
|
||||
# * [Forum](https://forum.cloudron.io/)
|
||||
#
|
||||
# submitTickets: true
|
||||
#
|
||||
# alerts:
|
||||
# email: support@cloudron.io
|
||||
# notifyCloudronAdmins: false
|
||||
#
|
||||
# footer:
|
||||
# body: '© 2019 [Cloudron](https://cloudron.io) [Forum <i class="fa fa-comments"></i>](https://forum.cloudron.io)'
|
||||
10
setup/start/logrotate/box
Normal file
10
setup/start/logrotate/box
Normal file
@@ -0,0 +1,10 @@
|
||||
# logrotate config for box logs
|
||||
|
||||
# keep upto 5 logs of size 10M each
|
||||
/home/yellowtent/platformdata/logs/box.log {
|
||||
rotate 5
|
||||
size 10M
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
copytruncate
|
||||
}
|
||||
31
setup/start/logrotate/platform
Normal file
31
setup/start/logrotate/platform
Normal file
@@ -0,0 +1,31 @@
|
||||
# logrotate config for app, crash, addon and task logs
|
||||
|
||||
# man 7 glob
|
||||
/home/yellowtent/platformdata/logs/graphite/*.log
|
||||
/home/yellowtent/platformdata/logs/mail/*.log
|
||||
/home/yellowtent/platformdata/logs/mysql/*.log
|
||||
/home/yellowtent/platformdata/logs/mongodb/*.log
|
||||
/home/yellowtent/platformdata/logs/postgresql/*.log
|
||||
/home/yellowtent/platformdata/logs/sftp/*.log
|
||||
/home/yellowtent/platformdata/logs/redis-*/*.log
|
||||
/home/yellowtent/platformdata/logs/crash/*.log
|
||||
/home/yellowtent/platformdata/logs/updater/*.log {
|
||||
# only keep one rotated file, we currently do not send that over the api
|
||||
rotate 1
|
||||
size 10M
|
||||
missingok
|
||||
# we never compress so we can simply tail the files
|
||||
nocompress
|
||||
copytruncate
|
||||
}
|
||||
|
||||
# keep task logs for a week. the 'nocreate' option ensures empty log files are not
|
||||
# created post rotation
|
||||
/home/yellowtent/platformdata/logs/tasks/*.log {
|
||||
minage 7
|
||||
daily
|
||||
rotate 0
|
||||
missingok
|
||||
nocreate
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ max_allowed_packet=32M
|
||||
character-set-server = utf8mb4
|
||||
collation-server = utf8mb4_unicode_ci
|
||||
|
||||
# set timezone to UTC
|
||||
default_time_zone='+00:00'
|
||||
|
||||
[mysqldump]
|
||||
quick
|
||||
quote-names
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
# sudo logging breaks journalctl output with very long urls (systemd bug)
|
||||
Defaults !syslog
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmvolume.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmvolume.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/clearvolume.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/clearvolume.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmaddon.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddon.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/mvvolume.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mvvolume.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/mkdirvolume.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mkdirvolume.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/rmaddondir.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddondir.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/reloadnginx.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadnginx.sh
|
||||
@@ -25,15 +31,22 @@ 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
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/authorized_keys.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/authorized_keys.sh
|
||||
Defaults!/home/yellowtent/box/src/scripts/remotesupport.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/remotesupport.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/configurelogrotate.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/configurelogrotate.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/backuptask.js env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/backuptask.js
|
||||
Defaults!/home/yellowtent/box/src/scripts/backupupload.js env_keep="HOME BOX_ENV"
|
||||
Defaults!/home/yellowtent/box/src/scripts/backupupload.js closefrom_override
|
||||
yellowtent ALL=(root) NOPASSWD:SETENV: /home/yellowtent/box/src/scripts/backupupload.js
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/restart.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restart.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/restartdocker.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartdocker.sh
|
||||
|
||||
Defaults!/home/yellowtent/box/src/scripts/restartunbound.sh env_keep="HOME BOX_ENV"
|
||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/restartunbound.sh
|
||||
|
||||
|
||||
@@ -12,13 +12,17 @@ Wants=cloudron-resize-fs.service
|
||||
Type=idle
|
||||
WorkingDirectory=/home/yellowtent/box
|
||||
Restart=always
|
||||
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
|
||||
; 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"
|
||||
; 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
|
||||
OOMScoreAdjust=-999
|
||||
User=yellowtent
|
||||
Group=yellowtent
|
||||
MemoryLimit=200M
|
||||
; OOM killer is invoked in this unit beyond this. The start script replaces this with MemoryLimit for Ubuntu 16
|
||||
MemoryMax=400M
|
||||
TimeoutStopSec=5s
|
||||
StartLimitInterval=1
|
||||
StartLimitBurst=60
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
exports = module.exports = {
|
||||
SCOPE_APPS_READ: 'apps:read',
|
||||
SCOPE_APPS_MANAGE: 'apps:manage',
|
||||
SCOPE_APPSTORE: 'appstore',
|
||||
SCOPE_CLIENTS: 'clients',
|
||||
SCOPE_CLOUDRON: 'cloudron',
|
||||
SCOPE_DOMAINS_READ: 'domains:read',
|
||||
@@ -10,10 +11,10 @@ exports = module.exports = {
|
||||
SCOPE_MAIL: 'mail',
|
||||
SCOPE_PROFILE: 'profile',
|
||||
SCOPE_SETTINGS: 'settings',
|
||||
SCOPE_SUBSCRIPTION: 'subscription',
|
||||
SCOPE_USERS_READ: 'users:read',
|
||||
SCOPE_USERS_MANAGE: 'users:manage',
|
||||
SCOPE_APPSTORE: 'appstore',
|
||||
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'users' ], // keep this sorted
|
||||
VALID_SCOPES: [ 'apps', 'appstore', 'clients', 'cloudron', 'domains', 'mail', 'profile', 'settings', 'subscription', 'users' ], // keep this sorted
|
||||
|
||||
SCOPE_ANY: '*',
|
||||
|
||||
@@ -26,12 +27,10 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
config = require('./config.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
debug = require('debug')('box:accesscontrol'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
users = require('./users.js'),
|
||||
UsersError = users.UsersError,
|
||||
_ = require('underscore');
|
||||
|
||||
// returns scopes that does not have wildcards and is sorted
|
||||
@@ -78,13 +77,12 @@ function intersectScopes(allowedScopes, wantedScopes) {
|
||||
function validateScopeString(scope) {
|
||||
assert.strictEqual(typeof scope, 'string');
|
||||
|
||||
if (scope === '') return new Error('Empty scope not allowed');
|
||||
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 Error('Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '));
|
||||
|
||||
if (!allValid) return new BoxError(BoxError.BAD_FIELD, 'Invalid scope. Available scopes are ' + exports.VALID_SCOPES.join(', '), { field: 'scope' });
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -101,7 +99,7 @@ function hasScopes(authorizedScopes, requiredScopes) {
|
||||
// this allows apps:write if the token has a higher apps scope
|
||||
if (authorizedScopes.indexOf(requiredScopes[i]) === -1 && authorizedScopes.indexOf(scopeParts[0]) === -1) {
|
||||
debug('scope: missing scope "%s".', requiredScopes[i]);
|
||||
return new Error('Missing required scope "' + requiredScopes[i] + '"');
|
||||
return new BoxError(BoxError.NOT_FOUND, 'Missing required scope "' + requiredScopes[i] + '"');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,29 +112,28 @@ function scopesForUser(user, callback) {
|
||||
|
||||
if (user.admin) return callback(null, exports.VALID_SCOPES);
|
||||
|
||||
callback(null, config.isSpacesEnabled() ? [ 'profile', 'apps', 'domains:read', 'users:read' ] : [ 'profile', 'apps:read' ]);
|
||||
callback(null, [ 'profile', 'apps:read' ]);
|
||||
}
|
||||
|
||||
function validateToken(accessToken, callback) {
|
||||
assert.strictEqual(typeof accessToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.get(accessToken, function (error, token) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
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
|
||||
|
||||
users.get(token.identifier, function (error, user) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
if (error) return callback(error);
|
||||
|
||||
if (!user.active) return callback(null, null /* user */, 'Invalid Token'); // will end up as a 401
|
||||
|
||||
scopesForUser(user, function (error, userScopes) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
|
||||
const skipPasswordVerification = token.clientId === 'cid-sdk' || token.clientId === 'cid-cli'; // these clients do not require password checks unlike UI
|
||||
var info = { authorizedScopes: authorizedScopes, skipPasswordVerification: skipPasswordVerification }; // ends up in req.authInfo
|
||||
|
||||
callback(null, user, info);
|
||||
const authorizedScopes = intersectScopes(userScopes, token.scope.split(','));
|
||||
callback(null, user, { authorizedScopes }); // ends up in req.authInfo
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
1138
src/addons.js
1138
src/addons.js
File diff suppressed because it is too large
Load Diff
294
src/appdb.js
294
src/appdb.js
@@ -18,38 +18,12 @@ exports = module.exports = {
|
||||
getAddonConfigByName: getAddonConfigByName,
|
||||
unsetAddonConfig: unsetAddonConfig,
|
||||
unsetAddonConfigByAppId: unsetAddonConfigByAppId,
|
||||
getAppIdByAddonConfigValue: getAppIdByAddonConfigValue,
|
||||
|
||||
setHealth: setHealth,
|
||||
setInstallationCommand: setInstallationCommand,
|
||||
setRunCommand: setRunCommand,
|
||||
setTask: setTask,
|
||||
getAppStoreIds: getAppStoreIds,
|
||||
|
||||
setOwner: setOwner,
|
||||
transferOwnership: transferOwnership,
|
||||
|
||||
// installation codes (keep in sync in UI)
|
||||
ISTATE_PENDING_INSTALL: 'pending_install', // installs and fresh reinstalls
|
||||
ISTATE_PENDING_CLONE: 'pending_clone', // clone
|
||||
ISTATE_PENDING_CONFIGURE: 'pending_configure', // config (location, port) changes and on infra update
|
||||
ISTATE_PENDING_UNINSTALL: 'pending_uninstall', // uninstallation
|
||||
ISTATE_PENDING_RESTORE: 'pending_restore', // restore to previous backup or on upgrade
|
||||
ISTATE_PENDING_UPDATE: 'pending_update', // update from installed state preserving data
|
||||
ISTATE_PENDING_FORCE_UPDATE: 'pending_force_update', // update from any state preserving data
|
||||
ISTATE_PENDING_BACKUP: 'pending_backup', // backup the app
|
||||
ISTATE_ERROR: 'error', // error executing last pending_* command
|
||||
ISTATE_INSTALLED: 'installed', // app is installed
|
||||
|
||||
RSTATE_RUNNING: 'running',
|
||||
RSTATE_PENDING_START: 'pending_start',
|
||||
RSTATE_PENDING_STOP: 'pending_stop',
|
||||
RSTATE_STOPPED: 'stopped', // app stopped by use
|
||||
|
||||
// run codes (keep in sync in UI)
|
||||
HEALTH_HEALTHY: 'healthy',
|
||||
HEALTH_UNHEALTHY: 'unhealthy',
|
||||
HEALTH_ERROR: 'error',
|
||||
HEALTH_DEAD: 'dead',
|
||||
|
||||
// subdomain table types
|
||||
SUBDOMAIN_TYPE_PRIMARY: 'primary',
|
||||
SUBDOMAIN_TYPE_REDIRECT: 'redirect',
|
||||
@@ -59,17 +33,18 @@ exports = module.exports = {
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror'),
|
||||
mailboxdb = require('./mailboxdb.js'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.installationProgress', 'apps.runState',
|
||||
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.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit',
|
||||
'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup',
|
||||
'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.ts' ].join(',');
|
||||
'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',
|
||||
'apps.dataDir', 'apps.ts', 'apps.healthTime' ].join(',');
|
||||
|
||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
|
||||
|
||||
@@ -82,17 +57,13 @@ function postProcess(result) {
|
||||
result.manifest = safe.JSON.parse(result.manifestJson);
|
||||
delete result.manifestJson;
|
||||
|
||||
assert(result.oldConfigJson === null || typeof result.oldConfigJson === 'string');
|
||||
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
|
||||
delete result.oldConfigJson;
|
||||
assert(result.tagsJson === null || typeof result.tagsJson === 'string');
|
||||
result.tags = safe.JSON.parse(result.tagsJson) || [];
|
||||
delete result.tagsJson;
|
||||
|
||||
assert(result.updateConfigJson === null || typeof result.updateConfigJson === 'string');
|
||||
result.updateConfig = safe.JSON.parse(result.updateConfigJson);
|
||||
delete result.updateConfigJson;
|
||||
|
||||
assert(result.restoreConfigJson === null || typeof result.restoreConfigJson === 'string');
|
||||
result.restoreConfig = safe.JSON.parse(result.restoreConfigJson);
|
||||
delete result.restoreConfigJson;
|
||||
assert(result.reverseProxyConfigJson === null || typeof result.reverseProxyConfigJson === 'string');
|
||||
result.reverseProxyConfig = safe.JSON.parse(result.reverseProxyConfigJson) || {};
|
||||
delete result.reverseProxyConfigJson;
|
||||
|
||||
assert(result.hostPorts === null || typeof result.hostPorts === 'string');
|
||||
assert(result.environmentVariables === null || typeof result.environmentVariables === 'string');
|
||||
@@ -115,11 +86,9 @@ function postProcess(result) {
|
||||
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
|
||||
delete result.accessRestrictionJson;
|
||||
|
||||
// TODO remove later once all apps have this attribute
|
||||
result.xFrameOptions = result.xFrameOptions || 'SAMEORIGIN';
|
||||
|
||||
result.sso = !!result.sso; // make it bool
|
||||
result.enableBackup = !!result.enableBackup; // make it bool
|
||||
result.enableAutomaticUpdate = !!result.enableAutomaticUpdate; // make it bool
|
||||
|
||||
assert(result.debugModeJson === null || typeof result.debugModeJson === 'string');
|
||||
result.debugMode = safe.JSON.parse(result.debugModeJson);
|
||||
@@ -138,6 +107,11 @@ function postProcess(result) {
|
||||
for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty
|
||||
if (envNames[i]) result.env[envNames[i]] = envValues[i];
|
||||
}
|
||||
|
||||
result.error = safe.JSON.parse(result.errorJson);
|
||||
delete result.errorJson;
|
||||
|
||||
result.taskId = result.taskId ? String(result.taskId) : null;
|
||||
}
|
||||
|
||||
function get(id, callback) {
|
||||
@@ -152,11 +126,11 @@ function get(id, callback) {
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE apps.id = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
|
||||
@@ -179,11 +153,11 @@ function getByHttpPort(httpPort, callback) {
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE httpPort = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, httpPort ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
postProcess(result[0]);
|
||||
@@ -205,11 +179,11 @@ function getByContainerId(containerId, callback) {
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' WHERE containerId = ? GROUP BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY, containerId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE appId = ? AND type = ?', [ result[0].id, exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
result[0].alternateDomains = alternateDomains;
|
||||
postProcess(result[0]);
|
||||
@@ -230,10 +204,10 @@ function getAll(callback) {
|
||||
+ ' LEFT OUTER JOIN appEnvVars ON apps.id = appEnvVars.appId'
|
||||
+ ' LEFT OUTER JOIN subdomains ON apps.id = subdomains.appId AND subdomains.type = ?'
|
||||
+ ' GROUP BY apps.id ORDER BY apps.id', [ exports.SUBDOMAIN_TYPE_PRIMARY ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
database.query('SELECT ' + SUBDOMAIN_FIELDS + ' FROM subdomains WHERE type = ?', [ exports.SUBDOMAIN_TYPE_REDIRECT ], function (error, alternateDomains) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
alternateDomains.forEach(function (d) {
|
||||
var domain = results.find(function (a) { return d.appId === a.id; });
|
||||
@@ -250,14 +224,13 @@ function getAll(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(id, appStoreId, manifest, location, domain, ownerId, portBindings, data, callback) {
|
||||
function add(id, appStoreId, manifest, location, domain, portBindings, data, callback) {
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof appStoreId, 'string');
|
||||
assert(manifest && typeof manifest === 'object');
|
||||
assert.strictEqual(typeof manifest.version, 'string');
|
||||
assert.strictEqual(typeof location, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof portBindings, 'object');
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
@@ -266,23 +239,27 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
|
||||
var manifestJson = JSON.stringify(manifest);
|
||||
|
||||
var accessRestriction = data.accessRestriction || null;
|
||||
var accessRestrictionJson = JSON.stringify(accessRestriction);
|
||||
var memoryLimit = data.memoryLimit || 0;
|
||||
var xFrameOptions = data.xFrameOptions || '';
|
||||
var installationState = data.installationState || exports.ISTATE_PENDING_INSTALL;
|
||||
var restoreConfigJson = data.restoreConfig ? JSON.stringify(data.restoreConfig) : null; // used when cloning
|
||||
var sso = 'sso' in data ? data.sso : null;
|
||||
var robotsTxt = 'robotsTxt' in data ? data.robotsTxt : null;
|
||||
var debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
|
||||
var env = data.env || {};
|
||||
const accessRestriction = data.accessRestriction || null;
|
||||
const accessRestrictionJson = JSON.stringify(accessRestriction);
|
||||
const memoryLimit = data.memoryLimit || 0;
|
||||
const installationState = data.installationState;
|
||||
const runState = data.runState;
|
||||
const sso = 'sso' in data ? data.sso : null;
|
||||
const debugModeJson = data.debugMode ? JSON.stringify(data.debugMode) : null;
|
||||
const env = data.env || {};
|
||||
const label = data.label || null;
|
||||
const tagsJson = data.tags ? JSON.stringify(data.tags) : null;
|
||||
const mailboxName = data.mailboxName || null;
|
||||
const reverseProxyConfigJson = data.reverseProxyConfig ? JSON.stringify(data.reverseProxyConfig) : null;
|
||||
|
||||
var queries = [];
|
||||
|
||||
queries.push({
|
||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId) ' +
|
||||
' VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
args: [ id, appStoreId, manifestJson, installationState, accessRestrictionJson, memoryLimit, xFrameOptions, restoreConfigJson, sso, debugModeJson, robotsTxt, ownerId ]
|
||||
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 ]
|
||||
});
|
||||
|
||||
queries.push({
|
||||
@@ -304,14 +281,6 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
});
|
||||
});
|
||||
|
||||
// only allocate a mailbox if mailboxName is set
|
||||
if (data.mailboxName) {
|
||||
queries.push({
|
||||
query: 'INSERT INTO mailboxes (name, type, domain, ownerId, ownerType) VALUES (?, ?, ?, ?, ?)',
|
||||
args: [ data.mailboxName, mailboxdb.TYPE_MAILBOX, domain, id, mailboxdb.OWNER_TYPE_APP ]
|
||||
});
|
||||
}
|
||||
|
||||
if (data.alternateDomains) {
|
||||
data.alternateDomains.forEach(function (d) {
|
||||
queries.push({
|
||||
@@ -322,9 +291,9 @@ function add(id, appStoreId, manifest, location, domain, ownerId, portBindings,
|
||||
}
|
||||
|
||||
database.transaction(queries, function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'no such domain'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new BoxError(BoxError.NOT_FOUND, 'no such domain'));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -335,7 +304,7 @@ function exists(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT 1 FROM apps WHERE id=?', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
return callback(null, result.length !== 0);
|
||||
});
|
||||
@@ -346,7 +315,7 @@ function getPortBindings(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + PORT_BINDINGS_FIELDS + ' FROM appPortBindings WHERE appId = ?', [ id ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
var portBindings = { };
|
||||
for (var i = 0; i < results.length; i++) {
|
||||
@@ -363,8 +332,8 @@ function delPortBinding(hostPort, type, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM appPortBindings WHERE hostPort=? AND type=?', [ hostPort, type ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -376,15 +345,14 @@ function del(id, callback) {
|
||||
|
||||
var queries = [
|
||||
{ query: 'DELETE FROM subdomains WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM mailboxes WHERE ownerId=?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appPortBindings WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM appEnvVars WHERE appId = ?', args: [ id ] },
|
||||
{ query: 'DELETE FROM apps WHERE id = ?', args: [ id ] }
|
||||
];
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results[4].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
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'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -400,7 +368,7 @@ function clear(callback) {
|
||||
database.query.bind(null, 'DELETE FROM appEnvVars'),
|
||||
database.query.bind(null, 'DELETE FROM apps')
|
||||
], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
@@ -417,6 +385,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
|
||||
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
|
||||
assert(!('alternateDomains' in app) || Array.isArray(app.alternateDomains));
|
||||
assert(!('tags' in app) || Array.isArray(app.tags));
|
||||
assert(!('env' in app) || typeof app.env === 'object');
|
||||
|
||||
var queries = [ ];
|
||||
@@ -442,12 +411,8 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
if ('location' in app) {
|
||||
queries.push({ query: 'UPDATE subdomains SET subdomain = ? WHERE appId = ? AND type = ?', args: [ app.location, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
}
|
||||
|
||||
if ('domain' in app) {
|
||||
queries.push({ query: 'UPDATE subdomains SET domain = ? WHERE appId = ? AND type = ?', args: [ app.domain, id, exports.SUBDOMAIN_TYPE_PRIMARY ]});
|
||||
if ('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 ]});
|
||||
}
|
||||
|
||||
if ('alternateDomains' in app) {
|
||||
@@ -459,7 +424,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
|
||||
var fields = [ ], values = [ ];
|
||||
for (var p in app) {
|
||||
if (p === 'manifest' || p === 'oldConfig' || p === 'updateConfig' || p === 'restoreConfig' || p === 'accessRestriction' || p === 'debugMode') {
|
||||
if (p === 'manifest' || p === 'tags' || p === 'accessRestriction' || p === 'debugMode' || p === 'error' || p === 'reverseProxyConfig') {
|
||||
fields.push(`${p}Json = ?`);
|
||||
values.push(JSON.stringify(app[p]));
|
||||
} else if (p !== 'portBindings' && p !== 'location' && p !== 'domain' && p !== 'alternateDomains' && p !== 'env') {
|
||||
@@ -474,76 +439,45 @@ function updateWithConstraints(id, app, constraints, callback) {
|
||||
}
|
||||
|
||||
database.transaction(queries, function (error, results) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS, error.message));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results[results.length - 1].affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS, error.message));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (results[results.length - 1].affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
// not sure if health should influence runState
|
||||
function setHealth(appId, health, callback) {
|
||||
function setHealth(appId, health, healthTime, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof health, 'string');
|
||||
assert(util.isDate(healthTime));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var values = { health: health };
|
||||
var values = { health, healthTime };
|
||||
|
||||
var constraints = 'AND runState NOT LIKE "pending_%" AND installationState = "installed"';
|
||||
|
||||
updateWithConstraints(appId, values, constraints, callback);
|
||||
updateWithConstraints(appId, values, '', callback);
|
||||
}
|
||||
|
||||
function setInstallationCommand(appId, installationState, values, callback) {
|
||||
function setTask(appId, values, options, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof installationState, 'string');
|
||||
|
||||
if (typeof values === 'function') {
|
||||
callback = values;
|
||||
values = { };
|
||||
} else {
|
||||
assert.strictEqual(typeof values, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
}
|
||||
|
||||
values.installationState = installationState;
|
||||
values.installationProgress = '';
|
||||
|
||||
// Rules are:
|
||||
// uninstall is allowed in any state
|
||||
// force update is allowed in any state including pending_uninstall! (for better or worse)
|
||||
// restore is allowed from installed or error state or currently restoring
|
||||
// configure is allowed in installed state or currently configuring or in error state
|
||||
// update and backup are allowed only in installed state
|
||||
|
||||
if (installationState === exports.ISTATE_PENDING_UNINSTALL || installationState === exports.ISTATE_PENDING_FORCE_UPDATE) {
|
||||
updateWithConstraints(appId, values, '', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error" OR installationState = "pending_restore")', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_BACKUP) {
|
||||
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
|
||||
} else if (installationState === exports.ISTATE_PENDING_CONFIGURE) {
|
||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "pending_configure" OR installationState = "error")', callback);
|
||||
} else {
|
||||
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
|
||||
}
|
||||
}
|
||||
|
||||
function setRunCommand(appId, runState, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof runState, 'string');
|
||||
assert.strictEqual(typeof values, 'object');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var values = { runState: runState };
|
||||
updateWithConstraints(appId, values, 'AND runState NOT LIKE "pending_%" AND installationState = "installed"', callback);
|
||||
if (!options.requireNullTaskId) return updateWithConstraints(appId, values, '', callback);
|
||||
|
||||
if (options.requiredState === null) {
|
||||
updateWithConstraints(appId, values, 'AND taskId IS NULL', callback);
|
||||
} else {
|
||||
updateWithConstraints(appId, values, `AND taskId IS NULL AND installationState = "${options.requiredState}"`, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function getAppStoreIds(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT id, appStoreId FROM apps', function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
@@ -568,7 +502,7 @@ function setAddonConfig(appId, addonId, env, callback) {
|
||||
}
|
||||
|
||||
database.query(query + queryArgs.join(','), args, function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -581,7 +515,7 @@ function unsetAddonConfig(appId, addonId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -592,7 +526,7 @@ function unsetAddonConfigByAppId(appId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -604,7 +538,7 @@ function getAddonConfig(appId, addonId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ? AND addonId = ?', [ appId, addonId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
@@ -615,50 +549,36 @@ function getAddonConfigByAppId(appId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT name, value FROM appAddonConfigs WHERE appId = ?', [ appId ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
}
|
||||
|
||||
function getAddonConfigByName(appId, addonId, name, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
function getAppIdByAddonConfigValue(addonId, namePattern, value, callback) {
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
assert.strictEqual(typeof namePattern, 'string');
|
||||
assert.strictEqual(typeof value, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name = ?', [ appId, addonId, name ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
database.query('SELECT appId FROM appAddonConfigs WHERE addonId = ? AND name LIKE ? AND value = ?', [ addonId, namePattern, value ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
callback(null, results[0].appId);
|
||||
});
|
||||
}
|
||||
|
||||
function getAddonConfigByName(appId, addonId, namePattern, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof addonId, 'string');
|
||||
assert.strictEqual(typeof namePattern, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT value FROM appAddonConfigs WHERE appId = ? AND addonId = ? AND name LIKE ?', [ appId, addonId, namePattern ], function (error, results) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (results.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'App not found'));
|
||||
|
||||
callback(null, results[0].value);
|
||||
});
|
||||
}
|
||||
|
||||
function setOwner(appId, ownerId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof ownerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE apps SET ownerId=? WHERE appId=?', [ ownerId, appId ], function (error, results) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
if (results.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such app'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function transferOwnership(oldOwnerId, newOwnerId, callback) {
|
||||
assert.strictEqual(typeof oldOwnerId, 'string');
|
||||
assert.strictEqual(typeof newOwnerId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('UPDATE apps SET ownerId=? WHERE ownerId=?', [ newOwnerId, oldOwnerId ], function (error) {
|
||||
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') return callback(new DatabaseError(DatabaseError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ var appdb = require('./appdb.js'),
|
||||
apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
debug = require('debug')('box:apphealthmonitor'),
|
||||
docker = require('./docker.js').connection,
|
||||
mailer = require('./mailer.js'),
|
||||
docker = require('./docker.js'),
|
||||
eventlog = require('./eventlog.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util');
|
||||
|
||||
@@ -15,11 +17,11 @@ exports = module.exports = {
|
||||
run: run
|
||||
};
|
||||
|
||||
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
|
||||
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
|
||||
var gHealthInfo = { }; // { time, emailSent }
|
||||
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 NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||
const OOM_EVENT_LIMIT = 60 * 60 * 1000; // 60 minutes
|
||||
let gLastOomMailTime = Date.now() - (5 * 60 * 1000); // pretend we sent email 5 minutes ago
|
||||
|
||||
function debugApp(app) {
|
||||
assert(typeof app === 'object');
|
||||
@@ -32,28 +34,30 @@ function setHealth(app, health, callback) {
|
||||
assert.strictEqual(typeof health, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var now = new Date();
|
||||
let now = new Date(), healthTime = app.healthTime, curHealth = app.health;
|
||||
|
||||
if (!(app.id in gHealthInfo)) { // add new apps to list
|
||||
gHealthInfo[app.id] = { time: now, emailSent: false };
|
||||
}
|
||||
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 (health === appdb.HEALTH_HEALTHY) {
|
||||
gHealthInfo[app.id].time = now;
|
||||
} else if (Math.abs(now - gHealthInfo[app.id].time) > UNHEALTHY_THRESHOLD) {
|
||||
if (gHealthInfo[app.id].emailSent) return callback(null);
|
||||
// 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));
|
||||
|
||||
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
|
||||
|
||||
if (!app.debugMode) mailer.appDied(app); // do not send mails for dev apps
|
||||
gHealthInfo[app.id].emailSent = true;
|
||||
// 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 sometime to update the app health');
|
||||
debugApp(app, 'waiting for %s seconds to update the app health', (UNHEALTHY_THRESHOLD - Math.abs(now - healthTime))/1000);
|
||||
return callback(null);
|
||||
}
|
||||
|
||||
appdb.setHealth(app.id, health, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null); // app uninstalled?
|
||||
appdb.setHealth(app.id, health, healthTime, function (error) {
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(null); // app uninstalled?
|
||||
if (error) return callback(error);
|
||||
|
||||
app.health = health;
|
||||
@@ -68,63 +72,72 @@ function checkAppHealth(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.RSTATE_RUNNING) {
|
||||
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);
|
||||
}
|
||||
|
||||
var container = docker.getContainer(app.containerId),
|
||||
manifest = app.manifest;
|
||||
const manifest = app.manifest;
|
||||
|
||||
container.inspect(function (err, data) {
|
||||
if (err || !data || !data.State) {
|
||||
docker.inspect(app.containerId, function (error, data) {
|
||||
if (error || !data || !data.State) {
|
||||
debugApp(app, 'Error inspecting container');
|
||||
return setHealth(app, appdb.HEALTH_ERROR, callback);
|
||||
return setHealth(app, apps.HEALTH_ERROR, callback);
|
||||
}
|
||||
|
||||
if (data.State.Running !== true) {
|
||||
debugApp(app, 'exited');
|
||||
return setHealth(app, appdb.HEALTH_DEAD, callback);
|
||||
return setHealth(app, apps.HEALTH_DEAD, callback);
|
||||
}
|
||||
|
||||
// non-appstore apps may not have healthCheckPath
|
||||
if (!manifest.healthCheckPath) return setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
||||
if (!manifest.healthCheckPath) return setHealth(app, apps.HEALTH_HEALTHY, callback);
|
||||
|
||||
// poll through docker network instead of nginx to bypass any potential oauth proxy
|
||||
var healthCheckUrl = 'http://127.0.0.1:' + app.httpPort + manifest.healthCheckPath;
|
||||
superagent
|
||||
.get(healthCheckUrl)
|
||||
.set('Host', app.fqdn) // required for some apache configs with rewrite rules
|
||||
.set('User-Agent', 'Mozilla') // required for some apps (e.g. minio)
|
||||
.set('User-Agent', 'Mozilla (CloudronHealth)') // required for some apps (e.g. minio)
|
||||
.redirects(0)
|
||||
.timeout(HEALTHCHECK_INTERVAL)
|
||||
.end(function (error, res) {
|
||||
if (error && !error.response) {
|
||||
debugApp(app, 'not alive (network error): %s', error.message);
|
||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
|
||||
debugApp(app, 'not alive : %s', error || res.status);
|
||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||
setHealth(app, apps.HEALTH_UNHEALTHY, callback);
|
||||
} else {
|
||||
setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
||||
setHealth(app, apps.HEALTH_HEALTHY, callback);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getContainerInfo(containerId, callback) {
|
||||
docker.inspect(containerId, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const appId = safe.query(result, 'Config.Labels.appId', null);
|
||||
|
||||
if (!appId) return callback(null, null /* app */, { name: result.Name }); // addon
|
||||
|
||||
apps.get(appId, callback); // don't get by container id as this can be an exec container
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
OOM can be tested using stress tool like so:
|
||||
docker run -ti -m 100M cloudron/base:0.10.0 /bin/bash
|
||||
apt-get update && apt-get install stress
|
||||
stress --vm 1 --vm-bytes 200M --vm-hang 0
|
||||
*/
|
||||
function processDockerEvents(interval, callback) {
|
||||
assert.strictEqual(typeof interval, 'number');
|
||||
function processDockerEvents(intervalSecs, callback) {
|
||||
assert.strictEqual(typeof intervalSecs, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const OOM_MAIL_LIMIT = 60 * 60 * 1000; // 60 minutes
|
||||
let lastOomMailTime = new Date(new Date() - OOM_MAIL_LIMIT);
|
||||
const since = ((new Date().getTime() / 1000) - interval).toFixed(0);
|
||||
const since = ((new Date().getTime() / 1000) - intervalSecs).toFixed(0);
|
||||
const until = ((new Date().getTime() / 1000) - 1).toFixed(0);
|
||||
|
||||
docker.getEvents({ since: since, until: until, filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
|
||||
@@ -132,19 +145,22 @@ function processDockerEvents(interval, callback) {
|
||||
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('data', function (data) {
|
||||
var ev = JSON.parse(data);
|
||||
appdb.getByContainerId(ev.id, function (error, app) { // this can error for addons
|
||||
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
|
||||
var context = JSON.stringify(ev);
|
||||
var now = new Date();
|
||||
if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n';
|
||||
const event = JSON.parse(data);
|
||||
const containerId = String(event.id);
|
||||
|
||||
debug('OOM Context: %s', context);
|
||||
getContainerInfo(containerId, function (error, app, addon) {
|
||||
const program = error ? containerId : (app ? app.fqdn : addon.name);
|
||||
const now = Date.now();
|
||||
const notifyUser = !(app && app.debugMode) && ((now - gLastOomMailTime) > OOM_EVENT_LIMIT);
|
||||
|
||||
debug('OOM %s notifyUser: %s. lastOomTime: %s (now: %s)', program, notifyUser, gLastOomMailTime, now);
|
||||
|
||||
// do not send mails for dev apps
|
||||
if ((!app || !app.debugMode) && (now - lastOomMailTime > OOM_MAIL_LIMIT)) {
|
||||
mailer.oomEvent(program, context); // app can be null if it's an addon crash
|
||||
lastOomMailTime = now;
|
||||
if (notifyUser) {
|
||||
// app can be null for addon containers
|
||||
eventlog.add(eventlog.ACTION_APP_OOM, auditSource.HEALTH_MONITOR, { event: event, containerId: containerId, addon: addon || null, app: app || null });
|
||||
|
||||
gLastOomMailTime = now;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -171,7 +187,7 @@ function processApp(callback) {
|
||||
if (error) console.error(error);
|
||||
|
||||
var alive = result
|
||||
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
|
||||
.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);
|
||||
@@ -181,14 +197,13 @@ function processApp(callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function run(interval, callback) {
|
||||
assert.strictEqual(typeof interval, 'number');
|
||||
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
function run(intervalSecs, callback) {
|
||||
assert.strictEqual(typeof intervalSecs, 'number');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
processDockerEvents.bind(null, interval),
|
||||
processApp
|
||||
processApp, // this is first because docker.getEvents seems to get 'stuck' sometimes
|
||||
processDockerEvents.bind(null, intervalSecs)
|
||||
], function (error) {
|
||||
if (error) debug(error);
|
||||
|
||||
|
||||
1745
src/apps.js
1745
src/apps.js
File diff suppressed because it is too large
Load Diff
389
src/appstore.js
389
src/appstore.js
@@ -1,8 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
purchase: purchase,
|
||||
unpurchase: unpurchase,
|
||||
getApps: getApps,
|
||||
getApp: getApp,
|
||||
getAppVersion: getAppVersion,
|
||||
|
||||
registerWithLoginCredentials: registerWithLoginCredentials,
|
||||
registerWithLicense: registerWithLicense,
|
||||
|
||||
purchaseApp: purchaseApp,
|
||||
unpurchaseApp: unpurchaseApp,
|
||||
|
||||
getSubscription: getSubscription,
|
||||
isFreePlan: isFreePlan,
|
||||
@@ -12,80 +19,98 @@ exports = module.exports = {
|
||||
getAppUpdate: getAppUpdate,
|
||||
getBoxUpdate: getBoxUpdate,
|
||||
|
||||
getAccount: getAccount,
|
||||
|
||||
sendFeedback: sendFeedback,
|
||||
|
||||
AppstoreError: AppstoreError
|
||||
createTicket: createTicket
|
||||
};
|
||||
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
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'),
|
||||
safe = require('safetydance'),
|
||||
semver = require('semver'),
|
||||
settings = require('./settings.js'),
|
||||
superagent = require('superagent'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
|
||||
function AppstoreError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(AppstoreError, Error);
|
||||
AppstoreError.INTERNAL_ERROR = 'Internal Error';
|
||||
AppstoreError.EXTERNAL_ERROR = 'External Error';
|
||||
AppstoreError.NOT_FOUND = 'Internal Error';
|
||||
AppstoreError.BILLING_REQUIRED = 'Billing Required';
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function getAppstoreConfig(callback) {
|
||||
function getCloudronToken(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getAppstoreConfig(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
if (!result.token) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
settings.getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
if (!token) return callback(new BoxError(BoxError.LICENSE_ERROR, 'Missing token'));
|
||||
|
||||
callback(null, result);
|
||||
callback(null, token);
|
||||
});
|
||||
}
|
||||
|
||||
function login(email, password, totpToken, callback) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof totpToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = {
|
||||
email: email,
|
||||
password: password,
|
||||
totpToken: totpToken
|
||||
};
|
||||
|
||||
const url = settings.apiServerOrigin() + '/api/v1/login';
|
||||
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 === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `login status code: ${result.statusCode}`));
|
||||
|
||||
callback(null, result.body); // { userId, accessToken }
|
||||
});
|
||||
}
|
||||
|
||||
function registerUser(email, password, callback) {
|
||||
assert.strictEqual(typeof email, 'string');
|
||||
assert.strictEqual(typeof password, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var data = {
|
||||
email: email,
|
||||
password: password,
|
||||
};
|
||||
|
||||
const url = 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 !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `register status code: ${result.statusCode}`));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function getSubscription(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/subscription';
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'invalid appstore token'));
|
||||
if (result.statusCode === 403) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'wrong user'));
|
||||
if (result.statusCode === 502) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'stripe error'));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, 'unknown error'));
|
||||
const url = settings.apiServerOrigin() + '/api/v1/subscription';
|
||||
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR));
|
||||
if (result.statusCode === 502) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Stripe error: ${error.message}`));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unknown error: ${error.message}`));
|
||||
|
||||
callback(null, result.body.subscription);
|
||||
callback(null, result.body); // { email, subscription }
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -95,50 +120,53 @@ function isFreePlan(subscription) {
|
||||
}
|
||||
|
||||
// See app.js install it will create a db record first but remove it again if appstore purchase fails
|
||||
function purchase(appId, data, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
function purchaseApp(data, callback) {
|
||||
assert.strictEqual(typeof data, 'object'); // { appstoreId, manifestId, appId }
|
||||
assert(data.appstoreId || data.manifestId);
|
||||
assert.strictEqual(typeof data.appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps`;
|
||||
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode === 402) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED, result.body.message));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
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.message));
|
||||
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND)); // appstoreId does not exist
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 402) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
// 200 if already purchased, 201 is newly purchased
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App purchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function unpurchase(appId, data, callback) {
|
||||
function unpurchaseApp(appId, data, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof data, 'object'); // { appstoreId, manifestId }
|
||||
assert(data.appstoreId || data.manifestId);
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/apps/' + appId;
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/cloudronapps/${appId}`;
|
||||
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
superagent.get(url).query({ accessToken: token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 404) return callback(null); // was never purchased
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 201 && result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
superagent.del(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new AppstoreError(AppstoreError.BILLING_REQUIRED));
|
||||
if (result.statusCode !== 204) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
superagent.del(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 === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode !== 204) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App unpurchase failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -149,36 +177,50 @@ function unpurchase(appId, data, callback) {
|
||||
function sendAliveStatus(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var allSettings, allDomains, mailDomains, loginEvents;
|
||||
let allSettings, allDomains, mailDomains, loginEvents, userCount, groupCount;
|
||||
|
||||
async.series([
|
||||
function (callback) {
|
||||
settings.getAll(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(error);
|
||||
allSettings = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
domains.getAll(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(error);
|
||||
allDomains = result;
|
||||
callback();
|
||||
});
|
||||
},
|
||||
function (callback) {
|
||||
mail.getDomains(function (error, result) {
|
||||
if (error) return callback(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
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(new AppstoreError(AppstoreError.INTERNAL_ERROR, error));
|
||||
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);
|
||||
@@ -198,15 +240,18 @@ function sendAliveStatus(callback) {
|
||||
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: config.version(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
provider: config.provider(),
|
||||
version: constants.VERSION,
|
||||
adminFqdn: settings.adminFqdn(),
|
||||
provider: settings.provider(),
|
||||
backendSettings: backendSettings,
|
||||
machine: {
|
||||
cpus: os.cpus(),
|
||||
@@ -217,14 +262,16 @@ function sendAliveStatus(callback) {
|
||||
}
|
||||
};
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/alive';
|
||||
superagent.post(url).send(data).query({ accessToken: appstoreConfig.token }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 404) return callback(new AppstoreError(AppstoreError.NOT_FOUND));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Sending alive status failed. %s %j', result.status, result.body)));
|
||||
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);
|
||||
});
|
||||
@@ -235,23 +282,32 @@ function sendAliveStatus(callback) {
|
||||
function getBoxUpdate(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/boxupdate';
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/boxupdate`;
|
||||
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version() }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error.message));
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION }).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 === 204) return callback(null); // no update
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
var updateInfo = result.body;
|
||||
|
||||
if (!semver.valid(updateInfo.version) || semver.gt(config.version(), updateInfo.version)) {
|
||||
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
|
||||
if (!semver.valid(updateInfo.version) || semver.gt(constants.VERSION, updateInfo.version)) {
|
||||
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Invalid update version: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
|
||||
// updateInfo: { version, changelog, upgrade, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
|
||||
// updateInfo: { version, changelog, sourceTarballUrl, sourceTarballSigUrl, boxVersionsUrl, boxVersionsSigUrl }
|
||||
if (!updateInfo.version || typeof updateInfo.version !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.changelog || !Array.isArray(updateInfo.changelog)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad version): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.sourceTarballUrl || typeof updateInfo.sourceTarballUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.sourceTarballSigUrl || typeof updateInfo.sourceTarballSigUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad sourceTarballSigUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.boxVersionsUrl || typeof updateInfo.boxVersionsUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsUrl): %s %s', result.statusCode, result.text)));
|
||||
if (!updateInfo.boxVersionsSigUrl || typeof updateInfo.boxVersionsSigUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response (bad boxVersionsSigUrl): %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback(null, updateInfo);
|
||||
});
|
||||
});
|
||||
@@ -261,15 +317,17 @@ function getAppUpdate(app, callback) {
|
||||
assert.strictEqual(typeof app, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/appupdate';
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/appupdate`;
|
||||
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token, boxVersion: config.version(), appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
superagent.get(url).query({ accessToken: token, boxVersion: constants.VERSION, appId: app.appStoreId, appVersion: app.manifest.version }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error));
|
||||
if (result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode === 204) return callback(null); // no update
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 200 || !result.body) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
|
||||
const updateInfo = result.body;
|
||||
|
||||
@@ -279,7 +337,7 @@ function getAppUpdate(app, callback) {
|
||||
// do some sanity checks
|
||||
if (!safe.query(updateInfo, 'manifest.version') || semver.gt(curAppVersion, safe.query(updateInfo, 'manifest.version'))) {
|
||||
debug('Skipping malformed update of app %s version: %s. got %j', app.id, curAppVersion, updateInfo);
|
||||
return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
|
||||
return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Malformed update: %s %s', result.statusCode, result.text)));
|
||||
}
|
||||
|
||||
// { id, creationDate, manifest }
|
||||
@@ -288,25 +346,73 @@ function getAppUpdate(app, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function getAccount(callback) {
|
||||
function registerCloudron(data, callback) {
|
||||
assert.strictEqual(typeof data, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
if (error) return callback(error);
|
||||
const url = `${settings.apiServerOrigin()}/api/v1/register_cloudron`;
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId;
|
||||
superagent.post(url).send(data).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, `Unable to register cloudron: ${error.message}`));
|
||||
|
||||
superagent.get(url).query({ accessToken: appstoreConfig.token }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 200) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
// cloudronId, token, licenseKey
|
||||
if (!result.body.cloudronId) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no cloudron id'));
|
||||
if (!result.body.cloudronToken) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no token'));
|
||||
if (!result.body.licenseKey) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response - no license'));
|
||||
|
||||
// { profile: { id, email, groupId, billing, firstName, lastName, company, street, city, zip, state, country } }
|
||||
callback(null, result.body.profile);
|
||||
async.series([
|
||||
settings.setCloudronId.bind(null, result.body.cloudronId),
|
||||
settings.setCloudronToken.bind(null, result.body.cloudronToken),
|
||||
settings.setLicenseKey.bind(null, result.body.licenseKey),
|
||||
], function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug(`registerCloudron: Cloudron registered with id ${result.body.cloudronId}`);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendFeedback(info, callback) {
|
||||
function registerWithLicense(license, domain, callback) {
|
||||
assert.strictEqual(typeof license, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (token) return callback(new BoxError(BoxError.CONFLICT));
|
||||
|
||||
registerCloudron({ license, domain }, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function registerWithLoginCredentials(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
function maybeSignup(done) {
|
||||
if (!options.signup) return done();
|
||||
|
||||
registerUser(options.email, options.password, done);
|
||||
}
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (token) return callback(new BoxError(BoxError.CONFLICT));
|
||||
|
||||
maybeSignup(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
login(options.email, options.password, options.totpToken || '', function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
registerCloudron({ domain: settings.adminDomain(), accessToken: result.accessToken }, callback);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function createTicket(info, callback) {
|
||||
assert.strictEqual(typeof info, 'object');
|
||||
assert.strictEqual(typeof info.email, 'string');
|
||||
assert.strictEqual(typeof info.displayName, 'string');
|
||||
@@ -320,21 +426,78 @@ function sendFeedback(info, callback) {
|
||||
apps.get(info.appId, callback);
|
||||
}
|
||||
|
||||
getAppstoreConfig(function (error, appstoreConfig) {
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
collectAppInfoIfNeeded(function (error, result) {
|
||||
if (error) console.error('Unable to get app info', error);
|
||||
if (result) info.app = result;
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/users/' + appstoreConfig.userId + '/cloudrons/' + appstoreConfig.cloudronId + '/feedback';
|
||||
let url = settings.apiServerOrigin() + '/api/v1/ticket';
|
||||
|
||||
superagent.post(url).query({ accessToken: appstoreConfig.token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode !== 201) return callback(new AppstoreError(AppstoreError.EXTERNAL_ERROR, util.format('Bad response: %s %s', result.statusCode, result.text)));
|
||||
info.supportEmail = custom.spec().support.email; // destination address for tickets
|
||||
|
||||
superagent.post(url).query({ accessToken: token }).send(info).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new 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)));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getApps(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
settings.getUnstableAppsConfig(function (error, unstable) {
|
||||
if (error) return callback(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) {
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getAppVersion(appId, version, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof version, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCloudronToken(function (error, token) {
|
||||
if (error) return callback(error);
|
||||
|
||||
let url = `${settings.apiServerOrigin()}/api/v1/apps/${appId}`;
|
||||
if (version !== 'latest') url += `/versions/${version}`;
|
||||
|
||||
superagent.get(url).query({ accessToken: token }).timeout(10 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, error.message));
|
||||
if (result.statusCode === 403 || result.statusCode === 401) return callback(new BoxError(BoxError.INVALID_CREDENTIALS));
|
||||
if (result.statusCode === 404) return callback(new BoxError(BoxError.NOT_FOUND));
|
||||
if (result.statusCode === 422) return callback(new BoxError(BoxError.LICENSE_ERROR, result.body.message));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('App fetch failed. %s %j', result.status, result.body)));
|
||||
|
||||
callback(null, result.body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getApp(appId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getAppVersion(appId, 'latest', callback);
|
||||
}
|
||||
|
||||
1120
src/apptask.js
1120
src/apptask.js
File diff suppressed because it is too large
Load Diff
86
src/apptaskmanager.js
Normal file
86
src/apptaskmanager.js
Normal file
@@ -0,0 +1,86 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
scheduleTask: scheduleTask
|
||||
};
|
||||
|
||||
let assert = require('assert'),
|
||||
debug = require('debug')('box:apptaskmanager'),
|
||||
fs = require('fs'),
|
||||
locker = require('./locker.js'),
|
||||
safe = require('safetydance'),
|
||||
path = require('path'),
|
||||
paths = require('./paths.js'),
|
||||
tasks = require('./tasks.js');
|
||||
|
||||
let gActiveTasks = { }; // indexed by app id
|
||||
let gPendingTasks = [ ];
|
||||
let gInitialized = false;
|
||||
|
||||
const TASK_CONCURRENCY = 3;
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function waitText(lockOperation) {
|
||||
if (lockOperation === locker.OP_BOX_UPDATE) return 'Waiting for Cloudron to finish updating. See the Settings view';
|
||||
if (lockOperation === locker.OP_PLATFORM_START) return 'Waiting for Cloudron to initialize';
|
||||
if (lockOperation === locker.OP_FULL_BACKUP) return 'Wait for Cloudron to finish backup. See the Backups view';
|
||||
|
||||
return ''; // cannot happen
|
||||
}
|
||||
|
||||
function initializeSync() {
|
||||
gInitialized = true;
|
||||
locker.on('unlocked', startNextTask);
|
||||
}
|
||||
|
||||
// callback is called when task is finished
|
||||
function scheduleTask(appId, taskId, callback) {
|
||||
assert.strictEqual(typeof appId, 'string');
|
||||
assert.strictEqual(typeof taskId, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!gInitialized) initializeSync();
|
||||
|
||||
if (appId in gActiveTasks) {
|
||||
return callback(new Error(`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 });
|
||||
return;
|
||||
}
|
||||
|
||||
var lockError = locker.recursiveLock(locker.OP_APPTASK);
|
||||
|
||||
if (lockError) {
|
||||
debug(`Could not get lock. ${lockError.message}, queueing task id ${taskId}`);
|
||||
tasks.update(taskId, { percent: 0, message: waitText(lockError.operation) }, NOOP_CALLBACK);
|
||||
gPendingTasks.push({ appId, taskId, callback });
|
||||
return;
|
||||
}
|
||||
|
||||
gActiveTasks[appId] = {};
|
||||
|
||||
const logFile = path.join(paths.LOG_DIR, appId, 'apptask.log');
|
||||
|
||||
if (!fs.existsSync(path.dirname(logFile))) safe.fs.mkdirSync(path.dirname(logFile)); // ensure directory
|
||||
|
||||
tasks.startTask(taskId, { logFile, timeout: 20 * 60 * 60 * 1000 /* 20 hours */ }, function (error, result) {
|
||||
callback(error, result);
|
||||
|
||||
delete gActiveTasks[appId];
|
||||
locker.unlock(locker.OP_APPTASK); // unlock event will trigger next task
|
||||
});
|
||||
}
|
||||
|
||||
function startNextTask() {
|
||||
if (gPendingTasks.length === 0) return;
|
||||
|
||||
assert(Object.keys(gActiveTasks).length < TASK_CONCURRENCY);
|
||||
|
||||
const t = gPendingTasks.shift();
|
||||
scheduleTask(t.appId, t.taskId, t.callback);
|
||||
}
|
||||
|
||||
15
src/auditsource.js
Normal file
15
src/auditsource.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
CRON: { userId: null, username: 'cron' },
|
||||
HEALTH_MONITOR: { userId: null, username: 'healthmonitor' },
|
||||
APP_TASK: { userId: null, username: 'apptask' },
|
||||
EXTERNAL_LDAP_TASK: { userId: null, username: 'externalldap' },
|
||||
|
||||
fromRequest: fromRequest
|
||||
};
|
||||
|
||||
function fromRequest(req) {
|
||||
var ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress || null;
|
||||
return { ip: ip, username: req.user ? req.user.username : null, userId: req.user ? req.user.id : null };
|
||||
}
|
||||
@@ -12,8 +12,8 @@ exports = module.exports = {
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror');
|
||||
BoxError = require('./boxerror.js'),
|
||||
database = require('./database.js');
|
||||
|
||||
var AUTHCODES_FIELDS = [ 'authCode', 'userId', 'clientId', 'expiresAt' ].join(',');
|
||||
|
||||
@@ -22,8 +22,8 @@ function get(authCode, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + AUTHCODES_FIELDS + ' FROM authcodes WHERE authCode = ? AND expiresAt > ?', [ authCode, Date.now() ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
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]);
|
||||
});
|
||||
@@ -37,12 +37,12 @@ function add(authCode, clientId, userId, expiresAt, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('INSERT INTO authcodes (authCode, clientId, userId, expiresAt) VALUES (?, ?, ?, ?)',
|
||||
[ authCode, clientId, userId, expiresAt ], function (error, result) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error || result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
[ 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);
|
||||
});
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function del(authCode, callback) {
|
||||
@@ -50,8 +50,8 @@ function del(authCode, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM authcodes WHERE authCode = ?', [ authCode ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
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);
|
||||
});
|
||||
@@ -61,7 +61,7 @@ function delExpired(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM authcodes WHERE expiresAt <= ?', [ Date.now() ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
return callback(null, result.affectedRows);
|
||||
});
|
||||
}
|
||||
@@ -70,7 +70,7 @@ function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM authcodes', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror.js'),
|
||||
safe = require('safetydance'),
|
||||
util = require('util');
|
||||
|
||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format' ];
|
||||
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', 'manifestJson', 'format', 'preserveSecs' ];
|
||||
|
||||
exports = module.exports = {
|
||||
add: add,
|
||||
@@ -47,7 +47,7 @@ function getByTypeAndStatePaged(type, state, page, perPage, callback) {
|
||||
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||
[ type, state, (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
@@ -63,7 +63,7 @@ function getByTypePaged(type, page, perPage, callback) {
|
||||
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||
[ type, (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
@@ -80,7 +80,7 @@ function getByAppIdPaged(page, perPage, appId, callback) {
|
||||
// box versions (0.93.x and below) used to use appbackup_ prefix
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? AND id LIKE ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||
[ exports.BACKUP_TYPE_APP, exports.BACKUP_STATE_NORMAL, '%app%\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
results.forEach(function (result) { postProcess(result); });
|
||||
|
||||
@@ -94,8 +94,8 @@ function get(id, callback) {
|
||||
|
||||
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE id = ? ORDER BY creationTime DESC',
|
||||
[ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Backup not found'));
|
||||
|
||||
postProcess(result[0]);
|
||||
|
||||
@@ -103,24 +103,24 @@ function get(id, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
function add(backup, callback) {
|
||||
assert(backup && typeof backup === 'object');
|
||||
assert.strictEqual(typeof backup.id, 'string');
|
||||
assert.strictEqual(typeof backup.version, 'string');
|
||||
assert(backup.type === exports.BACKUP_TYPE_APP || backup.type === exports.BACKUP_TYPE_BOX);
|
||||
assert(util.isArray(backup.dependsOn));
|
||||
assert.strictEqual(typeof backup.manifest, 'object');
|
||||
assert.strictEqual(typeof backup.format, 'string');
|
||||
function add(id, data, callback) {
|
||||
assert(data && typeof data === 'object');
|
||||
assert.strictEqual(typeof id, 'string');
|
||||
assert.strictEqual(typeof data.version, 'string');
|
||||
assert(data.type === exports.BACKUP_TYPE_APP || data.type === exports.BACKUP_TYPE_BOX);
|
||||
assert(util.isArray(data.dependsOn));
|
||||
assert.strictEqual(typeof data.manifest, 'object');
|
||||
assert.strictEqual(typeof data.format, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var creationTime = backup.creationTime || new Date(); // allow tests to set the time
|
||||
var manifestJson = JSON.stringify(backup.manifest);
|
||||
var creationTime = data.creationTime || new Date(); // allow tests to set the time
|
||||
var manifestJson = JSON.stringify(data.manifest);
|
||||
|
||||
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn, manifestJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(','), manifestJson, backup.format ],
|
||||
[ id, data.version, data.type, creationTime, exports.BACKUP_STATE_NORMAL, data.dependsOn.join(','), manifestJson, data.format ],
|
||||
function (error) {
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error && error.code === 'ER_DUP_ENTRY') return callback(new BoxError(BoxError.ALREADY_EXISTS));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -139,8 +139,8 @@ function update(id, backup, callback) {
|
||||
values.push(id);
|
||||
|
||||
database.query('UPDATE backups SET ' + fields.join(', ') + ' WHERE id = ?', values, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(new BoxError(BoxError.NOT_FOUND, 'Backup not found'));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -150,7 +150,7 @@ function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('TRUNCATE TABLE backups', [], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
@@ -160,7 +160,7 @@ function del(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM backups WHERE id=?', [ id ], function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
943
src/backups.js
943
src/backups.js
File diff suppressed because it is too large
Load Diff
@@ -1,57 +0,0 @@
|
||||
#!/bin/bash
|
||||
':' //# comment; exec /usr/bin/env node --max_old_space_size=300 "$0" "$@"
|
||||
|
||||
// to understand the above hack read http://sambal.org/2014/02/passing-options-node-shebang-line/
|
||||
|
||||
'use strict';
|
||||
|
||||
if (process.argv[2] === '--check') return console.log('OK');
|
||||
|
||||
require('supererror')({ splatchError: true });
|
||||
|
||||
// remove timestamp from debug() based output
|
||||
require('debug').formatArgs = function formatArgs(args) {
|
||||
args[0] = this.namespace + ' ' + args[0];
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
backups = require('./backups.js'),
|
||||
database = require('./database.js'),
|
||||
debug = require('debug')('box:backuptask'),
|
||||
paths = require('./paths.js'),
|
||||
safe = require('safetydance');
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.initialize(callback);
|
||||
}
|
||||
|
||||
// Main process starts here
|
||||
var backupId = process.argv[2];
|
||||
var format = process.argv[3];
|
||||
var dataDir = process.argv[4];
|
||||
|
||||
debug(`Backing up ${dataDir} to ${backupId}`);
|
||||
|
||||
process.on('SIGTERM', function () {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
initialize(function (error) {
|
||||
if (error) throw error;
|
||||
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, '');
|
||||
|
||||
backups.upload(backupId, format, dataDir, function resultHandler(error) {
|
||||
if (error) debug('upload completed with error', error);
|
||||
|
||||
debug('upload completed');
|
||||
|
||||
safe.fs.writeFileSync(paths.BACKUP_RESULT_FILE, error ? error.message : '');
|
||||
|
||||
// https://nodejs.org/api/process.html are exit codes used by node. apps.js uses the value below
|
||||
// to check apptask crashes
|
||||
process.exit(error ? 50 : 0);
|
||||
});
|
||||
});
|
||||
95
src/boxerror.js
Normal file
95
src/boxerror.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/* jslint node:true */
|
||||
|
||||
'use strict';
|
||||
|
||||
const assert = require('assert'),
|
||||
HttpError = require('connect-lastmile').HttpError,
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
exports = module.exports = BoxError;
|
||||
|
||||
function BoxError(reason, errorOrMessage, details) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
assert(typeof details === 'object' || typeof details === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
this.details = details || {};
|
||||
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else { // error object
|
||||
this.message = errorOrMessage.message;
|
||||
this.nestedError = errorOrMessage;
|
||||
_.extend(this.details, errorOrMessage); // copy enumerable properies
|
||||
}
|
||||
}
|
||||
util.inherits(BoxError, Error);
|
||||
BoxError.ACCESS_DENIED = 'Access Denied';
|
||||
BoxError.ALREADY_EXISTS = 'Already Exists';
|
||||
BoxError.BAD_FIELD = 'Bad Field';
|
||||
BoxError.BAD_STATE = 'Bad State';
|
||||
BoxError.BUSY = 'Busy';
|
||||
BoxError.COLLECTD_ERROR = 'Collectd Error';
|
||||
BoxError.CONFLICT = 'Conflict';
|
||||
BoxError.CRYPTO_ERROR = 'Crypto Error';
|
||||
BoxError.DATABASE_ERROR = 'Database Error';
|
||||
BoxError.DNS_ERROR = 'DNS Error';
|
||||
BoxError.DOCKER_ERROR = 'Docker Error';
|
||||
BoxError.EXTERNAL_ERROR = 'External Error'; // use this for external API errors
|
||||
BoxError.FS_ERROR = 'FileSystem Error';
|
||||
BoxError.INACTIVE = 'Inactive';
|
||||
BoxError.INTERNAL_ERROR = 'Internal Error';
|
||||
BoxError.INVALID_CREDENTIALS = 'Invalid Credentials';
|
||||
BoxError.LICENSE_ERROR = 'License Error';
|
||||
BoxError.LOGROTATE_ERROR = 'Logrotate Error';
|
||||
BoxError.MAIL_ERROR = 'Mail Error';
|
||||
BoxError.NETWORK_ERROR = 'Network Error';
|
||||
BoxError.NGINX_ERROR = 'Nginx Error';
|
||||
BoxError.NOT_FOUND = 'Not found';
|
||||
BoxError.NOT_IMPLEMENTED = 'Not implemented';
|
||||
BoxError.NOT_SIGNED = 'Not Signed';
|
||||
BoxError.OPENSSL_ERROR = 'OpenSSL Error';
|
||||
BoxError.PLAN_LIMIT = 'Plan Limit';
|
||||
BoxError.TASK_ERROR = 'Task Error';
|
||||
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);
|
||||
};
|
||||
|
||||
// this is a class method for now in case error is not a BoxError
|
||||
BoxError.toHttpError = function (error) {
|
||||
switch (error.reason) {
|
||||
case BoxError.BAD_FIELD:
|
||||
return new HttpError(400, error);
|
||||
case BoxError.LICENSE_ERROR:
|
||||
return new HttpError(402, error);
|
||||
case BoxError.NOT_FOUND:
|
||||
return new HttpError(404, error);
|
||||
case BoxError.ALREADY_EXISTS:
|
||||
case BoxError.BAD_STATE:
|
||||
case BoxError.CONFLICT:
|
||||
return new HttpError(409, error);
|
||||
case BoxError.INVALID_CREDENTIALS:
|
||||
return new HttpError(412, error);
|
||||
case BoxError.EXTERNAL_ERROR:
|
||||
case BoxError.NETWORK_ERROR:
|
||||
case BoxError.FS_ERROR:
|
||||
case BoxError.MAIL_ERROR:
|
||||
case BoxError.DOCKER_ERROR:
|
||||
return new HttpError(424, error);
|
||||
case BoxError.DATABASE_ERROR:
|
||||
case BoxError.INTERNAL_ERROR:
|
||||
default:
|
||||
return new HttpError(500, error);
|
||||
}
|
||||
};
|
||||
266
src/caas.js
266
src/caas.js
@@ -1,266 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
verifySetupToken: verifySetupToken,
|
||||
setupDone: setupDone,
|
||||
|
||||
changePlan: changePlan,
|
||||
upgrade: upgrade,
|
||||
sendHeartbeat: sendHeartbeat,
|
||||
getBoxAndUserDetails: getBoxAndUserDetails,
|
||||
setPtrRecord: setPtrRecord,
|
||||
|
||||
CaasError: CaasError
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
backups = require('./backups.js'),
|
||||
config = require('./config.js'),
|
||||
debug = require('debug')('box:caas'),
|
||||
locker = require('./locker.js'),
|
||||
path = require('path'),
|
||||
progress = require('./progress.js'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
const RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
|
||||
|
||||
function CaasError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(CaasError, Error);
|
||||
CaasError.BAD_FIELD = 'Field error';
|
||||
CaasError.BAD_STATE = 'Bad state';
|
||||
CaasError.INVALID_TOKEN = 'Invalid Token';
|
||||
CaasError.INTERNAL_ERROR = 'Internal Error';
|
||||
CaasError.EXTERNAL_ERROR = 'External Error';
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function retire(reason, info, callback) {
|
||||
assert(reason === 'migrate' || reason === 'upgrade');
|
||||
info = info || { };
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
|
||||
var data = {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
adminFqdn: config.adminFqdn()
|
||||
};
|
||||
shell.sudo('retire', [ RETIRE_CMD, reason, JSON.stringify(info), JSON.stringify(data) ], callback);
|
||||
}
|
||||
|
||||
function getCaasConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCaasConfig(function (error, result) {
|
||||
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
|
||||
function verifySetupToken(setupToken, callback) {
|
||||
assert.strictEqual(typeof setupToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCaasConfig(function (error, caasConfig) {
|
||||
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
|
||||
|
||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/verify').query({ setupToken: setupToken })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
|
||||
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
|
||||
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setupDone(setupToken, callback) {
|
||||
assert.strictEqual(typeof setupToken, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getCaasConfig(function (error, caasConfig) {
|
||||
if (error) return callback(new CaasError(CaasError.INTERNAL_ERROR, error));
|
||||
|
||||
// Now let the api server know we got activated
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/setup/done').query({ setupToken: setupToken })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
if (result.statusCode === 403) return callback(new CaasError(CaasError.INVALID_TOKEN));
|
||||
if (result.statusCode === 409) return callback(new CaasError(CaasError.BAD_STATE, 'Already setup'));
|
||||
if (result.statusCode !== 201) return callback(new CaasError(CaasError.EXTERNAL_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
function doMigrate(options, caasConfig, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof caasConfig, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = locker.lock(locker.OP_MIGRATE);
|
||||
if (error) return callback(new CaasError(CaasError.BAD_STATE, error.message));
|
||||
|
||||
function unlock(error) {
|
||||
debug('Failed to migrate', error);
|
||||
locker.unlock(locker.OP_MIGRATE);
|
||||
progress.set(progress.MIGRATE, -1, 'Backup failed: ' + error.message);
|
||||
}
|
||||
|
||||
progress.set(progress.MIGRATE, 10, 'Backing up for migration');
|
||||
|
||||
// initiate the migration in the background
|
||||
backups.backupBoxAndApps({ userId: null, username: 'migrator' }, function (error) {
|
||||
if (error) return unlock(error);
|
||||
|
||||
debug('migrate: domain: %s size %s region %s', options.domain, options.size, options.region);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId + '/migrate')
|
||||
.query({ token: caasConfig.token })
|
||||
.send(options)
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return unlock(error); // network error
|
||||
if (result.statusCode === 409) return unlock(new CaasError(CaasError.BAD_STATE));
|
||||
if (result.statusCode === 404) return unlock(new CaasError(CaasError.NOT_FOUND));
|
||||
if (result.statusCode !== 202) return unlock(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.MIGRATE, 10, 'Migrating');
|
||||
|
||||
retire('migrate', _.pick(options, 'domain', 'size', 'region'));
|
||||
});
|
||||
});
|
||||
|
||||
callback(null);
|
||||
}
|
||||
|
||||
function changePlan(options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.isDemo()) return callback(new CaasError(CaasError.BAD_FIELD, 'Not allowed in demo mode'));
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
doMigrate(options, result, callback);
|
||||
});
|
||||
}
|
||||
|
||||
// this function expects a lock
|
||||
function upgrade(boxUpdateInfo, callback) {
|
||||
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
|
||||
|
||||
function upgradeError(e) {
|
||||
progress.set(progress.UPDATE, -1, e.message);
|
||||
callback(e);
|
||||
}
|
||||
|
||||
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
|
||||
|
||||
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
|
||||
if (error) return upgradeError(error);
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return upgradeError(error);
|
||||
|
||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/upgrade')
|
||||
.query({ token: result.token })
|
||||
.send({ version: boxUpdateInfo.version })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
|
||||
if (result.statusCode !== 202) return upgradeError(new Error(util.format('Server not ready to upgrade. statusCode: %s body: %j', result.status, result.body)));
|
||||
|
||||
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||
|
||||
// no need to unlock since this is the last thing we ever do on this box
|
||||
callback();
|
||||
|
||||
retire('upgrade');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendHeartbeat() {
|
||||
assert(config.provider() === 'caas', 'Heartbeat is only sent for managed cloudrons');
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return debug('Caas config missing', error);
|
||||
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/heartbeat';
|
||||
superagent.post(url).query({ token: result.token, version: config.version() }).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) debug('Network error sending heartbeat.', error);
|
||||
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
|
||||
else debug('Heartbeat sent to %s', url);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getBoxAndUserDetails(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (config.provider() !== 'caas') return callback(null, {});
|
||||
|
||||
getCaasConfig(function (error, caasConfig) {
|
||||
if (error) return callback(error);
|
||||
|
||||
superagent
|
||||
.get(config.apiServerOrigin() + '/api/v1/boxes/' + caasConfig.boxId)
|
||||
.query({ token: caasConfig.token })
|
||||
.timeout(30 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
|
||||
if (result.statusCode !== 200) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null, result.body);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setPtrRecord(domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
getCaasConfig(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
superagent
|
||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + result.boxId + '/ptr')
|
||||
.query({ token: result.token })
|
||||
.send({ domain: domain })
|
||||
.timeout(5 * 1000)
|
||||
.end(function (error, result) {
|
||||
if (error && !error.response) return callback(new CaasError(CaasError.EXTERNAL_ERROR, 'Cannot reach appstore'));
|
||||
if (result.statusCode !== 202) return callback(new CaasError(CaasError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,481 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme1'),
|
||||
execSync = require('safetydance').child_process.execSync,
|
||||
fs = require('fs'),
|
||||
parseLinks = require('parse-links'),
|
||||
path = require('path'),
|
||||
paths = require('../paths.js'),
|
||||
safe = require('safetydance'),
|
||||
superagent = require('superagent'),
|
||||
util = require('util'),
|
||||
_ = require('underscore');
|
||||
|
||||
var CA_PROD = 'https://acme-v01.api.letsencrypt.org',
|
||||
CA_STAGING = 'https://acme-staging.api.letsencrypt.org',
|
||||
LE_AGREEMENT = 'https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf';
|
||||
|
||||
exports = module.exports = {
|
||||
getCertificate: getCertificate,
|
||||
|
||||
// testing
|
||||
_name: 'acme'
|
||||
};
|
||||
|
||||
function Acme1Error(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(Acme1Error, Error);
|
||||
Acme1Error.INTERNAL_ERROR = 'Internal Error';
|
||||
Acme1Error.EXTERNAL_ERROR = 'External Error';
|
||||
Acme1Error.ALREADY_EXISTS = 'Already Exists';
|
||||
Acme1Error.NOT_COMPLETED = 'Not Completed';
|
||||
Acme1Error.FORBIDDEN = 'Forbidden';
|
||||
|
||||
// http://jose.readthedocs.org/en/latest/
|
||||
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
|
||||
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
|
||||
|
||||
function Acme1(options) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
|
||||
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
|
||||
this.accountKeyPem = null; // Buffer
|
||||
this.email = options.email;
|
||||
}
|
||||
|
||||
Acme1.prototype.getNonce = function (callback) {
|
||||
superagent.get(this.caOrigin + '/directory').timeout(30 * 1000).end(function (error, response) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching nonce : ' + response.statusCode));
|
||||
|
||||
return callback(null, response.headers['Replay-Nonce'.toLowerCase()]);
|
||||
});
|
||||
};
|
||||
|
||||
// urlsafe base64 encoding (jose)
|
||||
function urlBase64Encode(string) {
|
||||
return string.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||
}
|
||||
|
||||
function b64(str) {
|
||||
var buf = util.isBuffer(str) ? str : new Buffer(str);
|
||||
return urlBase64Encode(buf.toString('base64'));
|
||||
}
|
||||
|
||||
function getModulus(pem) {
|
||||
assert(util.isBuffer(pem));
|
||||
|
||||
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
|
||||
if (!stdout) return null;
|
||||
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
|
||||
if (!match) return null;
|
||||
return Buffer.from(match[1], 'hex');
|
||||
}
|
||||
|
||||
Acme1.prototype.sendSignedRequest = function (url, payload, callback) {
|
||||
assert.strictEqual(typeof url, 'string');
|
||||
assert.strictEqual(typeof payload, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
assert(util.isBuffer(this.accountKeyPem));
|
||||
|
||||
var that = this;
|
||||
var header = {
|
||||
alg: 'RS256',
|
||||
jwk: {
|
||||
e: b64(Buffer.from([0x01, 0x00, 0x01])), // exponent - 65537
|
||||
kty: 'RSA',
|
||||
n: b64(getModulus(this.accountKeyPem))
|
||||
}
|
||||
};
|
||||
|
||||
var payload64 = b64(payload);
|
||||
|
||||
this.getNonce(function (error, nonce) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('sendSignedRequest: using nonce %s for url %s', nonce, url);
|
||||
|
||||
var protected64 = b64(JSON.stringify(_.extend({ }, header, { nonce: nonce })));
|
||||
|
||||
var signer = crypto.createSign('RSA-SHA256');
|
||||
signer.update(protected64 + '.' + payload64, 'utf8');
|
||||
var signature64 = urlBase64Encode(signer.sign(that.accountKeyPem, 'base64'));
|
||||
|
||||
var data = {
|
||||
header: header,
|
||||
protected: protected64,
|
||||
payload: payload64,
|
||||
signature: signature64
|
||||
};
|
||||
|
||||
superagent.post(url).set('Content-Type', 'application/x-www-form-urlencoded').send(JSON.stringify(data)).timeout(30 * 1000).end(function (error, res) {
|
||||
if (error && !error.response) return callback(error); // network errors
|
||||
|
||||
callback(null, res);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.updateContact = function (registrationUri, callback) {
|
||||
assert.strictEqual(typeof registrationUri, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('updateContact: %s %s', registrationUri, this.email);
|
||||
|
||||
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||
var payload = {
|
||||
resource: 'reg',
|
||||
contact: [ 'mailto:' + this.email ],
|
||||
agreement: LE_AGREEMENT
|
||||
};
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('updateContact: contact of user updated to %s', that.email);
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.registerUser = function (callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
resource: 'new-reg',
|
||||
contact: [ 'mailto:' + this.email ],
|
||||
agreement: LE_AGREEMENT
|
||||
};
|
||||
|
||||
debug('registerUser: %s', this.email);
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-reg', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
|
||||
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('registerUser: registered user %s', that.email);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.registerDomain = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var payload = {
|
||||
resource: 'new-authz',
|
||||
identifier: {
|
||||
type: 'dns',
|
||||
value: domain
|
||||
}
|
||||
};
|
||||
|
||||
debug('registerDomain: %s', domain);
|
||||
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-authz', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new Acme1Error(Acme1Error.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug('registerDomain: registered %s', domain);
|
||||
|
||||
callback(null, result.body);
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.prepareHttpChallenge = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
|
||||
|
||||
var token = challenge.token;
|
||||
|
||||
assert(util.isBuffer(this.accountKeyPem));
|
||||
|
||||
var jwk = {
|
||||
e: b64(Buffer.from([0x01, 0x00, 0x01])), // Exponent - 65537
|
||||
kty: 'RSA',
|
||||
n: b64(getModulus(this.accountKeyPem))
|
||||
};
|
||||
|
||||
var shasum = crypto.createHash('sha256');
|
||||
shasum.update(JSON.stringify(jwk));
|
||||
var thumbprint = urlBase64Encode(shasum.digest('base64'));
|
||||
var keyAuthorization = token + '.' + thumbprint;
|
||||
|
||||
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, token));
|
||||
|
||||
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, token), token + '.' + thumbprint, function (error) {
|
||||
if (error) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, error));
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.notifyChallengeReady = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('notifyChallengeReady: %s was met', challenge.uri);
|
||||
|
||||
var keyAuthorization = fs.readFileSync(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), 'utf8');
|
||||
|
||||
var payload = {
|
||||
resource: 'challenge',
|
||||
keyAuthorization: keyAuthorization
|
||||
};
|
||||
|
||||
this.sendSignedRequest(challenge.uri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 202) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.waitForChallenge = function (challenge, callback) {
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('waitingForChallenge: %j', challenge);
|
||||
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
debug('waitingForChallenge: getting status');
|
||||
|
||||
superagent.get(challenge.uri).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) {
|
||||
debug('waitForChallenge: network error getting uri %s', challenge.uri);
|
||||
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, error.message)); // network error
|
||||
}
|
||||
if (result.statusCode !== 202) {
|
||||
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending') return retryCallback(new Acme1Error(Acme1Error.NOT_COMPLETED));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
else return retryCallback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
});
|
||||
}, function retryFinished(error) {
|
||||
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
// https://community.letsencrypt.org/t/public-beta-rate-limits/4772 for rate limits
|
||||
Acme1.prototype.signCertificate = function (domain, csrDer, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert(util.isBuffer(csrDer));
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
|
||||
var payload = {
|
||||
resource: 'new-cert',
|
||||
csr: b64(csrDer)
|
||||
};
|
||||
|
||||
debug('signCertificate: sending new-cert request');
|
||||
|
||||
this.sendSignedRequest(this.caOrigin + '/acme/new-cert', JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||
// 429 means we reached the cert limit for this domain
|
||||
if (result.statusCode !== 201) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var certUrl = result.headers.location;
|
||||
|
||||
if (!certUrl) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Missing location in downloadCertificate'));
|
||||
|
||||
safe.fs.writeFileSync(path.join(outdir, domain + '.url'), certUrl, 'utf8'); // maybe use for renewal
|
||||
|
||||
return callback(null, result.headers.location);
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.createKeyAndCsr = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
var csrFile = path.join(outdir, domain + '.csr');
|
||||
var privateKeyFile = path.join(outdir, domain + '.key');
|
||||
|
||||
if (safe.fs.existsSync(privateKeyFile)) {
|
||||
// in some old releases, csr file was corrupt. so always regenerate it
|
||||
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
|
||||
} else {
|
||||
var key = execSync('openssl genrsa 4096');
|
||||
if (!key) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
|
||||
}
|
||||
|
||||
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
|
||||
if (!csrDer) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error)); // bookkeeping
|
||||
|
||||
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
|
||||
|
||||
callback(null, csrDer);
|
||||
};
|
||||
|
||||
// TODO: download the chain in a loop following 'up' header
|
||||
Acme1.prototype.downloadChain = function (linkHeader, callback) {
|
||||
if (!linkHeader) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
|
||||
|
||||
debug('downloadChain: linkHeader %s', linkHeader);
|
||||
|
||||
var linkInfo = parseLinks(linkHeader);
|
||||
if (!linkInfo || !linkInfo.up) return new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
|
||||
|
||||
var intermediateCertUrl = linkInfo.up.startsWith('https://') ? linkInfo.up : (this.caOrigin + linkInfo.up);
|
||||
|
||||
debug('downloadChain: downloading from %s', intermediateCertUrl);
|
||||
|
||||
superagent.get(intermediateCertUrl).buffer().parse(function (res, done) {
|
||||
var data = [ ];
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
}).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var chainDer = result.text;
|
||||
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
|
||||
if (!chainPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
callback(null, chainPem);
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.downloadCertificate = function (domain, certUrl, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof certUrl, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
var that = this;
|
||||
|
||||
superagent.get(certUrl).buffer().parse(function (res, done) {
|
||||
var data = [ ];
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
}).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode === 202) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, 'Retry not implemented yet'));
|
||||
if (result.statusCode !== 200) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
var certificateDer = result.text;
|
||||
|
||||
safe.fs.writeFileSync(path.join(outdir, domain + '.der'), certificateDer);
|
||||
debug('downloadCertificate: cert der file for %s saved', domain);
|
||||
|
||||
var certificatePem = execSync('openssl x509 -inform DER -outform PEM', { input: certificateDer }); // this is really just base64 encoding with header
|
||||
if (!certificatePem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
that.downloadChain(result.header['link'], function (error, chainPem) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var certificateFile = path.join(outdir, domain + '.cert');
|
||||
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.acmeFlow = function (domain, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
|
||||
debug('getCertificate: generating acme account key on first run');
|
||||
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!this.accountKeyPem) return callback(new Acme1Error(Acme1Error.INTERNAL_ERROR, safe.error));
|
||||
|
||||
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
|
||||
} else {
|
||||
debug('getCertificate: using existing acme account key');
|
||||
this.accountKeyPem = fs.readFileSync(paths.ACME_ACCOUNT_KEY_FILE);
|
||||
}
|
||||
|
||||
var that = this;
|
||||
this.registerUser(function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
that.registerDomain(domain, function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
debug('acmeFlow: challenges: %j', result);
|
||||
|
||||
var httpChallenges = result.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
if (httpChallenges.length === 0) return callback(new Acme1Error(Acme1Error.EXTERNAL_ERROR, 'no http challenges'));
|
||||
var challenge = httpChallenges[0];
|
||||
|
||||
async.waterfall([
|
||||
that.prepareHttpChallenge.bind(that, challenge),
|
||||
that.notifyChallengeReady.bind(that, challenge),
|
||||
that.waitForChallenge.bind(that, challenge),
|
||||
that.createKeyAndCsr.bind(that, domain),
|
||||
that.signCertificate.bind(that, domain),
|
||||
that.downloadCertificate.bind(that, domain)
|
||||
], callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Acme1.prototype.getCertificate = function (hostname, domain, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('getCertificate: start acme flow for %s from %s', hostname, this.caOrigin);
|
||||
this.acmeFlow(hostname, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
callback(null, path.join(outdir, hostname + '.cert'), path.join(outdir, hostname + '.key'));
|
||||
});
|
||||
};
|
||||
|
||||
function getCertificate(hostname, domain, options, callback) {
|
||||
assert.strictEqual(typeof hostname, 'string');
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var acme = new Acme1(options || { });
|
||||
acme.getCertificate(hostname, domain, callback);
|
||||
}
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('../boxerror.js'),
|
||||
crypto = require('crypto'),
|
||||
debug = require('debug')('box:cert/acme2'),
|
||||
domains = require('../domains.js'),
|
||||
execSync = require('safetydance').child_process.execSync,
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
paths = require('../paths.js'),
|
||||
@@ -25,31 +25,6 @@ exports = module.exports = {
|
||||
_getChallengeSubdomain: getChallengeSubdomain
|
||||
};
|
||||
|
||||
function Acme2Error(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(Acme2Error, Error);
|
||||
Acme2Error.INTERNAL_ERROR = 'Internal Error';
|
||||
Acme2Error.EXTERNAL_ERROR = 'External Error';
|
||||
Acme2Error.ALREADY_EXISTS = 'Already Exists';
|
||||
Acme2Error.NOT_COMPLETED = 'Not Completed';
|
||||
Acme2Error.FORBIDDEN = 'Forbidden';
|
||||
|
||||
// http://jose.readthedocs.org/en/latest/
|
||||
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
|
||||
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
|
||||
@@ -81,14 +56,14 @@ function urlBase64Encode(string) {
|
||||
}
|
||||
|
||||
function b64(str) {
|
||||
var buf = util.isBuffer(str) ? str : new Buffer(str);
|
||||
var buf = util.isBuffer(str) ? str : Buffer.from(str);
|
||||
return urlBase64Encode(buf.toString('base64'));
|
||||
}
|
||||
|
||||
function getModulus(pem) {
|
||||
assert(util.isBuffer(pem));
|
||||
|
||||
var stdout = execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
|
||||
var stdout = safe.child_process.execSync('openssl rsa -modulus -noout', { input: pem, encoding: 'utf8' });
|
||||
if (!stdout) return null;
|
||||
var match = stdout.match(/Modulus=([0-9a-fA-F]+)$/m);
|
||||
if (!match) return null;
|
||||
@@ -159,8 +134,8 @@ Acme2.prototype.updateContact = function (registrationUri, callback) {
|
||||
|
||||
const that = this;
|
||||
this.sendSignedRequest(registrationUri, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(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)));
|
||||
|
||||
debug(`updateContact: contact of user updated to ${that.email}`);
|
||||
|
||||
@@ -179,9 +154,9 @@ Acme2.prototype.registerUser = function (callback) {
|
||||
|
||||
var that = this;
|
||||
this.sendSignedRequest(this.directory.newAccount, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering new account: ' + error.message));
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when registering user: ${error.message}`));
|
||||
// 200 if already exists. 201 for new accounts
|
||||
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 200 && result.statusCode !== 201) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to register new account. Expecting 200 or 201, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
debug(`registerUser: user registered keyid: ${result.headers.location}`);
|
||||
|
||||
@@ -205,17 +180,17 @@ Acme2.prototype.newOrder = function (domain, callback) {
|
||||
debug('newOrder: %s', domain);
|
||||
|
||||
this.sendSignedRequest(this.directory.newOrder, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||
if (result.statusCode === 403) return callback(new Acme2Error(Acme2Error.FORBIDDEN, result.body.detail));
|
||||
if (result.statusCode !== 201) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(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)));
|
||||
|
||||
debug('newOrder: created order %s %j', domain, result.body);
|
||||
|
||||
const order = result.body, orderUrl = result.headers.location;
|
||||
|
||||
if (!Array.isArray(order.authorizations)) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid authorizations in order'));
|
||||
if (typeof order.finalize !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid finalize in order'));
|
||||
if (typeof orderUrl !== 'string') return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'invalid order location in order header'));
|
||||
if (!Array.isArray(order.authorizations)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid authorizations in order'));
|
||||
if (typeof order.finalize !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid finalize in order'));
|
||||
if (typeof orderUrl !== 'string') return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'invalid order location in order header'));
|
||||
|
||||
callback(null, order, orderUrl);
|
||||
});
|
||||
@@ -227,24 +202,24 @@ Acme2.prototype.waitForOrder = function (orderUrl, callback) {
|
||||
|
||||
debug(`waitForOrder: ${orderUrl}`);
|
||||
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
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) {
|
||||
debug('waitForOrder: network error getting uri %s', orderUrl);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
|
||||
return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error waiting for order: ${error.message}`)); // network error
|
||||
}
|
||||
if (result.statusCode !== 200) {
|
||||
debug('waitForOrder: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForOrder: status is "%s %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
|
||||
if (result.body.status === 'pending' || result.body.status === 'processing') return retryCallback(new BoxError(BoxError.TRY_AGAIN, `Request is in ${result.body.status} state`));
|
||||
else if (result.body.status === 'valid' && result.body.certificate) return retryCallback(null, result.body.certificate);
|
||||
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
|
||||
else return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unexpected status or invalid response: ' + result.body));
|
||||
});
|
||||
}, callback);
|
||||
};
|
||||
@@ -278,8 +253,8 @@ Acme2.prototype.notifyChallengeReady = function (challenge, callback) {
|
||||
};
|
||||
|
||||
this.sendSignedRequest(challenge.url, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
if (error) return callback(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)));
|
||||
|
||||
callback();
|
||||
});
|
||||
@@ -291,24 +266,24 @@ Acme2.prototype.waitForChallenge = function (challenge, callback) {
|
||||
|
||||
debug('waitingForChallenge: %j', challenge);
|
||||
|
||||
async.retry({ times: 10, interval: 5000 }, function (retryCallback) {
|
||||
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) {
|
||||
debug('waitForChallenge: network error getting uri %s', challenge.url);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message)); // network error
|
||||
return retryCallback(new BoxError(BoxError.NETWORK_ERROR, `Network error waiting for challenge: ${error.message}`));
|
||||
}
|
||||
if (result.statusCode !== 200) {
|
||||
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
|
||||
return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||
}
|
||||
|
||||
debug('waitForChallenge: status is "%s %j', result.body.status, result.body);
|
||||
|
||||
if (result.body.status === 'pending') return retryCallback(new Acme2Error(Acme2Error.NOT_COMPLETED));
|
||||
if (result.body.status === 'pending') return retryCallback(new BoxError(BoxError.TRY_AGAIN));
|
||||
else if (result.body.status === 'valid') return retryCallback();
|
||||
else return retryCallback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
else return retryCallback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unexpected status: ' + result.body.status));
|
||||
});
|
||||
}, function retryFinished(error) {
|
||||
// async.retry will pass 'undefined' as second arg making it unusable with async.waterfall()
|
||||
@@ -330,9 +305,9 @@ Acme2.prototype.signCertificate = function (domain, finalizationUrl, csrDer, cal
|
||||
debug('signCertificate: sending sign request');
|
||||
|
||||
this.sendSignedRequest(finalizationUrl, JSON.stringify(payload), function (error, result) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||
if (error) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when signing certificate: ${error.message}`));
|
||||
// 429 means we reached the cert limit for this domain
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
if (result.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
|
||||
return callback(null);
|
||||
});
|
||||
@@ -351,16 +326,16 @@ Acme2.prototype.createKeyAndCsr = function (hostname, callback) {
|
||||
// in some old releases, csr file was corrupt. so always regenerate it
|
||||
debug('createKeyAndCsr: reuse the key for renewal at %s', privateKeyFile);
|
||||
} else {
|
||||
var key = execSync('openssl genrsa 4096');
|
||||
if (!key) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
var key = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!key) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new BoxError(BoxError.FS_ERROR, safe.error));
|
||||
|
||||
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
|
||||
}
|
||||
|
||||
var csrDer = execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
|
||||
if (!csrDer) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error)); // bookkeeping
|
||||
var csrDer = safe.child_process.execSync(`openssl req -new -key ${privateKeyFile} -outform DER -subj /CN=${hostname}`);
|
||||
if (!csrDer) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
|
||||
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new BoxError(BoxError.FS_ERROR, safe.error)); // bookkeeping
|
||||
|
||||
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
|
||||
|
||||
@@ -374,25 +349,29 @@ Acme2.prototype.downloadCertificate = function (hostname, certUrl, callback) {
|
||||
|
||||
var outdir = paths.APP_CERTS_DIR;
|
||||
|
||||
superagent.get(certUrl).buffer().parse(function (res, done) {
|
||||
var data = [ ];
|
||||
res.on('data', function(chunk) { data.push(chunk); });
|
||||
res.on('end', function () { res.text = Buffer.concat(data); done(); });
|
||||
}).timeout(30 * 1000).end(function (error, result) {
|
||||
if (error && !error.response) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||
if (result.statusCode === 202) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, 'Retry not implemented yet'));
|
||||
if (result.statusCode !== 200) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||
async.retry({ times: 5, interval: 20000 }, function (retryCallback) {
|
||||
debug('downloadCertificate: downloading certificate');
|
||||
|
||||
const fullChainPem = result.text;
|
||||
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)));
|
||||
|
||||
const certName = hostname.replace('*.', '_.');
|
||||
var certificateFile = path.join(outdir, `${certName}.cert`);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
const fullChainPem = result.text;
|
||||
|
||||
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
|
||||
const certName = hostname.replace('*.', '_.');
|
||||
var certificateFile = path.join(outdir, `${certName}.cert`);
|
||||
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return retryCallback(new BoxError(BoxError.FS_ERROR, safe.error));
|
||||
|
||||
callback();
|
||||
});
|
||||
debug('downloadCertificate: cert file for %s saved at %s', hostname, certificateFile);
|
||||
|
||||
retryCallback(null);
|
||||
});
|
||||
}, callback);
|
||||
};
|
||||
|
||||
Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization, callback) {
|
||||
@@ -403,7 +382,7 @@ Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization
|
||||
|
||||
debug('acmeFlow: challenges: %j', authorization);
|
||||
let httpChallenges = authorization.challenges.filter(function(x) { return x.type === 'http-01'; });
|
||||
if (httpChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no http challenges'));
|
||||
if (httpChallenges.length === 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'no http challenges'));
|
||||
let challenge = httpChallenges[0];
|
||||
|
||||
debug('prepareHttpChallenge: preparing for challenge %j', challenge);
|
||||
@@ -413,7 +392,7 @@ Acme2.prototype.prepareHttpChallenge = function (hostname, domain, authorization
|
||||
debug('prepareHttpChallenge: writing %s to %s', keyAuthorization, path.join(paths.ACME_CHALLENGES_DIR, challenge.token));
|
||||
|
||||
fs.writeFile(path.join(paths.ACME_CHALLENGES_DIR, challenge.token), keyAuthorization, function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.FS_ERROR, error));
|
||||
|
||||
callback(null, challenge);
|
||||
});
|
||||
@@ -455,7 +434,7 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
|
||||
|
||||
debug('acmeFlow: challenges: %j', authorization);
|
||||
let dnsChallenges = authorization.challenges.filter(function(x) { return x.type === 'dns-01'; });
|
||||
if (dnsChallenges.length === 0) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, 'no dns challenges'));
|
||||
if (dnsChallenges.length === 0) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'no dns challenges'));
|
||||
let challenge = dnsChallenges[0];
|
||||
|
||||
const keyAuthorization = this.getKeyAuthorization(challenge.token);
|
||||
@@ -468,10 +447,10 @@ Acme2.prototype.prepareDnsChallenge = function (hostname, domain, authorization,
|
||||
debug(`prepareDnsChallenge: update ${challengeSubdomain} with ${txtValue}`);
|
||||
|
||||
domains.upsertDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(error);
|
||||
|
||||
domains.waitForDnsRecord(challengeSubdomain, domain, 'TXT', txtValue, { interval: 5000, times: 200 }, function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error.message));
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, challenge);
|
||||
});
|
||||
@@ -494,7 +473,7 @@ Acme2.prototype.cleanupDnsChallenge = function (hostname, domain, challenge, cal
|
||||
debug(`cleanupDnsChallenge: remove ${challengeSubdomain} with ${txtValue}`);
|
||||
|
||||
domains.removeDnsRecords(challengeSubdomain, domain, 'TXT', [ `"${txtValue}"` ], function (error) {
|
||||
if (error) return callback(new Acme2Error(Acme2Error.EXTERNAL_ERROR, error));
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -506,10 +485,12 @@ Acme2.prototype.prepareChallenge = function (hostname, domain, authorizationUrl,
|
||||
assert.strictEqual(typeof authorizationUrl, 'string');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`prepareChallenge: http: ${this.performHttpAuthorization}`);
|
||||
|
||||
const that = this;
|
||||
superagent.get(authorizationUrl).timeout(30 * 1000).end(function (error, response) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (response.statusCode !== 200) return callback(new Error('Invalid response code getting authorization : ' + response.statusCode));
|
||||
if (error && !error.response) return callback(new BoxError(BoxError.NETWORK_ERROR, `Network error when preparing challenge: ${error.message}`));
|
||||
if (response.statusCode !== 200) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Invalid response code getting authorization : ' + response.statusCode));
|
||||
|
||||
const authorization = response.body;
|
||||
|
||||
@@ -527,6 +508,8 @@ Acme2.prototype.cleanupChallenge = function (hostname, domain, challenge, callba
|
||||
assert.strictEqual(typeof challenge, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`cleanupChallenge: http: ${this.performHttpAuthorization}`);
|
||||
|
||||
if (this.performHttpAuthorization) {
|
||||
this.cleanupHttpChallenge(hostname, domain, challenge, callback);
|
||||
} else {
|
||||
@@ -542,7 +525,7 @@ Acme2.prototype.acmeFlow = function (hostname, domain, callback) {
|
||||
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
|
||||
debug('getCertificate: generating acme account key on first run');
|
||||
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
|
||||
if (!this.accountKeyPem) return callback(new Acme2Error(Acme2Error.INTERNAL_ERROR, safe.error));
|
||||
if (!this.accountKeyPem) return callback(new BoxError(BoxError.OPENSSL_ERROR, safe.error));
|
||||
|
||||
safe.fs.writeFileSync(paths.ACME_ACCOUNT_KEY_FILE, this.accountKeyPem);
|
||||
} else {
|
||||
@@ -587,8 +570,8 @@ Acme2.prototype.getDirectory = function (callback) {
|
||||
const that = this;
|
||||
|
||||
superagent.get(this.caDirectory).timeout(30 * 1000).end(function (error, response) {
|
||||
if (error && !error.response) return callback(error);
|
||||
if (response.statusCode !== 200) return callback(new Error('Invalid response code when fetching directory : ' + response.statusCode));
|
||||
if (error && !error.response) 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' ||
|
||||
@@ -632,6 +615,11 @@ function getCertificate(hostname, domain, options, callback) {
|
||||
assert.strictEqual(typeof options, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var acme = new Acme2(options || { });
|
||||
acme.getCertificate(hostname, domain, callback);
|
||||
let attempt = 1;
|
||||
async.retry({ times: 3, interval: 0 }, function (retryCallback) {
|
||||
debug(`getCertificate: attempt ${attempt++}`);
|
||||
|
||||
let acme = new Acme2(options || { });
|
||||
acme.getCertificate(hostname, domain, retryCallback);
|
||||
}, callback);
|
||||
}
|
||||
|
||||
38
src/changelog.js
Normal file
38
src/changelog.js
Normal file
@@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
let assert = require('assert'),
|
||||
fs = require('fs'),
|
||||
path = require('path');
|
||||
|
||||
exports = module.exports = {
|
||||
getChanges: getChanges
|
||||
};
|
||||
|
||||
function getChanges(version) {
|
||||
assert.strictEqual(typeof version, 'string');
|
||||
|
||||
let changelog = [ ];
|
||||
const lines = fs.readFileSync(path.join(__dirname, '../CHANGES'), 'utf8').split('\n');
|
||||
|
||||
version = version.replace(/[+-].*/, ''); // strip prerelease
|
||||
|
||||
let i;
|
||||
for (i = 0; i < lines.length; i++) {
|
||||
if (lines[i] === '[' + version + ']') break;
|
||||
}
|
||||
|
||||
for (i = i + 1; i < lines.length; i++) {
|
||||
if (lines[i] === '') continue;
|
||||
if (lines[i][0] === '[') break;
|
||||
|
||||
lines[i] = lines[i].trim();
|
||||
|
||||
// detect and remove list style - and * in changelog lines
|
||||
if (lines[i].indexOf('-') === 0) lines[i] = lines[i].slice(1).trim();
|
||||
if (lines[i].indexOf('*') === 0) lines[i] = lines[i].slice(1).trim();
|
||||
|
||||
changelog.push(lines[i]);
|
||||
}
|
||||
|
||||
return changelog;
|
||||
}
|
||||
@@ -15,14 +15,12 @@ exports = module.exports = {
|
||||
delByAppId: delByAppId,
|
||||
delByAppIdAndType: delByAppIdAndType,
|
||||
|
||||
_clear: clear,
|
||||
_addDefaultClients: addDefaultClients
|
||||
_clear: clear
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
async = require('async'),
|
||||
database = require('./database.js'),
|
||||
DatabaseError = require('./databaseerror.js');
|
||||
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(',');
|
||||
@@ -32,8 +30,8 @@ function get(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE id = ?', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
@@ -43,7 +41,7 @@ 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 DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
@@ -53,7 +51,7 @@ 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 DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
@@ -64,7 +62,7 @@ function getAllWithTokenCountByIdentifier(identifier, 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 WHERE tokens.identifier=? GROUP BY clients.id', [ identifier ], function (error, results) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null, results);
|
||||
});
|
||||
@@ -75,8 +73,8 @@ function getByAppId(appId, callback) {
|
||||
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 DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
@@ -88,8 +86,8 @@ function getByAppIdAndType(appId, type, callback) {
|
||||
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 DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.length === 0) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
|
||||
|
||||
callback(null, result[0]);
|
||||
});
|
||||
@@ -107,8 +105,8 @@ function add(id, appId, type, clientSecret, redirectURI, scope, callback) {
|
||||
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 DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error || result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
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);
|
||||
});
|
||||
@@ -126,8 +124,8 @@ function upsert(id, appId, type, clientSecret, redirectURI, scope, callback) {
|
||||
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 DatabaseError(DatabaseError.ALREADY_EXISTS));
|
||||
if (error || result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
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);
|
||||
});
|
||||
@@ -138,8 +136,8 @@ function del(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM clients WHERE id = ?', [ id ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -150,8 +148,8 @@ function delByAppId(appId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM clients WHERE appId=?', [ appId ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -163,8 +161,8 @@ function delByAppIdAndType(appId, type, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM clients WHERE appId=? AND type=?', [ appId, type ], function (error, result) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
if (result.affectedRows !== 1) return callback(new BoxError(BoxError.NOT_FOUND, 'Client not found'));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -173,17 +171,9 @@ function delByAppIdAndType(appId, type, callback) {
|
||||
function clear(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
database.query('DELETE FROM clients WHERE id!="cid-webadmin" AND id!="cid-sdk" AND id!="cid-cli"', function (error) {
|
||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||
database.query('DELETE FROM clients', function (error) {
|
||||
if (error) return callback(new BoxError(BoxError.DATABASE_ERROR, error));
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function addDefaultClients(callback) {
|
||||
async.series([
|
||||
add.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', 'https://admin-localhost', '*'),
|
||||
add.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', 'https://admin-localhost', '*'),
|
||||
add.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', 'https://admin-localhost', '*')
|
||||
], callback);
|
||||
}
|
||||
|
||||
109
src/clients.js
109
src/clients.js
@@ -1,8 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
ClientsError: ClientsError,
|
||||
|
||||
add: add,
|
||||
get: get,
|
||||
del: del,
|
||||
@@ -18,6 +16,13 @@ exports = module.exports = {
|
||||
|
||||
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',
|
||||
@@ -28,53 +33,25 @@ exports = module.exports = {
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
clientdb = require('./clientdb.js'),
|
||||
constants = require('./constants.js'),
|
||||
DatabaseError = require('./databaseerror.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'),
|
||||
UsersError = users.UsersError,
|
||||
util = require('util'),
|
||||
uuid = require('uuid');
|
||||
|
||||
function ClientsError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(ClientsError, Error);
|
||||
ClientsError.INVALID_SCOPE = 'Invalid scope';
|
||||
ClientsError.INVALID_CLIENT = 'Invalid client';
|
||||
ClientsError.INVALID_TOKEN = 'Invalid token';
|
||||
ClientsError.BAD_FIELD = 'Bad field';
|
||||
ClientsError.NOT_FOUND = 'Not found';
|
||||
ClientsError.INTERNAL_ERROR = 'Internal Error';
|
||||
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
|
||||
uuid = require('uuid'),
|
||||
_ = require('underscore');
|
||||
|
||||
function validateClientName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
if (name.length < 1) return new ClientsError(ClientsError.BAD_FIELD, 'Name must be atleast 1 character');
|
||||
if (name.length > 128) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
|
||||
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 ClientsError(ClientsError.BAD_FIELD, 'Username can only contain alphanumerals and dash');
|
||||
if (/[^a-zA-Z0-9-]/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'name can only contain alphanumerals and dash', { field: 'name' });
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -82,7 +59,7 @@ function validateClientName(name) {
|
||||
function validateTokenName(name) {
|
||||
assert.strictEqual(typeof name, 'string');
|
||||
|
||||
if (name.length > 64) return new ClientsError(ClientsError.BAD_FIELD, 'Name too long');
|
||||
if (name.length > 64) return new BoxError(BoxError.BAD_FIELD, 'name too long', { field: 'name' });
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -95,7 +72,7 @@ function add(appId, type, redirectURI, scope, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var error = accesscontrol.validateScopeString(scope);
|
||||
if (error) return callback(new ClientsError(ClientsError.INVALID_SCOPE, error.message));
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validateClientName(appId);
|
||||
if (error) return callback(error);
|
||||
@@ -124,8 +101,8 @@ function get(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.get(id, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
@@ -135,8 +112,8 @@ function del(id, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.del(id, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
@@ -145,7 +122,7 @@ function getAll(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.getAll(function (error, results) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, []);
|
||||
if (error && error.reason === BoxError.NOT_FOUND) return callback(null, []);
|
||||
if (error) return callback(error);
|
||||
|
||||
var tmp = [];
|
||||
@@ -187,8 +164,8 @@ function getByAppIdAndType(appId, type, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
clientdb.getByAppIdAndType(appId, type, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, result);
|
||||
});
|
||||
}
|
||||
@@ -199,7 +176,7 @@ function getTokensByUserId(clientId, userId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.getByIdentifierAndClientId(userId, clientId, function (error, result) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) {
|
||||
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);
|
||||
@@ -218,7 +195,7 @@ function delTokensByUserId(clientId, userId, callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tokendb.delByIdentifierAndClientId(userId, clientId, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) {
|
||||
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);
|
||||
@@ -240,10 +217,9 @@ function delByAppIdAndType(appId, type, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tokendb.delByClientId(result.id, function (error) {
|
||||
if (error && error.reason !== DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error);
|
||||
|
||||
clientdb.delByAppIdAndType(appId, type, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
@@ -267,22 +243,29 @@ function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
users.get(userId, function (error, user) {
|
||||
if (error && error.reason === UsersError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such user'));
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(error);
|
||||
|
||||
accesscontrol.scopesForUser(user, function (error, userScopes) {
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(error);
|
||||
|
||||
var scope = accesscontrol.canonicalScopeString(result.scope);
|
||||
var authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(','));
|
||||
const scope = accesscontrol.canonicalScopeString(result.scope);
|
||||
const authorizedScopes = accesscontrol.intersectScopes(userScopes, scope.split(','));
|
||||
|
||||
var token = tokendb.generateToken();
|
||||
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, userId, result.id, expiresAt, authorizedScopes.join(','), name, function (error) {
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
tokendb.add(token, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null, {
|
||||
accessToken: token,
|
||||
accessToken: token.accessToken,
|
||||
tokenScopes: authorizedScopes,
|
||||
identifier: userId,
|
||||
clientId: result.id,
|
||||
@@ -294,7 +277,6 @@ function addTokenByUserId(clientId, userId, expiresAt, options, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
// this issues a cid-cli token that does not require a password in various routes
|
||||
function issueDeveloperToken(userObject, auditSource, callback) {
|
||||
assert.strictEqual(typeof userObject, 'object');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
@@ -302,7 +284,7 @@ function issueDeveloperToken(userObject, auditSource, callback) {
|
||||
|
||||
const expiresAt = Date.now() + constants.DEFAULT_TOKEN_EXPIRATION;
|
||||
|
||||
addTokenByUserId('cid-cli', userObject.id, expiresAt, {}, function (error, result) {
|
||||
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) });
|
||||
@@ -320,8 +302,7 @@ function delToken(clientId, tokenId, callback) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tokendb.del(tokenId, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.INVALID_TOKEN, 'Invalid token'));
|
||||
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(error);
|
||||
|
||||
callback(null);
|
||||
});
|
||||
@@ -337,8 +318,12 @@ function addDefaultClients(origin, callback) {
|
||||
// The domain might have changed, therefor we have to update the record
|
||||
// id, appId, type, clientSecret, redirectURI, scope
|
||||
async.series([
|
||||
clientdb.upsert.bind(null, 'cid-webadmin', 'Settings', 'built-in', 'secret-webadmin', origin, '*'),
|
||||
clientdb.upsert.bind(null, 'cid-sdk', 'SDK', 'built-in', 'secret-sdk', origin, '*'),
|
||||
clientdb.upsert.bind(null, 'cid-cli', 'Cloudron Tool', 'built-in', 'secret-cli', origin, '*')
|
||||
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');
|
||||
}
|
||||
|
||||
409
src/cloudron.js
409
src/cloudron.js
@@ -1,82 +1,65 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
CloudronError: CloudronError,
|
||||
|
||||
initialize: initialize,
|
||||
uninitialize: uninitialize,
|
||||
getConfig: getConfig,
|
||||
getDisks: getDisks,
|
||||
getLogs: getLogs,
|
||||
|
||||
reboot: reboot,
|
||||
isRebootRequired: isRebootRequired,
|
||||
|
||||
onActivated: onActivated,
|
||||
|
||||
checkDiskSpace: checkDiskSpace
|
||||
prepareDashboardDomain: prepareDashboardDomain,
|
||||
setDashboardDomain: setDashboardDomain,
|
||||
setDashboardAndMailDomain: setDashboardAndMailDomain,
|
||||
renewCerts: renewCerts,
|
||||
|
||||
setupDashboard: setupDashboard,
|
||||
|
||||
runSystemChecks: runSystemChecks,
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
var apps = require('./apps.js'),
|
||||
assert = require('assert'),
|
||||
async = require('async'),
|
||||
config = require('./config.js'),
|
||||
auditSource = require('./auditsource.js'),
|
||||
backups = require('./backups.js'),
|
||||
BoxError = require('./boxerror.js'),
|
||||
clients = require('./clients.js'),
|
||||
constants = require('./constants.js'),
|
||||
cron = require('./cron.js'),
|
||||
debug = require('debug')('box:cloudron'),
|
||||
df = require('@sindresorhus/df'),
|
||||
mailer = require('./mailer.js'),
|
||||
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'),
|
||||
progress = require('./progress.js'),
|
||||
reverseProxy = require('./reverseproxy.js'),
|
||||
safe = require('safetydance'),
|
||||
settings = require('./settings.js'),
|
||||
shell = require('./shell.js'),
|
||||
spawn = require('child_process').spawn,
|
||||
split = require('split'),
|
||||
users = require('./users.js'),
|
||||
util = require('util');
|
||||
tasks = require('./tasks.js'),
|
||||
users = require('./users.js');
|
||||
|
||||
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh');
|
||||
|
||||
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function CloudronError(reason, errorOrMessage) {
|
||||
assert.strictEqual(typeof reason, 'string');
|
||||
assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined');
|
||||
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
||||
this.name = this.constructor.name;
|
||||
this.reason = reason;
|
||||
if (typeof errorOrMessage === 'undefined') {
|
||||
this.message = reason;
|
||||
} else if (typeof errorOrMessage === 'string') {
|
||||
this.message = errorOrMessage;
|
||||
} else {
|
||||
this.message = 'Internal error';
|
||||
this.nestedError = errorOrMessage;
|
||||
}
|
||||
}
|
||||
util.inherits(CloudronError, Error);
|
||||
CloudronError.BAD_FIELD = 'Field error';
|
||||
CloudronError.INTERNAL_ERROR = 'Internal Error';
|
||||
CloudronError.EXTERNAL_ERROR = 'External Error';
|
||||
CloudronError.BAD_STATE = 'Bad state';
|
||||
CloudronError.ALREADY_UPTODATE = 'No Update Available';
|
||||
CloudronError.NOT_FOUND = 'Not found';
|
||||
CloudronError.SELF_UPGRADE_NOT_SUPPORTED = 'Self upgrade not supported';
|
||||
const NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||
|
||||
function initialize(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
async.series([
|
||||
settings.initialize,
|
||||
reverseProxy.configureDefaultServer,
|
||||
cron.startPreActivationJobs,
|
||||
onActivated
|
||||
], callback);
|
||||
runStartupTasks();
|
||||
|
||||
notifyUpdate(callback);
|
||||
}
|
||||
|
||||
function uninitialize(callback) {
|
||||
@@ -84,51 +67,55 @@ function uninitialize(callback) {
|
||||
|
||||
async.series([
|
||||
cron.stopJobs,
|
||||
platform.stop,
|
||||
settings.uninitialize
|
||||
platform.stop
|
||||
], callback);
|
||||
}
|
||||
|
||||
function onActivated(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
// Starting the platform after a user is available means:
|
||||
// 1. mail bounces can now be sent to the cloudron owner
|
||||
// 2. the restore code path can run without sudo (since mail/ is non-root)
|
||||
users.count(function (error, count) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
if (!count) return callback(); // not activated
|
||||
async.series([
|
||||
platform.start,
|
||||
cron.startJobs
|
||||
], callback);
|
||||
}
|
||||
|
||||
async.series([
|
||||
platform.start,
|
||||
cron.startPostActivationJobs
|
||||
], callback);
|
||||
function notifyUpdate(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
const version = safe.fs.readFileSync(paths.VERSION_FILE, 'utf8');
|
||||
if (version === constants.VERSION) return callback();
|
||||
|
||||
eventlog.add(eventlog.ACTION_UPDATE_FINISH, auditSource.CRON, { errorMessage: '', oldVersion: version || 'dev', newVersion: constants.VERSION }, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tasks.setCompletedByType(tasks.TASK_UPDATE, { error: null }, function (error) {
|
||||
if (error && error.reason !== BoxError.NOT_FOUND) return callback(error); // when hotfixing, task may not exist
|
||||
|
||||
safe.fs.writeFileSync(paths.VERSION_FILE, constants.VERSION, 'utf8');
|
||||
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getDisks(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
// 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);
|
||||
|
||||
var disks = {
|
||||
boxDataDisk: null,
|
||||
platformDataDisk: null,
|
||||
appsDataDisk: null
|
||||
};
|
||||
// always generate webadmin config since we have no versioning mechanism for the ejs
|
||||
if (settings.adminDomain()) reverseProxy.writeAdminConfig(settings.adminDomain(), NOOP_CALLBACK);
|
||||
|
||||
df.file(paths.BOX_DATA_DIR).then(function (result) {
|
||||
disks.boxDataDisk = result.filesystem;
|
||||
// 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
|
||||
|
||||
return df.file(paths.PLATFORM_DATA_DIR);
|
||||
}).then(function (result) {
|
||||
disks.platformDataDisk = result.filesystem;
|
||||
|
||||
return df.file(paths.APPS_DATA_DIR);
|
||||
}).then(function (result) {
|
||||
disks.appsDataDisk = result.filesystem;
|
||||
|
||||
callback(null, disks);
|
||||
}).catch(function (error) {
|
||||
callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
onActivated(NOOP_CALLBACK);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -136,69 +123,80 @@ function getConfig(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
settings.getAll(function (error, allSettings) {
|
||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||
if (error) return callback(error);
|
||||
|
||||
// be picky about what we send out here since this is sent for 'normal' users as well
|
||||
callback(null, {
|
||||
apiServerOrigin: config.apiServerOrigin(),
|
||||
webServerOrigin: config.webServerOrigin(),
|
||||
adminDomain: config.adminDomain(),
|
||||
adminFqdn: config.adminFqdn(),
|
||||
mailFqdn: config.mailFqdn(),
|
||||
version: config.version(),
|
||||
progress: progress.getAll(),
|
||||
isDemo: config.isDemo(),
|
||||
edition: config.edition(),
|
||||
apiServerOrigin: settings.apiServerOrigin(),
|
||||
webServerOrigin: settings.webServerOrigin(),
|
||||
adminDomain: settings.adminDomain(),
|
||||
adminFqdn: settings.adminFqdn(),
|
||||
mailFqdn: settings.mailFqdn(),
|
||||
version: constants.VERSION,
|
||||
isDemo: settings.isDemo(),
|
||||
memory: os.totalmem(),
|
||||
provider: config.provider(),
|
||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY]
|
||||
provider: settings.provider(),
|
||||
cloudronName: allSettings[settings.CLOUDRON_NAME_KEY],
|
||||
uiSpec: custom.uiSpec()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function reboot(callback) {
|
||||
shell.sudo('reboot', [ REBOOT_CMD ], callback);
|
||||
shell.sudo('reboot', [ REBOOT_CMD ], {}, callback);
|
||||
}
|
||||
|
||||
function checkDiskSpace(callback) {
|
||||
callback = callback || NOOP_CALLBACK;
|
||||
function isRebootRequired(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('Checking disk space');
|
||||
// https://serverfault.com/questions/92932/how-does-ubuntu-keep-track-of-the-system-restart-required-flag-in-motd
|
||||
callback(null, fs.existsSync('/var/run/reboot-required'));
|
||||
}
|
||||
|
||||
getDisks(function (error, disks) {
|
||||
if (error) {
|
||||
debug('df error %s', error.message);
|
||||
return callback();
|
||||
}
|
||||
// called from cron.js
|
||||
function runSystemChecks() {
|
||||
async.parallel([
|
||||
checkBackupConfiguration,
|
||||
checkMailStatus,
|
||||
checkRebootRequired
|
||||
], function (error) {
|
||||
debug('runSystemChecks: done', error);
|
||||
});
|
||||
}
|
||||
|
||||
df().then(function (entries) {
|
||||
/*
|
||||
[{
|
||||
filesystem: '/dev/disk1',
|
||||
size: 499046809600,
|
||||
used: 443222245376,
|
||||
available: 55562420224,
|
||||
capacity: 0.89,
|
||||
mountpoint: '/'
|
||||
}, ...]
|
||||
*/
|
||||
var oos = entries.some(function (entry) {
|
||||
// ignore other filesystems but where box, app and platform data is
|
||||
if (entry.filesystem !== disks.boxDataDisk && entry.filesystem !== disks.platformDataDisk && entry.filesystem !== disks.appsDataDisk) return false;
|
||||
function checkBackupConfiguration(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
return (entry.available <= (1.25 * 1024 * 1024 * 1024)); // 1.5G
|
||||
});
|
||||
debug('Checking backup configuration');
|
||||
|
||||
debug('Disk space checked. ok: %s', !oos);
|
||||
backups.checkConfiguration(function (error, message) {
|
||||
if (error) return callback(error);
|
||||
|
||||
if (oos) mailer.outOfDiskSpace(JSON.stringify(entries, null, 4));
|
||||
notifications.alert(notifications.ALERT_BACKUP_CONFIG, 'Backup configuration is unsafe', message, callback);
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
}).catch(function (error) {
|
||||
debug('df error %s', error.message);
|
||||
mailer.outOfDiskSpace(error.message);
|
||||
return callback();
|
||||
});
|
||||
function checkMailStatus(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('checking mail status');
|
||||
|
||||
mail.checkConfiguration(function (error, message) {
|
||||
if (error) return callback(error);
|
||||
|
||||
notifications.alert(notifications.ALERT_MAIL_STATUS, 'Email is not configured properly', message, callback);
|
||||
});
|
||||
}
|
||||
|
||||
function checkRebootRequired(callback) {
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug('checking if reboot required');
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -207,61 +205,39 @@ function getLogs(unit, options, callback) {
|
||||
assert(options && typeof options === 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
var lines = options.lines || 100,
|
||||
assert.strictEqual(typeof options.lines, 'number');
|
||||
assert.strictEqual(typeof options.format, 'string');
|
||||
assert.strictEqual(typeof options.follow, 'boolean');
|
||||
|
||||
var lines = options.lines === -1 ? '+1' : options.lines,
|
||||
format = options.format || 'json',
|
||||
follow = !!options.follow;
|
||||
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
|
||||
assert.strictEqual(typeof lines, 'number');
|
||||
assert.strictEqual(typeof format, 'string');
|
||||
follow = options.follow;
|
||||
|
||||
debug('Getting logs for %s as %s', unit, format);
|
||||
|
||||
var cp, transformStream;
|
||||
if (unit === 'box') {
|
||||
let args = [ '--no-pager', `--lines=${lines}` ];
|
||||
if (format === 'short') args.push('--output=short', '-a'); else args.push('--output=json');
|
||||
if (follow) args.push('--follow');
|
||||
args.push('--unit=box');
|
||||
args.push('--unit=cloudron-updater');
|
||||
cp = spawn('/bin/journalctl', args);
|
||||
let args = [ '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
|
||||
transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
// need to handle box.log without subdir
|
||||
if (unit === 'box') args.push(path.join(paths.LOG_DIR, 'box.log'));
|
||||
else if (unit.startsWith('crash-')) args.push(path.join(paths.CRASH_LOG_DIR, unit.slice(6) + '.log'));
|
||||
else return callback(new BoxError(BoxError.BAD_FIELD, 'No such unit', { field: 'unit' }));
|
||||
|
||||
var obj = safe.JSON.parse(line);
|
||||
if (!obj) return undefined;
|
||||
var cp = spawn('/usr/bin/tail', args);
|
||||
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: obj.__REALTIME_TIMESTAMP,
|
||||
monotonicTimestamp: obj.__MONOTONIC_TIMESTAMP,
|
||||
message: obj.MESSAGE,
|
||||
source: obj.SYSLOG_IDENTIFIER || ''
|
||||
}) + '\n';
|
||||
});
|
||||
} else { // mail, mongodb, mysql, postgresql, backup
|
||||
let args = [ '--lines=' + lines ];
|
||||
if (follow) args.push('--follow');
|
||||
args.push(path.join(paths.LOG_DIR, unit, 'app.log'));
|
||||
var transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
cp = spawn('/usr/bin/tail', args);
|
||||
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
|
||||
var timestamp = (new Date(data[0])).getTime();
|
||||
if (isNaN(timestamp)) timestamp = 0;
|
||||
|
||||
transformStream = split(function mapper(line) {
|
||||
if (format !== 'json') return line + '\n';
|
||||
|
||||
var data = line.split(' '); // logs are <ISOtimestamp> <msg>
|
||||
var timestamp = (new Date(data[0])).getTime();
|
||||
if (isNaN(timestamp)) timestamp = 0;
|
||||
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: timestamp * 1000,
|
||||
message: line.slice(data[0].length+1),
|
||||
source: unit
|
||||
}) + '\n';
|
||||
});
|
||||
}
|
||||
return JSON.stringify({
|
||||
realtimeTimestamp: timestamp * 1000,
|
||||
message: line.slice(data[0].length+1),
|
||||
source: unit
|
||||
}) + '\n';
|
||||
});
|
||||
|
||||
transformStream.close = cp.kill.bind(cp, 'SIGKILL'); // closing stream kills the child process
|
||||
|
||||
@@ -269,3 +245,104 @@ function getLogs(unit, options, callback) {
|
||||
|
||||
return callback(null, transformStream);
|
||||
}
|
||||
|
||||
function prepareDashboardDomain(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`prepareDashboardDomain: ${domain}`);
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
const fqdn = domains.fqdn(constants.ADMIN_LOCATION, domainObject);
|
||||
|
||||
apps.getAll(function (error, result) {
|
||||
if (error) return callback(error);
|
||||
|
||||
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) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tasks.startTask(taskId, {}, NOOP_CALLBACK);
|
||||
|
||||
callback(null, taskId);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// call this only pre activation since it won't start mail server
|
||||
function setDashboardDomain(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`setDashboardDomain: ${domain}`);
|
||||
|
||||
domains.get(domain, function (error, domainObject) {
|
||||
if (error) return callback(error);
|
||||
|
||||
reverseProxy.writeAdminConfig(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) {
|
||||
if (error) return callback(error);
|
||||
|
||||
eventlog.add(eventlog.ACTION_DASHBOARD_DOMAIN_UPDATE, auditSource, { domain: domain, fqdn: fqdn });
|
||||
|
||||
callback(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// call this only post activation because it will restart mail server
|
||||
function setDashboardAndMailDomain(domain, auditSource, callback) {
|
||||
assert.strictEqual(typeof domain, 'string');
|
||||
assert.strictEqual(typeof auditSource, 'object');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
debug(`setDashboardAndMailDomain: ${domain}`);
|
||||
|
||||
setDashboardDomain(domain, auditSource, function (error) {
|
||||
if (error) return callback(error);
|
||||
|
||||
mail.onMailFqdnChanged(NOOP_CALLBACK); // this will update dns and re-configure mail server
|
||||
|
||||
callback(null);
|
||||
});
|
||||
}
|
||||
|
||||
function 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');
|
||||
assert.strictEqual(typeof callback, 'function');
|
||||
|
||||
tasks.add(tasks.TASK_RENEW_CERTS, [ options, auditSource ], function (error, taskId) {
|
||||
if (error) return callback(error);
|
||||
|
||||
tasks.startTask(taskId, {}, NOOP_CALLBACK);
|
||||
|
||||
callback(null, taskId);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -30,3 +30,13 @@ LoadPlugin "table"
|
||||
</Result>
|
||||
</Table>
|
||||
</Plugin>
|
||||
|
||||
<Plugin python>
|
||||
<Module du>
|
||||
<Path>
|
||||
Instance "<%= appId %>"
|
||||
Dir "<%= appDataDir %>"
|
||||
</Path>
|
||||
</Module>
|
||||
</Plugin>
|
||||
|
||||
|
||||
246
src/config.js
246
src/config.js
@@ -1,246 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
baseDir: baseDir,
|
||||
|
||||
// values set here will be lost after a upgrade/update. use the sqlite database
|
||||
// for persistent values that need to be backed up
|
||||
get: get,
|
||||
set: set,
|
||||
|
||||
// ifdefs to check environment
|
||||
CLOUDRON: process.env.BOX_ENV === 'cloudron',
|
||||
TEST: process.env.BOX_ENV === 'test',
|
||||
|
||||
// convenience getters
|
||||
provider: provider,
|
||||
apiServerOrigin: apiServerOrigin,
|
||||
webServerOrigin: webServerOrigin,
|
||||
adminDomain: adminDomain,
|
||||
setFqdn: setAdminDomain,
|
||||
setAdminDomain: setAdminDomain,
|
||||
setAdminFqdn: setAdminFqdn,
|
||||
setAdminLocation: setAdminLocation,
|
||||
version: version,
|
||||
database: database,
|
||||
edition: edition,
|
||||
|
||||
// these values are derived
|
||||
adminOrigin: adminOrigin,
|
||||
internalAdminOrigin: internalAdminOrigin,
|
||||
sysadminOrigin: sysadminOrigin, // caas routes
|
||||
adminLocation: adminLocation,
|
||||
adminFqdn: adminFqdn,
|
||||
mailLocation: mailLocation,
|
||||
mailFqdn: mailFqdn,
|
||||
hasIPv6: hasIPv6,
|
||||
dkimSelector: dkimSelector,
|
||||
|
||||
isManaged: isManaged,
|
||||
isDemo: isDemo,
|
||||
|
||||
// feature flags based on editions (these have a separate license from standard edition)
|
||||
isSpacesEnabled: isSpacesEnabled,
|
||||
|
||||
// for testing resets to defaults
|
||||
_reset: _reset
|
||||
};
|
||||
|
||||
var assert = require('assert'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
safe = require('safetydance'),
|
||||
_ = require('underscore');
|
||||
|
||||
|
||||
// assert on unknown environment can't proceed
|
||||
assert(exports.CLOUDRON || exports.TEST, 'Unknown environment. This should not happen!');
|
||||
|
||||
var data = { };
|
||||
|
||||
function baseDir() {
|
||||
const homeDir = process.env.HOME || process.env.HOMEPATH || process.env.USERPROFILE;
|
||||
if (exports.CLOUDRON) return homeDir;
|
||||
if (exports.TEST) return path.join(homeDir, '.cloudron_test');
|
||||
// cannot reach
|
||||
}
|
||||
|
||||
const cloudronConfigFileName = exports.CLOUDRON ? '/etc/cloudron/cloudron.conf' : path.join(baseDir(), 'cloudron.conf');
|
||||
|
||||
function saveSync() {
|
||||
// only save values we want to have in the cloudron.conf, see start.sh
|
||||
var conf = {
|
||||
apiServerOrigin: data.apiServerOrigin,
|
||||
webServerOrigin: data.webServerOrigin,
|
||||
adminDomain: data.adminDomain,
|
||||
adminFqdn: data.adminFqdn,
|
||||
adminLocation: data.adminLocation,
|
||||
provider: data.provider,
|
||||
isDemo: data.isDemo,
|
||||
edition: data.edition
|
||||
};
|
||||
|
||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(conf, null, 4)); // functions are ignored by JSON.stringify
|
||||
}
|
||||
|
||||
function _reset(callback) {
|
||||
safe.fs.unlinkSync(cloudronConfigFileName);
|
||||
|
||||
initConfig();
|
||||
|
||||
if (callback) callback();
|
||||
}
|
||||
|
||||
function initConfig() {
|
||||
// setup defaults
|
||||
data.adminFqdn = '';
|
||||
data.adminDomain = '';
|
||||
data.adminLocation = 'my';
|
||||
data.port = 3000;
|
||||
data.apiServerOrigin = null;
|
||||
data.webServerOrigin = null;
|
||||
data.provider = 'generic';
|
||||
data.smtpPort = 2525; // this value comes from mail container
|
||||
data.sysadminPort = 3001;
|
||||
data.ldapPort = 3002;
|
||||
data.dockerProxyPort = 3003;
|
||||
data.edition = '';
|
||||
|
||||
// keep in sync with start.sh
|
||||
data.database = {
|
||||
hostname: '127.0.0.1',
|
||||
username: 'root',
|
||||
password: 'password',
|
||||
port: 3306,
|
||||
name: 'box'
|
||||
};
|
||||
|
||||
// overrides for local testings
|
||||
if (exports.TEST) {
|
||||
data.port = 5454;
|
||||
data.apiServerOrigin = 'http://localhost:6060'; // hock doesn't support https
|
||||
data.database.password = '';
|
||||
data.database.name = 'boxtest';
|
||||
}
|
||||
|
||||
// overwrite defaults with saved config
|
||||
var existingData = safe.JSON.parse(safe.fs.readFileSync(cloudronConfigFileName, 'utf8'));
|
||||
_.extend(data, existingData);
|
||||
}
|
||||
|
||||
initConfig();
|
||||
|
||||
// set(obj) or set(key, value)
|
||||
function set(key, value) {
|
||||
if (typeof key === 'object') {
|
||||
var obj = key;
|
||||
for (var k in obj) {
|
||||
assert(k in data, 'config.js is missing key "' + k + '"');
|
||||
data[k] = obj[k];
|
||||
}
|
||||
} else {
|
||||
data = safe.set(data, key, value);
|
||||
}
|
||||
|
||||
saveSync();
|
||||
}
|
||||
|
||||
function get(key) {
|
||||
assert.strictEqual(typeof key, 'string');
|
||||
|
||||
return safe.query(data, key);
|
||||
}
|
||||
|
||||
function apiServerOrigin() {
|
||||
return get('apiServerOrigin');
|
||||
}
|
||||
|
||||
function webServerOrigin() {
|
||||
return get('webServerOrigin');
|
||||
}
|
||||
|
||||
function setAdminDomain(domain) {
|
||||
set('adminDomain', domain);
|
||||
}
|
||||
|
||||
function adminDomain() {
|
||||
return get('adminDomain');
|
||||
}
|
||||
|
||||
function mailLocation() {
|
||||
return get('adminLocation'); // not a typo! should be same as admin location until we figure out certificates
|
||||
}
|
||||
|
||||
function setAdminLocation(location) {
|
||||
set('adminLocation', location);
|
||||
}
|
||||
|
||||
|
||||
function adminLocation() {
|
||||
return get('adminLocation');
|
||||
}
|
||||
|
||||
function setAdminFqdn(adminFqdn) {
|
||||
set('adminFqdn', adminFqdn);
|
||||
}
|
||||
|
||||
function adminFqdn() {
|
||||
return get('adminFqdn');
|
||||
}
|
||||
|
||||
function mailFqdn() {
|
||||
return adminFqdn();
|
||||
}
|
||||
|
||||
function adminOrigin() {
|
||||
return 'https://' + adminFqdn();
|
||||
}
|
||||
|
||||
function internalAdminOrigin() {
|
||||
return 'http://127.0.0.1:' + get('port');
|
||||
}
|
||||
|
||||
function sysadminOrigin() {
|
||||
return 'http://127.0.0.1:' + get('sysadminPort');
|
||||
}
|
||||
|
||||
function version() {
|
||||
if (exports.TEST) return '3.0.0-test';
|
||||
return fs.readFileSync(path.join(__dirname, '../VERSION'), 'utf8').trim();
|
||||
}
|
||||
|
||||
function database() {
|
||||
return get('database');
|
||||
}
|
||||
|
||||
function isDemo() {
|
||||
return get('isDemo') === true;
|
||||
}
|
||||
|
||||
function isSpacesEnabled() {
|
||||
return get('edition') === 'education';
|
||||
}
|
||||
|
||||
function provider() {
|
||||
return get('provider');
|
||||
}
|
||||
|
||||
function isManaged() {
|
||||
return edition() === 'hostingprovider';
|
||||
}
|
||||
|
||||
function hasIPv6() {
|
||||
const IPV6_PROC_FILE = '/proc/net/if_inet6';
|
||||
return fs.existsSync(IPV6_PROC_FILE);
|
||||
}
|
||||
|
||||
// it has to change with the adminLocation so that multiple cloudrons
|
||||
// can send out emails at the same time.
|
||||
function dkimSelector() {
|
||||
var loc = adminLocation();
|
||||
return loc === 'my' ? 'cloudron' : `cloudron-${loc.replace(/\./g, '')}`;
|
||||
}
|
||||
|
||||
function edition() {
|
||||
return get('edition');
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user