Compare commits
2602 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
060d9e88ef | ||
|
|
9cf497a87d | ||
|
|
b174765992 | ||
|
|
9c2d217176 | ||
|
|
3197349058 | ||
|
|
5f3378878e | ||
|
|
53cd45496b | ||
|
|
942339435a | ||
|
|
2bd6519795 | ||
|
|
1763c36a0b | ||
|
|
2c0eb33625 | ||
|
|
040b9993c7 | ||
|
|
8f21126697 | ||
|
|
716d29165c | ||
|
|
a2ec308155 | ||
|
|
b82610ba00 | ||
|
|
ed4674cd14 | ||
|
|
4e9dc75a37 | ||
|
|
f284b4cd83 | ||
|
|
15cf83b37c | ||
|
|
0eff8911ee | ||
|
|
814a0ce3a6 | ||
|
|
b3e1c221b7 | ||
|
|
dc31946e50 | ||
|
|
36bbb98970 | ||
|
|
ea4cea9733 | ||
|
|
7c06937a57 | ||
|
|
597704d3ed | ||
|
|
63290b9936 | ||
|
|
324222b040 | ||
|
|
f37b92da04 | ||
|
|
0de3b8fbdb | ||
|
|
f0cb3f94cb | ||
|
|
1508a5c6b9 | ||
|
|
9b9db6acf1 | ||
|
|
001bf94773 | ||
|
|
0160c12965 | ||
|
|
d08397336d | ||
|
|
880754877d | ||
|
|
984a191e4c | ||
|
|
cdca43311b | ||
|
|
020b47841a | ||
|
|
3f602c8a04 | ||
|
|
dea0c5642d | ||
|
|
3d2b75860b | ||
|
|
0da754a14b | ||
|
|
3d408c8c90 | ||
|
|
40348a5132 | ||
|
|
9a177d9e46 | ||
|
|
6f1df9980d | ||
|
|
0c9d331f47 | ||
|
|
f9db24e162 | ||
|
|
385bf3561b | ||
|
|
4304f20fe0 | ||
|
|
509083265f | ||
|
|
1b9dbd06c8 | ||
|
|
d6482414bb | ||
|
|
194b9b35bd | ||
|
|
6b9acb4722 | ||
|
|
08c3cb9376 | ||
|
|
79631ba996 | ||
|
|
4776a005a5 | ||
|
|
e954df2120 | ||
|
|
526a62a20e | ||
|
|
e2432d002f | ||
|
|
6e4d6d1099 | ||
|
|
fc2d1d61d7 | ||
|
|
4c4ae08b44 | ||
|
|
401c0e1b44 | ||
|
|
e431bd6040 | ||
|
|
a69cd204d6 | ||
|
|
3c3de6205e | ||
|
|
16444f775d | ||
|
|
2676658b5d | ||
|
|
fbb8a842c1 | ||
|
|
62b586e8dd | ||
|
|
313d98ef70 | ||
|
|
06448f146d | ||
|
|
064d950f87 | ||
|
|
3236ce9cd6 | ||
|
|
f74b22645f | ||
|
|
3540f2c197 | ||
|
|
3231fe7874 | ||
|
|
dc8fd2eab3 | ||
|
|
3ae388602c | ||
|
|
733187f3c4 | ||
|
|
02d2a7058e | ||
|
|
25003bcf40 | ||
|
|
234caa60eb | ||
|
|
a0227b6043 | ||
|
|
46ac6c4918 | ||
|
|
4afde79297 | ||
|
|
17d48f3fce | ||
|
|
facdabcc8d | ||
|
|
691803f10b | ||
|
|
8144c6d086 | ||
|
|
290ab6cc7d | ||
|
|
8e5af17e5d | ||
|
|
d9d94faf75 | ||
|
|
0201ab19e4 | ||
|
|
721fe74f3c | ||
|
|
96eeb247a1 | ||
|
|
6261231593 | ||
|
|
d62d2b17fe | ||
|
|
89cef4f050 | ||
|
|
8602e033c5 | ||
|
|
3598d89b12 | ||
|
|
ffd552583c | ||
|
|
9eabc9d266 | ||
|
|
edf8cd736e | ||
|
|
c5ebe2c2bf | ||
|
|
5d0ccc0dd7 | ||
|
|
4147455654 | ||
|
|
f3436a99a2 | ||
|
|
70d569e2e8 | ||
|
|
684625fbaf | ||
|
|
c8b9ae542c | ||
|
|
af29c1ba86 | ||
|
|
207e81345f | ||
|
|
d880731351 | ||
|
|
e603cfe96e | ||
|
|
5b93a2870f | ||
|
|
1214300800 | ||
|
|
8159334cbf | ||
|
|
78135c807a | ||
|
|
bfa33e4d8e | ||
|
|
8b23174769 | ||
|
|
a078c94b97 | ||
|
|
c86392cd60 | ||
|
|
f0e9256d46 | ||
|
|
0cd4e4f03a | ||
|
|
1766da9174 | ||
|
|
dbdcf1ec27 | ||
|
|
c916ea2589 | ||
|
|
5540b5f545 | ||
|
|
1e38190e68 | ||
|
|
8f3553090f | ||
|
|
cc0f5a1f03 | ||
|
|
a1c531d2a8 | ||
|
|
57cb3b04d7 | ||
|
|
a49cf98a8d | ||
|
|
da6cab8dd6 | ||
|
|
3b7cfdd7db | ||
|
|
f9251c8b37 | ||
|
|
4068ff5f21 | ||
|
|
ee073c91a3 | ||
|
|
9e8742ca87 | ||
|
|
7f99fe2399 | ||
|
|
bfe8df35df | ||
|
|
e2848d3e08 | ||
|
|
bc823b4a75 | ||
|
|
c24f780722 | ||
|
|
0d51ec9920 | ||
|
|
e07e544029 | ||
|
|
5aff55c5ca | ||
|
|
5ebc29746d | ||
|
|
8fc44e6bc9 | ||
|
|
44f4872134 | ||
|
|
49dd584a41 | ||
|
|
6d8f1f90d4 | ||
|
|
c1ded66c1a | ||
|
|
4df49a82e5 | ||
|
|
92e6ee9539 | ||
|
|
3ad2a2a5ca | ||
|
|
226537de04 | ||
|
|
41b324eb2d | ||
|
|
1360729e97 | ||
|
|
725e1debcc | ||
|
|
201efa70b7 | ||
|
|
c52d0369fa | ||
|
|
b4dfad3aa3 | ||
|
|
7667cdc66d | ||
|
|
3a9a667890 | ||
|
|
304cfed5a9 | ||
|
|
778c583a52 | ||
|
|
f988bb4d14 | ||
|
|
7057f1aaa2 | ||
|
|
e06f5f88b8 | ||
|
|
03cd3f0b6f | ||
|
|
615f875169 | ||
|
|
f27ba04a00 | ||
|
|
3e0006a327 | ||
|
|
558ca42ae8 | ||
|
|
9d8a803185 | ||
|
|
105047b0c4 | ||
|
|
e335aa5dee | ||
|
|
10163733db | ||
|
|
251fad8514 | ||
|
|
036740f97b | ||
|
|
f4958d936c | ||
|
|
80ca69a128 | ||
|
|
097d23c412 | ||
|
|
13a1213b0d | ||
|
|
76fe2bf531 | ||
|
|
50c4e4c91e | ||
|
|
46441d1814 | ||
|
|
a4e73be834 | ||
|
|
6be0d0814d | ||
|
|
e30d71921e | ||
|
|
a49c78f32c | ||
|
|
b077223e58 | ||
|
|
d2864dfe56 | ||
|
|
6d08af35a8 | ||
|
|
54f9d653f7 | ||
|
|
8d65f93fa4 | ||
|
|
462440bb30 | ||
|
|
65261dc4d5 | ||
|
|
54ead09aac | ||
|
|
28b3550214 | ||
|
|
e2e70da4c5 | ||
|
|
7326ea27ca | ||
|
|
1fe00f7f80 | ||
|
|
e9e9d6000d | ||
|
|
6dccb3655f | ||
|
|
c3113bd74d | ||
|
|
e79119b72a | ||
|
|
086cfdc1e6 | ||
|
|
1f091d3b4b | ||
|
|
892fa4b2ec | ||
|
|
a87b4b207c | ||
|
|
bdd14022d6 | ||
|
|
3d40cf03b1 | ||
|
|
594be7dbbd | ||
|
|
a52e2ffc23 | ||
|
|
8eeee712aa | ||
|
|
0f62faa198 | ||
|
|
bfd66cf309 | ||
|
|
c2f7d61e34 | ||
|
|
d5d5e356ae | ||
|
|
531752cd43 | ||
|
|
9eac56578c | ||
|
|
d06398dbfd | ||
|
|
60ce6b69ee | ||
|
|
4fcc7fe99f | ||
|
|
82cd215ffa | ||
|
|
1dcea84068 | ||
|
|
4107252bfe | ||
|
|
9cc6cb56f7 | ||
|
|
48b99a4203 | ||
|
|
824767adbb | ||
|
|
3d84880d92 | ||
|
|
dfa08469d6 | ||
|
|
d798073d95 | ||
|
|
41632b8c11 | ||
|
|
6ccc46717e | ||
|
|
2495caf2eb | ||
|
|
ae9c104a8b | ||
|
|
683f371778 | ||
|
|
eb29bdd575 | ||
|
|
b13de298bf | ||
|
|
47978436c2 | ||
|
|
71b5cc4702 | ||
|
|
5a9e32d41a | ||
|
|
b03e4db8d5 | ||
|
|
663ff2410a | ||
|
|
f763759008 | ||
|
|
69aa11d6c6 | ||
|
|
65041743c5 | ||
|
|
76214d3d7a | ||
|
|
be83a967fc | ||
|
|
119e095710 | ||
|
|
5df3a41988 | ||
|
|
a34b611e20 | ||
|
|
75c1731443 | ||
|
|
9e36b7abf4 | ||
|
|
b37226d4d1 | ||
|
|
311efe5d10 | ||
|
|
ebdd6d8a31 | ||
|
|
3ee9f70113 | ||
|
|
adfc069e16 | ||
|
|
31fd0d711a | ||
|
|
a6a852cfae | ||
|
|
e9b3e22e86 | ||
|
|
564d61bcf5 | ||
|
|
5582ac7402 | ||
|
|
a05b6ad78d | ||
|
|
ec71390d0b | ||
|
|
68a3862ee5 | ||
|
|
a9f70d8363 | ||
|
|
e91539d79a | ||
|
|
5546bfbf0e | ||
|
|
803d47b426 | ||
|
|
e4c0192243 | ||
|
|
d5b5289e0c | ||
|
|
2909aad72a | ||
|
|
cafbb31e78 | ||
|
|
080128539c | ||
|
|
cf93a99a4e | ||
|
|
ce927bfa22 | ||
|
|
6993a9c7e7 | ||
|
|
84d04cce16 | ||
|
|
f735fd8172 | ||
|
|
53e28db1d6 | ||
|
|
77457d1ea9 | ||
|
|
161b7cf76b | ||
|
|
01b6defd24 | ||
|
|
badc524ff2 | ||
|
|
b3f53099f0 | ||
|
|
a28560cdc0 | ||
|
|
4afdf50736 | ||
|
|
078e36f07f | ||
|
|
3b8e15a61c | ||
|
|
67682c5d27 | ||
|
|
48e3b8ebf9 | ||
|
|
2072dedf66 | ||
|
|
51f43ecc27 | ||
|
|
2347a7ced2 | ||
|
|
b2cadaf95c | ||
|
|
957f787701 | ||
|
|
ad48067bb2 | ||
|
|
12b6c46558 | ||
|
|
b4ba17c599 | ||
|
|
7fb28662c1 | ||
|
|
4845db538a | ||
|
|
8429985253 | ||
|
|
aff9ff47bc | ||
|
|
9b3077eca3 | ||
|
|
39396cb3ab | ||
|
|
364f0ead51 | ||
|
|
5ac1d5575c | ||
|
|
e5a030baff | ||
|
|
a100837e69 | ||
|
|
ffacf17a42 | ||
|
|
f5d37b6443 | ||
|
|
d71d09c1ba | ||
|
|
c1a2444dfa | ||
|
|
ef40aae3ba | ||
|
|
9570086c87 | ||
|
|
57a823a698 | ||
|
|
ec0ee07b17 | ||
|
|
3d7545133e | ||
|
|
bcc752469a | ||
|
|
da85f4c096 | ||
|
|
3b740a5651 | ||
|
|
7eb202f19a | ||
|
|
8dbd4c8527 | ||
|
|
88f2ce554d | ||
|
|
57888659a6 | ||
|
|
ebdefa7f18 | ||
|
|
569150f602 | ||
|
|
6ccb806628 | ||
|
|
ae807b28b6 | ||
|
|
00726b01e2 | ||
|
|
f5b777ab33 | ||
|
|
d84e584222 | ||
|
|
31e452e1cc | ||
|
|
e015b9bd7a | ||
|
|
10e0cbcebc | ||
|
|
2768c3a336 | ||
|
|
37512c4cac | ||
|
|
0aaaa866e4 | ||
|
|
53cb7fe687 | ||
|
|
da42f2f00c | ||
|
|
27d2daae93 | ||
|
|
42cc8249f8 | ||
|
|
de055492ef | ||
|
|
efa3ccaffe | ||
|
|
32e238818a | ||
|
|
2c1083d58b | ||
|
|
517b967fe9 | ||
|
|
3c4ca8e9c8 | ||
|
|
4ec043836b | ||
|
|
7ec93b733b | ||
|
|
a81262afb5 | ||
|
|
266603bb19 | ||
|
|
6dcecaaf55 | ||
|
|
099eb2bca4 | ||
|
|
b92ed8d079 | ||
|
|
0838ce4ef8 | ||
|
|
cc8767274a | ||
|
|
dfaed79e31 | ||
|
|
9dc1a95992 | ||
|
|
5be05529c2 | ||
|
|
6ff7786f04 | ||
|
|
a833b65ef3 | ||
|
|
83a252bd20 | ||
|
|
7ef3805dbc | ||
|
|
e5d906a065 | ||
|
|
b5e4e9fed6 | ||
|
|
3ccb72f891 | ||
|
|
45cd4ba349 | ||
|
|
5b9b21c469 | ||
|
|
ed55ad1c6f | ||
|
|
560f460a32 | ||
|
|
aa116ce58c | ||
|
|
3f0e2024e4 | ||
|
|
d9c5b2b642 | ||
|
|
5322ed054d | ||
|
|
39c4954371 | ||
|
|
78ad49bd74 | ||
|
|
f56c960b92 | ||
|
|
8e077660c4 | ||
|
|
1b8b4900a2 | ||
|
|
27ddcb9758 | ||
|
|
fdb951c9e5 | ||
|
|
0f2037513b | ||
|
|
9da4e038bd | ||
|
|
ae3e0177bb | ||
|
|
0751974624 | ||
|
|
b8242c82d6 | ||
|
|
442c02fa1b | ||
|
|
d5306052bb | ||
|
|
8543dbe3be | ||
|
|
bf42b735d1 | ||
|
|
a2ba3989d0 | ||
|
|
6f36d79358 | ||
|
|
1da24564b3 | ||
|
|
da61d5c0f1 | ||
|
|
79da7b31c7 | ||
|
|
631b238b63 | ||
|
|
ff5ca617b1 | ||
|
|
e16125c67e | ||
|
|
646ba096c3 | ||
|
|
8be3b4c281 | ||
|
|
5afff5eecc | ||
|
|
84206738e1 | ||
|
|
8b2e4ce700 | ||
|
|
776f184dbc | ||
|
|
a54466f8c2 | ||
|
|
f36641b443 | ||
|
|
36eb107b83 | ||
|
|
fa16ae9a0c | ||
|
|
517b36b3f0 | ||
|
|
83a28afc8f | ||
|
|
e76c7de259 | ||
|
|
fcda4a771c | ||
|
|
76d8f16e22 | ||
|
|
1b3cd1f373 | ||
|
|
62b020e96d | ||
|
|
bc78f4a6d8 | ||
|
|
a8e458e935 | ||
|
|
e4747ef50c | ||
|
|
0d6637de27 | ||
|
|
28e513a434 | ||
|
|
e73174685b | ||
|
|
3af95508f5 | ||
|
|
4fa8ab596b | ||
|
|
54c9bb7409 | ||
|
|
4c7dc5056d | ||
|
|
e986a67d39 | ||
|
|
da8de173a6 | ||
|
|
cbc906f8d1 | ||
|
|
c7958f8e1d | ||
|
|
b88ee8143a | ||
|
|
e413f7ba9b | ||
|
|
7e1055ae44 | ||
|
|
c61ce40362 | ||
|
|
e48156dceb | ||
|
|
f3811e3df9 | ||
|
|
0d40b1b80d | ||
|
|
8b92c8f7ae | ||
|
|
d41eb81b3d | ||
|
|
3adf91afed | ||
|
|
18f05de8ae | ||
|
|
b0f4396389 | ||
|
|
bf99475dbd | ||
|
|
d50fa70f47 | ||
|
|
0e655cadb0 | ||
|
|
496e1c3dc1 | ||
|
|
325252699e | ||
|
|
2d43e22285 | ||
|
|
9e673c3890 | ||
|
|
c3c18e8a4b | ||
|
|
cb1bd58cb9 | ||
|
|
0bdff14c9f | ||
|
|
c4ae9526af | ||
|
|
8d79ac9ae0 | ||
|
|
7d4ed5bafc | ||
|
|
85db8f398b | ||
|
|
636b71ce6f | ||
|
|
b46008f0b1 | ||
|
|
9d9bd42cd2 | ||
|
|
0f6a2a42f2 | ||
|
|
fc8bf82993 | ||
|
|
e56192913d | ||
|
|
2d08ce441f | ||
|
|
87497c2047 | ||
|
|
32a0bf6fd2 | ||
|
|
291e625785 | ||
|
|
6bcfd33e10 | ||
|
|
b4c15b1719 | ||
|
|
920626192c | ||
|
|
778371b818 | ||
|
|
d2a1cea1e8 | ||
|
|
5683cefe89 | ||
|
|
126d64ffa8 | ||
|
|
7262eb208f | ||
|
|
f57f8f5e58 | ||
|
|
00ad1308aa | ||
|
|
2cc37a9c31 | ||
|
|
13b0093f20 | ||
|
|
5617baea50 | ||
|
|
da79e4f229 | ||
|
|
6c1bd522e6 | ||
|
|
7fca9decc1 | ||
|
|
7cf08b6a4d | ||
|
|
ffedbdfa13 | ||
|
|
43e207a301 | ||
|
|
bea6019dc4 | ||
|
|
9042e9e1a9 | ||
|
|
b855dee4cb | ||
|
|
b322f6805f | ||
|
|
ccc119ddec | ||
|
|
994cbaa22a | ||
|
|
d7a34bbf68 | ||
|
|
1f31fe6f8f | ||
|
|
37bdd2672b | ||
|
|
63702f836a | ||
|
|
5898428a6c | ||
|
|
f4a6c64956 | ||
|
|
f9d4d3014d | ||
|
|
8254337552 | ||
|
|
d811115f21 | ||
|
|
fec388b648 | ||
|
|
a969e323a6 | ||
|
|
09584ac29c | ||
|
|
7967610f3f | ||
|
|
727332fe66 | ||
|
|
09595d1c43 | ||
|
|
c4ad6c803f | ||
|
|
fd1a00d280 | ||
|
|
5c2a650681 | ||
|
|
43051cea3b | ||
|
|
3d50a251ee | ||
|
|
219df8babd | ||
|
|
d3d9706a70 | ||
|
|
8a3ad6c964 | ||
|
|
90719cd4d9 | ||
|
|
30445ddab9 | ||
|
|
157fbc89b8 | ||
|
|
71219c6af7 | ||
|
|
934abafbd4 | ||
|
|
bc6e896507 | ||
|
|
ca8731c282 | ||
|
|
c511019d79 | ||
|
|
992c4ee847 | ||
|
|
8c427553ba | ||
|
|
c1df22f079 | ||
|
|
7673ecde2f | ||
|
|
a9d0cf66fd | ||
|
|
db89784af8 | ||
|
|
4143f903ad | ||
|
|
12820db4a5 | ||
|
|
8837cc5a3c | ||
|
|
f2545e3def | ||
|
|
7a72bf3f78 | ||
|
|
87351f04ef | ||
|
|
4a04e0b52f | ||
|
|
5945bce00e | ||
|
|
d2a3925e04 | ||
|
|
9c9f82e2c5 | ||
|
|
8fd3ff0ccc | ||
|
|
dec2fdb6bb | ||
|
|
c581d2a52c | ||
|
|
e6e748e30d | ||
|
|
36fddacf5c | ||
|
|
183c1608a6 | ||
|
|
51c8f65e8d | ||
|
|
0da6e9a5b9 | ||
|
|
31c7a17684 | ||
|
|
a1e2cd438e | ||
|
|
6b0e00e28b | ||
|
|
19948851e0 | ||
|
|
bfe4f75881 | ||
|
|
7f13594f01 | ||
|
|
4fafac035e | ||
|
|
ca41e6acfd | ||
|
|
9893dd6640 | ||
|
|
8f7e4c2053 | ||
|
|
d037b13401 | ||
|
|
83c955d25b | ||
|
|
0bb6d969a4 | ||
|
|
6062a5bdd2 | ||
|
|
70ab492efa | ||
|
|
aab035f7b9 | ||
|
|
0e825272ae | ||
|
|
46fee9e431 | ||
|
|
0789c96992 | ||
|
|
a4adc581fa | ||
|
|
500fb452e7 | ||
|
|
e11b762ea1 | ||
|
|
f5d1726352 | ||
|
|
3d5aa9fd23 | ||
|
|
ef12740060 | ||
|
|
415902d68e | ||
|
|
0ef0e010a3 | ||
|
|
2d27da89d2 | ||
|
|
9d8def8349 | ||
|
|
2533111bfa | ||
|
|
20d6da8230 | ||
|
|
f159cacfbb | ||
|
|
5e9ea98b66 | ||
|
|
d87b7dcb75 | ||
|
|
6eea2fef9a | ||
|
|
34fd5f14a5 | ||
|
|
a4e73e747c | ||
|
|
eadff099eb | ||
|
|
15653cb3f8 | ||
|
|
2f8dc35c5d | ||
|
|
a97720d204 | ||
|
|
73898505b0 | ||
|
|
88b4b6a38b | ||
|
|
3da82e3a63 | ||
|
|
dad1585704 | ||
|
|
e81dbdb36c | ||
|
|
ee2478e500 | ||
|
|
0f7a6964a4 | ||
|
|
5fa974ffe6 | ||
|
|
e1b7198a29 | ||
|
|
37d6354627 | ||
|
|
6ab3e04fc1 | ||
|
|
b1987868be | ||
|
|
72eb3007c4 | ||
|
|
6c1da45ad1 | ||
|
|
b9857cdb65 | ||
|
|
d5c251115c | ||
|
|
64c66e248b | ||
|
|
bb53c4f331 | ||
|
|
3215d4a3c9 | ||
|
|
68c4d77494 | ||
|
|
44bf299e10 | ||
|
|
6b1e14b464 | ||
|
|
8dcde84c3c | ||
|
|
a0deedb958 | ||
|
|
a2096bec18 | ||
|
|
4f82bcec43 | ||
|
|
491356ce8d | ||
|
|
6c99105a7e | ||
|
|
71f847776b | ||
|
|
87c5371603 | ||
|
|
01d676628d | ||
|
|
60badce935 | ||
|
|
182ae6bf1f | ||
|
|
c62ef9e156 | ||
|
|
96383a1fae | ||
|
|
5e9542ee76 | ||
|
|
cc28d49df4 | ||
|
|
18f3733d6e | ||
|
|
87dcf42c7e | ||
|
|
32d8627045 | ||
|
|
6a607f9565 | ||
|
|
c623770b44 | ||
|
|
69f3620b22 | ||
|
|
21110bb2e0 | ||
|
|
fabe55622e | ||
|
|
73e079cc6c | ||
|
|
a7d22a1972 | ||
|
|
5c1970b37f | ||
|
|
db065bd0fc | ||
|
|
db6d8deec4 | ||
|
|
414b21f29a | ||
|
|
c4c7668b5a | ||
|
|
b9fa87cca2 | ||
|
|
218c9099fd | ||
|
|
916d97f7bd | ||
|
|
109f777c00 | ||
|
|
4bf3a78227 | ||
|
|
c03e69232e | ||
|
|
91a016ee91 | ||
|
|
8256f97e9d | ||
|
|
d095899aef | ||
|
|
6293c0aede | ||
|
|
101ce62ef3 | ||
|
|
9f443e2d07 | ||
|
|
0a30585a05 | ||
|
|
ed78bd05c8 | ||
|
|
c24d7e7b3c | ||
|
|
389d2be82d | ||
|
|
38b85e6006 | ||
|
|
de2cde7333 | ||
|
|
08410569c0 | ||
|
|
be3b08a7b4 | ||
|
|
2724cfd0ad | ||
|
|
d7c8cf5e0e | ||
|
|
11f89da3a0 | ||
|
|
a803af2300 | ||
|
|
6991402a8c | ||
|
|
259798a8f2 | ||
|
|
d83395ecfb | ||
|
|
6d3dd452be | ||
|
|
40bee79e3d | ||
|
|
95de25560b | ||
|
|
79eee94a5e | ||
|
|
82651a33c7 | ||
|
|
212a0ffcd9 | ||
|
|
115ed12c36 | ||
|
|
53268b67dc | ||
|
|
40dd12ba68 | ||
|
|
7a111e29ad | ||
|
|
065c65317d | ||
|
|
91a5d711f4 | ||
|
|
9071ea6c5e | ||
|
|
34521735da | ||
|
|
b7f6dfb197 | ||
|
|
fa330b4652 | ||
|
|
3bdbcff811 | ||
|
|
ea3bd6d71d | ||
|
|
d5cc96b1ff | ||
|
|
4ed368cdd8 | ||
|
|
5229222014 | ||
|
|
9b0aa331e1 | ||
|
|
70cc073b1c | ||
|
|
29502fd8af | ||
|
|
8d75fcfe67 | ||
|
|
b2668579d6 | ||
|
|
ba663faa64 | ||
|
|
8db76f6b70 | ||
|
|
322e9faee7 | ||
|
|
af9d489395 | ||
|
|
4565291c1c | ||
|
|
be127ec313 | ||
|
|
8b3a44b33c | ||
|
|
08b5d7003d | ||
|
|
60cc4c988f | ||
|
|
68219748ec | ||
|
|
cfb56d7eee | ||
|
|
4690616230 | ||
|
|
96d625b866 | ||
|
|
2e281f8554 | ||
|
|
5da5d86bc8 | ||
|
|
103c0bd688 | ||
|
|
275d8c2121 | ||
|
|
4c964bcaf8 | ||
|
|
e6c2c77f03 | ||
|
|
819095b465 | ||
|
|
1453fd3c54 | ||
|
|
867278a0b6 | ||
|
|
382fca3cf2 | ||
|
|
f210501e12 | ||
|
|
499921e3af | ||
|
|
db19df9395 | ||
|
|
6e2067bfe7 | ||
|
|
8eb1b374ef | ||
|
|
1734555974 | ||
|
|
7136de4d08 | ||
|
|
21e8bc1ce5 | ||
|
|
13020be6e6 | ||
|
|
3b922ff8b2 | ||
|
|
69402d0079 | ||
|
|
99850f1161 | ||
|
|
b205212bf2 | ||
|
|
baf586b028 | ||
|
|
94faa3575c | ||
|
|
544c1474d1 | ||
|
|
bb25279878 | ||
|
|
4939f526d5 | ||
|
|
68af03f401 | ||
|
|
f744fee708 | ||
|
|
c7ceb29845 | ||
|
|
56d9d5913d | ||
|
|
f7887228d3 | ||
|
|
73ed0384ea | ||
|
|
3051d4c22a | ||
|
|
b32a0bcfad | ||
|
|
61c79aab23 | ||
|
|
9740ffd504 | ||
|
|
435ec2365b | ||
|
|
ff3562b0e8 | ||
|
|
3be5511e33 | ||
|
|
c8604e95ab | ||
|
|
bbaf4c77fd | ||
|
|
1c9fc3f3dc | ||
|
|
577959f281 | ||
|
|
8af01f2955 | ||
|
|
c73213b2f2 | ||
|
|
36f3f4b8f4 | ||
|
|
31bd5cdee3 | ||
|
|
fd0326efb1 | ||
|
|
65c6806109 | ||
|
|
1b7406784e | ||
|
|
8cbf83058f | ||
|
|
e058e22cae | ||
|
|
c84674529b | ||
|
|
a0098a8883 | ||
|
|
f6547c9b71 | ||
|
|
6dc17183ee | ||
|
|
bba3dd5ec0 | ||
|
|
9eec6c2e9d | ||
|
|
c235b82660 | ||
|
|
67ac0fcd5a | ||
|
|
87ca147e65 | ||
|
|
0cf2bfb792 | ||
|
|
a112e614e6 | ||
|
|
0b1dcd2940 | ||
|
|
951934f275 | ||
|
|
78518ff5f6 | ||
|
|
b8d0c01187 | ||
|
|
572e5c4938 | ||
|
|
e4fabd20c1 | ||
|
|
726d154890 | ||
|
|
7a5ac1a2f5 | ||
|
|
c90a8041e2 | ||
|
|
18b91b5fa0 | ||
|
|
f058c266d2 | ||
|
|
e0114c87ac | ||
|
|
c98275000b | ||
|
|
553509c462 | ||
|
|
306bef96b4 | ||
|
|
497eaea65e | ||
|
|
8aacc503a6 | ||
|
|
ec160fe45f | ||
|
|
82c74e6787 | ||
|
|
bbff195863 | ||
|
|
e528dbcfc0 | ||
|
|
0467e80c71 | ||
|
|
c9ef0056e0 | ||
|
|
efb228cf5e | ||
|
|
af700827c5 | ||
|
|
3135783fe3 | ||
|
|
496f530b9f | ||
|
|
f44c2707f0 | ||
|
|
9fbbddc3eb | ||
|
|
5afb16aa98 | ||
|
|
8f2b0bae5e | ||
|
|
fcfd1dceac | ||
|
|
d839f0b762 | ||
|
|
16a65fb185 | ||
|
|
aaeb355183 | ||
|
|
c236072c4c | ||
|
|
5d92cff638 | ||
|
|
1b539b8d22 | ||
|
|
a21a913f34 | ||
|
|
357f6f0552 | ||
|
|
b16aa4c007 | ||
|
|
1fed5ee353 | ||
|
|
29077abf7c | ||
|
|
f5c7116573 | ||
|
|
42fc2d446c | ||
|
|
9ef04dc67f | ||
|
|
3ea2070cdb | ||
|
|
fc11484b51 | ||
|
|
b4ddfa94cc | ||
|
|
9e7ae1a4f7 | ||
|
|
d27159275b | ||
|
|
6c2ae756f1 | ||
|
|
92e4433dff | ||
|
|
c4cbd9f4e4 | ||
|
|
f413afb835 | ||
|
|
915c37a72f | ||
|
|
1ddb3a58da | ||
|
|
a4aa5bbc59 | ||
|
|
39cc5d07d1 | ||
|
|
f3a05931df | ||
|
|
df39384056 | ||
|
|
47c5cad239 | ||
|
|
ec380aa41e | ||
|
|
7d1a663a87 | ||
|
|
ba69316c14 | ||
|
|
c097651a88 | ||
|
|
22b8154a39 | ||
|
|
9e8179a235 | ||
|
|
3fbeb2a1c1 | ||
|
|
2c4cf0a505 | ||
|
|
adab544e99 | ||
|
|
ae8a371597 | ||
|
|
ead076bd9f | ||
|
|
f8c683f451 | ||
|
|
b56bc08e9a | ||
|
|
daadbfa23f | ||
|
|
a215443c56 | ||
|
|
4e22c6d5ac | ||
|
|
d43810fea9 | ||
|
|
f5ab63e8ec | ||
|
|
b1f172ed17 | ||
|
|
413f9231b3 | ||
|
|
11513f9428 | ||
|
|
5042741435 | ||
|
|
75ed9c4a63 | ||
|
|
8c36f3aab4 | ||
|
|
7aa5e8720a | ||
|
|
14ef71002f | ||
|
|
ea87841e77 | ||
|
|
091e424c0e | ||
|
|
20629ea078 | ||
|
|
b1b6a9ae65 | ||
|
|
7ddbf7b652 | ||
|
|
3d088aa9c4 | ||
|
|
f329e0da92 | ||
|
|
a18737882b | ||
|
|
a58a458950 | ||
|
|
44c5f84c56 | ||
|
|
d6b92ee301 | ||
|
|
c769a12c45 | ||
|
|
017c32c3dd | ||
|
|
5d54c9e668 | ||
|
|
adaaca5ceb | ||
|
|
4a73e1490e | ||
|
|
f31a7a5061 | ||
|
|
3499a4cc6c | ||
|
|
42796b12dc | ||
|
|
20ac040dde | ||
|
|
7f2b3eb835 | ||
|
|
2b562f76ea | ||
|
|
b942033512 | ||
|
|
fa4a8c2036 | ||
|
|
27febbf1e9 | ||
|
|
8da2eb36cc | ||
|
|
cbb34005c6 | ||
|
|
efc1627648 | ||
|
|
f513dcdf3b | ||
|
|
61a52d8888 | ||
|
|
4cfc187063 | ||
|
|
065af03e5f | ||
|
|
c4eeebdfbe | ||
|
|
b1004de358 | ||
|
|
fbca0fef38 | ||
|
|
d658530e66 | ||
|
|
21d4cc9cb2 | ||
|
|
e2b7ec3ffd | ||
|
|
8014e2eaf8 | ||
|
|
a10ed73af2 | ||
|
|
8b2903015d | ||
|
|
d157bf30f3 | ||
|
|
7996b32022 | ||
|
|
4b77703902 | ||
|
|
4dd82d10ad | ||
|
|
83d05c99d3 | ||
|
|
b0acdfb908 | ||
|
|
b062dab65c | ||
|
|
eadcdeee1c | ||
|
|
9de6f9c1c2 | ||
|
|
89f54245f7 | ||
|
|
5fbd1dae30 | ||
|
|
486ced0946 | ||
|
|
d1c1fb8786 | ||
|
|
57ff8b6770 | ||
|
|
d12d8f5c0b | ||
|
|
17deac756b | ||
|
|
f7bb3bac98 | ||
|
|
744c721000 | ||
|
|
0500bae221 | ||
|
|
a7b5b49d96 | ||
|
|
93ef1919c2 | ||
|
|
254d6ac92e | ||
|
|
3a12265f42 | ||
|
|
71eeb47f0f | ||
|
|
5ea5023d97 | ||
|
|
1148e21cd4 | ||
|
|
e9a2b2a7cf | ||
|
|
7a34f40611 | ||
|
|
c630de1003 | ||
|
|
74da8f5af8 | ||
|
|
b758be5ae2 | ||
|
|
c585be4eec | ||
|
|
3ebc569438 | ||
|
|
5a2cf3cbfe | ||
|
|
715c5f9f61 | ||
|
|
6843fda601 | ||
|
|
a78f3b1db3 | ||
|
|
1419108a86 | ||
|
|
7a8b457ce9 | ||
|
|
10967ff8ce | ||
|
|
1fdfd3681c | ||
|
|
187d4f9ca2 | ||
|
|
6b67e64bf1 | ||
|
|
7ae6061d72 | ||
|
|
e96b9c3e3f | ||
|
|
c9ca05a703 | ||
|
|
23e5bed247 | ||
|
|
bae0d728b3 | ||
|
|
5cd1c7d714 | ||
|
|
d430e902bf | ||
|
|
4fb89de34f | ||
|
|
7cd3bb31e1 | ||
|
|
2857158543 | ||
|
|
82a347ea4b | ||
|
|
b5c7f978a2 | ||
|
|
625da29fce | ||
|
|
b82b183df6 | ||
|
|
ce36fadf2b | ||
|
|
2429599733 | ||
|
|
261a0a1728 | ||
|
|
d8def61f67 | ||
|
|
2732af24c1 | ||
|
|
3d48da0e8d | ||
|
|
d3b8bd1314 | ||
|
|
f600ebcf19 | ||
|
|
160467e199 | ||
|
|
384c410e7c | ||
|
|
84c4187fa9 | ||
|
|
4f7fd9177c | ||
|
|
b5b0ab7475 | ||
|
|
a0d7406b3c | ||
|
|
7165be0513 | ||
|
|
9c995277f7 | ||
|
|
aa693e529b | ||
|
|
63013c7297 | ||
|
|
c8db6419d8 | ||
|
|
93c1ddd982 | ||
|
|
df102ec374 | ||
|
|
9688e4c124 | ||
|
|
00d277b1c3 | ||
|
|
0fb44bfbc1 | ||
|
|
c167bd8996 | ||
|
|
a3737c3797 | ||
|
|
8fcb0b46a5 | ||
|
|
f5189e0a56 | ||
|
|
86f14b0149 | ||
|
|
30913006e3 | ||
|
|
81bd4f2ea5 | ||
|
|
351ddcb218 | ||
|
|
dd18f9741a | ||
|
|
cdce6e605d | ||
|
|
d4480ec407 | ||
|
|
85c92ab0b4 | ||
|
|
230c24d6c6 | ||
|
|
07c935dfec | ||
|
|
eab3bda8e1 | ||
|
|
f731c1ed0b | ||
|
|
edec3601f4 | ||
|
|
9e87fd0440 | ||
|
|
8cb304e1c9 | ||
|
|
a24335d68b | ||
|
|
78d1ed7aa5 | ||
|
|
deb30e440a | ||
|
|
86ef9074b1 | ||
|
|
1a13128ae1 | ||
|
|
b41642552d | ||
|
|
f5570c2e63 | ||
|
|
b0d11ddcab | ||
|
|
804464c304 | ||
|
|
ecf7f442ba | ||
|
|
9ddd3aeb07 | ||
|
|
864e3ff217 | ||
|
|
9bf1fe3b7d | ||
|
|
b32a48c212 | ||
|
|
22a3dd7653 | ||
|
|
132b463e0a | ||
|
|
7aefe5226a | ||
|
|
656c1bfd3a | ||
|
|
e237b609f5 | ||
|
|
057b9e954e | ||
|
|
f79c00d9be | ||
|
|
5f96d862ab | ||
|
|
79199bf023 | ||
|
|
beec4dddca | ||
|
|
7c243cb219 | ||
|
|
754e33af2a | ||
|
|
63cab7d751 | ||
|
|
503714a10b | ||
|
|
ada5be6ae0 | ||
|
|
2112494b43 | ||
|
|
c0b45ad71e | ||
|
|
5669d387af | ||
|
|
957f20a9a8 | ||
|
|
71bfc1cbda | ||
|
|
489ea3a980 | ||
|
|
8c6f655628 | ||
|
|
75d22d7988 | ||
|
|
a7bf043a9e | ||
|
|
402385faca | ||
|
|
cdd82fa456 | ||
|
|
2f7d99f3f6 | ||
|
|
e4799991ec | ||
|
|
66167e74dc | ||
|
|
5643d49bef | ||
|
|
81ec26e45c | ||
|
|
72c5ebcc06 | ||
|
|
ecf7575dd3 | ||
|
|
98a7f44dc1 | ||
|
|
5fce9c8d1f | ||
|
|
0ea89fccb8 | ||
|
|
2c2922d725 | ||
|
|
fbeefeca7d | ||
|
|
163ceef527 | ||
|
|
db5cc1f694 | ||
|
|
a3b9a7365c | ||
|
|
213b2a2802 | ||
|
|
229d09bb9e | ||
|
|
f127680c8c | ||
|
|
f767f7f1b9 | ||
|
|
acb1afa955 | ||
|
|
d132109925 | ||
|
|
820e417026 | ||
|
|
94bd0c606b | ||
|
|
9a8328e6db | ||
|
|
5c75d64a07 | ||
|
|
a8001995c8 | ||
|
|
9ba4d52fb7 | ||
|
|
0e613a1cab | ||
|
|
cf3d503a74 | ||
|
|
1ab46a96f9 | ||
|
|
1a3164ef32 | ||
|
|
bd62efcff5 | ||
|
|
7fc37b7c70 | ||
|
|
8ddccae15a | ||
|
|
675d7c8730 | ||
|
|
ba35d4a313 | ||
|
|
c1280ddcc2 | ||
|
|
36ded4c06a | ||
|
|
9fb276019e | ||
|
|
19982b1815 | ||
|
|
459d5b8f60 | ||
|
|
8ba5dc2352 | ||
|
|
8c73a7c7c2 | ||
|
|
e78dd41e88 | ||
|
|
59ecb056d0 | ||
|
|
11b17fec3a | ||
|
|
5ea81d0fd3 | ||
|
|
19cbd1f394 | ||
|
|
1b7265f866 | ||
|
|
1cdb64e78d | ||
|
|
eec8708249 | ||
|
|
ab003bf81f | ||
|
|
2d60901b6e | ||
|
|
3fc9bde4f4 | ||
|
|
4fc0df31fe | ||
|
|
3ac326e766 | ||
|
|
4770f9ddf6 | ||
|
|
7e60fd554a | ||
|
|
c1cd7ac129 | ||
|
|
aab62263a7 | ||
|
|
79889a0aac | ||
|
|
f413bfb3a0 | ||
|
|
2b0791f4a3 | ||
|
|
d95339534f | ||
|
|
82cf667f3b | ||
|
|
e20b3f75e4 | ||
|
|
6cca7b3e0e | ||
|
|
0b814af206 | ||
|
|
bfdabf9272 | ||
|
|
60988ff7f3 | ||
|
|
3649fd0c31 | ||
|
|
00c5aa041f | ||
|
|
4569b67007 | ||
|
|
1fb26bc441 | ||
|
|
e6d23a9701 | ||
|
|
0785266741 | ||
|
|
e752949752 | ||
|
|
199eb2b3e1 | ||
|
|
49cbea93fb | ||
|
|
451c410547 | ||
|
|
f6541720c4 | ||
|
|
5e5435e869 | ||
|
|
0d4f113d7d | ||
|
|
14fab0992f | ||
|
|
d7eb004bc1 | ||
|
|
c34f3ee653 | ||
|
|
96d595de39 | ||
|
|
b1f4508313 | ||
|
|
52ce59faaf | ||
|
|
85085ae0b2 | ||
|
|
c14cf9c260 | ||
|
|
a47c6f0774 | ||
|
|
888955bd9b | ||
|
|
6abf5e2c44 | ||
|
|
b1935c3550 | ||
|
|
e39d7750c5 | ||
|
|
1d83a48a1a | ||
|
|
802ee6c456 | ||
|
|
278085ba22 | ||
|
|
b945a8a04c | ||
|
|
7ef92071c5 | ||
|
|
c16ab95193 | ||
|
|
c5e2d9a9cc | ||
|
|
07df76b25e | ||
|
|
5b264565db | ||
|
|
a3561bd040 | ||
|
|
6e4f47e807 | ||
|
|
471965dc66 | ||
|
|
3b109ea2e7 | ||
|
|
6011526d5e | ||
|
|
1395d2971b | ||
|
|
e9d6badae7 | ||
|
|
65ddc7f24c | ||
|
|
fa871c7ada | ||
|
|
8652d6c136 | ||
|
|
16d976a145 | ||
|
|
fa1f5cc454 | ||
|
|
84c3b367d5 | ||
|
|
793aa6512d | ||
|
|
98ab99ab34 | ||
|
|
24a826bdd1 | ||
|
|
05245f5fc7 | ||
|
|
b718c8d044 | ||
|
|
2888a85081 | ||
|
|
307262244a | ||
|
|
9a875634f8 | ||
|
|
4af33486ae | ||
|
|
befa898f18 | ||
|
|
18525e1236 | ||
|
|
28ffd01cf4 | ||
|
|
09c7aa4440 | ||
|
|
ea4862d351 | ||
|
|
3e4d62329e | ||
|
|
d12366576b | ||
|
|
7b1d906494 | ||
|
|
0972c88b8b | ||
|
|
9464a26a7e | ||
|
|
10f1ad5cfe | ||
|
|
fd11eb8da0 | ||
|
|
62d5e99802 | ||
|
|
48305f0e95 | ||
|
|
8170b490f2 | ||
|
|
072962bbc3 | ||
|
|
33bc1cf7d9 | ||
|
|
85df9d1472 | ||
|
|
109ba3bf56 | ||
|
|
8083362e71 | ||
|
|
9b4c385a64 | ||
|
|
ee9c8ba4eb | ||
|
|
000a64d54a | ||
|
|
eba74d77a6 | ||
|
|
714a1bcb1d | ||
|
|
02d17dc2e4 | ||
|
|
4b54e776cc | ||
|
|
ba6f05b119 | ||
|
|
1d9ae120dc | ||
|
|
3ce841e050 | ||
|
|
436fc2ba13 | ||
|
|
77d652fc2b | ||
|
|
ac3681296e | ||
|
|
5254d3325f | ||
|
|
ce0a24a95d | ||
|
|
1bb596bf58 | ||
|
|
c384ac6080 | ||
|
|
61c2ce0f47 | ||
|
|
7a71315d33 | ||
|
|
0a658e5862 | ||
|
|
5f8c99aa0e | ||
|
|
4c6f1e4b4a | ||
|
|
226ae627f9 | ||
|
|
27a02aa918 | ||
|
|
3c43503df8 | ||
|
|
35c926d504 | ||
|
|
ea18ca5c60 | ||
|
|
55a56355d5 | ||
|
|
dc83ba2686 | ||
|
|
62615dfd0f | ||
|
|
a6998550a7 | ||
|
|
3b199170be | ||
|
|
1f93787a63 | ||
|
|
199c5b926a | ||
|
|
d9ad7085c3 | ||
|
|
df12f31800 | ||
|
|
ad205da3db | ||
|
|
34aab65db3 | ||
|
|
63c06a508e | ||
|
|
a2899c9c65 | ||
|
|
ff6d5e9efc | ||
|
|
f48fe0a7c0 | ||
|
|
5f6c8ca520 | ||
|
|
0eaa3a8d94 | ||
|
|
8ad190fa83 | ||
|
|
70f096c820 | ||
|
|
2840251862 | ||
|
|
b43966df22 | ||
|
|
cc22285beb | ||
|
|
b72d48b49f | ||
|
|
3a6b9c23c6 | ||
|
|
b2da364345 | ||
|
|
de7a6abc50 | ||
|
|
10f74349ca | ||
|
|
05a771c365 | ||
|
|
cfa2089d7b | ||
|
|
d56abd94a9 | ||
|
|
2f20ff8def | ||
|
|
9706daf330 | ||
|
|
a246b3e90c | ||
|
|
e28e1b239f | ||
|
|
4aead483de | ||
|
|
f8cc6e471e | ||
|
|
6b9ed9472d | ||
|
|
a763b08c41 | ||
|
|
178f904143 | ||
|
|
bb88fa3620 | ||
|
|
1e1249d8e0 | ||
|
|
bcb0e61bfc | ||
|
|
022ff89836 | ||
|
|
b9d4b8f6e8 | ||
|
|
0f5ce651cc | ||
|
|
6b8d5f92de | ||
|
|
55e556c725 | ||
|
|
19bb0a6ec2 | ||
|
|
290132f432 | ||
|
|
4a8be8e62d | ||
|
|
23b61aef0c | ||
|
|
24cc433a3d | ||
|
|
e014b7de81 | ||
|
|
0895a2bdea | ||
|
|
03ca4887ba | ||
|
|
9eeb17c397 | ||
|
|
6a5da2745a | ||
|
|
e1111ba2bb | ||
|
|
d186084835 | ||
|
|
06c2ba9fa9 | ||
|
|
b82e5fd8c6 | ||
|
|
6e1f96a832 | ||
|
|
f68135c7aa | ||
|
|
f48cbb457b | ||
|
|
8d192dc992 | ||
|
|
b70324aa24 | ||
|
|
390afaf614 | ||
|
|
5112322e7d | ||
|
|
2cb498d500 | ||
|
|
2bd6e02cdc | ||
|
|
85423cbc20 | ||
|
|
1c0d027bd3 | ||
|
|
5a8a023039 | ||
|
|
196b059cfb | ||
|
|
2d930b9c3d | ||
|
|
a5ba3faa49 | ||
|
|
02ba91f1bb | ||
|
|
bfa917e057 | ||
|
|
909dd0725a | ||
|
|
74860f2d16 | ||
|
|
132ebb4e74 | ||
|
|
698158cd93 | ||
|
|
5bfc684f1b | ||
|
|
c944c9b65b | ||
|
|
d61698b894 | ||
|
|
a4d32009ad | ||
|
|
3007875e35 | ||
|
|
b4aad138fc | ||
|
|
8df7eb2acb | ||
|
|
18cab6f861 | ||
|
|
b2071c65d8 | ||
|
|
402dba096e | ||
|
|
abf0c81de4 | ||
|
|
613985a17c | ||
|
|
bfc9801699 | ||
|
|
ee705eb979 | ||
|
|
67b94c7fde | ||
|
|
77e5d3f4bb | ||
|
|
30618b8644 | ||
|
|
57a2613286 | ||
|
|
e15bd89ba2 | ||
|
|
d2ed816f44 | ||
|
|
e51234928b | ||
|
|
3aa668aea3 | ||
|
|
870edab78a | ||
|
|
ebc9d9185d | ||
|
|
093150d4e3 | ||
|
|
de80a6692d | ||
|
|
c28f564a47 | ||
|
|
eb6a09c2bd | ||
|
|
19f404e092 | ||
|
|
55799ebb2d | ||
|
|
fdf4d8fdcf | ||
|
|
6dc11edafe | ||
|
|
c82ca1c69d | ||
|
|
7ef3d55cbf | ||
|
|
44e4f53827 | ||
|
|
643e490cbb | ||
|
|
e61498c3b6 | ||
|
|
bb6b61d810 | ||
|
|
cff173c2e6 | ||
|
|
226501d103 | ||
|
|
c5b8b0e3db | ||
|
|
46878e4363 | ||
|
|
f77682365e | ||
|
|
d9850fa660 | ||
|
|
9258585746 | ||
|
|
e635aaaa58 | ||
|
|
d0d6725df5 | ||
|
|
61f4fea9c3 | ||
|
|
66d59c1d6c | ||
|
|
f9725965e2 | ||
|
|
4629739a14 | ||
|
|
e9b3a1e99c | ||
|
|
8ac27b9dc7 | ||
|
|
2edd434474 | ||
|
|
bebf480321 | ||
|
|
10c09d9def | ||
|
|
6ce6b96e5c | ||
|
|
16a9cae80e | ||
|
|
e865e2ae6d | ||
|
|
06363a43f9 | ||
|
|
09a88e6a1c | ||
|
|
28baef8929 | ||
|
|
9b061a4c7c | ||
|
|
0b542dfbdf | ||
|
|
d3b039ebd8 | ||
|
|
c22924eed7 | ||
|
|
033ccb121f | ||
|
|
ecd91e8f2a | ||
|
|
1cdb954967 | ||
|
|
f309f87f55 | ||
|
|
989ab3094d | ||
|
|
70ac18d139 | ||
|
|
8f43236e2e | ||
|
|
38416d46a6 | ||
|
|
fb96b00922 | ||
|
|
0601ea2f39 | ||
|
|
a92d4f2af7 | ||
|
|
658305c969 | ||
|
|
8aed2be19b | ||
|
|
263f6e49d8 | ||
|
|
d822e38016 | ||
|
|
6cf78f19bb | ||
|
|
4a3319406c | ||
|
|
cb4418b973 | ||
|
|
5a797124b3 | ||
|
|
63430fbce6 | ||
|
|
bc2085139e | ||
|
|
f98c710f5b | ||
|
|
eb7101deff | ||
|
|
826f50da7e | ||
|
|
4e94c8ea56 | ||
|
|
3120eca721 | ||
|
|
26c9bcbc28 | ||
|
|
7a2e73a5d6 | ||
|
|
cd35ab5932 | ||
|
|
efaacdb534 | ||
|
|
5eb3c208f1 | ||
|
|
8347b62c1b | ||
|
|
48c3c7b4dc | ||
|
|
faefe078af | ||
|
|
66eb0481b5 | ||
|
|
0a0fc130d4 | ||
|
|
44afd7b657 | ||
|
|
165b572a5f | ||
|
|
1a30e622cc | ||
|
|
aec3238e42 | ||
|
|
249868dba7 | ||
|
|
f82e714b3c | ||
|
|
baa7eae77c | ||
|
|
c69542a34d | ||
|
|
9d45892603 | ||
|
|
e65f247b99 | ||
|
|
600f061e47 | ||
|
|
8ac0e9e751 | ||
|
|
772787fc22 | ||
|
|
985b33b65b | ||
|
|
ef7c5c2d2b | ||
|
|
0d49aafb54 | ||
|
|
98aae5ddc6 | ||
|
|
070e8606fa | ||
|
|
511b2848c3 | ||
|
|
7dd96d9a75 | ||
|
|
7d80f69ee8 | ||
|
|
85491cb7b5 | ||
|
|
3c0b88a1ee | ||
|
|
33c072f544 | ||
|
|
a8d08bca3f | ||
|
|
d7ddc56ab3 | ||
|
|
b562cd5c73 | ||
|
|
2549a41eb3 | ||
|
|
464f0fc231 | ||
|
|
5914fd9fb7 | ||
|
|
2e03217551 | ||
|
|
e0dc974f87 | ||
|
|
fdf1ed829d | ||
|
|
ba90490ad9 | ||
|
|
910be97f54 | ||
|
|
e162582045 | ||
|
|
cfd197d26c | ||
|
|
aa0486bc2b | ||
|
|
97a1fc62ae | ||
|
|
fd9dcd065a | ||
|
|
59997560eb | ||
|
|
026e71cc6e | ||
|
|
98ecc24425 | ||
|
|
3886006343 | ||
|
|
6d539c9203 | ||
|
|
5f778e61dd | ||
|
|
9860489f05 | ||
|
|
21ca8ac883 | ||
|
|
ec93becb17 | ||
|
|
0319445888 | ||
|
|
7cba9f50c8 | ||
|
|
9483c3afbc | ||
|
|
e518976534 | ||
|
|
3626cc2394 | ||
|
|
3a61fc7181 | ||
|
|
4a95fa5e87 | ||
|
|
8e3d1422f3 | ||
|
|
640a0b2627 | ||
|
|
30e0cb6515 | ||
|
|
e3253aacdb | ||
|
|
32f49d2122 | ||
|
|
b4bef44135 | ||
|
|
ce38742caf | ||
|
|
a7d39cc8d4 | ||
|
|
cb7eb660b9 | ||
|
|
bad28c60ae | ||
|
|
ab4c04085c | ||
|
|
761002f39d | ||
|
|
996f9c7f5d | ||
|
|
a6eca44a0d | ||
|
|
1820751801 | ||
|
|
030faaa5d1 | ||
|
|
95e1947352 | ||
|
|
7deb11a0a6 | ||
|
|
41c597801f | ||
|
|
128a138e74 | ||
|
|
ca74b5740a | ||
|
|
9afcbe1565 | ||
|
|
9e531a05e1 | ||
|
|
7c3562cea2 | ||
|
|
1edddf79d2 | ||
|
|
114f03e434 | ||
|
|
3ee1487985 | ||
|
|
a9eda2176e | ||
|
|
bb90bafb62 | ||
|
|
cea8783fec | ||
|
|
789d1fef84 | ||
|
|
d83939b165 | ||
|
|
3a4ec5c86a | ||
|
|
013c14530b | ||
|
|
ebf1cfc113 | ||
|
|
ec4d04c338 | ||
|
|
584b7790e4 | ||
|
|
ef25f66107 | ||
|
|
3060b34bdd | ||
|
|
3fb5c682f8 | ||
|
|
bb5dfa13ee | ||
|
|
9e391941c5 | ||
|
|
8b1d3e5fba | ||
|
|
07e322df96 | ||
|
|
a8959cbf26 | ||
|
|
d793e5bae5 | ||
|
|
29954fa9e8 | ||
|
|
0384fa9a51 | ||
|
|
75b19d3883 | ||
|
|
c15f84da08 | ||
|
|
8539d4caf1 | ||
|
|
3eb1fe5e4b | ||
|
|
16b88b697e | ||
|
|
08ba6ac831 | ||
|
|
49710618ff | ||
|
|
b4ba001617 | ||
|
|
87e0876cce | ||
|
|
87f5e3f102 | ||
|
|
e921d0db6e | ||
|
|
b7a85580fa | ||
|
|
2e93bc2e1d | ||
|
|
5ac15c8c49 | ||
|
|
98c05f3614 | ||
|
|
05af56defc | ||
|
|
ce48a2fc12 | ||
|
|
01e910af79 | ||
|
|
3e2ce9e94c | ||
|
|
9db602b274 | ||
|
|
a2d0ac7ee3 | ||
|
|
24cbd1a345 | ||
|
|
8b3e6742d5 | ||
|
|
62bd3f6e83 | ||
|
|
20ac2ff6e7 | ||
|
|
aa7c9e06a4 | ||
|
|
0c2fb7c0d9 | ||
|
|
7ec2b1da8c | ||
|
|
190c2b2756 | ||
|
|
7c975384cd | ||
|
|
fe042891a3 | ||
|
|
a9b594373d | ||
|
|
5edc3cde2a | ||
|
|
a636731764 | ||
|
|
b4433af9b5 | ||
|
|
72cc318607 | ||
|
|
5ae45381e2 | ||
|
|
b533d325a4 | ||
|
|
9dad7ff563 | ||
|
|
1ae2e07883 | ||
|
|
aa34850d4e | ||
|
|
9f524da642 | ||
|
|
8b707e23ca | ||
|
|
a4ea693c3c | ||
|
|
aca443a909 | ||
|
|
2ae5223da9 | ||
|
|
b5b67f2e6a | ||
|
|
fe723f5a53 | ||
|
|
c55e1ff6b7 | ||
|
|
4bd88e1220 | ||
|
|
f46af93528 | ||
|
|
8ead0e662a | ||
|
|
365ee01f96 | ||
|
|
fca6de3997 | ||
|
|
dceb265742 | ||
|
|
409096cbff | ||
|
|
e5a40faf82 | ||
|
|
859c78c785 | ||
|
|
89bff16053 | ||
|
|
a89476c538 | ||
|
|
f51b61e407 | ||
|
|
177103bccd | ||
|
|
f31d63aabd | ||
|
|
fd20246e8b | ||
|
|
0c1ea39a02 | ||
|
|
a409dd026d | ||
|
|
4731f8e5a7 | ||
|
|
7e05259b0e | ||
|
|
14ab85dc4f | ||
|
|
0651bfc4b8 | ||
|
|
21b94b2655 | ||
|
|
4e40c2341a | ||
|
|
d9a83eacd2 | ||
|
|
7b40674c0d | ||
|
|
936c1989f1 | ||
|
|
cfe336c37c | ||
|
|
d8a1e4aab0 | ||
|
|
be4d2afff3 | ||
|
|
c2a4ef5f93 | ||
|
|
b389d30728 | ||
|
|
22634b4ceb | ||
|
|
abc4975b3d | ||
|
|
36d81ff8d1 | ||
|
|
fe94190c2f | ||
|
|
f32027e15b | ||
|
|
4b6a92955b | ||
|
|
35a2da744c | ||
|
|
9d91340223 | ||
|
|
e0a56f75c3 | ||
|
|
4cfd30f9e8 | ||
|
|
3fbcbf0e5d | ||
|
|
8b7833e8b1 | ||
|
|
66441f133d | ||
|
|
8a12d6019a | ||
|
|
39c626dc75 | ||
|
|
a7480c3f29 | ||
|
|
8af682acf1 | ||
|
|
95eba1db81 | ||
|
|
0b8fde7d8d | ||
|
|
2f7517152a | ||
|
|
3e2ea0e087 | ||
|
|
723556d6a2 | ||
|
|
1f53d76cef | ||
|
|
d15488431b | ||
|
|
cf80fd7dc5 | ||
|
|
73d891b98e | ||
|
|
875ec1028d | ||
|
|
fd985c2011 | ||
|
|
47981004c9 | ||
|
|
e3f7c8f63d | ||
|
|
853db53f82 | ||
|
|
5992c0534a | ||
|
|
1874c93c5c | ||
|
|
3c4adb1aed | ||
|
|
66db918273 | ||
|
|
69845d5ddd | ||
|
|
42181d597b | ||
|
|
b56e9ca745 | ||
|
|
5fc4788269 | ||
|
|
d0f8293b73 | ||
|
|
44582bcd4b | ||
|
|
5c73aed953 | ||
|
|
e1ec48530e | ||
|
|
54c4053728 | ||
|
|
79ffb0df5c | ||
|
|
c510952c88 | ||
|
|
6109da531d | ||
|
|
56877332db | ||
|
|
6fc972d160 | ||
|
|
5346153d9b | ||
|
|
aaf266d272 | ||
|
|
0750db9aae | ||
|
|
316976d295 | ||
|
|
593b5d945b | ||
|
|
88f0240757 | ||
|
|
f5c2f8849d | ||
|
|
5c4a8f7803 | ||
|
|
5b8fdad5cb | ||
|
|
fe819f95ec | ||
|
|
be6728f8cb | ||
|
|
24d3a81bc8 | ||
|
|
268c7b5bcf | ||
|
|
64716a2de5 | ||
|
|
d2c8457ab1 | ||
|
|
667cb84af7 | ||
|
|
df8653cdd5 | ||
|
|
32f677ca0d | ||
|
|
6f5408f0d6 | ||
|
|
23c04fb10b | ||
|
|
0c5d6b1045 | ||
|
|
33f30decd1 | ||
|
|
9595b63939 | ||
|
|
b9695b09cd | ||
|
|
606885b23c | ||
|
|
bc7b8aadc4 | ||
|
|
d136b2065f | ||
|
|
3b2683463d | ||
|
|
989730d402 | ||
|
|
50f7209ba2 | ||
|
|
44b728c660 | ||
|
|
9abc5bbf96 | ||
|
|
56dd936e9c | ||
|
|
e982281cd4 | ||
|
|
a6b7b5fa94 | ||
|
|
ef00114aab | ||
|
|
ba4edc5c0e | ||
|
|
dae2d81764 | ||
|
|
cee9cd14c0 | ||
|
|
f1ec110673 | ||
|
|
7104a3b738 | ||
|
|
114951b18c | ||
|
|
3c85a602a4 | ||
|
|
a6415b8689 | ||
|
|
b37670de84 | ||
|
|
c9053bb0bc | ||
|
|
5362102be6 | ||
|
|
bf4601470b | ||
|
|
bd6274282b | ||
|
|
5a0f7df377 | ||
|
|
2e54be3df8 | ||
|
|
6625610aca | ||
|
|
5c9abfe97a | ||
|
|
e06f3d4180 | ||
|
|
331b4d8524 | ||
|
|
e3cc12da4f | ||
|
|
1e19f68cb5 | ||
|
|
c286b491d6 | ||
|
|
c7acdbf20d | ||
|
|
3cd0cc01c4 | ||
|
|
87d109727a | ||
|
|
47b6819ec8 | ||
|
|
00ee89a693 | ||
|
|
ac14b08af4 | ||
|
|
db97d7e836 | ||
|
|
5a0c80611e | ||
|
|
4e872865a3 | ||
|
|
aea39a83b6 | ||
|
|
3d80821203 | ||
|
|
d9bfcc7c8a | ||
|
|
8bd9a6c109 | ||
|
|
d89db24bfc | ||
|
|
352b5ca736 | ||
|
|
6bd9173a9d | ||
|
|
0cef3e1090 | ||
|
|
6bd68961d1 | ||
|
|
7f8ad917d9 | ||
|
|
7cd89accaf | ||
|
|
ffee084d2b | ||
|
|
2bb657a733 | ||
|
|
bc48171626 | ||
|
|
50924b0cd3 | ||
|
|
3d86950cc9 | ||
|
|
db9ddf9969 | ||
|
|
1b507370dc | ||
|
|
b9a3c508c9 | ||
|
|
9ae49e7169 | ||
|
|
7a1cdd62a4 | ||
|
|
a242881101 | ||
|
|
3c5e221c39 | ||
|
|
9c37f35d5a | ||
|
|
44ca59ac70 | ||
|
|
398dfce698 | ||
|
|
0ebe6bde3d | ||
|
|
4044070d76 | ||
|
|
8f05917d97 | ||
|
|
3766d67daa | ||
|
|
b1290c073e | ||
|
|
15f686fc69 | ||
|
|
36daf86ea2 | ||
|
|
4fb07a6ab3 | ||
|
|
8f2119272b | ||
|
|
ee5bd456e0 | ||
|
|
9c549ed4d8 | ||
|
|
61fc8b7968 | ||
|
|
6b30d65e05 | ||
|
|
10c876ac75 | ||
|
|
0966bd0bb1 | ||
|
|
294d1bfca4 | ||
|
|
af1d1236ea | ||
|
|
eaf9febdfd | ||
|
|
8748226ef3 | ||
|
|
73568777c0 | ||
|
|
c64697dde7 | ||
|
|
0701e38a04 | ||
|
|
2a27d96e08 | ||
|
|
ba42611701 | ||
|
|
54486138f0 | ||
|
|
13d3f506b0 | ||
|
|
32ca686e1f | ||
|
|
a5ef9ff372 | ||
|
|
738bfa7601 | ||
|
|
40cdd270b1 | ||
|
|
53a2a8015e | ||
|
|
15aaa440a2 | ||
|
|
d8a4014eff | ||
|
|
d25d423ccd | ||
|
|
49b0fde18b | ||
|
|
8df7f17186 | ||
|
|
adc395f888 | ||
|
|
e770664365 | ||
|
|
05d4ad3b5d | ||
|
|
cc6f726f71 | ||
|
|
a4923f894c | ||
|
|
12200f2e0d | ||
|
|
a853afc407 | ||
|
|
de471b0012 | ||
|
|
b6f1ad75b8 | ||
|
|
e6840f352d | ||
|
|
6456874f97 | ||
|
|
66b4a4b02a | ||
|
|
7e36b3f8e5 | ||
|
|
12061cc707 | ||
|
|
afcc62ecf6 | ||
|
|
bec6850c98 | ||
|
|
d253a06bab | ||
|
|
857c5c69b1 | ||
|
|
766fc49f39 | ||
|
|
941e09ca9f | ||
|
|
2466a97fb8 | ||
|
|
81f92f5182 | ||
|
|
91e1d442ff | ||
|
|
a1d6ae2296 | ||
|
|
b529fd3bea | ||
|
|
bf319cf593 | ||
|
|
15eedd2a84 | ||
|
|
d0cd3d1c32 | ||
|
|
747786d0c8 | ||
|
|
b232255170 | ||
|
|
bd2982ea69 | ||
|
|
1c948cc83c | ||
|
|
ccde1e51ad | ||
|
|
03ec940352 | ||
|
|
bd5b15e279 | ||
|
|
b6897a4577 | ||
|
|
f7225523ec | ||
|
|
6a0b8e0722 | ||
|
|
9d9509525c | ||
|
|
b1dbb3570b | ||
|
|
c075160e5d | ||
|
|
612ceba98a | ||
|
|
7d5e0040bc | ||
|
|
d6e19d2000 | ||
|
|
a01d5db2a0 | ||
|
|
5de3baffd4 | ||
|
|
63c10e8f02 | ||
|
|
a99e7c2783 | ||
|
|
88b1cc553f | ||
|
|
316e8dedd3 | ||
|
|
bb040eb5a8 | ||
|
|
f106a76cd5 | ||
|
|
95b2bea828 | ||
|
|
51a0ad70aa | ||
|
|
71faa5f89e | ||
|
|
58d6166592 | ||
|
|
d42f66bfed | ||
|
|
6cf0554e23 | ||
|
|
5bd8579e73 | ||
|
|
01cd0b6b87 | ||
|
|
b4aec552fc | ||
|
|
93ab606d94 | ||
|
|
94e94f136d | ||
|
|
1b57128ef6 | ||
|
|
219a2b0798 | ||
|
|
b37d5b0fda | ||
|
|
0e9aac14eb | ||
|
|
cf81ab0306 | ||
|
|
00d8148e46 | ||
|
|
0b59281dbb | ||
|
|
e0c845ca16 | ||
|
|
d6bff57c7d | ||
|
|
5c4b4d764e | ||
|
|
bf13b5b931 | ||
|
|
afade0a5ac | ||
|
|
40da8736d4 | ||
|
|
a55675b440 | ||
|
|
6ce71c7506 | ||
|
|
0dda91078d | ||
|
|
93632f5c76 | ||
|
|
cb4cd10013 | ||
|
|
62bcf09ab4 | ||
|
|
b466dc1970 | ||
|
|
0a10eb66cc | ||
|
|
c6322c00aa | ||
|
|
b549a4bddf | ||
|
|
3fa50f2a1a | ||
|
|
ddded0ebfb | ||
|
|
71c0945607 | ||
|
|
f0295c5dc5 | ||
|
|
4e1286a8cf | ||
|
|
d69cead362 | ||
|
|
7699cffa26 | ||
|
|
1021fc566f | ||
|
|
1fb3b2c373 | ||
|
|
2428000262 | ||
|
|
3d5b4f3191 | ||
|
|
eb6a217f4a | ||
|
|
06aaf98716 | ||
|
|
26fc1fd7a6 | ||
|
|
a9aa3c4fd8 | ||
|
|
61d4509a8e | ||
|
|
8cff4f4ff1 | ||
|
|
5dc30e02c4 | ||
|
|
55f070e12c | ||
|
|
0afb8f51c3 | ||
|
|
42f2637078 | ||
|
|
bbec7c6610 | ||
|
|
76fc257661 | ||
|
|
58ce50571a | ||
|
|
14205d2810 | ||
|
|
d798fc4b3f | ||
|
|
d29d07cb2d | ||
|
|
07a0b360f6 | ||
|
|
8b253a8a61 | ||
|
|
fddbf96c9c | ||
|
|
d1d01ae4b8 | ||
|
|
51706afc46 | ||
|
|
d4ea23b1ac | ||
|
|
0460beccf0 | ||
|
|
aa5ed17dfa | ||
|
|
32173b19c9 | ||
|
|
1a8fd7dd92 | ||
|
|
f0047bc1aa | ||
|
|
917832e0ae | ||
|
|
cf8948ac69 | ||
|
|
b2df639155 | ||
|
|
70ace09ff5 | ||
|
|
35a69f595a | ||
|
|
f4c4a931d2 | ||
|
|
7caced2fe8 | ||
|
|
846e5deb36 | ||
|
|
eca328b247 | ||
|
|
c0e9091e4b | ||
|
|
6b6e417435 | ||
|
|
954bb7039c | ||
|
|
ae01f517c7 | ||
|
|
385bfe07e2 | ||
|
|
25aff6a53b | ||
|
|
edcbf79b85 | ||
|
|
2591b8e10c | ||
|
|
9df9d1667f | ||
|
|
7798111af1 | ||
|
|
12351113a9 | ||
|
|
d9256f99af | ||
|
|
cf021066ed | ||
|
|
04eb2a982f | ||
|
|
22dcc787b5 | ||
|
|
5d4d0c0a86 | ||
|
|
e81db9728a | ||
|
|
db305af8c9 | ||
|
|
4b3aca7773 | ||
|
|
5b5abe99e7 | ||
|
|
8f670eb755 | ||
|
|
21a604814c | ||
|
|
7eeb835d96 | ||
|
|
57de915133 | ||
|
|
a892de5c2d | ||
|
|
69cd01955b | ||
|
|
f39809c941 | ||
|
|
09c4bfeb51 | ||
|
|
615789a9ad | ||
|
|
bec5eaf3c9 | ||
|
|
4f13ef9cea | ||
|
|
873de48beb | ||
|
|
87e70b86d3 | ||
|
|
140aa85223 | ||
|
|
3ac3207497 | ||
|
|
e36a0b9a30 | ||
|
|
0b1aac7687 | ||
|
|
e008cde2ff | ||
|
|
d1e46be8ad | ||
|
|
dc18a18248 | ||
|
|
b9a0ad73ab | ||
|
|
e2c3fb309c | ||
|
|
d5255b8cf4 | ||
|
|
42e70e870b | ||
|
|
8ffd7b0197 | ||
|
|
01ead194d8 | ||
|
|
80b9d4be50 | ||
|
|
ef06836804 | ||
|
|
916870b546 | ||
|
|
2da7216be6 | ||
|
|
54215cff7a | ||
|
|
166257bbdc | ||
|
|
d502e04cbd | ||
|
|
1fca680a67 | ||
|
|
4ea3238391 | ||
|
|
fa12e7bd97 | ||
|
|
6118535c4a | ||
|
|
920f04aab3 | ||
|
|
ed13f2d6ef | ||
|
|
dff27fe7b3 | ||
|
|
5d589e7330 | ||
|
|
01ec16f472 | ||
|
|
f510d4bc10 | ||
|
|
2db2eb13af | ||
|
|
82e1c07722 | ||
|
|
23ba078a17 | ||
|
|
b5358e7565 | ||
|
|
697699bd5f | ||
|
|
dd2a806ab8 | ||
|
|
84d96cebee | ||
|
|
10658606d7 | ||
|
|
f72d89fa76 | ||
|
|
f9f4a8e7ad | ||
|
|
fd58e83da9 | ||
|
|
bfcedfdb2a | ||
|
|
d11e030150 | ||
|
|
6103640b53 | ||
|
|
259199897b | ||
|
|
ee498b9e2b | ||
|
|
18a464b1d2 | ||
|
|
d1c8e34540 | ||
|
|
a151846f1c | ||
|
|
9f19b0bc9e | ||
|
|
289fe76adc | ||
|
|
1eb1c44926 | ||
|
|
bc09e4204b | ||
|
|
1a2948df85 | ||
|
|
16df15cf55 | ||
|
|
0566bad6d9 | ||
|
|
edc90ccc00 | ||
|
|
3688602d16 | ||
|
|
0deadc5cf2 | ||
|
|
10ac435d53 | ||
|
|
16f025181f | ||
|
|
3808f60e69 | ||
|
|
a00615bd4e | ||
|
|
14bc2c7232 | ||
|
|
76d286703c | ||
|
|
c80a5b59ab | ||
|
|
db6882e9f5 | ||
|
|
3fd9d9622b | ||
|
|
5ae4c891de | ||
|
|
fb2e7cb009 | ||
|
|
8124f0ac7f | ||
|
|
446f571bec | ||
|
|
142ae76542 | ||
|
|
ed1873f47e | ||
|
|
0ee04e6ef3 | ||
|
|
1e4475b275 | ||
|
|
9dd9743943 | ||
|
|
5fbcebf80b | ||
|
|
852b016389 | ||
|
|
73f28d7653 | ||
|
|
1f28678c27 | ||
|
|
daba68265c | ||
|
|
6d04481c27 | ||
|
|
ed5d6f73bb | ||
|
|
d0360e9e68 | ||
|
|
32ddda404c | ||
|
|
41de667e3d | ||
|
|
8530e70af6 | ||
|
|
7a840ad15f | ||
|
|
682c2721d2 | ||
|
|
fb56795cbd | ||
|
|
15aa4ecc5d | ||
|
|
351d7d22fb | ||
|
|
79999887a9 | ||
|
|
25d74ed649 | ||
|
|
9346666b3e | ||
|
|
13453552b5 | ||
|
|
ef38074b55 | ||
|
|
e5e8eea7ac | ||
|
|
9be2efc4f2 | ||
|
|
990b7a2d20 | ||
|
|
8d6dd62ef4 | ||
|
|
69d09e8133 | ||
|
|
6671b211e0 | ||
|
|
307e815e97 | ||
|
|
d8e2bd6ff5 | ||
|
|
e74c2f686b | ||
|
|
c7d5115a56 | ||
|
|
774ba11a92 | ||
|
|
322edbdc20 | ||
|
|
c1ba551e07 | ||
|
|
9917412329 | ||
|
|
2f4adb4d5f | ||
|
|
b61b864094 | ||
|
|
fa193276c9 | ||
|
|
0ca09c384a | ||
|
|
a6a39cc4e6 | ||
|
|
c9f84f6259 | ||
|
|
07063ca4f0 | ||
|
|
b5cfdcf875 | ||
|
|
373db25077 | ||
|
|
f8c2ebe61a | ||
|
|
ae23fade1e | ||
|
|
5386c05c0d | ||
|
|
aed94c8aaf | ||
|
|
37185fc4d5 | ||
|
|
cc64c6c9f7 | ||
|
|
0c0782ccd7 | ||
|
|
5bc9f9e995 | ||
|
|
22402d1741 | ||
|
|
8f203b07a1 | ||
|
|
9c157246b7 | ||
|
|
d0dfe1ef7f | ||
|
|
a9ccc7e2aa | ||
|
|
63edbae1be | ||
|
|
8afe537497 | ||
|
|
f33844d8f1 | ||
|
|
c750d00355 | ||
|
|
bb9b39e3c0 | ||
|
|
057b89ab8e | ||
|
|
23fc4bec36 | ||
|
|
6b82fb9ddb | ||
|
|
a3ca5a36e8 | ||
|
|
f57c91847d | ||
|
|
eda4dc83a3 | ||
|
|
5a0bf8071e | ||
|
|
09dfc6a34b | ||
|
|
3b8ebe9a59 | ||
|
|
2ba1092809 | ||
|
|
7c97ab5408 | ||
|
|
ac1991f8d1 | ||
|
|
2a573f6ac5 | ||
|
|
9833d0cce6 | ||
|
|
fbc3ed0213 | ||
|
|
c916a76e6b | ||
|
|
ae1bfaf0c8 | ||
|
|
0aedff4fec | ||
|
|
73d88a3491 | ||
|
|
5d389337cd | ||
|
|
a977597217 | ||
|
|
b3b4106b99 | ||
|
|
7f29eed326 | ||
|
|
ec895a4f31 | ||
|
|
fe0c1745e1 | ||
|
|
3fc0a96bb0 | ||
|
|
c154f342c2 | ||
|
|
8f1666dcca | ||
|
|
9aa4750f55 | ||
|
|
c52d985d45 | ||
|
|
376d8d9a38 | ||
|
|
08de0a4e79 | ||
|
|
11d327edcf | ||
|
|
d2f7b83ea7 | ||
|
|
72ca1b39e8 | ||
|
|
69bd234abc | ||
|
|
94e6978abf | ||
|
|
b5272cbf4d | ||
|
|
edb213089c | ||
|
|
b772cf3e5a | ||
|
|
e86d043794 | ||
|
|
4727187071 | ||
|
|
d8b8f5424c | ||
|
|
8425c99a4e | ||
|
|
c023dbbc1c | ||
|
|
af516f42b4 | ||
|
|
dbd8e6a08d | ||
|
|
c24bec9bc6 | ||
|
|
9854598648 | ||
|
|
2cdf73adab | ||
|
|
1e7e2e5e97 | ||
|
|
081e496878 | ||
|
|
aaff7f463a | ||
|
|
55f937bf51 | ||
|
|
d5d1d061bb | ||
|
|
bc6f602891 | ||
|
|
ca461057e7 | ||
|
|
b1c5c2468a | ||
|
|
562ce3192f | ||
|
|
3787dd98b4 | ||
|
|
6c667e4325 | ||
|
|
0eec693a85 | ||
|
|
c3bf672c2a | ||
|
|
c3a3b6412f | ||
|
|
44291b842a | ||
|
|
36cf502b56 | ||
|
|
2df77cf280 | ||
|
|
a453e49c27 | ||
|
|
e34c34de46 | ||
|
|
8dc5bf96e3 | ||
|
|
d2c3e1d1ae | ||
|
|
4eab101b78 | ||
|
|
e460d6d15b | ||
|
|
3012f68a56 | ||
|
|
1909050be2 | ||
|
|
d4c62c7295 | ||
|
|
4eb3d1b918 | ||
|
|
fb6bf50e48 | ||
|
|
31125dedc8 | ||
|
|
a2e2d70660 | ||
|
|
d8213f99b1 | ||
|
|
7fe1b02ccd | ||
|
|
7d7b759930 | ||
|
|
04e27496bd | ||
|
|
51e2e5ec9c | ||
|
|
6f2bc555e0 | ||
|
|
a8c43ddf4a | ||
|
|
3eabc27877 | ||
|
|
cd9602e641 | ||
|
|
ad379bd766 | ||
|
|
8303991217 | ||
|
|
c1047535d4 | ||
|
|
12eae2c002 | ||
|
|
10142cc00b | ||
|
|
5e1487d12a | ||
|
|
39e0c13701 | ||
|
|
c80d984ee6 | ||
|
|
3e474767d1 | ||
|
|
e2b954439c | ||
|
|
950d1eb5c3 | ||
|
|
f48a2520c3 | ||
|
|
f366d41034 | ||
|
|
b3c40e1ba7 | ||
|
|
b686d6e011 | ||
|
|
99489e5e77 | ||
|
|
5c9ff468cc | ||
|
|
bf18307168 | ||
|
|
5663198bfb | ||
|
|
bad50fd78b | ||
|
|
9bd43e3f74 | ||
|
|
f0fefab8ad | ||
|
|
449231c791 | ||
|
|
bd161ec677 | ||
|
|
8040d4ac2d | ||
|
|
06d7820566 | ||
|
|
4818a3feee | ||
|
|
9fcb2c0733 | ||
|
|
6906b4159a | ||
|
|
763b9309f6 | ||
|
|
2bb4d1c22b | ||
|
|
23303363ee | ||
|
|
79c17abad2 | ||
|
|
3234e0e3f0 | ||
|
|
982cd1e1f3 | ||
|
|
df39fc86a4 | ||
|
|
870e0c4144 | ||
|
|
57704b706e | ||
|
|
223e0dfd1f | ||
|
|
51c438b0b6 | ||
|
|
b3fa76f8c5 | ||
|
|
93d210a754 | ||
|
|
8a77242072 | ||
|
|
c453df55d6 | ||
|
|
265ee15ac7 | ||
|
|
d0da47e0b3 | ||
|
|
0e8553d1a7 | ||
|
|
9229dd2fd5 | ||
|
|
75d69050d5 | ||
|
|
c2a8744240 | ||
|
|
bc7e07f6a6 | ||
|
|
bfd6f8965e | ||
|
|
eb1e4a1aea | ||
|
|
dc3e8a9cb5 | ||
|
|
494bcc1711 | ||
|
|
7e071d9f23 | ||
|
|
6f821222db | ||
|
|
6e464dbc81 | ||
|
|
be8ef370c6 | ||
|
|
39a05665b0 | ||
|
|
737e22116a | ||
|
|
43e1e4829f | ||
|
|
390285d9e5 | ||
|
|
c95778178f | ||
|
|
04870313b7 | ||
|
|
6ca040149c | ||
|
|
e487b9d46b | ||
|
|
1375e16ad2 | ||
|
|
312f1f0085 | ||
|
|
721900fc47 | ||
|
|
2d815a92a3 | ||
|
|
1c192b7c11 | ||
|
|
8ef15df7c0 | ||
|
|
4a887336bc | ||
|
|
8f6521f942 | ||
|
|
fbdfaa4dc7 | ||
|
|
bf4290db3e | ||
|
|
94ad633128 | ||
|
|
c552917991 | ||
|
|
a7ee8c853e | ||
|
|
29e4879451 | ||
|
|
8b92344808 | ||
|
|
0877cec2e6 | ||
|
|
b1ca577be7 | ||
|
|
9b484f5ac9 | ||
|
|
b6a9fd81da | ||
|
|
109f9567ea | ||
|
|
f19113f88e | ||
|
|
3837bee51f | ||
|
|
748eadd225 | ||
|
|
b8e115ddf6 | ||
|
|
11d4df4f7d | ||
|
|
0c285f21c1 | ||
|
|
89c3296632 | ||
|
|
db55f0696e | ||
|
|
03d4ae9058 | ||
|
|
f8b41b703c | ||
|
|
c9bf017637 | ||
|
|
2a989e455c | ||
|
|
cd24decca0 | ||
|
|
0d78150f10 | ||
|
|
f36946a8aa | ||
|
|
5c51619798 | ||
|
|
f39842a001 | ||
|
|
a022bdb30d | ||
|
|
2cfb91d0ce | ||
|
|
2a39526a4c | ||
|
|
ded5d4c98b | ||
|
|
a0ca59c3f2 | ||
|
|
5885d76b89 | ||
|
|
53cfc49807 | ||
|
|
942eb579e4 | ||
|
|
5cb1a2d120 | ||
|
|
5819cfe412 | ||
|
|
5cb62ca412 | ||
|
|
df10c245de | ||
|
|
4a804dc52b | ||
|
|
53fa339363 | ||
|
|
5f0bb0c6ce | ||
|
|
ed2f25a998 | ||
|
|
7510c9fe29 | ||
|
|
78a1d53728 | ||
|
|
e9b078cd58 | ||
|
|
dd8b928684 | ||
|
|
3dec6ac9f1 | ||
|
|
185b574bdc | ||
|
|
a89726a8c6 | ||
|
|
c80aca27e6 | ||
|
|
4c9ec582dc | ||
|
|
029acab333 | ||
|
|
4f9f10e130 | ||
|
|
9ba11d2e14 | ||
|
|
23a5a1f79f | ||
|
|
e8dc617d40 | ||
|
|
28b000c820 | ||
|
|
d56794e846 | ||
|
|
30320e0ac6 | ||
|
|
88b682a317 | ||
|
|
2663ec7da0 | ||
|
|
eec4ae98cd | ||
|
|
6c5a5c0882 | ||
|
|
1d27fffe44 | ||
|
|
a3383b1f98 | ||
|
|
f9c2b0acd1 | ||
|
|
a9444ed879 | ||
|
|
8d5a3ecd69 | ||
|
|
c31a0f4e09 | ||
|
|
44ff676eef | ||
|
|
4bb017b740 | ||
|
|
0f2435c308 | ||
|
|
739db23514 | ||
|
|
8598fb444b | ||
|
|
0cd56f4d4c | ||
|
|
0b630ff504 | ||
|
|
84169dea3d | ||
|
|
e328ec2382 | ||
|
|
d83b5de47a | ||
|
|
2719c4240f | ||
|
|
4b5ac67993 | ||
|
|
d749756b53 | ||
|
|
0401c61c15 | ||
|
|
34f45da2de | ||
|
|
baecbf783c | ||
|
|
2f141cd6e0 | ||
|
|
1296299d02 | ||
|
|
998ac74d32 | ||
|
|
b4a34e6432 | ||
|
|
cb73218dfe | ||
|
|
5523c2d34a | ||
|
|
01889c45a2 | ||
|
|
e89b4a151e | ||
|
|
ec235eafe8 | ||
|
|
d99720258a | ||
|
|
3f064322e4 | ||
|
|
11592279e2 | ||
|
|
31b4923eb2 | ||
|
|
cfdfb9a907 | ||
|
|
e7a21c821e | ||
|
|
e70c9d55db | ||
|
|
268aee6265 | ||
|
|
255422d5be | ||
|
|
1ba7b0e0fb | ||
|
|
72788fdb11 | ||
|
|
435afec13c | ||
|
|
2cb1877669 | ||
|
|
edd672cba7 | ||
|
|
991f37fe05 | ||
|
|
c147d8004b | ||
|
|
cdcc4dfda8 | ||
|
|
2eaba686fb | ||
|
|
236032b4a6 | ||
|
|
5fcba59b3e | ||
|
|
6efd8fddeb | ||
|
|
02b4990cb1 | ||
|
|
8aff2b9e74 | ||
|
|
fbae432b98 | ||
|
|
9cad7773ff | ||
|
|
4adf122486 | ||
|
|
ea47c26d3f | ||
|
|
f57aae9545 | ||
|
|
cdeb830706 | ||
|
|
0c9618f19a | ||
|
|
1cd9d07d8c | ||
|
|
f028649582 | ||
|
|
11bf39374c | ||
|
|
d57236959a | ||
|
|
ebe975f463 | ||
|
|
a94267fc98 | ||
|
|
f186ea7cc3 | ||
|
|
29e05b1caa | ||
|
|
6945a712df | ||
|
|
04cf382de5 | ||
|
|
03048d7d2f | ||
|
|
38884bc0e6 | ||
|
|
a4d0394d1a | ||
|
|
28b768b146 | ||
|
|
8292e78ef2 | ||
|
|
c1e4dceb01 | ||
|
|
6243404d1d | ||
|
|
954d14cd66 | ||
|
|
2f5e9e2e26 | ||
|
|
b3c058593f | ||
|
|
3e47e11992 | ||
|
|
6fe67c93fe | ||
|
|
8c7dfdcef2 | ||
|
|
c88591489d | ||
|
|
719404b6cf | ||
|
|
f2c27489c8 | ||
|
|
d6a0c93f2f | ||
|
|
c64d5fd2e3 | ||
|
|
5b62aeb73a | ||
|
|
7e83f2dd4a | ||
|
|
ed48f84355 | ||
|
|
f3d15cd4a5 | ||
|
|
8c270269db | ||
|
|
bea605310a | ||
|
|
8184894563 | ||
|
|
47a87cc298 | ||
|
|
553a6347e6 | ||
|
|
422b65d934 | ||
|
|
a35ebd57f9 | ||
|
|
97174d7af0 | ||
|
|
659268c04a | ||
|
|
67d06c5efa | ||
|
|
6e6d8c0bc5 | ||
|
|
658af3edcf | ||
|
|
9753d9dc7e | ||
|
|
2fa3a3c47e | ||
|
|
4e331cfb35 | ||
|
|
a1fa94707b | ||
|
|
88f1107ed6 | ||
|
|
27e4810239 | ||
|
|
e97b9fcc60 | ||
|
|
71fe643099 | ||
|
|
74874a459d | ||
|
|
7c5fc17500 | ||
|
|
cbdae3547b | ||
|
|
a5d122c0b3 | ||
|
|
26aefadfba | ||
|
|
51a28842cf | ||
|
|
210c2f3cc1 | ||
|
|
773c326eb7 | ||
|
|
cb2fb026c5 | ||
|
|
47b662be09 | ||
|
|
a4731ad054 | ||
|
|
aa33938fb5 | ||
|
|
edfe8f1ad0 | ||
|
|
41399a2593 | ||
|
|
2a4c467ab8 | ||
|
|
1ee09825a0 | ||
|
|
6be6092c0e | ||
|
|
e76584b0da | ||
|
|
0a679da968 | ||
|
|
59d174004e | ||
|
|
d0d0d95475 | ||
|
|
b3816615db | ||
|
|
b08a6840f5 | ||
|
|
212d0bd55a | ||
|
|
77ada9c151 | ||
|
|
712ada940e | ||
|
|
222e6b6611 | ||
|
|
ba690c6346 | ||
|
|
e910e19f57 | ||
|
|
0c2532b0b5 | ||
|
|
9c9b17a5f0 | ||
|
|
816dea91ec | ||
|
|
c228f8d4d5 | ||
|
|
05bb99fad4 | ||
|
|
51b2457b3d | ||
|
|
ed71fca23e | ||
|
|
20e8e72ac2 | ||
|
|
13fe0eb882 | ||
|
|
e0476c9030 | ||
|
|
70c93c7be7 | ||
|
|
b73fc70ecf | ||
|
|
eab33150ad | ||
|
|
fca82fd775 | ||
|
|
37c8ba8ddd | ||
|
|
f87011b5c2 | ||
|
|
7f149700f8 | ||
|
|
78ba9070fc | ||
|
|
e31e5e1f69 | ||
|
|
31d9027677 | ||
|
|
debcd6f353 | ||
|
|
5cb1681922 | ||
|
|
9074bccea0 | ||
|
|
21c16d2009 | ||
|
|
56413ecce6 | ||
|
|
291798f574 | ||
|
|
b104843ae1 | ||
|
|
e94f2a95de | ||
|
|
c2a43b69a9 | ||
|
|
dd062c656f | ||
|
|
ae2eb718c6 | ||
|
|
7ac26bb653 | ||
|
|
41a726e8a7 | ||
|
|
4b69216548 | ||
|
|
99395ddf5a | ||
|
|
5f9fa5c352 | ||
|
|
9013331917 | ||
|
|
3a8f80477b | ||
|
|
c90e0fd21e | ||
|
|
6744621415 | ||
|
|
a6680a775f | ||
|
|
5a67be2292 | ||
|
|
c8b2b34138 | ||
|
|
af8f4676ba | ||
|
|
b51cb9d84a | ||
|
|
ec7a61021f | ||
|
|
8d0d19132e | ||
|
|
d2bde5f0b1 | ||
|
|
3f9ae5d6bf | ||
|
|
9b97e26b58 | ||
|
|
219032bbbb | ||
|
|
f0fd4ea45c | ||
|
|
23a5a275f8 | ||
|
|
813c680ed0 | ||
|
|
3a1bfa91d1 | ||
|
|
a0eccd615f | ||
|
|
59be539ecd | ||
|
|
a04740114c | ||
|
|
a4f77dfcd0 | ||
|
|
60b5d71c74 | ||
|
|
795ba3e365 | ||
|
|
83ef4234bc | ||
|
|
b12b464462 | ||
|
|
04726ba697 | ||
|
|
4d607ada9d | ||
|
|
0a8b4b0c43 | ||
|
|
2c7cf9faa1 | ||
|
|
ec21105c47 | ||
|
|
444258e7ee | ||
|
|
e6fd05c2bd | ||
|
|
9fdcd452d0 | ||
|
|
f39b9d5618 | ||
|
|
76e4c4919d | ||
|
|
d1f159cdb4 | ||
|
|
c63065e460 | ||
|
|
124c1d94a4 | ||
|
|
e9161b726a | ||
|
|
fd0d27b192 | ||
|
|
50064a40fe | ||
|
|
c9bc5fc38e | ||
|
|
58f533fe50 | ||
|
|
efcdffd8ff | ||
|
|
22793c3886 | ||
|
|
797ddbacc0 | ||
|
|
e011962469 | ||
|
|
b376ad9815 | ||
|
|
77248fe65c | ||
|
|
1dad115203 | ||
|
|
8812d58031 | ||
|
|
fff7568f7e | ||
|
|
ff6662579d | ||
|
|
0cf9fbd909 | ||
|
|
7efa2fd072 | ||
|
|
9adf5167c9 | ||
|
|
4978984d75 | ||
|
|
3b7ef4615a | ||
|
|
af8f4b64c0 | ||
|
|
848b745fcb | ||
|
|
93042d862d | ||
|
|
ba5424c250 | ||
|
|
afdde9b032 | ||
|
|
9a35c40b24 | ||
|
|
1f1e6124cd | ||
|
|
a033480500 | ||
|
|
033df970ad | ||
|
|
dd80a795a0 | ||
|
|
1eec6a39c6 | ||
|
|
14333e2910 | ||
|
|
dd6b8face9 | ||
|
|
288de7e03a | ||
|
|
a760ef4d22 | ||
|
|
20df96b6ba | ||
|
|
0dd745bce4 | ||
|
|
d4d5d371ac | ||
|
|
205bf4ddbd | ||
|
|
4ab84d42c6 | ||
|
|
ee74badf3a | ||
|
|
aa173ff74c | ||
|
|
a7729e1597 | ||
|
|
b584fc33f5 | ||
|
|
15c9d8682e | ||
|
|
361be8c26b | ||
|
|
4db9a5edd6 | ||
|
|
bcc878da43 | ||
|
|
79f179fed4 | ||
|
|
a924a9a627 | ||
|
|
45d444df0e | ||
|
|
92461a3366 | ||
|
|
032a430c51 | ||
|
|
a6a3855e79 | ||
|
|
29c3233375 | ||
|
|
2386545814 | ||
|
|
2059152dd3 | ||
|
|
32d2c260ab | ||
|
|
384c7873aa | ||
|
|
d0eab70974 | ||
|
|
f3cbb91527 | ||
|
|
9772cfe1f2 | ||
|
|
3557e8a125 | ||
|
|
9e0bb6ca34 | ||
|
|
bb74eef601 | ||
|
|
d1db38ba8e | ||
|
|
e9af9fb16b | ||
|
|
fd74be8848 | ||
|
|
926fafd7f6 | ||
|
|
c51c715cee | ||
|
|
aa80210075 | ||
|
|
768654ae63 | ||
|
|
6efb291449 | ||
|
|
7a431b9b83 | ||
|
|
c78d09df66 | ||
|
|
7d30d9e867 | ||
|
|
d3fb244cef |
7
.gitattributes
vendored
7
.gitattributes
vendored
@@ -1,6 +1,7 @@
|
|||||||
# Skip files when using git archive
|
# following files are skipped when exporting using git archive
|
||||||
|
/release export-ignore
|
||||||
|
/admin export-ignore
|
||||||
|
test export-ignore
|
||||||
.gitattributes export-ignore
|
.gitattributes export-ignore
|
||||||
.gitignore export-ignore
|
.gitignore export-ignore
|
||||||
/scripts export-ignore
|
|
||||||
test export-ignore
|
|
||||||
|
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -3,11 +3,9 @@ coverage/
|
|||||||
docs/
|
docs/
|
||||||
webadmin/dist/
|
webadmin/dist/
|
||||||
setup/splash/website/
|
setup/splash/website/
|
||||||
|
installer/src/certs/server.key
|
||||||
|
|
||||||
# vim swam files
|
# vim swap files
|
||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
# supervisor
|
|
||||||
supervisord.pid
|
|
||||||
supervisord.log
|
|
||||||
|
|
||||||
|
|||||||
537
CHANGES
Normal file
537
CHANGES
Normal file
@@ -0,0 +1,537 @@
|
|||||||
|
[0.0.1]
|
||||||
|
- Hot Chocolate
|
||||||
|
|
||||||
|
[0.0.2]
|
||||||
|
- Hotfix appstore ui in webadim
|
||||||
|
|
||||||
|
[0.0.3]
|
||||||
|
- Tall Pike
|
||||||
|
|
||||||
|
[0.0.4]
|
||||||
|
- This will be 0.0.4 changes
|
||||||
|
|
||||||
|
[0.0.5]
|
||||||
|
- App install/configure route fixes
|
||||||
|
|
||||||
|
[0.0.6]
|
||||||
|
- Not sure what happenned here
|
||||||
|
|
||||||
|
[0.0.7]
|
||||||
|
- resetToken is now sent as part of create user
|
||||||
|
- Same as 0.0.7 which got released by mistake
|
||||||
|
|
||||||
|
[0.0.8]
|
||||||
|
- Manifest changes
|
||||||
|
|
||||||
|
[0.0.9]
|
||||||
|
- Fix app restore
|
||||||
|
- Fix backup issues
|
||||||
|
|
||||||
|
[0.0.10]
|
||||||
|
- Unknown orchestra
|
||||||
|
|
||||||
|
[0.0.11]
|
||||||
|
- Add ldap addon
|
||||||
|
|
||||||
|
[0.0.12]
|
||||||
|
- Support OAuth2 state
|
||||||
|
|
||||||
|
[0.0.13]
|
||||||
|
- Use docker image from cloudron repository
|
||||||
|
|
||||||
|
[0.0.14]
|
||||||
|
- Improve setup flow
|
||||||
|
|
||||||
|
[0.0.15]
|
||||||
|
- Improved Appstore view
|
||||||
|
|
||||||
|
[0.0.16]
|
||||||
|
- Improved Backup approach
|
||||||
|
|
||||||
|
[0.0.17]
|
||||||
|
- Upgrade testing
|
||||||
|
- App auto updates
|
||||||
|
- Usage graphs
|
||||||
|
|
||||||
|
[0.0.18]
|
||||||
|
- Rework backups and updates
|
||||||
|
|
||||||
|
[0.0.19]
|
||||||
|
- Graphite fixes
|
||||||
|
- Avatar and Cloudron name support
|
||||||
|
|
||||||
|
[0.0.20]
|
||||||
|
- Apptask fixes
|
||||||
|
- Chrome related fixes
|
||||||
|
|
||||||
|
[0.0.21]
|
||||||
|
- Increase nginx hostname size to 64
|
||||||
|
|
||||||
|
[0.0.22]
|
||||||
|
- Testing the e2e tests
|
||||||
|
|
||||||
|
[0.0.23]
|
||||||
|
- Better error status page
|
||||||
|
- Fix updater and backup progress reporting
|
||||||
|
- New avatar set
|
||||||
|
- Improved setup wizard
|
||||||
|
|
||||||
|
[0.0.24]
|
||||||
|
- Hotfix the ldap support
|
||||||
|
|
||||||
|
[0.0.25]
|
||||||
|
- Add support page
|
||||||
|
- Really fix ldap issues
|
||||||
|
|
||||||
|
[0.0.26]
|
||||||
|
- Add configurePath support
|
||||||
|
|
||||||
|
[0.0.27]
|
||||||
|
- Improved log collector
|
||||||
|
|
||||||
|
[0.0.28]
|
||||||
|
- Improve app feedback
|
||||||
|
- Restyle login page
|
||||||
|
|
||||||
|
[0.0.29]
|
||||||
|
- Update to ubuntu 15.04
|
||||||
|
|
||||||
|
[0.0.30]
|
||||||
|
- Move to docker 1.7
|
||||||
|
|
||||||
|
[0.0.31]
|
||||||
|
- WARNING: This update restarts your containers
|
||||||
|
- System processes are prioritized over apps
|
||||||
|
- Add ldap group support
|
||||||
|
|
||||||
|
[0.0.32]
|
||||||
|
- MySQL addon update
|
||||||
|
|
||||||
|
[0.0.33]
|
||||||
|
- Fix graphs
|
||||||
|
- Fix MySQL 5.6 memory usage
|
||||||
|
|
||||||
|
[0.0.34]
|
||||||
|
- Correctly mark apps pending for approval
|
||||||
|
|
||||||
|
[0.0.35]
|
||||||
|
- Fix ldap admin group username
|
||||||
|
|
||||||
|
[0.0.36]
|
||||||
|
- Fix restore without backup
|
||||||
|
- Optimize image deletion during updates
|
||||||
|
- Add memory accounting
|
||||||
|
- Restrict access to metadata from containers
|
||||||
|
|
||||||
|
[0.0.37]
|
||||||
|
- Prepare for Selfhosting 1. part
|
||||||
|
- Use userData instead of provisioning calls
|
||||||
|
|
||||||
|
[0.0.38]
|
||||||
|
- Account for Ext4 reserved block when partitioning disk
|
||||||
|
|
||||||
|
[0.0.39]
|
||||||
|
- Move subdomain management to the cloudron
|
||||||
|
|
||||||
|
[0.0.40]
|
||||||
|
- Add journal limit
|
||||||
|
- Fix reprovisioning on reboot
|
||||||
|
- Fix subdomain management during startup
|
||||||
|
|
||||||
|
[0.0.41]
|
||||||
|
- Finally bring things to a sane state
|
||||||
|
|
||||||
|
[0.0.42]
|
||||||
|
- Parallel apptask
|
||||||
|
|
||||||
|
[0.0.43]
|
||||||
|
- Move to systemd
|
||||||
|
|
||||||
|
[0.0.44]
|
||||||
|
- Fix apptask concurrency bug
|
||||||
|
|
||||||
|
[0.0.45]
|
||||||
|
- Retry subdomain registration
|
||||||
|
|
||||||
|
[0.0.46]
|
||||||
|
- Fix app update email notification
|
||||||
|
|
||||||
|
[0.0.47]
|
||||||
|
- Ensure box code quits within 5 seconds
|
||||||
|
|
||||||
|
[0.0.48]
|
||||||
|
- Styling fixes
|
||||||
|
- Improved session handling
|
||||||
|
|
||||||
|
[0.0.49]
|
||||||
|
- Fix app autoupdate logic
|
||||||
|
|
||||||
|
[0.0.50]
|
||||||
|
- Use domainmanagement via CaaS
|
||||||
|
|
||||||
|
[0.0.51]
|
||||||
|
- Fix memory management
|
||||||
|
|
||||||
|
[0.0.52]
|
||||||
|
- Restrict addons memory
|
||||||
|
- Get nofication about container OOMs
|
||||||
|
|
||||||
|
[0.0.53]
|
||||||
|
- Restrict addons memory
|
||||||
|
- Get notification about container OOMs
|
||||||
|
- Add retry to subdomain logic
|
||||||
|
|
||||||
|
[0.0.54]
|
||||||
|
- OAuth Proxy now uses internal port forwarding
|
||||||
|
|
||||||
|
[0.0.55]
|
||||||
|
- Setup cloudron timezone based on droplet region
|
||||||
|
|
||||||
|
[0.0.56]
|
||||||
|
- Use correct timezone in updater
|
||||||
|
|
||||||
|
[0.0.57]
|
||||||
|
- Fix systemd logging issues
|
||||||
|
|
||||||
|
[0.0.58]
|
||||||
|
- Ensure backups of failed apps are retained across archival cycles
|
||||||
|
|
||||||
|
[0.0.59]
|
||||||
|
- Installer API fixes
|
||||||
|
|
||||||
|
[0.0.60]
|
||||||
|
- Do full box backup on updates
|
||||||
|
|
||||||
|
[0.0.61]
|
||||||
|
- Track update notifications to inform admin only once
|
||||||
|
|
||||||
|
[0.0.62]
|
||||||
|
- Export bind dn and password from LDAP addon
|
||||||
|
|
||||||
|
[0.0.63]
|
||||||
|
- Fix creation of TXT records
|
||||||
|
|
||||||
|
[0.0.64]
|
||||||
|
- Stop apps in a retired cloudron
|
||||||
|
- Retry downloading application on failure
|
||||||
|
|
||||||
|
[0.0.65]
|
||||||
|
- Do not send crash mails for apps in development
|
||||||
|
|
||||||
|
[0.0.66]
|
||||||
|
- Readonly application and addon containers
|
||||||
|
|
||||||
|
[0.0.67]
|
||||||
|
- Fix email notifications
|
||||||
|
- Fix bug when restoring from certain backups
|
||||||
|
|
||||||
|
[0.0.68]
|
||||||
|
- Update graphite image
|
||||||
|
- Add simpleauth addon support
|
||||||
|
|
||||||
|
[0.0.69]
|
||||||
|
- Support newer manifest format
|
||||||
|
- Fix app listing rendering in chrome
|
||||||
|
- Fix redis backup across upgrades
|
||||||
|
|
||||||
|
[0.0.70]
|
||||||
|
- Retry app download on error
|
||||||
|
|
||||||
|
[0.0.71]
|
||||||
|
- Fix oauth and simple auth login
|
||||||
|
|
||||||
|
[0.0.72]
|
||||||
|
- Cleanup application volumes periodically
|
||||||
|
- New application logging design
|
||||||
|
|
||||||
|
[0.0.73]
|
||||||
|
- Update SSL certificate
|
||||||
|
|
||||||
|
[0.0.74]
|
||||||
|
- Support singleUser apps
|
||||||
|
|
||||||
|
[0.0.75]
|
||||||
|
- scheduler addon
|
||||||
|
|
||||||
|
[0.0.76]
|
||||||
|
- DNS Sync fixes
|
||||||
|
- Show warning to user when memory limit reached
|
||||||
|
|
||||||
|
[0.0.77]
|
||||||
|
- Do not set hostname in app containers
|
||||||
|
|
||||||
|
[0.0.78]
|
||||||
|
- Support custom domains
|
||||||
|
|
||||||
|
[0.0.79]
|
||||||
|
- Move SSH Port
|
||||||
|
|
||||||
|
[0.0.80]
|
||||||
|
- Use journalctl for container logs
|
||||||
|
|
||||||
|
[0.1.0]
|
||||||
|
- Wait for configuration changes before starting Cloudron
|
||||||
|
|
||||||
|
[0.1.1]
|
||||||
|
- Ensure dns config for all cloudrons
|
||||||
|
|
||||||
|
[0.1.2]
|
||||||
|
- Make email work again
|
||||||
|
- Add DKIM keys for custom domains
|
||||||
|
|
||||||
|
[0.1.3]
|
||||||
|
- Storage backend
|
||||||
|
|
||||||
|
[0.1.4]
|
||||||
|
- CaaS Backup configuration fix
|
||||||
|
|
||||||
|
[0.1.5]
|
||||||
|
- Use correct tokens for DNS backend
|
||||||
|
|
||||||
|
[0.1.6]
|
||||||
|
- Add hook to determine the api server of the box
|
||||||
|
- Fix crash notification
|
||||||
|
|
||||||
|
[0.2.0]
|
||||||
|
- New cloudron exec implementation
|
||||||
|
|
||||||
|
[0.2.1]
|
||||||
|
- Update to node 4.1.1
|
||||||
|
- Fix certification installation with custom domains
|
||||||
|
|
||||||
|
[0.2.2]
|
||||||
|
- Better debug output
|
||||||
|
- Retry more times if docker registry goes down
|
||||||
|
|
||||||
|
[0.3.0]
|
||||||
|
- Update SSH keys
|
||||||
|
- Allow bigger manifest files
|
||||||
|
|
||||||
|
[0.4.0]
|
||||||
|
- Update to docker 1.9.0
|
||||||
|
|
||||||
|
[0.4.1]
|
||||||
|
- Fix scheduler crash
|
||||||
|
- Crucial OAuth fixes
|
||||||
|
|
||||||
|
[0.4.2]
|
||||||
|
- Fix crash when reporting backup error
|
||||||
|
- Allow larger manifests
|
||||||
|
|
||||||
|
[0.4.3]
|
||||||
|
- Fix cloudron exec
|
||||||
|
|
||||||
|
[0.4.4]
|
||||||
|
- Initial Lets Encrypt integration
|
||||||
|
|
||||||
|
[0.4.5]
|
||||||
|
- Fixup nginx configuration to allow dynamic certificates
|
||||||
|
|
||||||
|
[0.4.6]
|
||||||
|
- LetsEncrypt integration for custom domains
|
||||||
|
- Rate limit crash emails
|
||||||
|
|
||||||
|
[0.5.0]
|
||||||
|
- Enable staging Lets Encrypt Integration
|
||||||
|
|
||||||
|
[0.5.1]
|
||||||
|
- Display error dialog for app installation errors
|
||||||
|
- Enable prod Lets Encrypt Integration
|
||||||
|
- Handle apptask crashes correctly
|
||||||
|
|
||||||
|
[0.5.2]
|
||||||
|
- Fix apphealthtask crash
|
||||||
|
- Use cgroup fs driver instead of systemd cgroup driver in docker
|
||||||
|
|
||||||
|
[0.5.3]
|
||||||
|
- Changes for e2e testing
|
||||||
|
|
||||||
|
[0.5.4]
|
||||||
|
- Fix bug in LE server selection
|
||||||
|
|
||||||
|
[0.5.5]
|
||||||
|
- Scheduler redesign
|
||||||
|
- Fix journalctl logging
|
||||||
|
|
||||||
|
[0.5.6]
|
||||||
|
- Prepare for selfhosting option
|
||||||
|
|
||||||
|
[0.5.7]
|
||||||
|
- Move app images off the btrfs subvolume
|
||||||
|
|
||||||
|
[0.6.0]
|
||||||
|
- Consolidate code repositories
|
||||||
|
|
||||||
|
[0.6.1]
|
||||||
|
- Use no-reply as email from address for apps in naked domains
|
||||||
|
- Update Lets Encrypt account with owner email when available
|
||||||
|
- Fix email templates to indicate auto update
|
||||||
|
- Add notification UI
|
||||||
|
|
||||||
|
[0.6.2]
|
||||||
|
- Fix `cloudron exec` container to have same namespaces as app
|
||||||
|
- Add developmentMode to manifest
|
||||||
|
|
||||||
|
[0.6.3]
|
||||||
|
- Make sending invite for new users optional
|
||||||
|
|
||||||
|
[0.6.4]
|
||||||
|
- Add support for display names
|
||||||
|
- Send invite links to admins for user setup
|
||||||
|
- Enforce stronger passwords
|
||||||
|
|
||||||
|
[0.6.5]
|
||||||
|
- Finalize stronger password requirement
|
||||||
|
|
||||||
|
[0.7.0]
|
||||||
|
- Upgrade to 15.10
|
||||||
|
- Do not remove docker images when in use by another container
|
||||||
|
- Fix sporadic error when reconfiguring apps
|
||||||
|
- Handle journald crashes gracefully
|
||||||
|
|
||||||
|
[0.7.1]
|
||||||
|
- Allow admins to edit users
|
||||||
|
- Fix graphs
|
||||||
|
- Support more LDAP cases
|
||||||
|
- Allow appstore deep linking
|
||||||
|
|
||||||
|
[0.7.2]
|
||||||
|
- Fix 5xx errors when password does not meet requirements
|
||||||
|
- Improved box update management using prereleases
|
||||||
|
- Less aggressive disk space checks
|
||||||
|
|
||||||
|
[0.8.0]
|
||||||
|
- MySQL addon : multiple database support
|
||||||
|
|
||||||
|
[0.8.1]
|
||||||
|
- Set Host HTTP header when querying healthCheckPath
|
||||||
|
- Show application Changelog in app update emails
|
||||||
|
|
||||||
|
[0.9.0]
|
||||||
|
- Fix bug in multdb mysql addon backup
|
||||||
|
- Add initial user group support
|
||||||
|
- Improved app memory limit handling
|
||||||
|
|
||||||
|
[0.9.1]
|
||||||
|
- Introduce per app group access control
|
||||||
|
|
||||||
|
[0.9.2]
|
||||||
|
- Fix bug where reconfiguring apps would trigger memory limit warning
|
||||||
|
- Allow more apps to be installed in bigger sized cloudrons
|
||||||
|
- Allow user to override memory limit warning and install anyway
|
||||||
|
|
||||||
|
[0.9.3]
|
||||||
|
- Admin flag is handled outside of groups
|
||||||
|
- User interface fixes for groups
|
||||||
|
- Allow to set access restrictions on app installation
|
||||||
|
|
||||||
|
[0.10.0]
|
||||||
|
- Upgrade to docker 1.10.2
|
||||||
|
- Fix MySQL addon to handle heavier loads
|
||||||
|
- Allow listing and download of backups (using the CLI tool)
|
||||||
|
- Ubuntu security updates till 8th March 2016 (http://www.ubuntu.com/usn)
|
||||||
|
|
||||||
|
[0.10.1]
|
||||||
|
- Fix Let's Encrypt certificate renewal
|
||||||
|
|
||||||
|
[0.10.2]
|
||||||
|
- Apps can now bind with username or email with LDAP
|
||||||
|
- Disallow updating an app with mismatching manifest id
|
||||||
|
- Use admin domain instead of naked domain in the SPF record
|
||||||
|
- Download Lets Encrypt intermediate cert
|
||||||
|
|
||||||
|
[0.10.3]
|
||||||
|
- Store the backup config for each backup. This will allow using multiple buckets/providers for backups simultaneously.
|
||||||
|
- Fix SPF record check
|
||||||
|
|
||||||
|
[0.10.4]
|
||||||
|
- Fix restore for droplets in EU region
|
||||||
|
|
||||||
|
[0.11.0]
|
||||||
|
- Store backups in the same region as the Cloudron
|
||||||
|
- Fix PCRE security issue (http://www.ubuntu.com/usn/usn-2943-1/)
|
||||||
|
|
||||||
|
[0.11.1]
|
||||||
|
- Improve the backup logic
|
||||||
|
|
||||||
|
[0.11.2]
|
||||||
|
- Allow users to choose a username on first sign up
|
||||||
|
- Fix app graphs
|
||||||
|
|
||||||
|
[0.12.0]
|
||||||
|
- Fix upload of large backups
|
||||||
|
- Postgres addon whitelists pg_trgm and hstore extensions
|
||||||
|
- Suppress boring update emails from patch releases
|
||||||
|
- Setup bounce alerts for emails
|
||||||
|
- Query admin's name in activation wizard
|
||||||
|
- Admin emails are now delivered as no-reply
|
||||||
|
- Fix crash when user attempts to set a duplicate email
|
||||||
|
- Improved mongodb crash recovery
|
||||||
|
|
||||||
|
[0.12.1]
|
||||||
|
- Fix crash when backing up apps
|
||||||
|
|
||||||
|
[0.12.2]
|
||||||
|
- Improved error handling for addons
|
||||||
|
|
||||||
|
[0.12.3]
|
||||||
|
- LDAP: Do not set sn attribute when user has no surname
|
||||||
|
|
||||||
|
[0.12.4]
|
||||||
|
- Install app only after platform is ready
|
||||||
|
|
||||||
|
[0.12.5]
|
||||||
|
- Get alerts for app task failures
|
||||||
|
- Fix update issue when one or more apps are in failed state
|
||||||
|
|
||||||
|
[0.12.6]
|
||||||
|
- Allow setting an alternate external domain for apps
|
||||||
|
|
||||||
|
[0.12.7]
|
||||||
|
- Fix changing password
|
||||||
|
|
||||||
|
[0.13.0]
|
||||||
|
- Upgrade to ubuntu 16.04
|
||||||
|
- Add event log
|
||||||
|
|
||||||
|
[0.13.1]
|
||||||
|
- Make activity log viewable to admins
|
||||||
|
- Fix geoip lookup
|
||||||
|
|
||||||
|
[0.13.2]
|
||||||
|
- Fix crash in app auto updater
|
||||||
|
- Fix crash with empty timezone
|
||||||
|
|
||||||
|
[0.13.3]
|
||||||
|
- Enable auth in email addon
|
||||||
|
- Add search for activity log
|
||||||
|
- Add tutorial for first time users
|
||||||
|
|
||||||
|
[0.13.4]
|
||||||
|
- Fix mail addon restart issue
|
||||||
|
|
||||||
|
[0.14.0]
|
||||||
|
- You have mail :-)
|
||||||
|
|
||||||
|
[0.14.1]
|
||||||
|
- 2-character usernames are now allowed
|
||||||
|
- Make cloudron CLI push/pull more robust
|
||||||
|
|
||||||
|
[0.14.2]
|
||||||
|
- Update mail addon
|
||||||
|
|
||||||
|
[0.15.0]
|
||||||
|
- [REST API](https://cloudron.io/references/api.html) is now in public beta
|
||||||
|
- Enable Developer mode by default for new Cloudrons
|
||||||
|
- Reverse proxy fixes for apps exposing a WebDav server
|
||||||
|
- Allow admins to optionally set the username and displayName on user creation
|
||||||
|
- Fix app autoupdate logic to detect if one or more in-use port bindings was removed
|
||||||
|
|
||||||
|
[0.15.1]
|
||||||
|
- Fix mail connectivity from IPv6 clients
|
||||||
|
- Add API token management UI
|
||||||
|
- Improved UI to enter email aliases
|
||||||
|
|
||||||
|
[0.15.2]
|
||||||
|
- Allow restoring apps from any previous backup
|
||||||
|
|
||||||
24
README.md
24
README.md
@@ -1,11 +1,17 @@
|
|||||||
The Box
|
Cloudron a Smart Server
|
||||||
=======
|
=======================
|
||||||
|
|
||||||
Development setup
|
|
||||||
-----------------
|
|
||||||
* sudo useradd -m yellowtent
|
|
||||||
** This dummy user is required for supervisor 'box' configs
|
|
||||||
** Add admin-localhost as 127.0.0.1 in /etc/hosts
|
|
||||||
** All apps will be installed as hypened-subdomains of localhost. You should add
|
|
||||||
hyphened-subdomains of your apps into /etc/hosts
|
|
||||||
|
|
||||||
|
|
||||||
|
Selfhost Instructions
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
The smart server currently relies on an AWS account with access to Route53 and S3 and is tested on DigitalOcean and EC2.
|
||||||
|
|
||||||
|
First create a virtual private server with Ubuntu 15.04 and run the following commands in an ssh session to initialize the base image:
|
||||||
|
|
||||||
|
```
|
||||||
|
curl https://s3.amazonaws.com/prod-cloudron-releases/installer.sh -o installer.sh
|
||||||
|
chmod +x installer.sh
|
||||||
|
./installer.sh <domain> <aws access key> <aws acccess secret> <backup bucket> <provider> <release sha1>
|
||||||
|
```
|
||||||
|
|||||||
47
app.js
47
app.js
@@ -1,47 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
require('supererror')({ splatchError: true });
|
|
||||||
|
|
||||||
var server = require('./src/server.js'),
|
|
||||||
ldap = require('./src/ldap.js'),
|
|
||||||
config = require('./src/config.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 token: ', config.token());
|
|
||||||
console.log(' Appstore API server origin: ', config.apiServerOrigin());
|
|
||||||
console.log(' Appstore Web server origin: ', config.webServerOrigin());
|
|
||||||
console.log();
|
|
||||||
console.log('==========================================');
|
|
||||||
console.log();
|
|
||||||
|
|
||||||
server.start(function (err) {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error starting server', err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Server listening on port ' + config.get('port'));
|
|
||||||
|
|
||||||
ldap.start(function (error) {
|
|
||||||
if (error) {
|
|
||||||
console.error('Error LDAP starting server', err);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('LDAP server listen on port ' + config.get('ldapPort'));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
var NOOP_CALLBACK = function () { };
|
|
||||||
|
|
||||||
process.on('SIGINT', function () { server.stop(NOOP_CALLBACK); });
|
|
||||||
process.on('SIGTERM', function () { server.stop(NOOP_CALLBACK); });
|
|
||||||
147
apphealthtask.js
147
apphealthtask.js
@@ -1,147 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
require('supererror')({ splatchError: true });
|
|
||||||
|
|
||||||
var appdb = require('./src/appdb.js'),
|
|
||||||
assert = require('assert'),
|
|
||||||
async = require('async'),
|
|
||||||
database = require('./src/database.js'),
|
|
||||||
DatabaseError = require('./src/databaseerror.js'),
|
|
||||||
debug = require('debug')('box:apphealthtask'),
|
|
||||||
docker = require('./src/docker.js'),
|
|
||||||
mailer = require('./src/mailer.js'),
|
|
||||||
superagent = require('superagent'),
|
|
||||||
util = require('util');
|
|
||||||
|
|
||||||
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 = 3 * 60 * 1000; // 3 minutes
|
|
||||||
var gHealthInfo = { }; // { time, emailSent }
|
|
||||||
|
|
||||||
function debugApp(app, args) {
|
|
||||||
assert(!app || typeof app === 'object');
|
|
||||||
|
|
||||||
var prefix = app ? app.location : '(no app)';
|
|
||||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function initialize(callback) {
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
database.initialize,
|
|
||||||
mailer.initialize
|
|
||||||
], callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setHealth(app, health, callback) {
|
|
||||||
assert.strictEqual(typeof app, 'object');
|
|
||||||
assert.strictEqual(typeof health, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
var now = new Date();
|
|
||||||
|
|
||||||
if (!(app.id in gHealthInfo)) { // add new apps to list
|
|
||||||
gHealthInfo[app.id] = { time: now, emailSent: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
|
|
||||||
|
|
||||||
mailer.appDied(app);
|
|
||||||
gHealthInfo[app.id].emailSent = true;
|
|
||||||
} else {
|
|
||||||
debugApp(app, 'waiting for sometime to update the app health');
|
|
||||||
return callback(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
appdb.setHealth(app.id, health, function (error) {
|
|
||||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null); // app uninstalled?
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
app.health = health;
|
|
||||||
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// callback is called with error for fatal errors and not if health check failed
|
|
||||||
function checkAppHealth(app, callback) {
|
|
||||||
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.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;
|
|
||||||
|
|
||||||
container.inspect(function (err, data) {
|
|
||||||
if (err || !data || !data.State) {
|
|
||||||
debugApp(app, 'Error inspecting container');
|
|
||||||
return setHealth(app, appdb.HEALTH_ERROR, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.State.Running !== true) {
|
|
||||||
debugApp(app, 'exited');
|
|
||||||
return setHealth(app, appdb.HEALTH_DEAD, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
.redirects(0)
|
|
||||||
.timeout(HEALTHCHECK_INTERVAL)
|
|
||||||
.end(function (error, res) {
|
|
||||||
|
|
||||||
if (error || res.status >= 400) { // 2xx and 3xx are ok
|
|
||||||
debugApp(app, 'not alive : %s', error || res.status);
|
|
||||||
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
|
||||||
} else {
|
|
||||||
debugApp(app, 'alive');
|
|
||||||
setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function processApps(callback) {
|
|
||||||
appdb.getAll(function (error, apps) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
async.each(apps, checkAppHealth, function (error) {
|
|
||||||
if (error) console.error(error);
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
processApps(function (error) {
|
|
||||||
if (error) console.error(error);
|
|
||||||
|
|
||||||
setTimeout(run, HEALTHCHECK_INTERVAL);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
initialize(function (error) {
|
|
||||||
if (error) {
|
|
||||||
console.error('apphealth task exiting with error', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
186
baseimage/createImage
Executable file
186
baseimage/createImage
Executable file
@@ -0,0 +1,186 @@
|
|||||||
|
#!/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"
|
||||||
|
|
||||||
|
provider="digitalocean"
|
||||||
|
installer_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 "provider:,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) installer_revision="$2"; shift 2;;
|
||||||
|
--provider) provider="$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 image using ${provider}"
|
||||||
|
if [[ "${provider}" == "digitalocean" ]]; then
|
||||||
|
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"
|
||||||
|
else
|
||||||
|
echo "Unknown provider : ${provider}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
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 "${installer_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 ${installer_revision}"; then
|
||||||
|
echo "Init script failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Shutting down server with id : ${server_id}"
|
||||||
|
$ssh202 "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."
|
||||||
260
baseimage/digitalocean.sh
Normal file
260
baseimage/digitalocean.sh
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [[ -z "${DIGITAL_OCEAN_TOKEN}" ]]; then
|
||||||
|
echo "Script requires DIGITAL_OCEAN_TOKEN env to be set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${JSON}" ]]; then
|
||||||
|
echo "Script requires JSON env to be set to path of JSON binary"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
readonly CURL="curl --retry 5 -s -u ${DIGITAL_OCEAN_TOKEN}:"
|
||||||
|
|
||||||
|
function debug() {
|
||||||
|
echo "$@" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_ssh_key_id() {
|
||||||
|
id=$($CURL "https://api.digitalocean.com/v2/account/keys" \
|
||||||
|
| $JSON ssh_keys \
|
||||||
|
| $JSON -c "this.name === \"$1\"" \
|
||||||
|
| $JSON 0.id)
|
||||||
|
[[ -z "$id" ]] && exit 1
|
||||||
|
echo "$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
function create_droplet() {
|
||||||
|
local ssh_key_id="$1"
|
||||||
|
local box_name="$2"
|
||||||
|
|
||||||
|
local image_region="sfo1"
|
||||||
|
local ubuntu_image_slug="ubuntu-16-04-x64"
|
||||||
|
local box_size="512mb"
|
||||||
|
|
||||||
|
local data="{\"name\":\"${box_name}\",\"size\":\"${box_size}\",\"region\":\"${image_region}\",\"image\":\"${ubuntu_image_slug}\",\"ssh_keys\":[ \"${ssh_key_id}\" ],\"backups\":false}"
|
||||||
|
|
||||||
|
id=$($CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets" | $JSON droplet.id)
|
||||||
|
[[ -z "$id" ]] && exit 1
|
||||||
|
echo "$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_droplet_ip() {
|
||||||
|
local droplet_id="$1"
|
||||||
|
ip=$($CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}" | $JSON "droplet.networks.v4[0].ip_address")
|
||||||
|
[[ -z "$ip" ]] && exit 1
|
||||||
|
echo "$ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_droplet_id() {
|
||||||
|
local droplet_name="$1"
|
||||||
|
id=$($CURL "https://api.digitalocean.com/v2/droplets?per_page=100" | $JSON "droplets" | $JSON -c "this.name === '${droplet_name}'" | $JSON "[0].id")
|
||||||
|
[[ -z "$id" ]] && exit 1
|
||||||
|
echo "$id"
|
||||||
|
}
|
||||||
|
|
||||||
|
function power_off_droplet() {
|
||||||
|
local droplet_id="$1"
|
||||||
|
local data='{"type":"power_off"}'
|
||||||
|
local response=$($CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions")
|
||||||
|
local event_id=`echo "${response}" | $JSON action.id`
|
||||||
|
|
||||||
|
if [[ -z "${event_id}" ]]; then
|
||||||
|
debug "Got no event id, assuming already powered off."
|
||||||
|
debug "Response: ${response}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
debug "Powered off droplet. Event id: ${event_id}"
|
||||||
|
debug -n "Waiting for droplet to power off"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
|
||||||
|
if [[ "${event_status}" == "completed" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
debug -n "."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
debug ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function power_on_droplet() {
|
||||||
|
local droplet_id="$1"
|
||||||
|
local data='{"type":"power_on"}'
|
||||||
|
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions" | $JSON action.id`
|
||||||
|
|
||||||
|
debug "Powered on droplet. Event id: ${event_id}"
|
||||||
|
|
||||||
|
if [[ -z "${event_id}" ]]; then
|
||||||
|
debug "Got no event id, assuming already powered on"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
debug -n "Waiting for droplet to power on"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
local event_status=`$CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}" | $JSON action.status`
|
||||||
|
if [[ "${event_status}" == "completed" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
debug -n "."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
debug ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_image_id() {
|
||||||
|
local snapshot_name="$1"
|
||||||
|
local image_id=""
|
||||||
|
|
||||||
|
if ! response=$($CURL "https://api.digitalocean.com/v2/images?per_page=100"); then
|
||||||
|
echo "Failed to get image listing. ${response}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! image_id=$(echo "$response" \
|
||||||
|
| $JSON images \
|
||||||
|
| $JSON -c "this.name === \"${snapshot_name}\"" 0.id); then
|
||||||
|
echo "Failed to parse curl response: ${response}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${image_id}" ]]; then
|
||||||
|
echo "Failed to get image id of ${snapshot_name}. reponse: ${response}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${image_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapshot_droplet() {
|
||||||
|
local droplet_id="$1"
|
||||||
|
local snapshot_name="$2"
|
||||||
|
local data="{\"type\":\"snapshot\",\"name\":\"${snapshot_name}\"}"
|
||||||
|
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions" | $JSON action.id`
|
||||||
|
|
||||||
|
debug "Droplet snapshotted as ${snapshot_name}. Event id: ${event_id}"
|
||||||
|
debug -n "Waiting for snapshot to complete"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if ! response=$($CURL "https://api.digitalocean.com/v2/droplets/${droplet_id}/actions/${event_id}"); then
|
||||||
|
echo "Could not get action status. ${response}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! event_status=$(echo "${response}" | $JSON action.status); then
|
||||||
|
echo "Could not parse action.status from response. ${response}"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [[ "${event_status}" == "completed" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
debug -n "."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
debug "! done"
|
||||||
|
|
||||||
|
if ! image_id=$(get_image_id "${snapshot_name}"); then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "${image_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroy_droplet() {
|
||||||
|
local droplet_id="$1"
|
||||||
|
# TODO: check for 204 status
|
||||||
|
$CURL -X DELETE "https://api.digitalocean.com/v2/droplets/${droplet_id}"
|
||||||
|
debug "Droplet destroyed"
|
||||||
|
debug ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function transfer_image() {
|
||||||
|
local image_id="$1"
|
||||||
|
local region_slug="$2"
|
||||||
|
local data="{\"type\":\"transfer\",\"region\":\"${region_slug}\"}"
|
||||||
|
local event_id=`$CURL -X POST -H 'Content-Type: application/json' -d "${data}" "https://api.digitalocean.com/v2/images/${image_id}/actions" | $JSON action.id`
|
||||||
|
echo "${event_id}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function wait_for_image_event() {
|
||||||
|
local image_id="$1"
|
||||||
|
local event_id="$2"
|
||||||
|
|
||||||
|
debug -n "Waiting for ${event_id}"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
local event_status=`$CURL "https://api.digitalocean.com/v2/images/${image_id}/actions/${event_id}" | $JSON action.status`
|
||||||
|
if [[ "${event_status}" == "completed" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
debug -n "."
|
||||||
|
sleep 10
|
||||||
|
done
|
||||||
|
debug ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function transfer_image_to_all_regions() {
|
||||||
|
local image_id="$1"
|
||||||
|
|
||||||
|
xfer_events=()
|
||||||
|
image_regions=(ams3) ## sfo1 is where the image is created
|
||||||
|
for image_region in ${image_regions[@]}; do
|
||||||
|
xfer_event=$(transfer_image ${image_id} ${image_region})
|
||||||
|
echo "Image transfer to ${image_region} initiated. Event id: ${xfer_event}"
|
||||||
|
xfer_events+=("${xfer_event}")
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Image transfer initiated, but they will take some time to get transferred."
|
||||||
|
|
||||||
|
for xfer_event in ${xfer_events[@]}; do
|
||||||
|
$vps wait_for_image_event "${image_id}" "${xfer_event}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $# -lt 1 ]]; then
|
||||||
|
debug "<command> <params...>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
case $1 in
|
||||||
|
get_ssh_key_id)
|
||||||
|
get_ssh_key_id "${@:2}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
create)
|
||||||
|
create_droplet "${@:2}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
get_id)
|
||||||
|
get_droplet_id "${@:2}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
get_ip)
|
||||||
|
get_droplet_ip "${@:2}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
power_on)
|
||||||
|
power_on_droplet "${@:2}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
power_off)
|
||||||
|
power_off_droplet "${@:2}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
snapshot)
|
||||||
|
snapshot_droplet "${@:2}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
destroy)
|
||||||
|
destroy_droplet "${@:2}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
transfer_image_to_all_regions)
|
||||||
|
transfer_image_to_all_regions "${@:2}"
|
||||||
|
;;
|
||||||
|
|
||||||
|
*)
|
||||||
|
echo "Unknown command $1"
|
||||||
|
exit 1
|
||||||
|
esac
|
||||||
303
baseimage/initializeBaseUbuntuImage.sh
Normal file
303
baseimage/initializeBaseUbuntuImage.sh
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -euv -o pipefail
|
||||||
|
|
||||||
|
readonly USER=yellowtent
|
||||||
|
readonly USER_HOME="/home/${USER}"
|
||||||
|
readonly INSTALLER_SOURCE_DIR="${USER_HOME}/installer"
|
||||||
|
readonly INSTALLER_REVISION="$1"
|
||||||
|
readonly SELFHOSTED=$(( $# > 1 ? 1 : 0 ))
|
||||||
|
readonly USER_DATA_FILE="/root/user_data.img"
|
||||||
|
readonly USER_DATA_DIR="/home/yellowtent/data"
|
||||||
|
|
||||||
|
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
function die {
|
||||||
|
echo $1
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ "$(systemd --version 2>&1)" == *"systemd 229"* ]] || die "Expecting systemd to be 229"
|
||||||
|
|
||||||
|
if [ ${SELFHOSTED} == 0 ]; then
|
||||||
|
echo "!! Initializing Ubuntu image for CaaS"
|
||||||
|
else
|
||||||
|
echo "!! Initializing Ubuntu image for Selfhosting"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==== Create User ${USER} ===="
|
||||||
|
if ! id "${USER}"; then
|
||||||
|
useradd "${USER}" -m
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Yellowtent base image preparation (installer revision - ${INSTALLER_REVISION}) ==="
|
||||||
|
|
||||||
|
echo "=== Prepare installer source ==="
|
||||||
|
rm -rf "${INSTALLER_SOURCE_DIR}" && mkdir -p "${INSTALLER_SOURCE_DIR}"
|
||||||
|
rm -rf /tmp/box && mkdir -p /tmp/box
|
||||||
|
tar xvf /tmp/box.tar.gz -C /tmp/box && rm /tmp/box.tar.gz
|
||||||
|
cp -rf /tmp/box/installer/* "${INSTALLER_SOURCE_DIR}"
|
||||||
|
echo "${INSTALLER_REVISION}" > "${INSTALLER_SOURCE_DIR}/REVISION"
|
||||||
|
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
echo "=== Upgrade ==="
|
||||||
|
apt-get update
|
||||||
|
apt-get dist-upgrade -y
|
||||||
|
apt-get install -y curl
|
||||||
|
|
||||||
|
# Setup firewall before everything. docker creates it's own chain and the -X below will remove it
|
||||||
|
# Do NOT use iptables-persistent because it's startup ordering conflicts with docker
|
||||||
|
echo "=== Setting up firewall ==="
|
||||||
|
# clear tables and set default policy
|
||||||
|
iptables -F # flush all chains
|
||||||
|
iptables -X # delete all chains
|
||||||
|
# default policy for filter table
|
||||||
|
iptables -P INPUT DROP
|
||||||
|
iptables -P FORWARD ACCEPT # TODO: disable icc and make this as reject
|
||||||
|
iptables -P OUTPUT ACCEPT
|
||||||
|
|
||||||
|
# NOTE: keep these in sync with src/apps.js validatePortBindings
|
||||||
|
# allow ssh, http, https, ping, dns
|
||||||
|
iptables -I INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
|
||||||
|
if [ ${SELFHOSTED} == 0 ]; then
|
||||||
|
iptables -A INPUT -p tcp -m tcp -m multiport --dports 25,80,202,443,587,993,4190 -j ACCEPT
|
||||||
|
else
|
||||||
|
iptables -A INPUT -p tcp -m tcp -m multiport --dports 25,80,22,443,587,993,4190 -j ACCEPT
|
||||||
|
fi
|
||||||
|
iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT
|
||||||
|
iptables -A INPUT -p icmp --icmp-type echo-reply -j ACCEPT
|
||||||
|
iptables -A INPUT -p udp --sport 53 -j ACCEPT
|
||||||
|
iptables -A INPUT -s 172.17.0.0/16 -j ACCEPT # required to accept any connections from apps to our IP:<public port>
|
||||||
|
|
||||||
|
# loopback
|
||||||
|
iptables -A INPUT -i lo -j ACCEPT
|
||||||
|
iptables -A OUTPUT -o lo -j ACCEPT
|
||||||
|
|
||||||
|
# prevent DoS
|
||||||
|
# iptables -A INPUT -p tcp --dport 80 -m limit --limit 25/minute --limit-burst 100 -j ACCEPT
|
||||||
|
|
||||||
|
# log dropped incoming. keep this at the end of all the rules
|
||||||
|
iptables -N LOGGING # new chain
|
||||||
|
iptables -A INPUT -j LOGGING # last rule in INPUT chain
|
||||||
|
iptables -A LOGGING -m limit --limit 2/min -j LOG --log-prefix "IPTables Packet Dropped: " --log-level 7
|
||||||
|
iptables -A LOGGING -j DROP
|
||||||
|
|
||||||
|
echo "==== Install btrfs tools ==="
|
||||||
|
apt-get -y install btrfs-tools
|
||||||
|
|
||||||
|
echo "==== Install docker ===="
|
||||||
|
# install docker from binary to pin it to a specific version. the current debian repo does not allow pinning
|
||||||
|
curl https://get.docker.com/builds/Linux/x86_64/docker-1.10.2 > /usr/bin/docker
|
||||||
|
apt-get -y install aufs-tools
|
||||||
|
chmod +x /usr/bin/docker
|
||||||
|
groupadd docker
|
||||||
|
cat > /etc/systemd/system/docker.socket <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Docker Socket for the API
|
||||||
|
PartOf=docker.service
|
||||||
|
|
||||||
|
[Socket]
|
||||||
|
ListenStream=/var/run/docker.sock
|
||||||
|
SocketMode=0660
|
||||||
|
SocketUser=root
|
||||||
|
SocketGroup=docker
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=sockets.target
|
||||||
|
EOF
|
||||||
|
cat > /etc/systemd/system/docker.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Docker Application Container Engine
|
||||||
|
After=network.target docker.socket
|
||||||
|
Requires=docker.socket
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/docker daemon -H fd:// --log-driver=journald --exec-opt native.cgroupdriver=cgroupfs
|
||||||
|
MountFlags=slave
|
||||||
|
LimitNOFILE=1048576
|
||||||
|
LimitNPROC=1048576
|
||||||
|
LimitCORE=infinity
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "=== Setup btrfs data ==="
|
||||||
|
truncate -s "8192m" "${USER_DATA_FILE}" # 8gb start (this will get resized dynamically by box-setup.service)
|
||||||
|
mkfs.btrfs -L UserHome "${USER_DATA_FILE}"
|
||||||
|
mkdir -p "${USER_DATA_DIR}"
|
||||||
|
mount -t btrfs -o loop,nosuid "${USER_DATA_FILE}" ${USER_DATA_DIR}
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable docker
|
||||||
|
systemctl start docker
|
||||||
|
|
||||||
|
# give docker sometime to start up and create iptables rules
|
||||||
|
# those rules come in after docker has started, and we want to wait for them to be sure iptables-save has all of them
|
||||||
|
sleep 10
|
||||||
|
|
||||||
|
# Disable forwarding to metadata route from containers
|
||||||
|
iptables -I FORWARD -d 169.254.169.254 -j DROP
|
||||||
|
|
||||||
|
# ubuntu will restore iptables from this file automatically. this is here so that docker's chain is saved to this file
|
||||||
|
mkdir /etc/iptables && iptables-save > /etc/iptables/rules.v4
|
||||||
|
|
||||||
|
echo "=== Enable memory accounting =="
|
||||||
|
sed -e 's/GRUB_CMDLINE_LINUX=.*/GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1 panic_on_oops=1 panic=5"/' -i /etc/default/grub
|
||||||
|
update-grub
|
||||||
|
|
||||||
|
# now add the user to the docker group
|
||||||
|
usermod "${USER}" -a -G docker
|
||||||
|
|
||||||
|
echo "==== Install nodejs ===="
|
||||||
|
# Cannot use anything above 4.1.1 - https://github.com/nodejs/node/issues/3803
|
||||||
|
mkdir -p /usr/local/node-4.1.1
|
||||||
|
curl -sL https://nodejs.org/dist/v4.1.1/node-v4.1.1-linux-x64.tar.gz | tar zxvf - --strip-components=1 -C /usr/local/node-4.1.1
|
||||||
|
ln -s /usr/local/node-4.1.1/bin/node /usr/bin/node
|
||||||
|
ln -s /usr/local/node-4.1.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"
|
||||||
|
|
||||||
|
echo "==== Downloading docker images ===="
|
||||||
|
images=$(node -e "var i = require('${SOURCE_DIR}/infra_version.js'); console.log(i.baseImage, Object.keys(i.images).map(function (x) { return i.images[x].tag; }).join(' '));")
|
||||||
|
|
||||||
|
echo "Pulling images: ${images}"
|
||||||
|
for image in ${images}; do
|
||||||
|
docker pull "${image}"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==== Install nginx ===="
|
||||||
|
apt-get -y install nginx-full
|
||||||
|
[[ "$(nginx -v 2>&1)" == *"nginx/1.10."* ]] || die "Expecting nginx version to be 1.10.x"
|
||||||
|
|
||||||
|
echo "==== Install build-essential ===="
|
||||||
|
apt-get -y install build-essential rcconf
|
||||||
|
|
||||||
|
echo "==== Install mysql ===="
|
||||||
|
debconf-set-selections <<< 'mysql-server mysql-server/root_password password password'
|
||||||
|
debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password password'
|
||||||
|
apt-get -y install mysql-server-5.7
|
||||||
|
[[ "$(mysqld --version 2>&1)" == *"5.7."* ]] || die "Expecting mysql version to be 5.7.x"
|
||||||
|
|
||||||
|
echo "==== Install pwgen and swaks awscli ===="
|
||||||
|
apt-get -y install pwgen swaks awscli
|
||||||
|
|
||||||
|
echo "==== Install collectd ==="
|
||||||
|
if ! apt-get install -y collectd collectd-utils; then
|
||||||
|
# FQDNLookup is true in default debian config. The box code has a custom collectd.conf that fixes this
|
||||||
|
echo "Failed to install collectd. Presumably because of http://mailman.verplant.org/pipermail/collectd/2015-March/006491.html"
|
||||||
|
sed -e 's/^FQDNLookup true/FQDNLookup false/' -i /etc/collectd/collectd.conf
|
||||||
|
fi
|
||||||
|
update-rc.d -f collectd remove
|
||||||
|
|
||||||
|
# this simply makes it explicit that we run logrotate via cron. it's already part of base ubuntu
|
||||||
|
echo "==== Install logrotate ==="
|
||||||
|
apt-get install -y cron logrotate
|
||||||
|
systemctl enable cron
|
||||||
|
|
||||||
|
echo "=== Rebuilding npm packages ==="
|
||||||
|
cd "${INSTALLER_SOURCE_DIR}" && npm install --production
|
||||||
|
chown "${USER}:${USER}" -R "${INSTALLER_SOURCE_DIR}"
|
||||||
|
|
||||||
|
echo "==== Install installer systemd script ===="
|
||||||
|
provisionEnv="PROVISION=digitalocean"
|
||||||
|
if [ ${SELFHOSTED} == 1 ]; then
|
||||||
|
provisionEnv="PROVISION=local"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > /etc/systemd/system/cloudron-installer.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Cloudron Installer
|
||||||
|
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
|
||||||
|
BindsTo=systemd-journald.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=idle
|
||||||
|
ExecStart="${INSTALLER_SOURCE_DIR}/src/server.js"
|
||||||
|
Environment="DEBUG=installer*,connect-lastmile" ${provisionEnv}
|
||||||
|
; kill any child (installer.sh) as well
|
||||||
|
KillMode=control-group
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Restore iptables before docker
|
||||||
|
echo "==== Install iptables-restore systemd script ===="
|
||||||
|
cat > /etc/systemd/system/iptables-restore.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=IPTables Restore
|
||||||
|
Before=docker.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/sbin/iptables-restore /etc/iptables/rules.v4
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Allocate swap files
|
||||||
|
# https://bbs.archlinux.org/viewtopic.php?id=194792 ensures this runs after do-resize.service
|
||||||
|
echo "==== Install box-setup systemd script ===="
|
||||||
|
cat > /etc/systemd/system/box-setup.service <<EOF
|
||||||
|
[Unit]
|
||||||
|
Description=Box Setup
|
||||||
|
Before=docker.service collectd.service mysql.service
|
||||||
|
After=do-resize.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart="${INSTALLER_SOURCE_DIR}/systemd/box-setup.sh"
|
||||||
|
RemainAfterExit=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable cloudron-installer
|
||||||
|
systemctl enable iptables-restore
|
||||||
|
systemctl enable box-setup
|
||||||
|
|
||||||
|
# Configure systemd
|
||||||
|
sed -e "s/^#SystemMaxUse=.*$/SystemMaxUse=100M/" \
|
||||||
|
-e "s/^#ForwardToSyslog=.*$/ForwardToSyslog=no/" \
|
||||||
|
-i /etc/systemd/journald.conf
|
||||||
|
|
||||||
|
# When rotating logs, systemd kills journald too soon sometimes
|
||||||
|
# See https://github.com/systemd/systemd/issues/1353 (this is upstream default)
|
||||||
|
sed -e "s/^WatchdogSec=.*$/WatchdogSec=3min/" \
|
||||||
|
-i /lib/systemd/system/systemd-journald.service
|
||||||
|
|
||||||
|
sync
|
||||||
|
|
||||||
|
# Configure time
|
||||||
|
sed -e 's/^#NTP=/NTP=0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org/' -i /etc/systemd/timesyncd.conf
|
||||||
|
timedatectl set-ntp 1
|
||||||
|
timedatectl set-timezone UTC
|
||||||
|
|
||||||
|
# Give user access to system logs
|
||||||
|
apt-get -y install acl
|
||||||
|
usermod -a -G systemd-journal ${USER}
|
||||||
|
mkdir -p /var/log/journal # in some images, this directory is not created making system log to /run/systemd instead
|
||||||
|
chown root:systemd-journal /var/log/journal
|
||||||
|
systemctl restart systemd-journald
|
||||||
|
setfacl -n -m u:${USER}:r /var/log/journal/*/system.journal
|
||||||
|
|
||||||
|
if [ ${SELFHOSTED} == 0 ]; then
|
||||||
|
echo "==== Install ssh ==="
|
||||||
|
apt-get -y install openssh-server
|
||||||
|
# https://stackoverflow.com/questions/4348166/using-with-sed on why ? must be escaped
|
||||||
|
sed -e 's/^#\?Port .*/Port 202/g' \
|
||||||
|
-e 's/^#\?PermitRootLogin .*/PermitRootLogin without-password/g' \
|
||||||
|
-e 's/^#\?PermitEmptyPasswords .*/PermitEmptyPasswords no/g' \
|
||||||
|
-e 's/^#\?PasswordAuthentication .*/PasswordAuthentication no/g' \
|
||||||
|
-i /etc/ssh/sshd_config
|
||||||
|
|
||||||
|
# required so we can connect to this machine since port 22 is blocked by iptables by now
|
||||||
|
systemctl reload sshd
|
||||||
|
fi
|
||||||
64
box.js
Executable file
64
box.js
Executable file
@@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
require('supererror')({ splatchError: true });
|
||||||
|
|
||||||
|
// remove timestamp from debug() based output
|
||||||
|
require('debug').formatArgs = function formatArgs() {
|
||||||
|
arguments[0] = this.namespace + ' ' + arguments[0];
|
||||||
|
return arguments;
|
||||||
|
};
|
||||||
|
|
||||||
|
var appHealthMonitor = require('./src/apphealthmonitor.js'),
|
||||||
|
async = require('async'),
|
||||||
|
config = require('./src/config.js'),
|
||||||
|
ldap = require('./src/ldap.js'),
|
||||||
|
oauthproxy = require('./src/oauthproxy.js'),
|
||||||
|
server = require('./src/server.js'),
|
||||||
|
simpleauth = require('./src/simpleauth.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();
|
||||||
|
console.log('==========================================');
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
async.series([
|
||||||
|
server.start,
|
||||||
|
ldap.start,
|
||||||
|
simpleauth.start,
|
||||||
|
appHealthMonitor.start,
|
||||||
|
oauthproxy.start
|
||||||
|
], function (error) {
|
||||||
|
if (error) {
|
||||||
|
console.error('Error starting server', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var NOOP_CALLBACK = function () { };
|
||||||
|
|
||||||
|
process.on('SIGINT', function () {
|
||||||
|
server.stop(NOOP_CALLBACK);
|
||||||
|
ldap.stop(NOOP_CALLBACK);
|
||||||
|
simpleauth.stop(NOOP_CALLBACK);
|
||||||
|
oauthproxy.stop(NOOP_CALLBACK);
|
||||||
|
setTimeout(process.exit.bind(process), 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGTERM', function () {
|
||||||
|
server.stop(NOOP_CALLBACK);
|
||||||
|
ldap.stop(NOOP_CALLBACK);
|
||||||
|
simpleauth.stop(NOOP_CALLBACK);
|
||||||
|
oauthproxy.stop(NOOP_CALLBACK);
|
||||||
|
setTimeout(process.exit.bind(process), 3000);
|
||||||
|
});
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
// WARNING This is a supervisor eventlistener!
|
|
||||||
// The communication happens via stdin/stdout
|
|
||||||
// !! No console.log() allowed
|
|
||||||
// !! Do not set DEBUG
|
|
||||||
|
|
||||||
var assert = require('assert'),
|
|
||||||
mailer = require('./src/mailer.js'),
|
|
||||||
safe = require('safetydance'),
|
|
||||||
supervisor = require('supervisord-eventlistener'),
|
|
||||||
path = require('path'),
|
|
||||||
util = require('util');
|
|
||||||
|
|
||||||
var gLastNotifyTime = {};
|
|
||||||
var gCooldownTime = 1000 * 60 * 5; // 5 min
|
|
||||||
var COLLECT_LOGS_CMD = path.join(__dirname, 'src/scripts/collectlogs.sh');
|
|
||||||
|
|
||||||
function collectLogs(program, callback) {
|
|
||||||
assert.strictEqual(typeof program, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
var logs = safe.child_process.execSync('sudo ' + COLLECT_LOGS_CMD + ' ' + program, { encoding: 'utf8' });
|
|
||||||
callback(null, logs);
|
|
||||||
}
|
|
||||||
|
|
||||||
supervisor.on('PROCESS_STATE_EXITED', function (headers, data) {
|
|
||||||
if (data.expected === '1') return console.error('Normal app %s exit', data.processname);
|
|
||||||
|
|
||||||
console.error('%s exited unexpectedly', data.processname);
|
|
||||||
|
|
||||||
collectLogs(data.processname, function (error, result) {
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to collect logs.', error);
|
|
||||||
result = util.format('Failed to collect logs.', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gLastNotifyTime[data.processname] || gLastNotifyTime[data.processname] < Date.now() - gCooldownTime) {
|
|
||||||
console.error('Send mail.');
|
|
||||||
mailer.sendCrashNotification(data.processname, result);
|
|
||||||
gLastNotifyTime[data.processname] = Date.now();
|
|
||||||
} else {
|
|
||||||
console.error('Do not send mail, already sent one recently.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
mailer.initialize(function () {
|
|
||||||
supervisor.listen(process.stdin, process.stdout);
|
|
||||||
console.error('Crashnotifier listening...');
|
|
||||||
});
|
|
||||||
16
crashnotifierservice.js
Executable file
16
crashnotifierservice.js
Executable file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var sendFailureLogs = require('./src/logcollector').sendFailureLogs;
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
if (process.argv.length !== 3) return console.error('Usage: crashnotifier.js <processName>');
|
||||||
|
|
||||||
|
var processName = process.argv[2];
|
||||||
|
console.log('Started crash notifier for', processName);
|
||||||
|
|
||||||
|
sendFailureLogs(processName, { unit: processName });
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
23
gulpfile.js
23
gulpfile.js
@@ -10,7 +10,7 @@ var ejs = require('gulp-ejs'),
|
|||||||
serve = require('gulp-serve'),
|
serve = require('gulp-serve'),
|
||||||
sass = require('gulp-sass'),
|
sass = require('gulp-sass'),
|
||||||
sourcemaps = require('gulp-sourcemaps'),
|
sourcemaps = require('gulp-sourcemaps'),
|
||||||
minifyCSS = require('gulp-minify-css'),
|
cssnano = require('gulp-cssnano'),
|
||||||
autoprefixer = require('gulp-autoprefixer'),
|
autoprefixer = require('gulp-autoprefixer'),
|
||||||
argv = require('yargs').argv;
|
argv = require('yargs').argv;
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ gulp.task('3rdparty', function () {
|
|||||||
'webadmin/src/3rdparty/**/*.otf',
|
'webadmin/src/3rdparty/**/*.otf',
|
||||||
'webadmin/src/3rdparty/**/*.eot',
|
'webadmin/src/3rdparty/**/*.eot',
|
||||||
'webadmin/src/3rdparty/**/*.svg',
|
'webadmin/src/3rdparty/**/*.svg',
|
||||||
|
'webadmin/src/3rdparty/**/*.gif',
|
||||||
'webadmin/src/3rdparty/**/*.ttf',
|
'webadmin/src/3rdparty/**/*.ttf',
|
||||||
'webadmin/src/3rdparty/**/*.woff',
|
'webadmin/src/3rdparty/**/*.woff',
|
||||||
'webadmin/src/3rdparty/**/*.woff2'
|
'webadmin/src/3rdparty/**/*.woff2'
|
||||||
@@ -39,7 +40,7 @@ gulp.task('3rdparty', function () {
|
|||||||
// JavaScript
|
// JavaScript
|
||||||
// --------------
|
// --------------
|
||||||
|
|
||||||
gulp.task('js', ['js-index', 'js-setup', 'js-update', 'js-error'], function () {});
|
gulp.task('js', ['js-index', 'js-setup', 'js-update'], function () {});
|
||||||
|
|
||||||
var oauth = {
|
var oauth = {
|
||||||
clientId: argv.clientId || 'cid-webadmin',
|
clientId: argv.clientId || 'cid-webadmin',
|
||||||
@@ -80,14 +81,6 @@ gulp.task('js-setup', function () {
|
|||||||
.pipe(gulp.dest('webadmin/dist/js'));
|
.pipe(gulp.dest('webadmin/dist/js'));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('js-error', function () {
|
|
||||||
gulp.src(['webadmin/src/js/error.js'])
|
|
||||||
.pipe(sourcemaps.init())
|
|
||||||
.pipe(uglify())
|
|
||||||
.pipe(sourcemaps.write())
|
|
||||||
.pipe(gulp.dest('webadmin/dist/js'));
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task('js-update', function () {
|
gulp.task('js-update', function () {
|
||||||
gulp.src(['webadmin/src/js/update.js'])
|
gulp.src(['webadmin/src/js/update.js'])
|
||||||
.pipe(sourcemaps.init())
|
.pipe(sourcemaps.init())
|
||||||
@@ -102,7 +95,7 @@ gulp.task('js-update', function () {
|
|||||||
// HTML
|
// HTML
|
||||||
// --------------
|
// --------------
|
||||||
|
|
||||||
gulp.task('html', ['html-views', 'html-update'], function () {
|
gulp.task('html', ['html-views', 'html-update', 'html-templates'], function () {
|
||||||
return gulp.src('webadmin/src/*.html').pipe(gulp.dest('webadmin/dist'));
|
return gulp.src('webadmin/src/*.html').pipe(gulp.dest('webadmin/dist'));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,6 +107,10 @@ gulp.task('html-views', function () {
|
|||||||
return gulp.src('webadmin/src/views/**/*.html').pipe(gulp.dest('webadmin/dist/views'));
|
return gulp.src('webadmin/src/views/**/*.html').pipe(gulp.dest('webadmin/dist/views'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task('html-templates', function () {
|
||||||
|
return gulp.src('webadmin/src/templates/**/*.html').pipe(gulp.dest('webadmin/dist/templates'));
|
||||||
|
});
|
||||||
|
|
||||||
// --------------
|
// --------------
|
||||||
// CSS
|
// CSS
|
||||||
// --------------
|
// --------------
|
||||||
@@ -123,7 +120,7 @@ gulp.task('css', function () {
|
|||||||
.pipe(sourcemaps.init())
|
.pipe(sourcemaps.init())
|
||||||
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
|
.pipe(sass({ includePaths: ['node_modules/bootstrap-sass/assets/stylesheets/'] }).on('error', sass.logError))
|
||||||
.pipe(autoprefixer())
|
.pipe(autoprefixer())
|
||||||
.pipe(minifyCSS())
|
.pipe(cssnano())
|
||||||
.pipe(sourcemaps.write())
|
.pipe(sourcemaps.write())
|
||||||
.pipe(gulp.dest('webadmin/dist'))
|
.pipe(gulp.dest('webadmin/dist'))
|
||||||
.pipe(gulp.dest('setup/splash/website'));
|
.pipe(gulp.dest('setup/splash/website'));
|
||||||
@@ -143,8 +140,8 @@ gulp.task('watch', ['default'], function () {
|
|||||||
gulp.watch(['webadmin/src/img/*'], ['images']);
|
gulp.watch(['webadmin/src/img/*'], ['images']);
|
||||||
gulp.watch(['webadmin/src/**/*.html'], ['html']);
|
gulp.watch(['webadmin/src/**/*.html'], ['html']);
|
||||||
gulp.watch(['webadmin/src/views/*.html'], ['html-views']);
|
gulp.watch(['webadmin/src/views/*.html'], ['html-views']);
|
||||||
|
gulp.watch(['webadmin/src/templates/*.html'], ['html-templates']);
|
||||||
gulp.watch(['webadmin/src/js/update.js'], ['js-update']);
|
gulp.watch(['webadmin/src/js/update.js'], ['js-update']);
|
||||||
gulp.watch(['webadmin/src/js/error.js'], ['js-error']);
|
|
||||||
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
|
gulp.watch(['webadmin/src/js/setup.js', 'webadmin/src/js/client.js'], ['js-setup']);
|
||||||
gulp.watch(['webadmin/src/js/index.js', 'webadmin/src/js/client.js', 'webadmin/src/js/appstore.js', 'webadmin/src/js/main.js', 'webadmin/src/views/*.js'], ['js-index']);
|
gulp.watch(['webadmin/src/js/index.js', 'webadmin/src/js/client.js', 'webadmin/src/js/appstore.js', 'webadmin/src/js/main.js', 'webadmin/src/views/*.js'], ['js-index']);
|
||||||
gulp.watch(['webadmin/src/3rdparty/**/*'], ['3rdparty']);
|
gulp.watch(['webadmin/src/3rdparty/**/*'], ['3rdparty']);
|
||||||
|
|||||||
164
installer.sh.ejs
Executable file
164
installer.sh.ejs
Executable file
@@ -0,0 +1,164 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eu -o pipefail
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "======== Cloudron Installer ========"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ $# -lt 4 ]; then
|
||||||
|
echo "Usage: ./installer.sh <fqdn> <aws key id> <aws key secret> <bucket> <provider> <revision>"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# commandline arguments
|
||||||
|
readonly fqdn="${1}"
|
||||||
|
readonly aws_access_key_id="${2}"
|
||||||
|
readonly aws_access_key_secret="${3}"
|
||||||
|
readonly aws_backup_bucket="${4}"
|
||||||
|
readonly provider="${5}"
|
||||||
|
readonly revision="${6}"
|
||||||
|
|
||||||
|
# environment specific urls
|
||||||
|
<% if (env === 'prod') { %>
|
||||||
|
readonly api_server_origin="https://api.cloudron.io"
|
||||||
|
readonly web_server_origin="https://cloudron.io"
|
||||||
|
<% } else { %>
|
||||||
|
readonly api_server_origin="https://api.<%= env %>.cloudron.io"
|
||||||
|
readonly web_server_origin="https://<%= env %>.cloudron.io"
|
||||||
|
<% } %>
|
||||||
|
readonly release_bucket_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases"
|
||||||
|
readonly versions_url="https://s3.amazonaws.com/<%= env %>-cloudron-releases/versions.json"
|
||||||
|
readonly installer_code_url="${release_bucket_url}/box-${revision}.tar.gz"
|
||||||
|
|
||||||
|
# runtime consts
|
||||||
|
readonly installer_code_file="/tmp/box.tar.gz"
|
||||||
|
readonly installer_tmp_dir="/tmp/box"
|
||||||
|
readonly cert_folder="/tmp/certificates"
|
||||||
|
|
||||||
|
# check for fqdn in /ets/hosts
|
||||||
|
echo "[INFO] checking for hostname entry"
|
||||||
|
readonly hostentry_found=$(grep "${fqdn}" /etc/hosts || true)
|
||||||
|
if [[ -z $hostentry_found ]]; then
|
||||||
|
echo "[WARNING] No entry for ${fqdn} found in /etc/hosts"
|
||||||
|
echo "Adding an entry ..."
|
||||||
|
|
||||||
|
cat >> /etc/hosts <<EOF
|
||||||
|
|
||||||
|
# The following line was added by the Cloudron installer script
|
||||||
|
127.0.1.1 ${fqdn} ${fqdn}
|
||||||
|
EOF
|
||||||
|
else
|
||||||
|
echo "Valid hostname entry found in /etc/hosts"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[INFO] ensure minimal dependencies ..."
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y curl
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[INFO] Generating certificates ..."
|
||||||
|
rm -rf "${cert_folder}"
|
||||||
|
mkdir -p "${cert_folder}"
|
||||||
|
|
||||||
|
cat > "${cert_folder}/CONFIG" <<EOF
|
||||||
|
[ req ]
|
||||||
|
default_bits = 1024
|
||||||
|
default_keyfile = keyfile.pem
|
||||||
|
distinguished_name = req_distinguished_name
|
||||||
|
prompt = no
|
||||||
|
req_extensions = v3_req
|
||||||
|
|
||||||
|
[ req_distinguished_name ]
|
||||||
|
C = DE
|
||||||
|
ST = Berlin
|
||||||
|
L = Berlin
|
||||||
|
O = Cloudron UG
|
||||||
|
OU = Cloudron
|
||||||
|
CN = ${fqdn}
|
||||||
|
emailAddress = cert@cloudron.io
|
||||||
|
|
||||||
|
[ v3_req ]
|
||||||
|
# Extensions to add to a certificate request
|
||||||
|
basicConstraints = CA:FALSE
|
||||||
|
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
|
||||||
|
subjectAltName = @alt_names
|
||||||
|
|
||||||
|
[alt_names]
|
||||||
|
DNS.1 = ${fqdn}
|
||||||
|
DNS.2 = *.${fqdn}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# generate cert files
|
||||||
|
openssl genrsa 2048 > "${cert_folder}/host.key"
|
||||||
|
openssl req -new -out "${cert_folder}/host.csr" -key "${cert_folder}/host.key" -config "${cert_folder}/CONFIG"
|
||||||
|
openssl x509 -req -days 3650 -in "${cert_folder}/host.csr" -signkey "${cert_folder}/host.key" -out "${cert_folder}/host.cert" -extensions v3_req -extfile "${cert_folder}/CONFIG"
|
||||||
|
|
||||||
|
# make them json compatible, by collapsing to one line
|
||||||
|
tls_cert=$(sed ':a;N;$!ba;s/\n/\\n/g' "${cert_folder}/host.cert")
|
||||||
|
tls_key=$(sed ':a;N;$!ba;s/\n/\\n/g' "${cert_folder}/host.key")
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[INFO] Fetching installer code ..."
|
||||||
|
curl "${installer_code_url}" -o "${installer_code_file}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[INFO] Extracting installer code to ${installer_tmp_dir} ..."
|
||||||
|
rm -rf "${installer_tmp_dir}" && mkdir -p "${installer_tmp_dir}"
|
||||||
|
tar xvf "${installer_code_file}" -C "${installer_tmp_dir}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Creating initial provisioning config ..."
|
||||||
|
cat > /root/provision.json <<EOF
|
||||||
|
{
|
||||||
|
"sourceTarballUrl": "",
|
||||||
|
"data": {
|
||||||
|
"apiServerOrigin": "${api_server_origin}",
|
||||||
|
"webServerOrigin": "${web_server_origin}",
|
||||||
|
"fqdn": "${fqdn}",
|
||||||
|
"token": "",
|
||||||
|
"isCustomDomain": true,
|
||||||
|
"boxVersionsUrl": "${versions_url}",
|
||||||
|
"version": "",
|
||||||
|
"tlsCert": "${tls_cert}",
|
||||||
|
"tlsKey": "${tls_key}",
|
||||||
|
"provider": "${provider}",
|
||||||
|
"backupConfig": {
|
||||||
|
"provider": "s3",
|
||||||
|
"accessKeyId": "${aws_access_key_id}",
|
||||||
|
"secretAccessKey": "${aws_access_key_secret}",
|
||||||
|
"bucket": "${aws_backup_bucket}",
|
||||||
|
"prefix": "backups"
|
||||||
|
},
|
||||||
|
"dnsConfig": {
|
||||||
|
"provider": "route53",
|
||||||
|
"accessKeyId": "${aws_access_key_id}",
|
||||||
|
"secretAccessKey": "${aws_access_key_secret}"
|
||||||
|
},
|
||||||
|
"tlsConfig": {
|
||||||
|
"provider": "letsencrypt-<%= env %>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "[INFO] Running Ubuntu initializing script ..."
|
||||||
|
/bin/bash "${installer_tmp_dir}/baseimage/initializeBaseUbuntuImage.sh" "${revision}" selfhosting
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[INFO] Reloading systemd daemon ..."
|
||||||
|
systemctl daemon-reload
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[INFO] Restart docker ..."
|
||||||
|
systemctl restart docker
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "[FINISHED] Now starting Cloudron init jobs ..."
|
||||||
|
systemctl start box-setup
|
||||||
|
|
||||||
|
# TODO this is only for convenience we should probably just let the user do a restart
|
||||||
|
sleep 5 && sync
|
||||||
|
systemctl start cloudron-installer
|
||||||
|
journalctl -u cloudron-installer.service -f
|
||||||
516
installer/npm-shrinkwrap.json
generated
Normal file
516
installer/npm-shrinkwrap.json
generated
Normal file
@@ -0,0 +1,516 @@
|
|||||||
|
{
|
||||||
|
"name": "installer",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"dependencies": {
|
||||||
|
"async": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"from": "async@>=1.5.0 <2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-1.5.0.tgz"
|
||||||
|
},
|
||||||
|
"body-parser": {
|
||||||
|
"version": "1.14.1",
|
||||||
|
"from": "body-parser@>=1.12.0 <2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.14.1.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"from": "bytes@2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-2.1.0.tgz"
|
||||||
|
},
|
||||||
|
"content-type": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"from": "content-type@>=1.0.1 <1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz"
|
||||||
|
},
|
||||||
|
"depd": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"from": "depd@>=1.1.0 <1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz"
|
||||||
|
},
|
||||||
|
"http-errors": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"from": "http-errors@>=1.3.1 <1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"from": "inherits@>=2.0.1 <2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"from": "statuses@>=1.0.0 <2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"iconv-lite": {
|
||||||
|
"version": "0.4.12",
|
||||||
|
"from": "iconv-lite@0.4.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.12.tgz"
|
||||||
|
},
|
||||||
|
"on-finished": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"from": "on-finished@>=2.3.0 <2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"from": "ee-first@1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"qs": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"from": "qs@5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-5.1.0.tgz"
|
||||||
|
},
|
||||||
|
"raw-body": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"from": "raw-body@>=2.1.4 <2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.1.4.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"from": "unpipe@1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type-is": {
|
||||||
|
"version": "1.6.9",
|
||||||
|
"from": "type-is@>=1.6.9 <1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.9.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"media-typer": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"from": "media-typer@0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
|
||||||
|
},
|
||||||
|
"mime-types": {
|
||||||
|
"version": "2.1.7",
|
||||||
|
"from": "mime-types@>=2.1.7 <2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.7.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": {
|
||||||
|
"version": "1.19.0",
|
||||||
|
"from": "mime-db@>=1.19.0 <1.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.19.0.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"connect-lastmile": {
|
||||||
|
"version": "0.0.13",
|
||||||
|
"from": "connect-lastmile@0.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.13.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"from": "debug@>=2.1.0 <2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"from": "ms@0.7.0",
|
||||||
|
"resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"from": "debug@>=2.1.1 <3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"from": "ms@0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"express": {
|
||||||
|
"version": "4.13.3",
|
||||||
|
"from": "express@>=4.11.2 <5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-4.13.3.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": {
|
||||||
|
"version": "1.2.13",
|
||||||
|
"from": "accepts@>=1.2.12 <1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.2.13.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": {
|
||||||
|
"version": "2.1.7",
|
||||||
|
"from": "mime-types@>=2.1.6 <2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.7.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": {
|
||||||
|
"version": "1.19.0",
|
||||||
|
"from": "mime-db@>=1.19.0 <1.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.19.0.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"negotiator": {
|
||||||
|
"version": "0.5.3",
|
||||||
|
"from": "negotiator@0.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.5.3.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"array-flatten": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"from": "array-flatten@1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz"
|
||||||
|
},
|
||||||
|
"content-disposition": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"from": "content-disposition@0.5.0",
|
||||||
|
"resolved": "http://registry.npmjs.org/content-disposition/-/content-disposition-0.5.0.tgz"
|
||||||
|
},
|
||||||
|
"content-type": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"from": "content-type@>=1.0.1 <1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.1.tgz"
|
||||||
|
},
|
||||||
|
"cookie": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"from": "cookie@0.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.1.3.tgz"
|
||||||
|
},
|
||||||
|
"cookie-signature": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"from": "cookie-signature@1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
|
||||||
|
},
|
||||||
|
"depd": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"from": "depd@>=1.0.1 <1.1.0",
|
||||||
|
"resolved": "http://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
|
||||||
|
},
|
||||||
|
"escape-html": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"from": "escape-html@1.0.2",
|
||||||
|
"resolved": "http://registry.npmjs.org/escape-html/-/escape-html-1.0.2.tgz"
|
||||||
|
},
|
||||||
|
"etag": {
|
||||||
|
"version": "1.7.0",
|
||||||
|
"from": "etag@>=1.7.0 <1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.7.0.tgz"
|
||||||
|
},
|
||||||
|
"finalhandler": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"from": "finalhandler@0.4.0",
|
||||||
|
"resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-0.4.0.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"from": "unpipe@>=1.0.0 <1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fresh": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"from": "fresh@0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.3.0.tgz"
|
||||||
|
},
|
||||||
|
"merge-descriptors": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"from": "merge-descriptors@1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.0.tgz"
|
||||||
|
},
|
||||||
|
"methods": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"from": "methods@>=1.1.1 <1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.1.tgz"
|
||||||
|
},
|
||||||
|
"on-finished": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"from": "on-finished@>=2.3.0 <2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"from": "ee-first@1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"parseurl": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"from": "parseurl@>=1.3.0 <1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.0.tgz"
|
||||||
|
},
|
||||||
|
"path-to-regexp": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"from": "path-to-regexp@0.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
|
||||||
|
},
|
||||||
|
"proxy-addr": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"from": "proxy-addr@>=1.0.8 <1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-1.0.8.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"forwarded": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"from": "forwarded@>=0.1.0 <0.2.0",
|
||||||
|
"resolved": "http://registry.npmjs.org/forwarded/-/forwarded-0.1.0.tgz"
|
||||||
|
},
|
||||||
|
"ipaddr.js": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"from": "ipaddr.js@1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.0.1.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"qs": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"from": "qs@4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-4.0.0.tgz"
|
||||||
|
},
|
||||||
|
"range-parser": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"from": "range-parser@>=1.0.2 <1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.0.3.tgz"
|
||||||
|
},
|
||||||
|
"send": {
|
||||||
|
"version": "0.13.0",
|
||||||
|
"from": "send@0.13.0",
|
||||||
|
"resolved": "http://registry.npmjs.org/send/-/send-0.13.0.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"destroy": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"from": "destroy@1.0.3",
|
||||||
|
"resolved": "http://registry.npmjs.org/destroy/-/destroy-1.0.3.tgz"
|
||||||
|
},
|
||||||
|
"http-errors": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"from": "http-errors@>=1.3.1 <1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.3.1.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"inherits": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"from": "inherits@>=2.0.1 <2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mime": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"from": "mime@1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.3.4.tgz"
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"from": "ms@0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"from": "statuses@>=1.2.1 <1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.2.1.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"serve-static": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"from": "serve-static@>=1.10.0 <1.11.0",
|
||||||
|
"resolved": "http://registry.npmjs.org/serve-static/-/serve-static-1.10.0.tgz"
|
||||||
|
},
|
||||||
|
"type-is": {
|
||||||
|
"version": "1.6.9",
|
||||||
|
"from": "type-is@>=1.6.9 <1.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.9.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"media-typer": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"from": "media-typer@0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
|
||||||
|
},
|
||||||
|
"mime-types": {
|
||||||
|
"version": "2.1.7",
|
||||||
|
"from": "mime-types@>=2.1.6 <2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.7.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": {
|
||||||
|
"version": "1.19.0",
|
||||||
|
"from": "mime-db@>=1.19.0 <1.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.19.0.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"utils-merge": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"from": "utils-merge@1.0.0",
|
||||||
|
"resolved": "http://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz"
|
||||||
|
},
|
||||||
|
"vary": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"from": "vary@>=1.0.1 <1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.0.1.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"json": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"from": "json@>=9.0.3 <10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json/-/json-9.0.3.tgz"
|
||||||
|
},
|
||||||
|
"morgan": {
|
||||||
|
"version": "1.6.1",
|
||||||
|
"from": "morgan@>=1.5.1 <2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.6.1.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"basic-auth": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"from": "basic-auth@>=1.0.3 <1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.0.3.tgz"
|
||||||
|
},
|
||||||
|
"depd": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"from": "depd@>=1.0.1 <1.1.0",
|
||||||
|
"resolved": "http://registry.npmjs.org/depd/-/depd-1.0.1.tgz"
|
||||||
|
},
|
||||||
|
"on-finished": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"from": "on-finished@>=2.3.0 <2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"from": "ee-first@1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"on-headers": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"from": "on-headers@>=1.0.0 <1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"proxy-middleware": {
|
||||||
|
"version": "0.15.0",
|
||||||
|
"from": "proxy-middleware@>=0.15.0 <0.16.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-middleware/-/proxy-middleware-0.15.0.tgz"
|
||||||
|
},
|
||||||
|
"safetydance": {
|
||||||
|
"version": "0.0.19",
|
||||||
|
"from": "safetydance@0.0.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz"
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"from": "semver@>=5.1.0 <6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz"
|
||||||
|
},
|
||||||
|
"superagent": {
|
||||||
|
"version": "0.21.0",
|
||||||
|
"from": "superagent@>=0.21.0 <0.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/superagent/-/superagent-0.21.0.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"component-emitter": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"from": "component-emitter@1.1.2",
|
||||||
|
"resolved": "http://registry.npmjs.org/component-emitter/-/component-emitter-1.1.2.tgz"
|
||||||
|
},
|
||||||
|
"cookiejar": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"from": "cookiejar@2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.0.1.tgz"
|
||||||
|
},
|
||||||
|
"extend": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"from": "extend@>=1.2.1 <1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/extend/-/extend-1.2.1.tgz"
|
||||||
|
},
|
||||||
|
"form-data": {
|
||||||
|
"version": "0.1.3",
|
||||||
|
"from": "form-data@0.1.3",
|
||||||
|
"resolved": "http://registry.npmjs.org/form-data/-/form-data-0.1.3.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"async": {
|
||||||
|
"version": "0.9.2",
|
||||||
|
"from": "async@>=0.9.0 <0.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz"
|
||||||
|
},
|
||||||
|
"combined-stream": {
|
||||||
|
"version": "0.0.7",
|
||||||
|
"from": "combined-stream@>=0.0.4 <0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-0.0.7.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": {
|
||||||
|
"version": "0.0.5",
|
||||||
|
"from": "delayed-stream@0.0.5",
|
||||||
|
"resolved": "http://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.5.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"formidable": {
|
||||||
|
"version": "1.0.14",
|
||||||
|
"from": "formidable@1.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/formidable/-/formidable-1.0.14.tgz"
|
||||||
|
},
|
||||||
|
"methods": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"from": "methods@1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.0.1.tgz"
|
||||||
|
},
|
||||||
|
"mime": {
|
||||||
|
"version": "1.2.11",
|
||||||
|
"from": "mime@1.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.2.11.tgz"
|
||||||
|
},
|
||||||
|
"qs": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"from": "qs@1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-1.2.0.tgz"
|
||||||
|
},
|
||||||
|
"readable-stream": {
|
||||||
|
"version": "1.0.27-1",
|
||||||
|
"from": "readable-stream@1.0.27-1",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.27-1.tgz",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"from": "core-util-is@>=1.0.0 <1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.1.tgz"
|
||||||
|
},
|
||||||
|
"inherits": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"from": "inherits@>=2.0.1 <2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
|
||||||
|
},
|
||||||
|
"isarray": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"from": "isarray@0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
|
||||||
|
},
|
||||||
|
"string_decoder": {
|
||||||
|
"version": "0.10.31",
|
||||||
|
"from": "string_decoder@>=0.10.0 <0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reduce-component": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"from": "reduce-component@1.0.1",
|
||||||
|
"resolved": "http://registry.npmjs.org/reduce-component/-/reduce-component-1.0.1.tgz"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
installer/package.json
Normal file
47
installer/package.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "installer",
|
||||||
|
"description": "Cloudron Installer",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": "true",
|
||||||
|
"author": {
|
||||||
|
"name": "Cloudron authors"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git"
|
||||||
|
},
|
||||||
|
"engines": [
|
||||||
|
"node >=4.0.0 <=4.1.1"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"async": "^1.5.0",
|
||||||
|
"body-parser": "^1.12.0",
|
||||||
|
"connect-lastmile": "0.0.13",
|
||||||
|
"debug": "^2.1.1",
|
||||||
|
"express": "^4.11.2",
|
||||||
|
"json": "^9.0.3",
|
||||||
|
"morgan": "^1.5.1",
|
||||||
|
"proxy-middleware": "^0.15.0",
|
||||||
|
"safetydance": "0.0.19",
|
||||||
|
"semver": "^5.1.0",
|
||||||
|
"superagent": "^0.21.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"colors": "^1.1.2",
|
||||||
|
"commander": "^2.8.1",
|
||||||
|
"expect.js": "^0.3.1",
|
||||||
|
"istanbul": "^0.3.5",
|
||||||
|
"lodash": "^3.2.0",
|
||||||
|
"mocha": "^2.1.0",
|
||||||
|
"nock": "^0.59.1",
|
||||||
|
"sleep": "^3.0.0",
|
||||||
|
"superagent-sync": "^0.2.0",
|
||||||
|
"supererror": "^0.7.0",
|
||||||
|
"yesno": "0.0.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "NODE_ENV=test ./node_modules/istanbul/lib/cli.js test $1 ./node_modules/mocha/bin/_mocha -- -R spec ./src/test",
|
||||||
|
"precommit": "/bin/true",
|
||||||
|
"prepush": "npm test",
|
||||||
|
"postmerge": "/bin/true"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
installer/src/certs/.gitignore
vendored
Normal file
0
installer/src/certs/.gitignore
vendored
Normal file
112
installer/src/installer.js
Normal file
112
installer/src/installer.js
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/* jslint node: true */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var assert = require('assert'),
|
||||||
|
child_process = require('child_process'),
|
||||||
|
debug = require('debug')('installer:installer'),
|
||||||
|
path = require('path'),
|
||||||
|
safe = require('safetydance'),
|
||||||
|
semver = require('semver'),
|
||||||
|
superagent = require('superagent'),
|
||||||
|
util = require('util');
|
||||||
|
|
||||||
|
exports = module.exports = {
|
||||||
|
InstallerError: InstallerError,
|
||||||
|
|
||||||
|
provision: provision,
|
||||||
|
|
||||||
|
_ensureVersion: ensureVersion
|
||||||
|
};
|
||||||
|
|
||||||
|
var INSTALLER_CMD = path.join(__dirname, 'scripts/installer.sh'),
|
||||||
|
SUDO = '/usr/bin/sudo';
|
||||||
|
|
||||||
|
function InstallerError(reason, info) {
|
||||||
|
Error.call(this);
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
|
||||||
|
this.name = this.constructor.name;
|
||||||
|
this.reason = reason;
|
||||||
|
this.message = !info ? reason : (typeof info === 'object' ? JSON.stringify(info) : info);
|
||||||
|
}
|
||||||
|
util.inherits(InstallerError, Error);
|
||||||
|
InstallerError.INTERNAL_ERROR = 1;
|
||||||
|
InstallerError.ALREADY_PROVISIONED = 2;
|
||||||
|
|
||||||
|
// system until file has KillMode=control-group to bring down child processes
|
||||||
|
function spawn(tag, cmd, args, callback) {
|
||||||
|
assert.strictEqual(typeof tag, 'string');
|
||||||
|
assert.strictEqual(typeof cmd, 'string');
|
||||||
|
assert(util.isArray(args));
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var cp = child_process.spawn(cmd, args, { timeout: 0 });
|
||||||
|
cp.stdout.setEncoding('utf8');
|
||||||
|
cp.stdout.on('data', function (data) { debug('%s (stdout): %s', tag, data); });
|
||||||
|
cp.stderr.setEncoding('utf8');
|
||||||
|
cp.stderr.on('data', function (data) { debug('%s (stderr): %s', tag, data); });
|
||||||
|
|
||||||
|
cp.on('error', function (error) {
|
||||||
|
debug('%s : child process errored %s', tag, error.message);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
cp.on('exit', function (code, signal) {
|
||||||
|
debug('%s : child process exited. code: %d signal: %d', tag, code, signal);
|
||||||
|
if (signal) return callback(new Error('Exited with signal ' + signal));
|
||||||
|
if (code !== 0) return callback(new Error('Exited with code ' + code));
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureVersion(args, callback) {
|
||||||
|
assert.strictEqual(typeof args, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
if (!args.data || !args.data.boxVersionsUrl) return callback(new Error('No boxVersionsUrl specified'));
|
||||||
|
|
||||||
|
if (args.sourceTarballUrl) return callback(null, args);
|
||||||
|
|
||||||
|
superagent.get(args.data.boxVersionsUrl).end(function (error, result) {
|
||||||
|
if (error && !error.response) return callback(error);
|
||||||
|
if (result.statusCode !== 200) return callback(new Error(util.format('Bad status: %s %s', result.statusCode, result.text)));
|
||||||
|
|
||||||
|
var versions = safe.JSON.parse(result.text);
|
||||||
|
|
||||||
|
if (!versions || typeof versions !== 'object') return callback(new Error('versions is not in valid format:' + safe.error));
|
||||||
|
|
||||||
|
var latestVersion = Object.keys(versions).sort(semver.compare).pop();
|
||||||
|
debug('ensureVersion: Latest version is %s etag:%s', latestVersion, result.header['etag']);
|
||||||
|
|
||||||
|
if (!versions[latestVersion]) return callback(new Error('No version available'));
|
||||||
|
if (!versions[latestVersion].sourceTarballUrl) return callback(new Error('No sourceTarballUrl specified'));
|
||||||
|
|
||||||
|
args.sourceTarballUrl = versions[latestVersion].sourceTarballUrl;
|
||||||
|
args.data.version = latestVersion;
|
||||||
|
|
||||||
|
callback(null, args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function provision(args, callback) {
|
||||||
|
assert.strictEqual(typeof args, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'test') return callback(null);
|
||||||
|
|
||||||
|
ensureVersion(args, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
var pargs = [ INSTALLER_CMD ];
|
||||||
|
pargs.push('--sourcetarballurl', result.sourceTarballUrl);
|
||||||
|
pargs.push('--data', JSON.stringify(result.data));
|
||||||
|
|
||||||
|
debug('provision: calling with args %j', pargs);
|
||||||
|
|
||||||
|
// sudo is required for update()
|
||||||
|
spawn('provision', SUDO, pargs, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
67
installer/src/scripts/installer.sh
Executable file
67
installer/src/scripts/installer.sh
Executable file
@@ -0,0 +1,67 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eu -o pipefail
|
||||||
|
|
||||||
|
readonly BOX_SRC_DIR=/home/yellowtent/box
|
||||||
|
readonly DATA_DIR=/home/yellowtent/data
|
||||||
|
|
||||||
|
readonly script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
readonly json="${script_dir}/../../node_modules/.bin/json"
|
||||||
|
readonly curl="curl --fail --connect-timeout 20 --retry 10 --retry-delay 2 --max-time 300"
|
||||||
|
|
||||||
|
readonly is_update=$([[ -d "${BOX_SRC_DIR}" ]] && echo "yes" || echo "no")
|
||||||
|
|
||||||
|
# create a provision file for testing. %q escapes args. %q is reused as much as necessary to satisfy $@
|
||||||
|
(echo -e "#!/bin/bash\n"; printf "%q " "${script_dir}/installer.sh" "$@") > /home/yellowtent/provision.sh
|
||||||
|
chmod +x /home/yellowtent/provision.sh
|
||||||
|
|
||||||
|
arg_source_tarball_url=""
|
||||||
|
arg_data=""
|
||||||
|
|
||||||
|
args=$(getopt -o "" -l "sourcetarballurl:,data:" -n "$0" -- "$@")
|
||||||
|
eval set -- "${args}"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
case "$1" in
|
||||||
|
--sourcetarballurl) arg_source_tarball_url="$2";;
|
||||||
|
--data) arg_data="$2";;
|
||||||
|
--) break;;
|
||||||
|
*) echo "Unknown option $1"; exit 1;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
shift 2
|
||||||
|
done
|
||||||
|
|
||||||
|
box_src_tmp_dir=$(mktemp -dt box-src-XXXXXX)
|
||||||
|
echo "Downloading box code from ${arg_source_tarball_url} to ${box_src_tmp_dir}"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
if $curl -L "${arg_source_tarball_url}" | tar -zxf - -C "${box_src_tmp_dir}"; then break; fi
|
||||||
|
echo "Failed to download source tarball, trying again"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
while true; do
|
||||||
|
# for reasons unknown, the dtrace package will fail. but rebuilding second time will work
|
||||||
|
if cd "${box_src_tmp_dir}" && npm rebuild; then break; fi
|
||||||
|
echo "Failed to rebuild, trying again"
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${is_update}" == "yes" ]]; then
|
||||||
|
echo "Setting up update splash screen"
|
||||||
|
"${box_src_tmp_dir}/setup/splashpage.sh" --data "${arg_data}" # show splash from new code
|
||||||
|
${BOX_SRC_DIR}/setup/stop.sh # stop the old code
|
||||||
|
fi
|
||||||
|
|
||||||
|
# switch the codes
|
||||||
|
rm -rf "${BOX_SRC_DIR}"
|
||||||
|
mv "${box_src_tmp_dir}" "${BOX_SRC_DIR}"
|
||||||
|
chown -R yellowtent.yellowtent "${BOX_SRC_DIR}"
|
||||||
|
|
||||||
|
# create a start file for testing. %q escapes args
|
||||||
|
(echo -e "#!/bin/bash\n"; printf "%q " "${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}") > /home/yellowtent/setup_start.sh
|
||||||
|
chmod +x /home/yellowtent/setup_start.sh
|
||||||
|
|
||||||
|
echo "Calling box setup script"
|
||||||
|
"${BOX_SRC_DIR}/setup/start.sh" --data "${arg_data}"
|
||||||
|
|
||||||
144
installer/src/server.js
Executable file
144
installer/src/server.js
Executable file
@@ -0,0 +1,144 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/* jslint node: true */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var assert = require('assert'),
|
||||||
|
async = require('async'),
|
||||||
|
debug = require('debug')('installer:server'),
|
||||||
|
express = require('express'),
|
||||||
|
fs = require('fs'),
|
||||||
|
http = require('http'),
|
||||||
|
HttpError = require('connect-lastmile').HttpError,
|
||||||
|
HttpSuccess = require('connect-lastmile').HttpSuccess,
|
||||||
|
installer = require('./installer.js'),
|
||||||
|
json = require('body-parser').json,
|
||||||
|
lastMile = require('connect-lastmile'),
|
||||||
|
morgan = require('morgan'),
|
||||||
|
superagent = require('superagent');
|
||||||
|
|
||||||
|
exports = module.exports = {
|
||||||
|
start: start,
|
||||||
|
stop: stop
|
||||||
|
};
|
||||||
|
|
||||||
|
var PROVISION_CONFIG_FILE = '/root/provision.json';
|
||||||
|
var CLOUDRON_CONFIG_FILE = '/home/yellowtent/configs/cloudron.conf';
|
||||||
|
|
||||||
|
var gHttpServer = null; // update server; used for updates
|
||||||
|
|
||||||
|
function provisionDigitalOcean(callback) {
|
||||||
|
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) return callback(null); // already provisioned
|
||||||
|
|
||||||
|
superagent.get('http://169.254.169.254/metadata/v1.json').end(function (error, result) {
|
||||||
|
if (error || result.statusCode !== 200) {
|
||||||
|
console.error('Error getting metadata', error);
|
||||||
|
return callback(new Error('Error getting metadata'));
|
||||||
|
}
|
||||||
|
|
||||||
|
var userData = JSON.parse(result.body.user_data);
|
||||||
|
|
||||||
|
installer.provision(userData, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function provisionLocal(callback) {
|
||||||
|
if (fs.existsSync(CLOUDRON_CONFIG_FILE)) return callback(null); // already provisioned
|
||||||
|
|
||||||
|
if (!fs.existsSync(PROVISION_CONFIG_FILE)) {
|
||||||
|
console.error('No provisioning data found at %s', PROVISION_CONFIG_FILE);
|
||||||
|
return callback(new Error('No provisioning data found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
var userData = require(PROVISION_CONFIG_FILE);
|
||||||
|
|
||||||
|
installer.provision(userData, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(req, res, next) {
|
||||||
|
assert.strictEqual(typeof req.body, 'object');
|
||||||
|
|
||||||
|
if (!req.body.sourceTarballUrl || typeof req.body.sourceTarballUrl !== 'string') return next(new HttpError(400, 'No sourceTarballUrl provided'));
|
||||||
|
if (!req.body.data || typeof req.body.data !== 'object') return next(new HttpError(400, 'No data provided'));
|
||||||
|
|
||||||
|
debug('provision: received from box %j', req.body);
|
||||||
|
|
||||||
|
installer.provision(req.body, function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
next(new HttpSuccess(202, { }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function startUpdateServer(callback) {
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
debug('Starting update server');
|
||||||
|
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
var router = new express.Router();
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'test') app.use(morgan('dev', { immediate: false }));
|
||||||
|
|
||||||
|
app.use(json({ strict: true }))
|
||||||
|
.use(router)
|
||||||
|
.use(lastMile());
|
||||||
|
|
||||||
|
router.post('/api/v1/installer/update', update);
|
||||||
|
|
||||||
|
gHttpServer = http.createServer(app);
|
||||||
|
gHttpServer.on('error', console.error);
|
||||||
|
|
||||||
|
gHttpServer.listen(2020, '127.0.0.1', callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopUpdateServer(callback) {
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
debug('Stopping update server');
|
||||||
|
|
||||||
|
if (!gHttpServer) return callback(null);
|
||||||
|
|
||||||
|
gHttpServer.close(callback);
|
||||||
|
gHttpServer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function start(callback) {
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var actions;
|
||||||
|
|
||||||
|
if (process.env.PROVISION === 'local') {
|
||||||
|
debug('Starting Installer in selfhost mode');
|
||||||
|
|
||||||
|
actions = [
|
||||||
|
startUpdateServer,
|
||||||
|
provisionLocal
|
||||||
|
];
|
||||||
|
} else { // current fallback, should be 'digitalocean' eventually, see initializeBaseUbuntuImage.sh
|
||||||
|
debug('Starting Installer in managed mode');
|
||||||
|
|
||||||
|
actions = [
|
||||||
|
startUpdateServer,
|
||||||
|
provisionDigitalOcean
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
async.series(actions, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop(callback) {
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
async.series([
|
||||||
|
stopUpdateServer
|
||||||
|
], callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
start(function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
179
installer/src/test/installer-test.js
Normal file
179
installer/src/test/installer-test.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
/* jslint node:true */
|
||||||
|
/* global it:false */
|
||||||
|
/* global describe:false */
|
||||||
|
/* global before:false */
|
||||||
|
/* global after:false */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var expect = require('expect.js'),
|
||||||
|
fs = require('fs'),
|
||||||
|
path = require('path'),
|
||||||
|
nock = require('nock'),
|
||||||
|
os = require('os'),
|
||||||
|
request = require('superagent'),
|
||||||
|
server = require('../server.js'),
|
||||||
|
installer = require('../installer.js'),
|
||||||
|
_ = require('lodash');
|
||||||
|
|
||||||
|
var EXTERNAL_SERVER_URL = 'https://localhost:4443';
|
||||||
|
var INTERNAL_SERVER_URL = 'http://localhost:2020';
|
||||||
|
var APPSERVER_ORIGIN = 'http://appserver';
|
||||||
|
var FQDN = os.hostname();
|
||||||
|
|
||||||
|
describe('Server', function () {
|
||||||
|
this.timeout(5000);
|
||||||
|
|
||||||
|
before(function (done) {
|
||||||
|
var user_data = JSON.stringify({ apiServerOrigin: APPSERVER_ORIGIN }); // user_data is a string
|
||||||
|
var scope = nock('http://169.254.169.254')
|
||||||
|
.persist()
|
||||||
|
.get('/metadata/v1.json')
|
||||||
|
.reply(200, JSON.stringify({ user_data: user_data }), { 'Content-Type': 'application/json' });
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function (done) {
|
||||||
|
nock.cleanAll();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('starts and stop', function () {
|
||||||
|
it('starts', function (done) {
|
||||||
|
server.start(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stops', function (done) {
|
||||||
|
server.stop(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('update (internal server)', function () {
|
||||||
|
before(function (done) {
|
||||||
|
server.start(done);
|
||||||
|
});
|
||||||
|
after(function (done) {
|
||||||
|
server.stop(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not respond to provision', function (done) {
|
||||||
|
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/provision').send({ }).end(function (error, result) {
|
||||||
|
expect(error).to.not.be.ok();
|
||||||
|
expect(result.statusCode).to.equal(404);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not respond to restore', function (done) {
|
||||||
|
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/restore').send({ }).end(function (error, result) {
|
||||||
|
expect(error).to.not.be.ok();
|
||||||
|
expect(result.statusCode).to.equal(404);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
sourceTarballUrl: "https://foo.tar.gz",
|
||||||
|
|
||||||
|
data: {
|
||||||
|
token: 'sometoken',
|
||||||
|
apiServerOrigin: APPSERVER_ORIGIN,
|
||||||
|
webServerOrigin: 'https://somethingelse.com',
|
||||||
|
fqdn: 'www.something.com',
|
||||||
|
tlsKey: 'key',
|
||||||
|
tlsCert: 'cert',
|
||||||
|
boxVersionsUrl: 'https://versions.json',
|
||||||
|
version: '0.1'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(data).forEach(function (key) {
|
||||||
|
it('fails due to missing ' + key, function (done) {
|
||||||
|
var dataCopy = _.merge({ }, data);
|
||||||
|
delete dataCopy[key];
|
||||||
|
|
||||||
|
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/update').send(dataCopy).end(function (error, result) {
|
||||||
|
expect(error).to.not.be.ok();
|
||||||
|
expect(result.statusCode).to.equal(400);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('succeeds', function (done) {
|
||||||
|
request.post(INTERNAL_SERVER_URL + '/api/v1/installer/update').send(data).end(function (error, result) {
|
||||||
|
expect(error).to.not.be.ok();
|
||||||
|
expect(result.statusCode).to.equal(202);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ensureVersion', function () {
|
||||||
|
before(function () {
|
||||||
|
process.env.NODE_ENV = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function () {
|
||||||
|
process.env.NODE_ENV = 'test';
|
||||||
|
});
|
||||||
|
|
||||||
|
it ('fails without data', function (done) {
|
||||||
|
installer._ensureVersion({}, function (error) {
|
||||||
|
expect(error).to.be.an(Error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it ('fails without boxVersionsUrl', function (done) {
|
||||||
|
installer._ensureVersion({ data: {}}, function (error) {
|
||||||
|
expect(error).to.be.an(Error);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it ('succeeds with sourceTarballUrl', function (done) {
|
||||||
|
var data = {
|
||||||
|
sourceTarballUrl: 'sometarballurl',
|
||||||
|
data: {
|
||||||
|
boxVersionsUrl: 'http://foobar/versions.json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
installer._ensureVersion(data, function (error, result) {
|
||||||
|
expect(error).to.equal(null);
|
||||||
|
expect(result).to.eql(data);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it ('succeeds without sourceTarballUrl', function (done) {
|
||||||
|
var versions = {
|
||||||
|
'0.1.0': {
|
||||||
|
sourceTarballUrl: 'sometarballurl1'
|
||||||
|
},
|
||||||
|
'0.2.0': {
|
||||||
|
sourceTarballUrl: 'sometarballurl2'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var scope = nock('http://foobar')
|
||||||
|
.get('/versions.json')
|
||||||
|
.reply(200, JSON.stringify(versions), { 'Content-Type': 'application/json' });
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
data: {
|
||||||
|
boxVersionsUrl: 'http://foobar/versions.json'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
installer._ensureVersion(data, function (error, result) {
|
||||||
|
expect(error).to.equal(null);
|
||||||
|
expect(result.sourceTarballUrl).to.equal(versions['0.2.0'].sourceTarballUrl);
|
||||||
|
expect(result.data.boxVersionsUrl).to.equal(data.data.boxVersionsUrl);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
54
installer/systemd/box-setup.sh
Executable file
54
installer/systemd/box-setup.sh
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eu -o pipefail
|
||||||
|
|
||||||
|
readonly USER_HOME="/home/yellowtent"
|
||||||
|
readonly APPS_SWAP_FILE="/apps.swap"
|
||||||
|
readonly USER_DATA_FILE="/root/user_data.img"
|
||||||
|
readonly USER_DATA_DIR="/home/yellowtent/data"
|
||||||
|
|
||||||
|
# detect device
|
||||||
|
if [[ -b "/dev/vda1" ]]; then
|
||||||
|
disk_device="/dev/vda1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -b "/dev/xvda1" ]]; then
|
||||||
|
disk_device="/dev/xvda1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# all sizes are in mb
|
||||||
|
readonly physical_memory=$(free -m | awk '/Mem:/ { print $2 }')
|
||||||
|
readonly swap_size="${physical_memory}" # if you change this, fix enoughResourcesAvailable() in client.js
|
||||||
|
readonly app_count=$((${physical_memory} / 200)) # estimated app count
|
||||||
|
readonly disk_size_gb=$(fdisk -l ${disk_device} | grep "Disk ${disk_device}" | awk '{ print $3 }')
|
||||||
|
readonly disk_size=$((disk_size_gb * 1024))
|
||||||
|
readonly system_size=10240 # 10 gigs for system libs, apps images, installer, box code and tmp
|
||||||
|
readonly ext4_reserved=$((disk_size * 5 / 100)) # this can be changes using tune2fs -m percent /dev/vda1
|
||||||
|
|
||||||
|
echo "Disk device: ${disk_device}"
|
||||||
|
echo "Physical memory: ${physical_memory}"
|
||||||
|
echo "Estimated app count: ${app_count}"
|
||||||
|
echo "Disk size: ${disk_size}"
|
||||||
|
|
||||||
|
# Allocate swap for general app usage
|
||||||
|
if [[ ! -f "${APPS_SWAP_FILE}" ]]; then
|
||||||
|
echo "Creating Apps swap file of size ${swap_size}M"
|
||||||
|
fallocate -l "${swap_size}m" "${APPS_SWAP_FILE}"
|
||||||
|
chmod 600 "${APPS_SWAP_FILE}"
|
||||||
|
mkswap "${APPS_SWAP_FILE}"
|
||||||
|
swapon "${APPS_SWAP_FILE}"
|
||||||
|
echo "${APPS_SWAP_FILE} none swap sw 0 0" >> /etc/fstab
|
||||||
|
else
|
||||||
|
echo "Apps Swap file already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Resizing data volume"
|
||||||
|
home_data_size=$((disk_size - system_size - swap_size - ext4_reserved))
|
||||||
|
echo "Resizing up btrfs user data to size ${home_data_size}M"
|
||||||
|
umount "${USER_DATA_DIR}" || true
|
||||||
|
# Do not preallocate (non-sparse). Doing so overallocates for data too much in advance and causes problems when using many apps with smaller data
|
||||||
|
# fallocate -l "${home_data_size}m" "${USER_DATA_FILE}" # does not overwrite existing data
|
||||||
|
truncate -s "${home_data_size}m" "${USER_DATA_FILE}" # this will shrink it if the file had existed. this is useful when running this script on a live system
|
||||||
|
mount -t btrfs -o loop,nosuid "${USER_DATA_FILE}" ${USER_DATA_DIR}
|
||||||
|
btrfs filesystem resize max "${USER_DATA_DIR}"
|
||||||
|
|
||||||
70
janitor.js
70
janitor.js
@@ -1,70 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
require('supererror')({ splatchError: true });
|
|
||||||
|
|
||||||
var assert = require('assert'),
|
|
||||||
debug = require('debug')('box:janitor'),
|
|
||||||
async = require('async'),
|
|
||||||
tokendb = require('./src/tokendb.js'),
|
|
||||||
authcodedb = require('./src/authcodedb.js'),
|
|
||||||
database = require('./src/database.js');
|
|
||||||
|
|
||||||
var TOKEN_CLEANUP_INTERVAL = 30000;
|
|
||||||
|
|
||||||
function initialize(callback) {
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
database.initialize
|
|
||||||
], callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanupExpiredTokens(callback) {
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
tokendb.delExpired(function (error, result) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
debug('Cleaned up %s expired tokens.', result);
|
|
||||||
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanupExpiredAuthCodes(callback) {
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
authcodedb.delExpired(function (error, result) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
debug('Cleaned up %s expired authcodes.', result);
|
|
||||||
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
cleanupExpiredTokens(function (error) {
|
|
||||||
if (error) console.error(error);
|
|
||||||
|
|
||||||
cleanupExpiredAuthCodes(function (error) {
|
|
||||||
if (error) console.error(error);
|
|
||||||
|
|
||||||
setTimeout(run, TOKEN_CLEANUP_INTERVAL);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
initialize(function (error) {
|
|
||||||
if (error) {
|
|
||||||
console.error('janitor task exiting with error', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
17
migrations/20151013073519-apps-add-oauthProxy.js
Normal file
17
migrations/20151013073519-apps-add-oauthProxy.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
dbm = dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps ADD COLUMN oauthProxy BOOLEAN DEFAULT 0', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps DROP COLUMN oauthProxy', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
17
migrations/20151015232915-clients-add-type.js
Normal file
17
migrations/20151015232915-clients-add-type.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
dbm = dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
var async = require('async');
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
async.series([
|
||||||
|
db.runSql.bind(db, 'DELETE FROM clients'),
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE clients ADD COLUMN type VARCHAR(16) NOT NULL'),
|
||||||
|
], callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE clients DROP COLUMN type', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
17
migrations/20151016131005-apps-rename-accessRestriction.js
Normal file
17
migrations/20151016131005-apps-rename-accessRestriction.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
var dbm = global.dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps CHANGE accessRestriction accessRestrictionJson VARCHAR(2048)', [], function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps CHANGE accessRestrictionJson accessRestriction VARCHAR(2048)', [], function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
16
migrations/20151113091257-apps-alter-manifestJson.js
Normal file
16
migrations/20151113091257-apps-alter-manifestJson.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
dbm = dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps MODIFY manifestJson TEXT', [], function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps MODIFY manifestJson VARCHAR(2048)', [], function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
19
migrations/20151126111337-apps-alter-jsons.js
Normal file
19
migrations/20151126111337-apps-alter-jsons.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
dbm = dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
var async = require('async');
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
async.series([
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE apps MODIFY accessRestrictionJson TEXT'),
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE apps MODIFY lastBackupConfigJson TEXT'),
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE apps MODIFY oldConfigJson TEXT')
|
||||||
|
], callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
async.series([
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE apps MODIFY accessRestrictionJson VARCHAR(2048)'),
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE apps MODIFY lastBackupConfigJson VARCHAR(2048)'),
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE apps MODIFY oldConfigJson VARCHAR(2048)')
|
||||||
|
], callback);
|
||||||
|
};
|
||||||
15
migrations/20160119113143-users-add-displayName.js
Normal file
15
migrations/20160119113143-users-add-displayName.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
dbm = dbm || require('db-migrate');
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE users ADD COLUMN displayName VARCHAR(512) DEFAULT ""', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE users DROP COLUMN displayName', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
15
migrations/20160205135326-apps-add-memoryLimit.js
Normal file
15
migrations/20160205135326-apps-add-memoryLimit.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
dbm = dbm || require('db-migrate');
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps ADD COLUMN memoryLimit BIGINT DEFAULT 0', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps DROP COLUMN memoryLimit', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
21
migrations/20160208031241-groups-add-table.js
Normal file
21
migrations/20160208031241-groups-add-table.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
var dbm = global.dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
var cmd = "CREATE TABLE groups(" +
|
||||||
|
"id VARCHAR(128) NOT NULL UNIQUE," +
|
||||||
|
"name VARCHAR(128) NOT NULL UNIQUE," +
|
||||||
|
"PRIMARY KEY(id))";
|
||||||
|
|
||||||
|
db.runSql(cmd, function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('DROP TABLE groups', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
22
migrations/20160208100000-groupMembers-add-table.js
Normal file
22
migrations/20160208100000-groupMembers-add-table.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
var dbm = global.dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
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(userId) REFERENCES users(id));";
|
||||||
|
|
||||||
|
db.runSql(cmd, function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('DROP TABLE groupMembers', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
30
migrations/20160208164735-groups-add-admin.js
Normal file
30
migrations/20160208164735-groups-add-admin.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var dbm = global.dbm || require('db-migrate');
|
||||||
|
var async = require('async');
|
||||||
|
|
||||||
|
var ADMIN_GROUP_ID = 'admin'; // see groups.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' ]),
|
||||||
|
function migrateAdminFlag(done) {
|
||||||
|
db.all('SELECT * FROM users WHERE admin=1', function (error, results) {
|
||||||
|
if (error) return done(error);
|
||||||
|
|
||||||
|
console.dir(results);
|
||||||
|
|
||||||
|
async.eachSeries(results, function (r, next) {
|
||||||
|
db.runSql('INSERT INTO groupMembers (groupId, userId) VALUES (?, ?)', [ ADMIN_GROUP_ID, r.id ], next);
|
||||||
|
}, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
db.runSql.bind(db, 'ALTER TABLE users DROP COLUMN admin'),
|
||||||
|
db.runSql.bind(db, 'COMMIT')
|
||||||
|
], callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
callback();
|
||||||
|
};
|
||||||
25
migrations/20160307172229-backups-add-table.js
Normal file
25
migrations/20160307172229-backups-add-table.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
var dbm = global.dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
var cmd = "CREATE TABLE backups(" +
|
||||||
|
"filename VARCHAR(128) NOT NULL," +
|
||||||
|
"creationTime TIMESTAMP," +
|
||||||
|
"version VARCHAR(128) NOT NULL," +
|
||||||
|
"type VARCHAR(16) NOT NULL," +
|
||||||
|
"dependsOn VARCHAR(4096)," +
|
||||||
|
"state VARCHAR(16) NOT NULL," +
|
||||||
|
"PRIMARY KEY (filename))";
|
||||||
|
|
||||||
|
db.runSql(cmd, function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('DROP TABLE backups', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
17
migrations/20160330215005-backups-add-configJson.js
Normal file
17
migrations/20160330215005-backups-add-configJson.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
var dbm = global.dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE backups ADD COLUMN configJson TEXT', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE backups DROP COLUMN configJson', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
17
migrations/20160404052453-backups-drop-configJson.js
Normal file
17
migrations/20160404052453-backups-drop-configJson.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
var dbm = dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE backups DROP COLUMN configJson', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE backups ADD COLUMN configJson TEXT', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
16
migrations/20160404191651-backups-rename-filename-to-id.js
Normal file
16
migrations/20160404191651-backups-rename-filename-to-id.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
var dbm = global.dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE backups CHANGE filename id VARCHAR(128)', [], function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE backups CHANGE id filename VARCHAR(128)', [], function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
15
migrations/20160405083451-users-drop-username-null.js
Normal file
15
migrations/20160405083451-users-drop-username-null.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
dbm = dbm || require('db-migrate');
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE users MODIFY username VARCHAR(254) UNIQUE', [], function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE users MODIFY username VARCHAR(254) NOT NULL UNIQUE', [], function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
17
migrations/20160419070047-apps-add-altDomain.js
Normal file
17
migrations/20160419070047-apps-add-altDomain.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var dbm = dbm || require('db-migrate');
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps ADD COLUMN altDomain VARCHAR(256)', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps DROP COLUMN altDomain', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
24
migrations/20160430062446-eventlog-add-table.js
Normal file
24
migrations/20160430062446-eventlog-add-table.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
var dbm = global.dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
var cmd = "CREATE TABLE eventlog(" +
|
||||||
|
"id VARCHAR(128) NOT NULL," +
|
||||||
|
"source JSON," +
|
||||||
|
"creationTime TIMESTAMP," +
|
||||||
|
"action VARCHAR(128) NOT NULL," +
|
||||||
|
"data JSON," +
|
||||||
|
"PRIMARY KEY (id))";
|
||||||
|
|
||||||
|
db.runSql(cmd, function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('DROP TABLE eventlog', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
15
migrations/20160506111925-users-add-showTutorial.js
Normal file
15
migrations/20160506111925-users-add-showTutorial.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
dbm = dbm || require('db-migrate');
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE users ADD COLUMN showTutorial BOOLEAN DEFAULT 0', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE users DROP COLUMN showTutorial', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
24
migrations/20160527010012-mailboxes-add-table.js
Normal file
24
migrations/20160527010012-mailboxes-add-table.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var dbm = global.dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
var cmd = 'CREATE TABLE mailboxes(' +
|
||||||
|
'name VARCHAR(128) NOT NULL,' +
|
||||||
|
'aliasTarget VARCHAR(128),' +
|
||||||
|
'creationTime TIMESTAMP,' +
|
||||||
|
'PRIMARY KEY (name))';
|
||||||
|
|
||||||
|
db.runSql(cmd, function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('DROP TABLE mailboxes', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
25
migrations/20160528090040-mailboxes-import-existing-users.js
Normal file
25
migrations/20160528090040-mailboxes-import-existing-users.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
var dbm = global.dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
// imports mailbox entries for existing users
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
async.series([
|
||||||
|
db.runSql.bind(db, 'START TRANSACTION;'),
|
||||||
|
function addUserMailboxes(done) {
|
||||||
|
db.all('SELECT username FROM users', function (error, results) {
|
||||||
|
if (error) return done(error);
|
||||||
|
|
||||||
|
async.eachSeries(results, function (r, next) {
|
||||||
|
if (!r.username) return next();
|
||||||
|
|
||||||
|
db.runSql('INSERT INTO mailboxes (name) VALUES (?)', [ r.username ], next);
|
||||||
|
}, done);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
db.runSql.bind(db, 'COMMIT')
|
||||||
|
], callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
callback();
|
||||||
|
};
|
||||||
16
migrations/20160614021819-apps-drop-lastBackupConfigJson.js
Normal file
16
migrations/20160614021819-apps-drop-lastBackupConfigJson.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
dbm = dbm || require('db-migrate');
|
||||||
|
var type = dbm.dataType;
|
||||||
|
|
||||||
|
exports.up = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps DROP COLUMN lastBackupConfigJson', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function(db, callback) {
|
||||||
|
db.runSql('ALTER TABLE apps ADD COLUMN lastBackupConfigJson TEXT', function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -11,26 +11,40 @@
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS users(
|
CREATE TABLE IF NOT EXISTS users(
|
||||||
id VARCHAR(128) NOT NULL UNIQUE,
|
id VARCHAR(128) NOT NULL UNIQUE,
|
||||||
username VARCHAR(254) NOT NULL UNIQUE,
|
username VARCHAR(254) UNIQUE,
|
||||||
email VARCHAR(254) NOT NULL UNIQUE,
|
email VARCHAR(254) NOT NULL UNIQUE,
|
||||||
password VARCHAR(1024) NOT NULL,
|
password VARCHAR(1024) NOT NULL,
|
||||||
salt VARCHAR(512) NOT NULL,
|
salt VARCHAR(512) NOT NULL,
|
||||||
createdAt VARCHAR(512) NOT NULL,
|
createdAt VARCHAR(512) NOT NULL,
|
||||||
modifiedAt VARCHAR(512) NOT NULL,
|
modifiedAt VARCHAR(512) NOT NULL,
|
||||||
admin INTEGER NOT NULL,
|
admin INTEGER NOT NULL,
|
||||||
|
displayName VARCHAR(512) DEFAULT '',
|
||||||
|
showTutorial BOOLEAN DEFAULT 0,
|
||||||
PRIMARY KEY(id));
|
PRIMARY KEY(id));
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS groups(
|
||||||
|
id VARCHAR(128) NOT NULL UNIQUE,
|
||||||
|
username VARCHAR(254) NOT NULL UNIQUE,
|
||||||
|
PRIMARY KEY(id));
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS groupMembers(
|
||||||
|
groupId VARCHAR(128) NOT NULL,
|
||||||
|
userId VARCHAR(128) NOT NULL,
|
||||||
|
FOREIGN KEY(groupId) REFERENCES groups(id),
|
||||||
|
FOREIGN KEY(userId) REFERENCES users(id));
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS tokens(
|
CREATE TABLE IF NOT EXISTS tokens(
|
||||||
accessToken VARCHAR(128) NOT NULL UNIQUE,
|
accessToken VARCHAR(128) NOT NULL UNIQUE,
|
||||||
identifier VARCHAR(128) NOT NULL,
|
identifier VARCHAR(128) NOT NULL,
|
||||||
clientId VARCHAR(128),
|
clientId VARCHAR(128),
|
||||||
scope VARCHAR(512) NOT NULL,
|
scope VARCHAR(512) NOT NULL,
|
||||||
expires BIGINT NOT NULL,
|
expires BIGINT NOT NULL, // FIXME: make this a timestamp
|
||||||
PRIMARY KEY(accessToken));
|
PRIMARY KEY(accessToken));
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS clients(
|
CREATE TABLE IF NOT EXISTS clients(
|
||||||
id VARCHAR(128) NOT NULL UNIQUE,
|
id VARCHAR(128) NOT NULL UNIQUE, // prefixed with cid- to identify token easily in auth routes
|
||||||
appId VARCHAR(128) NOT NULL,
|
appId VARCHAR(128) NOT NULL,
|
||||||
|
type VARCHAR(16) NOT NULL,
|
||||||
clientSecret VARCHAR(512) NOT NULL,
|
clientSecret VARCHAR(512) NOT NULL,
|
||||||
redirectURI VARCHAR(512) NOT NULL,
|
redirectURI VARCHAR(512) NOT NULL,
|
||||||
scope VARCHAR(512) NOT NULL,
|
scope VARCHAR(512) NOT NULL,
|
||||||
@@ -44,15 +58,20 @@ CREATE TABLE IF NOT EXISTS apps(
|
|||||||
runState VARCHAR(512),
|
runState VARCHAR(512),
|
||||||
health VARCHAR(128),
|
health VARCHAR(128),
|
||||||
containerId VARCHAR(128),
|
containerId VARCHAR(128),
|
||||||
manifestJson VARCHAR(2048),
|
manifestJson TEXT,
|
||||||
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
|
httpPort INTEGER, // this is the nginx proxy port and not manifest.httpPort
|
||||||
location VARCHAR(128) NOT NULL UNIQUE,
|
location VARCHAR(128) NOT NULL UNIQUE,
|
||||||
dnsRecordId VARCHAR(512),
|
dnsRecordId VARCHAR(512),
|
||||||
accessRestriction VARCHAR(512),
|
accessRestrictionJson TEXT, // { users: [ ], groups: [ ] }
|
||||||
|
oauthProxy BOOLEAN DEFAULT 0,
|
||||||
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
createdAt TIMESTAMP(2) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
memoryLimit BIGINT DEFAULT 0,
|
||||||
|
altDomain VARCHAR(256),
|
||||||
|
|
||||||
|
lastBackupId VARCHAR(128), // tracks last valid backup, can be removed
|
||||||
|
|
||||||
|
oldConfigJson TEXT, // used to pass old config for apptask, can be removed when we use a queue
|
||||||
|
|
||||||
lastBackupId VARCHAR(128),
|
|
||||||
lastBackupConfigJson VARCHAR(2048), // used for appstore and non-appstore installs. it's here so it's easy to do REST validation
|
|
||||||
PRIMARY KEY(id));
|
PRIMARY KEY(id));
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS appPortBindings(
|
CREATE TABLE IF NOT EXISTS appPortBindings(
|
||||||
@@ -66,7 +85,7 @@ CREATE TABLE IF NOT EXISTS authcodes(
|
|||||||
authCode VARCHAR(128) NOT NULL UNIQUE,
|
authCode VARCHAR(128) NOT NULL UNIQUE,
|
||||||
userId VARCHAR(128) NOT NULL,
|
userId VARCHAR(128) NOT NULL,
|
||||||
clientId VARCHAR(128) NOT NULL,
|
clientId VARCHAR(128) NOT NULL,
|
||||||
expiresAt BIGINT NOT NULL,
|
expiresAt BIGINT NOT NULL, // ## FIXME: make this a timestamp
|
||||||
PRIMARY KEY(authCode));
|
PRIMARY KEY(authCode));
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS settings(
|
CREATE TABLE IF NOT EXISTS settings(
|
||||||
@@ -80,3 +99,32 @@ CREATE TABLE IF NOT EXISTS appAddonConfigs(
|
|||||||
value VARCHAR(512) NOT NULL,
|
value VARCHAR(512) NOT NULL,
|
||||||
FOREIGN KEY(appId) REFERENCES apps(id));
|
FOREIGN KEY(appId) REFERENCES apps(id));
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS backups(
|
||||||
|
filename VARCHAR(128) NOT NULL,
|
||||||
|
creationTime TIMESTAMP,
|
||||||
|
version VARCHAR(128) NOT NULL, /* app version or box version */
|
||||||
|
type VARCHAR(16) NOT NULL, /* 'box' or 'app' */
|
||||||
|
dependsOn VARCHAR(4096), /* comma separate list of objects this backup depends on */
|
||||||
|
state VARCHAR(16) NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (filename));
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS eventlog(
|
||||||
|
id VARCHAR(128) NOT NULL,
|
||||||
|
action VARCHAR(128) NOT NULL,
|
||||||
|
source JSON, /* { userId, username, ip }. userId can be null for cron,sysadmin */
|
||||||
|
data JSON, /* free flowing json based on action */
|
||||||
|
creationTime TIMESTAMP, /* FIXME: precision must be TIMESTAMP(2) */
|
||||||
|
|
||||||
|
PRIMARY KEY (id));
|
||||||
|
|
||||||
|
/* Future fields:
|
||||||
|
* accessRestriction - to determine who can access it. So this has foreign keys
|
||||||
|
* quota - per mailbox quota
|
||||||
|
*/
|
||||||
|
CREATE TABLE IF NOT EXISTS mailboxes(
|
||||||
|
name VARCHAR(128) NOT NULL,
|
||||||
|
aliasTarget VARCHAR(128), /* the target name type is an alias */
|
||||||
|
creationTime TIMESTAMP,
|
||||||
|
|
||||||
|
PRIMARY KEY (id));
|
||||||
|
|||||||
6530
npm-shrinkwrap.json
generated
6530
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
185
oauthproxy.js
185
oauthproxy.js
@@ -1,185 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
require('supererror')({ splatchError: true });
|
|
||||||
|
|
||||||
var express = require('express'),
|
|
||||||
url = require('url'),
|
|
||||||
uuid = require('node-uuid'),
|
|
||||||
async = require('async'),
|
|
||||||
superagent = require('superagent'),
|
|
||||||
assert = require('assert'),
|
|
||||||
debug = require('debug')('box:proxy'),
|
|
||||||
proxy = require('proxy-middleware'),
|
|
||||||
session = require('cookie-session'),
|
|
||||||
database = require('./src/database.js'),
|
|
||||||
appdb = require('./src/appdb.js'),
|
|
||||||
clientdb = require('./src/clientdb.js'),
|
|
||||||
config = require('./src/config.js'),
|
|
||||||
http = require('http');
|
|
||||||
|
|
||||||
// Allow self signed certs!
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
||||||
|
|
||||||
var gSessions = {};
|
|
||||||
var gProxyMiddlewareCache = {};
|
|
||||||
var gApp = express();
|
|
||||||
var gHttpServer = http.createServer(gApp);
|
|
||||||
|
|
||||||
var CALLBACK_URI = '/callback';
|
|
||||||
var PORT = 4000;
|
|
||||||
|
|
||||||
function startServer(callback) {
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
gHttpServer.on('error', console.error);
|
|
||||||
|
|
||||||
gApp.use(session({
|
|
||||||
keys: ['blue', 'cheese', 'is', 'something']
|
|
||||||
}));
|
|
||||||
|
|
||||||
// ensure we have a in memory store for the session to cache client information
|
|
||||||
gApp.use(function (req, res, next) {
|
|
||||||
assert.strictEqual(typeof req.session, 'object');
|
|
||||||
|
|
||||||
if (!req.session.id || !gSessions[req.session.id]) {
|
|
||||||
req.session.id = uuid.v4();
|
|
||||||
gSessions[req.session.id] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// attach the session data to the requeset
|
|
||||||
req.sessionData = gSessions[req.session.id];
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
gApp.use(function verifySession(req, res, next) {
|
|
||||||
assert.strictEqual(typeof req.sessionData, 'object');
|
|
||||||
|
|
||||||
if (!req.sessionData.accessToken) {
|
|
||||||
req.authenticated = false;
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
superagent.get(config.adminOrigin() + '/api/v1/profile').query({ access_token: req.sessionData.accessToken}).end(function (error, result) {
|
|
||||||
if (error) {
|
|
||||||
console.error(error);
|
|
||||||
req.authenticated = false;
|
|
||||||
} else if (result.statusCode !== 200) {
|
|
||||||
req.sessionData.accessToken = null;
|
|
||||||
req.authenticated = false;
|
|
||||||
} else {
|
|
||||||
req.authenticated = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
gApp.use(function (req, res, next) {
|
|
||||||
// proceed if we are authenticated
|
|
||||||
if (req.authenticated) return next();
|
|
||||||
|
|
||||||
if (req.path === CALLBACK_URI && req.sessionData.returnTo) {
|
|
||||||
// exchange auth code for an access token
|
|
||||||
var query = {
|
|
||||||
response_type: 'token',
|
|
||||||
client_id: req.sessionData.clientId
|
|
||||||
};
|
|
||||||
|
|
||||||
var data = {
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
code: req.query.code,
|
|
||||||
redirect_uri: req.sessionData.returnTo,
|
|
||||||
client_id: req.sessionData.clientId,
|
|
||||||
client_secret: req.sessionData.clientSecret
|
|
||||||
};
|
|
||||||
|
|
||||||
superagent.post(config.adminOrigin() + '/api/v1/oauth/token').query(query).send(data).end(function (error, result) {
|
|
||||||
if (error) {
|
|
||||||
console.error(error);
|
|
||||||
return res.send(500, 'Unable to contact the oauth server.');
|
|
||||||
}
|
|
||||||
if (result.statusCode !== 200) {
|
|
||||||
console.error('Failed to exchange auth code for a token.', result.statusCode, result.body);
|
|
||||||
return res.send(500, 'Failed to exchange auth code for a token.');
|
|
||||||
}
|
|
||||||
|
|
||||||
req.sessionData.accessToken = result.body.access_token;
|
|
||||||
|
|
||||||
debug('user verified.');
|
|
||||||
|
|
||||||
// now redirect to the actual initially requested URL
|
|
||||||
res.redirect(req.sessionData.returnTo);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
var port = parseInt(req.headers['x-cloudron-proxy-port'], 10);
|
|
||||||
|
|
||||||
if (!Number.isFinite(port)) {
|
|
||||||
console.error('Failed to parse nginx proxy header to get app port.');
|
|
||||||
return res.send(500, 'Routing error. No forwarded port.');
|
|
||||||
}
|
|
||||||
|
|
||||||
debug('begin verifying user for app on port %s.', port);
|
|
||||||
|
|
||||||
appdb.getByHttpPort(port, function (error, result) {
|
|
||||||
if (error) {
|
|
||||||
console.error('Unknown app.', error);
|
|
||||||
return res.send(500, 'Unknown app.');
|
|
||||||
}
|
|
||||||
|
|
||||||
clientdb.getByAppId('proxy-' + result.id, function (error, result) {
|
|
||||||
if (error) {
|
|
||||||
console.error('Unkonwn OAuth client.', error);
|
|
||||||
return res.send(500, 'Unknown OAuth client.');
|
|
||||||
}
|
|
||||||
|
|
||||||
req.sessionData.port = port;
|
|
||||||
req.sessionData.returnTo = result.redirectURI + req.path;
|
|
||||||
req.sessionData.clientId = result.id;
|
|
||||||
req.sessionData.clientSecret = result.clientSecret;
|
|
||||||
|
|
||||||
var callbackUrl = result.redirectURI + CALLBACK_URI;
|
|
||||||
var scope = 'profile,roleUser';
|
|
||||||
var oauthLogin = config.adminOrigin() + '/api/v1/oauth/dialog/authorize?response_type=code&client_id=' + result.id + '&redirect_uri=' + callbackUrl + '&scope=' + scope;
|
|
||||||
|
|
||||||
debug('begin OAuth flow for client %s.', result.name);
|
|
||||||
|
|
||||||
// begin the OAuth flow
|
|
||||||
res.redirect(oauthLogin);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
gApp.use(function (req, res, next) {
|
|
||||||
var port = req.sessionData.port;
|
|
||||||
|
|
||||||
debug('proxy request for port %s with path %s.', port, req.path);
|
|
||||||
|
|
||||||
var proxyMiddleware = gProxyMiddlewareCache[port];
|
|
||||||
if (!proxyMiddleware) {
|
|
||||||
console.log('Adding proxy middleware for port %d', port);
|
|
||||||
|
|
||||||
proxyMiddleware = proxy(url.parse('http://127.0.0.1:' + port));
|
|
||||||
gProxyMiddlewareCache[port] = proxyMiddleware;
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyMiddleware(req, res, next);
|
|
||||||
});
|
|
||||||
|
|
||||||
gHttpServer.listen(PORT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
database.initialize,
|
|
||||||
startServer
|
|
||||||
], function (error) {
|
|
||||||
if (error) {
|
|
||||||
console.error('Failed to start proxy server.', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Proxy server listening...');
|
|
||||||
});
|
|
||||||
44
package.json
44
package.json
@@ -10,17 +10,15 @@
|
|||||||
"type": "git"
|
"type": "git"
|
||||||
},
|
},
|
||||||
"engines": [
|
"engines": [
|
||||||
"node >= 0.12.0"
|
"node >=4.0.0 <=4.1.1"
|
||||||
],
|
],
|
||||||
"bin": {
|
|
||||||
"cloudron": "./app.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "^1.2.1",
|
"async": "^1.2.1",
|
||||||
|
"aws-sdk": "^2.1.46",
|
||||||
"body-parser": "^1.13.1",
|
"body-parser": "^1.13.1",
|
||||||
"cloudron-manifestformat": "^1.6.0",
|
"cloudron-manifestformat": "^2.4.0",
|
||||||
"connect-ensure-login": "^0.1.1",
|
"connect-ensure-login": "^0.1.1",
|
||||||
"connect-lastmile": "0.0.13",
|
"connect-lastmile": "^0.1.0",
|
||||||
"connect-timeout": "^1.5.0",
|
"connect-timeout": "^1.5.0",
|
||||||
"cookie-parser": "^1.3.5",
|
"cookie-parser": "^1.3.5",
|
||||||
"cookie-session": "^1.1.0",
|
"cookie-session": "^1.1.0",
|
||||||
@@ -28,65 +26,69 @@
|
|||||||
"csurf": "^1.6.6",
|
"csurf": "^1.6.6",
|
||||||
"db-migrate": "^0.9.2",
|
"db-migrate": "^0.9.2",
|
||||||
"debug": "^2.2.0",
|
"debug": "^2.2.0",
|
||||||
"dockerode": "^2.2.2",
|
"dockerode": "^2.2.10",
|
||||||
"ejs": "^2.2.4",
|
"ejs": "^2.2.4",
|
||||||
"ejs-cli": "^1.0.1",
|
"ejs-cli": "^1.2.0",
|
||||||
"express": "^4.12.4",
|
"express": "^4.12.4",
|
||||||
"express-session": "^1.11.3",
|
"express-session": "^1.11.3",
|
||||||
"hat": "0.0.3",
|
"hat": "0.0.3",
|
||||||
|
"ini": "^1.3.4",
|
||||||
"json": "^9.0.3",
|
"json": "^9.0.3",
|
||||||
"ldapjs": "^0.7.1",
|
"ldapjs": "^0.7.1",
|
||||||
"memorystream": "^0.3.0",
|
|
||||||
"mime": "^1.3.4",
|
"mime": "^1.3.4",
|
||||||
"morgan": "^1.6.0",
|
"morgan": "^1.7.0",
|
||||||
"multiparty": "^4.1.2",
|
"multiparty": "^4.1.2",
|
||||||
"mysql": "^2.7.0",
|
"mysql": "^2.7.0",
|
||||||
"native-dns": "^0.7.0",
|
"native-dns": "^0.7.0",
|
||||||
|
"node-df": "^0.1.1",
|
||||||
"node-uuid": "^1.4.3",
|
"node-uuid": "^1.4.3",
|
||||||
"nodemailer": "^1.3.0",
|
"nodemailer": "^1.3.0",
|
||||||
"nodemailer-smtp-transport": "^1.0.3",
|
"nodemailer-smtp-transport": "^1.0.3",
|
||||||
"oauth2orize": "^1.0.1",
|
"oauth2orize": "^1.0.1",
|
||||||
"once": "^1.3.2",
|
"once": "^1.3.2",
|
||||||
|
"parse-links": "^0.1.0",
|
||||||
"passport": "^0.2.2",
|
"passport": "^0.2.2",
|
||||||
"passport-http": "^0.2.2",
|
"passport-http": "^0.2.2",
|
||||||
"passport-http-bearer": "^1.0.1",
|
"passport-http-bearer": "^1.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"passport-oauth2-client-password": "^0.1.2",
|
"passport-oauth2-client-password": "^0.1.2",
|
||||||
"password-generator": "^1.0.0",
|
"password-generator": "^2.0.2",
|
||||||
"proxy-middleware": "^0.13.0",
|
"proxy-middleware": "^0.13.0",
|
||||||
"safetydance": "0.0.19",
|
"safetydance": "^0.1.1",
|
||||||
"semver": "^4.3.6",
|
"semver": "^4.3.6",
|
||||||
"serve-favicon": "^2.2.0",
|
|
||||||
"split": "^1.0.0",
|
"split": "^1.0.0",
|
||||||
"superagent": "~0.21.0",
|
"superagent": "^1.8.3",
|
||||||
"supererror": "^0.7.0",
|
"supererror": "^0.7.1",
|
||||||
"supervisord-eventlistener": "^0.1.0",
|
|
||||||
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
|
"tail-stream": "https://registry.npmjs.org/tail-stream/-/tail-stream-0.2.1.tgz",
|
||||||
|
"tldjs": "^1.6.2",
|
||||||
"underscore": "^1.7.0",
|
"underscore": "^1.7.0",
|
||||||
|
"ursa": "^0.9.3",
|
||||||
"valid-url": "^1.0.9",
|
"valid-url": "^1.0.9",
|
||||||
"validator": "^3.30.0"
|
"validator": "^4.9.0",
|
||||||
|
"x509": "^0.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"apidoc": "*",
|
"apidoc": "*",
|
||||||
"aws-sdk": "^2.1.10",
|
|
||||||
"bootstrap-sass": "^3.3.3",
|
"bootstrap-sass": "^3.3.3",
|
||||||
|
"deep-extend": "^0.4.1",
|
||||||
"del": "^1.1.1",
|
"del": "^1.1.1",
|
||||||
"expect.js": "*",
|
"expect.js": "*",
|
||||||
"gulp": "^3.8.11",
|
"gulp": "^3.8.11",
|
||||||
"gulp-autoprefixer": "^2.3.0",
|
"gulp-autoprefixer": "^2.3.0",
|
||||||
"gulp-concat": "^2.4.3",
|
"gulp-concat": "^2.4.3",
|
||||||
|
"gulp-cssnano": "^2.1.0",
|
||||||
"gulp-ejs": "^1.0.0",
|
"gulp-ejs": "^1.0.0",
|
||||||
"gulp-minify-css": "^1.1.3",
|
|
||||||
"gulp-sass": "^2.0.1",
|
"gulp-sass": "^2.0.1",
|
||||||
"gulp-serve": "^1.0.0",
|
"gulp-serve": "^1.0.0",
|
||||||
"gulp-sourcemaps": "^1.5.2",
|
"gulp-sourcemaps": "^1.5.2",
|
||||||
"gulp-uglify": "^1.1.0",
|
"gulp-uglify": "^1.1.0",
|
||||||
"hock": "~1.2.0",
|
"hock": "~1.2.0",
|
||||||
"istanbul": "*",
|
"istanbul": "*",
|
||||||
|
"js2xmlparser": "^1.0.0",
|
||||||
"mocha": "*",
|
"mocha": "*",
|
||||||
"nock": "^2.6.0",
|
"nock": "^3.4.0",
|
||||||
"node-sass": "^3.0.0-alpha.0",
|
"node-sass": "^3.0.0-alpha.0",
|
||||||
"redis": "^0.12.1",
|
"request": "^2.65.0",
|
||||||
"sinon": "^1.12.2",
|
"sinon": "^1.12.2",
|
||||||
"yargs": "^3.15.0"
|
"yargs": "^3.15.0"
|
||||||
},
|
},
|
||||||
|
|||||||
123
scripts/createReleaseTarball
Executable file
123
scripts/createReleaseTarball
Executable file
@@ -0,0 +1,123 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
assertNotEmpty() {
|
||||||
|
: "${!1:? "$1 is not set."}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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:,output:,publish,no-upload" -n "$0" -- "$@")
|
||||||
|
eval set -- "${args}"
|
||||||
|
|
||||||
|
readonly RELEASE_TOOL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../release" && pwd)"
|
||||||
|
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
|
||||||
|
delete_bundle="yes"
|
||||||
|
commitish="HEAD"
|
||||||
|
publish="no"
|
||||||
|
upload="yes"
|
||||||
|
bundle_file=""
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
case "$1" in
|
||||||
|
--revision) commitish="$2"; shift 2;;
|
||||||
|
--output) bundle_file="$2"; delete_bundle="no"; shift 2;;
|
||||||
|
--no-upload) upload="no"; shift;;
|
||||||
|
--publish) publish="yes"; shift;;
|
||||||
|
--) break;;
|
||||||
|
*) echo "Unknown option $1"; exit 1;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "${upload}" == "no" && "${publish}" == "yes" ]]; then
|
||||||
|
echo "Cannot publish without uploading"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
readonly TMPDIR=${TMPDIR:-/tmp} # why is this not set on mint?
|
||||||
|
|
||||||
|
assertNotEmpty AWS_DEV_ACCESS_KEY
|
||||||
|
assertNotEmpty AWS_DEV_SECRET_KEY
|
||||||
|
|
||||||
|
if ! $(cd "${SOURCE_DIR}" && git diff --exit-code >/dev/null); then
|
||||||
|
echo "You have local changes, stash or commit them to proceed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$(node --version)" != "v4.1.1" ]]; then
|
||||||
|
echo "This script requires node 4.1.1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
version=$(cd "${SOURCE_DIR}" && git rev-parse "${commitish}")
|
||||||
|
bundle_dir=$(mktemp -d -t box 2>/dev/null || mktemp -d box-XXXXXXXXXX --tmpdir=$TMPDIR)
|
||||||
|
[[ -z "$bundle_file" ]] && bundle_file="${TMPDIR}/box-${version}.tar.gz"
|
||||||
|
|
||||||
|
chmod "o+rx,g+rx" "${bundle_dir}" # otherwise extracted tarball director won't be readable by others/group
|
||||||
|
echo "Checking out code [${version}] into ${bundle_dir}"
|
||||||
|
(cd "${SOURCE_DIR}" && git archive --format=tar ${version} | (cd "${bundle_dir}" && tar xf -))
|
||||||
|
|
||||||
|
if diff "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.all" "${bundle_dir}/npm-shrinkwrap.json" >/dev/null 2>&1; then
|
||||||
|
echo "Reusing dev modules from cache"
|
||||||
|
cp -r "${TMPDIR}/boxtarball.cache/node_modules-all/." "${bundle_dir}/node_modules"
|
||||||
|
else
|
||||||
|
echo "Installing modules with dev dependencies"
|
||||||
|
(cd "${bundle_dir}" && npm install)
|
||||||
|
|
||||||
|
echo "Caching dev dependencies"
|
||||||
|
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-all"
|
||||||
|
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-all/"
|
||||||
|
cp "${bundle_dir}/npm-shrinkwrap.json" "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.all"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building webadmin assets"
|
||||||
|
(cd "${bundle_dir}" && gulp)
|
||||||
|
|
||||||
|
echo "Remove intermediate files required at build-time only"
|
||||||
|
rm -rf "${bundle_dir}/node_modules/"
|
||||||
|
rm -rf "${bundle_dir}/webadmin/src"
|
||||||
|
rm -rf "${bundle_dir}/gulpfile.js"
|
||||||
|
|
||||||
|
if diff "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.prod" "${bundle_dir}/npm-shrinkwrap.json" >/dev/null 2>&1; then
|
||||||
|
echo "Reusing prod modules from cache"
|
||||||
|
cp -r "${TMPDIR}/boxtarball.cache/node_modules-prod/." "${bundle_dir}/node_modules"
|
||||||
|
else
|
||||||
|
echo "Installing modules for production"
|
||||||
|
(cd "${bundle_dir}" && npm install --production --no-optional)
|
||||||
|
|
||||||
|
echo "Caching prod dependencies"
|
||||||
|
mkdir -p "${TMPDIR}/boxtarball.cache/node_modules-prod"
|
||||||
|
rsync -a --delete "${bundle_dir}/node_modules/" "${TMPDIR}/boxtarball.cache/node_modules-prod/"
|
||||||
|
cp "${bundle_dir}/npm-shrinkwrap.json" "${TMPDIR}/boxtarball.cache/npm-shrinkwrap.json.prod"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Create final tarball"
|
||||||
|
(cd "${bundle_dir}" && tar czf "${bundle_file}" .)
|
||||||
|
echo "Cleaning up ${bundle_dir}"
|
||||||
|
rm -rf "${bundle_dir}"
|
||||||
|
|
||||||
|
if [[ "${upload}" == "yes" ]]; then
|
||||||
|
echo "Uploading bundle to S3"
|
||||||
|
# That special header is needed to allow access with singed urls created with different aws credentials than the ones the file got uploaded
|
||||||
|
s3cmd --multipart-chunk-size-mb=5 --ssl --acl-public --access_key="${AWS_DEV_ACCESS_KEY}" --secret_key="${AWS_DEV_SECRET_KEY}" --no-mime-magic put "${bundle_file}" "s3://dev-cloudron-releases/box-${version}.tar.gz"
|
||||||
|
|
||||||
|
versions_file_url="https://dev-cloudron-releases.s3.amazonaws.com/box-${version}.tar.gz"
|
||||||
|
echo "The URL for the versions file is: ${versions_file_url}"
|
||||||
|
|
||||||
|
if [[ "${publish}" == "yes" ]]; then
|
||||||
|
echo "Publishing to dev"
|
||||||
|
${RELEASE_TOOL_DIR}/release create --env dev --code "${versions_file_url}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${delete_bundle}" == "no" ]]; then
|
||||||
|
echo "Tarball preserved at ${bundle_file}"
|
||||||
|
else
|
||||||
|
rm "${bundle_file}"
|
||||||
|
fi
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ and replace it with a new one for an update.
|
|||||||
|
|
||||||
Because we do not package things as Docker yet, we should be careful
|
Because we do not package things as Docker yet, we should be careful
|
||||||
about the code here. We have to expect remains of an older setup code.
|
about the code here. We have to expect remains of an older setup code.
|
||||||
For example, older supervisor or nginx configs might be around.
|
For example, older systemd or nginx configs might be around.
|
||||||
|
|
||||||
The config directory is _part_ of the container and is not a VOLUME.
|
The config directory is _part_ of the container and is not a VOLUME.
|
||||||
Which is to say that the files will be nuked from one update to the next.
|
Which is to say that the files will be nuked from one update to the next.
|
||||||
@@ -40,7 +40,7 @@ version (see below) or the mysql/postgresql data etc.
|
|||||||
|
|
||||||
* It then setups up the cloud infra (setup_infra.sh) and creates cloudron.conf.
|
* It then setups up the cloud infra (setup_infra.sh) and creates cloudron.conf.
|
||||||
|
|
||||||
* supervisor is then started
|
* box services are then started
|
||||||
|
|
||||||
setup_infra.sh
|
setup_infra.sh
|
||||||
This setups containers like graphite, mail and the addons containers.
|
This setups containers like graphite, mail and the addons containers.
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# If you change the infra version, be sure to put a warning
|
|
||||||
# in the change log
|
|
||||||
|
|
||||||
INFRA_VERSION=8
|
|
||||||
|
|
||||||
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
|
|
||||||
# These constants are used in the installer script as well
|
|
||||||
BASE_IMAGE=cloudron/base:0.3.1
|
|
||||||
MYSQL_IMAGE=cloudron/mysql:0.3.2
|
|
||||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.3.1
|
|
||||||
MONGODB_IMAGE=cloudron/mongodb:0.3.1
|
|
||||||
REDIS_IMAGE=cloudron/redis:0.3.1 # if you change this, fix src/addons.js as well
|
|
||||||
MAIL_IMAGE=cloudron/mail:0.3.1
|
|
||||||
GRAPHITE_IMAGE=cloudron/graphite:0.3.3
|
|
||||||
|
|
||||||
@@ -11,11 +11,17 @@ arg_is_custom_domain="false"
|
|||||||
arg_restore_key=""
|
arg_restore_key=""
|
||||||
arg_restore_url=""
|
arg_restore_url=""
|
||||||
arg_retire="false"
|
arg_retire="false"
|
||||||
|
arg_tls_config=""
|
||||||
arg_tls_cert=""
|
arg_tls_cert=""
|
||||||
arg_tls_key=""
|
arg_tls_key=""
|
||||||
arg_token=""
|
arg_token=""
|
||||||
arg_version=""
|
arg_version=""
|
||||||
arg_web_server_origin=""
|
arg_web_server_origin=""
|
||||||
|
arg_backup_config=""
|
||||||
|
arg_dns_config=""
|
||||||
|
arg_update_config=""
|
||||||
|
arg_provider=""
|
||||||
|
arg_app_bundle=""
|
||||||
|
|
||||||
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
|
args=$(getopt -o "" -l "data:,retire" -n "$0" -- "$@")
|
||||||
eval set -- "${args}"
|
eval set -- "${args}"
|
||||||
@@ -28,19 +34,36 @@ while true; do
|
|||||||
;;
|
;;
|
||||||
--data)
|
--data)
|
||||||
# only read mandatory non-empty parameters here
|
# only read mandatory non-empty parameters here
|
||||||
read -r arg_api_server_origin arg_web_server_origin arg_fqdn arg_token arg_is_custom_domain arg_box_versions_url arg_version <<EOF
|
read -r arg_api_server_origin arg_web_server_origin arg_fqdn arg_is_custom_domain arg_box_versions_url arg_version <<EOF
|
||||||
$(echo "$2" | $json apiServerOrigin webServerOrigin fqdn token isCustomDomain boxVersionsUrl version | tr '\n' ' ')
|
$(echo "$2" | $json apiServerOrigin webServerOrigin fqdn isCustomDomain boxVersionsUrl version | tr '\n' ' ')
|
||||||
EOF
|
EOF
|
||||||
# read possibly empty parameters here
|
# read possibly empty parameters here
|
||||||
|
arg_app_bundle=$(echo "$2" | $json appBundle)
|
||||||
|
[[ "${arg_app_bundle}" == "" ]] && arg_app_bundle="[]"
|
||||||
|
|
||||||
arg_tls_cert=$(echo "$2" | $json tlsCert)
|
arg_tls_cert=$(echo "$2" | $json tlsCert)
|
||||||
arg_tls_key=$(echo "$2" | $json tlsKey)
|
arg_tls_key=$(echo "$2" | $json tlsKey)
|
||||||
|
arg_token=$(echo "$2" | $json token)
|
||||||
|
arg_provider=$(echo "$2" | $json provider)
|
||||||
|
|
||||||
arg_restore_url=$(echo "$2" | $json restoreUrl)
|
arg_tls_config=$(echo "$2" | $json tlsConfig)
|
||||||
|
[[ "${arg_tls_config}" == "null" ]] && arg_tls_config=""
|
||||||
|
|
||||||
|
arg_restore_url=$(echo "$2" | $json restore.url)
|
||||||
[[ "${arg_restore_url}" == "null" ]] && arg_restore_url=""
|
[[ "${arg_restore_url}" == "null" ]] && arg_restore_url=""
|
||||||
|
|
||||||
arg_restore_key=$(echo "$2" | $json restoreKey)
|
arg_restore_key=$(echo "$2" | $json restore.key)
|
||||||
[[ "${arg_restore_key}" == "null" ]] && arg_restore_key=""
|
[[ "${arg_restore_key}" == "null" ]] && arg_restore_key=""
|
||||||
|
|
||||||
|
arg_backup_config=$(echo "$2" | $json backupConfig)
|
||||||
|
[[ "${arg_backup_config}" == "null" ]] && arg_backup_config=""
|
||||||
|
|
||||||
|
arg_dns_config=$(echo "$2" | $json dnsConfig)
|
||||||
|
[[ "${arg_dns_config}" == "null" ]] && arg_dns_config=""
|
||||||
|
|
||||||
|
arg_update_config=$(echo "$2" | $json updateConfig)
|
||||||
|
[[ "${arg_update_config}" == "null" ]] && arg_update_config=""
|
||||||
|
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
--) break;;
|
--) break;;
|
||||||
@@ -58,5 +81,7 @@ echo "restore url: ${arg_restore_url}"
|
|||||||
echo "tls cert: ${arg_tls_cert}"
|
echo "tls cert: ${arg_tls_cert}"
|
||||||
echo "tls key: ${arg_tls_key}"
|
echo "tls key: ${arg_tls_key}"
|
||||||
echo "token: ${arg_token}"
|
echo "token: ${arg_token}"
|
||||||
|
echo "tlsConfig: ${arg_tls_config}"
|
||||||
echo "version: ${arg_version}"
|
echo "version: ${arg_version}"
|
||||||
echo "web server: ${arg_web_server_origin}"
|
echo "web server: ${arg_web_server_origin}"
|
||||||
|
echo "provider: ${arg_provider}"
|
||||||
|
|||||||
@@ -13,22 +13,24 @@ readonly DATA_DIR="/home/yellowtent/data"
|
|||||||
rm -rf "${CONFIG_DIR}"
|
rm -rf "${CONFIG_DIR}"
|
||||||
sudo -u yellowtent mkdir "${CONFIG_DIR}"
|
sudo -u yellowtent mkdir "${CONFIG_DIR}"
|
||||||
|
|
||||||
########## logrotate (default ubuntu runs this daily)
|
########## systemd
|
||||||
rm -rf /etc/logrotate.d/*
|
rm -f /etc/systemd/system/janitor.*
|
||||||
cp -r "${container_files}/logrotate/." /etc/logrotate.d/
|
cp -r "${container_files}/systemd/." /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
########## supervisor
|
systemctl enable cloudron.target
|
||||||
rm -rf /etc/supervisor/*
|
|
||||||
cp -r "${container_files}/supervisor/." /etc/supervisor/
|
|
||||||
|
|
||||||
########## sudoers
|
########## sudoers
|
||||||
rm /etc/sudoers.d/*
|
rm -f /etc/sudoers.d/yellowtent
|
||||||
cp "${container_files}/sudoers" /etc/sudoers.d/yellowtent
|
cp "${container_files}/sudoers" /etc/sudoers.d/yellowtent
|
||||||
|
|
||||||
########## collectd
|
########## collectd
|
||||||
rm -rf /etc/collectd
|
rm -rf /etc/collectd
|
||||||
ln -sfF "${DATA_DIR}/collectd" /etc/collectd
|
ln -sfF "${DATA_DIR}/collectd" /etc/collectd
|
||||||
|
|
||||||
|
########## apparmor docker profile
|
||||||
|
cp "${container_files}/docker-cloudron-app.apparmor" /etc/apparmor.d/docker-cloudron-app
|
||||||
|
systemctl restart apparmor
|
||||||
|
|
||||||
########## nginx
|
########## nginx
|
||||||
# link nginx config to system config
|
# link nginx config to system config
|
||||||
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
|
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
|
||||||
|
|||||||
32
setup/container/docker-cloudron-app.apparmor
Normal file
32
setup/container/docker-cloudron-app.apparmor
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#include <tunables/global>
|
||||||
|
|
||||||
|
|
||||||
|
profile docker-cloudron-app flags=(attach_disconnected,mediate_deleted) {
|
||||||
|
|
||||||
|
#include <abstractions/base>
|
||||||
|
|
||||||
|
ptrace peer=@{profile_name},
|
||||||
|
|
||||||
|
network,
|
||||||
|
capability,
|
||||||
|
file,
|
||||||
|
umount,
|
||||||
|
|
||||||
|
deny @{PROC}/sys/fs/** wklx,
|
||||||
|
deny @{PROC}/sysrq-trigger rwklx,
|
||||||
|
deny @{PROC}/mem rwklx,
|
||||||
|
deny @{PROC}/kmem rwklx,
|
||||||
|
deny @{PROC}/sys/kernel/[^s][^h][^m]* wklx,
|
||||||
|
deny @{PROC}/sys/kernel/*/** wklx,
|
||||||
|
|
||||||
|
deny mount,
|
||||||
|
|
||||||
|
deny /sys/[^f]*/** wklx,
|
||||||
|
deny /sys/f[^s]*/** wklx,
|
||||||
|
deny /sys/fs/[^c]*/** wklx,
|
||||||
|
deny /sys/fs/c[^g]*/** wklx,
|
||||||
|
deny /sys/fs/cg[^r]*/** wklx,
|
||||||
|
deny /sys/firmware/efi/efivars/** rwklx,
|
||||||
|
deny /sys/kernel/security/** rwklx,
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
/var/log/cloudron/*log {
|
|
||||||
missingok
|
|
||||||
notifempty
|
|
||||||
size 100k
|
|
||||||
nocompress
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
/var/log/supervisor/*log {
|
|
||||||
missingok
|
|
||||||
copytruncate
|
|
||||||
notifempty
|
|
||||||
size 100k
|
|
||||||
nocompress
|
|
||||||
}
|
|
||||||
@@ -4,4 +4,4 @@
|
|||||||
# http://bugs.mysql.com/bug.php?id=68514
|
# http://bugs.mysql.com/bug.php?id=68514
|
||||||
[mysqld]
|
[mysqld]
|
||||||
performance_schema=OFF
|
performance_schema=OFF
|
||||||
max_connection=50
|
max_connections=50
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# sudo logging breaks journalctl output with very long urls (systemd bug)
|
||||||
|
Defaults !syslog
|
||||||
|
|
||||||
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME BOX_ENV"
|
Defaults!/home/yellowtent/box/src/scripts/createappdir.sh env_keep="HOME BOX_ENV"
|
||||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh
|
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/createappdir.sh
|
||||||
|
|
||||||
@@ -22,8 +25,12 @@ yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reboot.sh
|
|||||||
Defaults!/home/yellowtent/box/src/scripts/reloadcollectd.sh env_keep="HOME BOX_ENV"
|
Defaults!/home/yellowtent/box/src/scripts/reloadcollectd.sh env_keep="HOME BOX_ENV"
|
||||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadcollectd.sh
|
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/reloadcollectd.sh
|
||||||
|
|
||||||
Defaults!/home/yellowtent/box/src/scripts/backupswap.sh env_keep="HOME BOX_ENV"
|
|
||||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/backupswap.sh
|
|
||||||
|
|
||||||
Defaults!/home/yellowtent/box/src/scripts/collectlogs.sh env_keep="HOME BOX_ENV"
|
Defaults!/home/yellowtent/box/src/scripts/collectlogs.sh env_keep="HOME BOX_ENV"
|
||||||
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
|
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/collectlogs.sh
|
||||||
|
|
||||||
|
Defaults!/home/yellowtent/box/src/scripts/retire.sh env_keep="HOME BOX_ENV"
|
||||||
|
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/retire.sh
|
||||||
|
|
||||||
|
Defaults!/home/yellowtent/box/src/scripts/setup_infra.sh env_keep="HOME BOX_ENV"
|
||||||
|
yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/setup_infra.sh
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
[program:apphealthtask]
|
|
||||||
command=/usr/bin/node "/home/yellowtent/box/apphealthtask.js"
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
redirect_stderr=true
|
|
||||||
stdout_logfile=/var/log/supervisor/apphealthtask.log
|
|
||||||
stdout_logfile_maxbytes=50MB
|
|
||||||
stdout_logfile_backups=2
|
|
||||||
user=yellowtent
|
|
||||||
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",BOX_ENV="cloudron",NODE_ENV="production"
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[program:box]
|
|
||||||
command=/usr/bin/node "/home/yellowtent/box/app.js"
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
redirect_stderr=true
|
|
||||||
stdout_logfile=/var/log/supervisor/box.log
|
|
||||||
stdout_logfile_maxbytes=50MB
|
|
||||||
stdout_logfile_backups=2
|
|
||||||
user=yellowtent
|
|
||||||
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*,connect-lastmile",BOX_ENV="cloudron",NODE_ENV="production"
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
[eventlistener:crashnotifier]
|
|
||||||
command=/usr/bin/node "/home/yellowtent/box/crashnotifier.js"
|
|
||||||
events=PROCESS_STATE
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
redirect_stderr=false
|
|
||||||
stderr_logfile=/var/log/supervisor/crashnotifier.log
|
|
||||||
stderr_logfile_maxbytes=50MB
|
|
||||||
stderr_logfile_backups=2
|
|
||||||
user=yellowtent
|
|
||||||
environment=HOME="/home/yellowtent",USER="yellowtent",BOX_ENV="cloudron",NODE_ENV="production"
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[program:janitor]
|
|
||||||
command=/usr/bin/node "/home/yellowtent/box/janitor.js"
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
redirect_stderr=true
|
|
||||||
stdout_logfile=/var/log/supervisor/janitor.log
|
|
||||||
stdout_logfile_maxbytes=50MB
|
|
||||||
stdout_logfile_backups=2
|
|
||||||
user=yellowtent
|
|
||||||
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",BOX_ENV="cloudron",NODE_ENV="production"
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
[program:oauthproxy]
|
|
||||||
command=/usr/bin/node "/home/yellowtent/box/oauthproxy.js"
|
|
||||||
autostart=true
|
|
||||||
autorestart=true
|
|
||||||
redirect_stderr=true
|
|
||||||
stdout_logfile=/var/log/supervisor/oauthproxy.log
|
|
||||||
stdout_logfile_maxbytes=50MB
|
|
||||||
stdout_logfile_backups=2
|
|
||||||
user=yellowtent
|
|
||||||
environment=HOME="/home/yellowtent",USER="yellowtent",DEBUG="box*",BOX_ENV="cloudron",NODE_ENV="production"
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
; supervisor config file
|
|
||||||
|
|
||||||
; http://coffeeonthekeyboard.com/using-supervisorctl-with-linux-permissions-but-without-root-or-sudo-977/
|
|
||||||
[inet_http_server]
|
|
||||||
port = 127.0.0.1:9001
|
|
||||||
|
|
||||||
[supervisord]
|
|
||||||
logfile=/var/log/supervisor/supervisord.log ; (main log file;default $CWD/supervisord.log)
|
|
||||||
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
|
|
||||||
logfile_maxbytes = 50MB
|
|
||||||
logfile_backups=10
|
|
||||||
loglevel = info
|
|
||||||
nodaemon = false
|
|
||||||
childlogdir = /var/log/supervisor/
|
|
||||||
|
|
||||||
; the below section must remain in the config file for RPC
|
|
||||||
; (supervisorctl/web interface) to work, additional interfaces may be
|
|
||||||
; added by defining them in separate rpcinterface: sections
|
|
||||||
[rpcinterface:supervisor]
|
|
||||||
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
|
||||||
|
|
||||||
[supervisorctl]
|
|
||||||
serverurl=http://127.0.0.1:9001
|
|
||||||
|
|
||||||
; The [include] section can just contain the "files" setting. This
|
|
||||||
; setting can list multiple files (separated by whitespace or
|
|
||||||
; newlines). It can also contain wildcards. The filenames are
|
|
||||||
; interpreted as relative to this file. Included files *cannot*
|
|
||||||
; include files themselves.
|
|
||||||
|
|
||||||
[include]
|
|
||||||
files = conf.d/*.conf
|
|
||||||
|
|
||||||
22
setup/container/systemd/box.service
Normal file
22
setup/container/systemd/box.service
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Cloudron Admin
|
||||||
|
OnFailure=crashnotifier@%n.service
|
||||||
|
StopWhenUnneeded=true
|
||||||
|
; journald crashes result in a EPIPE in node. Cannot ignore it as it results in loss of logs.
|
||||||
|
BindsTo=systemd-journald.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=idle
|
||||||
|
WorkingDirectory=/home/yellowtent/box
|
||||||
|
Restart=always
|
||||||
|
ExecStart=/usr/bin/node --max_old_space_size=150 /home/yellowtent/box/box.js
|
||||||
|
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||||
|
; kill apptask processes as well
|
||||||
|
KillMode=control-group
|
||||||
|
User=yellowtent
|
||||||
|
Group=yellowtent
|
||||||
|
MemoryLimit=200M
|
||||||
|
TimeoutStopSec=5s
|
||||||
|
StartLimitInterval=1
|
||||||
|
StartLimitBurst=60
|
||||||
|
|
||||||
10
setup/container/systemd/cloudron.target
Normal file
10
setup/container/systemd/cloudron.target
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Cloudron Smartserver
|
||||||
|
Documentation=https://cloudron.io/documentation.html
|
||||||
|
StopWhenUnneeded=true
|
||||||
|
Requires=box.service
|
||||||
|
After=box.service
|
||||||
|
# AllowIsolate=yes
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
15
setup/container/systemd/crashnotifier@.service
Normal file
15
setup/container/systemd/crashnotifier@.service
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# http://northernlightlabs.se/systemd.status.mail.on.unit.failure
|
||||||
|
[Unit]
|
||||||
|
Description=Cloudron Crash Notifier for %i
|
||||||
|
# otherwise, systemd will kill this unit immediately as nobody requires it
|
||||||
|
StopWhenUnneeded=false
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=idle
|
||||||
|
WorkingDirectory=/home/yellowtent/box
|
||||||
|
ExecStart="/home/yellowtent/box/crashnotifierservice.js" %I
|
||||||
|
Environment="HOME=/home/yellowtent" "USER=yellowtent" "DEBUG=box*,connect-lastmile" "BOX_ENV=cloudron" "NODE_ENV=production"
|
||||||
|
KillMode=process
|
||||||
|
User=yellowtent
|
||||||
|
Group=yellowtent
|
||||||
|
MemoryLimit=50M
|
||||||
@@ -9,8 +9,6 @@ readonly BOX_SRC_DIR="/home/yellowtent/box"
|
|||||||
readonly DATA_DIR="/home/yellowtent/data"
|
readonly DATA_DIR="/home/yellowtent/data"
|
||||||
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
|
readonly ADMIN_LOCATION="my" # keep this in sync with constants.js
|
||||||
|
|
||||||
source "${script_dir}/INFRA_VERSION" # this injects INFRA_VERSION
|
|
||||||
|
|
||||||
echo "Setting up nginx update page"
|
echo "Setting up nginx update page"
|
||||||
|
|
||||||
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
|
source "${script_dir}/argparser.sh" "$@" # this injects the arg_* variables used below
|
||||||
@@ -24,15 +22,18 @@ rm -rf "${SETUP_WEBSITE_DIR}" && mkdir -p "${SETUP_WEBSITE_DIR}"
|
|||||||
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
|
cp -r "${script_dir}/splash/website/"* "${SETUP_WEBSITE_DIR}"
|
||||||
|
|
||||||
# create nginx config
|
# create nginx config
|
||||||
infra_version="none"
|
readonly current_infra=$(node -e "console.log(require('${script_dir}/../src/infra_version.js').version);")
|
||||||
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && infra_version=$(cat "${DATA_DIR}/INFRA_VERSION")
|
existing_infra="none"
|
||||||
if [[ "${arg_retire}" == "true" || "${infra_version}" != "${INFRA_VERSION}" ]]; then
|
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && existing_infra=$(node -e "console.log(JSON.parse(require('fs').readFileSync('${DATA_DIR}/INFRA_VERSION', 'utf8')).version);")
|
||||||
|
if [[ "${arg_retire}" == "true" || "${existing_infra}" != "${current_infra}" ]]; then
|
||||||
|
echo "Showing progress bar on all subdomains in retired mode or infra update. retire: ${arg_retire} existing: ${existing_infra} current: ${current_infra}"
|
||||||
rm -f ${DATA_DIR}/nginx/applications/*
|
rm -f ${DATA_DIR}/nginx/applications/*
|
||||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||||
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
-O "{ \"vhost\": \"~^(.+)\$\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||||
else
|
else
|
||||||
|
echo "Show progress bar only on admin domain for normal update"
|
||||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"splash\", \"sourceDir\": \"${SETUP_WEBSITE_DIR}\", \"certFilePath\": \"cert/host.cert\", \"keyFilePath\": \"cert/host.key\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
|
echo '{ "update": { "percent": "10", "message": "Updating cloudron software" }, "backup": null }' > "${SETUP_WEBSITE_DIR}/progress.json"
|
||||||
|
|||||||
108
setup/start.sh
108
setup/start.sh
@@ -38,7 +38,9 @@ set_progress "10" "Ensuring directories"
|
|||||||
# keep these in sync with paths.js
|
# keep these in sync with paths.js
|
||||||
[[ "${is_update}" == "false" ]] && btrfs subvolume create "${DATA_DIR}/box"
|
[[ "${is_update}" == "false" ]] && btrfs subvolume create "${DATA_DIR}/box"
|
||||||
mkdir -p "${DATA_DIR}/box/appicons"
|
mkdir -p "${DATA_DIR}/box/appicons"
|
||||||
mkdir -p "${DATA_DIR}/box/mail"
|
mkdir -p "${DATA_DIR}/box/certs"
|
||||||
|
mkdir -p "${DATA_DIR}/box/mail/dkim/${arg_fqdn}"
|
||||||
|
mkdir -p "${DATA_DIR}/box/acme" # acme keys
|
||||||
mkdir -p "${DATA_DIR}/graphite"
|
mkdir -p "${DATA_DIR}/graphite"
|
||||||
|
|
||||||
mkdir -p "${DATA_DIR}/mysql"
|
mkdir -p "${DATA_DIR}/mysql"
|
||||||
@@ -47,6 +49,7 @@ mkdir -p "${DATA_DIR}/mongodb"
|
|||||||
mkdir -p "${DATA_DIR}/snapshots"
|
mkdir -p "${DATA_DIR}/snapshots"
|
||||||
mkdir -p "${DATA_DIR}/addons"
|
mkdir -p "${DATA_DIR}/addons"
|
||||||
mkdir -p "${DATA_DIR}/collectd/collectd.conf.d"
|
mkdir -p "${DATA_DIR}/collectd/collectd.conf.d"
|
||||||
|
mkdir -p "${DATA_DIR}/acme" # acme challenges
|
||||||
|
|
||||||
# bookkeep the version as part of data
|
# bookkeep the version as part of data
|
||||||
echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_versions_url}\" }" > "${DATA_DIR}/box/version"
|
echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_versions_url}\" }" > "${DATA_DIR}/box/version"
|
||||||
@@ -89,34 +92,36 @@ EOF
|
|||||||
|
|
||||||
set_progress "28" "Setup collectd"
|
set_progress "28" "Setup collectd"
|
||||||
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
|
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
|
||||||
# collectd 5.4.1 has some bug where we simply cannot get it to create df-vda1
|
|
||||||
mkdir -p "${DATA_DIR}/graphite/whisper/collectd/localhost/"
|
|
||||||
vda1_id=$(blkid -s UUID -o value /dev/vda1)
|
|
||||||
ln -sfF "df-disk_by-uuid_${vda1_id}" "${DATA_DIR}/graphite/whisper/collectd/localhost/df-vda1"
|
|
||||||
service collectd restart
|
service collectd restart
|
||||||
|
|
||||||
set_progress "30" "Setup nginx"
|
set_progress "30" "Setup nginx"
|
||||||
# setup naked domain to use admin by default. app restoration will overwrite this config
|
|
||||||
mkdir -p "${DATA_DIR}/nginx/applications"
|
mkdir -p "${DATA_DIR}/nginx/applications"
|
||||||
|
cp "${script_dir}/start/nginx/nginx.conf" "${DATA_DIR}/nginx/nginx.conf"
|
||||||
cp "${script_dir}/start/nginx/mime.types" "${DATA_DIR}/nginx/mime.types"
|
cp "${script_dir}/start/nginx/mime.types" "${DATA_DIR}/nginx/mime.types"
|
||||||
|
|
||||||
# generate the main nginx config file
|
|
||||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/nginx.ejs" \
|
|
||||||
-O "{ \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/nginx.conf"
|
|
||||||
|
|
||||||
# generate these for update code paths as well to overwrite splash
|
# generate these for update code paths as well to overwrite splash
|
||||||
|
admin_cert_file="${DATA_DIR}/nginx/cert/host.cert"
|
||||||
|
admin_key_file="${DATA_DIR}/nginx/cert/host.key"
|
||||||
|
if [[ -f "${DATA_DIR}/box/certs/${admin_fqdn}.cert" && -f "${DATA_DIR}/box/certs/${admin_fqdn}.key" ]]; then
|
||||||
|
admin_cert_file="${DATA_DIR}/box/certs/${admin_fqdn}.cert"
|
||||||
|
admin_key_file="${DATA_DIR}/box/certs/${admin_fqdn}.key"
|
||||||
|
fi
|
||||||
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
${BOX_SRC_DIR}/node_modules/.bin/ejs-cli -f "${script_dir}/start/nginx/appconfig.ejs" \
|
||||||
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
-O "{ \"vhost\": \"${admin_fqdn}\", \"adminOrigin\": \"${admin_origin}\", \"endpoint\": \"admin\", \"sourceDir\": \"${BOX_SRC_DIR}\", \"certFilePath\": \"${admin_cert_file}\", \"keyFilePath\": \"${admin_key_file}\" }" > "${DATA_DIR}/nginx/applications/admin.conf"
|
||||||
|
|
||||||
mkdir -p "${DATA_DIR}/nginx/cert"
|
mkdir -p "${DATA_DIR}/nginx/cert"
|
||||||
echo "${arg_tls_cert}" > ${DATA_DIR}/nginx/cert/host.cert
|
if [[ -f "${DATA_DIR}/box/certs/host.cert" && -f "${DATA_DIR}/box/certs/host.key" ]]; then
|
||||||
echo "${arg_tls_key}" > ${DATA_DIR}/nginx/cert/host.key
|
cp "${DATA_DIR}/box/certs/host.cert" "${DATA_DIR}/nginx/cert/host.cert"
|
||||||
|
cp "${DATA_DIR}/box/certs/host.key" "${DATA_DIR}/nginx/cert/host.key"
|
||||||
|
else
|
||||||
|
echo "${arg_tls_cert}" > "${DATA_DIR}/nginx/cert/host.cert"
|
||||||
|
echo "${arg_tls_key}" > "${DATA_DIR}/nginx/cert/host.key"
|
||||||
|
fi
|
||||||
|
|
||||||
set_progress "33" "Changing ownership"
|
set_progress "33" "Changing ownership"
|
||||||
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons"
|
chown "${USER}:${USER}" -R "${DATA_DIR}/box" "${DATA_DIR}/nginx" "${DATA_DIR}/collectd" "${DATA_DIR}/addons" "${DATA_DIR}/acme"
|
||||||
|
chown "${USER}:${USER}" "${DATA_DIR}/INFRA_VERSION" || true
|
||||||
set_progress "40" "Setting up infra"
|
chown "${USER}:${USER}" "${DATA_DIR}"
|
||||||
${script_dir}/start/setup_infra.sh "${arg_fqdn}"
|
|
||||||
|
|
||||||
set_progress "65" "Creating cloudron.conf"
|
set_progress "65" "Creating cloudron.conf"
|
||||||
sudo -u yellowtent -H bash <<EOF
|
sudo -u yellowtent -H bash <<EOF
|
||||||
@@ -131,14 +136,15 @@ cat > "${CONFIG_DIR}/cloudron.conf" <<CONF_END
|
|||||||
"fqdn": "${arg_fqdn}",
|
"fqdn": "${arg_fqdn}",
|
||||||
"isCustomDomain": ${arg_is_custom_domain},
|
"isCustomDomain": ${arg_is_custom_domain},
|
||||||
"boxVersionsUrl": "${arg_box_versions_url}",
|
"boxVersionsUrl": "${arg_box_versions_url}",
|
||||||
"adminEmail": "admin@${arg_fqdn}",
|
"provider": "${arg_provider}",
|
||||||
"database": {
|
"database": {
|
||||||
"hostname": "localhost",
|
"hostname": "localhost",
|
||||||
"username": "root",
|
"username": "root",
|
||||||
"password": "${mysql_root_password}",
|
"password": "${mysql_root_password}",
|
||||||
"port": 3306,
|
"port": 3306,
|
||||||
"name": "box"
|
"name": "box"
|
||||||
}
|
},
|
||||||
|
"appBundle": ${arg_app_bundle}
|
||||||
}
|
}
|
||||||
CONF_END
|
CONF_END
|
||||||
|
|
||||||
@@ -150,35 +156,57 @@ cat > "${BOX_SRC_DIR}/webadmin/dist/config.json" <<CONF_END
|
|||||||
CONF_END
|
CONF_END
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Add webadmin oauth client
|
# Add Backup Configuration
|
||||||
|
if [[ ! -z "${arg_backup_config}" ]]; then
|
||||||
|
echo "Add Backup Config"
|
||||||
|
|
||||||
|
mysql -u root -p${mysql_root_password} \
|
||||||
|
-e "REPLACE INTO settings (name, value) VALUES (\"backup_config\", '$arg_backup_config')" box
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add DNS Configuration
|
||||||
|
if [[ ! -z "${arg_dns_config}" ]]; then
|
||||||
|
echo "Add DNS Config"
|
||||||
|
|
||||||
|
mysql -u root -p${mysql_root_password} \
|
||||||
|
-e "REPLACE INTO settings (name, value) VALUES (\"dns_config\", '$arg_dns_config')" box
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add Update Configuration
|
||||||
|
if [[ ! -z "${arg_update_config}" ]]; then
|
||||||
|
echo "Add Update Config"
|
||||||
|
|
||||||
|
mysql -u root -p${mysql_root_password} \
|
||||||
|
-e "REPLACE INTO settings (name, value) VALUES (\"update_config\", '$arg_update_config')" box
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add TLS Configuration
|
||||||
|
if [[ ! -z "${arg_tls_config}" ]]; then
|
||||||
|
echo "Add TLS Config"
|
||||||
|
|
||||||
|
mysql -u root -p${mysql_root_password} \
|
||||||
|
-e "REPLACE INTO settings (name, value) VALUES (\"tls_config\", '$arg_tls_config')" box
|
||||||
|
fi
|
||||||
|
|
||||||
# The domain might have changed, therefor we have to update the record
|
# The domain might have changed, therefor we have to update the record
|
||||||
# !!! This needs to be in sync with the webadmin, specifically login_callback.js
|
# !!! This needs to be in sync with the webadmin, specifically login_callback.js
|
||||||
echo "Add webadmin oauth cient"
|
echo "Add webadmin api cient"
|
||||||
ADMIN_SCOPES="root,developer,profile,users,apps,settings,roleUser"
|
readonly ADMIN_SCOPES="cloudron,developer,profile,users,apps,settings"
|
||||||
mysql -u root -p${mysql_root_password} \
|
mysql -u root -p${mysql_root_password} \
|
||||||
-e "REPLACE INTO clients (id, appId, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"webadmin\", \"secret-webadmin\", \"${admin_origin}\", \"${ADMIN_SCOPES}\")" box
|
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-webadmin\", \"Settings\", \"built-in\", \"secret-webadmin\", \"${admin_origin}\", \"${ADMIN_SCOPES}\")" box
|
||||||
|
|
||||||
echo "Add localhost test oauth cient"
|
echo "Add SDK api client"
|
||||||
ADMIN_SCOPES="root,developer,profile,users,apps,settings,roleUser"
|
|
||||||
mysql -u root -p${mysql_root_password} \
|
mysql -u root -p${mysql_root_password} \
|
||||||
-e "REPLACE INTO clients (id, appId, clientSecret, redirectURI, scope) VALUES (\"cid-test\", \"test\", \"secret-test\", \"http://127.0.0.1:5000\", \"${ADMIN_SCOPES}\")" box
|
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-sdk\", \"SDK\", \"built-in\", \"secret-sdk\", \"${admin_origin}\", \"*,roleSdk\")" box
|
||||||
|
|
||||||
set_progress "80" "Reloading supervisor"
|
echo "Add cli api client"
|
||||||
# looks like restarting supervisor completely is the only way to reload it
|
mysql -u root -p${mysql_root_password} \
|
||||||
service supervisor stop || true
|
-e "REPLACE INTO clients (id, appId, type, clientSecret, redirectURI, scope) VALUES (\"cid-cli\", \"Cloudron Tool\", \"built-in\", \"secret-cli\", \"${admin_origin}\", \"*,roleSdk\")" box
|
||||||
|
|
||||||
echo -n "Waiting for supervisord to stop"
|
set_progress "80" "Starting Cloudron"
|
||||||
while test -e "/var/run/supervisord.pid" && kill -0 `cat /var/run/supervisord.pid`; do
|
systemctl start cloudron.target
|
||||||
echo -n "."
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "Starting supervisor"
|
sleep 2 # give systemd sometime to start the processes
|
||||||
|
|
||||||
service supervisor start
|
|
||||||
|
|
||||||
sleep 2 # give supervisor sometime to start the processes
|
|
||||||
|
|
||||||
set_progress "85" "Reloading nginx"
|
set_progress "85" "Reloading nginx"
|
||||||
nginx -s reload
|
nginx -s reload
|
||||||
|
|||||||
@@ -133,10 +133,10 @@ LoadPlugin nginx
|
|||||||
# Globals true
|
# Globals true
|
||||||
#</LoadPlugin>
|
#</LoadPlugin>
|
||||||
#LoadPlugin pinba
|
#LoadPlugin pinba
|
||||||
LoadPlugin ping
|
#LoadPlugin ping
|
||||||
#LoadPlugin postgresql
|
#LoadPlugin postgresql
|
||||||
#LoadPlugin powerdns
|
#LoadPlugin powerdns
|
||||||
LoadPlugin processes
|
#LoadPlugin processes
|
||||||
#LoadPlugin protocols
|
#LoadPlugin protocols
|
||||||
#<LoadPlugin python>
|
#<LoadPlugin python>
|
||||||
# Globals true
|
# Globals true
|
||||||
@@ -161,7 +161,7 @@ LoadPlugin tail
|
|||||||
#LoadPlugin users
|
#LoadPlugin users
|
||||||
#LoadPlugin uuid
|
#LoadPlugin uuid
|
||||||
#LoadPlugin varnish
|
#LoadPlugin varnish
|
||||||
LoadPlugin vmem
|
#LoadPlugin vmem
|
||||||
#LoadPlugin vserver
|
#LoadPlugin vserver
|
||||||
#LoadPlugin wireless
|
#LoadPlugin wireless
|
||||||
LoadPlugin write_graphite
|
LoadPlugin write_graphite
|
||||||
@@ -193,11 +193,11 @@ LoadPlugin write_graphite
|
|||||||
</Plugin>
|
</Plugin>
|
||||||
|
|
||||||
<Plugin df>
|
<Plugin df>
|
||||||
FSType "tmpfs"
|
FSType "ext4"
|
||||||
MountPoint "/dev"
|
FSType "btrfs"
|
||||||
|
|
||||||
ReportByDevice true
|
ReportByDevice true
|
||||||
IgnoreSelected true
|
IgnoreSelected false
|
||||||
|
|
||||||
ValuesAbsolute true
|
ValuesAbsolute true
|
||||||
ValuesPercentage true
|
ValuesPercentage true
|
||||||
@@ -212,17 +212,6 @@ LoadPlugin write_graphite
|
|||||||
URL "http://127.0.0.1/nginx_status"
|
URL "http://127.0.0.1/nginx_status"
|
||||||
</Plugin>
|
</Plugin>
|
||||||
|
|
||||||
<Plugin ping>
|
|
||||||
Host "google.com"
|
|
||||||
Interval 1.0
|
|
||||||
Timeout 0.9
|
|
||||||
TTL 255
|
|
||||||
</Plugin>
|
|
||||||
|
|
||||||
<Plugin processes>
|
|
||||||
ProcessMatch "app" "node app.js"
|
|
||||||
</Plugin>
|
|
||||||
|
|
||||||
<Plugin swap>
|
<Plugin swap>
|
||||||
ReportByDevice false
|
ReportByDevice false
|
||||||
ReportBytes true
|
ReportBytes true
|
||||||
@@ -255,10 +244,6 @@ LoadPlugin write_graphite
|
|||||||
</File>
|
</File>
|
||||||
</Plugin>
|
</Plugin>
|
||||||
|
|
||||||
<Plugin vmem>
|
|
||||||
Verbose false
|
|
||||||
</Plugin>
|
|
||||||
|
|
||||||
<Plugin write_graphite>
|
<Plugin write_graphite>
|
||||||
<Node "graphing">
|
<Node "graphing">
|
||||||
Host "localhost"
|
Host "localhost"
|
||||||
|
|||||||
@@ -10,17 +10,18 @@ server {
|
|||||||
|
|
||||||
ssl on;
|
ssl on;
|
||||||
# paths are relative to prefix and not to this file
|
# paths are relative to prefix and not to this file
|
||||||
ssl_certificate cert/host.cert;
|
ssl_certificate <%= certFilePath %>;
|
||||||
ssl_certificate_key cert/host.key;
|
ssl_certificate_key <%= keyFilePath %>;
|
||||||
ssl_session_timeout 5m;
|
ssl_session_timeout 5m;
|
||||||
ssl_session_cache shared:SSL:50m;
|
ssl_session_cache shared:SSL:50m;
|
||||||
|
|
||||||
# https://bettercrypto.org/static/applied-crypto-hardening.pdf
|
# https://bettercrypto.org/static/applied-crypto-hardening.pdf
|
||||||
# https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
# https://mozilla.github.io/server-side-tls/ssl-config-generator/
|
||||||
# https://cipherli.st/
|
# https://cipherli.st/
|
||||||
|
# https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
|
ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # don't use SSLv3 ref: POODLE
|
||||||
ssl_ciphers 'AES128+EECDH:AES128+EDH';
|
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
|
||||||
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
|
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains";
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
@@ -37,11 +38,24 @@ server {
|
|||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection $connection_upgrade;
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
|
||||||
error_page 500 502 503 504 @appstatus;
|
# only serve up the status page if we get proxy gateway errors
|
||||||
|
error_page 502 503 504 @appstatus;
|
||||||
location @appstatus {
|
location @appstatus {
|
||||||
return 307 <%= adminOrigin %>/appstatus.html?referrer=https://$host$request_uri;
|
return 307 <%= adminOrigin %>/appstatus.html?referrer=https://$host$request_uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<% if ( endpoint === 'app' ) { %>
|
||||||
|
# For some reason putting this webdav block inside location does not work
|
||||||
|
# http://serverfault.com/questions/121766/webdav-rename-fails-on-an-apache-mod-dav-install-behind-nginx
|
||||||
|
if ($request_method ~ ^(COPY|MOVE)$) {
|
||||||
|
set $destination $http_destination;
|
||||||
|
}
|
||||||
|
if ($destination ~* ^https(.+)$) {
|
||||||
|
set $destination http$1;
|
||||||
|
}
|
||||||
|
proxy_set_header Destination $destination;
|
||||||
|
<% } %>
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
# increase the proxy buffer sizes to not run into buffer issues (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers)
|
# increase the proxy buffer sizes to not run into buffer issues (http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffers)
|
||||||
proxy_buffer_size 128k;
|
proxy_buffer_size 128k;
|
||||||
@@ -57,19 +71,25 @@ server {
|
|||||||
client_max_body_size 1m;
|
client_max_body_size 1m;
|
||||||
}
|
}
|
||||||
|
|
||||||
# graphite paths
|
# the read timeout is between successive reads and not the whole connection
|
||||||
location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
location ~ ^/api/v1/apps/.*/exec$ {
|
||||||
proxy_pass http://127.0.0.1:8000;
|
proxy_pass http://127.0.0.1:3000;
|
||||||
client_max_body_size 1m;
|
proxy_read_timeout 30m;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# graphite paths
|
||||||
|
# location ~ ^/(graphite|content|metrics|dashboard|render|browser|composer)/ {
|
||||||
|
# proxy_pass http://127.0.0.1:8000;
|
||||||
|
# client_max_body_size 1m;
|
||||||
|
# }
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
root <%= sourceDir %>/webadmin/dist;
|
root <%= sourceDir %>/webadmin/dist;
|
||||||
index index.html index.htm;
|
index index.html index.htm;
|
||||||
}
|
}
|
||||||
|
|
||||||
<% } else if ( endpoint === 'oauthproxy' ) { %>
|
<% } else if ( endpoint === 'oauthproxy' ) { %>
|
||||||
proxy_pass http://127.0.0.1:4000;
|
proxy_pass http://127.0.0.1:3003;
|
||||||
proxy_set_header X-Cloudron-Proxy-Port <%= port %>;
|
proxy_set_header X-Cloudron-Proxy-Port <%= port %>;
|
||||||
<% } else if ( endpoint === 'app' ) { %>
|
<% } else if ( endpoint === 'app' ) { %>
|
||||||
proxy_pass http://127.0.0.1:<%= port %>;
|
proxy_pass http://127.0.0.1:<%= port %>;
|
||||||
|
|||||||
@@ -24,7 +24,14 @@ http {
|
|||||||
|
|
||||||
sendfile on;
|
sendfile on;
|
||||||
|
|
||||||
keepalive_timeout 65;
|
# timeout for client to finish sending headers
|
||||||
|
client_header_timeout 30s;
|
||||||
|
|
||||||
|
# timeout for reading client request body (successive read timeout and not whole body!)
|
||||||
|
client_body_timeout 60s;
|
||||||
|
|
||||||
|
# keep-alive connections timeout in 65s. this is because many browsers timeout in 60 seconds
|
||||||
|
keepalive_timeout 65s;
|
||||||
|
|
||||||
# HTTP server
|
# HTTP server
|
||||||
server {
|
server {
|
||||||
@@ -38,14 +45,21 @@ http {
|
|||||||
deny all;
|
deny all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# acme challenges
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
default_type text/plain;
|
||||||
|
alias /home/yellowtent/data/acme/;
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
# redirect everything to HTTPS
|
# redirect everything to HTTPS
|
||||||
return 301 https://$host$request_uri;
|
return 301 https://$host$request_uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# We have to enable https for nginx to read in the vhost in http request
|
# This server handles the naked domain for custom domains.
|
||||||
# and send a 404. This is a side-effect of using wildcard DNS
|
# It can also be used for wildcard subdomain 404. This feature is not used by the Cloudron itself
|
||||||
|
# because box always sets up DNS records for app subdomains.
|
||||||
server {
|
server {
|
||||||
listen 443 default_server;
|
listen 443 default_server;
|
||||||
ssl on;
|
ssl on;
|
||||||
@@ -55,11 +69,21 @@ http {
|
|||||||
error_page 404 = @fallback;
|
error_page 404 = @fallback;
|
||||||
location @fallback {
|
location @fallback {
|
||||||
internal;
|
internal;
|
||||||
root <%= sourceDir %>/webadmin/dist;
|
root /home/yellowtent/box/webadmin/dist;
|
||||||
rewrite ^/$ /nakeddomain.html break;
|
rewrite ^/$ /nakeddomain.html break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 404;
|
location / {
|
||||||
|
internal;
|
||||||
|
root /home/yellowtent/box/webadmin/dist;
|
||||||
|
rewrite ^/$ /nakeddomain.html break;
|
||||||
|
}
|
||||||
|
|
||||||
|
# required for /api/v1/cloudron/avatar
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://127.0.0.1:3000;
|
||||||
|
client_max_body_size 1m;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
include applications/*.conf;
|
include applications/*.conf;
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -eu -o pipefail
|
|
||||||
|
|
||||||
readonly DATA_DIR="/home/yellowtent/data"
|
|
||||||
|
|
||||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
source "${script_dir}/../INFRA_VERSION" # this injects INFRA_VERSION
|
|
||||||
|
|
||||||
arg_fqdn="$1"
|
|
||||||
|
|
||||||
# removing containers ensures containers are launched with latest config updates
|
|
||||||
# restore code in appatask does not delete old containers
|
|
||||||
infra_version="none"
|
|
||||||
[[ -f "${DATA_DIR}/INFRA_VERSION" ]] && infra_version=$(cat "${DATA_DIR}/INFRA_VERSION")
|
|
||||||
if [[ "${infra_version}" == "${INFRA_VERSION}" ]]; then
|
|
||||||
echo "Infrastructure is upto date"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Upgrading infrastructure from ${infra_version} to ${INFRA_VERSION}"
|
|
||||||
|
|
||||||
existing_containers=$(docker ps -qa)
|
|
||||||
echo "Remove containers: ${existing_containers}"
|
|
||||||
if [[ -n "${existing_containers}" ]]; then
|
|
||||||
echo "${existing_containers}" | xargs docker rm -f
|
|
||||||
fi
|
|
||||||
|
|
||||||
# graphite
|
|
||||||
graphite_container_id=$(docker run --restart=always -d --name="graphite" \
|
|
||||||
-p 127.0.0.1:2003:2003 \
|
|
||||||
-p 127.0.0.1:2004:2004 \
|
|
||||||
-p 127.0.0.1:8000:8000 \
|
|
||||||
-v "${DATA_DIR}/graphite:/app/data" \
|
|
||||||
"${GRAPHITE_IMAGE}")
|
|
||||||
echo "Graphite container id: ${graphite_container_id}"
|
|
||||||
|
|
||||||
# mail
|
|
||||||
mail_container_id=$(docker run --restart=always -d --name="mail" \
|
|
||||||
-p 127.0.0.1:25:25 \
|
|
||||||
-h "${arg_fqdn}" \
|
|
||||||
-e "DOMAIN_NAME=${arg_fqdn}" \
|
|
||||||
-v "${DATA_DIR}/box/mail:/app/data" \
|
|
||||||
"${MAIL_IMAGE}")
|
|
||||||
echo "Mail container id: ${mail_container_id}"
|
|
||||||
|
|
||||||
# mysql
|
|
||||||
mysql_addon_root_password=$(pwgen -1 -s)
|
|
||||||
docker0_ip=$(/sbin/ifconfig docker0 | grep "inet addr" | awk -F: '{print $2}' | awk '{print $1}')
|
|
||||||
cat > "${DATA_DIR}/addons/mysql_vars.sh" <<EOF
|
|
||||||
readonly MYSQL_ROOT_PASSWORD='${mysql_addon_root_password}'
|
|
||||||
readonly MYSQL_ROOT_HOST='${docker0_ip}'
|
|
||||||
EOF
|
|
||||||
mysql_container_id=$(docker run --restart=always -d --name="mysql" \
|
|
||||||
-h "${arg_fqdn}" \
|
|
||||||
-v "${DATA_DIR}/mysql:/var/lib/mysql" \
|
|
||||||
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
|
|
||||||
"${MYSQL_IMAGE}")
|
|
||||||
echo "MySQL container id: ${mysql_container_id}"
|
|
||||||
|
|
||||||
# postgresql
|
|
||||||
postgresql_addon_root_password=$(pwgen -1 -s)
|
|
||||||
cat > "${DATA_DIR}/addons/postgresql_vars.sh" <<EOF
|
|
||||||
readonly POSTGRESQL_ROOT_PASSWORD='${postgresql_addon_root_password}'
|
|
||||||
EOF
|
|
||||||
postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
|
|
||||||
-h "${arg_fqdn}" \
|
|
||||||
-v "${DATA_DIR}/postgresql:/var/lib/postgresql" \
|
|
||||||
-v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
|
|
||||||
"${POSTGRESQL_IMAGE}")
|
|
||||||
echo "PostgreSQL container id: ${postgresql_container_id}"
|
|
||||||
|
|
||||||
# mongodb
|
|
||||||
mongodb_addon_root_password=$(pwgen -1 -s)
|
|
||||||
cat > "${DATA_DIR}/addons/mongodb_vars.sh" <<EOF
|
|
||||||
readonly MONGODB_ROOT_PASSWORD='${mongodb_addon_root_password}'
|
|
||||||
EOF
|
|
||||||
mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
|
|
||||||
-h "${arg_fqdn}" \
|
|
||||||
-v "${DATA_DIR}/mongodb:/var/lib/mongodb" \
|
|
||||||
-v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
|
|
||||||
"${MONGODB_IMAGE}")
|
|
||||||
echo "Mongodb container id: ${mongodb_container_id}"
|
|
||||||
|
|
||||||
if [[ "${infra_version}" == "none" ]]; then
|
|
||||||
# if no existing infra was found (for new and restoring cloudons), download app backups
|
|
||||||
echo "Marking installed apps for restore"
|
|
||||||
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_restore" WHERE installationState = "installed"' box
|
|
||||||
else
|
|
||||||
# if existing infra was found, just mark apps for reconfiguration
|
|
||||||
mysql -u root -ppassword -e 'UPDATE apps SET installationState = "pending_configure" WHERE installationState = "installed"' box
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo -n "${INFRA_VERSION}" > "${DATA_DIR}/INFRA_VERSION"
|
|
||||||
|
|
||||||
@@ -2,14 +2,6 @@
|
|||||||
|
|
||||||
set -eu -o pipefail
|
set -eu -o pipefail
|
||||||
|
|
||||||
echo "Stopping box code"
|
echo "Stopping cloudron"
|
||||||
|
|
||||||
service supervisor stop || true
|
|
||||||
|
|
||||||
echo -n "Waiting for supervisord to stop"
|
|
||||||
while test -e "/var/run/supervisord.pid" && kill -0 `cat /var/run/supervisord.pid`; do
|
|
||||||
echo -n "."
|
|
||||||
sleep 1
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
|
systemctl stop cloudron.target
|
||||||
|
|||||||
701
src/addons.js
701
src/addons.js
File diff suppressed because it is too large
Load Diff
72
src/appdb.js
72
src/appdb.js
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
get: get,
|
get: get,
|
||||||
getBySubdomain: getBySubdomain,
|
|
||||||
getByHttpPort: getByHttpPort,
|
getByHttpPort: getByHttpPort,
|
||||||
|
getByContainerId: getByContainerId,
|
||||||
add: add,
|
add: add,
|
||||||
exists: exists,
|
exists: exists,
|
||||||
del: del,
|
del: del,
|
||||||
@@ -35,12 +35,12 @@ exports = module.exports = {
|
|||||||
ISTATE_ERROR: 'error', // error executing last pending_* command
|
ISTATE_ERROR: 'error', // error executing last pending_* command
|
||||||
ISTATE_INSTALLED: 'installed', // app is installed
|
ISTATE_INSTALLED: 'installed', // app is installed
|
||||||
|
|
||||||
// run codes (keep in sync in UI)
|
|
||||||
RSTATE_RUNNING: 'running',
|
RSTATE_RUNNING: 'running',
|
||||||
RSTATE_PENDING_START: 'pending_start',
|
RSTATE_PENDING_START: 'pending_start',
|
||||||
RSTATE_PENDING_STOP: 'pending_stop',
|
RSTATE_PENDING_STOP: 'pending_stop',
|
||||||
RSTATE_STOPPED: 'stopped', // app stopped by use
|
RSTATE_STOPPED: 'stopped', // app stopped by use
|
||||||
|
|
||||||
|
// run codes (keep in sync in UI)
|
||||||
HEALTH_HEALTHY: 'healthy',
|
HEALTH_HEALTHY: 'healthy',
|
||||||
HEALTH_UNHEALTHY: 'unhealthy',
|
HEALTH_UNHEALTHY: 'unhealthy',
|
||||||
HEALTH_ERROR: 'error',
|
HEALTH_ERROR: 'error',
|
||||||
@@ -56,13 +56,9 @@ var assert = require('assert'),
|
|||||||
safe = require('safetydance'),
|
safe = require('safetydance'),
|
||||||
util = require('util');
|
util = require('util');
|
||||||
|
|
||||||
var APPS_FIELDS = [ 'id', 'appStoreId', 'installationState', 'installationProgress', 'runState',
|
|
||||||
'health', 'containerId', 'manifestJson', 'httpPort', 'location', 'dnsRecordId',
|
|
||||||
'accessRestriction', 'lastBackupId', 'lastBackupConfigJson', 'oldConfigJson' ].join(',');
|
|
||||||
|
|
||||||
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.installationProgress', 'apps.runState',
|
||||||
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
|
'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'apps.location', 'apps.dnsRecordId',
|
||||||
'apps.accessRestriction', 'apps.lastBackupId', 'apps.lastBackupConfigJson', 'apps.oldConfigJson' ].join(',');
|
'apps.accessRestrictionJson', 'apps.lastBackupId', 'apps.oldConfigJson', 'apps.memoryLimit', 'apps.altDomain' ].join(',');
|
||||||
|
|
||||||
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
var PORT_BINDINGS_FIELDS = [ 'hostPort', 'environmentVariable', 'appId' ].join(',');
|
||||||
|
|
||||||
@@ -73,10 +69,6 @@ function postProcess(result) {
|
|||||||
result.manifest = safe.JSON.parse(result.manifestJson);
|
result.manifest = safe.JSON.parse(result.manifestJson);
|
||||||
delete result.manifestJson;
|
delete result.manifestJson;
|
||||||
|
|
||||||
assert(result.lastBackupConfigJson === null || typeof result.lastBackupConfigJson === 'string');
|
|
||||||
result.lastBackupConfig = safe.JSON.parse(result.lastBackupConfigJson);
|
|
||||||
delete result.lastBackupConfigJson;
|
|
||||||
|
|
||||||
assert(result.oldConfigJson === null || typeof result.oldConfigJson === 'string');
|
assert(result.oldConfigJson === null || typeof result.oldConfigJson === 'string');
|
||||||
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
|
result.oldConfig = safe.JSON.parse(result.oldConfigJson);
|
||||||
delete result.oldConfigJson;
|
delete result.oldConfigJson;
|
||||||
@@ -94,6 +86,11 @@ function postProcess(result) {
|
|||||||
for (var i = 0; i < environmentVariables.length; i++) {
|
for (var i = 0; i < environmentVariables.length; i++) {
|
||||||
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
|
result.portBindings[environmentVariables[i]] = parseInt(hostPorts[i], 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assert(result.accessRestrictionJson === null || typeof result.accessRestrictionJson === 'string');
|
||||||
|
result.accessRestriction = safe.JSON.parse(result.accessRestrictionJson);
|
||||||
|
if (result.accessRestriction && !result.accessRestriction.users) result.accessRestriction.users = [];
|
||||||
|
delete result.accessRestrictionJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
function get(id, callback) {
|
function get(id, callback) {
|
||||||
@@ -112,22 +109,6 @@ function get(id, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBySubdomain(subdomain, callback) {
|
|
||||||
assert.strictEqual(typeof subdomain, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
|
||||||
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
|
||||||
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE location = ? GROUP BY apps.id', [ subdomain ], function (error, result) {
|
|
||||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
|
||||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
|
||||||
|
|
||||||
postProcess(result[0]);
|
|
||||||
|
|
||||||
callback(null, result[0]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getByHttpPort(httpPort, callback) {
|
function getByHttpPort(httpPort, callback) {
|
||||||
assert.strictEqual(typeof httpPort, 'number');
|
assert.strictEqual(typeof httpPort, 'number');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
@@ -144,6 +125,22 @@ function getByHttpPort(httpPort, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getByContainerId(containerId, callback) {
|
||||||
|
assert.strictEqual(typeof containerId, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
database.query('SELECT ' + APPS_FIELDS_PREFIXED + ','
|
||||||
|
+ 'GROUP_CONCAT(CAST(appPortBindings.hostPort AS CHAR(6))) AS hostPorts, GROUP_CONCAT(appPortBindings.environmentVariable) AS environmentVariables'
|
||||||
|
+ ' FROM apps LEFT OUTER JOIN appPortBindings ON apps.id = appPortBindings.appId WHERE containerId = ? GROUP BY apps.id', [ containerId ], function (error, result) {
|
||||||
|
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||||
|
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||||
|
|
||||||
|
postProcess(result[0]);
|
||||||
|
|
||||||
|
callback(null, result[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getAll(callback) {
|
function getAll(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
@@ -159,24 +156,27 @@ function getAll(callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, callback) {
|
function add(id, appStoreId, manifest, location, portBindings, accessRestriction, memoryLimit, altDomain, callback) {
|
||||||
assert.strictEqual(typeof id, 'string');
|
assert.strictEqual(typeof id, 'string');
|
||||||
assert.strictEqual(typeof appStoreId, 'string');
|
assert.strictEqual(typeof appStoreId, 'string');
|
||||||
assert(manifest && typeof manifest === 'object');
|
assert(manifest && typeof manifest === 'object');
|
||||||
assert.strictEqual(typeof manifest.version, 'string');
|
assert.strictEqual(typeof manifest.version, 'string');
|
||||||
assert.strictEqual(typeof location, 'string');
|
assert.strictEqual(typeof location, 'string');
|
||||||
assert.strictEqual(typeof portBindings, 'object');
|
assert.strictEqual(typeof portBindings, 'object');
|
||||||
assert.strictEqual(typeof accessRestriction, 'string');
|
assert.strictEqual(typeof accessRestriction, 'object');
|
||||||
|
assert.strictEqual(typeof memoryLimit, 'number');
|
||||||
|
assert(altDomain === null || typeof altDomain === 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
portBindings = portBindings || { };
|
portBindings = portBindings || { };
|
||||||
|
|
||||||
var manifestJson = JSON.stringify(manifest);
|
var manifestJson = JSON.stringify(manifest);
|
||||||
|
var accessRestrictionJson = JSON.stringify(accessRestriction);
|
||||||
|
|
||||||
var queries = [ ];
|
var queries = [ ];
|
||||||
queries.push({
|
queries.push({
|
||||||
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestriction) VALUES (?, ?, ?, ?, ?, ?)',
|
query: 'INSERT INTO apps (id, appStoreId, manifestJson, installationState, location, accessRestrictionJson, memoryLimit, altDomain) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestriction ]
|
args: [ id, appStoreId, manifestJson, exports.ISTATE_PENDING_INSTALL, location, accessRestrictionJson, memoryLimit, altDomain ]
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(portBindings).forEach(function (env) {
|
Object.keys(portBindings).forEach(function (env) {
|
||||||
@@ -261,6 +261,7 @@ function updateWithConstraints(id, app, constraints, callback) {
|
|||||||
assert.strictEqual(typeof constraints, 'string');
|
assert.strictEqual(typeof constraints, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
|
assert(!('portBindings' in app) || typeof app.portBindings === 'object');
|
||||||
|
assert(!('accessRestriction' in app) || typeof app.accessRestriction === 'object' || app.accessRestriction === '');
|
||||||
|
|
||||||
var queries = [ ];
|
var queries = [ ];
|
||||||
|
|
||||||
@@ -279,12 +280,12 @@ function updateWithConstraints(id, app, constraints, callback) {
|
|||||||
if (p === 'manifest') {
|
if (p === 'manifest') {
|
||||||
fields.push('manifestJson = ?');
|
fields.push('manifestJson = ?');
|
||||||
values.push(JSON.stringify(app[p]));
|
values.push(JSON.stringify(app[p]));
|
||||||
} else if (p === 'lastBackupConfig') {
|
|
||||||
fields.push('lastBackupConfigJson = ?');
|
|
||||||
values.push(JSON.stringify(app[p]));
|
|
||||||
} else if (p === 'oldConfig') {
|
} else if (p === 'oldConfig') {
|
||||||
fields.push('oldConfigJson = ?');
|
fields.push('oldConfigJson = ?');
|
||||||
values.push(JSON.stringify(app[p]));
|
values.push(JSON.stringify(app[p]));
|
||||||
|
} else if (p === 'accessRestriction') {
|
||||||
|
fields.push('accessRestrictionJson = ?');
|
||||||
|
values.push(JSON.stringify(app[p]));
|
||||||
} else if (p !== 'portBindings') {
|
} else if (p !== 'portBindings') {
|
||||||
fields.push(p + ' = ?');
|
fields.push(p + ' = ?');
|
||||||
values.push(app[p]);
|
values.push(app[p]);
|
||||||
@@ -335,6 +336,7 @@ function setInstallationCommand(appId, installationState, values, callback) {
|
|||||||
|
|
||||||
// Rules are:
|
// Rules are:
|
||||||
// uninstall is allowed in any state
|
// 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
|
// restore is allowed from installed or error state
|
||||||
// update and configure are allowed only in installed state
|
// update and configure are allowed only in installed state
|
||||||
|
|
||||||
@@ -342,7 +344,7 @@ function setInstallationCommand(appId, installationState, values, callback) {
|
|||||||
updateWithConstraints(appId, values, '', callback);
|
updateWithConstraints(appId, values, '', callback);
|
||||||
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
|
} else if (installationState === exports.ISTATE_PENDING_RESTORE) {
|
||||||
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error")', callback);
|
updateWithConstraints(appId, values, 'AND (installationState = "installed" OR installationState = "error")', callback);
|
||||||
} else if (installationState === exports.ISTATE_PENDING_UPDATE || exports.ISTATE_PENDING_CONFIGURE || installationState == exports.ISTATE_PENDING_BACKUP) {
|
} else if (installationState === exports.ISTATE_PENDING_UPDATE || installationState === exports.ISTATE_PENDING_CONFIGURE || installationState === exports.ISTATE_PENDING_BACKUP) {
|
||||||
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
|
updateWithConstraints(appId, values, 'AND installationState = "installed"', callback);
|
||||||
} else {
|
} else {
|
||||||
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
|
callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, 'invalid installationState'));
|
||||||
|
|||||||
200
src/apphealthmonitor.js
Normal file
200
src/apphealthmonitor.js
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var appdb = require('./appdb.js'),
|
||||||
|
assert = require('assert'),
|
||||||
|
async = require('async'),
|
||||||
|
config = require('./config.js'),
|
||||||
|
DatabaseError = require('./databaseerror.js'),
|
||||||
|
debug = require('debug')('box:apphealthmonitor'),
|
||||||
|
docker = require('./docker.js').connection,
|
||||||
|
mailer = require('./mailer.js'),
|
||||||
|
superagent = require('superagent'),
|
||||||
|
util = require('util');
|
||||||
|
|
||||||
|
exports = module.exports = {
|
||||||
|
start: start,
|
||||||
|
stop: stop
|
||||||
|
};
|
||||||
|
|
||||||
|
var HEALTHCHECK_INTERVAL = 10 * 1000; // every 10 seconds. this needs to be small since the UI makes only healthy apps clickable
|
||||||
|
var UNHEALTHY_THRESHOLD = 10 * 60 * 1000; // 10 minutes
|
||||||
|
var gHealthInfo = { }; // { time, emailSent }
|
||||||
|
var gRunTimeout = null;
|
||||||
|
var gDockerEventStream = null;
|
||||||
|
|
||||||
|
function debugApp(app) {
|
||||||
|
assert(!app || typeof app === 'object');
|
||||||
|
|
||||||
|
var prefix = app ? (app.location || 'naked_domain') : '(no app)';
|
||||||
|
var manifestAppId = app ? app.manifest.id : '';
|
||||||
|
var id = app ? app.id : '';
|
||||||
|
|
||||||
|
debug(prefix + ' ' + manifestAppId + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)) + ' - ' + id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHealth(app, health, callback) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert.strictEqual(typeof health, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var now = new Date();
|
||||||
|
|
||||||
|
if (!(app.id in gHealthInfo)) { // add new apps to list
|
||||||
|
gHealthInfo[app.id] = { time: now, emailSent: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
debugApp(app, 'marking as unhealthy since not seen for more than %s minutes', UNHEALTHY_THRESHOLD/(60 * 1000));
|
||||||
|
|
||||||
|
if (app.appStoreId !== '') mailer.appDied(app); // do not send mails for dev apps
|
||||||
|
gHealthInfo[app.id].emailSent = true;
|
||||||
|
} else {
|
||||||
|
debugApp(app, 'waiting for sometime to update the app health');
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
appdb.setHealth(app.id, health, function (error) {
|
||||||
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null); // app uninstalled?
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
app.health = health;
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// callback is called with error for fatal errors and not if health check failed
|
||||||
|
function checkAppHealth(app, callback) {
|
||||||
|
if (app.installationState !== appdb.ISTATE_INSTALLED || app.runState !== appdb.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;
|
||||||
|
|
||||||
|
container.inspect(function (err, data) {
|
||||||
|
if (err || !data || !data.State) {
|
||||||
|
debugApp(app, 'Error inspecting container');
|
||||||
|
return setHealth(app, appdb.HEALTH_ERROR, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.State.Running !== true) {
|
||||||
|
debugApp(app, 'exited');
|
||||||
|
return setHealth(app, appdb.HEALTH_DEAD, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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', config.appFqdn(app.location)) // required for some apache configs with rewrite rules
|
||||||
|
.redirects(0)
|
||||||
|
.timeout(HEALTHCHECK_INTERVAL)
|
||||||
|
.end(function (error, res) {
|
||||||
|
if (error && !error.response) {
|
||||||
|
debugApp(app, 'not alive (network error): %s', error.message);
|
||||||
|
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||||
|
} else if (res.statusCode >= 400) { // 2xx and 3xx are ok
|
||||||
|
debugApp(app, 'not alive : %s', error || res.status);
|
||||||
|
setHealth(app, appdb.HEALTH_UNHEALTHY, callback);
|
||||||
|
} else {
|
||||||
|
setHealth(app, appdb.HEALTH_HEALTHY, callback);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processApps(callback) {
|
||||||
|
appdb.getAll(function (error, apps) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
async.each(apps, checkAppHealth, function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
|
||||||
|
var alive = apps
|
||||||
|
.filter(function (a) { return a.installationState === appdb.ISTATE_INSTALLED && a.runState === appdb.RSTATE_RUNNING && a.health === appdb.HEALTH_HEALTHY; })
|
||||||
|
.map(function (a) { return (a.location || 'naked_domain') + '|' + a.manifest.id; }).join(', ');
|
||||||
|
|
||||||
|
debug('apps alive: [%s]', alive);
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
processApps(function (error) {
|
||||||
|
if (error) console.error(error);
|
||||||
|
|
||||||
|
gRunTimeout = setTimeout(run, HEALTHCHECK_INTERVAL);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
OOM can be tested using stress tool like so:
|
||||||
|
docker run -ti -m 100M cloudron/base:0.3.3 /bin/bash
|
||||||
|
apt-get update && apt-get install stress
|
||||||
|
stress --vm 1 --vm-bytes 200M --vm-hang 0
|
||||||
|
*/
|
||||||
|
function processDockerEvents() {
|
||||||
|
// note that for some reason, the callback is called only on the first event
|
||||||
|
debug('Listening for docker events');
|
||||||
|
docker.getEvents({ filters: JSON.stringify({ event: [ 'oom' ] }) }, function (error, stream) {
|
||||||
|
if (error) return console.error(error);
|
||||||
|
|
||||||
|
gDockerEventStream = stream;
|
||||||
|
|
||||||
|
stream.setEncoding('utf8');
|
||||||
|
stream.on('data', function (data) {
|
||||||
|
var ev = JSON.parse(data);
|
||||||
|
debug('Container ' + ev.id + ' went OOM');
|
||||||
|
appdb.getByContainerId(ev.id, function (error, app) {
|
||||||
|
var program = error || !app.appStoreId ? ev.id : app.appStoreId;
|
||||||
|
var context = JSON.stringify(ev);
|
||||||
|
if (app) context = context + '\n\n' + JSON.stringify(app, null, 4) + '\n';
|
||||||
|
|
||||||
|
debug('OOM Context: %s', context);
|
||||||
|
|
||||||
|
// do not send mails for dev apps
|
||||||
|
if (error || app.appStoreId !== '') mailer.unexpectedExit(program, context); // app can be null if it's an addon crash
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('error', function (error) {
|
||||||
|
console.error('Error reading docker events', error);
|
||||||
|
gDockerEventStream = null; // TODO: reconnect?
|
||||||
|
});
|
||||||
|
|
||||||
|
stream.on('end', function () {
|
||||||
|
console.error('Docker event stream ended');
|
||||||
|
gDockerEventStream = null; // TODO: reconnect?
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function start(callback) {
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
debug('Starting apphealthmonitor');
|
||||||
|
|
||||||
|
processDockerEvents();
|
||||||
|
|
||||||
|
run();
|
||||||
|
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop(callback) {
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
clearTimeout(gRunTimeout);
|
||||||
|
gDockerEventStream.end();
|
||||||
|
|
||||||
|
callback();
|
||||||
|
}
|
||||||
829
src/apps.js
829
src/apps.js
File diff suppressed because it is too large
Load Diff
635
src/apptask.js
635
src/apptask.js
File diff suppressed because it is too large
Load Diff
42
src/auth.js
42
src/auth.js
@@ -10,8 +10,9 @@ exports = module.exports = {
|
|||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
BasicStrategy = require('passport-http').BasicStrategy,
|
BasicStrategy = require('passport-http').BasicStrategy,
|
||||||
BearerStrategy = require('passport-http-bearer').Strategy,
|
BearerStrategy = require('passport-http-bearer').Strategy,
|
||||||
clientdb = require('./clientdb'),
|
clients = require('./clients'),
|
||||||
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy,
|
||||||
|
ClientsError = clients.ClientsError,
|
||||||
DatabaseError = require('./databaseerror'),
|
DatabaseError = require('./databaseerror'),
|
||||||
debug = require('debug')('box:auth'),
|
debug = require('debug')('box:auth'),
|
||||||
LocalStrategy = require('passport-local').Strategy,
|
LocalStrategy = require('passport-local').Strategy,
|
||||||
@@ -19,7 +20,6 @@ var assert = require('assert'),
|
|||||||
passport = require('passport'),
|
passport = require('passport'),
|
||||||
tokendb = require('./tokendb'),
|
tokendb = require('./tokendb'),
|
||||||
user = require('./user'),
|
user = require('./user'),
|
||||||
userdb = require('./userdb'),
|
|
||||||
UserError = user.UserError,
|
UserError = user.UserError,
|
||||||
_ = require('underscore');
|
_ = require('underscore');
|
||||||
|
|
||||||
@@ -27,11 +27,11 @@ function initialize(callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
passport.serializeUser(function (user, callback) {
|
passport.serializeUser(function (user, callback) {
|
||||||
callback(null, user.username);
|
callback(null, user.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
passport.deserializeUser(function(username, callback) {
|
passport.deserializeUser(function(userId, callback) {
|
||||||
userdb.get(username, function (error, result) {
|
user.get(userId, function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
var md5 = crypto.createHash('md5').update(result.email.toLowerCase()).digest('hex');
|
var md5 = crypto.createHash('md5').update(result.email.toLowerCase()).digest('hex');
|
||||||
@@ -43,7 +43,7 @@ function initialize(callback) {
|
|||||||
|
|
||||||
passport.use(new LocalStrategy(function (username, password, callback) {
|
passport.use(new LocalStrategy(function (username, password, callback) {
|
||||||
if (username.indexOf('@') === -1) {
|
if (username.indexOf('@') === -1) {
|
||||||
user.verify(username, password, function (error, result) {
|
user.verifyWithUsername(username, password, function (error, result) {
|
||||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||||
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
|
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
@@ -66,14 +66,14 @@ function initialize(callback) {
|
|||||||
debug('BasicStrategy: detected client id %s instead of username:password', username);
|
debug('BasicStrategy: detected client id %s instead of username:password', username);
|
||||||
// username is actually client id here
|
// username is actually client id here
|
||||||
// password is client secret
|
// password is client secret
|
||||||
clientdb.get(username, function (error, client) {
|
clients.get(username, function (error, client) {
|
||||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
if (client.clientSecret != password) return callback(null, false);
|
if (client.clientSecret != password) return callback(null, false);
|
||||||
return callback(null, client);
|
return callback(null, client);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
user.verify(username, password, function (error, result) {
|
user.verifyWithUsername(username, password, function (error, result) {
|
||||||
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||||
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
|
if (error && error.reason === UserError.WRONG_PASSWORD) return callback(null, false);
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
@@ -84,8 +84,8 @@ function initialize(callback) {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
|
passport.use(new ClientPasswordStrategy(function (clientId, clientSecret, callback) {
|
||||||
clientdb.get(clientId, function(error, client) {
|
clients.get(clientId, function(error, client) {
|
||||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
if (error && error.reason === ClientsError.NOT_FOUND) return callback(null, false);
|
||||||
if (error) { return callback(error); }
|
if (error) { return callback(error); }
|
||||||
if (client.clientSecret != clientSecret) { return callback(null, false); }
|
if (client.clientSecret != clientSecret) { return callback(null, false); }
|
||||||
return callback(null, client);
|
return callback(null, client);
|
||||||
@@ -100,29 +100,11 @@ function initialize(callback) {
|
|||||||
// scopes here can define what capabilities that token carries
|
// scopes here can define what capabilities that token carries
|
||||||
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
|
// passport put the 'info' object into req.authInfo, where we can further validate the scopes
|
||||||
var info = { scope: token.scope };
|
var info = { scope: token.scope };
|
||||||
var tokenType;
|
|
||||||
|
|
||||||
if (token.identifier.indexOf(tokendb.PREFIX_DEV) === 0) {
|
user.get(token.identifier, function (error, user) {
|
||||||
token.identifier = token.identifier.slice(tokendb.PREFIX_DEV.length);
|
|
||||||
tokenType = tokendb.TYPE_DEV;
|
|
||||||
} else if (token.identifier.indexOf(tokendb.PREFIX_APP) === 0) {
|
|
||||||
tokenType = tokendb.TYPE_APP;
|
|
||||||
return callback(null, { id: token.identifier.slice(tokendb.PREFIX_APP.length), tokenType: tokenType }, info);
|
|
||||||
} else if (token.identifier.indexOf(tokendb.PREFIX_USER) === 0) {
|
|
||||||
tokenType = tokendb.TYPE_USER;
|
|
||||||
token.identifier = token.identifier.slice(tokendb.PREFIX_USER.length);
|
|
||||||
} else {
|
|
||||||
// legacy tokens assuming a user access token
|
|
||||||
tokenType = tokendb.TYPE_USER;
|
|
||||||
}
|
|
||||||
|
|
||||||
userdb.get(token.identifier, function (error, user) {
|
|
||||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, false);
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
// amend the tokenType of the token owner
|
|
||||||
user.tokenType = tokenType;
|
|
||||||
|
|
||||||
callback(null, user, info);
|
callback(null, user, info);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
114
src/backupdb.js
Normal file
114
src/backupdb.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var assert = require('assert'),
|
||||||
|
database = require('./database.js'),
|
||||||
|
DatabaseError = require('./databaseerror.js'),
|
||||||
|
util = require('util');
|
||||||
|
|
||||||
|
var BACKUPS_FIELDS = [ 'id', 'creationTime', 'version', 'type', 'dependsOn', 'state', ];
|
||||||
|
|
||||||
|
exports = module.exports = {
|
||||||
|
add: add,
|
||||||
|
getPaged: getPaged,
|
||||||
|
get: get,
|
||||||
|
del: del,
|
||||||
|
getByAppIdPaged: getByAppIdPaged,
|
||||||
|
|
||||||
|
_clear: clear,
|
||||||
|
|
||||||
|
BACKUP_TYPE_APP: 'app',
|
||||||
|
BACKUP_TYPE_BOX: 'box',
|
||||||
|
|
||||||
|
BACKUP_STATE_NORMAL: 'normal', // should rename to created to avoid listing in UI?
|
||||||
|
};
|
||||||
|
|
||||||
|
function postProcess(result) {
|
||||||
|
assert.strictEqual(typeof result, 'object');
|
||||||
|
|
||||||
|
result.dependsOn = result.dependsOn ? result.dependsOn.split(',') : [ ];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPaged(page, perPage, callback) {
|
||||||
|
assert(typeof page === 'number' && page > 0);
|
||||||
|
assert(typeof perPage === 'number' && perPage > 0);
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
database.query('SELECT ' + BACKUPS_FIELDS + ' FROM backups WHERE type = ? AND state = ? ORDER BY creationTime DESC LIMIT ?,?',
|
||||||
|
[ exports.BACKUP_TYPE_BOX, exports.BACKUP_STATE_NORMAL, (page-1)*perPage, perPage ], function (error, results) {
|
||||||
|
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
results.forEach(function (result) { postProcess(result); });
|
||||||
|
|
||||||
|
callback(null, results);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getByAppIdPaged(page, perPage, appId, callback) {
|
||||||
|
assert(typeof page === 'number' && page > 0);
|
||||||
|
assert(typeof perPage === 'number' && perPage > 0);
|
||||||
|
assert.strictEqual(typeof appId, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
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, 'appbackup\\_' + appId + '\\_%', (page-1)*perPage, perPage ], function (error, results) {
|
||||||
|
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
results.forEach(function (result) { postProcess(result); });
|
||||||
|
|
||||||
|
callback(null, results);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(id, callback) {
|
||||||
|
assert.strictEqual(typeof id, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
postProcess(result[0]);
|
||||||
|
|
||||||
|
callback(null, result[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 callback, 'function');
|
||||||
|
|
||||||
|
var creationTime = backup.creationTime || new Date(); // allow tests to set the time
|
||||||
|
|
||||||
|
database.query('INSERT INTO backups (id, version, type, creationTime, state, dependsOn) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
|
[ backup.id, backup.version, backup.type, creationTime, exports.BACKUP_STATE_NORMAL, backup.dependsOn.join(',') ],
|
||||||
|
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));
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function del(id, callback) {
|
||||||
|
assert.strictEqual(typeof id, 'string');
|
||||||
|
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));
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
479
src/backups.js
479
src/backups.js
@@ -3,17 +3,56 @@
|
|||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
BackupsError: BackupsError,
|
BackupsError: BackupsError,
|
||||||
|
|
||||||
getAllPaged: getAllPaged,
|
getPaged: getPaged,
|
||||||
|
getByAppIdPaged: getByAppIdPaged,
|
||||||
|
|
||||||
getBackupUrl: getBackupUrl,
|
getRestoreUrl: getRestoreUrl,
|
||||||
getRestoreUrl: getRestoreUrl
|
getRestoreConfig: getRestoreConfig,
|
||||||
|
|
||||||
|
ensureBackup: ensureBackup,
|
||||||
|
|
||||||
|
backup: backup,
|
||||||
|
backupApp: backupApp,
|
||||||
|
restoreApp: restoreApp,
|
||||||
|
|
||||||
|
backupBoxAndApps: backupBoxAndApps
|
||||||
};
|
};
|
||||||
|
|
||||||
var assert = require('assert'),
|
var addons = require('./addons.js'),
|
||||||
|
appdb = require('./appdb.js'),
|
||||||
|
apps = require('./apps.js'),
|
||||||
|
async = require('async'),
|
||||||
|
assert = require('assert'),
|
||||||
|
backupdb = require('./backupdb.js'),
|
||||||
|
caas = require('./storage/caas.js'),
|
||||||
config = require('./config.js'),
|
config = require('./config.js'),
|
||||||
|
DatabaseError = require('./databaseerror.js'),
|
||||||
debug = require('debug')('box:backups'),
|
debug = require('debug')('box:backups'),
|
||||||
|
eventlog = require('./eventlog.js'),
|
||||||
|
locker = require('./locker.js'),
|
||||||
|
path = require('path'),
|
||||||
|
paths = require('./paths.js'),
|
||||||
|
progress = require('./progress.js'),
|
||||||
|
s3 = require('./storage/s3.js'),
|
||||||
|
safe = require('safetydance'),
|
||||||
|
shell = require('./shell.js'),
|
||||||
|
settings = require('./settings.js'),
|
||||||
superagent = require('superagent'),
|
superagent = require('superagent'),
|
||||||
util = require('util');
|
util = require('util'),
|
||||||
|
webhooks = require('./webhooks.js');
|
||||||
|
|
||||||
|
var BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
|
||||||
|
BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'),
|
||||||
|
RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh');
|
||||||
|
|
||||||
|
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||||
|
|
||||||
|
function debugApp(app, args) {
|
||||||
|
assert(!app || typeof app === 'object');
|
||||||
|
|
||||||
|
var prefix = app ? app.location : '(no app)';
|
||||||
|
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
||||||
|
}
|
||||||
|
|
||||||
function BackupsError(reason, errorOrMessage) {
|
function BackupsError(reason, errorOrMessage) {
|
||||||
assert.strictEqual(typeof reason, 'string');
|
assert.strictEqual(typeof reason, 'string');
|
||||||
@@ -36,60 +75,422 @@ function BackupsError(reason, errorOrMessage) {
|
|||||||
util.inherits(BackupsError, Error);
|
util.inherits(BackupsError, Error);
|
||||||
BackupsError.EXTERNAL_ERROR = 'external error';
|
BackupsError.EXTERNAL_ERROR = 'external error';
|
||||||
BackupsError.INTERNAL_ERROR = 'internal error';
|
BackupsError.INTERNAL_ERROR = 'internal error';
|
||||||
|
BackupsError.BAD_STATE = 'bad state';
|
||||||
|
BackupsError.MISSING_CREDENTIALS = 'missing credentials';
|
||||||
|
|
||||||
function getAllPaged(page, perPage, callback) {
|
// choose which storage backend we use for test purpose we use s3
|
||||||
assert.strictEqual(typeof page, 'number');
|
function api(provider) {
|
||||||
assert.strictEqual(typeof perPage, 'number');
|
switch (provider) {
|
||||||
|
case 'caas': return caas;
|
||||||
|
case 's3': return s3;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPaged(page, perPage, callback) {
|
||||||
|
assert(typeof page === 'number' && page > 0);
|
||||||
|
assert(typeof perPage === 'number' && perPage > 0);
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backups';
|
backupdb.getPaged(page, perPage, function (error, results) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
superagent.get(url).query({ token: config.token() }).end(function (error, result) {
|
callback(null, results);
|
||||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
|
||||||
if (result.statusCode !== 200) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
|
|
||||||
if (!result.body || !util.isArray(result.body.backups)) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response'));
|
|
||||||
|
|
||||||
// [ { creationTime, boxVersion, restoreKey, dependsOn: [ ] } ] sorted by time (latest first)
|
|
||||||
return callback(null, result.body.backups);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBackupUrl(app, appBackupIds, callback) {
|
function getByAppIdPaged(page, perPage, appId, callback) {
|
||||||
assert(!app || typeof app === 'object');
|
assert(typeof page === 'number' && page > 0);
|
||||||
assert(!appBackupIds || util.isArray(appBackupIds));
|
assert(typeof perPage === 'number' && perPage > 0);
|
||||||
|
assert.strictEqual(typeof appId, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/backupurl';
|
backupdb.getByAppIdPaged(page, perPage, appId, function (error, results) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
var data = {
|
callback(null, results);
|
||||||
boxVersion: config.version(),
|
|
||||||
appId: app ? app.id : null,
|
|
||||||
appVersion: app ? app.manifest.version : null,
|
|
||||||
appBackupIds: appBackupIds
|
|
||||||
};
|
|
||||||
|
|
||||||
superagent.put(url).query({ token: config.token() }).send(data).end(function (error, result) {
|
|
||||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
|
||||||
if (result.statusCode !== 201) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
|
|
||||||
if (!result.body || !result.body.url) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response'));
|
|
||||||
|
|
||||||
return callback(null, result.body);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBoxBackupCredentials(appBackupIds, callback) {
|
||||||
|
assert(util.isArray(appBackupIds));
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var now = new Date();
|
||||||
|
var filebase = util.format('backup_%s-v%s', now.toISOString(), config.version());
|
||||||
|
var filename = filebase + '.tar.gz';
|
||||||
|
|
||||||
|
settings.getBackupConfig(function (error, backupConfig) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
api(backupConfig.provider).getBackupCredentials(backupConfig, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
result.id = filename;
|
||||||
|
result.s3Url = 's3://' + backupConfig.bucket + '/' + backupConfig.prefix + '/' + filename;
|
||||||
|
result.backupKey = backupConfig.key;
|
||||||
|
|
||||||
|
debug('getBoxBackupCredentials: %j', result);
|
||||||
|
|
||||||
|
callback(null, result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAppBackupCredentials(app, manifest, callback) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert(manifest && typeof manifest === 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var now = new Date();
|
||||||
|
var filebase = util.format('appbackup_%s_%s-v%s', app.id, now.toISOString(), manifest.version);
|
||||||
|
var configFilename = filebase + '.json', dataFilename = filebase + '.tar.gz';
|
||||||
|
|
||||||
|
settings.getBackupConfig(function (error, backupConfig) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
api(backupConfig.provider).getBackupCredentials(backupConfig, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
result.id = dataFilename;
|
||||||
|
result.s3ConfigUrl = 's3://' + backupConfig.bucket + '/' + backupConfig.prefix + '/' + configFilename;
|
||||||
|
result.s3DataUrl = 's3://' + backupConfig.bucket + '/' + backupConfig.prefix + '/' + dataFilename;
|
||||||
|
result.backupKey = backupConfig.key;
|
||||||
|
|
||||||
|
debug('getAppBackupCredentials: %j', result);
|
||||||
|
|
||||||
|
callback(null, result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
|
||||||
|
function getRestoreConfig(backupId, callback) {
|
||||||
|
assert.strictEqual(typeof backupId, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var configFile = backupId.replace(/\.tar\.gz$/, '.json');
|
||||||
|
|
||||||
|
settings.getBackupConfig(function (error, backupConfig) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
api(backupConfig.provider).getRestoreUrl(backupConfig, configFile, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
superagent.get(result.url).buffer(true).end(function (error, response) {
|
||||||
|
if (error && !error.response) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||||
|
if (response.statusCode !== 200) return callback(new Error('Invalid response code when getting config.json : ' + response.statusCode));
|
||||||
|
|
||||||
|
var config = safe.JSON.parse(response.text);
|
||||||
|
if (!config) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Error in config:' + safe.error.message));
|
||||||
|
|
||||||
|
return callback(null, config);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// backupId is the s3 filename. appbackup_%s_%s-v%s.tar.gz
|
||||||
function getRestoreUrl(backupId, callback) {
|
function getRestoreUrl(backupId, callback) {
|
||||||
assert.strictEqual(typeof backupId, 'string');
|
assert.strictEqual(typeof backupId, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/restoreurl';
|
settings.getBackupConfig(function (error, backupConfig) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
superagent.put(url).query({ token: config.token(), backupId: backupId }).end(function (error, result) {
|
api(backupConfig.provider).getRestoreUrl(backupConfig, backupId, function (error, result) {
|
||||||
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
if (error) return callback(error);
|
||||||
if (result.statusCode !== 201) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, result.text));
|
|
||||||
if (!result.body || !result.body.url) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, 'Unexpected response'));
|
|
||||||
|
|
||||||
return callback(null, result.body);
|
var obj = {
|
||||||
|
id: backupId,
|
||||||
|
url: result.url,
|
||||||
|
backupKey: backupConfig.key
|
||||||
|
};
|
||||||
|
|
||||||
|
debug('getRestoreUrl: id:%s url:%s backupKey:%s', obj.id, obj.url, obj.backupKey);
|
||||||
|
|
||||||
|
callback(null, obj);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyLastBackup(app, manifest, callback) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert.strictEqual(typeof app.lastBackupId, 'string');
|
||||||
|
assert(manifest && typeof manifeset === 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var now = new Date();
|
||||||
|
var toFilenameArchive = util.format('appbackup_%s_%s-v%s.tar.gz', app.id, now.toISOString(), manifest.version);
|
||||||
|
var toFilenameConfig = util.format('appbackup_%s_%s-v%s.json', app.id, now.toISOString(), manifest.version);
|
||||||
|
|
||||||
|
settings.getBackupConfig(function (error, backupConfig) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
debug('copyLastBackup: copying archive %s to %s', app.lastBackupId, toFilenameArchive);
|
||||||
|
|
||||||
|
api(backupConfig.provider).copyObject(backupConfig, app.lastBackupId, toFilenameArchive, function (error) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
// TODO change that logic by adjusting app.lastBackupId to not contain the file type
|
||||||
|
var configFileId = app.lastBackupId.slice(0, -'.tar.gz'.length) + '.json';
|
||||||
|
|
||||||
|
debug('copyLastBackup: copying config %s to %s', configFileId, toFilenameConfig);
|
||||||
|
|
||||||
|
api(backupConfig.provider).copyObject(backupConfig, configFileId, toFilenameConfig, function (error) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
return callback(null, toFilenameArchive);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function backupBoxWithAppBackupIds(appBackupIds, callback) {
|
||||||
|
assert(util.isArray(appBackupIds));
|
||||||
|
|
||||||
|
getBoxBackupCredentials(appBackupIds, function (error, result) {
|
||||||
|
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message));
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
debug('backupBoxWithAppBackupIds: %j', result);
|
||||||
|
|
||||||
|
var args = [ result.s3Url, result.accessKeyId, result.secretAccessKey, result.sessionToken, result.region, result.backupKey ];
|
||||||
|
|
||||||
|
shell.sudo('backupBox', [ BACKUP_BOX_CMD ].concat(args), function (error) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
debug('backupBoxWithAppBackupIds: success');
|
||||||
|
|
||||||
|
backupdb.add({ id: result.id, version: config.version(), type: backupdb.BACKUP_TYPE_BOX, dependsOn: appBackupIds }, function (error) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
webhooks.backupDone(result.id, null /* app */, appBackupIds, function (error) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
callback(null, result.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function expects you to have a lock
|
||||||
|
// function backupBox(callback) {
|
||||||
|
// apps.getAll(function (error, allApps) {
|
||||||
|
// if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
//
|
||||||
|
// var appBackupIds = allApps.map(function (app) { return app.lastBackupId; });
|
||||||
|
// appBackupIds = appBackupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
|
||||||
|
//
|
||||||
|
// backupBoxWithAppBackupIds(appBackupIds, callback);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
function canBackupApp(app) {
|
||||||
|
// only backup apps that are installed or pending configure or called from apptask. Rest of them are in some
|
||||||
|
// state not good for consistent backup (i.e addons may not have been setup completely)
|
||||||
|
return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) ||
|
||||||
|
app.installationState === appdb.ISTATE_PENDING_CONFIGURE ||
|
||||||
|
app.installationState === appdb.ISTATE_PENDING_BACKUP || // called from apptask
|
||||||
|
app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask
|
||||||
|
}
|
||||||
|
|
||||||
|
// set the 'creation' date of lastBackup so that the backup persists across time based archival rules
|
||||||
|
// s3 does not allow changing creation time, so copying the last backup is easy way out for now
|
||||||
|
function reuseOldAppBackup(app, manifest, callback) {
|
||||||
|
assert.strictEqual(typeof app.lastBackupId, 'string');
|
||||||
|
assert(manifest && typeof manifest === 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
copyLastBackup(app, manifest, function (error, newBackupId) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
debugApp(app, 'reuseOldAppBackup: reused old backup %s as %s', app.lastBackupId, newBackupId);
|
||||||
|
|
||||||
|
callback(null, newBackupId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewAppBackup(app, manifest, callback) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert(manifest && typeof manifest === 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
getAppBackupCredentials(app, manifest, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
debugApp(app, 'createNewAppBackup: backup url:%s backup config url:%s', result.s3DataUrl, result.s3ConfigUrl);
|
||||||
|
|
||||||
|
var args = [ app.id, result.s3ConfigUrl, result.s3DataUrl, result.accessKeyId, result.secretAccessKey,
|
||||||
|
result.sessionToken, result.region, result.backupKey ];
|
||||||
|
|
||||||
|
async.series([
|
||||||
|
addons.backupAddons.bind(null, app, manifest.addons),
|
||||||
|
shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD ].concat(args))
|
||||||
|
], function (error) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
debugApp(app, 'createNewAppBackup: %s done', result.id);
|
||||||
|
|
||||||
|
backupdb.add({ id: result.id, version: manifest.version, type: backupdb.BACKUP_TYPE_APP, dependsOn: [ ] }, function (error) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
callback(null, result.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRestorePoint(appId, lastBackupId, callback) {
|
||||||
|
assert.strictEqual(typeof appId, 'string');
|
||||||
|
assert.strictEqual(typeof lastBackupId, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
appdb.update(appId, { lastBackupId: lastBackupId }, function (error) {
|
||||||
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, 'No such app'));
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function backupApp(app, manifest, callback) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert(manifest && typeof manifest === 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var backupFunction;
|
||||||
|
|
||||||
|
if (!canBackupApp(app)) {
|
||||||
|
if (!app.lastBackupId) {
|
||||||
|
debugApp(app, 'backupApp: cannot backup app');
|
||||||
|
return callback(new BackupsError(BackupsError.BAD_STATE, 'App not healthy and never backed up previously'));
|
||||||
|
}
|
||||||
|
|
||||||
|
backupFunction = reuseOldAppBackup.bind(null, app, manifest);
|
||||||
|
} else {
|
||||||
|
var appConfig = apps.getAppConfig(app);
|
||||||
|
appConfig.manifest = manifest;
|
||||||
|
backupFunction = createNewAppBackup.bind(null, app, manifest);
|
||||||
|
|
||||||
|
if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) {
|
||||||
|
return callback(safe.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backupFunction(function (error, backupId) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
debugApp(app, 'backupApp: successful id:%s', backupId);
|
||||||
|
|
||||||
|
setRestorePoint(app.id, backupId, function (error) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
return callback(null, backupId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// this function expects you to have a lock
|
||||||
|
function backupBoxAndApps(auditSource, callback) {
|
||||||
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
|
|
||||||
|
callback = callback || NOOP_CALLBACK;
|
||||||
|
|
||||||
|
eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { });
|
||||||
|
|
||||||
|
apps.getAll(function (error, allApps) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
var processed = 0;
|
||||||
|
var step = 100/(allApps.length+1);
|
||||||
|
|
||||||
|
progress.set(progress.BACKUP, processed, '');
|
||||||
|
|
||||||
|
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
|
||||||
|
++processed;
|
||||||
|
|
||||||
|
backupApp(app, app.manifest, function (error, backupId) {
|
||||||
|
if (error && error.reason !== BackupsError.BAD_STATE) {
|
||||||
|
debugApp(app, 'Unable to backup', error);
|
||||||
|
return iteratorCallback(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
|
||||||
|
|
||||||
|
iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up
|
||||||
|
});
|
||||||
|
}, function appsBackedUp(error, backupIds) {
|
||||||
|
if (error) {
|
||||||
|
progress.set(progress.BACKUP, 100, error.message);
|
||||||
|
return callback(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up
|
||||||
|
|
||||||
|
backupBoxWithAppBackupIds(backupIds, function (error, filename) {
|
||||||
|
progress.set(progress.BACKUP, 100, error ? error.message : '');
|
||||||
|
|
||||||
|
eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { errorMessage: error ? error.message : null, filename: filename });
|
||||||
|
|
||||||
|
callback(error, filename);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function backup(auditSource, callback) {
|
||||||
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var error = locker.lock(locker.OP_FULL_BACKUP);
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.BAD_STATE, error.message));
|
||||||
|
|
||||||
|
progress.set(progress.BACKUP, 0, 'Starting'); // ensure tools can 'wait' on progress
|
||||||
|
|
||||||
|
backupBoxAndApps(auditSource, function (error) { // start the backup operation in the background
|
||||||
|
if (error) console.error('backup failed.', error);
|
||||||
|
|
||||||
|
locker.unlock(locker.OP_FULL_BACKUP);
|
||||||
|
});
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureBackup(auditSource, callback) {
|
||||||
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
|
|
||||||
|
getPaged(1, 1, function (error, backups) {
|
||||||
|
if (error) {
|
||||||
|
debug('Unable to list backups', error);
|
||||||
|
return callback(error); // no point trying to backup if appstore is down
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
|
||||||
|
debug('Previous backup was %j, no need to backup now', backups[0]);
|
||||||
|
return callback(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
backup(auditSource, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreApp(app, addonsToRestore, backupId, callback) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert.strictEqual(typeof addonsToRestore, 'object');
|
||||||
|
assert.strictEqual(typeof backupId, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
assert(app.lastBackupId);
|
||||||
|
|
||||||
|
getRestoreUrl(backupId, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
debugApp(app, 'restoreApp: restoreUrl:%s', result.url);
|
||||||
|
|
||||||
|
shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ], function (error) {
|
||||||
|
if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
addons.restoreAddons(app, addonsToRestore, callback);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
467
src/cert/acme.js
Normal file
467
src/cert/acme.js
Normal file
@@ -0,0 +1,467 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var assert = require('assert'),
|
||||||
|
async = require('async'),
|
||||||
|
crypto = require('crypto'),
|
||||||
|
debug = require('debug')('box:cert/acme'),
|
||||||
|
fs = require('fs'),
|
||||||
|
parseLinks = require('parse-links'),
|
||||||
|
path = require('path'),
|
||||||
|
paths = require('../paths.js'),
|
||||||
|
safe = require('safetydance'),
|
||||||
|
superagent = require('superagent'),
|
||||||
|
ursa = require('ursa'),
|
||||||
|
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.0.1-July-27-2015.pdf';
|
||||||
|
|
||||||
|
exports = module.exports = {
|
||||||
|
getCertificate: getCertificate
|
||||||
|
};
|
||||||
|
|
||||||
|
function AcmeError(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(AcmeError, Error);
|
||||||
|
AcmeError.INTERNAL_ERROR = 'Internal Error';
|
||||||
|
AcmeError.EXTERNAL_ERROR = 'External Error';
|
||||||
|
AcmeError.ALREADY_EXISTS = 'Already Exists';
|
||||||
|
AcmeError.NOT_COMPLETED = 'Not Completed';
|
||||||
|
AcmeError.FORBIDDEN = 'Forbidden';
|
||||||
|
|
||||||
|
// http://jose.readthedocs.org/en/latest/
|
||||||
|
// https://www.ietf.org/proceedings/92/slides/slides-92-acme-1.pdf
|
||||||
|
// https://community.letsencrypt.org/t/list-of-client-implementations/2103
|
||||||
|
|
||||||
|
function Acme(options) {
|
||||||
|
assert.strictEqual(typeof options, 'object');
|
||||||
|
|
||||||
|
this.caOrigin = options.prod ? CA_PROD : CA_STAGING;
|
||||||
|
this.accountKeyPem = null; // Buffer
|
||||||
|
this.email = options.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
Acme.prototype.getNonce = function (callback) {
|
||||||
|
superagent.get(this.caOrigin + '/directory', function (error, response) {
|
||||||
|
if (error) 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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
Acme.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 privateKey = ursa.createPrivateKey(this.accountKeyPem);
|
||||||
|
|
||||||
|
var header = {
|
||||||
|
alg: 'RS256',
|
||||||
|
jwk: {
|
||||||
|
e: b64(privateKey.getExponent()),
|
||||||
|
kty: 'RSA',
|
||||||
|
n: b64(privateKey.getModulus())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = ursa.createSigner('sha256');
|
||||||
|
signer.update(protected64 + '.' + payload64, 'utf8');
|
||||||
|
var signature64 = urlBase64Encode(signer.sign(privateKey, '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)).end(function (error, res) {
|
||||||
|
if (error && !error.response) return callback(error); // network errors
|
||||||
|
|
||||||
|
callback(null, res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Acme.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 AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||||
|
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to update contact. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||||
|
|
||||||
|
debug('updateContact: contact of user updated to %s', that.email);
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Acme.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 AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering user: ' + error.message));
|
||||||
|
if (result.statusCode === 409) return that.updateContact(result.headers.location, callback); // already exists
|
||||||
|
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||||
|
|
||||||
|
debug('registerUser: registered user %s', that.email);
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Acme.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 AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when registering domain: ' + error.message));
|
||||||
|
if (result.statusCode === 403) return callback(new AcmeError(AcmeError.FORBIDDEN, result.body.detail));
|
||||||
|
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to register user. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||||
|
|
||||||
|
debug('registerDomain: registered %s', domain);
|
||||||
|
|
||||||
|
callback(null, result.body);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Acme.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 privateKey = ursa.createPrivateKey(this.accountKeyPem);
|
||||||
|
|
||||||
|
var jwk = {
|
||||||
|
e: b64(privateKey.getExponent()),
|
||||||
|
kty: 'RSA',
|
||||||
|
n: b64(privateKey.getModulus())
|
||||||
|
};
|
||||||
|
|
||||||
|
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 AcmeError(AcmeError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Acme.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 AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when notifying challenge: ' + error.message));
|
||||||
|
if (result.statusCode !== 202) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to notify challenge. Expecting 202, got %s %s', result.statusCode, result.text)));
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Acme.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).end(function (error, result) {
|
||||||
|
if (error && !error.response) {
|
||||||
|
debug('waitForChallenge: network error getting uri %s', challenge.uri);
|
||||||
|
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, error.message)); // network error
|
||||||
|
}
|
||||||
|
if (result.statusCode !== 202) {
|
||||||
|
debug('waitForChallenge: invalid response code getting uri %s', result.statusCode);
|
||||||
|
return retryCallback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Bad response code:' + result.statusCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('waitForChallenge: status is "%s"', result.body.status);
|
||||||
|
|
||||||
|
if (result.body.status === 'pending') return retryCallback(new AcmeError(AcmeError.NOT_COMPLETED));
|
||||||
|
else if (result.body.status === 'valid') return retryCallback();
|
||||||
|
else return retryCallback(new AcmeError(AcmeError.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
|
||||||
|
Acme.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 AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when signing certificate: ' + error.message));
|
||||||
|
// 429 means we reached the cert limit for this domain
|
||||||
|
if (result.statusCode !== 201) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to sign certificate. Expecting 201, got %s %s', result.statusCode, result.text)));
|
||||||
|
|
||||||
|
var certUrl = result.headers.location;
|
||||||
|
|
||||||
|
if (!certUrl) return callback(new AcmeError(AcmeError.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);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Acme.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');
|
||||||
|
var execSync = safe.child_process.execSync;
|
||||||
|
|
||||||
|
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 AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||||
|
if (!safe.fs.writeFileSync(privateKeyFile, key)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||||
|
|
||||||
|
debug('createKeyAndCsr: key file saved at %s', privateKeyFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
var csrDer = execSync(util.format('openssl req -new -key %s -outform DER -subj /CN=%s', privateKeyFile, domain));
|
||||||
|
if (!csrDer) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||||
|
if (!safe.fs.writeFileSync(csrFile, csrDer)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error)); // bookkeeping
|
||||||
|
|
||||||
|
debug('createKeyAndCsr: csr file (DER) saved at %s', csrFile);
|
||||||
|
|
||||||
|
callback(null, csrDer);
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: download the chain in a loop following 'up' header
|
||||||
|
Acme.prototype.downloadChain = function (linkHeader, callback) {
|
||||||
|
if (!linkHeader) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Empty link header when downloading certificate chain');
|
||||||
|
|
||||||
|
var linkInfo = parseLinks(linkHeader);
|
||||||
|
if (!linkInfo || !linkInfo.up) return new AcmeError(AcmeError.EXTERNAL_ERROR, 'Failed to parse link header when downloading certificate chain');
|
||||||
|
|
||||||
|
debug('downloadChain: downloading from %s', this.caOrigin + linkInfo.up);
|
||||||
|
|
||||||
|
superagent.get(this.caOrigin + linkInfo.up).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(); });
|
||||||
|
}).end(function (error, result) {
|
||||||
|
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||||
|
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||||
|
|
||||||
|
var chainDer = result.text;
|
||||||
|
var execSync = safe.child_process.execSync;
|
||||||
|
|
||||||
|
var chainPem = execSync('openssl x509 -inform DER -outform PEM', { input: chainDer }); // this is really just base64 encoding with header
|
||||||
|
if (!chainPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||||
|
|
||||||
|
callback(null, chainPem);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Acme.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(); });
|
||||||
|
}).end(function (error, result) {
|
||||||
|
if (error && !error.response) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, 'Network error when downloading certificate'));
|
||||||
|
if (result.statusCode === 202) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, 'Retry not implemented yet'));
|
||||||
|
if (result.statusCode !== 200) return callback(new AcmeError(AcmeError.EXTERNAL_ERROR, util.format('Failed to get cert. Expecting 200, got %s %s', result.statusCode, result.text)));
|
||||||
|
|
||||||
|
var certificateDer = result.text;
|
||||||
|
var execSync = safe.child_process.execSync;
|
||||||
|
|
||||||
|
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 AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||||
|
|
||||||
|
that.downloadChain(result.header['link'], function (error, chainPem) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
var certificateFile = path.join(outdir, domain + '.cert');
|
||||||
|
var fullChainPem = Buffer.concat([certificatePem, chainPem]);
|
||||||
|
if (!safe.fs.writeFileSync(certificateFile, fullChainPem)) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||||
|
|
||||||
|
debug('downloadCertificate: cert file for %s saved at %s', domain, certificateFile);
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Acme.prototype.acmeFlow = function (domain, callback) {
|
||||||
|
assert.strictEqual(typeof domain, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
if (!fs.existsSync(paths.ACME_ACCOUNT_KEY_FILE)) {
|
||||||
|
debug('getCertificate: generating acme account key on first run');
|
||||||
|
this.accountKeyPem = safe.child_process.execSync('openssl genrsa 4096');
|
||||||
|
if (!this.accountKeyPem) return callback(new AcmeError(AcmeError.INTERNAL_ERROR, safe.error));
|
||||||
|
|
||||||
|
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 AcmeError(AcmeError.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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Acme.prototype.getCertificate = function (domain, callback) {
|
||||||
|
assert.strictEqual(typeof domain, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
debug('getCertificate: start acme flow for %s from %s', domain, this.caOrigin);
|
||||||
|
this.acmeFlow(domain, function (error) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
var outdir = paths.APP_CERTS_DIR;
|
||||||
|
callback(null, path.join(outdir, domain + '.cert'), path.join(outdir, domain + '.key'));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function getCertificate(domain, options, callback) {
|
||||||
|
assert.strictEqual(typeof domain, 'string');
|
||||||
|
assert.strictEqual(typeof options, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var acme = new Acme(options || { });
|
||||||
|
acme.getCertificate(domain, callback);
|
||||||
|
}
|
||||||
18
src/cert/caas.js
Normal file
18
src/cert/caas.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports = module.exports = {
|
||||||
|
getCertificate: getCertificate
|
||||||
|
};
|
||||||
|
|
||||||
|
var assert = require('assert'),
|
||||||
|
debug = require('debug')('box:cert/caas.js');
|
||||||
|
|
||||||
|
function getCertificate(domain, options, callback) {
|
||||||
|
assert.strictEqual(typeof domain, 'string');
|
||||||
|
assert.strictEqual(typeof options, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
debug('getCertificate: using fallback certificate', domain);
|
||||||
|
|
||||||
|
return callback(null, 'cert/host.cert', 'cert/host.key');
|
||||||
|
}
|
||||||
336
src/certificates.js
Normal file
336
src/certificates.js
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports = module.exports = {
|
||||||
|
installAdminCertificate: installAdminCertificate,
|
||||||
|
renewAll: renewAll,
|
||||||
|
setFallbackCertificate: setFallbackCertificate,
|
||||||
|
setAdminCertificate: setAdminCertificate,
|
||||||
|
CertificatesError: CertificatesError,
|
||||||
|
validateCertificate: validateCertificate,
|
||||||
|
ensureCertificate: ensureCertificate,
|
||||||
|
getAdminCertificatePath: getAdminCertificatePath
|
||||||
|
};
|
||||||
|
|
||||||
|
var acme = require('./cert/acme.js'),
|
||||||
|
apps = require('./apps.js'),
|
||||||
|
assert = require('assert'),
|
||||||
|
async = require('async'),
|
||||||
|
caas = require('./cert/caas.js'),
|
||||||
|
cloudron = require('./cloudron.js'),
|
||||||
|
config = require('./config.js'),
|
||||||
|
constants = require('./constants.js'),
|
||||||
|
debug = require('debug')('box:src/certificates'),
|
||||||
|
eventlog = require('./eventlog.js'),
|
||||||
|
fs = require('fs'),
|
||||||
|
mailer = require('./mailer.js'),
|
||||||
|
nginx = require('./nginx.js'),
|
||||||
|
path = require('path'),
|
||||||
|
paths = require('./paths.js'),
|
||||||
|
safe = require('safetydance'),
|
||||||
|
settings = require('./settings.js'),
|
||||||
|
sysinfo = require('./sysinfo.js'),
|
||||||
|
user = require('./user.js'),
|
||||||
|
util = require('util'),
|
||||||
|
waitForDns = require('./waitfordns.js'),
|
||||||
|
x509 = require('x509');
|
||||||
|
|
||||||
|
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||||
|
|
||||||
|
function CertificatesError(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(CertificatesError, Error);
|
||||||
|
CertificatesError.INTERNAL_ERROR = 'Internal Error';
|
||||||
|
CertificatesError.INVALID_CERT = 'Invalid certificate';
|
||||||
|
CertificatesError.NOT_FOUND = 'Not Found';
|
||||||
|
|
||||||
|
function getApi(app, callback) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
settings.getTlsConfig(function (error, tlsConfig) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
var api = !app.altDomain && tlsConfig.provider === 'caas' ? caas : acme;
|
||||||
|
|
||||||
|
var options = { };
|
||||||
|
// used by acme backend to determine the LE origin.
|
||||||
|
options.prod = (api === caas) ? !config.isDev() : tlsConfig.provider.match(/.*-prod/) !== null;
|
||||||
|
|
||||||
|
// registering user with an email requires A or MX record (https://github.com/letsencrypt/boulder/issues/1197)
|
||||||
|
// we cannot use admin@fqdn because the user might not have set it up.
|
||||||
|
// we simply update the account with the latest email we have each time when getting letsencrypt certs
|
||||||
|
// https://github.com/ietf-wg-acme/acme/issues/30
|
||||||
|
user.getOwner(function (error, owner) {
|
||||||
|
options.email = error ? 'admin@cloudron.io' : owner.email; // can error if not activated yet
|
||||||
|
|
||||||
|
callback(null, api, options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function installAdminCertificate(callback) {
|
||||||
|
if (cloudron.isConfiguredSync()) return callback();
|
||||||
|
|
||||||
|
settings.getTlsConfig(function (error, tlsConfig) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
if (tlsConfig.provider === 'caas') return callback();
|
||||||
|
|
||||||
|
sysinfo.getIp(function (error, ip) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
waitForDns(config.adminFqdn(), ip, 'A', function (error) {
|
||||||
|
if (error) return callback(error); // this cannot happen because we retry forever
|
||||||
|
|
||||||
|
ensureCertificate({ location: constants.ADMIN_LOCATION }, function (error, certFilePath, keyFilePath) {
|
||||||
|
if (error) { // currently, this can never happen
|
||||||
|
debug('Error obtaining certificate. Proceed anyway', error);
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExpiringSync(certFilePath, hours) {
|
||||||
|
assert.strictEqual(typeof certFilePath, 'string');
|
||||||
|
assert.strictEqual(typeof hours, 'number');
|
||||||
|
|
||||||
|
if (!fs.existsSync(certFilePath)) return 2; // not found
|
||||||
|
|
||||||
|
var result = safe.child_process.spawnSync('/usr/bin/openssl', [ 'x509', '-checkend', String(60 * 60 * hours), '-in', certFilePath ]);
|
||||||
|
|
||||||
|
debug('isExpiringSync: %s %s %s', certFilePath, result.stdout.toString('utf8').trim(), result.status);
|
||||||
|
|
||||||
|
return result.status === 1; // 1 - expired 0 - not expired
|
||||||
|
}
|
||||||
|
|
||||||
|
function renewAll(auditSource, callback) {
|
||||||
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
debug('renewAll: Checking certificates for renewal');
|
||||||
|
|
||||||
|
apps.getAll(function (error, allApps) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
allApps.push({ location: constants.ADMIN_LOCATION }); // inject fake webadmin app
|
||||||
|
|
||||||
|
var expiringApps = [ ];
|
||||||
|
for (var i = 0; i < allApps.length; i++) {
|
||||||
|
var appDomain = allApps[i].altDomain || config.appFqdn(allApps[i].location);
|
||||||
|
var certFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.cert');
|
||||||
|
var keyFilePath = path.join(paths.APP_CERTS_DIR, appDomain + '.key');
|
||||||
|
|
||||||
|
if (!safe.fs.existsSync(keyFilePath)) {
|
||||||
|
debug('renewAll: no existing key file for %s. skipping', appDomain);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isExpiringSync(certFilePath, 24 * 30)) { // expired or not found
|
||||||
|
expiringApps.push(allApps[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('renewAll: %j needs to be renewed', expiringApps.map(function (a) { return a.altDomain || config.appFqdn(a.location); }));
|
||||||
|
|
||||||
|
async.eachSeries(expiringApps, function iterator(app, iteratorCallback) {
|
||||||
|
var domain = app.altDomain || config.appFqdn(app.location);
|
||||||
|
|
||||||
|
getApi(app, function (error, api, apiOptions) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
debug('renewAll: renewing cert for %s with options %j', domain, apiOptions);
|
||||||
|
|
||||||
|
api.getCertificate(domain, apiOptions, function (error) {
|
||||||
|
var certFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
||||||
|
var keyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
||||||
|
|
||||||
|
var errorMessage = error ? error.message : '';
|
||||||
|
eventlog.add(eventlog.ACTION_CERTIFICATE_RENEWAL, auditSource, { domain: domain, errorMessage: errorMessage });
|
||||||
|
mailer.certificateRenewed(domain, errorMessage);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
debug('renewAll: could not renew cert for %s because %s', domain, error);
|
||||||
|
|
||||||
|
// check if we should fallback if we expire in the coming day
|
||||||
|
if (!isExpiringSync(certFilePath, 24 * 1)) return iteratorCallback();
|
||||||
|
|
||||||
|
debug('renewAll: using fallback certs for %s since it expires soon', domain, error);
|
||||||
|
|
||||||
|
certFilePath = 'cert/host.cert';
|
||||||
|
keyFilePath = 'cert/host.key';
|
||||||
|
} else {
|
||||||
|
debug('renewAll: certificate for %s renewed', domain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconfigure and reload nginx. this is required for the case where we got a renewed cert after fallback
|
||||||
|
var configureFunc = app.location === constants.ADMIN_LOCATION ?
|
||||||
|
nginx.configureAdmin.bind(null, certFilePath, keyFilePath)
|
||||||
|
: nginx.configureApp.bind(null, app, certFilePath, keyFilePath);
|
||||||
|
|
||||||
|
configureFunc(function (ignoredError) {
|
||||||
|
if (ignoredError) debug('fallbackExpiredCertificates: error reconfiguring app', ignoredError);
|
||||||
|
|
||||||
|
iteratorCallback(); // move to next app
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// note: https://tools.ietf.org/html/rfc4346#section-7.4.2 (certificate_list) requires that the
|
||||||
|
// servers certificate appears first (and not the intermediate cert)
|
||||||
|
function validateCertificate(cert, key, fqdn) {
|
||||||
|
assert(cert === null || typeof cert === 'string');
|
||||||
|
assert(key === null || typeof key === 'string');
|
||||||
|
assert.strictEqual(typeof fqdn, 'string');
|
||||||
|
|
||||||
|
if (cert === null && key === null) return null;
|
||||||
|
if (!cert && key) return new Error('missing cert');
|
||||||
|
if (cert && !key) return new Error('missing key');
|
||||||
|
|
||||||
|
var content;
|
||||||
|
try {
|
||||||
|
content = x509.parseCert(cert);
|
||||||
|
} catch (e) {
|
||||||
|
return new Error('invalid cert: ' + e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check expiration
|
||||||
|
if (content.notAfter < new Date()) return new Error('cert expired');
|
||||||
|
|
||||||
|
function matchesDomain(domain) {
|
||||||
|
if (domain === fqdn) return true;
|
||||||
|
if (domain.indexOf('*') === 0 && domain.slice(2) === fqdn.slice(fqdn.indexOf('.') + 1)) return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check domain
|
||||||
|
var domains = content.altNames.concat(content.subject.commonName);
|
||||||
|
if (!domains.some(matchesDomain)) return new Error(util.format('cert is not valid for this domain. Expecting %s in %j', fqdn, domains));
|
||||||
|
|
||||||
|
// http://httpd.apache.org/docs/2.0/ssl/ssl_faq.html#verify
|
||||||
|
var certModulus = safe.child_process.execSync('openssl x509 -noout -modulus', { encoding: 'utf8', input: cert });
|
||||||
|
var keyModulus = safe.child_process.execSync('openssl rsa -noout -modulus', { encoding: 'utf8', input: key });
|
||||||
|
if (certModulus !== keyModulus) return new Error('key does not match the cert');
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFallbackCertificate(cert, key, callback) {
|
||||||
|
assert.strictEqual(typeof cert, 'string');
|
||||||
|
assert.strictEqual(typeof key, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var error = validateCertificate(cert, key, '*.' + config.fqdn());
|
||||||
|
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
|
||||||
|
|
||||||
|
// backup the cert
|
||||||
|
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||||
|
if (!safe.fs.writeFileSync(path.join(paths.APP_CERTS_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||||
|
|
||||||
|
// copy over fallback cert
|
||||||
|
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||||
|
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||||
|
|
||||||
|
nginx.reload(function (error) {
|
||||||
|
if (error) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFallbackCertificatePath(callback) {
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
// any user fallback cert is always copied over to nginx cert dir
|
||||||
|
callback(null, path.join(paths.NGINX_CERT_DIR, 'host.cert'), path.join(paths.NGINX_CERT_DIR, 'host.key'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: setting admin cert needs to restart the mail container because it uses admin cert
|
||||||
|
function setAdminCertificate(cert, key, callback) {
|
||||||
|
assert.strictEqual(typeof cert, 'string');
|
||||||
|
assert.strictEqual(typeof key, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var vhost = config.adminFqdn();
|
||||||
|
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
|
||||||
|
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
|
||||||
|
|
||||||
|
var error = validateCertificate(cert, key, vhost);
|
||||||
|
if (error) return callback(new CertificatesError(CertificatesError.INVALID_CERT, error.message));
|
||||||
|
|
||||||
|
// backup the cert
|
||||||
|
if (!safe.fs.writeFileSync(certFilePath, cert)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||||
|
if (!safe.fs.writeFileSync(keyFilePath, key)) return callback(new CertificatesError(CertificatesError.INTERNAL_ERROR, safe.error.message));
|
||||||
|
|
||||||
|
nginx.configureAdmin(certFilePath, keyFilePath, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAdminCertificatePath(callback) {
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var vhost = config.adminFqdn();
|
||||||
|
var certFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.cert');
|
||||||
|
var keyFilePath = path.join(paths.APP_CERTS_DIR, vhost + '.key');
|
||||||
|
|
||||||
|
if (fs.existsSync(certFilePath) && fs.existsSync(keyFilePath)) return callback(null, certFilePath, keyFilePath);
|
||||||
|
|
||||||
|
getFallbackCertificatePath(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCertificate(app, callback) {
|
||||||
|
assert.strictEqual(typeof app, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var domain = app.altDomain || config.appFqdn(app.location);
|
||||||
|
|
||||||
|
// check if user uploaded a specific cert. ideally, we should not mix user certs and automatic certs as we do here...
|
||||||
|
var userCertFilePath = path.join(paths.APP_CERTS_DIR, domain + '.cert');
|
||||||
|
var userKeyFilePath = path.join(paths.APP_CERTS_DIR, domain + '.key');
|
||||||
|
|
||||||
|
if (fs.existsSync(userCertFilePath) && fs.existsSync(userKeyFilePath)) {
|
||||||
|
debug('ensureCertificate: %s. certificate already exists at %s', domain, userKeyFilePath);
|
||||||
|
|
||||||
|
if (!isExpiringSync(userCertFilePath, 24 * 1)) return callback(null, userCertFilePath, userKeyFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('ensureCertificate: %s cert require renewal', domain);
|
||||||
|
|
||||||
|
getApi(app, function (error, api, apiOptions) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
debug('ensureCertificate: getting certificate for %s with options %j', domain, apiOptions);
|
||||||
|
|
||||||
|
api.getCertificate(domain, apiOptions, function (error, certFilePath, keyFilePath) {
|
||||||
|
if (error) {
|
||||||
|
debug('ensureCertificate: could not get certificate. using fallback certs', error);
|
||||||
|
return callback(null, 'cert/host.cert', 'cert/host.key'); // use fallback certs
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null, certFilePath, keyFilePath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -5,12 +5,15 @@
|
|||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
get: get,
|
get: get,
|
||||||
getAll: getAll,
|
getAll: getAll,
|
||||||
|
getAllWithTokenCount: getAllWithTokenCount,
|
||||||
getAllWithTokenCountByIdentifier: getAllWithTokenCountByIdentifier,
|
getAllWithTokenCountByIdentifier: getAllWithTokenCountByIdentifier,
|
||||||
add: add,
|
add: add,
|
||||||
del: del,
|
del: del,
|
||||||
update: update,
|
|
||||||
getByAppId: getByAppId,
|
getByAppId: getByAppId,
|
||||||
|
getByAppIdAndType: getByAppIdAndType,
|
||||||
|
|
||||||
delByAppId: delByAppId,
|
delByAppId: delByAppId,
|
||||||
|
delByAppIdAndType: delByAppIdAndType,
|
||||||
|
|
||||||
_clear: clear
|
_clear: clear
|
||||||
};
|
};
|
||||||
@@ -19,8 +22,8 @@ var assert = require('assert'),
|
|||||||
database = require('./database.js'),
|
database = require('./database.js'),
|
||||||
DatabaseError = require('./databaseerror.js');
|
DatabaseError = require('./databaseerror.js');
|
||||||
|
|
||||||
var CLIENTS_FIELDS = [ 'id', 'appId', 'clientSecret', 'redirectURI', 'scope' ].join(',');
|
var CLIENTS_FIELDS = [ 'id', 'appId', 'type', 'clientSecret', 'redirectURI', 'scope' ].join(',');
|
||||||
var CLIENTS_FIELDS_PREFIXED = [ 'clients.id', 'clients.appId', 'clients.clientSecret', 'clients.redirectURI', 'clients.scope' ].join(',');
|
var CLIENTS_FIELDS_PREFIXED = [ 'clients.id', 'clients.appId', 'clients.type', 'clients.clientSecret', 'clients.redirectURI', 'clients.scope' ].join(',');
|
||||||
|
|
||||||
function get(id, callback) {
|
function get(id, callback) {
|
||||||
assert.strictEqual(typeof id, 'string');
|
assert.strictEqual(typeof id, 'string');
|
||||||
@@ -44,14 +47,24 @@ function getAll(callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
callback(null, results);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getAllWithTokenCountByIdentifier(identifier, callback) {
|
function getAllWithTokenCountByIdentifier(identifier, callback) {
|
||||||
assert.strictEqual(typeof identifier, 'string');
|
assert.strictEqual(typeof identifier, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
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) {
|
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 DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||||
callback(null, results);
|
|
||||||
|
|
||||||
|
callback(null, results);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,21 +76,35 @@ function getByAppId(appId, callback) {
|
|||||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||||
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||||
|
|
||||||
return callback(null, result[0]);
|
callback(null, result[0]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function add(id, appId, clientSecret, redirectURI, scope, callback) {
|
function getByAppIdAndType(appId, type, callback) {
|
||||||
|
assert.strictEqual(typeof appId, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
database.query('SELECT ' + CLIENTS_FIELDS + ' FROM clients WHERE appId = ? AND type = ? LIMIT 1', [ appId, type ], function (error, result) {
|
||||||
|
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||||
|
if (result.length === 0) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||||
|
|
||||||
|
callback(null, result[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(id, appId, type, clientSecret, redirectURI, scope, callback) {
|
||||||
assert.strictEqual(typeof id, 'string');
|
assert.strictEqual(typeof id, 'string');
|
||||||
assert.strictEqual(typeof appId, 'string');
|
assert.strictEqual(typeof appId, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
assert.strictEqual(typeof clientSecret, 'string');
|
assert.strictEqual(typeof clientSecret, 'string');
|
||||||
assert.strictEqual(typeof redirectURI, 'string');
|
assert.strictEqual(typeof redirectURI, 'string');
|
||||||
assert.strictEqual(typeof scope, 'string');
|
assert.strictEqual(typeof scope, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
var data = [ id, appId, clientSecret, redirectURI, scope ];
|
var data = [ id, appId, type, clientSecret, redirectURI, scope ];
|
||||||
|
|
||||||
database.query('INSERT INTO clients (id, appId, clientSecret, redirectURI, scope) VALUES (?, ?, ?, ?, ?)', data, function (error, result) {
|
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 && 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 || result.affectedRows === 0) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
@@ -85,24 +112,6 @@ function add(id, appId, clientSecret, redirectURI, scope, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function update(id, appId, clientSecret, redirectURI, scope, callback) {
|
|
||||||
assert.strictEqual(typeof id, 'string');
|
|
||||||
assert.strictEqual(typeof appId, 'string');
|
|
||||||
assert.strictEqual(typeof clientSecret, 'string');
|
|
||||||
assert.strictEqual(typeof redirectURI, 'string');
|
|
||||||
assert.strictEqual(typeof scope, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
var data = [ appId, clientSecret, redirectURI, scope, id ];
|
|
||||||
|
|
||||||
database.query('UPDATE clients SET appId = ?, clientSecret = ?, redirectURI = ?, scope = ? WHERE id = ?', data, function (error, result) {
|
|
||||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
|
||||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
|
||||||
|
|
||||||
callback(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function del(id, callback) {
|
function del(id, callback) {
|
||||||
assert.strictEqual(typeof id, 'string');
|
assert.strictEqual(typeof id, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
@@ -123,17 +132,30 @@ function delByAppId(appId, callback) {
|
|||||||
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||||
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||||
|
|
||||||
return callback(null);
|
callback(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function delByAppIdAndType(appId, type, callback) {
|
||||||
|
assert.strictEqual(typeof appId, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
database.query('DELETE FROM clients WHERE appId=? AND type=?', [ appId, type ], function (error, result) {
|
||||||
|
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||||
|
if (result.affectedRows !== 1) return callback(new DatabaseError(DatabaseError.NOT_FOUND));
|
||||||
|
|
||||||
|
callback(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear(callback) {
|
function clear(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
database.query('DELETE FROM clients WHERE appId!="webadmin"', function (error) {
|
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));
|
if (error) return callback(new DatabaseError(DatabaseError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
return callback(null);
|
callback(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
206
src/clients.js
206
src/clients.js
@@ -5,11 +5,33 @@ exports = module.exports = {
|
|||||||
|
|
||||||
add: add,
|
add: add,
|
||||||
get: get,
|
get: get,
|
||||||
update: update,
|
|
||||||
del: del,
|
del: del,
|
||||||
getAllWithDetailsByUserId: getAllWithDetailsByUserId,
|
getAll: getAll,
|
||||||
|
getByAppIdAndType: getByAppIdAndType,
|
||||||
getClientTokensByUserId: getClientTokensByUserId,
|
getClientTokensByUserId: getClientTokensByUserId,
|
||||||
delClientTokensByUserId: delClientTokensByUserId
|
delClientTokensByUserId: delClientTokensByUserId,
|
||||||
|
delByAppIdAndType: delByAppIdAndType,
|
||||||
|
addClientTokenByUserId: addClientTokenByUserId,
|
||||||
|
delToken: delToken,
|
||||||
|
|
||||||
|
// keep this in sync with start.sh ADMIN_SCOPES that generates the cid-webadmin
|
||||||
|
SCOPE_APPS: 'apps',
|
||||||
|
SCOPE_DEVELOPER: 'developer',
|
||||||
|
SCOPE_PROFILE: 'profile',
|
||||||
|
SCOPE_CLOUDRON: 'cloudron',
|
||||||
|
SCOPE_SETTINGS: 'settings',
|
||||||
|
SCOPE_USERS: 'users',
|
||||||
|
|
||||||
|
// roles are handled just like the above scopes, they are parallel to scopes
|
||||||
|
// scopes enclose API groups, roles specify the usage role
|
||||||
|
SCOPE_ROLE_SDK: 'roleSdk',
|
||||||
|
|
||||||
|
// client type enums
|
||||||
|
TYPE_EXTERNAL: 'external',
|
||||||
|
TYPE_BUILT_IN: 'built-in',
|
||||||
|
TYPE_OAUTH: 'addon-oauth',
|
||||||
|
TYPE_SIMPLE_AUTH: 'addon-simpleauth',
|
||||||
|
TYPE_PROXY: 'addon-proxy'
|
||||||
};
|
};
|
||||||
|
|
||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
@@ -17,7 +39,6 @@ var assert = require('assert'),
|
|||||||
hat = require('hat'),
|
hat = require('hat'),
|
||||||
appdb = require('./appdb.js'),
|
appdb = require('./appdb.js'),
|
||||||
tokendb = require('./tokendb.js'),
|
tokendb = require('./tokendb.js'),
|
||||||
constants = require('./constants.js'),
|
|
||||||
async = require('async'),
|
async = require('async'),
|
||||||
clientdb = require('./clientdb.js'),
|
clientdb = require('./clientdb.js'),
|
||||||
DatabaseError = require('./databaseerror.js'),
|
DatabaseError = require('./databaseerror.js'),
|
||||||
@@ -43,36 +64,57 @@ function ClientsError(reason, errorOrMessage) {
|
|||||||
}
|
}
|
||||||
util.inherits(ClientsError, Error);
|
util.inherits(ClientsError, Error);
|
||||||
ClientsError.INVALID_SCOPE = 'Invalid scope';
|
ClientsError.INVALID_SCOPE = 'Invalid scope';
|
||||||
|
ClientsError.INVALID_CLIENT = 'Invalid client';
|
||||||
|
ClientsError.INVALID_TOKEN = 'Invalid token';
|
||||||
|
ClientsError.NOT_FOUND = 'Not found';
|
||||||
|
ClientsError.INTERNAL_ERROR = 'Internal Error';
|
||||||
|
ClientsError.NOT_ALLOWED = 'Not allowed to remove this client';
|
||||||
|
|
||||||
function validateScope(scope) {
|
function validateScope(scope) {
|
||||||
assert.strictEqual(typeof scope, 'string');
|
assert.strictEqual(typeof scope, 'string');
|
||||||
|
|
||||||
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE);
|
var VALID_SCOPES = [
|
||||||
if (scope === '*') return null;
|
exports.SCOPE_APPS,
|
||||||
|
exports.SCOPE_DEVELOPER,
|
||||||
|
exports.SCOPE_PROFILE,
|
||||||
|
exports.SCOPE_CLOUDRON,
|
||||||
|
exports.SCOPE_SETTINGS,
|
||||||
|
exports.SCOPE_USERS,
|
||||||
|
'*', // includes all scopes, but not roles
|
||||||
|
exports.SCOPE_ROLE_SDK
|
||||||
|
];
|
||||||
|
|
||||||
// TODO maybe validate all individual scopes if they exist
|
if (scope === '') return new ClientsError(ClientsError.INVALID_SCOPE, 'Empty scope not allowed');
|
||||||
|
|
||||||
|
var allValid = scope.split(',').every(function (s) { return VALID_SCOPES.indexOf(s) !== -1; });
|
||||||
|
if (!allValid) return new ClientsError(ClientsError.INVALID_SCOPE, 'Invalid scope. Available scopes are ' + VALID_SCOPES.join(', '));
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function add(appIdentifier, redirectURI, scope, callback) {
|
function add(appId, type, redirectURI, scope, callback) {
|
||||||
assert.strictEqual(typeof appIdentifier, 'string');
|
assert.strictEqual(typeof appId, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
assert.strictEqual(typeof redirectURI, 'string');
|
assert.strictEqual(typeof redirectURI, 'string');
|
||||||
assert.strictEqual(typeof scope, 'string');
|
assert.strictEqual(typeof scope, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
// allow whitespace
|
||||||
|
scope = scope.split(',').map(function (s) { return s.trim(); }).join(',');
|
||||||
|
|
||||||
var error = validateScope(scope);
|
var error = validateScope(scope);
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
var id = 'cid-' + uuid.v4();
|
var id = 'cid-' + uuid.v4();
|
||||||
var clientSecret = hat(256);
|
var clientSecret = hat(256);
|
||||||
|
|
||||||
clientdb.add(id, appIdentifier, clientSecret, redirectURI, scope, function (error) {
|
clientdb.add(id, appId, type, clientSecret, redirectURI, scope, function (error) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
var client = {
|
var client = {
|
||||||
id: id,
|
id: id,
|
||||||
appId: appIdentifier,
|
appId: appId,
|
||||||
|
type: type,
|
||||||
clientSecret: clientSecret,
|
clientSecret: clientSecret,
|
||||||
redirectURI: redirectURI,
|
redirectURI: redirectURI,
|
||||||
scope: scope
|
scope: scope
|
||||||
@@ -87,94 +129,52 @@ function get(id, callback) {
|
|||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
clientdb.get(id, function (error, result) {
|
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);
|
if (error) return callback(error);
|
||||||
callback(null, result);
|
callback(null, result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// we only allow appIdentifier and redirectURI to be updated
|
|
||||||
function update(id, appIdentifier, redirectURI, callback) {
|
|
||||||
assert.strictEqual(typeof id, 'string');
|
|
||||||
assert.strictEqual(typeof appIdentifier, 'string');
|
|
||||||
assert.strictEqual(typeof redirectURI, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
clientdb.get(id, function (error, result) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
clientdb.update(id, appIdentifier, result.clientSecret, redirectURI, result.scope, function (error, result) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
callback(null, result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function del(id, callback) {
|
function del(id, callback) {
|
||||||
assert.strictEqual(typeof id, 'string');
|
assert.strictEqual(typeof id, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
clientdb.del(id, function (error, result) {
|
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);
|
if (error) return callback(error);
|
||||||
callback(null, result);
|
callback(null, result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllWithDetailsByUserId(userId, callback) {
|
function getAll(callback) {
|
||||||
assert.strictEqual(typeof userId, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
clientdb.getAllWithTokenCountByIdentifier(tokendb.PREFIX_USER + userId, function (error, results) {
|
clientdb.getAll(function (error, results) {
|
||||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, []);
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(null, []);
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
// We have several types of records here
|
|
||||||
// 1) webadmin has an app id of 'webadmin'
|
|
||||||
// 2) oauth proxy records are always the app id prefixed with 'proxy-'
|
|
||||||
// 3) addon oauth records for apps prefixed with 'addon-'
|
|
||||||
// 4) external app records prefixed with 'external-'
|
|
||||||
// 5) normal apps on the cloudron without a prefix
|
|
||||||
|
|
||||||
var tmp = [];
|
var tmp = [];
|
||||||
async.each(results, function (record, callback) {
|
async.each(results, function (record, callback) {
|
||||||
if (record.appId === constants.ADMIN_CLIENT_ID) {
|
if (record.type === exports.TYPE_EXTERNAL || record.type === exports.TYPE_BUILT_IN) {
|
||||||
record.name = constants.ADMIN_NAME;
|
// the appId in this case holds the name
|
||||||
record.location = constants.ADMIN_LOCATION;
|
record.name = record.appId;
|
||||||
record.type = 'webadmin';
|
|
||||||
|
|
||||||
tmp.push(record);
|
|
||||||
|
|
||||||
return callback(null);
|
|
||||||
} else if (record.appId === constants.TEST_CLIENT_ID) {
|
|
||||||
record.name = constants.TEST_NAME;
|
|
||||||
record.location = constants.TEST_LOCATION;
|
|
||||||
record.type = 'test';
|
|
||||||
|
|
||||||
tmp.push(record);
|
tmp.push(record);
|
||||||
|
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var appId = record.appId;
|
appdb.get(record.appId, function (error, result) {
|
||||||
var type = 'app';
|
|
||||||
|
|
||||||
// Handle our different types of oauth clients
|
|
||||||
if (record.appId.indexOf('addon-') === 0) {
|
|
||||||
appId = record.appId.slice('addon-'.length);
|
|
||||||
type = 'addon';
|
|
||||||
} else if (record.appId.indexOf('proxy-') === 0) {
|
|
||||||
appId = record.appId.slice('proxy-'.length);
|
|
||||||
type = 'proxy';
|
|
||||||
}
|
|
||||||
|
|
||||||
appdb.get(appId, function (error, result) {
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Failed to get app details for oauth client', result, error);
|
console.error('Failed to get app details for oauth client', record.appId, error);
|
||||||
return callback(null); // ignore error so we continue listing clients
|
return callback(null); // ignore error so we continue listing clients
|
||||||
}
|
}
|
||||||
|
|
||||||
record.name = result.manifest.title + (record.appId.indexOf('proxy-') === 0 ? 'OAuth Proxy' : '');
|
if (record.type === exports.TYPE_PROXY) record.name = result.manifest.title + ' Website Proxy';
|
||||||
|
if (record.type === exports.TYPE_OAUTH) record.name = result.manifest.title + ' OAuth';
|
||||||
|
if (record.type === exports.TYPE_SIMPLE_AUTH) record.name = result.manifest.title + ' Simple Auth';
|
||||||
|
|
||||||
record.location = result.location;
|
record.location = result.location;
|
||||||
record.type = type;
|
|
||||||
|
|
||||||
tmp.push(record);
|
tmp.push(record);
|
||||||
|
|
||||||
@@ -187,15 +187,27 @@ function getAllWithDetailsByUserId(userId, callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getByAppIdAndType(appId, type, callback) {
|
||||||
|
assert.strictEqual(typeof appId, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
clientdb.getByAppIdAndType(appId, type, function (error, result) {
|
||||||
|
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new ClientsError(ClientsError.NOT_FOUND, 'No such client'));
|
||||||
|
if (error) return callback(error);
|
||||||
|
callback(null, result);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getClientTokensByUserId(clientId, userId, callback) {
|
function getClientTokensByUserId(clientId, userId, callback) {
|
||||||
assert.strictEqual(typeof clientId, 'string');
|
assert.strictEqual(typeof clientId, 'string');
|
||||||
assert.strictEqual(typeof userId, 'string');
|
assert.strictEqual(typeof userId, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
tokendb.getByIdentifierAndClientId(tokendb.PREFIX_USER + userId, clientId, function (error, result) {
|
tokendb.getByIdentifierAndClientId(userId, clientId, function (error, result) {
|
||||||
if (error && error.reason === DatabaseError.NOT_FOUND) {
|
if (error && error.reason === DatabaseError.NOT_FOUND) {
|
||||||
// this can mean either that there are no tokens or the clientId is actually unknown
|
// this can mean either that there are no tokens or the clientId is actually unknown
|
||||||
clientdb.get(clientId, function (error/*, result*/) {
|
get(clientId, function (error/*, result*/) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
callback(null, []);
|
callback(null, []);
|
||||||
});
|
});
|
||||||
@@ -211,10 +223,10 @@ function delClientTokensByUserId(clientId, userId, callback) {
|
|||||||
assert.strictEqual(typeof userId, 'string');
|
assert.strictEqual(typeof userId, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
tokendb.delByIdentifierAndClientId(tokendb.PREFIX_USER + userId, clientId, function (error) {
|
tokendb.delByIdentifierAndClientId(userId, clientId, function (error) {
|
||||||
if (error && error.reason === DatabaseError.NOT_FOUND) {
|
if (error && error.reason === DatabaseError.NOT_FOUND) {
|
||||||
// this can mean either that there are no tokens or the clientId is actually unknown
|
// this can mean either that there are no tokens or the clientId is actually unknown
|
||||||
clientdb.get(clientId, function (error/*, result*/) {
|
get(clientId, function (error/*, result*/) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
callback(null);
|
callback(null);
|
||||||
});
|
});
|
||||||
@@ -224,3 +236,57 @@ function delClientTokensByUserId(clientId, userId, callback) {
|
|||||||
callback(null);
|
callback(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function delByAppIdAndType(appId, type, callback) {
|
||||||
|
assert.strictEqual(typeof appId, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addClientTokenByUserId(clientId, userId, expiresAt, callback) {
|
||||||
|
assert.strictEqual(typeof clientId, 'string');
|
||||||
|
assert.strictEqual(typeof userId, 'string');
|
||||||
|
assert.strictEqual(typeof expiresAt, 'number');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
get(clientId, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
var token = tokendb.generateToken();
|
||||||
|
|
||||||
|
tokendb.add(token, userId, result.id, expiresAt, result.scope, function (error) {
|
||||||
|
if (error) return callback(new ClientsError(ClientsError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
callback(null, {
|
||||||
|
accessToken: token,
|
||||||
|
identifier: userId,
|
||||||
|
clientId: result.id,
|
||||||
|
scope: result.id,
|
||||||
|
expires: expiresAt
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function delToken(clientId, tokenId, callback) {
|
||||||
|
assert.strictEqual(typeof clientId, 'string');
|
||||||
|
assert.strictEqual(typeof tokenId, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
get(clientId, function (error, result) {
|
||||||
|
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));
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
731
src/cloudron.js
731
src/cloudron.js
@@ -1,5 +1,3 @@
|
|||||||
/* jslint node: true */
|
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
@@ -11,68 +9,63 @@ exports = module.exports = {
|
|||||||
getConfig: getConfig,
|
getConfig: getConfig,
|
||||||
getStatus: getStatus,
|
getStatus: getStatus,
|
||||||
|
|
||||||
setCertificate: setCertificate,
|
|
||||||
|
|
||||||
sendHeartbeat: sendHeartbeat,
|
sendHeartbeat: sendHeartbeat,
|
||||||
|
|
||||||
update: update,
|
updateToLatest: updateToLatest,
|
||||||
reboot: reboot,
|
reboot: reboot,
|
||||||
migrate: migrate,
|
retire: retire,
|
||||||
backup: backup,
|
|
||||||
ensureBackup: ensureBackup};
|
isConfiguredSync: isConfiguredSync,
|
||||||
|
|
||||||
|
checkDiskSpace: checkDiskSpace,
|
||||||
|
|
||||||
|
events: new (require('events').EventEmitter)(),
|
||||||
|
|
||||||
|
EVENT_ACTIVATED: 'activated',
|
||||||
|
EVENT_CONFIGURED: 'configured',
|
||||||
|
EVENT_FIRST_RUN: 'firstrun'
|
||||||
|
};
|
||||||
|
|
||||||
var apps = require('./apps.js'),
|
var apps = require('./apps.js'),
|
||||||
AppsError = require('./apps.js').AppsError,
|
|
||||||
assert = require('assert'),
|
assert = require('assert'),
|
||||||
async = require('async'),
|
async = require('async'),
|
||||||
backups = require('./backups.js'),
|
backups = require('./backups.js'),
|
||||||
BackupsError = require('./backups.js').BackupsError,
|
clients = require('./clients.js'),
|
||||||
clientdb = require('./clientdb.js'),
|
|
||||||
config = require('./config.js'),
|
config = require('./config.js'),
|
||||||
|
constants = require('./constants.js'),
|
||||||
debug = require('debug')('box:cloudron'),
|
debug = require('debug')('box:cloudron'),
|
||||||
|
df = require('node-df'),
|
||||||
|
eventlog = require('./eventlog.js'),
|
||||||
fs = require('fs'),
|
fs = require('fs'),
|
||||||
locker = require('./locker.js'),
|
locker = require('./locker.js'),
|
||||||
|
mailer = require('./mailer.js'),
|
||||||
|
os = require('os'),
|
||||||
path = require('path'),
|
path = require('path'),
|
||||||
paths = require('./paths.js'),
|
paths = require('./paths.js'),
|
||||||
progress = require('./progress.js'),
|
progress = require('./progress.js'),
|
||||||
safe = require('safetydance'),
|
safe = require('safetydance'),
|
||||||
settings = require('./settings.js'),
|
settings = require('./settings.js'),
|
||||||
SettingsError = settings.SettingsError,
|
|
||||||
shell = require('./shell.js'),
|
shell = require('./shell.js'),
|
||||||
|
subdomains = require('./subdomains.js'),
|
||||||
superagent = require('superagent'),
|
superagent = require('superagent'),
|
||||||
sysinfo = require('./sysinfo.js'),
|
sysinfo = require('./sysinfo.js'),
|
||||||
tokendb = require('./tokendb.js'),
|
tokendb = require('./tokendb.js'),
|
||||||
updateChecker = require('./updatechecker.js'),
|
updateChecker = require('./updatechecker.js'),
|
||||||
user = require('./user.js'),
|
user = require('./user.js'),
|
||||||
UserError = user.UserError,
|
UserError = user.UserError,
|
||||||
userdb = require('./userdb.js'),
|
user = require('./user.js'),
|
||||||
util = require('util');
|
util = require('util');
|
||||||
|
|
||||||
var RELOAD_NGINX_CMD = path.join(__dirname, 'scripts/reloadnginx.sh'),
|
var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
|
||||||
REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'),
|
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update',
|
||||||
BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'),
|
RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh');
|
||||||
BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'),
|
|
||||||
INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update';
|
|
||||||
|
|
||||||
var gAddMailDnsRecordsTimerId = null,
|
var NOOP_CALLBACK = function (error) { if (error) debug(error); };
|
||||||
gCloudronDetails = null; // cached cloudron details like region,size...
|
|
||||||
|
|
||||||
function debugApp(app, args) {
|
|
||||||
assert(!app || typeof app === 'object');
|
|
||||||
|
|
||||||
var prefix = app ? app.location : '(no app)';
|
|
||||||
debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function ignoreError(func) {
|
|
||||||
return function (callback) {
|
|
||||||
func(function (error) {
|
|
||||||
if (error) console.error('Ignored error:', error);
|
|
||||||
callback();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
var gUpdatingDns = false, // flag for dns update reentrancy
|
||||||
|
gCloudronDetails = null, // cached cloudron details like region,size...
|
||||||
|
gAppstoreUserDetails = {},
|
||||||
|
gIsConfigured = null; // cached configured state so that return value is synchronous. null means we are not initialized yet
|
||||||
|
|
||||||
function CloudronError(reason, errorOrMessage) {
|
function CloudronError(reason, errorOrMessage) {
|
||||||
assert.strictEqual(typeof reason, 'string');
|
assert.strictEqual(typeof reason, 'string');
|
||||||
@@ -97,95 +90,161 @@ CloudronError.BAD_FIELD = 'Field error';
|
|||||||
CloudronError.INTERNAL_ERROR = 'Internal Error';
|
CloudronError.INTERNAL_ERROR = 'Internal Error';
|
||||||
CloudronError.EXTERNAL_ERROR = 'External Error';
|
CloudronError.EXTERNAL_ERROR = 'External Error';
|
||||||
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
|
CloudronError.ALREADY_PROVISIONED = 'Already Provisioned';
|
||||||
CloudronError.BAD_USERNAME = 'Bad username';
|
|
||||||
CloudronError.BAD_EMAIL = 'Bad email';
|
|
||||||
CloudronError.BAD_PASSWORD = 'Bad password';
|
|
||||||
CloudronError.BAD_NAME = 'Bad name';
|
|
||||||
CloudronError.BAD_STATE = 'Bad state';
|
CloudronError.BAD_STATE = 'Bad state';
|
||||||
|
CloudronError.ALREADY_UPTODATE = 'No Update Available';
|
||||||
CloudronError.NOT_FOUND = 'Not found';
|
CloudronError.NOT_FOUND = 'Not found';
|
||||||
|
|
||||||
function initialize(callback) {
|
function initialize(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
if (process.env.BOX_ENV !== 'test') {
|
ensureDkimKeySync();
|
||||||
addMailDnsRecords();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send heartbeat once we are up and running, this speeds up the Cloudron creation, as otherwise we are bound to the cron.js settings
|
exports.events.on(exports.EVENT_CONFIGURED, addDnsRecords);
|
||||||
sendHeartbeat();
|
exports.events.on(exports.EVENT_FIRST_RUN, installAppBundle);
|
||||||
|
|
||||||
callback(null);
|
// check activation state for existing cloudrons that do not have first run file
|
||||||
|
// can be removed once cloudrons have been updated
|
||||||
|
isActivated(function (error, activated) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
debug('initialize: cloudron %s activated', activated ? '' : 'not');
|
||||||
|
|
||||||
|
if (activated) fs.writeFileSync(paths.FIRST_RUN_FILE, 'been there, done that', 'utf8');
|
||||||
|
|
||||||
|
if (!fs.existsSync(paths.FIRST_RUN_FILE)) {
|
||||||
|
// EE API is sync. do not keep the server waiting
|
||||||
|
debug('initialize: emitting first run event');
|
||||||
|
process.nextTick(function () { exports.events.emit(exports.EVENT_FIRST_RUN); });
|
||||||
|
fs.writeFileSync(paths.FIRST_RUN_FILE, 'been there, done that', 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
syncConfigState(callback);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninitialize(callback) {
|
function uninitialize(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
clearTimeout(gAddMailDnsRecordsTimerId);
|
exports.events.removeListener(exports.EVENT_CONFIGURED, addDnsRecords);
|
||||||
gAddMailDnsRecordsTimerId = null;
|
exports.events.removeListener(exports.EVENT_FIRST_RUN, installAppBundle);
|
||||||
|
|
||||||
callback(null);
|
callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isConfiguredSync() {
|
||||||
|
return gIsConfigured === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActivated(callback) {
|
||||||
|
user.getOwner(function (error) {
|
||||||
|
if (error && error.reason === UserError.NOT_FOUND) return callback(null, false);
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
callback(null, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isConfigured(callback) {
|
||||||
|
// set of rules to see if we have the configs required for cloudron to function
|
||||||
|
// note this checks for missing configs and not invalid configs
|
||||||
|
|
||||||
|
settings.getDnsConfig(function (error, dnsConfig) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
if (!dnsConfig) return callback(null, false);
|
||||||
|
|
||||||
|
var isConfigured = (config.isCustomDomain() && dnsConfig.provider === 'route53') ||
|
||||||
|
(!config.isCustomDomain() && dnsConfig.provider === 'caas');
|
||||||
|
|
||||||
|
callback(null, isConfigured);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncConfigState(callback) {
|
||||||
|
assert(!gIsConfigured);
|
||||||
|
|
||||||
|
callback = callback || NOOP_CALLBACK;
|
||||||
|
|
||||||
|
isConfigured(function (error, configured) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
debug('syncConfigState: configured = %s', configured);
|
||||||
|
|
||||||
|
if (configured) {
|
||||||
|
exports.events.emit(exports.EVENT_CONFIGURED);
|
||||||
|
} else {
|
||||||
|
settings.events.once(settings.DNS_CONFIG_KEY, function () { syncConfigState(); }); // check again later
|
||||||
|
}
|
||||||
|
|
||||||
|
gIsConfigured = configured;
|
||||||
|
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function setTimeZone(ip, callback) {
|
function setTimeZone(ip, callback) {
|
||||||
assert.strictEqual(typeof ip, 'string');
|
assert.strictEqual(typeof ip, 'string');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
debug('setTimeZone ip:%s', ip);
|
debug('setTimeZone ip:%s', ip);
|
||||||
|
|
||||||
superagent.get('http://www.telize.com/geoip/' + ip).end(function (error, result) {
|
// https://github.com/bluesmoon/node-geoip
|
||||||
if (error || result.statusCode !== 200) {
|
// https://github.com/runk/node-maxmind
|
||||||
debug('Failed to get geo location', error);
|
// { url: 'http://freegeoip.net/json/%s', jpath: 'time_zone' },
|
||||||
|
// { url: 'http://ip-api.com/json/%s', jpath: 'timezone' },
|
||||||
|
// { url: 'http://geoip.nekudo.com/api/%s', jpath: 'time_zone }
|
||||||
|
|
||||||
|
superagent.get('http://freegeoip.net/json/' + ip).end(function (error, result) {
|
||||||
|
if ((error && !error.response) || result.statusCode !== 200) {
|
||||||
|
debug('Failed to get geo location: %s', error.message);
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.body.timezone) {
|
if (!result.body.time_zone || typeof result.body.time_zone !== 'string') {
|
||||||
debug('No timezone in geoip response');
|
debug('No timezone in geoip response : %j', result.body);
|
||||||
return callback(null);
|
return callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('Setting timezone to ', result.body.timezone);
|
debug('Setting timezone to ', result.body.time_zone);
|
||||||
|
|
||||||
settings.setTimeZone(result.body.timezone, callback);
|
settings.setTimeZone(result.body.time_zone, callback);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function activate(username, password, email, name, ip, callback) {
|
function activate(username, password, email, displayName, ip, auditSource, callback) {
|
||||||
assert.strictEqual(typeof username, 'string');
|
assert.strictEqual(typeof username, 'string');
|
||||||
assert.strictEqual(typeof password, 'string');
|
assert.strictEqual(typeof password, 'string');
|
||||||
assert.strictEqual(typeof email, 'string');
|
assert.strictEqual(typeof email, 'string');
|
||||||
|
assert.strictEqual(typeof displayName, 'string');
|
||||||
assert.strictEqual(typeof ip, 'string');
|
assert.strictEqual(typeof ip, 'string');
|
||||||
assert(!name || typeof name, 'string');
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
debug('activating user:%s email:%s', username, email);
|
debug('activating user:%s email:%s', username, email);
|
||||||
|
|
||||||
setTimeZone(ip, function () { });
|
setTimeZone(ip, function () { }); // TODO: get this from user. note that timezone is detected based on the browser location and not the cloudron region
|
||||||
|
|
||||||
if (!name) name = settings.getDefaultSync(settings.CLOUDRON_NAME_KEY);
|
user.createOwner(username, password, email, displayName, auditSource, function (error, userObject) {
|
||||||
|
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
|
||||||
settings.setCloudronName(name, function (error) {
|
if (error && error.reason === UserError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_FIELD, error.message));
|
||||||
if (error && error.reason === SettingsError.BAD_FIELD) return callback(new CloudronError(CloudronError.BAD_NAME));
|
|
||||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
user.createOwner(username, password, email, function (error, userObject) {
|
clients.get('cid-webadmin', function (error, result) {
|
||||||
if (error && error.reason === UserError.ALREADY_EXISTS) return callback(new CloudronError(CloudronError.ALREADY_PROVISIONED));
|
|
||||||
if (error && error.reason === UserError.BAD_USERNAME) return callback(new CloudronError(CloudronError.BAD_USERNAME));
|
|
||||||
if (error && error.reason === UserError.BAD_PASSWORD) return callback(new CloudronError(CloudronError.BAD_PASSWORD));
|
|
||||||
if (error && error.reason === UserError.BAD_EMAIL) return callback(new CloudronError(CloudronError.BAD_EMAIL));
|
|
||||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
clientdb.getByAppId('webadmin', function (error, result) {
|
// Also generate a token so the admin creation can also act as a login
|
||||||
|
var token = tokendb.generateToken();
|
||||||
|
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
|
||||||
|
|
||||||
|
tokendb.add(token, userObject.id, result.id, expires, '*', function (error) {
|
||||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
// Also generate a token so the admin creation can also act as a login
|
// EE API is sync. do not keep the REST API reponse waiting
|
||||||
var token = tokendb.generateToken();
|
process.nextTick(function () { exports.events.emit(exports.EVENT_ACTIVATED); });
|
||||||
var expires = Date.now() + 24 * 60 * 60 * 1000; // 1 day
|
|
||||||
|
|
||||||
tokendb.add(token, tokendb.PREFIX_USER + userObject.id, result.id, expires, '*', function (error) {
|
eventlog.add(eventlog.ACTION_ACTIVATE, auditSource, { });
|
||||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
|
||||||
|
|
||||||
callback(null, { token: token, expires: expires });
|
callback(null, { token: token, expires: expires });
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -194,7 +253,7 @@ function activate(username, password, email, name, ip, callback) {
|
|||||||
function getStatus(callback) {
|
function getStatus(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
userdb.count(function (error, count) {
|
user.count(function (error, count) {
|
||||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
settings.getCloudronName(function (error, cloudronName) {
|
settings.getCloudronName(function (error, cloudronName) {
|
||||||
@@ -203,6 +262,9 @@ function getStatus(callback) {
|
|||||||
callback(null, {
|
callback(null, {
|
||||||
activated: count !== 0,
|
activated: count !== 0,
|
||||||
version: config.version(),
|
version: config.version(),
|
||||||
|
boxVersionsUrl: config.get('boxVersionsUrl'),
|
||||||
|
apiServerOrigin: config.apiServerOrigin(), // used by CaaS tool
|
||||||
|
provider: config.provider(),
|
||||||
cloudronName: cloudronName
|
cloudronName: cloudronName
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -214,14 +276,24 @@ function getCloudronDetails(callback) {
|
|||||||
|
|
||||||
if (gCloudronDetails) return callback(null, gCloudronDetails);
|
if (gCloudronDetails) return callback(null, gCloudronDetails);
|
||||||
|
|
||||||
|
if (!config.token()) {
|
||||||
|
gCloudronDetails = {
|
||||||
|
region: null,
|
||||||
|
size: null
|
||||||
|
};
|
||||||
|
|
||||||
|
return callback(null, gCloudronDetails);
|
||||||
|
}
|
||||||
|
|
||||||
superagent
|
superagent
|
||||||
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
|
.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn())
|
||||||
.query({ token: config.token() })
|
.query({ token: config.token() })
|
||||||
.end(function (error, result) {
|
.end(function (error, result) {
|
||||||
if (error) return callback(error);
|
if (error && !error.response) return callback(error);
|
||||||
if (result.status !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
if (result.statusCode !== 200) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
||||||
|
|
||||||
gCloudronDetails = result.body.box;
|
gCloudronDetails = result.body.box;
|
||||||
|
gAppstoreUserDetails = result.body.user;
|
||||||
|
|
||||||
return callback(null, gCloudronDetails);
|
return callback(null, gCloudronDetails);
|
||||||
});
|
});
|
||||||
@@ -230,10 +302,9 @@ function getCloudronDetails(callback) {
|
|||||||
function getConfig(callback) {
|
function getConfig(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
// TODO avoid pyramid of awesomeness with async
|
|
||||||
getCloudronDetails(function (error, result) {
|
getCloudronDetails(function (error, result) {
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Failed to fetch cloudron details.', error);
|
debug('Failed to fetch cloudron details.', error);
|
||||||
|
|
||||||
// set fallback values to avoid dependency on appstore
|
// set fallback values to avoid dependency on appstore
|
||||||
result = {
|
result = {
|
||||||
@@ -248,20 +319,27 @@ function getConfig(callback) {
|
|||||||
settings.getDeveloperMode(function (error, developerMode) {
|
settings.getDeveloperMode(function (error, developerMode) {
|
||||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
callback(null, {
|
sysinfo.getIp(function (error, ip) {
|
||||||
apiServerOrigin: config.apiServerOrigin(),
|
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||||
webServerOrigin: config.webServerOrigin(),
|
|
||||||
isDev: config.isDev(),
|
callback(null, {
|
||||||
fqdn: config.fqdn(),
|
apiServerOrigin: config.apiServerOrigin(),
|
||||||
ip: sysinfo.getIp(),
|
webServerOrigin: config.webServerOrigin(),
|
||||||
version: config.version(),
|
isDev: config.isDev(),
|
||||||
update: updateChecker.getUpdateInfo(),
|
fqdn: config.fqdn(),
|
||||||
progress: progress.get(),
|
ip: ip,
|
||||||
isCustomDomain: config.isCustomDomain(),
|
version: config.version(),
|
||||||
developerMode: developerMode,
|
update: updateChecker.getUpdateInfo(),
|
||||||
region: result.region,
|
progress: progress.get(),
|
||||||
size: result.size,
|
isCustomDomain: config.isCustomDomain(),
|
||||||
cloudronName: cloudronName
|
developerMode: developerMode,
|
||||||
|
region: result.region,
|
||||||
|
size: result.size,
|
||||||
|
billing: !!gAppstoreUserDetails.billing,
|
||||||
|
memory: os.totalmem(),
|
||||||
|
provider: config.provider(),
|
||||||
|
cloudronName: cloudronName
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -269,94 +347,146 @@ function getConfig(callback) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sendHeartbeat() {
|
function sendHeartbeat() {
|
||||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
if (!config.token()) return;
|
||||||
|
|
||||||
// TODO: this must be a POST
|
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
||||||
superagent.get(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
|
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
|
||||||
if (error) debug('Error sending heartbeat.', error);
|
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 if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
|
||||||
else debug('Heartbeat sent to %s', url);
|
else debug('Heartbeat sent to %s', url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendMailDnsRecordsRequest(callback) {
|
function ensureDkimKeySync() {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
var dkimPrivateKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/private');
|
||||||
|
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
|
||||||
|
|
||||||
var DKIM_SELECTOR = 'mail';
|
if (fs.existsSync(dkimPrivateKeyFile) && fs.existsSync(dkimPublicKeyFile)) {
|
||||||
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
|
debug('DKIM keys already present');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('Generating new DKIM keys');
|
||||||
|
|
||||||
|
safe.child_process.execSync('openssl genrsa -out ' + dkimPrivateKeyFile + ' 1024');
|
||||||
|
safe.child_process.execSync('openssl rsa -in ' + dkimPrivateKeyFile + ' -out ' + dkimPublicKeyFile + ' -pubout -outform PEM');
|
||||||
|
}
|
||||||
|
|
||||||
|
function readDkimPublicKeySync() {
|
||||||
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
|
var dkimPublicKeyFile = path.join(paths.MAIL_DATA_DIR, 'dkim/' + config.fqdn() + '/public');
|
||||||
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
|
var publicKey = safe.fs.readFileSync(dkimPublicKeyFile, 'utf8');
|
||||||
|
|
||||||
if (publicKey === null) return callback(new Error('Error reading dkim public key'));
|
if (publicKey === null) {
|
||||||
|
debug('Error reading dkim public key.', safe.error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// remove header, footer and new lines
|
// remove header, footer and new lines
|
||||||
publicKey = publicKey.split('\n').slice(1, -2).join('');
|
publicKey = publicKey.split('\n').slice(1, -2).join('');
|
||||||
|
|
||||||
// note that dmarc requires special DNS records for external RUF and RUA
|
return publicKey;
|
||||||
var records = [
|
|
||||||
// softfail all mails not from our IP. Note that this uses IP instead of 'a' should we use a load balancer in the future
|
|
||||||
{ subdomain: '', type: 'TXT', value: '"v=spf1 ip4:' + sysinfo.getIp() + ' ~all"' },
|
|
||||||
// t=s limits the domainkey to this domain and not it's subdomains
|
|
||||||
{ subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', value: '"v=DKIM1; t=s; p=' + publicKey + '"' },
|
|
||||||
// DMARC requires special setup if report email id is in different domain
|
|
||||||
{ subdomain: '_dmarc', type: 'TXT', value: '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' }
|
|
||||||
];
|
|
||||||
|
|
||||||
debug('sendMailDnsRecords request:%s', JSON.stringify(records));
|
|
||||||
|
|
||||||
superagent
|
|
||||||
.post(config.apiServerOrigin() + '/api/v1/subdomains')
|
|
||||||
.set('Accept', 'application/json')
|
|
||||||
.query({ token: config.token() })
|
|
||||||
.send({ records: records })
|
|
||||||
.end(function (error, res) {
|
|
||||||
if (error) return callback(error);
|
|
||||||
|
|
||||||
debug('sendMailDnsRecords status: %s', res.status);
|
|
||||||
|
|
||||||
if (res.status === 409) return callback(null); // already registered
|
|
||||||
|
|
||||||
if (res.status !== 201) return callback(new Error(util.format('Failed to add Mail DNS records: %s %j', res.status, res.body)));
|
|
||||||
|
|
||||||
return callback(null, res.body.ids);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addMailDnsRecords() {
|
// NOTE: if you change the SPF record here, be sure the wait check in mailer.js
|
||||||
if (config.get('mailDnsRecordIds').length !== 0) return; // already registered
|
function txtRecordsWithSpf(callback) {
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
sendMailDnsRecordsRequest(function (error, ids) {
|
subdomains.get('', 'TXT', function (error, txtRecords) {
|
||||||
if (error) {
|
if (error) return callback(error);
|
||||||
console.error('Mail DNS record addition failed', error);
|
|
||||||
gAddMailDnsRecordsTimerId = setTimeout(addMailDnsRecords, 30000);
|
debug('txtRecordsWithSpf: current txt records - %j', txtRecords);
|
||||||
return;
|
|
||||||
|
var i, validSpf;
|
||||||
|
|
||||||
|
for (i = 0; i < txtRecords.length; i++) {
|
||||||
|
if (txtRecords[i].indexOf('"v=spf1 ') !== 0) continue; // not SPF
|
||||||
|
|
||||||
|
validSpf = txtRecords[i].indexOf(' a:' + config.adminFqdn() + ' ') !== -1;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
debug('Added Mail DNS records successfully');
|
if (validSpf) return callback(null, null);
|
||||||
config.set('mailDnsRecordIds', ids);
|
|
||||||
|
if (i == txtRecords.length) {
|
||||||
|
txtRecords[i] = '"v=spf1 a:' + config.adminFqdn() + ' ~all"';
|
||||||
|
} else {
|
||||||
|
txtRecords[i] = '"v=spf1 a:' + config.adminFqdn() + ' ' + txtRecords[i].slice('"v=spf1 '.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback(null, txtRecords);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCertificate(certificate, key, callback) {
|
function addDnsRecords() {
|
||||||
assert.strictEqual(typeof certificate, 'string');
|
var callback = NOOP_CALLBACK;
|
||||||
assert.strictEqual(typeof key, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
debug('Updating certificates');
|
if (process.env.BOX_ENV === 'test') return callback();
|
||||||
|
|
||||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), certificate)) {
|
if (gUpdatingDns) {
|
||||||
return callback(new CloudronError(CloudronError.INTERNAL_ERROR, safe.error.message));
|
debug('addDnsRecords: dns update already in progress');
|
||||||
|
return callback();
|
||||||
}
|
}
|
||||||
|
gUpdatingDns = true;
|
||||||
|
|
||||||
if (!safe.fs.writeFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), key)) {
|
var DKIM_SELECTOR = 'cloudron';
|
||||||
return callback(new CloudronError(CloudronError.INTERNAL_ERROR, safe.error.message));
|
var DMARC_REPORT_EMAIL = 'dmarc-report@cloudron.io';
|
||||||
}
|
|
||||||
|
|
||||||
shell.sudo('setCertificate', [ RELOAD_NGINX_CMD ], function (error) {
|
var dkimKey = readDkimPublicKeySync();
|
||||||
|
if (!dkimKey) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, new Error('Failed to read dkim public key')));
|
||||||
|
|
||||||
|
sysinfo.getIp(function (error, ip) {
|
||||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
return callback(null);
|
var webadminRecord = { subdomain: constants.ADMIN_LOCATION, type: 'A', values: [ ip ] };
|
||||||
|
// t=s limits the domainkey to this domain and not it's subdomains
|
||||||
|
var dkimRecord = { subdomain: DKIM_SELECTOR + '._domainkey', type: 'TXT', values: [ '"v=DKIM1; t=s; p=' + dkimKey + '"' ] };
|
||||||
|
// DMARC requires special setup if report email id is in different domain
|
||||||
|
var dmarcRecord = { subdomain: '_dmarc', type: 'TXT', values: [ '"v=DMARC1; p=none; pct=100; rua=mailto:' + DMARC_REPORT_EMAIL + '; ruf=' + DMARC_REPORT_EMAIL + '"' ] };
|
||||||
|
|
||||||
|
var mxRecord = { subdomain: '', type: 'MX', values: [ '10 ' + config.mailFqdn() + '.' ] };
|
||||||
|
|
||||||
|
var records = [ ];
|
||||||
|
if (config.isCustomDomain()) {
|
||||||
|
records.push(webadminRecord);
|
||||||
|
records.push(dkimRecord);
|
||||||
|
records.push(mxRecord);
|
||||||
|
} else {
|
||||||
|
// for custom domains, we show a nakeddomain.html page
|
||||||
|
var nakedDomainRecord = { subdomain: '', type: 'A', values: [ ip ] };
|
||||||
|
|
||||||
|
records.push(nakedDomainRecord);
|
||||||
|
records.push(webadminRecord);
|
||||||
|
records.push(dkimRecord);
|
||||||
|
records.push(dmarcRecord);
|
||||||
|
records.push(mxRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug('addDnsRecords: %j', records);
|
||||||
|
|
||||||
|
async.retry({ times: 10, interval: 20000 }, function (retryCallback) {
|
||||||
|
txtRecordsWithSpf(function (error, txtRecords) {
|
||||||
|
if (error) return retryCallback(error);
|
||||||
|
|
||||||
|
if (txtRecords) records.push({ subdomain: '', type: 'TXT', values: txtRecords });
|
||||||
|
|
||||||
|
debug('addDnsRecords: will update %j', records);
|
||||||
|
|
||||||
|
async.mapSeries(records, function (record, iteratorCallback) {
|
||||||
|
subdomains.update(record.subdomain, record.type, record.values, iteratorCallback);
|
||||||
|
}, function (error, changeIds) {
|
||||||
|
if (error) debug('addDnsRecords: failed to update : %s. will retry', error);
|
||||||
|
else debug('addDnsRecords: records %j added with changeIds %j', records, changeIds);
|
||||||
|
|
||||||
|
retryCallback(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, function (error) {
|
||||||
|
gUpdatingDns = false;
|
||||||
|
|
||||||
|
debug('addDnsRecords: done updating records with error:', error);
|
||||||
|
|
||||||
|
callback(error);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,51 +494,9 @@ function reboot(callback) {
|
|||||||
shell.sudo('reboot', [ REBOOT_CMD ], callback);
|
shell.sudo('reboot', [ REBOOT_CMD ], callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrate(size, region, callback) {
|
function update(boxUpdateInfo, auditSource, callback) {
|
||||||
assert.strictEqual(typeof size, 'string');
|
|
||||||
assert.strictEqual(typeof region, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
var error = locker.lock(locker.OP_MIGRATE);
|
|
||||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
|
||||||
|
|
||||||
function unlock(error) {
|
|
||||||
if (error) {
|
|
||||||
debug('Failed to migrate', error);
|
|
||||||
locker.unlock(locker.OP_MIGRATE);
|
|
||||||
} else {
|
|
||||||
debug('Migration initiated successfully');
|
|
||||||
// do not unlock; cloudron is migrating
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// initiate the migration in the background
|
|
||||||
backupBoxAndApps(function (error, restoreKey) {
|
|
||||||
if (error) return unlock(error);
|
|
||||||
|
|
||||||
debug('migrate: size %s region %s restoreKey %s', size, region, restoreKey);
|
|
||||||
|
|
||||||
superagent
|
|
||||||
.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/migrate')
|
|
||||||
.query({ token: config.token() })
|
|
||||||
.send({ size: size, region: region, restoreKey: restoreKey })
|
|
||||||
.end(function (error, result) {
|
|
||||||
if (error) return unlock(error);
|
|
||||||
if (result.status === 409) return unlock(new CloudronError(CloudronError.BAD_STATE));
|
|
||||||
if (result.status === 404) return unlock(new CloudronError(CloudronError.NOT_FOUND));
|
|
||||||
if (result.status !== 202) return unlock(new CloudronError(CloudronError.EXTERNAL_ERROR, util.format('%s %j', result.status, result.body)));
|
|
||||||
|
|
||||||
return unlock(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
callback(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(boxUpdateInfo, callback) {
|
|
||||||
assert.strictEqual(typeof boxUpdateInfo, 'object');
|
assert.strictEqual(typeof boxUpdateInfo, 'object');
|
||||||
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
if (!boxUpdateInfo) return callback(null);
|
if (!boxUpdateInfo) return callback(null);
|
||||||
@@ -416,12 +504,22 @@ function update(boxUpdateInfo, callback) {
|
|||||||
var error = locker.lock(locker.OP_BOX_UPDATE);
|
var error = locker.lock(locker.OP_BOX_UPDATE);
|
||||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
||||||
|
|
||||||
|
eventlog.add(eventlog.ACTION_UPDATE, auditSource, { boxUpdateInfo: boxUpdateInfo });
|
||||||
|
|
||||||
|
// ensure tools can 'wait' on progress
|
||||||
|
progress.set(progress.UPDATE, 0, 'Starting');
|
||||||
|
|
||||||
// initiate the update/upgrade but do not wait for it
|
// initiate the update/upgrade but do not wait for it
|
||||||
if (boxUpdateInfo.upgrade) {
|
if (config.version().match(/[-+]/) !== null && config.version().replace(/[-+].*/, '') === boxUpdateInfo.version) {
|
||||||
|
doShortCircuitUpdate(boxUpdateInfo, function (error) {
|
||||||
|
if (error) debug('Short-circuit update failed', error);
|
||||||
|
locker.unlock(locker.OP_BOX_UPDATE);
|
||||||
|
});
|
||||||
|
} else if (boxUpdateInfo.upgrade) {
|
||||||
debug('Starting upgrade');
|
debug('Starting upgrade');
|
||||||
doUpgrade(boxUpdateInfo, function (error) {
|
doUpgrade(boxUpdateInfo, function (error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
debug('Upgrade failed with error: %s', error);
|
console.error('Upgrade failed with error:', error);
|
||||||
locker.unlock(locker.OP_BOX_UPDATE);
|
locker.unlock(locker.OP_BOX_UPDATE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -429,7 +527,7 @@ function update(boxUpdateInfo, callback) {
|
|||||||
debug('Starting update');
|
debug('Starting update');
|
||||||
doUpdate(boxUpdateInfo, function (error) {
|
doUpdate(boxUpdateInfo, function (error) {
|
||||||
if (error) {
|
if (error) {
|
||||||
debug('Update failed with error: %s', error);
|
console.error('Update failed with error:', error);
|
||||||
locker.unlock(locker.OP_BOX_UPDATE);
|
locker.unlock(locker.OP_BOX_UPDATE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -438,6 +536,27 @@ function update(boxUpdateInfo, callback) {
|
|||||||
callback(null);
|
callback(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function updateToLatest(auditSource, callback) {
|
||||||
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var boxUpdateInfo = updateChecker.getUpdateInfo().box;
|
||||||
|
if (!boxUpdateInfo) return callback(new CloudronError(CloudronError.ALREADY_UPTODATE, 'No update available'));
|
||||||
|
|
||||||
|
update(boxUpdateInfo, auditSource, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function doShortCircuitUpdate(boxUpdateInfo, callback) {
|
||||||
|
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
|
||||||
|
|
||||||
|
debug('Starting short-circuit from prerelease version %s to release version %s', config.version(), boxUpdateInfo.version);
|
||||||
|
config.setVersion(boxUpdateInfo.version);
|
||||||
|
progress.clear(progress.UPDATE);
|
||||||
|
updateChecker.resetUpdateInfo();
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
function doUpgrade(boxUpdateInfo, callback) {
|
function doUpgrade(boxUpdateInfo, callback) {
|
||||||
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
|
assert(boxUpdateInfo !== null && typeof boxUpdateInfo === 'object');
|
||||||
|
|
||||||
@@ -446,23 +565,23 @@ function doUpgrade(boxUpdateInfo, callback) {
|
|||||||
callback(e);
|
callback(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.set(progress.UPDATE, 5, 'Create app and box backup for upgrade');
|
progress.set(progress.UPDATE, 5, 'Backing up for upgrade');
|
||||||
|
|
||||||
backupBoxAndApps(function (error) {
|
backups.backupBoxAndApps({ userId: null, username: 'upgrader' }, function (error) {
|
||||||
if (error) return upgradeError(error);
|
if (error) return upgradeError(error);
|
||||||
|
|
||||||
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
|
superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade')
|
||||||
.query({ token: config.token() })
|
.query({ token: config.token() })
|
||||||
.send({ version: boxUpdateInfo.version })
|
.send({ version: boxUpdateInfo.version })
|
||||||
.end(function (error, result) {
|
.end(function (error, result) {
|
||||||
if (error) return upgradeError(new Error('Error making upgrade request: ' + error));
|
if (error && !error.response) return upgradeError(new Error('Network error making upgrade request: ' + error));
|
||||||
if (result.status !== 202) return upgradeError(new Error('Server not ready to upgrade: ' + result.body));
|
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');
|
progress.set(progress.UPDATE, 10, 'Updating base system');
|
||||||
|
|
||||||
// no need to unlock since this is the last thing we ever do on this box
|
// no need to unlock since this is the last thing we ever do on this box
|
||||||
|
callback();
|
||||||
callback(null);
|
retire();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -475,161 +594,115 @@ function doUpdate(boxUpdateInfo, callback) {
|
|||||||
callback(e);
|
callback(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.set(progress.UPDATE, 5, 'Create box backup for update');
|
progress.set(progress.UPDATE, 5, 'Backing up for update');
|
||||||
|
|
||||||
backupBox(function (error) {
|
backups.backupBoxAndApps({ userId: null, username: 'updater' }, function (error) {
|
||||||
if (error) return updateError(error);
|
if (error) return updateError(error);
|
||||||
|
|
||||||
// fetch a signed sourceTarballUrl
|
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
|
||||||
superagent.get(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/sourcetarballurl')
|
var args = {
|
||||||
.query({ token: config.token(), boxVersion: boxUpdateInfo.version })
|
sourceTarballUrl: boxUpdateInfo.sourceTarballUrl,
|
||||||
.end(function (error, result) {
|
|
||||||
if (error) return updateError(new Error('Error fetching sourceTarballUrl: ' + error));
|
|
||||||
if (result.status !== 200) return updateError(new Error('Error fetching sourceTarballUrl status: ' + result.status));
|
|
||||||
if (!safe.query(result, 'body.url')) return updateError(new Error('Error fetching sourceTarballUrl response: ' + result.body));
|
|
||||||
|
|
||||||
// NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic
|
// this data is opaque to the installer
|
||||||
var args = {
|
data: {
|
||||||
sourceTarballUrl: result.body.url,
|
token: config.token(),
|
||||||
|
apiServerOrigin: config.apiServerOrigin(),
|
||||||
|
webServerOrigin: config.webServerOrigin(),
|
||||||
|
fqdn: config.fqdn(),
|
||||||
|
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
|
||||||
|
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
|
||||||
|
isCustomDomain: config.isCustomDomain(),
|
||||||
|
|
||||||
// IMPORTANT: if you change this, fix up argparser.sh as well. keep these sorted for readability
|
appstore: {
|
||||||
data: {
|
|
||||||
apiServerOrigin: config.apiServerOrigin(),
|
|
||||||
boxVersionsUrl: config.get('boxVersionsUrl'),
|
|
||||||
fqdn: config.fqdn(),
|
|
||||||
isCustomDomain: config.isCustomDomain(),
|
|
||||||
restoreKey: null,
|
|
||||||
restoreUrl: null,
|
|
||||||
tlsKey: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.key'), 'utf8'),
|
|
||||||
tlsCert: fs.readFileSync(path.join(paths.NGINX_CERT_DIR, 'host.cert'), 'utf8'),
|
|
||||||
token: config.token(),
|
token: config.token(),
|
||||||
version: boxUpdateInfo.version,
|
apiServerOrigin: config.apiServerOrigin()
|
||||||
|
},
|
||||||
|
caas: {
|
||||||
|
token: config.token(),
|
||||||
|
apiServerOrigin: config.apiServerOrigin(),
|
||||||
webServerOrigin: config.webServerOrigin()
|
webServerOrigin: config.webServerOrigin()
|
||||||
}
|
},
|
||||||
};
|
|
||||||
|
|
||||||
debug('updating box %j', args);
|
version: boxUpdateInfo.version,
|
||||||
|
boxVersionsUrl: config.get('boxVersionsUrl')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
|
debug('updating box %j', args);
|
||||||
if (error) return updateError(error);
|
|
||||||
if (result.status !== 202) return updateError(new Error('Error initiating update: ' + result.body));
|
|
||||||
|
|
||||||
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
|
superagent.post(INSTALLER_UPDATE_URL).send(args).end(function (error, result) {
|
||||||
|
if (error && !error.response) return updateError(error);
|
||||||
|
if (result.statusCode !== 202) return updateError(new Error('Error initiating update: ' + JSON.stringify(result.body)));
|
||||||
|
|
||||||
callback(null);
|
progress.set(progress.UPDATE, 10, 'Updating cloudron software');
|
||||||
});
|
|
||||||
|
callback(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Do not add any code here. The installer script will stop the box code any instant
|
// Do not add any code here. The installer script will stop the box code any instant
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function backup(callback) {
|
function installAppBundle(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
callback = callback || NOOP_CALLBACK;
|
||||||
|
|
||||||
var error = locker.lock(locker.OP_FULL_BACKUP);
|
var bundle = config.get('appBundle');
|
||||||
if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message));
|
|
||||||
|
|
||||||
// start the backup operation in the background
|
if (!bundle || bundle.length === 0) {
|
||||||
backupBoxAndApps(function (error) {
|
debug('installAppBundle: no bundle set');
|
||||||
if (error) console.error('backup failed.', error);
|
return callback();
|
||||||
|
}
|
||||||
|
|
||||||
locker.unlock(locker.OP_FULL_BACKUP);
|
async.eachSeries(bundle, function (appInfo, iteratorCallback) {
|
||||||
|
debug('autoInstall: installing %s at %s', appInfo.appstoreId, appInfo.location);
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
appStoreId: appInfo.appstoreId,
|
||||||
|
location: appInfo.location,
|
||||||
|
portBindings: appInfo.portBindings || null,
|
||||||
|
accessRestriction: appInfo.accessRestriction || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
apps.install(data, { userId: null, username: 'autoinstaller' }, iteratorCallback);
|
||||||
|
}, function (error) {
|
||||||
|
if (error) debug('autoInstallApps: ', error);
|
||||||
|
|
||||||
|
callback();
|
||||||
});
|
});
|
||||||
|
|
||||||
callback(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureBackup(callback) {
|
function checkDiskSpace(callback) {
|
||||||
callback = callback || function () { };
|
callback = callback || NOOP_CALLBACK;
|
||||||
|
|
||||||
backups.getAllPaged(1, 1, function (error, backups) {
|
debug('Checking disk space');
|
||||||
|
|
||||||
|
df(function (error, entries) {
|
||||||
if (error) {
|
if (error) {
|
||||||
debug('Unable to list backups', error);
|
debug('df error %s', error.message);
|
||||||
return callback(error); // no point trying to backup if appstore is down
|
mailer.outOfDiskSpace(error.message);
|
||||||
|
return callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago
|
var oos = entries.some(function (entry) {
|
||||||
debug('Previous backup was %j, no need to backup now', backups[0]);
|
return (entry.mount === paths.DATA_DIR && entry.capacity >= 0.90) ||
|
||||||
return callback(null);
|
(entry.mount === '/' && entry.used <= (1.25 * 1024 * 1024)); // 1.5G
|
||||||
}
|
|
||||||
|
|
||||||
backup(callback);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function backupBoxWithAppBackupIds(appBackupIds, callback) {
|
|
||||||
assert(util.isArray(appBackupIds));
|
|
||||||
|
|
||||||
backups.getBackupUrl(null /* app */, appBackupIds, function (error, result) {
|
|
||||||
if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message));
|
|
||||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
|
||||||
|
|
||||||
debug('backup: url %s', result.url);
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])),
|
|
||||||
shell.sudo.bind(null, 'backupBox', [ BACKUP_BOX_CMD, result.url, result.backupKey ]),
|
|
||||||
ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])),
|
|
||||||
], function (error) {
|
|
||||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
|
||||||
|
|
||||||
debug('backup: successful');
|
|
||||||
|
|
||||||
callback(null, result.id);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
debug('Disk space checked. ok: %s', !oos);
|
||||||
|
|
||||||
|
if (oos) mailer.outOfDiskSpace(JSON.stringify(entries, null, 4));
|
||||||
|
|
||||||
|
callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// this function expects you to have a lock
|
function retire(callback) {
|
||||||
function backupBox(callback) {
|
callback = callback || NOOP_CALLBACK;
|
||||||
apps.getAll(function (error, allApps) {
|
|
||||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
|
||||||
|
|
||||||
var appBackupIds = allApps.map(function (app) { return app.lastBackupId; });
|
var data = {
|
||||||
appBackupIds = appBackupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
|
isCustomDomain: config.isCustomDomain(),
|
||||||
|
fqdn: config.fqdn()
|
||||||
backupBoxWithAppBackupIds(appBackupIds, callback);
|
};
|
||||||
});
|
shell.sudo('retire', [ RETIRE_CMD, JSON.stringify(data) ], callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
// this function expects you to have a lock
|
|
||||||
function backupBoxAndApps(callback) {
|
|
||||||
callback = callback || function () { }; // callback can be empty for timer triggered backup
|
|
||||||
|
|
||||||
apps.getAll(function (error, allApps) {
|
|
||||||
if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error));
|
|
||||||
|
|
||||||
var processed = 0;
|
|
||||||
var step = 100/(allApps.length+1);
|
|
||||||
|
|
||||||
progress.set(progress.BACKUP, processed, '');
|
|
||||||
|
|
||||||
async.mapSeries(allApps, function iterator(app, iteratorCallback) {
|
|
||||||
++processed;
|
|
||||||
|
|
||||||
apps.backupApp(app, app.manifest.addons, function (error, backupId) {
|
|
||||||
progress.set(progress.BACKUP, step * processed, 'Backing up app at ' + app.location);
|
|
||||||
|
|
||||||
if (error && error.reason === AppsError.BAD_STATE) {
|
|
||||||
debugApp(app, 'Skipping backup (istate:%s health:%s). using lastBackupId:%s', app.installationState, app.health, app.lastBackupId);
|
|
||||||
backupId = app.lastBackupId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return iteratorCallback(null, backupId);
|
|
||||||
});
|
|
||||||
}, function appsBackedUp(error, backupIds) {
|
|
||||||
if (error) {
|
|
||||||
progress.set(progress.BACKUP, 100, error.message);
|
|
||||||
return callback(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up
|
|
||||||
|
|
||||||
backupBoxWithAppBackupIds(backupIds, function (error, restoreKey) {
|
|
||||||
progress.set(progress.BACKUP, 100, error ? error.message : '');
|
|
||||||
callback(error, restoreKey);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
LoadPlugin "table"
|
LoadPlugin "table"
|
||||||
<Plugin table>
|
<Plugin table>
|
||||||
<Table "/sys/fs/cgroup/memory/system.slice/docker-<%= containerId %>.scope/memory.stat">
|
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.stat">
|
||||||
Instance "<%= appId %>-memory"
|
Instance "<%= appId %>-memory"
|
||||||
Separator " \\n"
|
Separator " \\n"
|
||||||
<Result>
|
<Result>
|
||||||
@@ -10,7 +10,7 @@ LoadPlugin "table"
|
|||||||
</Result>
|
</Result>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<Table "/sys/fs/cgroup/memory/system.slice/docker-<%= containerId %>.scope/memory.max_usage_in_bytes">
|
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.max_usage_in_bytes">
|
||||||
Instance "<%= appId %>-memory"
|
Instance "<%= appId %>-memory"
|
||||||
Separator "\\n"
|
Separator "\\n"
|
||||||
<Result>
|
<Result>
|
||||||
@@ -20,7 +20,7 @@ LoadPlugin "table"
|
|||||||
</Result>
|
</Result>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<Table "/sys/fs/cgroup/cpuacct/system.slice/docker-<%= containerId %>.scope/cpuacct.stat">
|
<Table "/sys/fs/cgroup/cpuacct/docker/<%= containerId %>/cpuacct.stat">
|
||||||
Instance "<%= appId %>-cpu"
|
Instance "<%= appId %>-cpu"
|
||||||
Separator " \\n"
|
Separator " \\n"
|
||||||
<Result>
|
<Result>
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/* jslint node: true */
|
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
@@ -15,23 +13,29 @@ exports = module.exports = {
|
|||||||
TEST: process.env.BOX_ENV === 'test',
|
TEST: process.env.BOX_ENV === 'test',
|
||||||
|
|
||||||
// convenience getters
|
// convenience getters
|
||||||
|
provider: provider,
|
||||||
apiServerOrigin: apiServerOrigin,
|
apiServerOrigin: apiServerOrigin,
|
||||||
webServerOrigin: webServerOrigin,
|
webServerOrigin: webServerOrigin,
|
||||||
fqdn: fqdn,
|
fqdn: fqdn,
|
||||||
token: token,
|
token: token,
|
||||||
version: version,
|
version: version,
|
||||||
|
setVersion: setVersion,
|
||||||
isCustomDomain: isCustomDomain,
|
isCustomDomain: isCustomDomain,
|
||||||
database: database,
|
database: database,
|
||||||
|
|
||||||
// these values are derived
|
// these values are derived
|
||||||
adminOrigin: adminOrigin,
|
adminOrigin: adminOrigin,
|
||||||
|
internalAdminOrigin: internalAdminOrigin,
|
||||||
|
sysadminOrigin: sysadminOrigin, // caas routes
|
||||||
|
adminFqdn: adminFqdn,
|
||||||
|
mailFqdn: mailFqdn,
|
||||||
appFqdn: appFqdn,
|
appFqdn: appFqdn,
|
||||||
zoneName: zoneName,
|
zoneName: zoneName,
|
||||||
|
|
||||||
isDev: isDev,
|
isDev: isDev,
|
||||||
|
|
||||||
// for testing resets to defaults
|
// for testing resets to defaults
|
||||||
_reset: initConfig
|
_reset: _reset
|
||||||
};
|
};
|
||||||
|
|
||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
@@ -56,20 +60,30 @@ function saveSync() {
|
|||||||
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
|
fs.writeFileSync(cloudronConfigFileName, JSON.stringify(data, null, 4)); // functions are ignored by JSON.stringify
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _reset (callback) {
|
||||||
|
safe.fs.unlinkSync(cloudronConfigFileName);
|
||||||
|
|
||||||
|
initConfig();
|
||||||
|
|
||||||
|
if (callback) callback();
|
||||||
|
}
|
||||||
|
|
||||||
function initConfig() {
|
function initConfig() {
|
||||||
// setup defaults
|
// setup defaults
|
||||||
data.fqdn = 'localhost';
|
data.fqdn = 'localhost';
|
||||||
|
|
||||||
data.token = null;
|
data.token = null;
|
||||||
data.mailServer = null;
|
|
||||||
data.adminEmail = null;
|
|
||||||
data.mailDnsRecordIds = [ ];
|
|
||||||
data.boxVersionsUrl = null;
|
data.boxVersionsUrl = null;
|
||||||
data.version = null;
|
data.version = null;
|
||||||
data.isCustomDomain = false;
|
data.isCustomDomain = false;
|
||||||
data.webServerOrigin = null;
|
data.webServerOrigin = null;
|
||||||
data.internalPort = 3001;
|
data.smtpPort = 2525; // // this value comes from mail container
|
||||||
|
data.sysadminPort = 3001;
|
||||||
data.ldapPort = 3002;
|
data.ldapPort = 3002;
|
||||||
|
data.oauthProxyPort = 3003;
|
||||||
|
data.simpleAuthPort = 3004;
|
||||||
|
data.provider = 'caas';
|
||||||
|
data.appBundle = [ ];
|
||||||
|
|
||||||
if (exports.CLOUDRON) {
|
if (exports.CLOUDRON) {
|
||||||
data.port = 3000;
|
data.port = 3000;
|
||||||
@@ -99,6 +113,9 @@ function initConfig() {
|
|||||||
saveSync();
|
saveSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanup any old config file we have for tests
|
||||||
|
if (exports.TEST) safe.fs.unlinkSync(cloudronConfigFileName);
|
||||||
|
|
||||||
initConfig();
|
initConfig();
|
||||||
|
|
||||||
// set(obj) or set(key, value)
|
// set(obj) or set(key, value)
|
||||||
@@ -141,10 +158,26 @@ function appFqdn(location) {
|
|||||||
return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn();
|
return isCustomDomain() ? location + '.' + fqdn() : location + '-' + fqdn();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function adminFqdn() {
|
||||||
|
return appFqdn(constants.ADMIN_LOCATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mailFqdn() {
|
||||||
|
return appFqdn(constants.MAIL_LOCATION);
|
||||||
|
}
|
||||||
|
|
||||||
function adminOrigin() {
|
function adminOrigin() {
|
||||||
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
|
return 'https://' + appFqdn(constants.ADMIN_LOCATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function internalAdminOrigin() {
|
||||||
|
return 'http://127.0.0.1:' + get('port');
|
||||||
|
}
|
||||||
|
|
||||||
|
function sysadminOrigin() {
|
||||||
|
return 'http://127.0.0.1:' + get('sysadminPort');
|
||||||
|
}
|
||||||
|
|
||||||
function token() {
|
function token() {
|
||||||
return get('token');
|
return get('token');
|
||||||
}
|
}
|
||||||
@@ -153,6 +186,10 @@ function version() {
|
|||||||
return get('version');
|
return get('version');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setVersion(version) {
|
||||||
|
set('version', version);
|
||||||
|
}
|
||||||
|
|
||||||
function isCustomDomain() {
|
function isCustomDomain() {
|
||||||
return get('isCustomDomain');
|
return get('isCustomDomain');
|
||||||
}
|
}
|
||||||
@@ -172,3 +209,6 @@ function isDev() {
|
|||||||
return /dev/i.test(get('boxVersionsUrl'));
|
return /dev/i.test(get('boxVersionsUrl'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function provider() {
|
||||||
|
return get('provider');
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,13 +4,16 @@
|
|||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
ADMIN_LOCATION: 'my',
|
ADMIN_LOCATION: 'my',
|
||||||
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
|
API_LOCATION: 'api', // this is unused but reserved for future use (#403)
|
||||||
|
SMTP_LOCATION: 'smtp',
|
||||||
|
IMAP_LOCATION: 'imap',
|
||||||
|
MAIL_LOCATION: 'my', // not a typo! should be same as admin location until we figure out certificates
|
||||||
|
POSTMAN_LOCATION: 'postman', // used in dovecot bounces
|
||||||
|
|
||||||
ADMIN_NAME: 'Settings',
|
ADMIN_NAME: 'Settings',
|
||||||
|
|
||||||
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
|
ADMIN_CLIENT_ID: 'webadmin', // oauth client id
|
||||||
ADMIN_APPID: 'admin', // admin appid (settingsdb)
|
ADMIN_APPID: 'admin', // admin appid (settingsdb)
|
||||||
|
|
||||||
TEST_NAME: 'Test',
|
DEFAULT_MEMORY_LIMIT: (256 * 1024 * 1024) // see also client.js
|
||||||
TEST_LOCATION: '',
|
|
||||||
TEST_CLIENT_ID: 'test'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
122
src/cron.js
122
src/cron.js
@@ -7,9 +7,14 @@ exports = module.exports = {
|
|||||||
|
|
||||||
var apps = require('./apps.js'),
|
var apps = require('./apps.js'),
|
||||||
assert = require('assert'),
|
assert = require('assert'),
|
||||||
|
backups = require('./backups.js'),
|
||||||
|
certificates = require('./certificates.js'),
|
||||||
cloudron = require('./cloudron.js'),
|
cloudron = require('./cloudron.js'),
|
||||||
|
config = require('./config.js'),
|
||||||
CronJob = require('cron').CronJob,
|
CronJob = require('cron').CronJob,
|
||||||
debug = require('debug')('box:cron'),
|
debug = require('debug')('box:cron'),
|
||||||
|
janitor = require('./janitor.js'),
|
||||||
|
scheduler = require('./scheduler.js'),
|
||||||
settings = require('./settings.js'),
|
settings = require('./settings.js'),
|
||||||
updateChecker = require('./updatechecker.js');
|
updateChecker = require('./updatechecker.js');
|
||||||
|
|
||||||
@@ -17,11 +22,15 @@ var gAutoupdaterJob = null,
|
|||||||
gBoxUpdateCheckerJob = null,
|
gBoxUpdateCheckerJob = null,
|
||||||
gAppUpdateCheckerJob = null,
|
gAppUpdateCheckerJob = null,
|
||||||
gHeartbeatJob = null,
|
gHeartbeatJob = null,
|
||||||
gBackupJob = null;
|
gBackupJob = null,
|
||||||
|
gCleanupTokensJob = null,
|
||||||
var gInitialized = false;
|
gDockerVolumeCleanerJob = null,
|
||||||
|
gSchedulerSyncJob = null,
|
||||||
|
gCertificateRenewJob = null,
|
||||||
|
gCheckDiskSpaceJob = null;
|
||||||
|
|
||||||
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
||||||
|
var AUDIT_SOURCE = { userId: null, username: 'cron' };
|
||||||
|
|
||||||
// cron format
|
// cron format
|
||||||
// Seconds: 0-59
|
// Seconds: 0-59
|
||||||
@@ -34,32 +43,39 @@ var NOOP_CALLBACK = function (error) { if (error) console.error(error); };
|
|||||||
function initialize(callback) {
|
function initialize(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
if (gInitialized) return callback();
|
gHeartbeatJob = new CronJob({
|
||||||
|
cronTime: '00 */1 * * * *', // every minute
|
||||||
|
onTick: cloudron.sendHeartbeat,
|
||||||
|
start: true
|
||||||
|
});
|
||||||
|
cloudron.sendHeartbeat(); // latest unpublished version of CronJob has runOnInit
|
||||||
|
|
||||||
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
|
if (cloudron.isConfiguredSync()) {
|
||||||
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
recreateJobs(callback);
|
||||||
|
} else {
|
||||||
gInitialized = true;
|
cloudron.events.on(cloudron.EVENT_ACTIVATED, recreateJobs);
|
||||||
|
callback();
|
||||||
recreateJobs(callback);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function recreateJobs(unusedTimeZone, callback) {
|
function recreateJobs(unusedTimeZone, callback) {
|
||||||
if (typeof unusedTimeZone === 'function') callback = unusedTimeZone;
|
if (typeof unusedTimeZone === 'function') callback = unusedTimeZone;
|
||||||
|
|
||||||
settings.getAll(function (error, allSettings) {
|
settings.getAll(function (error, allSettings) {
|
||||||
if (gHeartbeatJob) gHeartbeatJob.stop();
|
debug('Creating jobs with timezone %s', allSettings[settings.TIME_ZONE_KEY]);
|
||||||
gHeartbeatJob = new CronJob({
|
|
||||||
cronTime: '00 */1 * * * *', // every minute
|
|
||||||
onTick: cloudron.sendHeartbeat,
|
|
||||||
start: true,
|
|
||||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (gBackupJob) gBackupJob.stop();
|
if (gBackupJob) gBackupJob.stop();
|
||||||
gBackupJob = new CronJob({
|
gBackupJob = new CronJob({
|
||||||
cronTime: '00 00 */4 * * *', // every 4 hours
|
cronTime: '00 00 */4 * * *', // every 4 hours
|
||||||
onTick: cloudron.ensureBackup,
|
onTick: backups.ensureBackup.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||||
|
start: true,
|
||||||
|
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (gCheckDiskSpaceJob) gCheckDiskSpaceJob.stop();
|
||||||
|
gCheckDiskSpaceJob = new CronJob({
|
||||||
|
cronTime: '00 30 */4 * * *', // every 4 hours
|
||||||
|
onTick: cloudron.checkDiskSpace,
|
||||||
start: true,
|
start: true,
|
||||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||||
});
|
});
|
||||||
@@ -80,14 +96,52 @@ function recreateJobs(unusedTimeZone, callback) {
|
|||||||
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (gCleanupTokensJob) gCleanupTokensJob.stop();
|
||||||
|
gCleanupTokensJob = new CronJob({
|
||||||
|
cronTime: '00 */30 * * * *', // every 30 minutes
|
||||||
|
onTick: janitor.cleanupTokens,
|
||||||
|
start: true,
|
||||||
|
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop();
|
||||||
|
gDockerVolumeCleanerJob = new CronJob({
|
||||||
|
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||||
|
onTick: janitor.cleanupDockerVolumes,
|
||||||
|
start: true,
|
||||||
|
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (gSchedulerSyncJob) gSchedulerSyncJob.stop();
|
||||||
|
gSchedulerSyncJob = new CronJob({
|
||||||
|
cronTime: config.TEST ? '*/10 * * * * *' : '00 */1 * * * *', // every minute
|
||||||
|
onTick: scheduler.sync,
|
||||||
|
start: true,
|
||||||
|
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (gCertificateRenewJob) gCertificateRenewJob.stop();
|
||||||
|
gCertificateRenewJob = new CronJob({
|
||||||
|
cronTime: '00 00 */12 * * *', // every 12 hours
|
||||||
|
onTick: certificates.renewAll.bind(null, AUDIT_SOURCE, NOOP_CALLBACK),
|
||||||
|
start: true,
|
||||||
|
timeZone: allSettings[settings.TIME_ZONE_KEY]
|
||||||
|
});
|
||||||
|
|
||||||
|
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||||
|
settings.events.on(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||||
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
|
autoupdatePatternChanged(allSettings[settings.AUTOUPDATE_PATTERN_KEY]);
|
||||||
|
|
||||||
|
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
|
||||||
|
settings.events.on(settings.TIME_ZONE_KEY, recreateJobs);
|
||||||
|
|
||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoupdatePatternChanged(pattern) {
|
function autoupdatePatternChanged(pattern) {
|
||||||
assert.strictEqual(typeof pattern, 'string');
|
assert.strictEqual(typeof pattern, 'string');
|
||||||
|
assert(gBoxUpdateCheckerJob);
|
||||||
|
|
||||||
debug('Auto update pattern changed to %s', pattern);
|
debug('Auto update pattern changed to %s', pattern);
|
||||||
|
|
||||||
@@ -101,41 +155,53 @@ function autoupdatePatternChanged(pattern) {
|
|||||||
var updateInfo = updateChecker.getUpdateInfo();
|
var updateInfo = updateChecker.getUpdateInfo();
|
||||||
if (updateInfo.box) {
|
if (updateInfo.box) {
|
||||||
debug('Starting autoupdate to %j', updateInfo.box);
|
debug('Starting autoupdate to %j', updateInfo.box);
|
||||||
cloudron.update(updateInfo.box, NOOP_CALLBACK);
|
cloudron.updateToLatest(AUDIT_SOURCE, NOOP_CALLBACK);
|
||||||
} else if (updateInfo.apps) {
|
} else if (updateInfo.apps) {
|
||||||
debug('Starting app update to %j', updateInfo.apps);
|
debug('Starting app update to %j', updateInfo.apps);
|
||||||
apps.autoupdateApps(updateInfo.apps, NOOP_CALLBACK);
|
apps.updateApps(updateInfo.apps, AUDIT_SOURCE, NOOP_CALLBACK);
|
||||||
} else {
|
} else {
|
||||||
debug('No auto updates available');
|
debug('No auto updates available');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
start: true,
|
start: true,
|
||||||
timeZone: gBoxUpdateCheckerJob.cronTime.timeZone // hack
|
timeZone: gBoxUpdateCheckerJob.cronTime.zone // hack
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function uninitialize(callback) {
|
function uninitialize(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
if (!gInitialized) return callback();
|
cloudron.events.removeListener(cloudron.EVENT_ACTIVATED, recreateJobs);
|
||||||
|
|
||||||
|
settings.events.removeListener(settings.TIME_ZONE_KEY, recreateJobs);
|
||||||
|
settings.events.removeListener(settings.AUTOUPDATE_PATTERN_KEY, autoupdatePatternChanged);
|
||||||
|
|
||||||
if (gAutoupdaterJob) gAutoupdaterJob.stop();
|
if (gAutoupdaterJob) gAutoupdaterJob.stop();
|
||||||
gAutoupdaterJob = null;
|
gAutoupdaterJob = null;
|
||||||
|
|
||||||
gBoxUpdateCheckerJob.stop();
|
if (gBoxUpdateCheckerJob) gBoxUpdateCheckerJob.stop();
|
||||||
gBoxUpdateCheckerJob = null;
|
gBoxUpdateCheckerJob = null;
|
||||||
|
|
||||||
gAppUpdateCheckerJob.stop();
|
if (gAppUpdateCheckerJob) gAppUpdateCheckerJob.stop();
|
||||||
gAppUpdateCheckerJob = null;
|
gAppUpdateCheckerJob = null;
|
||||||
|
|
||||||
gHeartbeatJob.stop();
|
if (gHeartbeatJob) gHeartbeatJob.stop();
|
||||||
gHeartbeatJob = null;
|
gHeartbeatJob = null;
|
||||||
|
|
||||||
gBackupJob.stop();
|
if (gBackupJob) gBackupJob.stop();
|
||||||
gBackupJob = null;
|
gBackupJob = null;
|
||||||
|
|
||||||
gInitialized = false;
|
if (gCleanupTokensJob) gCleanupTokensJob.stop();
|
||||||
|
gCleanupTokensJob = null;
|
||||||
|
|
||||||
|
if (gDockerVolumeCleanerJob) gDockerVolumeCleanerJob.stop();
|
||||||
|
gDockerVolumeCleanerJob = null;
|
||||||
|
|
||||||
|
if (gSchedulerSyncJob) gSchedulerSyncJob.stop();
|
||||||
|
gSchedulerSyncJob = null;
|
||||||
|
|
||||||
|
if (gCertificateRenewJob) gCertificateRenewJob.stop();
|
||||||
|
gCertificateRenewJob = null;
|
||||||
|
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
/* jslint node: true */
|
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
@@ -116,16 +114,22 @@ function clear(callback) {
|
|||||||
async.series([
|
async.series([
|
||||||
require('./appdb.js')._clear,
|
require('./appdb.js')._clear,
|
||||||
require('./authcodedb.js')._clear,
|
require('./authcodedb.js')._clear,
|
||||||
|
require('./backupdb.js')._clear,
|
||||||
require('./clientdb.js')._clear,
|
require('./clientdb.js')._clear,
|
||||||
require('./tokendb.js')._clear,
|
require('./tokendb.js')._clear,
|
||||||
|
require('./groupdb.js')._clear,
|
||||||
require('./userdb.js')._clear,
|
require('./userdb.js')._clear,
|
||||||
require('./settingsdb.js')._clear
|
require('./settingsdb.js')._clear,
|
||||||
|
require('./eventlogdb.js')._clear,
|
||||||
|
require('./mailboxdb.js')._clear
|
||||||
], callback);
|
], callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
function beginTransaction(callback) {
|
function beginTransaction(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
if (gConnectionPool === null) return callback(new Error('No database connection pool.'));
|
||||||
|
|
||||||
gConnectionPool.getConnection(function (error, connection) {
|
gConnectionPool.getConnection(function (error, connection) {
|
||||||
if (error) return callback(error);
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
|||||||
@@ -30,3 +30,4 @@ DatabaseError.INTERNAL_ERROR = 'Internal error';
|
|||||||
DatabaseError.ALREADY_EXISTS = 'Entry already exist';
|
DatabaseError.ALREADY_EXISTS = 'Entry already exist';
|
||||||
DatabaseError.NOT_FOUND = 'Record not found';
|
DatabaseError.NOT_FOUND = 'Record not found';
|
||||||
DatabaseError.BAD_FIELD = 'Invalid field';
|
DatabaseError.BAD_FIELD = 'Invalid field';
|
||||||
|
DatabaseError.IN_USE = 'In Use';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
exports = module.exports = {
|
exports = module.exports = {
|
||||||
DeveloperError: DeveloperError,
|
DeveloperError: DeveloperError,
|
||||||
|
|
||||||
enabled: enabled,
|
isEnabled: isEnabled,
|
||||||
setEnabled: setEnabled,
|
setEnabled: setEnabled,
|
||||||
issueDeveloperToken: issueDeveloperToken,
|
issueDeveloperToken: issueDeveloperToken,
|
||||||
getNonApprovedApps: getNonApprovedApps
|
getNonApprovedApps: getNonApprovedApps
|
||||||
@@ -13,6 +13,9 @@ exports = module.exports = {
|
|||||||
|
|
||||||
var assert = require('assert'),
|
var assert = require('assert'),
|
||||||
config = require('./config.js'),
|
config = require('./config.js'),
|
||||||
|
clients = require('./clients.js'),
|
||||||
|
debug = require('debug')('box:developer'),
|
||||||
|
eventlog = require('./eventlog.js'),
|
||||||
tokendb = require('./tokendb.js'),
|
tokendb = require('./tokendb.js'),
|
||||||
settings = require('./settings.js'),
|
settings = require('./settings.js'),
|
||||||
superagent = require('superagent'),
|
superagent = require('superagent'),
|
||||||
@@ -38,8 +41,9 @@ function DeveloperError(reason, errorOrMessage) {
|
|||||||
}
|
}
|
||||||
util.inherits(DeveloperError, Error);
|
util.inherits(DeveloperError, Error);
|
||||||
DeveloperError.INTERNAL_ERROR = 'Internal Error';
|
DeveloperError.INTERNAL_ERROR = 'Internal Error';
|
||||||
|
DeveloperError.EXTERNAL_ERROR = 'External Error';
|
||||||
|
|
||||||
function enabled(callback) {
|
function isEnabled(callback) {
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
settings.getDeveloperMode(function (error, enabled) {
|
settings.getDeveloperMode(function (error, enabled) {
|
||||||
@@ -48,27 +52,35 @@ function enabled(callback) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setEnabled(enabled, callback) {
|
function setEnabled(enabled, auditSource, callback) {
|
||||||
assert.strictEqual(typeof enabled, 'boolean');
|
assert.strictEqual(typeof enabled, 'boolean');
|
||||||
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
settings.setDeveloperMode(enabled, function (error) {
|
settings.setDeveloperMode(enabled, function (error) {
|
||||||
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
|
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
|
eventlog.add(eventlog.ACTION_CLI_MODE, auditSource, { enabled: enabled });
|
||||||
|
|
||||||
callback(null);
|
callback(null);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function issueDeveloperToken(user, callback) {
|
function issueDeveloperToken(user, auditSource, callback) {
|
||||||
assert.strictEqual(typeof user, 'object');
|
assert.strictEqual(typeof user, 'object');
|
||||||
|
assert.strictEqual(typeof auditSource, 'object');
|
||||||
assert.strictEqual(typeof callback, 'function');
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
var token = tokendb.generateToken();
|
var token = tokendb.generateToken();
|
||||||
var expiresAt = Date.now() + 24 * 60 * 60 * 1000; // 1 day
|
var expiresAt = Date.now() + 24 * 60 * 60 * 1000; // 1 day
|
||||||
|
var scopes = '*,' + clients.SCOPE_ROLE_SDK;
|
||||||
|
|
||||||
tokendb.add(token, tokendb.PREFIX_DEV + user.id, '', expiresAt, 'apps,settings,roleDeveloper', function (error) {
|
tokendb.add(token, user.id, 'cid-cli', expiresAt, scopes, function (error) {
|
||||||
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
|
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
|
||||||
|
|
||||||
callback(null, { token: token, expiresAt: expiresAt });
|
eventlog.add(eventlog.ACTION_USER_LOGIN, auditSource, { authType: 'cli', userId: user.id, username: user.username });
|
||||||
|
|
||||||
|
callback(null, { token: token, expiresAt: new Date(expiresAt).toISOString() });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,8 +89,12 @@ function getNonApprovedApps(callback) {
|
|||||||
|
|
||||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
|
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/apps';
|
||||||
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).end(function (error, result) {
|
superagent.get(url).query({ token: config.token(), boxVersion: config.version() }).end(function (error, result) {
|
||||||
if (error) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, error));
|
if (error && !error.response) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, error));
|
||||||
if (result.status !== 200) return callback(new DeveloperError(DeveloperError.INTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
|
if (result.statusCode === 401) {
|
||||||
|
debug('Failed to list apps in development. Appstore token invalid or missing. Returning empty list.', result.body);
|
||||||
|
return callback(null, []);
|
||||||
|
}
|
||||||
|
if (result.statusCode !== 200) return callback(new DeveloperError(DeveloperError.EXTERNAL_ERROR, util.format('App listing failed. %s %j', result.status, result.body)));
|
||||||
|
|
||||||
callback(null, result.body.apps || []);
|
callback(null, result.body.apps || []);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
/* jslint node:true */
|
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
exports = module.exports = {
|
|
||||||
checkPtrRecord: checkPtrRecord
|
|
||||||
};
|
|
||||||
|
|
||||||
var assert = require('assert'),
|
|
||||||
debug = require('debug')('box:digitalocean'),
|
|
||||||
dns = require('native-dns');
|
|
||||||
|
|
||||||
function checkPtrRecord(ip, fqdn, callback) {
|
|
||||||
assert(ip === null || typeof ip === 'string');
|
|
||||||
assert.strictEqual(typeof fqdn, 'string');
|
|
||||||
assert.strictEqual(typeof callback, 'function');
|
|
||||||
|
|
||||||
debug('checkPtrRecord: ' + ip);
|
|
||||||
|
|
||||||
if (!ip) return callback(new Error('Network down'));
|
|
||||||
|
|
||||||
dns.resolve4('ns1.digitalocean.com', function (error, rdnsIps) {
|
|
||||||
if (error || rdnsIps.length === 0) return callback(new Error('Failed to query DO DNS'));
|
|
||||||
|
|
||||||
var reversedIp = ip.split('.').reverse().join('.');
|
|
||||||
|
|
||||||
var req = dns.Request({
|
|
||||||
question: dns.Question({ name: reversedIp + '.in-addr.arpa', type: 'PTR' }),
|
|
||||||
server: { address: rdnsIps[0] },
|
|
||||||
timeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('timeout', function () { return callback(new Error('Timedout')); });
|
|
||||||
|
|
||||||
req.on('message', function (error, message) {
|
|
||||||
if (error || !message.answer || message.answer.length === 0) return callback(new Error('Failed to query PTR'));
|
|
||||||
|
|
||||||
debug('checkPtrRecord: Actual:%s Expecting:%s', message.answer[0].data, fqdn);
|
|
||||||
callback(null, message.answer[0].data === fqdn);
|
|
||||||
});
|
|
||||||
|
|
||||||
req.send();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
136
src/dns/caas.js
Normal file
136
src/dns/caas.js
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
/* jslint node:true */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports = module.exports = {
|
||||||
|
add: add,
|
||||||
|
del: del,
|
||||||
|
update: update,
|
||||||
|
getChangeStatus: getChangeStatus,
|
||||||
|
get: get
|
||||||
|
};
|
||||||
|
|
||||||
|
var assert = require('assert'),
|
||||||
|
config = require('../config.js'),
|
||||||
|
debug = require('debug')('box:dns/caas'),
|
||||||
|
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||||
|
superagent = require('superagent'),
|
||||||
|
util = require('util'),
|
||||||
|
_ = require('underscore');
|
||||||
|
|
||||||
|
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||||
|
assert.strictEqual(typeof dnsConfig, 'object');
|
||||||
|
assert.strictEqual(typeof zoneName, 'string');
|
||||||
|
assert.strictEqual(typeof subdomain, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
|
assert(util.isArray(values));
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
|
||||||
|
|
||||||
|
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
type: type,
|
||||||
|
values: values
|
||||||
|
};
|
||||||
|
|
||||||
|
superagent
|
||||||
|
.post(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
|
||||||
|
.query({ token: dnsConfig.token })
|
||||||
|
.send(data)
|
||||||
|
.end(function (error, result) {
|
||||||
|
if (error && !error.response) return callback(error);
|
||||||
|
if (result.statusCode === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
|
||||||
|
if (result.statusCode !== 201) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||||
|
|
||||||
|
return callback(null, result.body.changeId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||||
|
assert.strictEqual(typeof dnsConfig, 'object');
|
||||||
|
assert.strictEqual(typeof zoneName, 'string');
|
||||||
|
assert.strictEqual(typeof subdomain, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var fqdn = subdomain !== '' && type === 'TXT' ? subdomain + '.' + config.fqdn() : config.appFqdn(subdomain);
|
||||||
|
|
||||||
|
debug('get: zoneName: %s subdomain: %s type: %s fqdn: %s', zoneName, subdomain, type, fqdn);
|
||||||
|
|
||||||
|
superagent
|
||||||
|
.get(config.apiServerOrigin() + '/api/v1/domains/' + fqdn)
|
||||||
|
.query({ token: dnsConfig.token, type: type })
|
||||||
|
.end(function (error, result) {
|
||||||
|
if (error && !error.response) return callback(error);
|
||||||
|
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||||
|
|
||||||
|
return callback(null, result.body.values);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||||
|
assert.strictEqual(typeof dnsConfig, 'object');
|
||||||
|
assert.strictEqual(typeof zoneName, 'string');
|
||||||
|
assert.strictEqual(typeof subdomain, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
|
assert(util.isArray(values));
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
get(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
if (_.isEqual(values, result)) return callback();
|
||||||
|
|
||||||
|
add(dnsConfig, zoneName, subdomain, type, values, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||||
|
assert.strictEqual(typeof dnsConfig, 'object');
|
||||||
|
assert.strictEqual(typeof zoneName, 'string');
|
||||||
|
assert.strictEqual(typeof subdomain, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
|
assert(util.isArray(values));
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
type: type,
|
||||||
|
values: values
|
||||||
|
};
|
||||||
|
|
||||||
|
superagent
|
||||||
|
.del(config.apiServerOrigin() + '/api/v1/domains/' + config.appFqdn(subdomain))
|
||||||
|
.query({ token: dnsConfig.token })
|
||||||
|
.send(data)
|
||||||
|
.end(function (error, result) {
|
||||||
|
if (error && !error.response) return callback(error);
|
||||||
|
if (result.statusCode === 420) return callback(new SubdomainError(SubdomainError.STILL_BUSY));
|
||||||
|
if (result.statusCode === 404) return callback(new SubdomainError(SubdomainError.NOT_FOUND));
|
||||||
|
if (result.statusCode !== 204) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||||
|
|
||||||
|
return callback(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChangeStatus(dnsConfig, changeId, callback) {
|
||||||
|
assert.strictEqual(typeof dnsConfig, 'object');
|
||||||
|
assert.strictEqual(typeof changeId, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
if (changeId === '') return callback(null, 'INSYNC');
|
||||||
|
|
||||||
|
superagent
|
||||||
|
.get(config.apiServerOrigin() + '/api/v1/domains/' + config.fqdn() + '/status/' + changeId)
|
||||||
|
.query({ token: dnsConfig.token })
|
||||||
|
.end(function (error, result) {
|
||||||
|
if (error && !error.response) return callback(error);
|
||||||
|
if (result.statusCode !== 200) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, util.format('%s %j', result.statusCode, result.body)));
|
||||||
|
|
||||||
|
return callback(null, result.body.status);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
216
src/dns/route53.js
Normal file
216
src/dns/route53.js
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
/* jslint node:true */
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports = module.exports = {
|
||||||
|
add: add,
|
||||||
|
get: get,
|
||||||
|
del: del,
|
||||||
|
update: update,
|
||||||
|
getChangeStatus: getChangeStatus
|
||||||
|
};
|
||||||
|
|
||||||
|
var assert = require('assert'),
|
||||||
|
AWS = require('aws-sdk'),
|
||||||
|
config = require('../config.js'),
|
||||||
|
debug = require('debug')('box:dns/route53'),
|
||||||
|
SubdomainError = require('../subdomains.js').SubdomainError,
|
||||||
|
util = require('util'),
|
||||||
|
_ = require('underscore');
|
||||||
|
|
||||||
|
function getDnsCredentials(dnsConfig) {
|
||||||
|
assert.strictEqual(typeof dnsConfig, 'object');
|
||||||
|
|
||||||
|
var credentials = {
|
||||||
|
accessKeyId: dnsConfig.accessKeyId,
|
||||||
|
secretAccessKey: dnsConfig.secretAccessKey,
|
||||||
|
region: dnsConfig.region
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dnsConfig.endpoint) credentials.endpoint = new AWS.Endpoint(dnsConfig.endpoint);
|
||||||
|
|
||||||
|
return credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getZoneByName(dnsConfig, zoneName, callback) {
|
||||||
|
assert.strictEqual(typeof dnsConfig, 'object');
|
||||||
|
assert.strictEqual(typeof zoneName, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||||
|
route53.listHostedZones({}, function (error, result) {
|
||||||
|
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||||
|
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||||
|
|
||||||
|
var zone = result.HostedZones.filter(function (zone) {
|
||||||
|
return zone.Name.slice(0, -1) === zoneName; // aws zone name contains a '.' at the end
|
||||||
|
})[0];
|
||||||
|
|
||||||
|
if (!zone) return callback(new SubdomainError(SubdomainError.NOT_FOUND, 'no such zone'));
|
||||||
|
|
||||||
|
callback(null, zone);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||||
|
assert.strictEqual(typeof dnsConfig, 'object');
|
||||||
|
assert.strictEqual(typeof zoneName, 'string');
|
||||||
|
assert.strictEqual(typeof subdomain, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
|
assert(util.isArray(values));
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
debug('add: %s for zone %s of type %s with values %j', subdomain, zoneName, type, values);
|
||||||
|
|
||||||
|
getZoneByName(dnsConfig, zoneName, function (error, zone) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
var fqdn = config.appFqdn(subdomain);
|
||||||
|
var records = values.map(function (v) { return { Value: v }; });
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
ChangeBatch: {
|
||||||
|
Changes: [{
|
||||||
|
Action: 'UPSERT',
|
||||||
|
ResourceRecordSet: {
|
||||||
|
Type: type,
|
||||||
|
Name: fqdn,
|
||||||
|
ResourceRecords: records,
|
||||||
|
TTL: 1
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
HostedZoneId: zone.Id
|
||||||
|
};
|
||||||
|
|
||||||
|
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||||
|
route53.changeResourceRecordSets(params, function(error, result) {
|
||||||
|
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||||
|
if (error && error.code === 'PriorRequestNotComplete') return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
|
||||||
|
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||||
|
|
||||||
|
callback(null, result.ChangeInfo.Id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function update(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||||
|
assert.strictEqual(typeof dnsConfig, 'object');
|
||||||
|
assert.strictEqual(typeof zoneName, 'string');
|
||||||
|
assert.strictEqual(typeof subdomain, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
|
assert(util.isArray(values));
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
get(dnsConfig, zoneName, subdomain, type, function (error, result) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
if (_.isEqual(values, result)) return callback();
|
||||||
|
|
||||||
|
add(dnsConfig, zoneName, subdomain, type, values, callback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get(dnsConfig, zoneName, subdomain, type, callback) {
|
||||||
|
assert.strictEqual(typeof dnsConfig, 'object');
|
||||||
|
assert.strictEqual(typeof zoneName, 'string');
|
||||||
|
assert.strictEqual(typeof subdomain, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
getZoneByName(dnsConfig, zoneName, function (error, zone) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
HostedZoneId: zone.Id,
|
||||||
|
MaxItems: '1',
|
||||||
|
StartRecordName: (subdomain ? subdomain + '.' : '') + zoneName + '.',
|
||||||
|
StartRecordType: type
|
||||||
|
};
|
||||||
|
|
||||||
|
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||||
|
route53.listResourceRecordSets(params, function (error, result) {
|
||||||
|
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||||
|
if (error) return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||||
|
if (result.ResourceRecordSets.length === 0) return callback(null, [ ]);
|
||||||
|
if (result.ResourceRecordSets[0].Name !== params.StartRecordName && result.ResourceRecordSets[0].Type !== params.StartRecordType) return callback(null, [ ]);
|
||||||
|
|
||||||
|
var values = result.ResourceRecordSets[0].ResourceRecords.map(function (record) { return record.Value; });
|
||||||
|
|
||||||
|
callback(null, values);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function del(dnsConfig, zoneName, subdomain, type, values, callback) {
|
||||||
|
assert.strictEqual(typeof dnsConfig, 'object');
|
||||||
|
assert.strictEqual(typeof zoneName, 'string');
|
||||||
|
assert.strictEqual(typeof subdomain, 'string');
|
||||||
|
assert.strictEqual(typeof type, 'string');
|
||||||
|
assert(util.isArray(values));
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
getZoneByName(dnsConfig, zoneName, function (error, zone) {
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
var fqdn = config.appFqdn(subdomain);
|
||||||
|
var records = values.map(function (v) { return { Value: v }; });
|
||||||
|
|
||||||
|
var resourceRecordSet = {
|
||||||
|
Name: fqdn,
|
||||||
|
Type: type,
|
||||||
|
ResourceRecords: records,
|
||||||
|
TTL: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
ChangeBatch: {
|
||||||
|
Changes: [{
|
||||||
|
Action: 'DELETE',
|
||||||
|
ResourceRecordSet: resourceRecordSet
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
HostedZoneId: zone.Id
|
||||||
|
};
|
||||||
|
|
||||||
|
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||||
|
route53.changeResourceRecordSets(params, function(error, result) {
|
||||||
|
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||||
|
if (error && error.message && error.message.indexOf('it was not found') !== -1) {
|
||||||
|
debug('del: resource record set not found.', error);
|
||||||
|
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
|
||||||
|
} else if (error && error.code === 'NoSuchHostedZone') {
|
||||||
|
debug('del: hosted zone not found.', error);
|
||||||
|
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
|
||||||
|
} else if (error && error.code === 'PriorRequestNotComplete') {
|
||||||
|
debug('del: resource is still busy', error);
|
||||||
|
return callback(new SubdomainError(SubdomainError.STILL_BUSY, error.message));
|
||||||
|
} else if (error && error.code === 'InvalidChangeBatch') {
|
||||||
|
debug('del: invalid change batch. No such record to be deleted.');
|
||||||
|
return callback(new SubdomainError(SubdomainError.NOT_FOUND, error.message));
|
||||||
|
} else if (error) {
|
||||||
|
debug('del: error', error);
|
||||||
|
return callback(new SubdomainError(SubdomainError.EXTERNAL_ERROR, error.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getChangeStatus(dnsConfig, changeId, callback) {
|
||||||
|
assert.strictEqual(typeof dnsConfig, 'object');
|
||||||
|
assert.strictEqual(typeof changeId, 'string');
|
||||||
|
assert.strictEqual(typeof callback, 'function');
|
||||||
|
|
||||||
|
if (changeId === '') return callback(null, 'INSYNC');
|
||||||
|
|
||||||
|
var route53 = new AWS.Route53(getDnsCredentials(dnsConfig));
|
||||||
|
route53.getChange({ Id: changeId }, function (error, result) {
|
||||||
|
if (error && error.code === 'AccessDenied') return callback(new SubdomainError(SubdomainError.ACCESS_DENIED, error.message));
|
||||||
|
if (error) return callback(error);
|
||||||
|
|
||||||
|
callback(null, result.ChangeInfo.Status);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user