mirror of
https://github.com/GravitLauncher/Launcher
synced 2025-06-28 20:18:10 +03:00
Compare commits
2842 commits
v4.0.0b3-d
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
e1ee1099cc | ||
|
b0e840a040 | ||
|
c76aae76a5 | ||
|
261e16cecb | ||
|
3c9e009433 | ||
|
2379fe5798 | ||
|
911ca1e69f | ||
|
90f74aaf25 | ||
|
880957fa9b | ||
|
2fea94071b | ||
|
b41e8db336 | ||
|
76570ddbe5 | ||
|
3a160b8124 | ||
|
8fb1dc275c | ||
|
95aed151e7 | ||
|
6934c37a33 | ||
|
5155c470c6 | ||
|
533dcfce14 | ||
|
d2f83d81eb | ||
|
2379398c30 | ||
|
f53c48a5ae | ||
|
394b64e22d | ||
|
7c15742478 | ||
|
d65fffade9 | ||
|
bb6c95ca12 | ||
|
9027987b29 | ||
|
d410533c8d | ||
|
af2c5c33e8 | ||
|
e67bb6a12f | ||
|
e2e0ef6ea4 | ||
|
18419fcd3a | ||
|
73804a555e | ||
|
b16281e04a | ||
|
06f0bc873a | ||
|
834fbab12b | ||
|
822872992f | ||
|
ac43034d45 | ||
|
00446b40f0 | ||
|
9e29053afa | ||
|
2e93102106 | ||
|
d4b69195b3 | ||
|
a67fbac8bc | ||
|
a7abb9cbfc | ||
|
689478ee0f | ||
|
df77a1ebd6 | ||
|
641796b44e | ||
|
b7ed56b27e | ||
|
f119bd4b30 | ||
|
bbff0eac64 | ||
|
41f93b9f8d | ||
|
263cf26258 | ||
|
b9ad7c0f26 | ||
|
5ba32e3405 | ||
|
907332ff06 | ||
|
1d63fbbd93 | ||
|
2f4667f5a6 | ||
|
92ada65079 | ||
|
29d98defff | ||
|
d7a379383b | ||
|
0111b2ca2b | ||
|
4671dfe49d | ||
|
bb63aaa0ab | ||
|
912caa6b8a | ||
|
926094076c | ||
|
a1af61a599 | ||
|
81b16cb54e | ||
|
a486f21fa2 | ||
|
86ea247f07 | ||
|
d26b179006 | ||
|
c4d1251429 | ||
|
f16f5fbc6d | ||
|
abe904d73c | ||
|
eaf685897f | ||
|
c8934d887a | ||
|
070a5d9b69 | ||
|
cc2bce4300 | ||
|
c7f4d8ac49 | ||
|
7dcb08fdaf | ||
|
9d870849a1 | ||
|
dda3ebc7b4 | ||
|
537623afaf | ||
|
0cff6e247a | ||
|
b85075c559 | ||
|
3e654f4d79 | ||
|
46f1f7b69e | ||
|
981f2ac3dd | ||
|
8509cbb6b5 | ||
|
1119625d12 | ||
|
9a69426547 | ||
|
31cbfe2919 | ||
|
27ebadcd19 | ||
|
b3349044b5 | ||
|
c43edeb982 | ||
|
1ff58099bd | ||
|
9fba637f83 | ||
|
ca70ee78d1 | ||
|
5e116a81e5 | ||
|
b0f799d194 | ||
|
63f9f8e21d | ||
|
b8ccbc5e48 | ||
|
a687c5afd8 | ||
|
3969d81db7 | ||
|
a30d0624a1 | ||
|
d5abe0d411 | ||
|
1e7a856a99 | ||
|
e6f5b585a7 | ||
|
2ed4abf9b0 | ||
|
af2dcec8cd | ||
|
9bffe07d36 | ||
|
4be299f6ca | ||
|
ef4f14f9b4 | ||
|
d720328bc4 | ||
|
88f1eaf750 | ||
|
a5ef86b105 | ||
|
b1a5ecdc13 | ||
|
68e9affbe0 | ||
|
7d7485afdc | ||
|
c2926b5b40 | ||
|
9c82d76781 | ||
|
450774de7e | ||
|
f88c0308f8 | ||
|
20f713be05 | ||
|
5bf92d9a00 | ||
|
903c4d40c5 | ||
|
ede9ab2c85 | ||
|
c8c83c0dba | ||
|
1bd5d8854c | ||
|
992d31c883 | ||
|
accbbe6b13 | ||
|
0894e0b9c3 | ||
|
925007015f | ||
|
ec526a343d | ||
|
1bebd8de2c | ||
|
c7781b30be | ||
|
df9d05a49c | ||
|
03d53d4a09 | ||
|
c261496af8 | ||
|
8c11ab0cbe | ||
|
78b4f1e3aa | ||
|
bbc5f1722b | ||
|
51411c5838 | ||
|
3b22b76278 | ||
|
ef9cbfe0da | ||
|
353b663e12 | ||
|
78be606029 | ||
|
584acdb8c3 | ||
|
7f6a645dd7 | ||
|
c6930ded74 | ||
|
c2a6a408c4 | ||
|
01cd50840a | ||
|
bb4d5b99c6 | ||
|
ff2f647b50 | ||
|
79fc42e86a | ||
|
5b8aa8cd5e | ||
|
a4e5ef9d01 | ||
|
1d5044c24a | ||
|
b84911d445 | ||
|
6a173b9b1b | ||
|
6a53891c6a | ||
|
7c637e078d | ||
|
c9a81da60c | ||
|
6c0ead015b | ||
|
b5457ee866 | ||
|
52c9196dcc | ||
|
095a5aef8b | ||
|
765f1a9d8f | ||
|
9bd65c797b | ||
|
8908710ad6 | ||
|
748612783c | ||
|
42cf9bc79e | ||
|
8a81989d65 | ||
|
58fd3a7e8b | ||
|
cc6ed82afb | ||
|
00ab20473c | ||
|
cdb54b34de | ||
|
1ebe68f5b8 | ||
|
b719255bd5 | ||
|
6ecf716fca | ||
|
873100cf0a | ||
|
a8b165f081 | ||
|
06ada30459 | ||
|
6017a89e20 | ||
|
bd677c26ba | ||
|
e5840243b3 | ||
|
09d36e066a | ||
|
038af764a1 | ||
|
f7decac23d | ||
|
1710eb7bec | ||
|
3926f3e5bf | ||
|
7759ea9182 | ||
|
ac6c312ed4 | ||
|
3e6af5afd3 | ||
|
044813cca2 | ||
|
730efae7c7 | ||
|
bc6da641d6 | ||
|
5c7f7eedec | ||
|
e72e4ebb92 | ||
|
d811a04cba | ||
|
572052163b | ||
|
666c8a4b3e | ||
|
ef5f932afb | ||
|
3002371fad | ||
|
998db80837 | ||
|
494b3227b6 | ||
|
d686d9a388 | ||
|
e28c9773fc | ||
|
7ff062f9e4 | ||
|
29619bb7a4 | ||
|
3179ee00eb | ||
|
f484f045ca | ||
|
ead4689bcf | ||
|
7a96e67517 | ||
|
b21082e201 | ||
|
8cd43b0324 | ||
|
3e8c1adebe | ||
|
666644c9e0 | ||
|
3152758d31 | ||
|
80176ff1e1 | ||
|
11382d3465 | ||
|
31285a8066 | ||
|
3ec79e3e93 | ||
|
a4bf033aa8 | ||
|
4e50cea93a | ||
|
d40dc09aca | ||
|
0b59d6c0ed | ||
|
80fc2900c8 | ||
|
4f47398211 | ||
|
9676e55bcb | ||
|
aa7b007616 | ||
|
35bdf1607f | ||
|
7060697bad | ||
|
34ac6a0f28 | ||
|
5370130c2d | ||
|
db6ab061de | ||
|
aeb55470ce | ||
|
a0788e4623 | ||
|
0c754ae5e6 | ||
|
c9ccf36252 | ||
|
b7b7afbdbb | ||
|
2fdd7d0199 | ||
|
1e3676778e | ||
|
ab884c8d23 | ||
|
48946d6e74 | ||
|
f075f39954 | ||
|
f1922c52e2 | ||
|
f42e6de0b0 | ||
|
efe967587c | ||
|
f2b92c2bbd | ||
|
4251725467 | ||
|
82bf2fdf56 | ||
|
f1bc0ea28a | ||
|
8762aa470c | ||
|
cfcd0010a7 | ||
|
b3eb0ebb98 | ||
|
8e82f5cd84 | ||
|
90f6d002d1 | ||
|
449798d52b | ||
|
f321b8bd27 | ||
|
64635cbb9b | ||
|
4607ab88bf | ||
|
e6516a8991 | ||
|
dfbb6e507a | ||
|
0855fc589d | ||
|
c9b6b0279a | ||
|
f8b060422e | ||
|
31489a2b24 | ||
|
6c0500f528 | ||
|
5896a12449 | ||
|
240e36aab6 | ||
|
f6f6ea13ad | ||
|
967b81cc85 | ||
|
6dadea1b67 | ||
|
a601a4ceef | ||
|
1c90681b3b | ||
|
7b1f449667 | ||
|
ae24fd6ccb | ||
|
2ff1d81076 | ||
|
ee0a7bc25a | ||
|
ebbd1c87e8 | ||
|
3754a327b0 | ||
|
d2f34ced28 | ||
|
43626bf1f4 | ||
|
380179faa3 | ||
|
2e60d45c63 | ||
|
d678daac7b | ||
|
1bc9351b0c | ||
|
54bfc6de9c | ||
|
fe374c1f9e | ||
|
3f4bdceb5a | ||
|
3d61635c6b | ||
|
0c2779f1c0 | ||
|
8f598a40c5 | ||
|
4720e4d106 | ||
|
0482cfa9ab | ||
|
a2167d483a | ||
|
84a3845f1a | ||
|
0d5d772141 | ||
|
9df116f951 | ||
|
ff3d500cc0 | ||
|
2e10c78ad2 | ||
|
b6be2e243d | ||
|
44bc8b0bbc | ||
|
6cd5a69149 | ||
|
b638efc0d1 | ||
|
f2cbf0ed38 | ||
|
0f36dfec16 | ||
|
c1df548258 | ||
|
429c7a45c4 | ||
|
90e116720c | ||
|
4b222b9526 | ||
|
1df283d659 | ||
|
b45618c0da | ||
|
0241a4d887 | ||
|
de5ebe4a74 | ||
|
80862e6116 | ||
|
f22aa36926 | ||
|
198ce95176 | ||
|
fca2ed2447 | ||
|
2792b5a008 | ||
|
a0335bd340 | ||
|
3c6b8322a5 | ||
|
a83f225933 | ||
|
99af83fb06 | ||
|
c4672387ac | ||
|
fe7ae41f65 | ||
|
474d557e3f | ||
|
50e6bb3b49 | ||
|
e85a12afab | ||
|
9e83e8bec8 | ||
|
fce8453bd1 | ||
|
224649aa13 | ||
|
a3bcfed793 | ||
|
e1429356df | ||
|
75f51c7727 | ||
|
90162a1a25 | ||
|
62e9276481 | ||
|
79f933646c | ||
|
01c963d852 | ||
|
941bf115b1 | ||
|
2e6d7bd94a | ||
|
d262b99be6 | ||
|
b5e10e8f9d | ||
|
cebeb55c00 | ||
|
7e16f36ea0 | ||
|
8973a3462b | ||
|
82dc299752 | ||
|
19f966062b | ||
|
93f916192e | ||
|
dbeca56b05 | ||
|
d7474255da | ||
|
b72fb643d4 | ||
|
52e8f693b5 | ||
|
1f3b7e0552 | ||
|
183cfe949f | ||
|
aff254a875 | ||
|
e12f2ef897 | ||
|
58d8ba2358 | ||
|
1cc3edd15b | ||
|
606df6cb1a | ||
|
98a314f697 | ||
|
d03c6120ff | ||
|
a34267e902 | ||
|
0ccef10a93 | ||
|
29aee9dd30 | ||
|
1d563249d1 | ||
|
d0d2860317 | ||
|
faa5189795 | ||
|
d9082f21a3 | ||
|
3475f2f912 | ||
|
b635447eaf | ||
|
0fb6102c29 | ||
|
d4cc28f96a | ||
|
49a5215783 | ||
|
f12d13ef58 | ||
|
55c77dd343 | ||
|
caebd6b5de | ||
|
647c8dba5c | ||
|
6ffbc5515f | ||
|
65c6520001 | ||
|
ac9a78cea4 | ||
|
00a4a13536 | ||
|
6e45a84c1c | ||
|
b31dd78b2b | ||
|
9de81095b1 | ||
|
4d1fd23e84 | ||
|
41f00c2310 | ||
|
85986c2916 | ||
|
1bc0443dd5 | ||
|
50de0b1e44 | ||
|
39d5eee51c | ||
|
8153c3a438 | ||
|
9d81db25d8 | ||
|
d4ca612bff | ||
|
680244e5d1 | ||
|
2f7b94365a | ||
|
60f742b3ef | ||
|
7fee478552 | ||
|
7efe7c8611 | ||
|
d4abf27989 | ||
|
e887035920 | ||
|
890591d2d2 | ||
|
82938fe8d4 | ||
|
cebe47939a | ||
|
fd24ca0ca7 | ||
|
663685934b | ||
|
43d944bee1 | ||
|
3a82065889 | ||
|
55c0cdfa0d | ||
|
b12c43676b | ||
|
9d49eebffe | ||
|
2aa8dffcaa | ||
|
216928f258 | ||
|
ec3775286c | ||
|
7950eea975 | ||
|
04dd7d655c | ||
|
bd83e8a4c5 | ||
|
d4f63a4e19 | ||
|
c458283efb | ||
|
bea7898939 | ||
|
bddf31c94e | ||
|
24d625fd16 | ||
|
0c23b59749 | ||
|
9b2c98e10b | ||
|
4a538cde61 | ||
|
d9be4bb577 | ||
|
ae426b2fd0 | ||
|
498325f3e8 | ||
|
907e7cc47e | ||
|
d4eabbc4c0 | ||
|
540ad0b0da | ||
|
3b8c01835d | ||
|
00baf4adf0 | ||
|
e338bb9a02 | ||
|
373fc8a255 | ||
|
73d8a037d5 | ||
|
aa47fd6f1b | ||
|
dec86c9a91 | ||
|
74af58bc7a | ||
|
d97b856ad6 | ||
|
5436b2a2d6 | ||
|
5aa4fe8d47 | ||
|
b10535042f | ||
|
9351f3ca1e | ||
|
e05aa4b204 | ||
|
bfa6966ec6 | ||
|
50b463b439 | ||
|
6caa34e255 | ||
|
26c017a277 | ||
|
48799cf3c2 | ||
|
3bc8040352 | ||
|
dcaec54814 | ||
|
76f8b4602c | ||
|
fc7f96d536 | ||
|
4ed687087f | ||
|
95da394a5d | ||
|
06e9bc8578 | ||
|
e0b3f3d6a5 | ||
|
6a057514b2 | ||
|
70d102222b | ||
|
55d2fbd57f | ||
|
111d7616d0 | ||
|
fb2883d215 | ||
|
dc664c7ee2 | ||
|
eff739ce12 | ||
|
17f9c28f3d | ||
|
71739f5670 | ||
|
e4bf8f1e9c | ||
|
a796b82a16 | ||
|
cf802fb0b3 | ||
|
57868a7136 | ||
|
8ac9866258 | ||
|
47392ceec7 | ||
|
c6f8793031 | ||
|
0d74d8a671 | ||
|
70012a2a8f | ||
|
3821fa7e51 | ||
|
930a5caf74 | ||
|
ffad29f53b | ||
|
bd4e454be9 | ||
|
0818b3037c | ||
|
43ffacdf5e | ||
|
5c374462ef | ||
|
ef5695f679 | ||
|
b4331819cb | ||
|
ebf25a65f7 | ||
|
5588b4aac1 | ||
|
fbaf9ab87f | ||
|
6d1440207b | ||
|
9d719c48e3 | ||
|
b0fba84fbb | ||
|
1ffd36fc82 | ||
|
9c359747ea | ||
|
2c1972c12c | ||
|
d30f0b900b | ||
|
f71444b8d6 | ||
|
a0ac58f0b5 | ||
|
a27d7f1597 | ||
|
7aa08c1846 | ||
|
2045f1ac99 | ||
|
292e7d2af7 | ||
|
5f7808afff | ||
|
3c10a668de | ||
|
ec222aed6f | ||
|
7dcc5aef3f | ||
|
ae994ebb4f | ||
|
553cdf5250 | ||
|
0d1b32fc1c | ||
|
7f4fe566de | ||
|
cc825df41e | ||
|
dd5ce00c3f | ||
|
7c7952545e | ||
|
cc8250d6cf | ||
|
3c5e25b67f | ||
|
a0722fb5f4 | ||
|
31b6ae35e7 | ||
|
8fb1349487 | ||
|
5631bc6af1 | ||
|
9cc1810831 | ||
|
841d01b417 | ||
|
a7d3cba949 | ||
|
b2d2059ebd | ||
|
10888ed2e7 | ||
|
82accb211d | ||
|
10df931c2a | ||
|
1996525b65 | ||
|
40d4681bce | ||
|
d11c9e92a5 | ||
|
60030bd769 | ||
|
bfa9a9c187 | ||
|
a560ab4812 | ||
|
a5b4282037 | ||
|
420fd53553 | ||
|
1bb7e99e12 | ||
|
c74f430129 | ||
|
df04b459be | ||
|
5a0ff3610b | ||
|
ca12e7cbd0 | ||
|
6fb9174681 | ||
|
36d97e7f8b | ||
|
28a9b5efc4 | ||
|
86f9f20b52 | ||
|
8bec134611 | ||
|
07be86f695 | ||
|
91f3773c54 | ||
|
b2486efb30 | ||
|
970761374a | ||
|
b43ff9e7a6 | ||
|
c7058499b7 | ||
|
6d5ae93889 | ||
|
78e5c8866f | ||
|
692aa8d3cf | ||
|
2324af1c46 | ||
|
7414132bad | ||
|
65ae13a042 | ||
|
230194f2a0 | ||
|
9bac9e3bef | ||
|
2d9037fedc | ||
|
385f2d9ec5 | ||
|
81b80a7938 | ||
|
bbb962c624 | ||
|
eb5bbb9acf | ||
|
3561522d14 | ||
|
c8768326ea | ||
|
dbdc1b4d6a | ||
|
d46c380f45 | ||
|
6bed8b383d | ||
|
b7a6cfda6d | ||
|
349cec3e40 | ||
|
1d349746d3 | ||
|
ab7e5ce2f7 | ||
|
afd147f92e | ||
|
08fe9b2eb4 | ||
|
bc3c7ba171 | ||
|
e8d3232dba | ||
|
0b38781858 | ||
|
6fac47f99d | ||
|
5b8260957a | ||
|
1e40ef94e4 | ||
|
63625f0e60 | ||
|
6e9ac8af90 | ||
|
44ba945b58 | ||
|
2c1129aa5b | ||
|
3a43ebff45 | ||
|
857901a667 | ||
|
4ff13a67b9 | ||
|
c75185f697 | ||
|
5d332a10b5 | ||
|
95704fc1bf | ||
|
4400cbe192 | ||
|
ccb297a498 | ||
|
126d23037e | ||
|
600dd02703 | ||
|
2ff46420a8 | ||
|
9b43592a27 | ||
|
517108deb4 | ||
|
83ae8a7e72 | ||
|
bf1967f32b | ||
|
8379a6efec | ||
|
7ef304c2cc | ||
|
8a51c98df8 | ||
|
d128c643ae | ||
|
344365d1ef | ||
|
7254a197a1 | ||
|
f4280b89f4 | ||
|
fa50a6f64c | ||
|
5e6ab3763e | ||
|
89b800c124 | ||
|
b8b841cdd9 | ||
|
b614ae6d69 | ||
|
a20b70e2a1 | ||
|
dc27fc04aa | ||
|
2709cbe95f | ||
|
be565e2218 | ||
|
7cedaef90a | ||
|
b135535147 | ||
|
7cd6b9c1d9 | ||
|
7bfa4ebdab | ||
|
8afa630546 | ||
|
c42f410bc4 | ||
|
7522e156ae | ||
|
67a53218d4 | ||
|
b67ae753f0 | ||
|
6663f38241 | ||
|
e0f291fdac | ||
|
b57118cd20 | ||
|
638ae3d5fe | ||
|
9b3b49966a | ||
|
f6ee175acd | ||
|
736bf51eaa | ||
|
dd3fd101f4 | ||
|
96915b0f78 | ||
|
6430b9e2d4 | ||
|
22aa7ad7ae | ||
|
922944dacc | ||
|
a54d7ba89a | ||
|
2b117f6717 | ||
|
6086b15c9f | ||
|
d38feed952 | ||
|
62094e0cf6 | ||
|
6aed114791 | ||
|
afbed1345f | ||
|
c351846a43 | ||
|
26835c79bb | ||
|
b72600656a | ||
|
e1f4f76f70 | ||
|
fecf458e82 | ||
|
5fb3a40bbc | ||
|
10e7adc4b1 | ||
|
6c8ba831c2 | ||
|
cbeb02950a | ||
|
9b9531ce5e | ||
|
999d7b7795 | ||
|
7bfa6f116b | ||
|
6ab0c7d6a2 | ||
|
07bfd0b340 | ||
|
ba0eca5e1f | ||
|
788499a45c | ||
|
2b95c58f28 | ||
|
4cb47d5891 | ||
|
5dd5092957 | ||
|
b559655486 | ||
|
8945037860 | ||
|
bde9fa958d | ||
|
dd48074e9a | ||
|
589477888f | ||
|
b496d60b40 | ||
|
8c84a80372 | ||
|
3a77a59eac | ||
|
f3f78b617a | ||
|
e40af46e8d | ||
|
c88fb1d8e3 | ||
|
99812da816 | ||
|
ce4f4cab62 | ||
|
796b2e2533 | ||
|
3b8a01fbb4 | ||
|
9a1498716f | ||
|
9c289cd1cb | ||
|
9cc1cf5feb | ||
|
0379477971 | ||
|
9f2c9bb589 | ||
|
a8e9418e89 | ||
|
81a94faaf6 | ||
|
559f4f4de7 | ||
|
3aedd4a2b8 | ||
|
d995b3508c | ||
|
4dc79f320d | ||
|
84a53304b3 | ||
|
ca943ac24e | ||
|
d8d4448f1e | ||
|
eecd61e6ba | ||
|
111639b963 | ||
|
fbb24bb904 | ||
|
f30daab0f7 | ||
|
50d8f12536 | ||
|
ac1279ff5c | ||
|
f8569f9165 | ||
|
2d0957f834 | ||
|
c7cb512710 | ||
|
0640fc6d60 | ||
|
2ef3ca7324 | ||
|
2a2c2f6b93 | ||
|
15da924aa6 | ||
|
618e981de5 | ||
|
e5b603e2a9 | ||
|
7bcf5f10ab | ||
|
af2155c4bf | ||
|
d1bc03664b | ||
|
b543aaf4a6 | ||
|
34e5b9027e | ||
|
f3a095966b | ||
|
d99f24c389 | ||
|
2b2aeedc20 | ||
|
39bdab50ca | ||
|
aed103e837 | ||
|
1a7160c394 | ||
|
7d8975041f | ||
|
d9a8e161f1 | ||
|
b5aeac3ff2 | ||
|
2171fb291f | ||
|
8b396dfb13 | ||
|
a283f907d6 | ||
|
b48059998f | ||
|
a1ab2d404d | ||
|
112ac31803 | ||
|
26ed69e1a7 | ||
|
d9b03d3c6a | ||
|
dabe2e8106 | ||
|
58dfc472ac | ||
|
9841ef3157 | ||
|
e782f0409d | ||
|
94a1a2f464 | ||
|
a94b6e8836 | ||
|
d5cc2ee094 | ||
|
a1284ff9cb | ||
|
baf2997089 | ||
|
dfd565d44f | ||
|
7a78cbb878 | ||
|
958686a032 | ||
|
4c78d00360 | ||
|
d2e222d67d | ||
|
39ac6cdad8 | ||
|
4bd84728f3 | ||
|
9b55e243e7 | ||
|
fbbde7991e | ||
|
eec54ad4d9 | ||
|
36a6cd7e6a | ||
|
1c5a403d69 | ||
|
6c5b02bdb1 | ||
|
3cce0e93f3 | ||
|
77b5188b52 | ||
|
7ab7c29fe4 | ||
|
04c854703e | ||
|
222cbb2f7b | ||
|
a09c2dff54 | ||
|
11af2943db | ||
|
f0ae4cbb12 | ||
|
9e7dedffc9 | ||
|
acf106baa9 | ||
|
8694963313 | ||
|
c44162dbbe | ||
|
6211744b83 | ||
|
f279e97ea2 | ||
|
ad5c2382cf | ||
|
62991ed709 | ||
|
077e6f3053 | ||
|
c0b1373542 | ||
|
12b933c8bd | ||
|
a91a578c58 | ||
|
e5cd39e544 | ||
|
8b1f0a7d8c | ||
|
fe994cdcca | ||
|
438b62b3ca | ||
|
b424da0d62 | ||
|
6f2f7bee13 | ||
|
5ea3788e7d | ||
|
92cdaa446e | ||
|
27f7082568 | ||
|
effbb18bbc | ||
|
fb7a552843 | ||
|
93fdd7f899 | ||
|
dc3d75c650 | ||
|
3e973c5de3 | ||
|
21e92cffc6 | ||
|
b7621ef760 | ||
|
2221fadace | ||
|
214ad8bacb | ||
|
59ca2cb00f | ||
|
a915d41b93 | ||
|
9fff11e887 | ||
|
7e77862cd4 | ||
|
5ba36c3a1c | ||
|
1abab46ce4 | ||
|
19344b9d27 | ||
|
0ab9327c74 | ||
|
5f86dc2420 | ||
|
c3d39310b5 | ||
|
902f09324b | ||
|
aa9b84ba4a | ||
|
711413c52f | ||
|
9f44673809 | ||
|
ce9c13efcc | ||
|
3eb69d1284 | ||
|
5d0ccdbde3 | ||
|
4dd30faf7e | ||
|
b0538abe63 | ||
|
fecc14010d | ||
|
eb9871123b | ||
|
dfa2b042e4 | ||
|
7ad308cc23 | ||
|
f9c644ff4e | ||
|
62e392359d | ||
|
579fec1518 | ||
|
30ec6409fd | ||
|
1912393a40 | ||
|
ac15002327 | ||
|
9112ca0c88 | ||
|
3be2452ceb | ||
|
afa0cd0366 | ||
|
7f1583c0a3 | ||
|
72e0c55455 | ||
|
8085b50b3d | ||
|
1a8ec31a5b | ||
|
b901a5b9e4 | ||
|
0cc47a7b06 | ||
|
736aa93106 | ||
|
442ff9b752 | ||
|
602a4b17f8 | ||
|
7ca3889bb5 | ||
|
3045619657 | ||
|
f82d5dfa03 | ||
|
27219f7dd7 | ||
|
3f0c972b08 | ||
|
b4718a8062 | ||
|
81be9d21de | ||
|
0c281ab50d | ||
|
e36cfea4f9 | ||
|
867f367860 | ||
|
7133727edd | ||
|
c1f1dae7f2 | ||
|
5abc456c3b | ||
|
b09cd5c456 | ||
|
661fb94594 | ||
|
d01c27177c | ||
|
8ea134dc27 | ||
|
9fe1fc4f23 | ||
|
201b6826ed | ||
|
fbd246a338 | ||
|
8c259a7702 | ||
|
781ab27127 | ||
|
25ddb485ac | ||
|
0f0d55ef07 | ||
|
7ae24071cf | ||
|
00bbad3451 | ||
|
3431fae109 | ||
|
32a4729970 | ||
|
8ad6f7ccbe | ||
|
fcdaa36421 | ||
|
1b81d5956c | ||
|
5886d1ac48 | ||
|
314eb8c09e | ||
|
386239bfd7 | ||
|
80a7e1b593 | ||
|
2d48219f61 | ||
|
c719f2448e | ||
|
8445ed2dca | ||
|
20a326a707 | ||
|
ffad6c0b74 | ||
|
a8573c5b57 | ||
|
a700ec4ca7 | ||
|
617f79cbdd | ||
|
8290321512 | ||
|
5920f524c7 | ||
|
265a872b08 | ||
|
73c6c48f42 | ||
|
568852b951 | ||
|
6c966b6126 | ||
|
c2f55998e3 | ||
|
5a349afe9d | ||
|
be78caa841 | ||
|
fc33ed11d0 | ||
|
aefb026952 | ||
|
cd99641327 | ||
|
2c41b510ad | ||
|
b7a7156408 | ||
|
ea3310b738 | ||
|
d3751732b0 | ||
|
d1d3f12abd | ||
|
b919020988 | ||
|
f317912de7 | ||
|
c37fb47795 | ||
|
e1f356feb7 | ||
|
aa9dc484e1 | ||
|
a8a360be03 | ||
|
96ef860c70 | ||
|
ab7565e7d1 | ||
|
d9f8b20a71 | ||
|
9da0ca8604 | ||
|
b5ce3102a9 | ||
|
929a3a4ebb | ||
|
c40f6f5883 | ||
|
6f6aa81554 | ||
|
b647f49390 | ||
|
97faf5ef79 | ||
|
30cabd25fd | ||
|
92a6947ab8 | ||
|
1667b00757 | ||
|
4cd3d145c9 | ||
|
c2b499b2a9 | ||
|
7a1746281d | ||
|
a4596b8c2f | ||
|
7277aba7ee | ||
|
afe3dd543e | ||
|
21a203356f | ||
|
9f8cd7070c | ||
|
43c45f6990 | ||
|
6482ab2c46 | ||
|
d837658d36 | ||
|
5b5da76bff | ||
|
ea7159dba6 | ||
|
f188378fd6 | ||
|
ae82964a24 | ||
|
e5f714e0f5 | ||
|
abdbf5039d | ||
|
8ab1dda430 | ||
|
7724c904e5 | ||
|
d522976f0b | ||
|
a34b2c206b | ||
|
37ec6b525c | ||
|
1b7e463ddb | ||
|
9f05594915 | ||
|
68d7c0a947 | ||
|
203fc638dc | ||
|
2058e3f38b | ||
|
2def9c20d9 | ||
|
54cb175ae7 | ||
|
5d35c0823e | ||
|
66db1eb861 | ||
|
64661abf0f | ||
|
5bff221e7e | ||
|
92e2a6c86d | ||
|
5bbe63f78c | ||
|
7886cce6f8 | ||
|
d1e671a935 | ||
|
40e4949c47 | ||
|
5547909c16 | ||
|
3b6997a723 | ||
|
169df2a79a | ||
|
e5e8fa1463 | ||
|
7015d45088 | ||
|
70c2e1c1af | ||
|
36ea9a9d3d | ||
|
e9b14c921c | ||
|
92f19ffc5d | ||
|
6c37dd6ed2 | ||
|
f1331b6d5d | ||
|
1cb2369bfa | ||
|
edbed3db1d | ||
|
dc51ca1c9c | ||
|
f8af81b141 | ||
|
fc13442c89 | ||
|
63891cd743 | ||
|
1452b59fa5 | ||
|
0751d2fd28 | ||
|
afc84ebae8 | ||
|
6127e92ce5 | ||
|
cd5c78c413 | ||
|
d7e44700f6 | ||
|
0b1f3f66af | ||
|
3b839da3d3 | ||
|
fb3241dd52 | ||
|
0c97fdf45c | ||
|
6da27bdf99 | ||
|
25d88ba30d | ||
|
155a60053c | ||
|
bdd402051a | ||
|
bbdfc204e7 | ||
|
b545c099eb | ||
|
344c61eaee | ||
|
1e7f5c9eb4 | ||
|
9d06689f35 | ||
|
8a016b7d46 | ||
|
6e54fcd2fb | ||
|
4baccf5b77 | ||
|
0ff47e9339 | ||
|
3ff39834d8 | ||
|
75a965120a | ||
|
99fb4002da | ||
|
8ff31888e9 | ||
|
610593b0f0 | ||
|
0e505a1feb | ||
|
b8be6d4ef6 | ||
|
3cd9ddee0d | ||
|
9084361a38 | ||
|
f6d954827e | ||
|
7154989827 | ||
|
3ccb0c5c8a | ||
|
bca195ca96 | ||
|
f4ef2b25e1 | ||
|
87cd668522 | ||
|
d0f13743ed | ||
|
bac794b912 | ||
|
f780db3e57 | ||
|
4592eee953 | ||
|
78265a7576 | ||
|
7f86d65d9c | ||
|
b8ca4daadf | ||
|
e2960393fb | ||
|
84c459bb0d | ||
|
4cc22faa55 | ||
|
5d39e168cd | ||
|
a4927dae4d | ||
|
669a7eb391 | ||
|
9d6c9c0abf | ||
|
1b2be8389a | ||
|
098a429c13 | ||
|
3cf5ee6d2c | ||
|
b897fa55fd | ||
|
de95a66742 | ||
|
826eb4c80f | ||
|
cb5535b8ec | ||
|
4f5c3370ac | ||
|
cafe8df3a9 | ||
|
ce70cab736 | ||
|
62c8ca878e | ||
|
aced575921 | ||
|
c596c30ff6 | ||
|
acf2d2d9cd | ||
|
c24b74ab91 | ||
|
08b23db5c6 | ||
|
f971356955 | ||
|
06aa6976e0 | ||
|
2688270fa0 | ||
|
5e048905cc | ||
|
ae2ef5e7c2 | ||
|
0f8f51ea97 | ||
|
447866d001 | ||
|
86d3b3365d | ||
|
fb5609b864 | ||
|
aee051627b | ||
|
94e60c3e51 | ||
|
18b23b195a | ||
|
3a80a74912 | ||
|
2164fda288 | ||
|
d9e71d08d7 | ||
|
a1cdd6e22c | ||
|
3de3ac24a9 | ||
|
90f360c565 | ||
|
a555f1b1d9 | ||
|
fe00a62971 | ||
|
f9066d1782 | ||
|
326012aad8 | ||
|
ef3fbaef18 | ||
|
78c0e0d54d | ||
|
438133bce7 | ||
|
32d7ca932a | ||
|
f9521b41a2 | ||
|
d7519688ee | ||
|
92df57e491 | ||
|
584e07328c | ||
|
3a3aafe5fa | ||
|
a4355d1d05 | ||
|
9766cd69e5 | ||
|
8ce5b4179c | ||
|
9efcd7af26 | ||
|
5646813885 | ||
|
186a749150 | ||
|
5413ba0dd7 | ||
|
211b440b8b | ||
|
ddab96f7ea | ||
|
5835676b7c | ||
|
8b8ef665ef | ||
|
72a8325a15 | ||
|
2271b91653 | ||
|
b2a3e5672a | ||
|
383551229d | ||
|
996d126611 | ||
|
57cdd64142 | ||
|
4d17f34ba6 | ||
|
886c085572 | ||
|
1362d71788 | ||
|
4917f19b81 | ||
|
2e238f3dc2 | ||
|
d5bab20610 | ||
|
9a748f0ea3 | ||
|
ea299c4fea | ||
|
d98c6c39c5 | ||
|
17bce5dff0 | ||
|
e3da81bc85 | ||
|
46ce073a1f | ||
|
15a0de33ce | ||
|
e304bd7285 | ||
|
2c8c640130 | ||
|
aa87ff05eb | ||
|
11e50e065c | ||
|
1fbad1cfa0 | ||
|
c969b16391 | ||
|
48a2a8346a | ||
|
84617f2fbc | ||
|
db2dffb51c | ||
|
d4389b9f1f | ||
|
57b0809e56 | ||
|
4b894c3ae1 | ||
|
90ec3efe3d | ||
|
cd890a73ee | ||
|
b92e9a1d29 | ||
|
4bea6bb16b | ||
|
6c76e0758d | ||
|
3ad7002da7 | ||
|
4502f978fb | ||
|
07ba1255ce | ||
|
dd11ba8179 | ||
|
6500b764e9 | ||
|
926e1534af | ||
|
3e6384cad9 | ||
|
09a6afdf57 | ||
|
a65837d81d | ||
|
5b77e4d8fd | ||
|
12f73aabcb | ||
|
36786c688d | ||
|
e7fd749c2f | ||
|
eca8cc2cbc | ||
|
aabd18e089 | ||
|
2043d301a1 | ||
|
dba9d70b37 | ||
|
81abc3fdc2 | ||
|
39f7d4bbda | ||
|
c85e5be4da | ||
|
ea7d50a8bc | ||
|
6511207a6e | ||
|
2a9a4256f9 | ||
|
98431cc796 | ||
|
898c6a33a1 | ||
|
c353de9267 | ||
|
bdecd57470 | ||
|
c7688afb32 | ||
|
72a8c03296 | ||
|
90519f0985 | ||
|
73fcf4aae4 | ||
|
fb00adb129 | ||
|
3baffcafb5 | ||
|
a180673b26 | ||
|
d2b82214fb | ||
|
98f1e30e61 | ||
|
3332faac57 | ||
|
b9d29008ff | ||
|
8d1c7621cf | ||
|
80e919f4a1 | ||
|
ca5c435341 | ||
|
96d2cfaf26 | ||
|
4e4f606636 | ||
|
19831a6f72 | ||
|
f676a5e1cf | ||
|
65693fa867 | ||
|
7f48772663 | ||
|
4ab66caad0 | ||
|
2f414ae9e7 | ||
|
fe21427d17 | ||
|
ea5449a196 | ||
|
6bee803f00 | ||
|
aed80e995a | ||
|
e81b098320 | ||
|
a71b62ca1a | ||
|
ac64ec5483 | ||
|
ac737c4ccb | ||
|
8a53e5a925 | ||
|
1abb66fcb8 | ||
|
85400d968b | ||
|
72232e4cc7 | ||
|
6077a51fd4 | ||
|
5ad1d58f62 | ||
|
d06f179e9c | ||
|
f94766679c | ||
|
4e472151e9 | ||
|
d2ce49e41a | ||
|
4592aeea2d | ||
|
c438f08d7a | ||
|
925c1170a3 | ||
|
bf932a4893 | ||
|
5c5240a2f3 | ||
|
ca694601df | ||
|
aa49c60fef | ||
|
c0824a84cd | ||
|
49a2d79ce1 | ||
|
447e03b999 | ||
|
68c06de23e | ||
|
34e941ddfd | ||
|
fd5803ca32 | ||
|
0d5dbe7794 | ||
|
3ed166f8e7 | ||
|
43a43ec30d | ||
|
45e3d0b18e | ||
|
fe59e27bfa | ||
|
63d9c73e00 | ||
|
3ce38a1b0d | ||
|
e226cb397d | ||
|
c49093cb31 | ||
|
0bda3a9dbd | ||
|
77c797f906 | ||
|
6c873f71c9 | ||
|
a2ec922d20 | ||
|
95e712e64e | ||
|
2653a0da89 | ||
|
69ee06727a | ||
|
e3bc798865 | ||
|
fabb2efe39 | ||
|
30cd1f21f0 | ||
|
eef76a2b4e | ||
|
7598ba5d62 | ||
|
edfc248351 | ||
|
775712d898 | ||
|
d2ef0b3aff | ||
|
d3c14be4b1 | ||
|
1587369ec5 | ||
|
12583aec89 | ||
|
6125d1c323 | ||
|
6dea2f81c0 | ||
|
44f3a17973 | ||
|
56e1920a8d | ||
|
0692fe367f | ||
|
8e1cf303ac | ||
|
144b15701b | ||
|
3201d2fc4f | ||
|
a9867d57c4 | ||
|
1f8a81ef58 | ||
|
88966edffb | ||
|
def886b0d6 | ||
|
39e3dd77de | ||
|
64bc2eb3c8 | ||
|
924f3dd958 | ||
|
98b096dff5 | ||
|
69bd5f3c66 | ||
|
00478527e5 | ||
|
fbfb074e0a | ||
|
c7468eba14 | ||
|
9ccaf3b1d7 | ||
|
58a208a3f5 | ||
|
0ddf04b6aa | ||
|
56b933bd3a | ||
|
7527251841 | ||
|
81d11198e2 | ||
|
c57cb90700 | ||
|
8dddb08255 | ||
|
8875146be3 | ||
|
eddfd61c06 | ||
|
abdb31de90 | ||
|
c2154b8a7c | ||
|
dc97bbb743 | ||
|
747e5fec0c | ||
|
602a20325c | ||
|
e26a15d26a | ||
|
de33b458e8 | ||
|
79f12edee6 | ||
|
04609c789e | ||
|
d50c306e80 | ||
|
d278712183 | ||
|
b8f25e669e | ||
|
01fa09a115 | ||
|
84543b85f3 | ||
|
1bbaadc5f1 | ||
|
b40a1497c8 | ||
|
665ed6a4f7 | ||
|
606eca22aa | ||
|
c44384ccb2 | ||
|
b2888d0cdf | ||
|
ab9a19d3aa | ||
|
f81e321223 | ||
|
233e28a08a | ||
|
1131f56e57 | ||
|
ca8f6f0861 | ||
|
a8b96a2adf | ||
|
6af348606b | ||
|
437da3de59 | ||
|
de47594083 | ||
|
ec5c076a16 | ||
|
28b31d20f9 | ||
|
95924261f3 | ||
|
3f307d7b20 | ||
|
308cc48d4c | ||
|
0f9fd46d29 | ||
|
3da2951cac | ||
|
19ac80d449 | ||
|
e84530cce4 | ||
|
5747dd9da0 | ||
|
8fa98f362e | ||
|
9e9153b124 | ||
|
76ecbaa19e | ||
|
f8aab10609 | ||
|
a3800958db | ||
|
cfc140d47c | ||
|
f2c0815123 | ||
|
dab4b21ae7 | ||
|
adefda970e | ||
|
b57ad71539 | ||
|
0e4502d38f | ||
|
bde9fd55f9 | ||
|
c714a1a5ed | ||
|
d68e1f781e | ||
|
63ea1f59b0 | ||
|
01b25aa6d8 | ||
|
ff5ed0eb27 | ||
|
2a7570b9b6 | ||
|
6b7468e366 | ||
|
554ca54c4a | ||
|
002477445c | ||
|
7979989724 | ||
|
32f76a395a | ||
|
ea87e58455 | ||
|
7b1ddf5577 | ||
|
d813e714da | ||
|
83a3c963d3 | ||
|
667fb21e9b | ||
|
b4a75b9076 | ||
|
ef6616dfda | ||
|
6ec2c035cf | ||
|
d4c4197e8e | ||
|
60b7a06fa3 | ||
|
9529017428 | ||
|
f81cdf6440 | ||
|
93436de104 | ||
|
4260199de7 | ||
|
2119688609 | ||
|
c075697316 | ||
|
5939ed758f | ||
|
495dba899d | ||
|
036b593356 | ||
|
567e2fefaf | ||
|
ea6e131b60 | ||
|
64d57faa48 | ||
|
770a9a18ce | ||
|
0dcd212d27 | ||
|
9351cc1de5 | ||
|
459f305f13 | ||
|
1ae7c80c72 | ||
|
7d5b912c3c | ||
|
160219c36b | ||
|
96930cbce3 | ||
|
43e1533c87 | ||
|
f0ca39cde9 | ||
|
284f550a9f | ||
|
5e923ec479 | ||
|
decd3a8d04 | ||
|
b617e58a4c | ||
|
1e169ce55b | ||
|
f4dcd99639 | ||
|
6f22ae3c7d | ||
|
c6e6dd672f | ||
|
c6dad02c9b | ||
|
011da08dc8 | ||
|
a03bc919a0 | ||
|
53c8a548f0 | ||
|
9cbdd6300c | ||
|
dcf9a56c01 | ||
|
3bfed5c3c7 | ||
|
a125fd8e8f | ||
|
a259527a20 | ||
|
4dcc1443f6 | ||
|
5e27db127a | ||
|
2fc32aa1aa | ||
|
0922c18b22 | ||
|
117b95d3fc | ||
|
4779d6e08b | ||
|
73e0ed53bb | ||
|
47e5902e94 | ||
|
6f6cb96080 | ||
|
a350b3bd77 | ||
|
4dab77bda9 | ||
|
3eef6ac882 | ||
|
efc3cd4a09 | ||
|
e673587d60 | ||
|
9ed4e37e22 | ||
|
38580b23bc | ||
|
4a572d0814 | ||
|
7cbfe93320 | ||
|
f62b037789 | ||
|
b21c8b04c3 | ||
|
20913cdbc6 | ||
|
71018a0cdf | ||
|
4136f4a6e8 | ||
|
f839691c87 | ||
|
4eb9c7fe7c | ||
|
03471e8bbf | ||
|
3296d00249 | ||
|
1086153945 | ||
|
fd54f4739f | ||
|
94c19c9c56 | ||
|
40bd92229a | ||
|
cf63e96c1e | ||
|
773981c32f | ||
|
8c62cfff11 | ||
|
b93848055b | ||
|
977f8dfaec | ||
|
e081bf5a76 | ||
|
ede1749f58 | ||
|
fe0b4ccf04 | ||
|
9456c45a3d | ||
|
358e92ad9e | ||
|
97b30aa234 | ||
|
af88a02a52 | ||
|
3923251fd2 | ||
|
90d2123f76 | ||
|
fccc9fe8c0 | ||
|
37e3991a9a | ||
|
aa4d4881c8 | ||
|
05093d8865 | ||
|
fc9ac07bc0 | ||
|
e9db1b307b | ||
|
178fab6ff7 | ||
|
5c39b1d4d2 | ||
|
17106886bb | ||
|
490ba360e7 | ||
|
641001f041 | ||
|
8730ecb749 | ||
|
0d31257625 | ||
|
ee955d002c | ||
|
8c1a8235a8 | ||
|
a841746ac8 | ||
|
31c32aa2c2 | ||
|
6b85de2806 | ||
|
b23b033d19 | ||
|
3291d2de6b | ||
|
0224ce8cd6 | ||
|
35f71f6797 | ||
|
888a3a92d9 | ||
|
ead52415fd | ||
|
c0b476a494 | ||
|
085924c831 | ||
|
5446142e6a | ||
|
f3c7f57e50 | ||
|
f7cfaaa543 | ||
|
625fb75f82 | ||
|
585fb06151 | ||
|
4ccf11f461 | ||
|
02cb32cd65 | ||
|
5018e6f35a | ||
|
e55f266b91 | ||
|
5e294d1517 | ||
|
a681f43312 | ||
|
becb12887d | ||
|
600c65d2f2 | ||
|
89c412cda1 | ||
|
e002d1a7e9 | ||
|
fb21aeb888 | ||
|
d40ccf47c7 | ||
|
081b86ff23 | ||
|
2c158135c2 | ||
|
fcb52f0eec | ||
|
a1e6c80894 | ||
|
ff30ba2670 | ||
|
68cb755212 | ||
|
e1c6775e34 | ||
|
8f779044fc | ||
|
72f938333a | ||
|
087e2b5a8c | ||
|
d84b13c319 | ||
|
f1dcae53b5 | ||
|
2b87d67e92 | ||
|
21a90f0540 | ||
|
d8536482ff | ||
|
12f5c7ef65 | ||
|
cfc63b535e | ||
|
5ed233613f | ||
|
f5fe5afe81 | ||
|
898dcb3ea0 | ||
|
8d098108d0 | ||
|
908bd1cb76 | ||
|
d9e5e3d350 | ||
|
0cab4f254e | ||
|
b9761637db | ||
|
176430e442 | ||
|
9a4b813bb7 | ||
|
8ba00d106e | ||
|
386a110eb1 | ||
|
ac55d0694b | ||
|
0b316dff56 | ||
|
fd23ba555b | ||
|
96d8883ecb | ||
|
7ef7099430 | ||
|
d33fe0e6d0 | ||
|
bcbc46238c | ||
|
05d5b72d05 | ||
|
cd1a9718de | ||
|
d897a692f7 | ||
|
7c5616ef2b | ||
|
4c1ba67c71 | ||
|
5a2aedbe06 | ||
|
8a52340b1a | ||
|
a7db7184db | ||
|
80e24715f6 | ||
|
cd4e89dc15 | ||
|
f0a235f12c | ||
|
4a0485d1de | ||
|
35a94ae59f | ||
|
a84f489f0d | ||
|
ce28ac4057 | ||
|
897f799aac | ||
|
a8c407e2fb | ||
|
ef1b61d258 | ||
|
cb91f83ec2 | ||
|
3d7c2f31ef | ||
|
16fbab7bf5 | ||
|
331bb42822 | ||
|
bb6e635836 | ||
|
6cc3c051cf | ||
|
78e882b943 | ||
|
e6333e057f | ||
|
30e08e1c26 | ||
|
f8fbe8e425 | ||
|
dda863db9d | ||
|
81f50a57f3 | ||
|
8e1000ec41 | ||
|
efd58d66c7 | ||
|
a38853180b | ||
|
50d5c941f4 | ||
|
6af7283b60 | ||
|
f89fabaf11 | ||
|
0ff391778a | ||
|
43c96039e4 | ||
|
1b5fb36b0a | ||
|
de144d90e0 | ||
|
03a636e79a | ||
|
610b6de76f | ||
|
ac3cde45f0 | ||
|
a3a2b1ac3e | ||
|
ac42f840a8 | ||
|
b8459c829d | ||
|
29519728b8 | ||
|
56bd3f1268 | ||
|
29ed4c5cf7 | ||
|
555c6a6c0b | ||
|
dea036a574 | ||
|
2cfcd7a964 | ||
|
833cc9ae8e | ||
|
07c9b9f956 | ||
|
034e5c154e | ||
|
327d243761 | ||
|
d5692bd575 | ||
|
05b214c11a | ||
|
8f42175ad4 | ||
|
f61cf67606 | ||
|
cc81494e45 | ||
|
f5d7fb435c | ||
|
37db7b99a8 | ||
|
b8017e38e3 | ||
|
f7ae98e4cb | ||
|
071230431f | ||
|
90bc4343b2 | ||
|
a0e03ca979 | ||
|
6a25d83fbe | ||
|
c7ae361234 | ||
|
0e2c19f79c | ||
|
e2d3e3068f | ||
|
053ca39dfe | ||
|
0e278b715b | ||
|
06c33a47ad | ||
|
b01e52bb17 | ||
|
09932de0c0 | ||
|
5de7eedc35 | ||
|
3441fc21a7 | ||
|
cc83869725 | ||
|
a10a56a911 | ||
|
45f121c23a | ||
|
cd63338db7 | ||
|
1735cdc0e7 | ||
|
ffe622e0b9 | ||
|
f4f329981c | ||
|
c17e59c4d1 | ||
|
d210666df2 | ||
|
22282df126 | ||
|
d9e4b066da | ||
|
a08c11c0ee | ||
|
3ee33b3207 | ||
|
8c07864e12 | ||
|
ba361c007a | ||
|
1dab2ed396 | ||
|
cf18ce6110 | ||
|
dbc7fa36e7 | ||
|
709d75eb80 | ||
|
a91135835a | ||
|
9fdcdfa915 | ||
|
b43f7c4ed0 | ||
|
ebda41bbd5 | ||
|
581b971cc3 | ||
|
a93748b6c6 | ||
|
aea67ae8ec | ||
|
d9c4cfdbc8 | ||
|
49e26d0d29 | ||
|
988d220f35 | ||
|
59e203f4b2 | ||
|
da6313e1cc | ||
|
e9d0ac7f66 | ||
|
c44ab07bf0 | ||
|
115b7e872c | ||
|
b17c70255f | ||
|
7755891139 | ||
|
f90d60b268 | ||
|
d05ea5e10b | ||
|
04f147e368 | ||
|
08a371f327 | ||
|
4d9fb827c3 | ||
|
f0eeeaad10 | ||
|
ceff8a8684 | ||
|
a09a7c8cf6 | ||
|
856fe8797d | ||
|
f6e2df3e1c | ||
|
31356af213 | ||
|
5f6da3a638 | ||
|
c5d09557da | ||
|
79a2881089 | ||
|
0920fe18f4 | ||
|
a6e4359216 | ||
|
afae94cffa | ||
|
67e293d6aa | ||
|
40b8e36961 | ||
|
04e0b65b4b | ||
|
3c4507b245 | ||
|
b11160f982 | ||
|
47e56828d9 | ||
|
a583a6649b | ||
|
68bdf17e03 | ||
|
d65d858bc9 | ||
|
497e07094a | ||
|
a980935092 | ||
|
114cd2f8c6 | ||
|
1646d8b473 | ||
|
efa1bf0dd7 | ||
|
f316b0598b | ||
|
867ae334fa | ||
|
12388226f5 | ||
|
ebfc7351dd | ||
|
248450373a | ||
|
82e3ae797c | ||
|
a30c1db986 | ||
|
36350cb661 | ||
|
9fb3d44d6a | ||
|
6cc18ec1b8 | ||
|
c32e6c51db | ||
|
81aaaf6e37 | ||
|
eb9762ce5b | ||
|
50433d0b9d | ||
|
015f15c14f | ||
|
b1fb7fa0ab | ||
|
bcbabc4598 | ||
|
e83462018d | ||
|
185ee359e1 | ||
|
19b5aabc82 | ||
|
f087cc5282 | ||
|
fef5eac085 | ||
|
27423f9f36 | ||
|
434fc8af05 | ||
|
3e003780c7 | ||
|
6a22542aeb | ||
|
163c07c783 | ||
|
e8ba2cc557 | ||
|
792202c519 | ||
|
898a3dd85d | ||
|
eb765608bd | ||
|
77101c21ff | ||
|
a72ed1a48a | ||
|
6788165332 | ||
|
b9d3c83314 | ||
|
d00b58f4b7 | ||
|
4edb793f81 | ||
|
4d7e80c0a0 | ||
|
95bc0ef53d | ||
|
0b682171a2 | ||
|
100b0fcda0 | ||
|
93c1fe20ea | ||
|
b4a0bd0daa | ||
|
37dbcf73f7 | ||
|
c596838d85 | ||
|
bc6315391b | ||
|
ce37be8247 | ||
|
635be61bd9 | ||
|
073b00c58e | ||
|
8f4fb2e94e | ||
|
b9f76bdf16 | ||
|
94a00ccab8 | ||
|
2a975d749c | ||
|
0524e77e3e | ||
|
7d279da5af | ||
|
6a590eb922 | ||
|
d80ffb28f2 | ||
|
ecd5a24ea5 | ||
|
ed239cc3c2 | ||
|
d0c16f2835 | ||
|
24f9c05e14 | ||
|
3f6eb3156d | ||
|
2eb994f313 | ||
|
884b82deb8 | ||
|
50a9d96f71 | ||
|
8b71025896 | ||
|
d20cf39e89 | ||
|
7c0cf17da3 | ||
|
0d8fb149fb | ||
|
e7f6b6f3f5 | ||
|
ffadb6abff | ||
|
95fd04ca91 | ||
|
71a08fe96a | ||
|
fd21033fef | ||
|
7f441ad98e | ||
|
fc0d3abf6d | ||
|
45b6f64cd7 | ||
|
ca19734a88 | ||
|
9f5c2666b0 | ||
|
58f8269c19 | ||
|
02b652ffd5 | ||
|
0031200679 | ||
|
85c1985435 | ||
|
3c4b1a2543 | ||
|
189f63efc3 | ||
|
9da5191738 | ||
|
65d5608efd | ||
|
e0113ac595 | ||
|
3bf744debe | ||
|
12162de22f | ||
|
6f77c5720f | ||
|
bbbe7a968c | ||
|
442e612ca3 | ||
|
8bb760ac01 | ||
|
50af5fb2d8 | ||
|
0abbb86e69 | ||
|
1a8dd7c204 | ||
|
484e3b69bd | ||
|
5a889e0471 | ||
|
d008dc7217 | ||
|
27b54cccba | ||
|
96309ff0f8 | ||
|
bfa466397a | ||
|
b67d1d5029 | ||
|
e3ec15b270 | ||
|
e1f07cdfc8 | ||
|
198b7037d5 | ||
|
aac9aef821 | ||
|
97bf5816c0 | ||
|
5b7ae04fbd | ||
|
752fb9e879 | ||
|
fd9e2f9bd8 | ||
|
95d8cc461c | ||
|
4b8a336ba0 | ||
|
623a8f189c | ||
|
67662dfdac | ||
|
493fbf8f2b | ||
|
2c9a9631f5 | ||
|
3bddccc2c1 | ||
|
6a5d110f9a | ||
|
73d1d6485b | ||
|
ea9084037c | ||
|
486fae8ccf | ||
|
c85350821e | ||
|
aebb96b32e | ||
|
2e5295dbaf | ||
|
c9f27a4f3c | ||
|
09afc9943d | ||
|
b39c640cd6 | ||
|
955020005a | ||
|
25ccda7ae2 | ||
|
c6782d86a7 | ||
|
37b679bbd6 | ||
|
6db16ac0f3 | ||
|
d737b8f41d | ||
|
c11f1660ca | ||
|
07dd7bcfc8 | ||
|
2d887c86a9 | ||
|
4bb5993ef0 | ||
|
c44208fed3 | ||
|
8ef61b8673 | ||
|
c8211ae7dd | ||
|
5df618787f | ||
|
494e714bb3 | ||
|
b408767343 | ||
|
b93c7d33eb | ||
|
e556bd70e8 | ||
|
ddb4bc9728 | ||
|
96c2aac849 | ||
|
94b2fb1424 | ||
|
9922479314 | ||
|
ae34a06a5f | ||
|
2a56594534 | ||
|
5192641e0b | ||
|
155e3ff367 | ||
|
57359d18a8 | ||
|
6e1750d25c | ||
|
a707dc4f3e | ||
|
b5fae1a2b6 | ||
|
2828a6df64 | ||
|
8b0a530324 | ||
|
bf28c66e2b | ||
|
3201be72cc | ||
|
a9735e22ff | ||
|
ae51eb3b6c | ||
|
3d1d9ffdc9 | ||
|
d2b7a11456 | ||
|
386f8d07bc | ||
|
2f345f78a8 | ||
|
5ee7346744 | ||
|
0048b00658 | ||
|
5231939302 | ||
|
92c4144697 | ||
|
ec220771ec | ||
|
068e2c6d64 | ||
|
80925b6366 | ||
|
8e6f1334e2 | ||
|
bbc48ec239 | ||
|
0b6f4e516b | ||
|
d1132806da | ||
|
408da409df | ||
|
af421c3e96 | ||
|
abc990bded | ||
|
084b84a6a2 | ||
|
d6b747ddc1 | ||
|
8f487cad40 | ||
|
1a26682e66 | ||
|
d69ab3c28c | ||
|
53fe4a12f1 | ||
|
4301947a1c | ||
|
6c178636c3 | ||
|
a56bd18e95 | ||
|
62785cd99e | ||
|
9a48f1639c | ||
|
ecb6eb61ae | ||
|
88a941cf59 | ||
|
bac39adf42 | ||
|
1e2d3ba1bd | ||
|
4542ea9919 | ||
|
cbfbc3c63b | ||
|
765d47deec | ||
|
91b7b2cd06 | ||
|
95f5ff13c3 | ||
|
7b348a1f0e | ||
|
57da09dee9 | ||
|
1f10bab874 | ||
|
483e052fff | ||
|
5eff6fae15 | ||
|
11a23b4464 | ||
|
2dde0bb5e1 | ||
|
05e98bcad6 | ||
|
49587a9ef8 | ||
|
b21b6c2362 | ||
|
a4d4aa834d | ||
|
3bfb3881d2 | ||
|
248bead428 | ||
|
952279b1f4 | ||
|
213e19763a | ||
|
e13c5580bc | ||
|
aced376e5e | ||
|
868f5295d2 | ||
|
439b3f41c1 | ||
|
03ef5265dd | ||
|
97ebc99b55 | ||
|
3d07b88adc | ||
|
62fa95a5b3 | ||
|
7b13f3c4bb | ||
|
c8017ff01d | ||
|
c896d2a196 | ||
|
3b947e1987 | ||
|
a1652eaf38 | ||
|
a0d6c5ad0e | ||
|
4cf828a948 | ||
|
fe60d6b234 | ||
|
592a6daa03 | ||
|
ba0572d4f6 | ||
|
fdc9730e01 | ||
|
27db641eae | ||
|
4af4a002a7 | ||
|
3ecc4c3bf1 | ||
|
bee6429bed | ||
|
377a4c921f | ||
|
2176221c8e | ||
|
8640a7c013 | ||
|
5f7b0ca647 | ||
|
ad46b99d58 | ||
|
9af2a5b923 | ||
|
0b8769b898 | ||
|
30c34d4ce9 | ||
|
07ba774ed9 | ||
|
bcb65726af | ||
|
1f692330a7 | ||
|
d0c59790bb | ||
|
dadaa34926 | ||
|
aa1be6a23f | ||
|
7a164dd1c7 | ||
|
539e98bf5a | ||
|
e55200f5eb | ||
|
a437c31695 | ||
|
e1eaad2cd0 | ||
|
3c5a83736b | ||
|
77e9fb3d31 | ||
|
aab9a27f73 | ||
|
65aec60ef5 | ||
|
c5635de295 | ||
|
5373856646 | ||
|
ac9ff4918a | ||
|
1e77d6cc66 | ||
|
7f6a83fb66 | ||
|
227d0db778 | ||
|
204a341d14 | ||
|
d4739d17ed | ||
|
877c26199d | ||
|
b7dfa457ed | ||
|
6e1126a541 | ||
|
14a80ef67f | ||
|
c4c50344fb | ||
|
dc7e075789 | ||
|
6e02b80b50 | ||
|
3af66f6280 | ||
|
1b80f4f7aa | ||
|
af5d99aa0b | ||
|
4371369dba | ||
|
00b6e4400c | ||
|
69aef41e5d | ||
|
c6389c697b | ||
|
a11764054c | ||
|
5e1fb46aec | ||
|
17ede8b98f | ||
|
405caef0f4 | ||
|
a409f19f66 | ||
|
3b8eb3d851 | ||
|
b985ae15a4 | ||
|
2e85180dc1 | ||
|
b98aba374a | ||
|
46d3d11e15 | ||
|
85e0a2e1f8 | ||
|
cd13b7f5b5 | ||
|
b6f01ca0fe | ||
|
85a4cca5e5 | ||
|
3a9ec5db61 | ||
|
3521f5882d | ||
|
846ec90575 | ||
|
ebceab5501 | ||
|
89477e52c6 | ||
|
77c8607d20 | ||
|
9bd5280818 | ||
|
96d07a4b25 | ||
|
6113a187c6 | ||
|
efcecb31a8 | ||
|
63a637eef5 | ||
|
dbffcd0030 | ||
|
f0d5afb677 | ||
|
6e1f4cd495 | ||
|
cf9f4ebef9 | ||
|
de19457a5a | ||
|
a8f8073f8c | ||
|
31ac1895af | ||
|
179733a987 | ||
|
21434b4ed6 | ||
|
4e27eca7fa | ||
|
cbe673140b | ||
|
94b5a07fc9 | ||
|
236b433d5f | ||
|
c8e9918cd9 | ||
|
66f0a8b9d1 | ||
|
8fe01853a2 | ||
|
0e3a5619f9 | ||
|
09cae122d1 | ||
|
77566cbd30 | ||
|
798a5ddc7e | ||
|
fe57424e2a | ||
|
a66d7a7164 | ||
|
bb04ed849c | ||
|
4ab3c3a9f4 | ||
|
4004d2f53c | ||
|
4a3a03173a | ||
|
8d8beb1569 | ||
|
8873d6ec90 | ||
|
686ffa6371 | ||
|
f2e68c9f62 | ||
|
b594947399 | ||
|
d33909af1f | ||
|
9e82dabf40 | ||
|
2f55ae5dd8 | ||
|
807517f038 | ||
|
67f1dcf5f9 | ||
|
07607ec1ef | ||
|
7904b0db31 | ||
|
77ac4b9462 | ||
|
07dbc2ba04 | ||
|
ea202eee5b | ||
|
0e9d8b4ca7 | ||
|
f2e38a150b | ||
|
ac3b3bf10c | ||
|
80c515ea98 | ||
|
986b3194ec | ||
|
1e02f5b2f9 | ||
|
6a70a23556 | ||
|
fcacd4fe12 | ||
|
08f811add2 | ||
|
f1cd39babd | ||
|
dd596805e1 | ||
|
6ec3211450 | ||
|
886524bb94 | ||
|
cbc2506a13 | ||
|
b807dd871a | ||
|
47e33b0588 | ||
|
b8cc9ec9f5 | ||
|
7ac537fa40 | ||
|
9b622e8391 | ||
|
32e8065f4c | ||
|
9a35344a14 | ||
|
dd6b906b8b | ||
|
b5014175d0 | ||
|
0e126482ef | ||
|
ffe738f323 | ||
|
c2072332de | ||
|
ab98a09f85 | ||
|
66e8f29c19 | ||
|
889748712a | ||
|
03a70c1d73 | ||
|
f49d007f9f | ||
|
e1a4ccd5f9 | ||
|
4615624431 | ||
|
2e9e8f8bb3 | ||
|
18a79ebe60 | ||
|
da4b89bbd0 | ||
|
6f802f3a76 | ||
|
5fad2f0281 | ||
|
0970b68760 | ||
|
ee03597184 | ||
|
5e0bcf5154 | ||
|
adb8091ff3 | ||
|
f820271418 | ||
|
02c8ee4999 | ||
|
72ce4234a3 | ||
|
43c44446d9 | ||
|
edc5f1f7b2 | ||
|
a921fde598 | ||
|
323cfe763a | ||
|
1cc08204e5 | ||
|
9480e94778 | ||
|
91d2690761 | ||
|
e172cb194a | ||
|
aa3a5b447a | ||
|
c6c86afce1 | ||
|
10a441b2ae | ||
|
8ba8d27241 | ||
|
57e334efc8 | ||
|
c4cd9812e0 | ||
|
175df1796c | ||
|
446acf2181 | ||
|
b3079b83dc | ||
|
24a453f338 | ||
|
b1fe3cf2a9 | ||
|
63183873a5 | ||
|
76661305af | ||
|
05539133cb | ||
|
a26311be88 | ||
|
4597ea64e9 | ||
|
36e1fd45f5 | ||
|
4aaf6bb206 | ||
|
2fcfaef058 | ||
|
701878bcf4 | ||
|
bb27559314 | ||
|
18bfe77c60 | ||
|
806887d56e | ||
|
27c4224030 | ||
|
d7351ce474 | ||
|
a49e523d5d | ||
|
02613ceef4 | ||
|
b432f8f13f | ||
|
76691da8c9 | ||
|
f32844aa15 | ||
|
dbdf3f3c89 | ||
|
7704e46996 | ||
|
cd490a0868 | ||
|
6e86af1037 | ||
|
50873253b0 | ||
|
3a69e622ec | ||
|
b325609a59 | ||
|
21d698deb3 | ||
|
4d6a456b8f | ||
|
b0baf1e1f6 | ||
|
c3fd501ef1 | ||
|
caaccadd83 | ||
|
d266fe8b74 | ||
|
b12c26047b | ||
|
019d864130 | ||
|
2ef2d95fb9 | ||
|
b55e6f1f40 | ||
|
4de0891dcc | ||
|
58858a0290 | ||
|
3a160659d4 | ||
|
b5cdb0c244 | ||
|
514e8e3535 | ||
|
c2f8ee7bab | ||
|
4027ec3b91 | ||
|
2bee616d1c | ||
|
1ad67d4a75 | ||
|
477798e30c | ||
|
c700ec2791 | ||
|
da739b3eb6 | ||
|
264bd514f5 | ||
|
f7091bf074 | ||
|
ae0e178d43 | ||
|
0283110a92 | ||
|
5e7230de21 | ||
|
da615fb11b | ||
|
ef89b3ce6e | ||
|
29e98917a9 | ||
|
f3a19701a2 | ||
|
d7a58ef42e | ||
|
43aebf9f84 | ||
|
5178cc4b6a | ||
|
3f7cbbb004 | ||
|
bd44a55f6b | ||
|
9bdde2a727 | ||
|
31b9d43a7f | ||
|
7869618d51 | ||
|
1f716f0307 | ||
|
da1fed63bd | ||
|
5612776da0 | ||
|
8192cc50d8 | ||
|
6d03a3bcaf | ||
|
c3fa410964 | ||
|
02c59cc457 | ||
|
9ba6c6a134 | ||
|
a2a1e72a78 | ||
|
81ae4c35f0 | ||
|
75e781d08e | ||
|
2180bd989e | ||
|
0201c8d474 | ||
|
3c0de3727d | ||
|
ab57e7d2d7 | ||
|
d0be72bf55 | ||
|
cf89635471 | ||
|
cfe6c6c2ac | ||
|
a540bdcf48 | ||
|
54c7526a66 | ||
|
76a9c07a48 | ||
|
b7673cf134 | ||
|
8e87f55cff | ||
|
0094c3b613 | ||
|
6d69efe258 | ||
|
e26d95b93d | ||
|
f747ef55d1 | ||
|
7f3102f292 | ||
|
bb611d2022 | ||
|
1d4d9ae170 | ||
|
467138ec2c | ||
|
94138252c1 | ||
|
314e955ca3 | ||
|
2590ee7533 | ||
|
fefafc30c1 | ||
|
902936ddd7 | ||
|
c1903428eb | ||
|
dbb07dee79 | ||
|
d805fb7515 | ||
|
e008c33aff | ||
|
0b0a99966b | ||
|
30645741f4 | ||
|
27ab69cd78 | ||
|
ae3e824c91 | ||
|
a0e2c28d81 | ||
|
8466894d49 | ||
|
40d4003696 | ||
|
7b695e77ad | ||
|
558ad30e24 | ||
|
04cdfcb360 | ||
|
468ff01f9f | ||
|
a9bba37df7 | ||
|
08f7a98dbb | ||
|
50adbac117 | ||
|
85eb2d205b | ||
|
1da05c49bd | ||
|
221666df3e | ||
|
bda1c3658b | ||
|
f1ccfbf58e | ||
|
a636fbe336 | ||
|
e9f36fa44f | ||
|
5c88b8eb14 | ||
|
d6ef1fd99d | ||
|
1a3bf0b055 | ||
|
a04801fa17 | ||
|
9f6fe314d2 | ||
|
6751ffb4a4 | ||
|
f1e6830a72 | ||
|
6b663c481c | ||
|
6a934ec2ae | ||
|
1ed588e1c0 | ||
|
24aefd47f1 | ||
|
695758aa18 | ||
|
0194ab7a5b | ||
|
fee94ff368 | ||
|
3d82253308 | ||
|
7fdc6e0b05 | ||
|
c69460d802 | ||
|
46f584076d | ||
|
65d47295bc | ||
|
24987bac6c | ||
|
826a6c7567 | ||
|
c9bcfcc8e0 | ||
|
97e3327b89 | ||
|
376fa1b76e | ||
|
28d592aa70 | ||
|
a8db9c8aeb | ||
|
b36a623e69 | ||
|
c75addd930 | ||
|
4fe0bf4195 | ||
|
255dd6bd56 | ||
|
4dd5b3a6b1 | ||
|
75f69a3234 | ||
|
0c5227b5ab | ||
|
9145243b8c | ||
|
bc87e65ec9 | ||
|
c562f48384 | ||
|
b34042b8e1 | ||
|
8def68fa90 | ||
|
4ea62e5e22 | ||
|
0ce407f1c3 | ||
|
5f252682f3 | ||
|
421fa07317 | ||
|
8329823f7a | ||
|
c9c867d8f3 | ||
|
a9cc2841ac | ||
|
64942b9852 | ||
|
f522f5014c | ||
|
bacf51b443 | ||
|
3a6e6f32d8 | ||
|
0268a48fe6 | ||
|
8ed337d709 | ||
|
f20816b9a2 | ||
|
4e26d85fe5 | ||
|
39cbfb0e33 | ||
|
9122a700e0 | ||
|
62af331966 | ||
|
f739cff28c | ||
|
e94908d08f | ||
|
7107b6e593 | ||
|
2b8a7f4946 | ||
|
8bea8f3ce9 | ||
|
dc2a978716 | ||
|
37be6b86c3 | ||
|
3dfdb88e99 | ||
|
7e5fafa159 | ||
|
e37bda8962 | ||
|
a2784aaa45 | ||
|
e0eb90ec37 | ||
|
d0d5eb69d7 | ||
|
3f887ef619 | ||
|
64f865fa47 | ||
|
694c994807 | ||
|
3eabd1e38e | ||
|
e95557c6fd | ||
|
f4c2c6e3a6 | ||
|
c658a85c35 | ||
|
c0acdd04c4 | ||
|
d2963e8b40 | ||
|
5ad5795a62 | ||
|
b16cd0cc67 | ||
|
1cc727239d | ||
|
367993daf0 | ||
|
88be2a623b | ||
|
f157e9c01f | ||
|
96254b11ea | ||
|
124f1ddbeb | ||
|
24b8227685 | ||
|
6c59b86779 | ||
|
9940202d64 | ||
|
c13f1db02d | ||
|
962137cc48 | ||
|
84e5f91fdf | ||
|
be4c2f4b07 | ||
|
94d677fe1e | ||
|
08696839dc | ||
|
07761dd88b | ||
|
5786699474 | ||
|
af90fd41ca | ||
|
d5c7e5ed68 | ||
|
2c0a202080 | ||
|
d06fcafdc7 | ||
|
1819e1c95c | ||
|
57231d948d | ||
|
ec5ef7af4f | ||
|
7f61f21a0b | ||
|
920a15a6b2 | ||
|
6033703b9b | ||
|
65dbab4581 | ||
|
f5d732ea7e | ||
|
7050e0e9ac | ||
|
87cbfdd130 | ||
|
7c3c2e8aaa | ||
|
9bc798108b | ||
|
693996a968 | ||
|
009aa2e6ab | ||
|
b9e8f48065 | ||
|
579761bcbd | ||
|
72d2f1d995 | ||
|
38bf8cc3fa | ||
|
c62a3865dd | ||
|
73fc66da0f | ||
|
c25e5a77aa | ||
|
df39276c2a | ||
|
2300584501 | ||
|
bda1913ca0 | ||
|
89e493aa6a | ||
|
0495c26546 | ||
|
0723c8533a | ||
|
0819dabc69 | ||
|
616aa951bc | ||
|
637652c1c6 | ||
|
b9aeacc38b | ||
|
fc21e854d3 | ||
|
67b88bc593 | ||
|
5d77b9c53a | ||
|
66f773a8ce | ||
|
a738f75667 | ||
|
dd27978e12 | ||
|
709efdd04e | ||
|
2fa638d3c6 | ||
|
57fc3b1c04 | ||
|
bcedb95eec | ||
|
e0c6386d23 | ||
|
833c322812 | ||
|
d18f0ee80a | ||
|
ecae747b64 | ||
|
88b72a13f3 | ||
|
caab7908dd | ||
|
2e1aa5918a | ||
|
9239a95f20 | ||
|
cffaed6de8 | ||
|
64b1e9cff0 | ||
|
517ffbd371 | ||
|
44a4feacde | ||
|
757c7690cb | ||
|
ca0e8c0299 | ||
|
ef1f7b0e76 | ||
|
b3d007c6d1 | ||
|
0fcb3fd5e4 | ||
|
ef3544f268 | ||
|
90b91a9635 | ||
|
7c5c4faa75 | ||
|
feca582da0 | ||
|
7b0f973a9a | ||
|
23e732e8b2 | ||
|
dd586f23eb | ||
|
acf8370c54 | ||
|
ec7e64fdf4 | ||
|
b5d2f47cf8 | ||
|
956cef2cd6 | ||
|
982a6f0342 | ||
|
f750e8897a | ||
|
a83754e7f1 | ||
|
be32df3e1e | ||
|
98354345a0 | ||
|
40fe55a46d | ||
|
c1711d39d3 | ||
|
7f062e720a | ||
|
746fdc36e4 | ||
|
5b22989e57 | ||
|
4c7e3a8d44 | ||
|
b4d184baae | ||
|
109b3f0423 | ||
|
943154374a | ||
|
4bb49799af | ||
|
5b0da238e2 | ||
|
52c470b487 | ||
|
d278641add | ||
|
7024c1325c | ||
|
f9129c28d2 | ||
|
4c6360b8af | ||
|
2c8f3d4f5b | ||
|
d7a2d0733f | ||
|
5dc07290eb | ||
|
4f1bd3712e | ||
|
aad6ea7833 | ||
|
aac9856e53 | ||
|
ca52798855 | ||
|
662b76b09c | ||
|
795bca03a3 | ||
|
64cf9788d9 | ||
|
7f9ca582d6 | ||
|
d3343518f1 | ||
|
bcd1c9018a | ||
|
8d979b3175 | ||
|
a3e3089032 | ||
|
9ca5158eeb | ||
|
0e215899e4 | ||
|
e2c7b455d2 | ||
|
748b9ac4e7 | ||
|
9adf69932c | ||
|
0d804a0558 | ||
|
dd4f97313e | ||
|
8726988291 | ||
|
92cb0b0628 | ||
|
2e4acb721a | ||
|
1ce4b6f11b | ||
|
751048ab3e | ||
|
52609413a2 | ||
|
a6686029a5 | ||
|
43de166d22 | ||
|
2937d70866 | ||
|
9ea4c051d5 | ||
|
01d6587c11 | ||
|
09802d418e | ||
|
e85b674f38 | ||
|
6ebc731382 | ||
|
003289dc71 | ||
|
103b72981f | ||
|
ff569cc2ec | ||
|
7abd9898c8 | ||
|
e7970bdbb3 | ||
|
f5ef34ce32 | ||
|
51ce84152c | ||
|
44d9be49da | ||
|
752a29449f | ||
|
2a59e8fe0b | ||
|
012870cf26 | ||
|
9c34019446 | ||
|
d62d039a6a | ||
|
f430879e14 | ||
|
5b59d6ba35 | ||
|
97f9f034e7 | ||
|
8e2e9d4aee | ||
|
4d7fca7b2a | ||
|
acd3c0258c | ||
|
6d39f514ca | ||
|
1a1c6b6c5a | ||
|
792755f097 | ||
|
6ec8267001 | ||
|
387c786eef | ||
|
f761460122 | ||
|
c865a8fec8 | ||
|
4adb51f6cf | ||
|
7e8e332891 | ||
|
9e4df1d196 | ||
|
e82ed5cb9c | ||
|
767feff6b9 | ||
|
e29cb1b5a4 | ||
|
237a2a2f92 | ||
|
f74b5cd1d9 | ||
|
642038576a | ||
|
ec0948594d | ||
|
f463e995dc | ||
|
f00c0489a7 | ||
|
25a0287f1d | ||
|
714d33d25f | ||
|
b3c3ccea90 | ||
|
6db5d818b0 | ||
|
aceb06bc9d | ||
|
9b7dec7360 | ||
|
9884730b1b | ||
|
a2b3ac523b | ||
|
dc6e87ef30 | ||
|
302a606d22 | ||
|
e08378813a | ||
|
aa116cdd4a | ||
|
3dd775807e | ||
|
9bfdcbc622 | ||
|
1e2363924f | ||
|
402ab0e481 | ||
|
681669cad6 | ||
|
98b7aac9fd | ||
|
076dac6a84 | ||
|
790dc5003a | ||
|
68540a1c44 | ||
|
fa531a7b55 | ||
|
9626185dbe | ||
|
1af6c27d3e | ||
|
75238943e4 | ||
|
35cfccba16 | ||
|
9ab1185cfb | ||
|
c7055129b6 | ||
|
8d8eb2e9a3 | ||
|
7f4c1e0dbf | ||
|
38f26ef793 | ||
|
1797a1e5a6 | ||
|
417db979f4 | ||
|
288fc7e70b | ||
|
8653de90d4 | ||
|
96e9cabc6b | ||
|
3908351a5d | ||
|
d3db63aba3 | ||
|
5b21ad4d7b | ||
|
a032e1546a | ||
|
329f38b1cc | ||
|
f011b08b12 | ||
|
59063daed3 | ||
|
2a957a6303 | ||
|
de22bd3d33 | ||
|
81ef824b85 | ||
|
349bfc82fb | ||
|
b984fa9d73 | ||
|
78def447ce | ||
|
5ba69c155a | ||
|
a238f79fe6 | ||
|
d250b6432c | ||
|
494d83ae92 | ||
|
669ca83bec | ||
|
5ee0ed089e | ||
|
dece60b8f8 | ||
|
742e432657 | ||
|
5af607327b | ||
|
d2858f7c37 | ||
|
9e0c94581c | ||
|
18020a78d3 | ||
|
4e1f268d4f | ||
|
2590c97061 | ||
|
104ece2be4 | ||
|
02c82745d0 | ||
|
ece52dd025 | ||
|
0c9bb41bee | ||
|
a8153c9101 | ||
|
b49d17a018 | ||
|
1b374c3093 | ||
|
a7c3edc9d1 | ||
|
9bad1e3f3f | ||
|
88510bb270 | ||
|
34711eea80 | ||
|
3fe5a34029 | ||
|
b70e781fa7 | ||
|
92c5989b5e | ||
|
d6b8394016 | ||
|
45666e6090 | ||
|
1b12dd895a | ||
|
7ed237c567 | ||
|
e525842d24 | ||
|
baaf2ec74e | ||
|
bac74a7d4f | ||
|
f812129d81 | ||
|
3d8700d727 | ||
|
8d34ae76fc | ||
|
c8b4500f1d | ||
|
f56c26ad2b | ||
|
3a8f4dcadc | ||
|
4538749192 | ||
|
2ee43370b2 | ||
|
1595a64c61 | ||
|
1c4720256a | ||
|
fe9551636e | ||
|
82ee2e43c3 | ||
|
8cf515ec13 | ||
|
7d89dc1c70 | ||
|
b892d50e6d | ||
|
b0a45a21f4 | ||
|
b5e6258353 | ||
|
edaf4d2ad6 | ||
|
fe2177151e | ||
|
726bcff1b4 | ||
|
49c53d4b35 | ||
|
22763f3439 | ||
|
9af4145ab7 | ||
|
cf5867c131 | ||
|
4d7234512c | ||
|
92a253eb82 | ||
|
edf6adafe0 | ||
|
4792e0453e | ||
|
005507a3a3 | ||
|
19fa135a61 | ||
|
935ecfbeec | ||
|
8a426914f0 | ||
|
563153a6d0 | ||
|
78f9fed6e1 | ||
|
4828e8e895 | ||
|
90103d16f4 | ||
|
e4f4d1861b | ||
|
fd45038b91 | ||
|
42c303d90a | ||
|
e8a4fbd7a3 | ||
|
426c7be60d | ||
|
6bc949752d | ||
|
ebdc354468 | ||
|
7f74005454 | ||
|
94c1cecd14 | ||
|
5f14c79415 | ||
|
245db2dda5 | ||
|
ff7887a006 | ||
|
b0b2493935 | ||
|
56e5874205 | ||
|
c64d3b1f42 | ||
|
ddeefc03b0 | ||
|
40036362f9 | ||
|
73dfdd1494 | ||
|
1a562792cf | ||
|
bac2ac9d50 | ||
|
c7c270fc2c | ||
|
172eb97ce6 | ||
|
e341f144b9 | ||
|
05bad22ede | ||
|
84a6784d26 | ||
|
291f6e4405 | ||
|
a606cb23e1 | ||
|
4c1fae008f | ||
|
0862e3c541 | ||
|
e915d408a9 | ||
|
19509d7ac5 | ||
|
db1abf9a17 | ||
|
1c5741d0d8 | ||
|
5eac763f8f | ||
|
8df7d148da | ||
|
7e97915fd1 | ||
|
232aeb5c68 | ||
|
d94e8f22e1 | ||
|
f58d387d92 | ||
|
c65124a21e | ||
|
08d7f61a19 | ||
|
28cb9b0f23 | ||
|
137b83f428 | ||
|
2c06cdec24 | ||
|
123250246c | ||
|
79891d711e | ||
|
7c35cb0a34 | ||
|
49b085278c | ||
|
d2916d180d | ||
|
87669d1d46 | ||
|
e15e4c1e35 | ||
|
f5534a1f67 | ||
|
df5d3e8020 | ||
|
4db1bd6d75 | ||
|
3654a450f8 | ||
|
1aba7624a1 | ||
|
52213d7c55 | ||
|
69a90dc806 | ||
|
c5d96f173d | ||
|
01b37db0d2 | ||
|
d450a17d58 | ||
|
23966a3b04 | ||
|
99d9533b1c | ||
|
3afe8a98dc | ||
|
707d64982a | ||
|
4e7e3c7680 | ||
|
39a5cee1c2 | ||
|
d5a7dd5d44 | ||
|
7b345f2302 | ||
|
5d570ed022 | ||
|
2a9e1258b5 | ||
|
2f56328780 | ||
|
f6b0255ee6 | ||
|
e7846adaf2 | ||
|
1b516ccf2f | ||
|
6f4a4b60bc | ||
|
21824962b9 | ||
|
2c04ddb514 | ||
|
6cfb3b3dad | ||
|
6692a01fb1 | ||
|
f25585abe1 | ||
|
eedd70061f | ||
|
6df2b97041 | ||
|
21209083be | ||
|
d21558e316 | ||
|
d6d002c2f6 | ||
|
2676cd0dde | ||
|
f879c98995 | ||
|
d6049e71c0 | ||
|
014d3f9c99 | ||
|
92818b12b2 | ||
|
637867dbc8 | ||
|
b175388465 | ||
|
d33d40b9ce | ||
|
c12d93b435 | ||
|
16803776d0 | ||
|
ed7a09d223 | ||
|
a1d7fdb2d4 | ||
|
e42a88ded9 | ||
|
07eaa0c691 | ||
|
3cd15a6168 | ||
|
8597206e84 | ||
|
444d4a94f1 | ||
|
1e35e89d1c | ||
|
1e54e661f5 | ||
|
1eb4066250 | ||
|
4140b2ad5b | ||
|
ebdf52d344 | ||
|
7d625b4640 | ||
|
4727ee9bbf | ||
|
46ea7c8500 | ||
|
5a13d56122 | ||
|
eac2a85478 | ||
|
3ac30f07d3 | ||
|
23d60cb28f | ||
|
7e7a3c5fa3 | ||
|
dc3a7542f8 | ||
|
04149c9491 | ||
|
c4193d27f7 | ||
|
a191c45a57 | ||
|
3c960eb501 | ||
|
e9f0af36c8 | ||
|
591bdea6f4 | ||
|
fb75e7f5a5 | ||
|
d89c976a5c | ||
|
f965f081db | ||
|
3b29043dca | ||
|
2bff9a4e18 | ||
|
4c4ff840d4 | ||
|
ad924961e6 | ||
|
d4d4f78387 | ||
|
7c58959e24 | ||
|
93ab36477e | ||
|
56f81e791a | ||
|
370466dbe7 | ||
|
78ecb7ff43 | ||
|
909d55076c | ||
|
a80b0d9ccb | ||
|
e37dae3a0d | ||
|
403da79e0f | ||
|
aa91d12d27 | ||
|
db08bafc40 | ||
|
9241c0064f | ||
|
e3bd1a2d2e | ||
|
bf9c232f85 | ||
|
ec6e27ab43 | ||
|
5c01d5fd17 | ||
|
f21572879e | ||
|
4300aafc38 | ||
|
4dc275bb17 | ||
|
9190fd98d0 | ||
|
b2b810ddea | ||
|
ef57f8b02f | ||
|
d2947b811d | ||
|
fab59163dd | ||
|
c5ec2cf3e1 | ||
|
eb409dcc01 | ||
|
65d62da507 | ||
|
e316674946 | ||
|
b4f097d187 | ||
|
30ae80c1a1 | ||
|
874a30eb6b | ||
|
f9b8bd2947 | ||
|
6377f9f287 | ||
|
06c94316dd | ||
|
5ef915c00b | ||
|
9c5d132ebc | ||
|
4a74816634 | ||
|
72701e0775 | ||
|
d65a60e61d | ||
|
9e2a154c50 | ||
|
3962041825 | ||
|
f79799bee1 | ||
|
c472e8d033 | ||
|
d845b73c85 | ||
|
7a7abd09bd | ||
|
077c4f4c78 | ||
|
c3a040ec25 | ||
|
ef840af7a0 | ||
|
01355fd93e | ||
|
ad8ce5e74e | ||
|
4ac26bf3d8 | ||
|
f77ca18878 | ||
|
897ed1b19e | ||
|
e82359a347 | ||
|
4f65a0a396 | ||
|
8594c2f702 | ||
|
9f0447dc62 | ||
|
89027479c1 | ||
|
49f6316417 | ||
|
9cdc96eadb | ||
|
d2faeec55f | ||
|
23f67c2160 | ||
|
91f9e11b36 | ||
|
65d36cf2e2 | ||
|
96bca9aa5d | ||
|
23677bd5a3 | ||
|
e286c951df | ||
|
a944282469 | ||
|
d6f203d9bf | ||
|
5155bf292a | ||
|
e5478930b4 | ||
|
2e0e187ffc | ||
|
82858575c7 | ||
|
4c629fb930 | ||
|
ec3a1d9bb0 | ||
|
300e8cfcec | ||
|
761ddc3087 | ||
|
4e75894438 | ||
|
6091be7606 | ||
|
01fd6d2689 | ||
|
d84193d5a4 | ||
|
67849bc1be | ||
|
ac643bf322 | ||
|
c95e3ff232 | ||
|
bc62dc3416 | ||
|
e4c1a8ce96 | ||
|
ecb904035a | ||
|
8d7db40909 | ||
|
35862d7c11 | ||
|
830786614f | ||
|
354974a0c7 | ||
|
7f470ab3e3 | ||
|
b83666a584 | ||
|
40392f24cd | ||
|
fc6a29cfa2 | ||
|
78d6321f9f | ||
|
3013e88103 | ||
|
01fdbdf305 | ||
|
5c99ffd69a | ||
|
e5d3dd9a03 | ||
|
615ed25e00 | ||
|
81a867e631 | ||
|
eb5864e9c7 | ||
|
e126623b6f | ||
|
441fdc92a4 | ||
|
a9a9a78983 | ||
|
311fee0443 | ||
|
bae9a511bc | ||
|
b4cd10ef85 | ||
|
76dd4ce250 | ||
|
1bb8b4621b | ||
|
4a2b535403 | ||
|
16f12d54f9 | ||
|
540a2d85a3 | ||
|
eef3d38f73 | ||
|
1115935a3a | ||
|
5fc4b01b8a | ||
|
2486c868b3 | ||
|
c0ccb4e5d3 | ||
|
ed826472e0 | ||
|
aedddbff0d | ||
|
abcf980844 | ||
|
f5289e7681 | ||
|
b285c81c09 | ||
|
953f316908 | ||
|
6167699c20 | ||
|
7ae32bf1e7 | ||
|
f9aecb5036 | ||
|
bbe3a5b111 | ||
|
75eca120f0 | ||
|
71efde7cd2 | ||
|
895253012f | ||
|
73c3dec29f | ||
|
b141d14d74 | ||
|
f27ec8cb7f | ||
|
788eb253f4 | ||
|
e8126f565d | ||
|
fb7ac48962 | ||
|
111ebd2f19 | ||
|
275305b16c | ||
|
2024018eb2 | ||
|
bc6313b546 | ||
|
fccd793168 | ||
|
e133fc06cb | ||
|
3fe1cdde60 | ||
|
79acc5b503 | ||
|
b60bdb72fa | ||
|
bf061bfaf2 | ||
|
6f16819cad | ||
|
a32402f1a6 | ||
|
b76e009aaa | ||
|
df0d9f37a2 | ||
|
3daabe584f | ||
|
b42d3d6a08 | ||
|
a1bb6d6e2f | ||
|
7f952af1b9 | ||
|
fdb7da1460 | ||
|
5fa271314c | ||
|
3dfde49ac9 |
780 changed files with 33764 additions and 20139 deletions
86
.gitattributes
vendored
86
.gitattributes
vendored
|
@ -1,26 +1,78 @@
|
||||||
* text eol=lf
|
* text=auto eol=lf
|
||||||
*.bat text eol=crlf
|
*.[cC][mM][dD] text eol=crlf
|
||||||
*.sh text eol=lf
|
*.[bB][aA][tT] text eol=crlf
|
||||||
|
*.[pP][sS]1 text eol=crlf
|
||||||
|
*.[sS][hH] text eol=lf
|
||||||
|
|
||||||
*.patch text eol=lf
|
*.patch text eol=lf
|
||||||
*.java text eol=lf
|
|
||||||
*.scala text eol=lf
|
|
||||||
*.groovy text eol=lf
|
|
||||||
|
|
||||||
*.gradle text eol=crlf
|
|
||||||
gradle.properties text eol=crlf
|
|
||||||
/gradle/wrapper/gradle-wrapper.properties text eol=crlf
|
|
||||||
*.cfg text eol=lf
|
|
||||||
|
|
||||||
*.png binary
|
*.png binary
|
||||||
*.jar binary
|
|
||||||
*.war binary
|
|
||||||
*.lzma binary
|
*.lzma binary
|
||||||
*.zip binary
|
*.zip binary
|
||||||
*.gzip binary
|
*.gzip binary
|
||||||
*.dll binary
|
|
||||||
*.so binary
|
|
||||||
*.exe binary
|
*.exe binary
|
||||||
|
*.ico binary
|
||||||
|
*.eot binary
|
||||||
|
*.ttf binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
*.a binary
|
||||||
|
*.lib binary
|
||||||
|
*.icns binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.mov binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.flv binary
|
||||||
|
*.fla binary
|
||||||
|
*.swf binary
|
||||||
|
*.gz binary
|
||||||
|
*.tar binary
|
||||||
|
*.tar.gz binary
|
||||||
|
*.7z binary
|
||||||
|
*.pyc binary
|
||||||
|
*.gpg binary
|
||||||
|
*.bin binary
|
||||||
|
|
||||||
*.gitattributes text eol=crlf
|
*.gitattributes text
|
||||||
*.gitignore text eol=crlf
|
.gitignore text
|
||||||
|
|
||||||
|
# Java sources
|
||||||
|
*.java text diff=java
|
||||||
|
*.kt text diff=kotlin
|
||||||
|
*.groovy text diff=java
|
||||||
|
*.scala text diff=java
|
||||||
|
*.gradle text diff=java
|
||||||
|
*.gradle.kts text diff=kotlin
|
||||||
|
|
||||||
|
# These files are text and should be normalized (Convert crlf => lf)
|
||||||
|
*.css text diff=css
|
||||||
|
*.scss text diff=css
|
||||||
|
*.sass text
|
||||||
|
*.df text
|
||||||
|
*.htm text diff=html
|
||||||
|
*.html text diff=html
|
||||||
|
*.js text
|
||||||
|
*.jsp text
|
||||||
|
*.jspf text
|
||||||
|
*.jspx text
|
||||||
|
*.properties text
|
||||||
|
*.tld text
|
||||||
|
*.tag text
|
||||||
|
*.tagx text
|
||||||
|
*.xml text
|
||||||
|
|
||||||
|
# These files are binary and should be left untouched
|
||||||
|
# (binary is a macro for -text -diff)
|
||||||
|
*.class binary
|
||||||
|
*.dll binary
|
||||||
|
*.ear binary
|
||||||
|
*.jar binary
|
||||||
|
*.so binary
|
||||||
|
*.war binary
|
||||||
|
*.jks binary
|
||||||
|
|
||||||
|
mvnw text eol=lf
|
||||||
|
gradlew text eol=lf
|
83
.github/workflows/push.yml
vendored
Normal file
83
.github/workflows/push.yml
vendored
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
name: push
|
||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
launcher:
|
||||||
|
name: Launcher
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.gradle/caches
|
||||||
|
key: gravit-${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}-launcher
|
||||||
|
|
||||||
|
- name: Set up JDK 21
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
java-version: 21
|
||||||
|
distribution: temurin
|
||||||
|
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Build with Gradle
|
||||||
|
run: ./gradlew build
|
||||||
|
|
||||||
|
- name: Generate and submit dependency graph
|
||||||
|
uses: gradle/actions/dependency-submission@417ae3ccd767c252f5661f1ace9f835f9654f2b5
|
||||||
|
|
||||||
|
- name: Create artifacts
|
||||||
|
run: |
|
||||||
|
mkdir -p artifacts/modules
|
||||||
|
cd LaunchServer/build/libs
|
||||||
|
mv proguard proguard-libraries
|
||||||
|
zip -r -9 ../../../artifacts/libraries.zip * -x "LaunchServer.jar" -x "LaunchServer-clean.jar"
|
||||||
|
cp LaunchServer.jar ../../../artifacts/LaunchServer.jar
|
||||||
|
cd ../../..
|
||||||
|
cp ServerWrapper/build/libs/ServerWrapper.jar artifacts/ServerWrapper.jar
|
||||||
|
cp ServerWrapper/build/libs/ServerWrapper-inline.jar artifacts/ServerWrapperInline.jar
|
||||||
|
cp LauncherAuthlib/build/libs/LauncherAuthlib.jar artifacts/LauncherAuthlib.jar || true
|
||||||
|
cp modules/*_module/build/libs/*.jar artifacts/modules || true
|
||||||
|
cp modules/*_lmodule/build/libs/*.jar artifacts/modules || true
|
||||||
|
cp javaargs.txt artifacts/javaargs.txt || true
|
||||||
|
cp java24args.txt artifacts/java24args.txt || true
|
||||||
|
|
||||||
|
- name: Upload artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Launcher
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Get version value, set to env
|
||||||
|
if: startsWith(github.event.ref, 'refs/tags')
|
||||||
|
run: echo "LAUNCHER_VERSION=$(echo ${{ github.event.ref }} | awk -F\/ '{print $3}')" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Prebuild release files
|
||||||
|
if: startsWith(github.event.ref, 'refs/tags')
|
||||||
|
run: |
|
||||||
|
cd artifacts
|
||||||
|
zip -r -9 Release.zip *
|
||||||
|
zip -j -9 LaunchServerModules.zip ../modules/*_module/build/libs/*.jar
|
||||||
|
zip -j -9 LauncherModules.zip ../modules/*_lmodule/build/libs/*.jar
|
||||||
|
cd ../LaunchServer/build/libs
|
||||||
|
zip -r -9 ../../../artifacts/LauncherBase.zip * -x "LaunchServer-clean.jar"
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
id: create_release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
if: startsWith(github.event.ref, 'refs/tags')
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# Список настроек тута: https://github.com/softprops/action-gh-release#-customizing
|
||||||
|
# Можно сделать пуш описания релиза из файла
|
||||||
|
with:
|
||||||
|
name: GravitLauncher ${{ env.LAUNCHER_VERSION }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
files: |
|
||||||
|
artifacts/*
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -107,3 +107,6 @@ buildnumber
|
||||||
*.directory
|
*.directory
|
||||||
cmd.bat
|
cmd.bat
|
||||||
cmd.sh
|
cmd.sh
|
||||||
|
project/target
|
||||||
|
## PVS Studio
|
||||||
|
.PVS-Studio/
|
||||||
|
|
2
.gitmodules
vendored
2
.gitmodules
vendored
|
@ -1,3 +1,3 @@
|
||||||
[submodule "modules"]
|
[submodule "modules"]
|
||||||
path = modules
|
path = modules
|
||||||
url = git@github.com:GravitLauncher/LauncherModules.git
|
url = https://github.com/GravitLauncher/LauncherModules.git
|
||||||
|
|
23
.travis.yml
23
.travis.yml
|
@ -1,23 +0,0 @@
|
||||||
# project is java
|
|
||||||
language: java
|
|
||||||
# Use https (public access) instead of git for git-submodules. This modifies only Travis-CI behavior!
|
|
||||||
# disable the default submodule logic
|
|
||||||
git:
|
|
||||||
submodules: false
|
|
||||||
# use sed to replace the SSH URL with the public URL, then init and update submodules
|
|
||||||
before_install:
|
|
||||||
- sed -i 's/git@github.com:/https:\/\/github.com\//' .gitmodules
|
|
||||||
- git submodule update --init --recursive
|
|
||||||
# gradle
|
|
||||||
before_cache:
|
|
||||||
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
|
|
||||||
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- $HOME/.gradle/caches/
|
|
||||||
- $HOME/.gradle/wrapper/
|
|
||||||
script:
|
|
||||||
- ./gradlew assemble build
|
|
||||||
# not working artifacts
|
|
||||||
addons:
|
|
||||||
artifacts: false
|
|
47
CODE_OF_CONDUCT.md
Normal file
47
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# GravitLauncher GitConvention #
|
||||||
|
|
||||||
|
Цель конвенции — внедрить простые, прозрачные и эффективные правила работы с Git.
|
||||||
|
|
||||||
|
Разработка GravitLauncher идёт на базе [Git Flow](https://leanpub.com/git-flow/read). Подробности ниже.
|
||||||
|
|
||||||
|
## Ветвление ##
|
||||||
|
|
||||||
|
| Название ветки | Значение ветки | Исходная ветка | Пример ветки |
|
||||||
|
| ------------- | ------------- | ------------- | ------------- |
|
||||||
|
| **master** | Полностью готовая для production-а | **release** | |
|
||||||
|
| **develop** | Разработка нового функционала | **master** | |
|
||||||
|
| **release** | Тестирование всего нового функционала | **develop** | |
|
||||||
|
| | | | |
|
||||||
|
| **bugfix-*** | Исправляет баг нового функционала | **release** | *bugfix-auth* |
|
||||||
|
| **feature-*** | Добавляет новую возможность | **develop** | *feature-auth* |
|
||||||
|
| **hotfix-*** | Вносит срочное исправление для production-а | **master** | *hotfix-auth* |
|
||||||
|
|
||||||
|
-----
|
||||||
|

|
||||||
|
-----
|
||||||
|
|
||||||
|
## Коммиты ##
|
||||||
|
|
||||||
|
**Основные правила:**
|
||||||
|
|
||||||
|
1. Все коммиты должны быть на английском языке.
|
||||||
|
2. Запрещено использовать прошедшее время.
|
||||||
|
3. Обязательно должен быть использован префикс.
|
||||||
|
4. В конце не должно быть лишнего знака препинания.
|
||||||
|
5. Длина любой части не должна превышать 100 символов.
|
||||||
|
|
||||||
|
**Структура:**
|
||||||
|
|
||||||
|
```
|
||||||
|
[Префикс] <Сообщение>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Префикс | Значение | Пример |
|
||||||
|
| ------- | -------- | ------ |
|
||||||
|
| **[FIX]** | Всё, что касается исправления багов | [FIX] Bug with failed authorization |
|
||||||
|
| **[DOCS]** | Всё, что касается документации | [DOCS] Documenting Authorization API |
|
||||||
|
| **[FEATURE]** | Всё, что касается новых возможностей | [FEATURE] 2FA on authorization |
|
||||||
|
| **[STYLE]** | Всё, что касается опечаток и форматирования | [STYLE] Typos in the authorization module |
|
||||||
|
| **[REFACTOR]** | Всё, что касается рефакторинга | [REFACTOR] Switching to EDA in the authorization module |
|
||||||
|
| **[TEST]** | Всё, что касается тестирования | [TEST] Coverage of the authorization module with tests |
|
||||||
|
| **[ANY]** | Всё, что не подходит к предыдущему. | [ANY] Connecting Travis CI |
|
12
Dockerfile
Normal file
12
Dockerfile
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
FROM ubuntu:latest
|
||||||
|
RUN apt-get update && apt-get install -y osslsigncode openjdk-11-jdk unzip jq screen
|
||||||
|
ADD https://download2.gluonhq.com/openjfx/11.0.2/openjfx-11.0.2_linux-x64_bin-jmods.zip .
|
||||||
|
RUN unzip openjfx-11.0.2_linux-x64_bin-jmods.zip && mv javafx-jmods-11.0.2/* /usr/lib/jvm/java-11-openjdk-amd64/jmods/ && rmdir javafx-jmods-11.0.2 && rm openjfx-11.0.2_linux-x64_bin-jmods.zip
|
||||||
|
RUN mkdir ./libraries ./launcher-libraries ./launcher-libraries-compile ./compat ./compat/modules
|
||||||
|
COPY ./LaunchServer/build/libs/LaunchServer.jar .
|
||||||
|
COPY ./LaunchServer/build/libs/libraries ./libraries
|
||||||
|
COPY ./LaunchServer/build/libs/launcher-libraries ./launcher-libraries
|
||||||
|
COPY ./LaunchServer/build/libs/launcher-libraries-compile ./launcher-libraries-compile
|
||||||
|
COPY ./compat/authlib/authlib-clean.jar ./LauncherAuthlib/build/libs/* ./ServerWrapper/build/libs/ServerWrapper.jar ./compat/
|
||||||
|
COPY ./modules/*_module/build/libs/* ./modules/*_lmodule/build/libs/* ./compat/modules/
|
||||||
|
CMD screen -DmS launchserver java -javaagent:LaunchServer.jar -jar LaunchServer.jar
|
|
@ -1,86 +1,199 @@
|
||||||
def mainClassName = "ru.gravit.launchserver.LaunchServer"
|
def mainClassName = "pro.gravit.launchserver.Main"
|
||||||
def mainAgentName = "ru.gravit.launchserver.StarterAgent"
|
def mainAgentName = "pro.gravit.launchserver.StarterAgent"
|
||||||
|
|
||||||
|
evaluationDependsOn(':Launcher')
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
|
||||||
url "https://hub.spigotmc.org/nexus/content/repositories/snapshots"
|
|
||||||
}
|
|
||||||
maven {
|
|
||||||
url "http://maven.geomajas.org/"
|
|
||||||
}
|
|
||||||
maven {
|
maven {
|
||||||
url "https://oss.sonatype.org/content/repositories/snapshots"
|
url "https://oss.sonatype.org/content/repositories/snapshots"
|
||||||
}
|
}
|
||||||
maven {
|
maven {
|
||||||
url "http://repo.md-5.net/content/groups/public"
|
url "https://jcenter.bintray.com/"
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
url "https://jitpack.io/"
|
||||||
|
}
|
||||||
|
maven {
|
||||||
|
url 'https://maven.gravit-support.ru/repository/jitpack'
|
||||||
|
credentials {
|
||||||
|
username = 'gravitlauncher'
|
||||||
|
password = 'gravitlauncher'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = '1.8'
|
sourceCompatibility = '21'
|
||||||
targetCompatibility = '1.8'
|
targetCompatibility = '21'
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
bundleOnly
|
bundleOnly
|
||||||
bundle
|
bundle
|
||||||
hikari
|
pack
|
||||||
bundle.extendsFrom bundleOnly
|
proguardPack
|
||||||
compileOnly.extendsFrom bundle, hikari
|
bundleOnly.extendsFrom bundle
|
||||||
|
api.extendsFrom bundle, pack
|
||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
dependsOn parent.childProjects.Launcher.tasks.build, parent.childProjects.Launcher.tasks.genRuntimeJS, parent.childProjects.Launcher.tasks.jar
|
dependsOn parent.childProjects.Launcher.tasks.assemble
|
||||||
from { configurations.runtime.collect { it.isDirectory() ? it : zipTree(it) } }
|
from { configurations.pack.collect { it.isDirectory() ? it : zipTree(it) } }
|
||||||
from(parent.childProjects.Launcher.tasks.jar.archivePath, parent.childProjects.Launcher.tasks.genRuntimeJS.archivePath)
|
exclude("module-info.class")
|
||||||
|
from(parent.childProjects.Launcher.tasks.shadowJar)
|
||||||
|
from(parent.childProjects.Launcher.tasks.genRuntimeJS)
|
||||||
manifest.attributes("Main-Class": mainClassName,
|
manifest.attributes("Main-Class": mainClassName,
|
||||||
"Premain-Class": mainAgentName,
|
"Premain-Class": mainAgentName,
|
||||||
"Can-Redefine-Classes": "true",
|
"Multi-Release": "true",
|
||||||
"Can-Retransform-Classes": "true",
|
"Automatic-Module-Name": "launchserver"
|
||||||
"Can-Set-Native-Method-Prefix": "true"
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
testLogging {
|
||||||
|
events "passed", "skipped", "failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('sourcesJar', Jar) {
|
||||||
|
from sourceSets.main.allJava
|
||||||
|
archiveClassifier.set('sources')
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('javadocJar', Jar) {
|
||||||
|
from javadoc
|
||||||
|
archiveClassifier.set('javadoc')
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('cleanjar', Jar) {
|
||||||
|
dependsOn jar
|
||||||
|
archiveClassifier.set('clean')
|
||||||
|
manifest.attributes("Main-Class": mainClassName,
|
||||||
|
"Automatic-Module-Name": "launchserver"
|
||||||
|
)
|
||||||
|
from sourceSets.main.output
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
compile project(':libLauncher') // pack
|
pack(project(':LauncherAPI')) {
|
||||||
compileOnly 'org.spigotmc:spigot-api:1.8-R0.1-SNAPSHOT' // api
|
exclude group: "com.google.code.gson"
|
||||||
compileOnly 'net.md-5:bungeecord-api:1.8-SNAPSHOT' // api
|
}
|
||||||
compileOnly 'org.ow2.asm:asm-debug-all:5.0.4'
|
bundle group: 'com.google.code.gson', name: 'gson', version: rootProject['verGson']
|
||||||
bundleOnly 'org.ow2.asm:asm-all:5.0.4'
|
bundle group: 'me.tongfei', name: 'progressbar', version: '0.10.1'
|
||||||
bundle 'org.apache.logging.log4j:log4j-core:2.9.0'
|
bundle group: 'org.fusesource.jansi', name: 'jansi', version: rootProject['verJansi']
|
||||||
bundle 'mysql:mysql-connector-java:8.0.12'
|
bundle group: 'org.jline', name: 'jline-native', version: rootProject['verJline']
|
||||||
bundle 'jline:jline:2.14.6'
|
bundle group: 'org.jline', name: 'jline-reader', version: rootProject['verJline']
|
||||||
bundle 'net.sf.proguard:proguard-base:6.0.3'
|
bundle group: 'org.jline', name: 'jline-terminal-ffm', version: rootProject['verJline']
|
||||||
bundle 'org.bouncycastle:bcpkix-jdk15on:1.49'
|
bundle group: 'org.bouncycastle', name: 'bcprov-jdk18on', version: rootProject['verBcpkix']
|
||||||
bundle 'org.fusesource.jansi:jansi:1.17.1'
|
bundle group: 'org.bouncycastle', name: 'bcpkix-jdk18on', version: rootProject['verBcpkix']
|
||||||
bundle 'commons-io:commons-io:2.6'
|
bundle group: 'org.ow2.asm', name: 'asm-commons', version: rootProject['verAsm']
|
||||||
bundle 'org.javassist:javassist:3.23.1-GA'
|
bundle group: 'io.netty', name: 'netty-codec-http', version: rootProject['verNetty']
|
||||||
bundle 'io.netty:netty-all:4.1.29.Final'
|
bundle group: 'io.netty', name: 'netty-transport-classes-epoll', version: rootProject['verNetty']
|
||||||
|
bundle group: 'io.netty', name: 'netty-transport-native-epoll', version: rootProject['verNetty'], classifier: 'linux-x86_64'
|
||||||
|
//bundle group: 'io.netty', name: 'netty-transport-native-epoll', version: rootProject['verNetty'], classifier: 'linux-aarch_64'
|
||||||
|
bundle group: 'io.netty', name: 'netty-transport-classes-io_uring', version: rootProject['verNetty']
|
||||||
|
bundle group: 'io.netty', name: 'netty-transport-native-io_uring', version: rootProject['verNetty'], classifier: 'linux-x86_64'
|
||||||
|
//bundle group: 'io.netty', name: 'netty-transport-native-io_uring', version: rootProject['verNetty'], classifier: 'linux-aarch_64'
|
||||||
|
// Netty
|
||||||
|
bundle 'org.jboss.marshalling:jboss-marshalling:1.4.11.Final'
|
||||||
|
bundle 'com.google.protobuf.nano:protobuf-javanano:3.1.0'
|
||||||
|
//
|
||||||
|
bundle group: 'org.slf4j', name: 'slf4j-api', version: rootProject['verSlf4j']
|
||||||
|
bundle group: 'com.mysql', name: 'mysql-connector-j', version: rootProject['verMySQLConn']
|
||||||
|
bundle group: 'org.mariadb.jdbc', name: 'mariadb-java-client', version: rootProject['verMariaDBConn']
|
||||||
|
bundle group: 'org.postgresql', name: 'postgresql', version: rootProject['verPostgreSQLConn']
|
||||||
|
bundle group: 'com.h2database', name: 'h2', version: rootProject['verH2Conn']
|
||||||
|
proguardPack group: 'com.guardsquare', name: 'proguard-base', version: rootProject['verProguard']
|
||||||
|
bundle group: 'org.apache.logging.log4j', name: 'log4j-core', version: rootProject['verLog4j']
|
||||||
|
bundle group: 'org.apache.logging.log4j', name: 'log4j-slf4j2-impl', version: rootProject['verLog4j']
|
||||||
|
bundle group: 'io.jsonwebtoken', name: 'jjwt-api', version: rootProject['verJwt']
|
||||||
|
bundle group: 'io.jsonwebtoken', name: 'jjwt-impl', version: rootProject['verJwt']
|
||||||
|
bundle group: 'io.jsonwebtoken', name: 'jjwt-gson', version: rootProject['verJwt']
|
||||||
|
bundle group: 'com.google.code.gson', name: 'gson', version: rootProject['verGson']
|
||||||
|
annotationProcessor(group: 'org.apache.logging.log4j', name: 'log4j-core', version: rootProject['verLog4j'])
|
||||||
|
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter', version: rootProject['verJunit']
|
||||||
|
|
||||||
bundle 'org.slf4j:slf4j-simple:1.7.25'
|
bundle 'io.micrometer:micrometer-core:1.14.4'
|
||||||
bundle 'org.slf4j:slf4j-api:1.7.25'
|
bundle('com.zaxxer:HikariCP:6.2.1') {
|
||||||
|
|
||||||
hikari 'io.micrometer:micrometer-core:1.0.6'
|
|
||||||
hikari('hikari-cp:hikari-cp:2.6.0') {
|
|
||||||
exclude group: 'javassist'
|
exclude group: 'javassist'
|
||||||
exclude group: 'io.micrometer'
|
exclude group: 'io.micrometer'
|
||||||
exclude group: 'org.slf4j'
|
exclude group: 'org.slf4j'
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOnly('net.sf.launch4j:launch4j:3.12') { // need user
|
|
||||||
exclude group: '*'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//compile 'org.mozilla:rhino:1.7.10' will be module
|
tasks.register('dumpLibs', Copy) {
|
||||||
}
|
duplicatesStrategy = 'EXCLUDE'
|
||||||
|
|
||||||
task hikari(type: Copy) {
|
|
||||||
into "$buildDir/libs/libraries/hikaricp"
|
|
||||||
from configurations.hikari
|
|
||||||
}
|
|
||||||
|
|
||||||
task dumpLibs(type: Copy) {
|
|
||||||
dependsOn tasks.hikari
|
|
||||||
into "$buildDir/libs/libraries"
|
into "$buildDir/libs/libraries"
|
||||||
from configurations.bundle
|
from configurations.bundleOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
build.dependsOn tasks.dumpLibs
|
tasks.register('dumpProguard', Copy) {
|
||||||
|
duplicatesStrategy = 'EXCLUDE'
|
||||||
|
into "$buildDir/libs/proguard"
|
||||||
|
from configurations.proguardPack
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('bundle', Zip) {
|
||||||
|
duplicatesStrategy = 'EXCLUDE'
|
||||||
|
dependsOn parent.childProjects.Launcher.tasks.build, tasks.dumpLibs, tasks.jar
|
||||||
|
archiveFileName = 'LaunchServer.zip'
|
||||||
|
destinationDirectory = file("$buildDir")
|
||||||
|
from(tasks.dumpLibs.destinationDir) { into 'libraries' }
|
||||||
|
from(tasks.jar)
|
||||||
|
from(parent.childProjects.Launcher.tasks.dumpLibs) { into 'launcher-libraries' }
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register('dumpClientLibs', Copy) {
|
||||||
|
dependsOn parent.childProjects.Launcher.tasks.build
|
||||||
|
into "$buildDir/libs/launcher-libraries"
|
||||||
|
from parent.childProjects.Launcher.tasks.dumpLibs
|
||||||
|
}
|
||||||
|
|
||||||
|
assemble.dependsOn tasks.dumpLibs, tasks.dumpClientLibs, tasks.bundle, tasks.cleanjar, tasks.dumpProguard
|
||||||
|
|
||||||
|
|
||||||
|
publishing {
|
||||||
|
publications {
|
||||||
|
launchserverapi(MavenPublication) {
|
||||||
|
artifactId = 'launchserver-api'
|
||||||
|
artifact(cleanjar) {
|
||||||
|
classifier ""
|
||||||
|
}
|
||||||
|
artifact sourcesJar
|
||||||
|
artifact javadocJar
|
||||||
|
pom {
|
||||||
|
name = 'GravitLauncher LaunchServer API'
|
||||||
|
description = 'GravitLauncher LaunchServer Module API'
|
||||||
|
url = 'https://gravitlauncher.com'
|
||||||
|
licenses {
|
||||||
|
license {
|
||||||
|
name = 'GNU General Public License, Version 3.0'
|
||||||
|
url = 'https://www.gnu.org/licenses/gpl-3.0.html'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
developers {
|
||||||
|
developer {
|
||||||
|
id = 'gravita'
|
||||||
|
name = 'Gravita'
|
||||||
|
email = 'gravita@gravit.pro'
|
||||||
|
}
|
||||||
|
developer {
|
||||||
|
id = 'zaxar163'
|
||||||
|
name = 'Zaxar163'
|
||||||
|
email = 'zahar.vcherachny@yandex.ru'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scm {
|
||||||
|
connection = 'scm:git:https://github.com/GravitLauncher/Launcher.git'
|
||||||
|
developerConnection = 'scm:git:ssh://git@github.com:GravitLauncher/Launcher.git'
|
||||||
|
url = 'https://gravitlauncher.com/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signing {
|
||||||
|
sign publishing.publications.launchserverapi
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
package pro.gravit.launchserver;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import pro.gravit.launcher.base.Launcher;
|
||||||
|
import pro.gravit.launchserver.helper.HttpHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
public class HttpRequester {
|
||||||
|
private transient final HttpClient httpClient = HttpClient.newBuilder().build();
|
||||||
|
|
||||||
|
public HttpRequester() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> SimpleErrorHandler<T> makeEH(Class<T> clazz) {
|
||||||
|
return new SimpleErrorHandler<>(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> SimpleErrorHandler<T> makeEH(Type clazz) {
|
||||||
|
return new SimpleErrorHandler<>(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> HttpRequest get(String url, String token) {
|
||||||
|
try {
|
||||||
|
var requestBuilder = HttpRequest.newBuilder()
|
||||||
|
.method("GET", HttpRequest.BodyPublishers.noBody())
|
||||||
|
.uri(new URI(url))
|
||||||
|
.header("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.timeout(Duration.ofMillis(10000));
|
||||||
|
if (token != null) {
|
||||||
|
requestBuilder.header("Authorization", "Bearer ".concat(token));
|
||||||
|
}
|
||||||
|
return requestBuilder.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> HttpRequest post(String url, T request, String token) {
|
||||||
|
try {
|
||||||
|
var requestBuilder = HttpRequest.newBuilder()
|
||||||
|
.method("POST", HttpRequest.BodyPublishers.ofString(Launcher.gsonManager.gson.toJson(request)))
|
||||||
|
.uri(new URI(url))
|
||||||
|
.header("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.timeout(Duration.ofMillis(10000));
|
||||||
|
if (token != null) {
|
||||||
|
requestBuilder.header("Authorization", "Bearer ".concat(token));
|
||||||
|
}
|
||||||
|
return requestBuilder.build();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> HttpHelper.HttpOptional<T, SimpleError> send(HttpRequest request, Class<T> clazz) throws IOException {
|
||||||
|
return HttpHelper.send(httpClient, request, makeEH(clazz));
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> HttpHelper.HttpOptional<T, SimpleError> send(HttpRequest request, Type type) throws IOException {
|
||||||
|
return HttpHelper.send(httpClient, request, makeEH(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class SimpleErrorHandler<T> implements HttpHelper.HttpJsonErrorHandler<T, SimpleError> {
|
||||||
|
private final Type type;
|
||||||
|
|
||||||
|
private SimpleErrorHandler(Type type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HttpHelper.HttpOptional<T, SimpleError> applyJson(JsonElement response, int statusCode) {
|
||||||
|
if (statusCode < 200 || statusCode >= 300) {
|
||||||
|
return new HttpHelper.HttpOptional<>(null, Launcher.gsonManager.gson.fromJson(response, SimpleError.class), statusCode);
|
||||||
|
}
|
||||||
|
if (type == Void.class) {
|
||||||
|
return new HttpHelper.HttpOptional<>(null, null, statusCode);
|
||||||
|
}
|
||||||
|
return new HttpHelper.HttpOptional<>(Launcher.gsonManager.gson.fromJson(response, type), null, statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SimpleError {
|
||||||
|
public String error;
|
||||||
|
public int code;
|
||||||
|
|
||||||
|
public SimpleError(String error) {
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SimpleError{" +
|
||||||
|
"error='" + error + '\'' +
|
||||||
|
", code=" + code +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,508 @@
|
||||||
|
package pro.gravit.launchserver;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launcher.base.events.RequestEvent;
|
||||||
|
import pro.gravit.launcher.base.events.request.ProfilesRequestEvent;
|
||||||
|
import pro.gravit.launcher.base.modules.events.ClosePhase;
|
||||||
|
import pro.gravit.launcher.base.profiles.ClientProfile;
|
||||||
|
import pro.gravit.launchserver.auth.AuthProviderPair;
|
||||||
|
import pro.gravit.launchserver.auth.core.RejectAuthCoreProvider;
|
||||||
|
import pro.gravit.launchserver.binary.EXELauncherBinary;
|
||||||
|
import pro.gravit.launchserver.binary.JARLauncherBinary;
|
||||||
|
import pro.gravit.launchserver.binary.LauncherBinary;
|
||||||
|
import pro.gravit.launchserver.config.LaunchServerConfig;
|
||||||
|
import pro.gravit.launchserver.config.LaunchServerRuntimeConfig;
|
||||||
|
import pro.gravit.launchserver.helper.SignHelper;
|
||||||
|
import pro.gravit.launchserver.launchermodules.LauncherModuleLoader;
|
||||||
|
import pro.gravit.launchserver.manangers.*;
|
||||||
|
import pro.gravit.launchserver.manangers.hook.AuthHookManager;
|
||||||
|
import pro.gravit.launchserver.modules.events.*;
|
||||||
|
import pro.gravit.launchserver.modules.impl.LaunchServerModulesManager;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.SocketCommandServer;
|
||||||
|
import pro.gravit.launchserver.socket.handlers.NettyServerSocketHandler;
|
||||||
|
import pro.gravit.launchserver.socket.response.auth.RestoreResponse;
|
||||||
|
import pro.gravit.utils.command.Command;
|
||||||
|
import pro.gravit.utils.command.CommandHandler;
|
||||||
|
import pro.gravit.utils.command.SubCommand;
|
||||||
|
import pro.gravit.utils.helper.CommonHelper;
|
||||||
|
import pro.gravit.utils.helper.JVMHelper;
|
||||||
|
import pro.gravit.utils.helper.SecurityHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main LaunchServer class. Contains links to all necessary objects
|
||||||
|
* Not a singletron
|
||||||
|
*/
|
||||||
|
public final class LaunchServer implements Runnable, AutoCloseable, Reconfigurable {
|
||||||
|
/**
|
||||||
|
* Working folder path
|
||||||
|
*/
|
||||||
|
public final Path dir;
|
||||||
|
/**
|
||||||
|
* Environment type (test / production)
|
||||||
|
*/
|
||||||
|
public final LaunchServerEnv env;
|
||||||
|
/**
|
||||||
|
* The path to the folder with libraries for the launcher
|
||||||
|
*/
|
||||||
|
public final Path launcherLibraries;
|
||||||
|
/**
|
||||||
|
* The path to the folder with compile-only libraries for the launcher
|
||||||
|
*/
|
||||||
|
public final Path launcherLibrariesCompile;
|
||||||
|
public final Path launcherPack;
|
||||||
|
/**
|
||||||
|
* The path to the folder with updates/webroot
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
|
public final Path updatesDir;
|
||||||
|
|
||||||
|
// Constant paths
|
||||||
|
/**
|
||||||
|
* Save/Reload LaunchServer config
|
||||||
|
*/
|
||||||
|
public final LaunchServerConfigManager launchServerConfigManager;
|
||||||
|
/**
|
||||||
|
* The path to the folder with profiles
|
||||||
|
*/
|
||||||
|
public final Path tmpDir;
|
||||||
|
public final Path modulesDir;
|
||||||
|
public final Path launcherModulesDir;
|
||||||
|
public final Path librariesDir;
|
||||||
|
public final Path controlFile;
|
||||||
|
public final Path proguardDir;
|
||||||
|
/**
|
||||||
|
* This object contains runtime configuration
|
||||||
|
*/
|
||||||
|
public final LaunchServerRuntimeConfig runtime;
|
||||||
|
/**
|
||||||
|
* Pipeline for building JAR
|
||||||
|
*/
|
||||||
|
public final JARLauncherBinary launcherBinary;
|
||||||
|
/**
|
||||||
|
* Pipeline for building EXE
|
||||||
|
*/
|
||||||
|
public final LauncherBinary launcherEXEBinary;
|
||||||
|
// Server config
|
||||||
|
public final AuthHookManager authHookManager;
|
||||||
|
public final LaunchServerModulesManager modulesManager;
|
||||||
|
// Launcher binary
|
||||||
|
public final MirrorManager mirrorManager;
|
||||||
|
public final AuthManager authManager;
|
||||||
|
public final ReconfigurableManager reconfigurableManager;
|
||||||
|
public final ConfigManager configManager;
|
||||||
|
public final FeaturesManager featuresManager;
|
||||||
|
public final KeyAgreementManager keyAgreementManager;
|
||||||
|
public final UpdatesManager updatesManager;
|
||||||
|
// HWID ban + anti-brutforce
|
||||||
|
public final CertificateManager certificateManager;
|
||||||
|
// Server
|
||||||
|
public final CommandHandler commandHandler;
|
||||||
|
public final NettyServerSocketHandler nettyServerSocketHandler;
|
||||||
|
public final SocketCommandServer socketCommandServer;
|
||||||
|
public final ScheduledExecutorService service;
|
||||||
|
public final AtomicBoolean started = new AtomicBoolean(false);
|
||||||
|
public final LauncherModuleLoader launcherModuleLoader;
|
||||||
|
private final Logger logger = LogManager.getLogger();
|
||||||
|
public final int shardId;
|
||||||
|
public LaunchServerConfig config;
|
||||||
|
|
||||||
|
public LaunchServer(LaunchServerDirectories directories, LaunchServerEnv env, LaunchServerConfig config, LaunchServerRuntimeConfig runtimeConfig, LaunchServerConfigManager launchServerConfigManager, LaunchServerModulesManager modulesManager, KeyAgreementManager keyAgreementManager, CommandHandler commandHandler, CertificateManager certificateManager, int shardId) throws IOException {
|
||||||
|
this.dir = directories.dir;
|
||||||
|
this.tmpDir = directories.tmpDir;
|
||||||
|
this.env = env;
|
||||||
|
this.config = config;
|
||||||
|
this.launchServerConfigManager = launchServerConfigManager;
|
||||||
|
this.modulesManager = modulesManager;
|
||||||
|
this.updatesDir = directories.updatesDir;
|
||||||
|
this.keyAgreementManager = keyAgreementManager;
|
||||||
|
this.commandHandler = commandHandler;
|
||||||
|
this.runtime = runtimeConfig;
|
||||||
|
this.certificateManager = certificateManager;
|
||||||
|
this.service = Executors.newScheduledThreadPool(config.netty.performance.schedulerThread);
|
||||||
|
launcherLibraries = directories.launcherLibrariesDir;
|
||||||
|
launcherLibrariesCompile = directories.launcherLibrariesCompileDir;
|
||||||
|
launcherPack = directories.launcherPackDir;
|
||||||
|
modulesDir = directories.modules;
|
||||||
|
launcherModulesDir = directories.launcherModules;
|
||||||
|
librariesDir = directories.librariesDir;
|
||||||
|
controlFile = directories.controlFile;
|
||||||
|
proguardDir = directories.proguardDir;
|
||||||
|
this.shardId = shardId;
|
||||||
|
if(!Files.isDirectory(launcherPack)) {
|
||||||
|
Files.createDirectories(launcherPack);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.setLaunchServer(this);
|
||||||
|
|
||||||
|
modulesManager.invokeEvent(new NewLaunchServerInstanceEvent(this));
|
||||||
|
|
||||||
|
// Print keypair fingerprints
|
||||||
|
|
||||||
|
runtime.verify();
|
||||||
|
config.verify();
|
||||||
|
|
||||||
|
// build hooks, anti-brutforce and other
|
||||||
|
mirrorManager = new MirrorManager();
|
||||||
|
reconfigurableManager = new ReconfigurableManager();
|
||||||
|
authHookManager = new AuthHookManager();
|
||||||
|
configManager = new ConfigManager();
|
||||||
|
featuresManager = new FeaturesManager(this);
|
||||||
|
authManager = new AuthManager(this);
|
||||||
|
updatesManager = new UpdatesManager(this);
|
||||||
|
RestoreResponse.registerProviders(this);
|
||||||
|
|
||||||
|
config.init(ReloadType.FULL);
|
||||||
|
registerObject("launchServer", this);
|
||||||
|
|
||||||
|
pro.gravit.launchserver.command.handler.CommandHandler.registerCommands(commandHandler, this);
|
||||||
|
|
||||||
|
// init modules
|
||||||
|
modulesManager.invokeEvent(new LaunchServerInitPhase(this));
|
||||||
|
|
||||||
|
// Set launcher EXE binary
|
||||||
|
launcherBinary = new JARLauncherBinary(this);
|
||||||
|
launcherEXEBinary = binary();
|
||||||
|
|
||||||
|
launcherBinary.init();
|
||||||
|
launcherEXEBinary.init();
|
||||||
|
syncLauncherBinaries();
|
||||||
|
launcherModuleLoader = new LauncherModuleLoader(this);
|
||||||
|
if (config.components != null) {
|
||||||
|
logger.debug("Init components");
|
||||||
|
config.components.forEach((k, v) -> {
|
||||||
|
logger.debug("Init component {}", k);
|
||||||
|
v.setComponentName(k);
|
||||||
|
v.init(this);
|
||||||
|
});
|
||||||
|
logger.debug("Init components successful");
|
||||||
|
}
|
||||||
|
launcherModuleLoader.init();
|
||||||
|
nettyServerSocketHandler = new NettyServerSocketHandler(this);
|
||||||
|
socketCommandServer = new SocketCommandServer(commandHandler, controlFile);
|
||||||
|
if(config.sign.checkCertificateExpired) {
|
||||||
|
checkCertificateExpired();
|
||||||
|
service.scheduleAtFixedRate(this::checkCertificateExpired, 24, 24, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
// post init modules
|
||||||
|
modulesManager.invokeEvent(new LaunchServerPostInitPhase(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reload(ReloadType type) throws Exception {
|
||||||
|
config.close(type);
|
||||||
|
Map<String, AuthProviderPair> pairs = null;
|
||||||
|
if (type.equals(ReloadType.NO_AUTH)) {
|
||||||
|
pairs = config.auth;
|
||||||
|
}
|
||||||
|
logger.info("Reading LaunchServer config file");
|
||||||
|
config = launchServerConfigManager.readConfig();
|
||||||
|
config.setLaunchServer(this);
|
||||||
|
if (type.equals(ReloadType.NO_AUTH)) {
|
||||||
|
config.auth = pairs;
|
||||||
|
}
|
||||||
|
config.verify();
|
||||||
|
config.init(type);
|
||||||
|
if (type.equals(ReloadType.FULL) && config.components != null) {
|
||||||
|
logger.debug("Init components");
|
||||||
|
config.components.forEach((k, v) -> {
|
||||||
|
logger.debug("Init component {}", k);
|
||||||
|
v.setComponentName(k);
|
||||||
|
v.init(this);
|
||||||
|
});
|
||||||
|
logger.debug("Init components successful");
|
||||||
|
}
|
||||||
|
if(!type.equals(ReloadType.NO_AUTH)) {
|
||||||
|
nettyServerSocketHandler.nettyServer.service.forEachActiveChannels((channel, wsHandler) -> {
|
||||||
|
Client client = wsHandler.getClient();
|
||||||
|
if(client.auth != null) {
|
||||||
|
client.auth = config.getAuthProviderPair(client.auth_id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Command> getCommands() {
|
||||||
|
Map<String, Command> commands = new HashMap<>();
|
||||||
|
SubCommand reload = new SubCommand("[type]", "reload launchserver config") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
if (args.length == 0) {
|
||||||
|
reload(ReloadType.FULL);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (args[0]) {
|
||||||
|
case "full" -> reload(ReloadType.FULL);
|
||||||
|
case "no_components" -> reload(ReloadType.NO_COMPONENTS);
|
||||||
|
default -> reload(ReloadType.NO_AUTH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
commands.put("reload", reload);
|
||||||
|
SubCommand save = new SubCommand("[]", "save launchserver config") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
launchServerConfigManager.writeConfig(config);
|
||||||
|
launchServerConfigManager.writeRuntimeConfig(runtime);
|
||||||
|
logger.info("LaunchServerConfig saved");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
commands.put("save", save);
|
||||||
|
LaunchServer instance = this;
|
||||||
|
SubCommand resetauth = new SubCommand("authId", "reset auth by id") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
verifyArgs(args, 1);
|
||||||
|
AuthProviderPair pair = config.getAuthProviderPair(args[0]);
|
||||||
|
if (pair == null) {
|
||||||
|
logger.error("Pair not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pair.core.close();
|
||||||
|
pair.core = new RejectAuthCoreProvider();
|
||||||
|
pair.core.init(instance, pair);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
commands.put("resetauth", resetauth);
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkCertificateExpired() {
|
||||||
|
if(!config.sign.enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
KeyStore keyStore = SignHelper.getStore(Paths.get(config.sign.keyStore), config.sign.keyStorePass, config.sign.keyStoreType);
|
||||||
|
Instant date = SignHelper.getCertificateExpired(keyStore, config.sign.keyAlias);
|
||||||
|
if(date == null) {
|
||||||
|
logger.debug("The certificate will expire at unlimited");
|
||||||
|
} else if(date.minus(Duration.ofDays(30)).isBefore(Instant.now())) {
|
||||||
|
logger.warn("The certificate will expire at {}", date.toString());
|
||||||
|
} else {
|
||||||
|
logger.debug("The certificate will expire at {}", date.toString());
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.error("Can't get certificate expire date", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LauncherBinary binary() {
|
||||||
|
LaunchServerLauncherExeInit event = new LaunchServerLauncherExeInit(this, null);
|
||||||
|
modulesManager.invokeEvent(event);
|
||||||
|
if(event.binary != null) {
|
||||||
|
return event.binary;
|
||||||
|
}
|
||||||
|
return new EXELauncherBinary(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void buildLauncherBinaries() throws IOException {
|
||||||
|
launcherBinary.build();
|
||||||
|
launcherEXEBinary.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws Exception {
|
||||||
|
service.shutdownNow();
|
||||||
|
logger.info("Close server socket");
|
||||||
|
nettyServerSocketHandler.close();
|
||||||
|
// Close handlers & providers
|
||||||
|
config.close(ReloadType.FULL);
|
||||||
|
modulesManager.invokeEvent(new ClosePhase());
|
||||||
|
logger.info("Save LaunchServer runtime config");
|
||||||
|
launchServerConfigManager.writeRuntimeConfig(runtime);
|
||||||
|
// Print last message before death :(
|
||||||
|
logger.info("LaunchServer stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
public Set<ClientProfile> getProfiles() {
|
||||||
|
return config.profileProvider.getProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
public void setProfiles(Set<ClientProfile> profilesList) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rebindNettyServerSocket() {
|
||||||
|
nettyServerSocketHandler.close();
|
||||||
|
CommonHelper.newThread("Netty Server Socket Thread", false, nettyServerSocketHandler).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (started.getAndSet(true))
|
||||||
|
throw new IllegalStateException("LaunchServer has been already started");
|
||||||
|
|
||||||
|
// Add shutdown hook, then start LaunchServer
|
||||||
|
if (!this.env.equals(LaunchServerEnv.TEST)) {
|
||||||
|
JVMHelper.RUNTIME.addShutdownHook(CommonHelper.newThread(null, false, () -> {
|
||||||
|
try {
|
||||||
|
close();
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("LaunchServer close error", e);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
CommonHelper.newThread("Command Thread", true, commandHandler).start();
|
||||||
|
CommonHelper.newThread("Socket Command Thread", true, socketCommandServer).start();
|
||||||
|
// Sync updates dir
|
||||||
|
CommonHelper.newThread("Profiles and updates sync", true, () -> {
|
||||||
|
try {
|
||||||
|
// Sync profiles dir
|
||||||
|
syncProfilesDir();
|
||||||
|
|
||||||
|
// Sync updates dir
|
||||||
|
config.updatesProvider.syncInitially();
|
||||||
|
|
||||||
|
|
||||||
|
modulesManager.invokeEvent(new LaunchServerProfilesSyncEvent(this));
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Updates/Profiles not synced", e);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
if (config.netty != null)
|
||||||
|
rebindNettyServerSocket();
|
||||||
|
try {
|
||||||
|
modulesManager.fullInitializedLaunchServer(this);
|
||||||
|
modulesManager.invokeEvent(new LaunchServerFullInitEvent(this));
|
||||||
|
logger.info("LaunchServer started");
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.error("LaunchServer startup failed", e);
|
||||||
|
JVMHelper.RUNTIME.exit(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncLauncherBinaries() throws IOException {
|
||||||
|
logger.info("Syncing launcher binaries");
|
||||||
|
|
||||||
|
// Syncing launcher binary
|
||||||
|
logger.info("Syncing launcher binary file");
|
||||||
|
if (!launcherBinary.sync()) logger.warn("Missing launcher binary file");
|
||||||
|
|
||||||
|
// Syncing launcher EXE binary
|
||||||
|
logger.info("Syncing launcher EXE binary file");
|
||||||
|
if (!launcherEXEBinary.sync())
|
||||||
|
logger.warn("Missing launcher EXE binary file");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncProfilesDir() throws IOException {
|
||||||
|
logger.info("Syncing profiles dir");
|
||||||
|
config.profileProvider.sync();
|
||||||
|
if (config.netty.sendProfileUpdatesEvent) {
|
||||||
|
sendUpdateProfilesEvent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendUpdateProfilesEvent() {
|
||||||
|
if (nettyServerSocketHandler == null || nettyServerSocketHandler.nettyServer == null || nettyServerSocketHandler.nettyServer.service == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nettyServerSocketHandler.nettyServer.service.forEachActiveChannels((ch, handler) -> {
|
||||||
|
Client client = handler.getClient();
|
||||||
|
if (client == null || !client.isAuth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ProfilesRequestEvent event = new ProfilesRequestEvent(config.profileProvider.getProfiles(client));
|
||||||
|
event.requestUUID = RequestEvent.eventUUID;
|
||||||
|
handler.service.sendObject(ch, event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void syncUpdatesDir(Collection<String> dirs) throws IOException {
|
||||||
|
updatesManager.syncUpdatesDir(dirs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerObject(String name, Object object) {
|
||||||
|
if (object instanceof Reconfigurable) {
|
||||||
|
reconfigurableManager.registerReconfigurable(name, (Reconfigurable) object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregisterObject(String name, Object object) {
|
||||||
|
if (object instanceof Reconfigurable) {
|
||||||
|
reconfigurableManager.unregisterReconfigurable(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public enum ReloadType {
|
||||||
|
NO_AUTH,
|
||||||
|
NO_COMPONENTS,
|
||||||
|
FULL
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LaunchServerEnv {
|
||||||
|
TEST,
|
||||||
|
DEV,
|
||||||
|
DEBUG,
|
||||||
|
PRODUCTION
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface LaunchServerConfigManager {
|
||||||
|
LaunchServerConfig readConfig() throws IOException;
|
||||||
|
|
||||||
|
LaunchServerRuntimeConfig readRuntimeConfig() throws IOException;
|
||||||
|
|
||||||
|
void writeConfig(LaunchServerConfig config) throws IOException;
|
||||||
|
|
||||||
|
void writeRuntimeConfig(LaunchServerRuntimeConfig config) throws IOException;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LaunchServerDirectories {
|
||||||
|
public static final String UPDATES_NAME = "updates",
|
||||||
|
TRUSTSTORE_NAME = "truststore", LAUNCHERLIBRARIES_NAME = "launcher-libraries",
|
||||||
|
LAUNCHERLIBRARIESCOMPILE_NAME = "launcher-libraries-compile", LAUNCHERPACK_NAME = "launcher-pack",
|
||||||
|
KEY_NAME = ".keys", MODULES = "modules", LAUNCHER_MODULES = "launcher-modules",
|
||||||
|
LIBRARIES = "libraries", CONTROL_FILE = "control-file", PROGUARD_DIR = "proguard-libraries";
|
||||||
|
public Path updatesDir;
|
||||||
|
public Path librariesDir;
|
||||||
|
public Path launcherLibrariesDir;
|
||||||
|
public Path launcherLibrariesCompileDir;
|
||||||
|
public Path launcherPackDir;
|
||||||
|
public Path keyDirectory;
|
||||||
|
public Path proguardDir;
|
||||||
|
public Path dir;
|
||||||
|
public Path trustStore;
|
||||||
|
public Path tmpDir;
|
||||||
|
public Path modules;
|
||||||
|
public Path launcherModules;
|
||||||
|
public Path controlFile;
|
||||||
|
|
||||||
|
public void collect() {
|
||||||
|
if (updatesDir == null) updatesDir = getPath(UPDATES_NAME);
|
||||||
|
if (trustStore == null) trustStore = getPath(TRUSTSTORE_NAME);
|
||||||
|
if (launcherLibrariesDir == null) launcherLibrariesDir = getPath(LAUNCHERLIBRARIES_NAME);
|
||||||
|
if (launcherLibrariesCompileDir == null)
|
||||||
|
launcherLibrariesCompileDir = getPath(LAUNCHERLIBRARIESCOMPILE_NAME);
|
||||||
|
if (launcherPackDir == null)
|
||||||
|
launcherPackDir = getPath(LAUNCHERPACK_NAME);
|
||||||
|
if (keyDirectory == null) keyDirectory = getPath(KEY_NAME);
|
||||||
|
if (modules == null) modules = getPath(MODULES);
|
||||||
|
if (launcherModules == null) launcherModules = getPath(LAUNCHER_MODULES);
|
||||||
|
if (librariesDir == null) librariesDir = getPath(LIBRARIES);
|
||||||
|
if (controlFile == null) controlFile = getPath(CONTROL_FILE);
|
||||||
|
if (proguardDir == null) proguardDir = getPath(PROGUARD_DIR);
|
||||||
|
if (tmpDir == null)
|
||||||
|
tmpDir = Paths.get(System.getProperty("java.io.tmpdir")).resolve("launchserver-%s".formatted(SecurityHelper.randomStringToken()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path getPath(String dirName) {
|
||||||
|
String property = System.getProperty("launchserver.dir." + dirName, null);
|
||||||
|
if (property == null) return dir.resolve(dirName);
|
||||||
|
else return Paths.get(property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package pro.gravit.launchserver;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.config.LaunchServerConfig;
|
||||||
|
import pro.gravit.launchserver.config.LaunchServerRuntimeConfig;
|
||||||
|
import pro.gravit.launchserver.manangers.CertificateManager;
|
||||||
|
import pro.gravit.launchserver.manangers.KeyAgreementManager;
|
||||||
|
import pro.gravit.launchserver.modules.impl.LaunchServerModulesManager;
|
||||||
|
import pro.gravit.utils.command.CommandHandler;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public class LaunchServerBuilder {
|
||||||
|
private LaunchServerConfig config;
|
||||||
|
private LaunchServerRuntimeConfig runtimeConfig;
|
||||||
|
private CommandHandler commandHandler;
|
||||||
|
private LaunchServer.LaunchServerEnv env;
|
||||||
|
private LaunchServerModulesManager modulesManager;
|
||||||
|
private LaunchServer.LaunchServerDirectories directories = new LaunchServer.LaunchServerDirectories();
|
||||||
|
private KeyAgreementManager keyAgreementManager;
|
||||||
|
private CertificateManager certificateManager;
|
||||||
|
private LaunchServer.LaunchServerConfigManager launchServerConfigManager;
|
||||||
|
private Integer shardId;
|
||||||
|
|
||||||
|
public LaunchServerBuilder setConfig(LaunchServerConfig config) {
|
||||||
|
this.config = config;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LaunchServerBuilder setEnv(LaunchServer.LaunchServerEnv env) {
|
||||||
|
this.env = env;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LaunchServerBuilder setModulesManager(LaunchServerModulesManager modulesManager) {
|
||||||
|
this.modulesManager = modulesManager;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LaunchServerBuilder setRuntimeConfig(LaunchServerRuntimeConfig runtimeConfig) {
|
||||||
|
this.runtimeConfig = runtimeConfig;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LaunchServerBuilder setCommandHandler(CommandHandler commandHandler) {
|
||||||
|
this.commandHandler = commandHandler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LaunchServerBuilder setDirectories(LaunchServer.LaunchServerDirectories directories) {
|
||||||
|
this.directories = directories;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LaunchServerBuilder setDir(Path dir) {
|
||||||
|
this.directories.dir = dir;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LaunchServerBuilder setShardId(Integer shardId) {
|
||||||
|
this.shardId = shardId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LaunchServerBuilder setLaunchServerConfigManager(LaunchServer.LaunchServerConfigManager launchServerConfigManager) {
|
||||||
|
this.launchServerConfigManager = launchServerConfigManager;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LaunchServer build() throws Exception {
|
||||||
|
directories.collect();
|
||||||
|
if (launchServerConfigManager == null) {
|
||||||
|
launchServerConfigManager = new NullLaunchServerConfigManager();
|
||||||
|
}
|
||||||
|
if (keyAgreementManager == null) {
|
||||||
|
keyAgreementManager = new KeyAgreementManager(directories.keyDirectory);
|
||||||
|
}
|
||||||
|
if(shardId == null) {
|
||||||
|
shardId = Integer.parseInt(System.getProperty("launchserver.shardId", "0"));
|
||||||
|
}
|
||||||
|
return new LaunchServer(directories, env, config, runtimeConfig, launchServerConfigManager, modulesManager, keyAgreementManager, commandHandler, certificateManager, shardId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LaunchServerBuilder setCertificateManager(CertificateManager certificateManager) {
|
||||||
|
this.certificateManager = certificateManager;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeyAgreementManager(KeyAgreementManager keyAgreementManager) {
|
||||||
|
this.keyAgreementManager = keyAgreementManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NullLaunchServerConfigManager implements LaunchServer.LaunchServerConfigManager {
|
||||||
|
@Override
|
||||||
|
public LaunchServerConfig readConfig() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LaunchServerRuntimeConfig readRuntimeConfig() {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeConfig(LaunchServerConfig config) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeRuntimeConfig(LaunchServerRuntimeConfig config) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,330 @@
|
||||||
|
package pro.gravit.launchserver;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import pro.gravit.launcher.base.Launcher;
|
||||||
|
import pro.gravit.launcher.core.LauncherTrustManager;
|
||||||
|
import pro.gravit.launcher.base.modules.events.PreConfigPhase;
|
||||||
|
import pro.gravit.launcher.base.profiles.optional.actions.OptionalAction;
|
||||||
|
import pro.gravit.launcher.base.profiles.optional.triggers.OptionalTrigger;
|
||||||
|
import pro.gravit.launcher.base.request.auth.AuthRequest;
|
||||||
|
import pro.gravit.launcher.base.request.auth.GetAvailabilityAuthRequest;
|
||||||
|
import pro.gravit.launchserver.auth.core.AuthCoreProvider;
|
||||||
|
import pro.gravit.launchserver.auth.mix.MixProvider;
|
||||||
|
import pro.gravit.launchserver.auth.password.PasswordVerifier;
|
||||||
|
import pro.gravit.launchserver.auth.profiles.ProfileProvider;
|
||||||
|
import pro.gravit.launchserver.auth.protect.ProtectHandler;
|
||||||
|
import pro.gravit.launchserver.auth.texture.TextureProvider;
|
||||||
|
import pro.gravit.launchserver.auth.updates.UpdatesProvider;
|
||||||
|
import pro.gravit.launchserver.components.Component;
|
||||||
|
import pro.gravit.launchserver.config.LaunchServerConfig;
|
||||||
|
import pro.gravit.launchserver.config.LaunchServerRuntimeConfig;
|
||||||
|
import pro.gravit.launchserver.manangers.CertificateManager;
|
||||||
|
import pro.gravit.launchserver.manangers.LaunchServerGsonManager;
|
||||||
|
import pro.gravit.launchserver.modules.impl.LaunchServerModulesManager;
|
||||||
|
import pro.gravit.launchserver.socket.WebSocketService;
|
||||||
|
import pro.gravit.utils.command.CommandHandler;
|
||||||
|
import pro.gravit.utils.command.JLineCommandHandler;
|
||||||
|
import pro.gravit.utils.command.StdCommandHandler;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
import pro.gravit.utils.helper.JVMHelper;
|
||||||
|
import pro.gravit.utils.helper.LogHelper;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class LaunchServerStarter {
|
||||||
|
public static final boolean allowUnsigned = Boolean.getBoolean("launchserver.allowUnsigned");
|
||||||
|
public static final boolean prepareMode = Boolean.getBoolean("launchserver.prepareMode");
|
||||||
|
private static final Logger logger = LogManager.getLogger();
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
JVMHelper.verifySystemProperties(LaunchServer.class, false);
|
||||||
|
//LogHelper.addOutput(IOHelper.WORKING_DIR.resolve("LaunchServer.log"));
|
||||||
|
LogHelper.printVersion("LaunchServer");
|
||||||
|
LogHelper.printLicense("LaunchServer");
|
||||||
|
Path dir = IOHelper.WORKING_DIR;
|
||||||
|
Path configFile, runtimeConfigFile;
|
||||||
|
try {
|
||||||
|
Class.forName("org.bouncycastle.jce.provider.BouncyCastleProvider");
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
} catch (ClassNotFoundException | NoClassDefFoundError ex) {
|
||||||
|
LogHelper.error("Library BouncyCastle not found! Is directory 'libraries' empty?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LaunchServer.LaunchServerDirectories directories = new LaunchServer.LaunchServerDirectories();
|
||||||
|
directories.dir = dir;
|
||||||
|
directories.collect();
|
||||||
|
CertificateManager certificateManager = new CertificateManager();
|
||||||
|
try {
|
||||||
|
certificateManager.readTrustStore(directories.trustStore);
|
||||||
|
} catch (CertificateException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
LauncherTrustManager.CheckClassResult result = certificateManager.checkClass(LaunchServer.class);
|
||||||
|
if (result.type == LauncherTrustManager.CheckClassResultType.SUCCESS) {
|
||||||
|
logger.info("LaunchServer signed by {}", result.endCertificate.getSubjectX500Principal().getName());
|
||||||
|
} else if (result.type == LauncherTrustManager.CheckClassResultType.NOT_SIGNED) {
|
||||||
|
// None
|
||||||
|
} else {
|
||||||
|
if (result.exception != null) {
|
||||||
|
logger.error(result.exception);
|
||||||
|
}
|
||||||
|
logger.warn("LaunchServer signed incorrectly. Status: {}", result.type.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchServerRuntimeConfig runtimeConfig;
|
||||||
|
LaunchServerConfig config;
|
||||||
|
LaunchServer.LaunchServerEnv env = LaunchServer.LaunchServerEnv.PRODUCTION;
|
||||||
|
LaunchServerModulesManager modulesManager = new LaunchServerModulesManager(directories.modules, dir.resolve("config"), certificateManager.trustManager);
|
||||||
|
modulesManager.autoload();
|
||||||
|
modulesManager.initModules(null);
|
||||||
|
registerAll();
|
||||||
|
initGson(modulesManager);
|
||||||
|
printExperimentalBranch();
|
||||||
|
if (IOHelper.exists(dir.resolve("LaunchServer.conf"))) {
|
||||||
|
configFile = dir.resolve("LaunchServer.conf");
|
||||||
|
} else {
|
||||||
|
configFile = dir.resolve("LaunchServer.json");
|
||||||
|
}
|
||||||
|
if (IOHelper.exists(dir.resolve("RuntimeLaunchServer.conf"))) {
|
||||||
|
runtimeConfigFile = dir.resolve("RuntimeLaunchServer.conf");
|
||||||
|
} else {
|
||||||
|
runtimeConfigFile = dir.resolve("RuntimeLaunchServer.json");
|
||||||
|
}
|
||||||
|
CommandHandler localCommandHandler;
|
||||||
|
try {
|
||||||
|
Class.forName("org.jline.terminal.Terminal");
|
||||||
|
|
||||||
|
// JLine2 available
|
||||||
|
localCommandHandler = new JLineCommandHandler();
|
||||||
|
logger.info("JLine2 terminal enabled");
|
||||||
|
} catch (ClassNotFoundException ignored) {
|
||||||
|
localCommandHandler = new StdCommandHandler(true);
|
||||||
|
logger.warn("JLine2 isn't in classpath, using std");
|
||||||
|
}
|
||||||
|
modulesManager.invokeEvent(new PreConfigPhase());
|
||||||
|
generateConfigIfNotExists(configFile, localCommandHandler, env);
|
||||||
|
logger.info("Reading LaunchServer config file");
|
||||||
|
try (BufferedReader reader = IOHelper.newReader(configFile)) {
|
||||||
|
config = Launcher.gsonManager.gson.fromJson(reader, LaunchServerConfig.class);
|
||||||
|
}
|
||||||
|
if (!Files.exists(runtimeConfigFile)) {
|
||||||
|
logger.info("Reset LaunchServer runtime config file");
|
||||||
|
runtimeConfig = new LaunchServerRuntimeConfig();
|
||||||
|
runtimeConfig.reset();
|
||||||
|
} else {
|
||||||
|
logger.info("Reading LaunchServer runtime config file");
|
||||||
|
try (BufferedReader reader = IOHelper.newReader(runtimeConfigFile)) {
|
||||||
|
runtimeConfig = Launcher.gsonManager.gson.fromJson(reader, LaunchServerRuntimeConfig.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchServer.LaunchServerConfigManager launchServerConfigManager = new BasicLaunchServerConfigManager(configFile, runtimeConfigFile);
|
||||||
|
LaunchServer server = new LaunchServerBuilder()
|
||||||
|
.setDirectories(directories)
|
||||||
|
.setEnv(env)
|
||||||
|
.setCommandHandler(localCommandHandler)
|
||||||
|
.setRuntimeConfig(runtimeConfig)
|
||||||
|
.setConfig(config)
|
||||||
|
.setModulesManager(modulesManager)
|
||||||
|
.setLaunchServerConfigManager(launchServerConfigManager)
|
||||||
|
.setCertificateManager(certificateManager)
|
||||||
|
.build();
|
||||||
|
List<String> allArgs = List.of(args);
|
||||||
|
boolean isPrepareMode = prepareMode || allArgs.contains("--prepare");
|
||||||
|
boolean isRunCommand = false;
|
||||||
|
String runCommand = null;
|
||||||
|
for(var e : allArgs) {
|
||||||
|
if(e.equals("--run")) {
|
||||||
|
isRunCommand = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(isRunCommand) {
|
||||||
|
runCommand = e;
|
||||||
|
isRunCommand = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(runCommand != null) {
|
||||||
|
localCommandHandler.eval(runCommand, false);
|
||||||
|
}
|
||||||
|
if (!isPrepareMode) {
|
||||||
|
server.run();
|
||||||
|
} else {
|
||||||
|
server.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void initGson(LaunchServerModulesManager modulesManager) {
|
||||||
|
Launcher.gsonManager = new LaunchServerGsonManager(modulesManager);
|
||||||
|
Launcher.gsonManager.initGson();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void registerAll() {
|
||||||
|
AuthCoreProvider.registerProviders();
|
||||||
|
PasswordVerifier.registerProviders();
|
||||||
|
TextureProvider.registerProviders();
|
||||||
|
Component.registerComponents();
|
||||||
|
ProtectHandler.registerHandlers();
|
||||||
|
WebSocketService.registerResponses();
|
||||||
|
AuthRequest.registerProviders();
|
||||||
|
GetAvailabilityAuthRequest.registerProviders();
|
||||||
|
OptionalAction.registerProviders();
|
||||||
|
OptionalTrigger.registerProviders();
|
||||||
|
MixProvider.registerProviders();
|
||||||
|
ProfileProvider.registerProviders();
|
||||||
|
UpdatesProvider.registerProviders();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void printExperimentalBranch() {
|
||||||
|
try(Reader reader = IOHelper.newReader(IOHelper.getResourceURL("experimental-build.json"))) {
|
||||||
|
ExperimentalBuild info = Launcher.gsonManager.configGson.fromJson(reader, ExperimentalBuild.class);
|
||||||
|
if(info.features == null || info.features.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.warn("This is experimental build. Please do not use this in production");
|
||||||
|
logger.warn("Experimental features: [{}]", String.join(",", info.features));
|
||||||
|
for(var e : info.info) {
|
||||||
|
logger.warn(e);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.warn("Build information not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record ExperimentalBuild(List<String> features, List<String> info) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void generateConfigIfNotExists(Path configFile, CommandHandler commandHandler, LaunchServer.LaunchServerEnv env) throws IOException {
|
||||||
|
if (IOHelper.isFile(configFile))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Create new config
|
||||||
|
logger.info("Creating LaunchServer config");
|
||||||
|
|
||||||
|
|
||||||
|
LaunchServerConfig newConfig = LaunchServerConfig.getDefault(env);
|
||||||
|
// Set server address
|
||||||
|
String address;
|
||||||
|
if (env.equals(LaunchServer.LaunchServerEnv.TEST)) {
|
||||||
|
address = "localhost";
|
||||||
|
newConfig.setProjectName("test");
|
||||||
|
} else {
|
||||||
|
address = System.getenv("ADDRESS");
|
||||||
|
if (address == null) {
|
||||||
|
address = System.getProperty("launchserver.address", null);
|
||||||
|
}
|
||||||
|
if (address == null) {
|
||||||
|
System.out.println("External launchServer address:port (default: localhost:9274): ");
|
||||||
|
address = commandHandler.readLine();
|
||||||
|
}
|
||||||
|
String projectName = System.getenv("PROJECTNAME");
|
||||||
|
if (projectName == null) {
|
||||||
|
projectName = System.getProperty("launchserver.projectname", null);
|
||||||
|
}
|
||||||
|
if (projectName == null) {
|
||||||
|
System.out.println("LaunchServer projectName: ");
|
||||||
|
projectName = commandHandler.readLine();
|
||||||
|
}
|
||||||
|
newConfig.setProjectName(projectName);
|
||||||
|
}
|
||||||
|
if (address == null || address.isEmpty()) {
|
||||||
|
logger.error("Address null. Using localhost:9274");
|
||||||
|
address = "localhost:9274";
|
||||||
|
}
|
||||||
|
if (newConfig.projectName == null || newConfig.projectName.isEmpty()) {
|
||||||
|
logger.error("ProjectName null. Using MineCraft");
|
||||||
|
newConfig.projectName = "MineCraft";
|
||||||
|
}
|
||||||
|
int port = 9274;
|
||||||
|
if(address.contains(":")) {
|
||||||
|
String portString = address.substring(address.indexOf(':')+1);
|
||||||
|
try {
|
||||||
|
port = Integer.parseInt(portString);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
logger.warn("Unknown port {}, using 9274", portString);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info("Address {} doesn't contains port (you want to use nginx?)", address);
|
||||||
|
}
|
||||||
|
newConfig.netty.address = "ws://" + address + "/api";
|
||||||
|
newConfig.netty.downloadURL = "http://" + address + "/%dirname%/";
|
||||||
|
newConfig.netty.launcherURL = "http://" + address + "/Launcher.jar";
|
||||||
|
newConfig.netty.launcherEXEURL = "http://" + address + "/Launcher.exe";
|
||||||
|
newConfig.netty.binds[0].port = port;
|
||||||
|
|
||||||
|
// Write LaunchServer config
|
||||||
|
logger.info("Writing LaunchServer config file");
|
||||||
|
try (BufferedWriter writer = IOHelper.newWriter(configFile)) {
|
||||||
|
Launcher.gsonManager.configGson.toJson(newConfig, writer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class BasicLaunchServerConfigManager implements LaunchServer.LaunchServerConfigManager {
|
||||||
|
private final Path configFile;
|
||||||
|
private final Path runtimeConfigFile;
|
||||||
|
|
||||||
|
public BasicLaunchServerConfigManager(Path configFile, Path runtimeConfigFile) {
|
||||||
|
this.configFile = configFile;
|
||||||
|
this.runtimeConfigFile = runtimeConfigFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LaunchServerConfig readConfig() throws IOException {
|
||||||
|
LaunchServerConfig config1;
|
||||||
|
try (BufferedReader reader = IOHelper.newReader(configFile)) {
|
||||||
|
config1 = Launcher.gsonManager.gson.fromJson(reader, LaunchServerConfig.class);
|
||||||
|
}
|
||||||
|
return config1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LaunchServerRuntimeConfig readRuntimeConfig() throws IOException {
|
||||||
|
LaunchServerRuntimeConfig config1;
|
||||||
|
try (BufferedReader reader = IOHelper.newReader(runtimeConfigFile)) {
|
||||||
|
config1 = Launcher.gsonManager.gson.fromJson(reader, LaunchServerRuntimeConfig.class);
|
||||||
|
}
|
||||||
|
return config1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeConfig(LaunchServerConfig config) throws IOException {
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
try (Writer writer = IOHelper.newWriter(output)) {
|
||||||
|
if (Launcher.gsonManager.configGson != null) {
|
||||||
|
Launcher.gsonManager.configGson.toJson(config, writer);
|
||||||
|
} else {
|
||||||
|
logger.error("Error writing LaunchServer config file. Gson is null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] bytes = output.toByteArray();
|
||||||
|
if(bytes.length > 0) {
|
||||||
|
IOHelper.write(configFile, bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeRuntimeConfig(LaunchServerRuntimeConfig config) throws IOException {
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
try (Writer writer = IOHelper.newWriter(output)) {
|
||||||
|
if (Launcher.gsonManager.configGson != null) {
|
||||||
|
Launcher.gsonManager.configGson.toJson(config, writer);
|
||||||
|
} else {
|
||||||
|
logger.error("Error writing LaunchServer runtime config file. Gson is null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byte[] bytes = output.toByteArray();
|
||||||
|
if(bytes.length > 0) {
|
||||||
|
IOHelper.write(runtimeConfigFile, bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
96
LaunchServer/src/main/java/pro/gravit/launchserver/Main.java
Normal file
96
LaunchServer/src/main/java/pro/gravit/launchserver/Main.java
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package pro.gravit.launchserver;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.holder.LaunchServerControlHolder;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
import pro.gravit.utils.launch.ClassLoaderControl;
|
||||||
|
import pro.gravit.utils.launch.LaunchOptions;
|
||||||
|
import pro.gravit.utils.launch.ModuleLaunch;
|
||||||
|
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.FileVisitOption;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class Main {
|
||||||
|
private static final List<String> classpathOnly = List.of("proguard", "progressbar", "kotlin");
|
||||||
|
private static final String LOG4J_PROPERTY = "log4j2.configurationFile";
|
||||||
|
private static final String DEBUG_PROPERTY = "launchserver.main.debug";
|
||||||
|
private static final String LIBRARIES_PROPERTY = "launchserver.dir.libraries";
|
||||||
|
private static boolean isClasspathOnly(Path path) {
|
||||||
|
var fileName = path.getFileName().toString();
|
||||||
|
for(var e : classpathOnly) {
|
||||||
|
if(fileName.contains(e)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void unpackLog4j() {
|
||||||
|
String log4jConfigurationFile = System.getProperty(LOG4J_PROPERTY);
|
||||||
|
if(log4jConfigurationFile == null) {
|
||||||
|
Path log4jConfigPath = Path.of("log4j2.xml");
|
||||||
|
if(!Files.exists(log4jConfigPath)) {
|
||||||
|
try(FileOutputStream output = new FileOutputStream(log4jConfigPath.toFile())) {
|
||||||
|
try(InputStream input = Main.class.getResourceAsStream("/log4j2.xml")) {
|
||||||
|
if(input == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
input.transferTo(output);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.setProperty(LOG4J_PROPERTY, log4jConfigPath.toAbsolutePath().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) throws Throwable {
|
||||||
|
unpackLog4j();
|
||||||
|
ModuleLaunch launch = new ModuleLaunch();
|
||||||
|
LaunchOptions options = new LaunchOptions();
|
||||||
|
options.moduleConf = new LaunchOptions.ModuleConf();
|
||||||
|
Path librariesPath = Path.of(System.getProperty(LIBRARIES_PROPERTY, "libraries"));
|
||||||
|
List<Path> libraries;
|
||||||
|
try(Stream<Path> files = Files.walk(librariesPath, FileVisitOption.FOLLOW_LINKS)) {
|
||||||
|
libraries = new ArrayList<>(files.filter(e -> e.getFileName().toString().endsWith(".jar")).toList());
|
||||||
|
}
|
||||||
|
List<Path> classpath = new ArrayList<>();
|
||||||
|
List<String> modulepath = new ArrayList<>();
|
||||||
|
for(var l : libraries) {
|
||||||
|
if(isClasspathOnly(l)) {
|
||||||
|
classpath.add(l);
|
||||||
|
} else {
|
||||||
|
modulepath.add(l.toAbsolutePath().toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
classpath.add(IOHelper.getCodeSource(LaunchServerStarter.class));
|
||||||
|
options.moduleConf.modulePath.addAll(modulepath);
|
||||||
|
options.moduleConf.modules.add("ALL-MODULE-PATH");
|
||||||
|
options.moduleConf.enableNativeAccess.add("org.fusesource.jansi");
|
||||||
|
options.moduleConf.enableNativeAccess.add("io.netty.common");
|
||||||
|
ClassLoaderControl control = launch.init(classpath, "natives", options);
|
||||||
|
control.clearLauncherPackages();
|
||||||
|
control.addLauncherPackage("pro.gravit.utils.launch");
|
||||||
|
control.addLauncherPackage("pro.gravit.launchserver.holder");
|
||||||
|
ModuleLayer.Controller controller = (ModuleLayer.Controller) control.getJava9ModuleController();
|
||||||
|
LaunchServerControlHolder.setControl(control);
|
||||||
|
LaunchServerControlHolder.setController(controller);
|
||||||
|
if(Boolean.getBoolean(DEBUG_PROPERTY)) {
|
||||||
|
for(var e : controller.layer().modules()) {
|
||||||
|
System.out.printf("Module %s\n", e.getName());
|
||||||
|
for(var p : e.getPackages()) {
|
||||||
|
System.out.printf("Package %s\n", p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch.launch("pro.gravit.launchserver.LaunchServerStarter", null, Arrays.asList(args));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package pro.gravit.launchserver;
|
||||||
|
|
||||||
|
import pro.gravit.utils.command.Command;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows calling commands using the config command
|
||||||
|
*/
|
||||||
|
public interface Reconfigurable {
|
||||||
|
/**
|
||||||
|
* Gets a list of commands available for this object.
|
||||||
|
*
|
||||||
|
* @return Key - Command Name
|
||||||
|
* Value is a command object
|
||||||
|
*/
|
||||||
|
Map<String, Command> getCommands();
|
||||||
|
|
||||||
|
default Map<String, Command> defaultCommandsMap() {
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package pro.gravit.launchserver;
|
||||||
|
|
||||||
|
import java.lang.instrument.Instrumentation;
|
||||||
|
import java.nio.file.*;
|
||||||
|
|
||||||
|
public final class StarterAgent {
|
||||||
|
|
||||||
|
public static Instrumentation inst = null;
|
||||||
|
public static Path libraries = null;
|
||||||
|
private static boolean isStarted = false;
|
||||||
|
|
||||||
|
public static boolean isAgentStarted() {
|
||||||
|
return isStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void premain(String agentArgument, Instrumentation inst) {
|
||||||
|
throw new UnsupportedOperationException("Please remove -javaagent option from start.sh");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,168 @@
|
||||||
|
package pro.gravit.launchserver.asm;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.ClassVisitor;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Позволяет искать методы внутри незагруженных классов и общие суперклассы для
|
||||||
|
* чего угодно. Работает через поиск class-файлов в classpath.
|
||||||
|
*/
|
||||||
|
public class ClassMetadataReader implements Closeable {
|
||||||
|
private final Logger logger = LogManager.getLogger(ClassMetadataReader.class);
|
||||||
|
private final List<JarFile> cp;
|
||||||
|
private final Map<String, Module> moduleClassFinder;
|
||||||
|
|
||||||
|
public ClassMetadataReader(List<JarFile> cp) {
|
||||||
|
this.cp = cp;
|
||||||
|
//var moduleLayer = ClassMetadataReader.class.getModule().getLayer() == null ? ModuleLayer.boot() : ClassMetadataReader.class.getModule().getLayer();
|
||||||
|
var moduleLayer = ModuleLayer.boot();
|
||||||
|
moduleClassFinder = collectModulePackages(moduleLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClassMetadataReader() {
|
||||||
|
this.cp = new ArrayList<>();
|
||||||
|
//var moduleLayer = ClassMetadataReader.class.getModule().getLayer() == null ? ModuleLayer.boot() : ClassMetadataReader.class.getModule().getLayer();
|
||||||
|
var moduleLayer = ModuleLayer.boot();
|
||||||
|
moduleClassFinder = collectModulePackages(moduleLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Module> collectModulePackages(ModuleLayer layer) {
|
||||||
|
var map = new HashMap<String, Module>();
|
||||||
|
for(var m : layer.modules()) {
|
||||||
|
for(var p : m.getPackages()) {
|
||||||
|
map.put(p, m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<JarFile> getCp() {
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void acceptVisitor(byte[] classData, ClassVisitor visitor) {
|
||||||
|
new ClassReader(classData).accept(visitor, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void acceptVisitor(String className, ClassVisitor visitor) throws IOException {
|
||||||
|
acceptVisitor(getClassData(className), visitor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void acceptVisitor(byte[] classData, ClassVisitor visitor, int flags) {
|
||||||
|
new ClassReader(classData).accept(visitor, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void acceptVisitor(String className, ClassVisitor visitor, int flags) throws IOException {
|
||||||
|
acceptVisitor(getClassData(className), visitor, flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getClassData(String className) throws IOException {
|
||||||
|
for (JarFile f : cp) {
|
||||||
|
if (f.getEntry(className + ".class") != null) {
|
||||||
|
byte[] bytes;
|
||||||
|
try (InputStream in = f.getInputStream(f.getEntry(className + ".class"))) {
|
||||||
|
bytes = IOHelper.read(in);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(ClassMetadataReader.class.getModule().isNamed()) {
|
||||||
|
String pkg = getClassPackage(className).replace('/', '.');
|
||||||
|
var module = moduleClassFinder.get(pkg);
|
||||||
|
if(module != null) {
|
||||||
|
var cl = module.getClassLoader();
|
||||||
|
if(cl == null) {
|
||||||
|
cl = ClassLoader.getPlatformClassLoader();
|
||||||
|
}
|
||||||
|
var stream = cl.getResourceAsStream(className+".class");
|
||||||
|
if(stream != null) {
|
||||||
|
try(stream) {
|
||||||
|
return IOHelper.read(stream);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new FileNotFoundException("Class "+className + ".class");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new FileNotFoundException("Package "+pkg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var stream = ClassLoader.getSystemClassLoader().getResourceAsStream(className+".class");
|
||||||
|
if(stream != null) {
|
||||||
|
try(stream) {
|
||||||
|
return IOHelper.read(stream);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new FileNotFoundException(className + ".class");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getClassPackage(String type) {
|
||||||
|
int idx = type.lastIndexOf("/");
|
||||||
|
if(idx <= 0) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
return type.substring(0, idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSuperClass(String type) {
|
||||||
|
if (type.equals("java/lang/Object")) return null;
|
||||||
|
try {
|
||||||
|
return getSuperClassASM(type);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("getSuperClass: type {} not found ({}: {})", type, e.getClass().getName(), e.getMessage());
|
||||||
|
return "java/lang/Object";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getSuperClassASM(String type) throws IOException {
|
||||||
|
CheckSuperClassVisitor cv = new CheckSuperClassVisitor();
|
||||||
|
acceptVisitor(type, cv);
|
||||||
|
return cv.superClassName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает суперклассы в порядке возрастающей конкретности (начиная с
|
||||||
|
* java/lang/Object и заканчивая данным типом)
|
||||||
|
*/
|
||||||
|
public ArrayList<String> getSuperClasses(String type) {
|
||||||
|
ArrayList<String> superclasses = new ArrayList<>(1);
|
||||||
|
superclasses.add(type);
|
||||||
|
while ((type = getSuperClass(type)) != null)
|
||||||
|
superclasses.add(type);
|
||||||
|
Collections.reverse(superclasses);
|
||||||
|
return superclasses;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
cp.forEach(IOHelper::close);
|
||||||
|
cp.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class CheckSuperClassVisitor extends ClassVisitor {
|
||||||
|
|
||||||
|
String superClassName;
|
||||||
|
|
||||||
|
public CheckSuperClassVisitor() {
|
||||||
|
super(Opcodes.ASM9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(int version, int access, String name, String signature, String superName,
|
||||||
|
String[] interfaces) {
|
||||||
|
superClassName = superName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,254 @@
|
||||||
|
package pro.gravit.launchserver.asm;
|
||||||
|
|
||||||
|
import org.objectweb.asm.AnnotationVisitor;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
import org.objectweb.asm.Type;
|
||||||
|
import org.objectweb.asm.tree.*;
|
||||||
|
import pro.gravit.launcher.core.LauncherInject;
|
||||||
|
import pro.gravit.launcher.core.LauncherInjectionConstructor;
|
||||||
|
import pro.gravit.launchserver.binary.BuildContext;
|
||||||
|
import pro.gravit.launchserver.binary.tasks.MainBuildTask;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
public class InjectClassAcceptor implements MainBuildTask.ASMTransformer {
|
||||||
|
private static final List<Class<?>> primitiveLDCClasses = Arrays.asList(java.lang.Integer.class, java.lang.Long.class,
|
||||||
|
java.lang.Float.class, java.lang.Double.class, java.lang.String.class);
|
||||||
|
private static final String INJECTED_FIELD_DESC = Type.getDescriptor(LauncherInject.class);
|
||||||
|
private static final String INJECTED_CONSTRUCTOR_DESC = Type.getDescriptor(LauncherInjectionConstructor.class);
|
||||||
|
private static final List<String> primitiveLDCDescriptors = Arrays.asList(Type.INT_TYPE.getDescriptor(), Type.DOUBLE_TYPE.getDescriptor(),
|
||||||
|
Type.FLOAT_TYPE.getDescriptor(), Type.LONG_TYPE.getDescriptor(), Type.getDescriptor(String.class));
|
||||||
|
private static final Map<Class<?>, Serializer<?>> serializers;
|
||||||
|
|
||||||
|
static {
|
||||||
|
serializers = new HashMap<>();
|
||||||
|
serializers.put(List.class, new ListSerializer());
|
||||||
|
serializers.put(Map.class, new MapSerializer());
|
||||||
|
serializers.put(byte[].class, new ByteArraySerializer());
|
||||||
|
serializers.put(Short.class, serializerClass(Opcodes.I2S));
|
||||||
|
serializers.put(Byte.class, serializerClass(Opcodes.I2B));
|
||||||
|
serializers.put(Type.class, (Serializer<Type>) e -> { // ow.Type == java.lang.Class in LDC
|
||||||
|
InsnList ret = new InsnList();
|
||||||
|
ret.add(new LdcInsnNode(e));
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
serializers.put(Boolean.class, (Serializer<Boolean>) e -> {
|
||||||
|
InsnList ret = new InsnList();
|
||||||
|
ret.add(new InsnNode(e ? Opcodes.ICONST_1 : Opcodes.ICONST_0));
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
serializers.put(Character.class, (Serializer<Character>) e -> {
|
||||||
|
InsnList ret = new InsnList();
|
||||||
|
ret.add(NodeUtils.push((int) e));
|
||||||
|
ret.add(new InsnNode(Opcodes.I2C));
|
||||||
|
return ret;
|
||||||
|
});
|
||||||
|
serializers.put(Enum.class, (Serializer<Enum>) NodeUtils::makeValueEnumGetter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Map<String, Object> values;
|
||||||
|
|
||||||
|
public InjectClassAcceptor(Map<String, Object> values) {
|
||||||
|
this.values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void visit(ClassNode classNode, Map<String, Object> values) {
|
||||||
|
MethodNode clinitMethod = classNode.methods.stream().filter(methodNode -> "<clinit>".equals(methodNode.name))
|
||||||
|
.findFirst().orElseGet(() -> {
|
||||||
|
MethodNode newClinitMethod = new MethodNode(Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC | Opcodes.ACC_SYNTHETIC,
|
||||||
|
"<clinit>", "()V", null, null);
|
||||||
|
newClinitMethod.instructions.add(new InsnNode(Opcodes.RETURN));
|
||||||
|
classNode.methods.add(newClinitMethod);
|
||||||
|
return newClinitMethod;
|
||||||
|
});
|
||||||
|
List<MethodNode> constructors = classNode.methods.stream().filter(method -> "<init>".equals(method.name))
|
||||||
|
.toList();
|
||||||
|
MethodNode initMethod = constructors.stream().filter(method -> method.invisibleAnnotations != null
|
||||||
|
&& method.invisibleAnnotations.stream().anyMatch(annotation -> INJECTED_CONSTRUCTOR_DESC.equals(annotation.desc))).findFirst()
|
||||||
|
.orElseGet(() -> constructors.stream().filter(method -> method.desc.equals("()V")).findFirst().orElse(null));
|
||||||
|
classNode.fields.forEach(field -> {
|
||||||
|
// Notice that fields that will be used with this algo should not have default
|
||||||
|
// value by = ...;
|
||||||
|
boolean isStatic = (field.access & Opcodes.ACC_STATIC) != 0;
|
||||||
|
injectTo(isStatic ? clinitMethod : initMethod, classNode, field, isStatic, values);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void injectTo(MethodNode initMethod, ClassNode classNode, FieldNode field, boolean isStatic, Map<String, Object> values) {
|
||||||
|
AnnotationNode valueAnnotation = field.invisibleAnnotations != null ? field.invisibleAnnotations.stream()
|
||||||
|
.filter(annotation -> INJECTED_FIELD_DESC.equals(annotation.desc)).findFirst()
|
||||||
|
.orElse(null) : null;
|
||||||
|
if (valueAnnotation == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
field.invisibleAnnotations.remove(valueAnnotation);
|
||||||
|
AtomicReference<String> valueName = new AtomicReference<>(null);
|
||||||
|
valueAnnotation.accept(new AnnotationVisitor(Opcodes.ASM7) {
|
||||||
|
@Override
|
||||||
|
public void visit(final String name, final Object value) {
|
||||||
|
if ("value".equals(name)) {
|
||||||
|
if (value.getClass() != String.class)
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Invalid annotation with value class %s".formatted(field.getClass().getName()));
|
||||||
|
valueName.set(value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (valueName.get() == null) {
|
||||||
|
throw new IllegalArgumentException("Annotation should always contains 'value' key");
|
||||||
|
}
|
||||||
|
if (!values.containsKey(valueName.get())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object value = values.get(valueName.get());
|
||||||
|
//if ((field.access & Opcodes.ACC_STATIC) != 0) {
|
||||||
|
if (isStatic) {
|
||||||
|
if (primitiveLDCDescriptors.contains(field.desc) && primitiveLDCClasses.contains(value.getClass())) {
|
||||||
|
field.value = value;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<FieldInsnNode> putStaticNodes = Arrays.stream(initMethod.instructions.toArray())
|
||||||
|
.filter(node -> node instanceof FieldInsnNode && node.getOpcode() == Opcodes.PUTSTATIC).map(p -> (FieldInsnNode) p)
|
||||||
|
.filter(node -> node.owner.equals(classNode.name) && node.name.equals(field.name) && node.desc.equals(field.desc)).toList();
|
||||||
|
InsnList setter = serializeValue(value);
|
||||||
|
if (putStaticNodes.isEmpty()) {
|
||||||
|
setter.add(new FieldInsnNode(Opcodes.PUTSTATIC, classNode.name, field.name, field.desc));
|
||||||
|
Arrays.stream(initMethod.instructions.toArray()).filter(node -> node.getOpcode() == Opcodes.RETURN)
|
||||||
|
.forEach(node -> initMethod.instructions.insertBefore(node, setter));
|
||||||
|
} else {
|
||||||
|
setter.insert(new InsnNode(Type.getType(field.desc).getSize() == 1 ? Opcodes.POP : Opcodes.POP2));
|
||||||
|
for (FieldInsnNode fieldInsnNode : putStaticNodes) {
|
||||||
|
initMethod.instructions.insertBefore(fieldInsnNode, setter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (initMethod == null) {
|
||||||
|
throw new IllegalArgumentException("Not found init in target: %s".formatted(classNode.name));
|
||||||
|
}
|
||||||
|
List<FieldInsnNode> putFieldNodes = Arrays.stream(initMethod.instructions.toArray())
|
||||||
|
.filter(node -> node instanceof FieldInsnNode && node.getOpcode() == Opcodes.PUTFIELD).map(p -> (FieldInsnNode) p)
|
||||||
|
.filter(node -> node.owner.equals(classNode.name) && node.name.equals(field.name) && node.desc.equals(field.desc)).toList();
|
||||||
|
InsnList setter = serializeValue(value);
|
||||||
|
if (putFieldNodes.isEmpty()) {
|
||||||
|
setter.insert(new VarInsnNode(Opcodes.ALOAD, 0));
|
||||||
|
setter.add(new FieldInsnNode(Opcodes.PUTFIELD, classNode.name, field.name, field.desc));
|
||||||
|
Arrays.stream(initMethod.instructions.toArray())
|
||||||
|
.filter(node -> node.getOpcode() == Opcodes.RETURN)
|
||||||
|
.forEach(node -> initMethod.instructions.insertBefore(node, setter));
|
||||||
|
} else {
|
||||||
|
setter.insert(new InsnNode(Type.getType(field.desc).getSize() == 1 ? Opcodes.POP : Opcodes.POP2));
|
||||||
|
for (FieldInsnNode fieldInsnNode : putFieldNodes) {
|
||||||
|
initMethod.instructions.insertBefore(fieldInsnNode, setter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Serializer<?> serializerClass(int opcode) {
|
||||||
|
return (Serializer<Number>) value -> {
|
||||||
|
InsnList ret = new InsnList();
|
||||||
|
ret.add(NodeUtils.push(value.intValue()));
|
||||||
|
ret.add(new InsnNode(opcode));
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static InsnList serializeValue(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
InsnList insnList = new InsnList();
|
||||||
|
insnList.add(new InsnNode(Opcodes.ACONST_NULL));
|
||||||
|
return insnList;
|
||||||
|
}
|
||||||
|
if (primitiveLDCClasses.contains(value.getClass())) {
|
||||||
|
InsnList insnList = new InsnList();
|
||||||
|
insnList.add(new LdcInsnNode(value));
|
||||||
|
return insnList;
|
||||||
|
}
|
||||||
|
for (Map.Entry<Class<?>, Serializer<?>> serializerEntry : serializers.entrySet()) {
|
||||||
|
if (serializerEntry.getKey().isInstance(value)) {
|
||||||
|
return ((Serializer) serializerEntry.getValue()).serialize(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new UnsupportedOperationException("Serialization of type %s is not supported".formatted(value.getClass()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSerializableValue(Object value) {
|
||||||
|
if (value == null) return true;
|
||||||
|
if (primitiveLDCClasses.contains(value.getClass())) return true;
|
||||||
|
for (Map.Entry<Class<?>, Serializer<?>> serializerEntry : serializers.entrySet()) {
|
||||||
|
if (serializerEntry.getKey().isInstance(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void transform(ClassNode classNode, String className, BuildContext context) {
|
||||||
|
visit(classNode, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
private interface Serializer<T> {
|
||||||
|
InsnList serialize(T value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ListSerializer implements Serializer<List> {
|
||||||
|
@Override
|
||||||
|
public InsnList serialize(List value) {
|
||||||
|
InsnList insnList = new InsnList();
|
||||||
|
insnList.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(ArrayList.class)));
|
||||||
|
insnList.add(new InsnNode(Opcodes.DUP));
|
||||||
|
insnList.add(NodeUtils.push(value.size()));
|
||||||
|
insnList.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(ArrayList.class), "<init>",
|
||||||
|
Type.getMethodDescriptor(Type.VOID_TYPE, Type.INT_TYPE), false));
|
||||||
|
for (Object object : value) {
|
||||||
|
insnList.add(new InsnNode(Opcodes.DUP));
|
||||||
|
insnList.add(serializeValue(object));
|
||||||
|
insnList.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, Type.getInternalName(List.class), "add",
|
||||||
|
Type.getMethodDescriptor(Type.BOOLEAN_TYPE, Type.getType(Object.class)), true));
|
||||||
|
insnList.add(new InsnNode(Opcodes.POP));
|
||||||
|
}
|
||||||
|
return insnList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MapSerializer implements Serializer<Map> {
|
||||||
|
@Override
|
||||||
|
public InsnList serialize(Map value) {
|
||||||
|
InsnList insnList = new InsnList();
|
||||||
|
insnList.add(new TypeInsnNode(Opcodes.NEW, Type.getInternalName(value.getClass())));
|
||||||
|
insnList.add(new InsnNode(Opcodes.DUP));
|
||||||
|
insnList.add(new MethodInsnNode(Opcodes.INVOKESPECIAL, Type.getInternalName(value.getClass()), "<init>",
|
||||||
|
Type.getMethodDescriptor(Type.VOID_TYPE), false));
|
||||||
|
for (Object entryObject : value.entrySet()) {
|
||||||
|
Map.Entry entry = (Map.Entry) entryObject;
|
||||||
|
insnList.add(new InsnNode(Opcodes.DUP));
|
||||||
|
insnList.add(serializeValue(entry.getKey()));
|
||||||
|
insnList.add(serializeValue(entry.getValue()));
|
||||||
|
insnList.add(new MethodInsnNode(Opcodes.INVOKEINTERFACE, Type.getInternalName(Map.class), "put",
|
||||||
|
Type.getMethodDescriptor(Type.getType(Object.class), Type.getType(Object.class), Type.getType(Object.class)),
|
||||||
|
true));
|
||||||
|
insnList.add(new InsnNode(Opcodes.POP));
|
||||||
|
}
|
||||||
|
return insnList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ByteArraySerializer implements Serializer<byte[]> {
|
||||||
|
@Override
|
||||||
|
public InsnList serialize(byte[] value) {
|
||||||
|
InsnList insnList = new InsnList();
|
||||||
|
insnList.add(new MethodInsnNode(Opcodes.INVOKESTATIC, Type.getInternalName(Base64.class),
|
||||||
|
"getDecoder", Type.getMethodDescriptor(Type.getType(Base64.Decoder.class)), false));
|
||||||
|
insnList.add(NodeUtils.getSafeStringInsnList(Base64.getEncoder().encodeToString(value)));
|
||||||
|
insnList.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, Type.getInternalName(Base64.Decoder.class),
|
||||||
|
"decode", Type.getMethodDescriptor(Type.getType(byte[].class), Type.getType(String.class)),
|
||||||
|
false));
|
||||||
|
return insnList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,243 @@
|
||||||
|
package pro.gravit.launchserver.asm;
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.Opcodes;
|
||||||
|
import org.objectweb.asm.Type;
|
||||||
|
import org.objectweb.asm.tree.*;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
import pro.gravit.utils.helper.JarHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.objectweb.asm.Opcodes.*;
|
||||||
|
|
||||||
|
public final class NodeUtils {
|
||||||
|
|
||||||
|
public static final int MAX_SAFE_BYTE_COUNT = 65535 - Byte.MAX_VALUE;
|
||||||
|
|
||||||
|
private NodeUtils() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ClassNode forClass(Class<?> cls, int flags) {
|
||||||
|
try (InputStream in = JarHelper.getClassBytesStream(cls)) {
|
||||||
|
ClassNode ret = new ClassNode();
|
||||||
|
new ClassReader(IOHelper.read(in)).accept(ret, flags);
|
||||||
|
return ret;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ClassNode forClass(String clazz, int flags, ClassMetadataReader r) {
|
||||||
|
try {
|
||||||
|
ClassNode ret = new ClassNode();
|
||||||
|
r.acceptVisitor(clazz, ret, flags);
|
||||||
|
return ret;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<AnnotationNode> annots(String clazz, String method, ClassMetadataReader r) {
|
||||||
|
if (clazz.startsWith("L")) clazz = Type.getType(clazz).getInternalName();
|
||||||
|
try {
|
||||||
|
List<AnnotationNode> ret = new ArrayList<>();
|
||||||
|
ClassNode n = forClass(clazz, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG, r);
|
||||||
|
if (n.visibleAnnotations != null) ret.addAll(n.visibleAnnotations);
|
||||||
|
if (n.invisibleAnnotations != null) ret.addAll(n.invisibleAnnotations);
|
||||||
|
for (MethodNode m : n.methods)
|
||||||
|
if (method.equals(m.name)) {
|
||||||
|
if (m.visibleAnnotations != null) ret.addAll(m.visibleAnnotations);
|
||||||
|
if (m.invisibleAnnotations != null) ret.addAll(m.invisibleAnnotations);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int doMethodEmulation(String desc) {
|
||||||
|
int result = 0;
|
||||||
|
Type returnType = Type.getReturnType(desc);
|
||||||
|
|
||||||
|
if (returnType.getSort() == Type.LONG || returnType.getSort() == Type.DOUBLE)
|
||||||
|
result++;
|
||||||
|
if (returnType.getSort() != Type.VOID)
|
||||||
|
result++;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int opcodeEmulation(AbstractInsnNode e) {
|
||||||
|
int stackSize = 0;
|
||||||
|
switch (e.getOpcode()) {
|
||||||
|
case NOP:
|
||||||
|
case LALOAD: // (index, arrayref) -> (long, long_top)
|
||||||
|
case DALOAD: // (index, arrayref) -> (double, double_top)
|
||||||
|
case SWAP: // (value1, value2) -> (value2, value1)
|
||||||
|
case INEG:
|
||||||
|
case LNEG:
|
||||||
|
case FNEG:
|
||||||
|
case DNEG:
|
||||||
|
case IINC:
|
||||||
|
case I2F:
|
||||||
|
case L2D:
|
||||||
|
case F2I:
|
||||||
|
case D2L:
|
||||||
|
case I2B:
|
||||||
|
case I2C:
|
||||||
|
case I2S:
|
||||||
|
case GOTO:
|
||||||
|
case RETURN:
|
||||||
|
case NEWARRAY:
|
||||||
|
case ANEWARRAY:
|
||||||
|
case ARRAYLENGTH:
|
||||||
|
case CHECKCAST:
|
||||||
|
case INSTANCEOF:
|
||||||
|
// Does nothing
|
||||||
|
break;
|
||||||
|
case ACONST_NULL:
|
||||||
|
case ICONST_M1:
|
||||||
|
case ICONST_0:
|
||||||
|
case ICONST_1:
|
||||||
|
case ICONST_2:
|
||||||
|
case ICONST_3:
|
||||||
|
case ICONST_4:
|
||||||
|
case ICONST_5:
|
||||||
|
case FCONST_0:
|
||||||
|
case FCONST_1:
|
||||||
|
case FCONST_2:
|
||||||
|
case BIPUSH:
|
||||||
|
case SIPUSH:
|
||||||
|
case ILOAD:
|
||||||
|
case FLOAD:
|
||||||
|
case ALOAD:
|
||||||
|
case DUP:
|
||||||
|
case DUP_X1:
|
||||||
|
case DUP_X2:
|
||||||
|
case I2L:
|
||||||
|
case I2D:
|
||||||
|
case F2L:
|
||||||
|
case F2D:
|
||||||
|
case NEW:
|
||||||
|
// Pushes one-word constant to stack
|
||||||
|
stackSize++;
|
||||||
|
break;
|
||||||
|
case LDC:
|
||||||
|
LdcInsnNode ldc = (LdcInsnNode) e;
|
||||||
|
if (ldc.cst instanceof Long || ldc.cst instanceof Double)
|
||||||
|
stackSize++;
|
||||||
|
|
||||||
|
stackSize++;
|
||||||
|
break;
|
||||||
|
case LCONST_0:
|
||||||
|
case LCONST_1:
|
||||||
|
case DCONST_0:
|
||||||
|
case DCONST_1:
|
||||||
|
case LLOAD:
|
||||||
|
case DLOAD:
|
||||||
|
case DUP2:
|
||||||
|
case DUP2_X1:
|
||||||
|
case DUP2_X2:
|
||||||
|
// Pushes two-word constant or two one-word constants to stack
|
||||||
|
stackSize++;
|
||||||
|
stackSize++;
|
||||||
|
break;
|
||||||
|
case INVOKEVIRTUAL:
|
||||||
|
case INVOKESPECIAL:
|
||||||
|
case INVOKEINTERFACE, INVOKESTATIC:
|
||||||
|
stackSize += doMethodEmulation(((MethodInsnNode) e).desc);
|
||||||
|
break;
|
||||||
|
case INVOKEDYNAMIC:
|
||||||
|
stackSize += doMethodEmulation(((InvokeDynamicInsnNode) e).desc);
|
||||||
|
break;
|
||||||
|
case JSR:
|
||||||
|
case RET:
|
||||||
|
throw new RuntimeException("Did not expect JSR/RET instructions");
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return stackSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InsnList getSafeStringInsnList(String string) {
|
||||||
|
InsnList insnList = new InsnList();
|
||||||
|
if ((string.length() * 3) < MAX_SAFE_BYTE_COUNT) { // faster check
|
||||||
|
insnList.add(new LdcInsnNode(string));
|
||||||
|
return insnList;
|
||||||
|
}
|
||||||
|
|
||||||
|
insnList.add(new TypeInsnNode(NEW, "java/lang/StringBuilder"));
|
||||||
|
insnList.add(new InsnNode(DUP));
|
||||||
|
insnList.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false));
|
||||||
|
|
||||||
|
String[] chunks = splitUtf8ToChunks(string, MAX_SAFE_BYTE_COUNT);
|
||||||
|
for (String chunk : chunks) {
|
||||||
|
insnList.add(new LdcInsnNode(chunk));
|
||||||
|
insnList.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false));
|
||||||
|
}
|
||||||
|
insnList.add(new MethodInsnNode(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false));
|
||||||
|
|
||||||
|
return insnList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] splitUtf8ToChunks(String text, int maxBytes) {
|
||||||
|
List<String> parts = new ArrayList<>();
|
||||||
|
|
||||||
|
char[] chars = text.toCharArray();
|
||||||
|
|
||||||
|
int lastCharIndex = 0;
|
||||||
|
int currentChunkSize = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < chars.length; i++) {
|
||||||
|
char c = chars[i];
|
||||||
|
int charSize = getUtf8CharSize(c);
|
||||||
|
if (currentChunkSize + charSize < maxBytes) {
|
||||||
|
currentChunkSize += charSize;
|
||||||
|
} else {
|
||||||
|
parts.add(text.substring(lastCharIndex, i));
|
||||||
|
currentChunkSize = 0;
|
||||||
|
lastCharIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChunkSize != 0) {
|
||||||
|
parts.add(text.substring(lastCharIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts.toArray(new String[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getUtf8CharSize(char c) {
|
||||||
|
if (c >= 0x0001 && c <= 0x007F) {
|
||||||
|
return 1;
|
||||||
|
} else if (c <= 0x07FF) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InsnList push(final int value) {
|
||||||
|
InsnList ret = new InsnList();
|
||||||
|
if (value >= -1 && value <= 5)
|
||||||
|
ret.add(new InsnNode(Opcodes.ICONST_0 + value));
|
||||||
|
else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE)
|
||||||
|
ret.add(new IntInsnNode(Opcodes.BIPUSH, value));
|
||||||
|
else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE)
|
||||||
|
ret.add(new IntInsnNode(Opcodes.SIPUSH, value));
|
||||||
|
else
|
||||||
|
ret.add(new LdcInsnNode(value));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static InsnList makeValueEnumGetter(@SuppressWarnings("rawtypes") Enum u) {
|
||||||
|
InsnList ret = new InsnList();
|
||||||
|
Type e = Type.getType(u.getClass());
|
||||||
|
ret.add(new FieldInsnNode(Opcodes.GETSTATIC, e.getInternalName(), u.name(), e.getDescriptor()));
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package pro.gravit.launchserver.asm;
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.ClassWriter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ClassWriter с другой реализацией метода getCommonSuperClass: при его
|
||||||
|
* использовании не происходит загрузки классов.
|
||||||
|
*/
|
||||||
|
public class SafeClassWriter extends ClassWriter {
|
||||||
|
|
||||||
|
private final ClassMetadataReader classMetadataReader;
|
||||||
|
|
||||||
|
public SafeClassWriter(ClassMetadataReader classMetadataReader, int flags) {
|
||||||
|
super(flags);
|
||||||
|
this.classMetadataReader = classMetadataReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public SafeClassWriter(ClassReader classReader, ClassMetadataReader classMetadataReader, int flags) {
|
||||||
|
super(classReader, flags);
|
||||||
|
this.classMetadataReader = classMetadataReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String getCommonSuperClass(String type1, String type2) {
|
||||||
|
ArrayList<String> superClasses1 = classMetadataReader.getSuperClasses(type1);
|
||||||
|
ArrayList<String> superClasses2 = classMetadataReader.getSuperClasses(type2);
|
||||||
|
int size = Math.min(superClasses1.size(), superClasses2.size());
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < size && superClasses1.get(i).equals(superClasses2.get(i)); i++)
|
||||||
|
;
|
||||||
|
if (i == 0)
|
||||||
|
return "java/lang/Object";
|
||||||
|
else
|
||||||
|
return superClasses1.get(i - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package pro.gravit.launchserver.auth;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.events.request.AuthRequestEvent;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public final class AuthException extends IOException {
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = -2586107832847245863L;
|
||||||
|
|
||||||
|
|
||||||
|
public AuthException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthException need2FA() {
|
||||||
|
return new AuthException(AuthRequestEvent.TWO_FACTOR_NEED_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthException needMFA(List<Integer> factors) {
|
||||||
|
String message = AuthRequestEvent.ONE_FACTOR_NEED_ERROR_MESSAGE_PREFIX
|
||||||
|
.concat(factors.stream().map(String::valueOf).collect(Collectors.joining(".")));
|
||||||
|
return new AuthException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthException wrongPassword() {
|
||||||
|
return new AuthException(AuthRequestEvent.WRONG_PASSWORD_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AuthException userNotFound() {
|
||||||
|
return new AuthException(AuthRequestEvent.USER_NOT_FOUND_ERROR_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return getMessage();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package pro.gravit.launchserver.auth;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.auth.core.AuthCoreProvider;
|
||||||
|
import pro.gravit.launchserver.auth.mix.MixProvider;
|
||||||
|
import pro.gravit.launchserver.auth.texture.TextureProvider;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class AuthProviderPair {
|
||||||
|
private transient final Logger logger = LogManager.getLogger();
|
||||||
|
public boolean isDefault = true;
|
||||||
|
public AuthCoreProvider core;
|
||||||
|
public TextureProvider textureProvider;
|
||||||
|
public Map<String, MixProvider> mixes;
|
||||||
|
public Map<String, String> links;
|
||||||
|
public transient String name;
|
||||||
|
public transient Set<String> features;
|
||||||
|
public String displayName;
|
||||||
|
public boolean visible = true;
|
||||||
|
|
||||||
|
public AuthProviderPair() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public AuthProviderPair(AuthCoreProvider core, TextureProvider textureProvider) {
|
||||||
|
this.core = core;
|
||||||
|
this.textureProvider = textureProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<String> getFeatures(Class<?> clazz) {
|
||||||
|
Set<String> list = new HashSet<>();
|
||||||
|
getFeatures(clazz, list);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<String> getFeatures() {
|
||||||
|
return features;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void getFeatures(Class<?> clazz, Set<String> list) {
|
||||||
|
Feature[] features = clazz.getAnnotationsByType(Feature.class);
|
||||||
|
for (Feature feature : features) {
|
||||||
|
list.add(feature.value());
|
||||||
|
}
|
||||||
|
Class<?> superClass = clazz.getSuperclass();
|
||||||
|
if (superClass != null && superClass != Object.class) {
|
||||||
|
getFeatures(superClass, list);
|
||||||
|
}
|
||||||
|
Class<?>[] interfaces = clazz.getInterfaces();
|
||||||
|
for (Class<?> i : interfaces) {
|
||||||
|
getFeatures(i, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T isSupport(Class<T> clazz) {
|
||||||
|
if (core == null) return null;
|
||||||
|
T result = core.isSupport(clazz);
|
||||||
|
if (result == null && mixes != null) {
|
||||||
|
for(var m : mixes.values()) {
|
||||||
|
result = m.isSupport(clazz);
|
||||||
|
if(result != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init(LaunchServer srv, String name) {
|
||||||
|
this.name = name;
|
||||||
|
if (links != null) link(srv);
|
||||||
|
core.init(srv, this);
|
||||||
|
features = new HashSet<>();
|
||||||
|
getFeatures(core.getClass(), features);
|
||||||
|
if(mixes != null) {
|
||||||
|
for(var m : mixes.values()) {
|
||||||
|
m.init(srv, core);
|
||||||
|
getFeatures(m.getClass(), features);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void link(LaunchServer srv) {
|
||||||
|
links.forEach((k, v) -> {
|
||||||
|
AuthProviderPair pair = srv.config.getAuthProviderPair(v);
|
||||||
|
if (pair == null) {
|
||||||
|
throw new NullPointerException("Auth %s link failed. Pair %s not found".formatted(name, v));
|
||||||
|
}
|
||||||
|
if ("core".equals(k)) {
|
||||||
|
if (pair.core == null)
|
||||||
|
throw new NullPointerException("Auth %s link failed. %s.core is null".formatted(name, v));
|
||||||
|
core = pair.core;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws IOException {
|
||||||
|
core.close();
|
||||||
|
if (textureProvider != null) {
|
||||||
|
textureProvider.close();
|
||||||
|
}
|
||||||
|
if(mixes != null) {
|
||||||
|
for(var m : mixes.values()) {
|
||||||
|
m.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package pro.gravit.launchserver.auth;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target({ElementType.TYPE})
|
||||||
|
@Repeatable(Features.class)
|
||||||
|
public @interface Feature {
|
||||||
|
String value();
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package pro.gravit.launchserver.auth;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
@Target({ElementType.TYPE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface Features {
|
||||||
|
Feature[] value();
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package pro.gravit.launchserver.auth;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Properties;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
public class HikariSQLSourceConfig implements SQLSourceConfig {
|
||||||
|
private transient volatile HikariDataSource dataSource;
|
||||||
|
private String dsClass;
|
||||||
|
private Properties dsProps;
|
||||||
|
private String driverClass;
|
||||||
|
private String jdbcUrl;
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private boolean initializeAtStart;
|
||||||
|
|
||||||
|
public void init() {
|
||||||
|
if(initializeAtStart) {
|
||||||
|
initializeConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeConnection() {
|
||||||
|
if (dataSource != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HikariConfig config = new HikariConfig();
|
||||||
|
consumeIfNotNull(config::setDataSourceClassName, dsClass);
|
||||||
|
consumeIfNotNull(config::setDataSourceProperties, dsProps);
|
||||||
|
consumeIfNotNull(config::setDriverClassName, driverClass);
|
||||||
|
consumeIfNotNull(config::setJdbcUrl, jdbcUrl);
|
||||||
|
consumeIfNotNull(config::setUsername, username);
|
||||||
|
consumeIfNotNull(config::setPassword, password);
|
||||||
|
|
||||||
|
this.dataSource = new HikariDataSource(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Connection getConnection() throws SQLException {
|
||||||
|
if(dataSource == null && !initializeAtStart) {
|
||||||
|
synchronized (this) {
|
||||||
|
initializeConnection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataSource.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> void consumeIfNotNull(Consumer<T> consumer, T val) {
|
||||||
|
if (val != null) {
|
||||||
|
consumer.accept(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
package pro.gravit.launchserver.auth;
|
||||||
|
|
||||||
|
import com.mysql.cj.jdbc.MysqlDataSource;
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.utils.helper.VerifyHelper;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||||
|
|
||||||
|
public final class MySQLSourceConfig implements AutoCloseable, SQLSourceConfig {
|
||||||
|
|
||||||
|
public static final int TIMEOUT = VerifyHelper.verifyInt(
|
||||||
|
Integer.parseUnsignedInt(System.getProperty("launcher.mysql.idleTimeout", Integer.toString(5000))),
|
||||||
|
VerifyHelper.POSITIVE, "launcher.mysql.idleTimeout can't be <= 5000");
|
||||||
|
private static final int MAX_POOL_SIZE = VerifyHelper.verifyInt(
|
||||||
|
Integer.parseUnsignedInt(System.getProperty("launcher.mysql.maxPoolSize", Integer.toString(3))),
|
||||||
|
VerifyHelper.POSITIVE, "launcher.mysql.maxPoolSize can't be <= 0");
|
||||||
|
|
||||||
|
// Instance
|
||||||
|
private transient final String poolName;
|
||||||
|
private transient final Logger logger = LogManager.getLogger();
|
||||||
|
|
||||||
|
// Config
|
||||||
|
private String address;
|
||||||
|
private int port;
|
||||||
|
private boolean useSSL;
|
||||||
|
private boolean verifyCertificates;
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private String database;
|
||||||
|
private String timezone;
|
||||||
|
private long hikariMaxLifetime = MINUTES.toMillis(30);
|
||||||
|
private boolean useHikari;
|
||||||
|
|
||||||
|
// Cache
|
||||||
|
private transient DataSource source;
|
||||||
|
private transient boolean hikari;
|
||||||
|
|
||||||
|
|
||||||
|
public MySQLSourceConfig(String poolName) {
|
||||||
|
this.poolName = poolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MySQLSourceConfig(String poolName, String address, int port, String username, String password, String database) {
|
||||||
|
this.poolName = poolName;
|
||||||
|
this.address = address;
|
||||||
|
this.port = port;
|
||||||
|
this.username = username;
|
||||||
|
this.password = password;
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MySQLSourceConfig(String poolName, DataSource source, boolean hikari) {
|
||||||
|
this.poolName = poolName;
|
||||||
|
this.source = source;
|
||||||
|
this.hikari = hikari;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void close() {
|
||||||
|
if (hikari)
|
||||||
|
((HikariDataSource) source).close();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public synchronized Connection getConnection() throws SQLException {
|
||||||
|
if (source == null) { // New data source
|
||||||
|
MysqlDataSource mysqlSource = new MysqlDataSource();
|
||||||
|
mysqlSource.setCharacterEncoding("UTF-8");
|
||||||
|
|
||||||
|
// Prep statements cache
|
||||||
|
mysqlSource.setPrepStmtCacheSize(250);
|
||||||
|
mysqlSource.setPrepStmtCacheSqlLimit(2048);
|
||||||
|
mysqlSource.setCachePrepStmts(true);
|
||||||
|
mysqlSource.setUseServerPrepStmts(true);
|
||||||
|
|
||||||
|
// General optimizations
|
||||||
|
mysqlSource.setCacheServerConfiguration(true);
|
||||||
|
mysqlSource.setUseLocalSessionState(true);
|
||||||
|
mysqlSource.setRewriteBatchedStatements(true);
|
||||||
|
mysqlSource.setMaintainTimeStats(false);
|
||||||
|
mysqlSource.setUseUnbufferedInput(false);
|
||||||
|
mysqlSource.setUseReadAheadInput(false);
|
||||||
|
mysqlSource.setUseSSL(useSSL);
|
||||||
|
mysqlSource.setVerifyServerCertificate(verifyCertificates);
|
||||||
|
// Set credentials
|
||||||
|
mysqlSource.setServerName(address);
|
||||||
|
mysqlSource.setPortNumber(port);
|
||||||
|
mysqlSource.setUser(username);
|
||||||
|
mysqlSource.setPassword(password);
|
||||||
|
mysqlSource.setDatabaseName(database);
|
||||||
|
mysqlSource.setTcpNoDelay(true);
|
||||||
|
if (timezone != null) mysqlSource.setServerTimezone(timezone);
|
||||||
|
hikari = false;
|
||||||
|
// Try using HikariCP
|
||||||
|
source = mysqlSource;
|
||||||
|
if (useHikari) {
|
||||||
|
try {
|
||||||
|
Class.forName("com.zaxxer.hikari.HikariDataSource");
|
||||||
|
hikari = true; // Used for shutdown. Not instanceof because of possible classpath error
|
||||||
|
HikariConfig hikariConfig = new HikariConfig();
|
||||||
|
hikariConfig.setDataSource(mysqlSource);
|
||||||
|
hikariConfig.setPoolName(poolName);
|
||||||
|
hikariConfig.setMinimumIdle(1);
|
||||||
|
hikariConfig.setMaximumPoolSize(MAX_POOL_SIZE);
|
||||||
|
hikariConfig.setConnectionTestQuery("SELECT 1");
|
||||||
|
hikariConfig.setConnectionTimeout(1000);
|
||||||
|
hikariConfig.setLeakDetectionThreshold(2000);
|
||||||
|
hikariConfig.setMaxLifetime(hikariMaxLifetime);
|
||||||
|
// Set HikariCP pool
|
||||||
|
// Replace source with hds
|
||||||
|
source = new HikariDataSource(hikariConfig);
|
||||||
|
} catch (ClassNotFoundException ignored) {
|
||||||
|
logger.debug("HikariCP isn't in classpath for '{}'", poolName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return source.getConnection();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
package pro.gravit.launchserver.auth;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.postgresql.ds.PGSimpleDataSource;
|
||||||
|
import pro.gravit.utils.helper.VerifyHelper;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
import static java.util.concurrent.TimeUnit.MINUTES;
|
||||||
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
|
||||||
|
public final class PostgreSQLSourceConfig implements AutoCloseable, SQLSourceConfig {
|
||||||
|
public static final int TIMEOUT = VerifyHelper.verifyInt(
|
||||||
|
Integer.parseUnsignedInt(System.getProperty("launcher.postgresql.idleTimeout", Integer.toString(5000))),
|
||||||
|
VerifyHelper.POSITIVE, "launcher.postgresql.idleTimeout can't be <= 5000");
|
||||||
|
private static final int MAX_POOL_SIZE = VerifyHelper.verifyInt(
|
||||||
|
Integer.parseUnsignedInt(System.getProperty("launcher.postgresql.maxPoolSize", Integer.toString(3))),
|
||||||
|
VerifyHelper.POSITIVE, "launcher.postgresql.maxPoolSize can't be <= 0");
|
||||||
|
private transient final Logger logger = LogManager.getLogger();
|
||||||
|
// Instance
|
||||||
|
private String poolName;
|
||||||
|
// Config
|
||||||
|
private String[] addresses;
|
||||||
|
private int[] ports;
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
private String database;
|
||||||
|
|
||||||
|
private long hikariMaxLifetime = MINUTES.toMillis(30); // 30 minutes
|
||||||
|
|
||||||
|
// Cache
|
||||||
|
private transient DataSource source;
|
||||||
|
private transient boolean hikari;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void close() {
|
||||||
|
if (hikari) { // Shutdown hikari pool
|
||||||
|
((HikariDataSource) source).close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized Connection getConnection() throws SQLException {
|
||||||
|
if (source == null) { // New data source
|
||||||
|
PGSimpleDataSource postgresqlSource = new PGSimpleDataSource();
|
||||||
|
|
||||||
|
// Set credentials
|
||||||
|
postgresqlSource.setServerNames(addresses);
|
||||||
|
postgresqlSource.setPortNumbers(ports);
|
||||||
|
postgresqlSource.setUser(username);
|
||||||
|
postgresqlSource.setPassword(password);
|
||||||
|
postgresqlSource.setDatabaseName(database);
|
||||||
|
|
||||||
|
// Try using HikariCP
|
||||||
|
source = postgresqlSource;
|
||||||
|
|
||||||
|
//noinspection Duplicates
|
||||||
|
try {
|
||||||
|
Class.forName("com.zaxxer.hikari.HikariDataSource");
|
||||||
|
hikari = true; // Used for shutdown. Not instanceof because of possible classpath error
|
||||||
|
|
||||||
|
// Set HikariCP pool
|
||||||
|
HikariDataSource hikariSource = new HikariDataSource();
|
||||||
|
hikariSource.setDataSource(source);
|
||||||
|
|
||||||
|
// Set pool settings
|
||||||
|
hikariSource.setPoolName(poolName);
|
||||||
|
hikariSource.setMinimumIdle(0);
|
||||||
|
hikariSource.setMaximumPoolSize(MAX_POOL_SIZE);
|
||||||
|
hikariSource.setIdleTimeout(SECONDS.toMillis(TIMEOUT));
|
||||||
|
hikariSource.setMaxLifetime(hikariMaxLifetime);
|
||||||
|
|
||||||
|
// Replace source with hds
|
||||||
|
source = hikariSource;
|
||||||
|
logger.info("HikariCP pooling enabled for '{}'", poolName);
|
||||||
|
} catch (ClassNotFoundException ignored) {
|
||||||
|
logger.warn("HikariCP isn't in classpath for '{}'", poolName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return source.getConnection();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package pro.gravit.launchserver.auth;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
|
||||||
|
public interface SQLSourceConfig {
|
||||||
|
Connection getConnection() throws SQLException;
|
||||||
|
|
||||||
|
void close();
|
||||||
|
}
|
|
@ -0,0 +1,420 @@
|
||||||
|
package pro.gravit.launchserver.auth.core;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.ExpiredJwtException;
|
||||||
|
import io.jsonwebtoken.JwtException;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launcher.base.ClientPermissions;
|
||||||
|
import pro.gravit.launcher.base.request.auth.AuthRequest;
|
||||||
|
import pro.gravit.launcher.base.request.auth.password.AuthPlainPassword;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.auth.AuthException;
|
||||||
|
import pro.gravit.launchserver.auth.AuthProviderPair;
|
||||||
|
import pro.gravit.launchserver.auth.MySQLSourceConfig;
|
||||||
|
import pro.gravit.launchserver.auth.SQLSourceConfig;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportSudo;
|
||||||
|
import pro.gravit.launchserver.auth.password.PasswordVerifier;
|
||||||
|
import pro.gravit.launchserver.helper.LegacySessionHelper;
|
||||||
|
import pro.gravit.launchserver.manangers.AuthManager;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.response.auth.AuthResponse;
|
||||||
|
import pro.gravit.utils.helper.SecurityHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static java.util.concurrent.TimeUnit.HOURS;
|
||||||
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
|
||||||
|
public abstract class AbstractSQLCoreProvider extends AuthCoreProvider implements AuthSupportSudo {
|
||||||
|
public final transient Logger logger = LogManager.getLogger();
|
||||||
|
public long expireSeconds = HOURS.toSeconds(1);
|
||||||
|
public String uuidColumn;
|
||||||
|
public String usernameColumn;
|
||||||
|
public String accessTokenColumn;
|
||||||
|
public String passwordColumn;
|
||||||
|
public String serverIDColumn;
|
||||||
|
public String table;
|
||||||
|
public String permissionsTable;
|
||||||
|
public String permissionsPermissionColumn;
|
||||||
|
public String permissionsUUIDColumn;
|
||||||
|
|
||||||
|
public String rolesTable;
|
||||||
|
public String rolesNameColumn;
|
||||||
|
public String rolesUUIDColumn;
|
||||||
|
|
||||||
|
public PasswordVerifier passwordVerifier;
|
||||||
|
public String customQueryByUUIDSQL;
|
||||||
|
public String customQueryByUsernameSQL;
|
||||||
|
public String customQueryByLoginSQL;
|
||||||
|
public String customQueryPermissionsByUUIDSQL;
|
||||||
|
public String customQueryRolesByUserUUID;
|
||||||
|
public String customUpdateAuthSQL;
|
||||||
|
public String customUpdateServerIdSQL;
|
||||||
|
// Prepared SQL queries
|
||||||
|
public transient String queryByUUIDSQL;
|
||||||
|
public transient String queryByUsernameSQL;
|
||||||
|
public transient String queryByLoginSQL;
|
||||||
|
public transient String queryPermissionsByUUIDSQL;
|
||||||
|
public transient String queryRolesByUserUUID;
|
||||||
|
|
||||||
|
public transient String updateAuthSQL;
|
||||||
|
public transient String updateServerIDSQL;
|
||||||
|
|
||||||
|
public abstract SQLSourceConfig getSQLConfig();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUserByUsername(String username) {
|
||||||
|
try {
|
||||||
|
return queryUser(queryByUsernameSQL, username);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("SQL error", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUserByUUID(UUID uuid) {
|
||||||
|
try {
|
||||||
|
return queryUser(queryByUUIDSQL, uuid.toString());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("SQL error", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUserByLogin(String login) {
|
||||||
|
try {
|
||||||
|
return queryUser(queryByLoginSQL, login);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("SQL error", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserSession getUserSessionByOAuthAccessToken(String accessToken) throws OAuthAccessTokenExpired {
|
||||||
|
try {
|
||||||
|
var info = LegacySessionHelper.getJwtInfoFromAccessToken(accessToken, server.keyAgreementManager.ecdsaPublicKey);
|
||||||
|
var user = (SQLUser) getUserByUUID(info.uuid());
|
||||||
|
if (user == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return createSession(user);
|
||||||
|
} catch (ExpiredJwtException e) {
|
||||||
|
throw new OAuthAccessTokenExpired();
|
||||||
|
} catch (JwtException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthManager.AuthReport refreshAccessToken(String refreshToken, AuthResponse.AuthContext context) {
|
||||||
|
String[] parts = refreshToken.split("\\.");
|
||||||
|
if (parts.length != 2) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String username = parts[0];
|
||||||
|
String token = parts[1];
|
||||||
|
var user = (SQLUser) getUserByUsername(username);
|
||||||
|
if (user == null || user.password == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var realToken = LegacySessionHelper.makeRefreshTokenFromPassword(username, user.password, server.keyAgreementManager.legacySalt);
|
||||||
|
if (!token.equals(realToken)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var accessToken = LegacySessionHelper.makeAccessJwtTokenFromString(user, LocalDateTime.now(Clock.systemUTC()).plusSeconds(expireSeconds), server.keyAgreementManager.ecdsaPrivateKey);
|
||||||
|
return new AuthManager.AuthReport(null, accessToken, refreshToken, SECONDS.toMillis(expireSeconds), createSession(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthManager.AuthReport authorize(String login, AuthResponse.AuthContext context, AuthRequest.AuthPasswordInterface password, boolean minecraftAccess) throws IOException {
|
||||||
|
SQLUser user = (SQLUser) getUserByLogin(login);
|
||||||
|
if (user == null) {
|
||||||
|
throw AuthException.userNotFound();
|
||||||
|
}
|
||||||
|
AuthPlainPassword plainPassword = (AuthPlainPassword) password;
|
||||||
|
if (plainPassword == null) {
|
||||||
|
throw AuthException.wrongPassword();
|
||||||
|
}
|
||||||
|
if (!passwordVerifier.check(user.password, plainPassword.password)) {
|
||||||
|
throw AuthException.wrongPassword();
|
||||||
|
}
|
||||||
|
SQLUserSession session = createSession(user);
|
||||||
|
var accessToken = LegacySessionHelper.makeAccessJwtTokenFromString(user, LocalDateTime.now(Clock.systemUTC()).plusSeconds(expireSeconds), server.keyAgreementManager.ecdsaPrivateKey);
|
||||||
|
var refreshToken = user.username.concat(".").concat(LegacySessionHelper.makeRefreshTokenFromPassword(user.username, user.password, server.keyAgreementManager.legacySalt));
|
||||||
|
if (minecraftAccess) {
|
||||||
|
String minecraftAccessToken = SecurityHelper.randomStringToken();
|
||||||
|
updateAuth(user, minecraftAccessToken);
|
||||||
|
return AuthManager.AuthReport.ofOAuthWithMinecraft(minecraftAccessToken, accessToken, refreshToken, SECONDS.toMillis(expireSeconds), session);
|
||||||
|
} else {
|
||||||
|
return AuthManager.AuthReport.ofOAuth(accessToken, refreshToken, SECONDS.toMillis(expireSeconds), session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthManager.AuthReport sudo(User user, boolean shadow) throws IOException {
|
||||||
|
SQLUser sqlUser = (SQLUser) user;
|
||||||
|
SQLUserSession session = createSession(sqlUser);
|
||||||
|
var accessToken = LegacySessionHelper.makeAccessJwtTokenFromString(sqlUser, LocalDateTime.now(Clock.systemUTC()).plusSeconds(expireSeconds), server.keyAgreementManager.ecdsaPrivateKey);
|
||||||
|
var refreshToken = sqlUser.username.concat(".").concat(LegacySessionHelper.makeRefreshTokenFromPassword(sqlUser.username, sqlUser.password, server.keyAgreementManager.legacySalt));
|
||||||
|
String minecraftAccessToken = SecurityHelper.randomStringToken();
|
||||||
|
updateAuth(user, minecraftAccessToken);
|
||||||
|
return AuthManager.AuthReport.ofOAuthWithMinecraft(minecraftAccessToken, accessToken, refreshToken, SECONDS.toMillis(expireSeconds), session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User checkServer(Client client, String username, String serverID) {
|
||||||
|
SQLUser user = (SQLUser) getUserByUsername(username);
|
||||||
|
if (user == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (user.getUsername().equals(username) && user.getServerId().equals(serverID)) {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean joinServer(Client client, String username, UUID uuid, String accessToken, String serverID) throws IOException {
|
||||||
|
SQLUser user = (SQLUser) client.getUser();
|
||||||
|
if (user == null) return false;
|
||||||
|
return (uuid == null ? user.getUsername().equals(username) : user.getUUID().equals(uuid)) && user.getAccessToken().equals(accessToken) && updateServerID(user, serverID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(LaunchServer server, AuthProviderPair pair) {
|
||||||
|
super.init(server, pair);
|
||||||
|
if (getSQLConfig() == null) logger.error("SQLHolder cannot be null");
|
||||||
|
if (uuidColumn == null) logger.error("uuidColumn cannot be null");
|
||||||
|
if (usernameColumn == null) logger.error("usernameColumn cannot be null");
|
||||||
|
if (accessTokenColumn == null) logger.error("accessTokenColumn cannot be null");
|
||||||
|
if (serverIDColumn == null) logger.error("serverIDColumn cannot be null");
|
||||||
|
if (table == null) logger.error("table cannot be null");
|
||||||
|
// Prepare SQL queries
|
||||||
|
String userInfoCols = makeUserCols();
|
||||||
|
queryByUUIDSQL = customQueryByUUIDSQL != null ? customQueryByUUIDSQL :
|
||||||
|
"SELECT %s FROM %s WHERE %s=? LIMIT 1".formatted(userInfoCols, table, uuidColumn);
|
||||||
|
queryByUsernameSQL = customQueryByUsernameSQL != null ? customQueryByUsernameSQL :
|
||||||
|
"SELECT %s FROM %s WHERE %s=? LIMIT 1".formatted(userInfoCols, table, usernameColumn);
|
||||||
|
queryByLoginSQL = customQueryByLoginSQL != null ? customQueryByLoginSQL : queryByUsernameSQL;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
updateAuthSQL = customUpdateAuthSQL != null ? customUpdateAuthSQL :
|
||||||
|
"UPDATE %s SET %s=?, %s=NULL WHERE %s=?".formatted(table, accessTokenColumn, serverIDColumn, uuidColumn);
|
||||||
|
updateServerIDSQL = customUpdateServerIdSQL != null ? customUpdateServerIdSQL :
|
||||||
|
"UPDATE %s SET %s=? WHERE %s=?".formatted(table, serverIDColumn, uuidColumn);
|
||||||
|
if (isEnabledPermissions()) {
|
||||||
|
if(isEnabledRoles()) {
|
||||||
|
queryPermissionsByUUIDSQL = customQueryPermissionsByUUIDSQL != null ? customQueryPermissionsByUUIDSQL :
|
||||||
|
"WITH RECURSIVE req AS (\n" +
|
||||||
|
"SELECT p."+permissionsPermissionColumn+" FROM "+permissionsTable+" p WHERE p."+permissionsUUIDColumn+" = ?\n" +
|
||||||
|
"UNION ALL\n" +
|
||||||
|
"SELECT p."+permissionsPermissionColumn+" FROM "+permissionsTable+" p\n" +
|
||||||
|
"INNER JOIN "+rolesTable+" r ON p."+permissionsUUIDColumn+" = r."+rolesUUIDColumn+"\n" +
|
||||||
|
"INNER JOIN req ON r."+rolesUUIDColumn+"=substring(req."+permissionsPermissionColumn+" from 6) or r.name=substring(req."+permissionsPermissionColumn+" from 6)\n" +
|
||||||
|
") SELECT * FROM req";
|
||||||
|
queryRolesByUserUUID = customQueryRolesByUserUUID != null ? customQueryRolesByUserUUID : "SELECT r." + rolesNameColumn + " FROM " + rolesTable + " r\n" +
|
||||||
|
"INNER JOIN " + permissionsTable + " pr ON r." + rolesUUIDColumn + "=substring(pr." + permissionsPermissionColumn + " from 6) or r." + rolesNameColumn + "=substring(pr." + permissionsPermissionColumn + " from 6)\n" +
|
||||||
|
"WHERE pr." + permissionsUUIDColumn + " = ?";
|
||||||
|
} else {
|
||||||
|
queryPermissionsByUUIDSQL = customQueryPermissionsByUUIDSQL != null ? customQueryPermissionsByUUIDSQL :
|
||||||
|
"SELECT (%s) FROM %s WHERE %s=?".formatted(permissionsPermissionColumn, permissionsTable, permissionsUUIDColumn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String makeUserCols() {
|
||||||
|
return "%s, %s, %s, %s, %s".formatted(uuidColumn, usernameColumn, accessTokenColumn, serverIDColumn, passwordColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void updateAuth(User user, String accessToken) throws IOException {
|
||||||
|
try (Connection c = getSQLConfig().getConnection()) {
|
||||||
|
SQLUser SQLUser = (SQLUser) user;
|
||||||
|
SQLUser.accessToken = accessToken;
|
||||||
|
PreparedStatement s = c.prepareStatement(updateAuthSQL);
|
||||||
|
s.setString(1, accessToken);
|
||||||
|
s.setString(2, user.getUUID().toString());
|
||||||
|
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||||
|
s.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean updateServerID(User user, String serverID) throws IOException {
|
||||||
|
try (Connection c = getSQLConfig().getConnection()) {
|
||||||
|
SQLUser SQLUser = (SQLUser) user;
|
||||||
|
SQLUser.serverId = serverID;
|
||||||
|
PreparedStatement s = c.prepareStatement(updateServerIDSQL);
|
||||||
|
s.setString(1, serverID);
|
||||||
|
s.setString(2, user.getUUID().toString());
|
||||||
|
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||||
|
return s.executeUpdate() > 0;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
getSQLConfig().close();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SQLUser constructUser(ResultSet set) throws SQLException {
|
||||||
|
return set.next() ? new SQLUser(UUID.fromString(set.getString(uuidColumn)), set.getString(usernameColumn),
|
||||||
|
set.getString(accessTokenColumn), set.getString(serverIDColumn), set.getString(passwordColumn)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientPermissions requestPermissions (String uuid) throws SQLException
|
||||||
|
{
|
||||||
|
return new ClientPermissions(isEnabledRoles() ? queryRolesNames(queryRolesByUserUUID,uuid) : new ArrayList<>(),
|
||||||
|
isEnabledPermissions() ? queryPermissions(queryPermissionsByUUIDSQL,uuid) : new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private SQLUser queryUser(String sql, String value) throws SQLException {
|
||||||
|
SQLUser user;
|
||||||
|
try (Connection c = getSQLConfig().getConnection()) {
|
||||||
|
PreparedStatement s = c.prepareStatement(sql);
|
||||||
|
s.setString(1, value);
|
||||||
|
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||||
|
try (ResultSet set = s.executeQuery()) {
|
||||||
|
user = constructUser(set);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(user != null) {
|
||||||
|
user.permissions = requestPermissions(user.uuid.toString());
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> queryPermissions(String sql, String value) throws SQLException {
|
||||||
|
try (Connection c = getSQLConfig().getConnection()) {
|
||||||
|
PreparedStatement s = c.prepareStatement(sql);
|
||||||
|
s.setString(1, value);
|
||||||
|
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||||
|
ResultSet set = s.executeQuery();
|
||||||
|
List<String> perms = new ArrayList<>();
|
||||||
|
while (set.next())
|
||||||
|
perms.add(set.getString(permissionsPermissionColumn));
|
||||||
|
return perms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected SQLUserSession createSession(SQLUser user) {
|
||||||
|
return new SQLUserSession(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabledPermissions() {
|
||||||
|
return permissionsPermissionColumn != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEnabledRoles() {
|
||||||
|
return rolesNameColumn != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> queryRolesNames(String sql, String value) throws SQLException {
|
||||||
|
try (Connection c = getSQLConfig().getConnection()) {
|
||||||
|
PreparedStatement s = c.prepareStatement(sql);
|
||||||
|
s.setString(1, value);
|
||||||
|
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||||
|
ResultSet set = s.executeQuery();
|
||||||
|
List<String> perms = new ArrayList<>();
|
||||||
|
while (set.next())
|
||||||
|
perms.add(set.getString(rolesNameColumn));
|
||||||
|
return perms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SQLUser implements User {
|
||||||
|
protected final UUID uuid;
|
||||||
|
protected final String username;
|
||||||
|
protected String accessToken;
|
||||||
|
protected String serverId;
|
||||||
|
protected final String password;
|
||||||
|
protected ClientPermissions permissions;
|
||||||
|
|
||||||
|
public SQLUser(UUID uuid, String username, String accessToken, String serverId, String password) {
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.username = username;
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
this.serverId = serverId;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UUID getUUID() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getServerId() {
|
||||||
|
return serverId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAccessToken() {
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientPermissions getPermissions() {
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SQLUser{" +
|
||||||
|
"uuid=" + uuid +
|
||||||
|
", username='" + username + '\'' +
|
||||||
|
", permissions=" + permissions +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SQLUserSession implements UserSession {
|
||||||
|
private final SQLUser user;
|
||||||
|
private final String id;
|
||||||
|
|
||||||
|
public SQLUserSession(SQLUser user) {
|
||||||
|
this.user = user;
|
||||||
|
this.id = user.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getID() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMinecraftAccessToken() {
|
||||||
|
return user.getAccessToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getExpireIn() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,414 @@
|
||||||
|
package pro.gravit.launchserver.auth.core;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launcher.base.Launcher;
|
||||||
|
import pro.gravit.launcher.base.events.RequestEvent;
|
||||||
|
import pro.gravit.launcher.base.events.request.AuthRequestEvent;
|
||||||
|
import pro.gravit.launcher.base.events.request.GetAvailabilityAuthRequestEvent;
|
||||||
|
import pro.gravit.launcher.base.profiles.PlayerProfile;
|
||||||
|
import pro.gravit.launcher.base.request.auth.AuthRequest;
|
||||||
|
import pro.gravit.launcher.base.request.auth.details.AuthPasswordDetails;
|
||||||
|
import pro.gravit.launcher.base.request.auth.password.AuthPlainPassword;
|
||||||
|
import pro.gravit.launcher.base.request.secure.HardwareReportRequest;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.Reconfigurable;
|
||||||
|
import pro.gravit.launchserver.auth.AuthException;
|
||||||
|
import pro.gravit.launchserver.auth.AuthProviderPair;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.UserHardware;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportGetAllUsers;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportHardware;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportRegistration;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportSudo;
|
||||||
|
import pro.gravit.launchserver.auth.core.openid.OpenIDAuthCoreProvider;
|
||||||
|
import pro.gravit.launchserver.manangers.AuthManager;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.response.auth.AuthResponse;
|
||||||
|
import pro.gravit.utils.ProviderMap;
|
||||||
|
import pro.gravit.utils.command.Command;
|
||||||
|
import pro.gravit.utils.command.SubCommand;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/*
|
||||||
|
All-In-One provider
|
||||||
|
*/
|
||||||
|
public abstract class AuthCoreProvider implements AutoCloseable, Reconfigurable {
|
||||||
|
public static final ProviderMap<AuthCoreProvider> providers = new ProviderMap<>("AuthCoreProvider");
|
||||||
|
private static final Logger logger = LogManager.getLogger();
|
||||||
|
private static boolean registredProviders = false;
|
||||||
|
protected transient LaunchServer server;
|
||||||
|
protected transient AuthProviderPair pair;
|
||||||
|
|
||||||
|
public static void registerProviders() {
|
||||||
|
if (!registredProviders) {
|
||||||
|
providers.register("reject", RejectAuthCoreProvider.class);
|
||||||
|
providers.register("mysql", MySQLCoreProvider.class);
|
||||||
|
providers.register("postgresql", PostgresSQLCoreProvider.class);
|
||||||
|
providers.register("memory", MemoryAuthCoreProvider.class);
|
||||||
|
providers.register("merge", MergeAuthCoreProvider.class);
|
||||||
|
providers.register("openid", OpenIDAuthCoreProvider.class);
|
||||||
|
providers.register("sql", SQLCoreProvider.class);
|
||||||
|
registredProviders = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract User getUserByUsername(String username);
|
||||||
|
|
||||||
|
public User getUserByLogin(String login) {
|
||||||
|
return getUserByUsername(login);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract User getUserByUUID(UUID uuid);
|
||||||
|
|
||||||
|
public abstract UserSession getUserSessionByOAuthAccessToken(String accessToken) throws OAuthAccessTokenExpired;
|
||||||
|
|
||||||
|
public abstract AuthManager.AuthReport refreshAccessToken(String refreshToken, AuthResponse.AuthContext context /* may be null */);
|
||||||
|
|
||||||
|
public void verifyAuth(AuthResponse.AuthContext context) throws AuthException {
|
||||||
|
// None
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract AuthManager.AuthReport authorize(String login, AuthResponse.AuthContext context /* may be null */, AuthRequest.AuthPasswordInterface password /* may be null */, boolean minecraftAccess) throws IOException;
|
||||||
|
|
||||||
|
public AuthManager.AuthReport authorize(User user, AuthResponse.AuthContext context /* may be null */, AuthRequest.AuthPasswordInterface password /* may be null */, boolean minecraftAccess) throws IOException {
|
||||||
|
return authorize(user.getUsername(), context, password, minecraftAccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init(LaunchServer server, AuthProviderPair pair) {
|
||||||
|
this.server = server;
|
||||||
|
this.pair = pair;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<GetAvailabilityAuthRequestEvent.AuthAvailabilityDetails> getDetails(Client client) {
|
||||||
|
return List.of(new AuthPasswordDetails());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Command> getCommands() {
|
||||||
|
Map<String, Command> map = defaultCommandsMap();
|
||||||
|
map.put("auth", new SubCommand("[login] (json/plain password data)", "Test auth") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
verifyArgs(args, 1);
|
||||||
|
AuthRequest.AuthPasswordInterface password = null;
|
||||||
|
if (args.length > 1) {
|
||||||
|
if (args[1].startsWith("{")) {
|
||||||
|
password = Launcher.gsonManager.gson.fromJson(args[1], AuthRequest.AuthPasswordInterface.class);
|
||||||
|
} else {
|
||||||
|
password = new AuthPlainPassword(args[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var report = authorize(args[0], null, password, false);
|
||||||
|
if (report.isUsingOAuth()) {
|
||||||
|
logger.info("OAuth: AccessToken: {} RefreshToken: {} MinecraftAccessToken: {}", report.oauthAccessToken(), report.oauthRefreshToken(), report.minecraftAccessToken());
|
||||||
|
if (report.session() != null) {
|
||||||
|
logger.info("UserSession: id {} expire {} user {}", report.session().getID(), report.session().getExpireIn(), report.session().getUser() == null ? "null" : "found");
|
||||||
|
logger.info(report.session().toString());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info("Basic: MinecraftAccessToken: {}", report.minecraftAccessToken());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.put("getuserbyusername", new SubCommand("[username]", "get user by username") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
verifyArgs(args, 1);
|
||||||
|
User user = getUserByUsername(args[0]);
|
||||||
|
if (user == null) {
|
||||||
|
logger.info("User {} not found", args[0]);
|
||||||
|
} else {
|
||||||
|
logger.info("User {}: {}", args[0], user.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.put("getuserbyuuid", new SubCommand("[uuid]", "get user by uuid") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
verifyArgs(args, 1);
|
||||||
|
User user = getUserByUUID(UUID.fromString(args[0]));
|
||||||
|
if (user == null) {
|
||||||
|
logger.info("User {} not found", args[0]);
|
||||||
|
} else {
|
||||||
|
logger.info("User {}: {}", args[0], user.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
{
|
||||||
|
var instance = isSupport(AuthSupportGetAllUsers.class);
|
||||||
|
if (instance != null) {
|
||||||
|
map.put("getallusers", new SubCommand("(limit)", "print all users information") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) {
|
||||||
|
int max = Integer.MAX_VALUE;
|
||||||
|
if (args.length > 0) max = Integer.parseInt(args[0]);
|
||||||
|
Iterable<User> users = instance.getAllUsers();
|
||||||
|
int counter = 0;
|
||||||
|
for (User u : users) {
|
||||||
|
logger.info("User {}", u.toString());
|
||||||
|
counter++;
|
||||||
|
if (counter == max) break;
|
||||||
|
}
|
||||||
|
logger.info("Found {} users", counter);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var instance = isSupport(AuthSupportHardware.class);
|
||||||
|
if (instance != null) {
|
||||||
|
map.put("gethardwarebyid", new SubCommand("[id]", "get hardware by id") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
verifyArgs(args, 1);
|
||||||
|
UserHardware hardware = instance.getHardwareInfoById(args[0]);
|
||||||
|
if (hardware == null) {
|
||||||
|
logger.info("UserHardware {} not found", args[0]);
|
||||||
|
} else {
|
||||||
|
logger.info("UserHardware: {}", hardware);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.put("gethardwarebydata", new SubCommand("[json data]", "fulltext search hardware by json data(slow)") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
verifyArgs(args, 1);
|
||||||
|
UserHardware hardware = instance.getHardwareInfoByData(Launcher.gsonManager.gson.fromJson(args[0], HardwareReportRequest.HardwareInfo.class));
|
||||||
|
if (hardware == null) {
|
||||||
|
logger.info("UserHardware {} not found", args[0]);
|
||||||
|
} else {
|
||||||
|
logger.info("UserHardware: {}", hardware);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.put("findmulti", new SubCommand("[hardware id]", "get all users in one hardware id") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
verifyArgs(args, 1);
|
||||||
|
UserHardware hardware = instance.getHardwareInfoById(args[0]);
|
||||||
|
if (hardware == null) {
|
||||||
|
logger.info("UserHardware {} not found", args[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Iterable<User> users = instance.getUsersByHardwareInfo(hardware);
|
||||||
|
for (User user : users) {
|
||||||
|
logger.info("User {}", user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.put("banhardware", new SubCommand("[hardware id]", "ban hardware by id") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
verifyArgs(args, 1);
|
||||||
|
UserHardware hardware = instance.getHardwareInfoById(args[0]);
|
||||||
|
if (hardware == null) {
|
||||||
|
logger.info("UserHardware {} not found", args[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
instance.banHardware(hardware);
|
||||||
|
logger.info("UserHardware {} banned", args[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.put("unbanhardware", new SubCommand("[hardware id]", "ban hardware by id") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
verifyArgs(args, 1);
|
||||||
|
UserHardware hardware = instance.getHardwareInfoById(args[0]);
|
||||||
|
if (hardware == null) {
|
||||||
|
logger.info("UserHardware {} not found", args[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
instance.unbanHardware(hardware);
|
||||||
|
logger.info("UserHardware {} unbanned", args[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
map.put("comparehardware", new SubCommand("[json data 1] [json data 2]", "compare hardware info") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
verifyArgs(args, 2);
|
||||||
|
HardwareReportRequest.HardwareInfo hardware1 = Launcher.gsonManager.gson.fromJson(args[0], HardwareReportRequest.HardwareInfo.class);
|
||||||
|
HardwareReportRequest.HardwareInfo hardware2 = Launcher.gsonManager.gson.fromJson(args[1], HardwareReportRequest.HardwareInfo.class);
|
||||||
|
AuthSupportHardware.HardwareInfoCompareResult result = instance.compareHardwareInfo(hardware1, hardware2);
|
||||||
|
if (result == null) {
|
||||||
|
logger.error("Method compareHardwareInfo return null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info("Compare result: {} Spoof: {} first {} second", result.compareLevel, result.firstSpoofingLevel, result.secondSpoofingLevel);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var instance = isSupport(AuthSupportRegistration.class);
|
||||||
|
if (instance != null) {
|
||||||
|
map.put("register", new SubCommand("[username] [email] [plain or json password] (json args)", "Register new user") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
verifyArgs(args, 2);
|
||||||
|
Map<String, String> map = null;
|
||||||
|
String username = args[0];
|
||||||
|
String email = args[1];
|
||||||
|
String plainPassword = args[2];
|
||||||
|
if (args.length > 3) {
|
||||||
|
Type typeOfMap = new TypeToken<Map<String, String>>() {
|
||||||
|
}.getType();
|
||||||
|
map = Launcher.gsonManager.gson.fromJson(args[2], typeOfMap);
|
||||||
|
}
|
||||||
|
AuthRequest.AuthPasswordInterface password;
|
||||||
|
if (plainPassword.startsWith("{")) {
|
||||||
|
password = Launcher.gsonManager.gson.fromJson(plainPassword, AuthRequest.AuthPasswordInterface.class);
|
||||||
|
} else {
|
||||||
|
password = new AuthPlainPassword(plainPassword);
|
||||||
|
}
|
||||||
|
User user = instance.registration(username, email, password, map);
|
||||||
|
logger.info("User '{}' registered", user.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var instance = isSupport(AuthSupportSudo.class);
|
||||||
|
if(instance != null) {
|
||||||
|
map.put("sudo", new SubCommand("[connectUUID] [username/uuid] [isShadow] (CLIENT/API)", "Authorize connectUUID as another user without password") {
|
||||||
|
@Override
|
||||||
|
public void invoke(String... args) throws Exception {
|
||||||
|
verifyArgs(args, 3);
|
||||||
|
UUID connectUUID = UUID.fromString(args[0]);
|
||||||
|
String login = args[1];
|
||||||
|
boolean isShadow = Boolean.parseBoolean(args[2]);
|
||||||
|
AuthResponse.ConnectTypes type;
|
||||||
|
if(args.length > 3) {
|
||||||
|
type = AuthResponse.ConnectTypes.valueOf(args[3]);
|
||||||
|
} else {
|
||||||
|
type = AuthResponse.ConnectTypes.CLIENT;
|
||||||
|
}
|
||||||
|
User user;
|
||||||
|
if(login.length() == 36) {
|
||||||
|
UUID uuid = UUID.fromString(login);
|
||||||
|
user = getUserByUUID(uuid);
|
||||||
|
} else {
|
||||||
|
user = getUserByUsername(login);
|
||||||
|
}
|
||||||
|
if(user == null) {
|
||||||
|
logger.error("User {} not found", login);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AtomicBoolean founded = new AtomicBoolean();
|
||||||
|
server.nettyServerSocketHandler.nettyServer.service.forEachActiveChannels((ch, fh) -> {
|
||||||
|
var client = fh.getClient();
|
||||||
|
if(client == null || !connectUUID.equals(fh.getConnectUUID())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info("Found connectUUID {} with IP {}", fh.getConnectUUID(), fh.context == null ? "null" : fh.context.ip);
|
||||||
|
var lock = server.config.netty.performance.disableThreadSafeClientObject ? null : client.writeLock();
|
||||||
|
if(lock != null) {
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var report = instance.sudo(user, isShadow);
|
||||||
|
User user1 = report.session().getUser();
|
||||||
|
server.authManager.internalAuth(client, type, pair, user1.getUsername(), user1.getUUID(), user1.getPermissions(), true);
|
||||||
|
client.sessionObject = report.session();
|
||||||
|
client.coreObject = report.session().getUser();
|
||||||
|
PlayerProfile playerProfile = server.authManager.getPlayerProfile(client);
|
||||||
|
AuthRequestEvent request = new AuthRequestEvent(user1.getPermissions(), playerProfile,
|
||||||
|
report.minecraftAccessToken(), null, null,
|
||||||
|
new AuthRequestEvent.OAuthRequestEvent(report.oauthAccessToken(), report.oauthRefreshToken(), report.oauthExpire()));
|
||||||
|
request.requestUUID = RequestEvent.eventUUID;
|
||||||
|
server.nettyServerSocketHandler.nettyServer.service.sendObject(ch, request);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.error("Sudo error", e);
|
||||||
|
} finally {
|
||||||
|
if(lock != null) {
|
||||||
|
lock.unlock();
|
||||||
|
}
|
||||||
|
founded.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if(!founded.get()) {
|
||||||
|
logger.error("ConnectUUID {} not found", connectUUID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract User checkServer(Client client, String username, String serverID) throws IOException;
|
||||||
|
|
||||||
|
public abstract boolean joinServer(Client client, String username, UUID uuid, String accessToken, String serverID) throws IOException;
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T isSupport(Class<T> clazz) {
|
||||||
|
if (clazz.isAssignableFrom(getClass())) return (T) this;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract void close();
|
||||||
|
|
||||||
|
public static class PasswordVerifyReport {
|
||||||
|
public static final PasswordVerifyReport REQUIRED_2FA = new PasswordVerifyReport(-1);
|
||||||
|
public static final PasswordVerifyReport FAILED = new PasswordVerifyReport(false);
|
||||||
|
public static final PasswordVerifyReport OK = new PasswordVerifyReport(true);
|
||||||
|
public final boolean success;
|
||||||
|
public final boolean needMoreFactors;
|
||||||
|
public final List<Integer> factors;
|
||||||
|
|
||||||
|
public PasswordVerifyReport(boolean success) {
|
||||||
|
this.success = success;
|
||||||
|
this.needMoreFactors = false;
|
||||||
|
this.factors = List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PasswordVerifyReport(AuthManager.AuthReport report) {
|
||||||
|
this.success = true;
|
||||||
|
this.needMoreFactors = false;
|
||||||
|
this.factors = List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
public PasswordVerifyReport(int nextFactor) {
|
||||||
|
this.success = false;
|
||||||
|
this.needMoreFactors = true;
|
||||||
|
this.factors = List.of(nextFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PasswordVerifyReport(List<Integer> factors) {
|
||||||
|
this.success = false;
|
||||||
|
this.needMoreFactors = false;
|
||||||
|
this.factors = Collections.unmodifiableList(factors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PasswordVerifyReport(boolean success, boolean needMoreFactors, List<Integer> factors) {
|
||||||
|
this.success = success;
|
||||||
|
this.needMoreFactors = needMoreFactors;
|
||||||
|
this.factors = factors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class OAuthAccessTokenExpired extends Exception {
|
||||||
|
public OAuthAccessTokenExpired() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthAccessTokenExpired(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public OAuthAccessTokenExpired(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,204 @@
|
||||||
|
package pro.gravit.launchserver.auth.core;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.ClientPermissions;
|
||||||
|
import pro.gravit.launcher.base.events.request.GetAvailabilityAuthRequestEvent;
|
||||||
|
import pro.gravit.launcher.base.request.auth.AuthRequest;
|
||||||
|
import pro.gravit.launcher.base.request.auth.details.AuthLoginOnlyDetails;
|
||||||
|
import pro.gravit.launchserver.auth.AuthException;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportSudo;
|
||||||
|
import pro.gravit.launchserver.manangers.AuthManager;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.response.auth.AuthResponse;
|
||||||
|
import pro.gravit.utils.helper.SecurityHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class MemoryAuthCoreProvider extends AuthCoreProvider implements AuthSupportSudo {
|
||||||
|
private transient final List<MemoryUser> memory = new ArrayList<>(16);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUserByUsername(String username) {
|
||||||
|
synchronized (memory) {
|
||||||
|
for (MemoryUser u : memory) {
|
||||||
|
if (u.username.equals(username)) {
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var result = new MemoryUser(username);
|
||||||
|
memory.add(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<GetAvailabilityAuthRequestEvent.AuthAvailabilityDetails> getDetails(Client client) {
|
||||||
|
return List.of(new AuthLoginOnlyDetails());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUserByUUID(UUID uuid) {
|
||||||
|
synchronized (memory) {
|
||||||
|
for (MemoryUser u : memory) {
|
||||||
|
if (u.uuid.equals(uuid)) {
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserSession getUserSessionByOAuthAccessToken(String accessToken) {
|
||||||
|
synchronized (memory) {
|
||||||
|
for (MemoryUser u : memory) {
|
||||||
|
if (u.accessToken.equals(accessToken)) {
|
||||||
|
return new MemoryUserSession(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthManager.AuthReport refreshAccessToken(String refreshToken, AuthResponse.AuthContext context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthManager.AuthReport authorize(String login, AuthResponse.AuthContext context, AuthRequest.AuthPasswordInterface password, boolean minecraftAccess) throws IOException {
|
||||||
|
if (login == null) {
|
||||||
|
throw AuthException.userNotFound();
|
||||||
|
}
|
||||||
|
MemoryUser user = null;
|
||||||
|
synchronized (memory) {
|
||||||
|
for (MemoryUser u : memory) {
|
||||||
|
if (u.username.equals(login)) {
|
||||||
|
user = u;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (user == null) {
|
||||||
|
user = new MemoryUser(login);
|
||||||
|
memory.add(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!minecraftAccess) {
|
||||||
|
return AuthManager.AuthReport.ofOAuth(user.accessToken, null, 0, new MemoryUserSession(user));
|
||||||
|
} else {
|
||||||
|
return AuthManager.AuthReport.ofOAuthWithMinecraft(user.accessToken, user.accessToken, null, 0, new MemoryUserSession(user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User checkServer(Client client, String username, String serverID) {
|
||||||
|
synchronized (memory) {
|
||||||
|
for (MemoryUser u : memory) {
|
||||||
|
if (u.username.equals(username)) {
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var result = new MemoryUser(username);
|
||||||
|
memory.add(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean joinServer(Client client, String username, UUID uuid, String accessToken, String serverID) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthManager.AuthReport sudo(User user, boolean shadow) throws IOException {
|
||||||
|
return authorize(user.getUsername(), null, null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MemoryUser implements User {
|
||||||
|
private final String username;
|
||||||
|
private final UUID uuid;
|
||||||
|
private String serverId;
|
||||||
|
private final String accessToken;
|
||||||
|
private final ClientPermissions permissions;
|
||||||
|
|
||||||
|
public MemoryUser(String username) {
|
||||||
|
this.username = username;
|
||||||
|
this.uuid = makeUuidFromUsername(username);
|
||||||
|
this.accessToken = SecurityHelper.randomStringToken();
|
||||||
|
this.permissions = new ClientPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UUID makeUuidFromUsername(String username) {
|
||||||
|
return UUID.nameUUIDFromBytes(username.getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UUID getUUID() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientPermissions getPermissions() {
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
MemoryUser that = (MemoryUser) o;
|
||||||
|
return uuid.equals(that.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MemoryUserSession implements UserSession {
|
||||||
|
private final String id;
|
||||||
|
private final MemoryUser user;
|
||||||
|
private final long expireIn;
|
||||||
|
|
||||||
|
public MemoryUserSession(MemoryUser user) {
|
||||||
|
this.id = SecurityHelper.randomStringToken();
|
||||||
|
this.user = user;
|
||||||
|
this.expireIn = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getID() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMinecraftAccessToken() {
|
||||||
|
return "IGNORED";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getExpireIn() {
|
||||||
|
return expireIn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package pro.gravit.launchserver.auth.core;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launcher.base.request.auth.AuthRequest;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.auth.AuthException;
|
||||||
|
import pro.gravit.launchserver.auth.AuthProviderPair;
|
||||||
|
import pro.gravit.launchserver.manangers.AuthManager;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.response.auth.AuthResponse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class MergeAuthCoreProvider extends AuthCoreProvider {
|
||||||
|
private transient final Logger logger = LogManager.getLogger(MergeAuthCoreProvider.class);
|
||||||
|
public List<String> list = new ArrayList<>();
|
||||||
|
private final transient List<AuthCoreProvider> providers = new ArrayList<>();
|
||||||
|
@Override
|
||||||
|
public User getUserByUsername(String username) {
|
||||||
|
for(var core : providers) {
|
||||||
|
var result = core.getUserByUsername(username);
|
||||||
|
if(result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUserByUUID(UUID uuid) {
|
||||||
|
for(var core : providers) {
|
||||||
|
var result = core.getUserByUUID(uuid);
|
||||||
|
if(result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserSession getUserSessionByOAuthAccessToken(String accessToken) throws OAuthAccessTokenExpired {
|
||||||
|
throw new OAuthAccessTokenExpired(); // Authorization not supported
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthManager.AuthReport refreshAccessToken(String refreshToken, AuthResponse.AuthContext context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthManager.AuthReport authorize(String login, AuthResponse.AuthContext context, AuthRequest.AuthPasswordInterface password, boolean minecraftAccess) throws IOException {
|
||||||
|
throw new AuthException("Authorization not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User checkServer(Client client, String username, String serverID) throws IOException {
|
||||||
|
for(var core : providers) {
|
||||||
|
var result = core.checkServer(client, username, serverID);
|
||||||
|
if(result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean joinServer(Client client, String username, UUID uuid, String accessToken, String serverID) {
|
||||||
|
return false; // Authorization not supported
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(LaunchServer server, AuthProviderPair pair1) {
|
||||||
|
for(var e : list) {
|
||||||
|
var pair = server.config.auth.get(e);
|
||||||
|
if(pair != null) {
|
||||||
|
providers.add(pair.core);
|
||||||
|
} else {
|
||||||
|
logger.warn("Provider {} not found", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// Providers closed automatically
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,354 @@
|
||||||
|
package pro.gravit.launchserver.auth.core;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.request.secure.HardwareReportRequest;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.auth.AuthProviderPair;
|
||||||
|
import pro.gravit.launchserver.auth.MySQLSourceConfig;
|
||||||
|
import pro.gravit.launchserver.auth.SQLSourceConfig;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.UserHardware;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportHardware;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.session.UserSessionSupportHardware;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class MySQLCoreProvider extends AbstractSQLCoreProvider implements AuthSupportHardware {
|
||||||
|
public MySQLSourceConfig mySQLHolder;
|
||||||
|
|
||||||
|
public String hardwareIdColumn;
|
||||||
|
public String tableHWID = "hwids";
|
||||||
|
public String tableHWIDLog = "hwidLog";
|
||||||
|
public double criticalCompareLevel = 1.0;
|
||||||
|
private transient String sqlFindHardwareByPublicKey;
|
||||||
|
private transient String sqlFindHardwareByData;
|
||||||
|
private transient String sqlFindHardwareById;
|
||||||
|
private transient String sqlCreateHardware;
|
||||||
|
private transient String sqlCreateHWIDLog;
|
||||||
|
private transient String sqlUpdateHardwarePublicKey;
|
||||||
|
private transient String sqlUpdateHardwareBanned;
|
||||||
|
private transient String sqlUpdateUsers;
|
||||||
|
private transient String sqlUsersByHwidId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLSourceConfig getSQLConfig() {
|
||||||
|
return mySQLHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(LaunchServer server, AuthProviderPair pair) {
|
||||||
|
super.init(server, pair);
|
||||||
|
logger.warn("Method 'mysql' deprecated and may be removed in future release. Please use new 'sql' method: https://gravitlauncher.com/auth");
|
||||||
|
String userInfoCols = makeUserCols();
|
||||||
|
String hardwareInfoCols = "id, hwDiskId, baseboardSerialNumber, displayId, bitness, totalMemory, logicalProcessors, physicalProcessors, processorMaxFreq, battery, id, graphicCard, banned, publicKey";
|
||||||
|
if (sqlFindHardwareByPublicKey == null)
|
||||||
|
sqlFindHardwareByPublicKey = "SELECT %s FROM %s WHERE `publicKey` = ?".formatted(hardwareInfoCols, tableHWID);
|
||||||
|
if (sqlFindHardwareById == null)
|
||||||
|
sqlFindHardwareById = "SELECT %s FROM %s WHERE `id` = ?".formatted(hardwareInfoCols, tableHWID);
|
||||||
|
if (sqlUsersByHwidId == null)
|
||||||
|
sqlUsersByHwidId = "SELECT %s FROM %s WHERE `%s` = ?".formatted(userInfoCols, table, hardwareIdColumn);
|
||||||
|
if (sqlFindHardwareByData == null)
|
||||||
|
sqlFindHardwareByData = "SELECT %s FROM %s".formatted(hardwareInfoCols, tableHWID);
|
||||||
|
if (sqlCreateHardware == null)
|
||||||
|
sqlCreateHardware = "INSERT INTO `%s` (`publickey`, `hwDiskId`, `baseboardSerialNumber`, `displayId`, `bitness`, `totalMemory`, `logicalProcessors`, `physicalProcessors`, `processorMaxFreq`, `graphicCard`, `battery`, `banned`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '0')".formatted(tableHWID);
|
||||||
|
if (sqlCreateHWIDLog == null)
|
||||||
|
sqlCreateHWIDLog = "INSERT INTO %s (`hwidId`, `newPublicKey`) VALUES (?, ?)".formatted(tableHWIDLog);
|
||||||
|
if (sqlUpdateHardwarePublicKey == null)
|
||||||
|
sqlUpdateHardwarePublicKey = "UPDATE %s SET `publicKey` = ? WHERE `id` = ?".formatted(tableHWID);
|
||||||
|
sqlUpdateHardwareBanned = "UPDATE %s SET `banned` = ? WHERE `id` = ?".formatted(tableHWID);
|
||||||
|
sqlUpdateUsers = "UPDATE %s SET `%s` = ? WHERE `%s` = ?".formatted(table, hardwareIdColumn, uuidColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String makeUserCols() {
|
||||||
|
return super.makeUserCols().concat(", ").concat(hardwareIdColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected MySQLUser constructUser(ResultSet set) throws SQLException {
|
||||||
|
return set.next() ? new MySQLUser(UUID.fromString(set.getString(uuidColumn)), set.getString(usernameColumn),
|
||||||
|
set.getString(accessTokenColumn), set.getString(serverIDColumn), set.getString(passwordColumn), set.getLong(hardwareIdColumn)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MySQLUserHardware fetchHardwareInfo(ResultSet set) throws SQLException, IOException {
|
||||||
|
HardwareReportRequest.HardwareInfo hardwareInfo = new HardwareReportRequest.HardwareInfo();
|
||||||
|
hardwareInfo.hwDiskId = set.getString("hwDiskId");
|
||||||
|
hardwareInfo.baseboardSerialNumber = set.getString("baseboardSerialNumber");
|
||||||
|
Blob displayId = set.getBlob("displayId");
|
||||||
|
hardwareInfo.displayId = displayId == null ? null : IOHelper.read(displayId.getBinaryStream());
|
||||||
|
hardwareInfo.bitness = set.getInt("bitness");
|
||||||
|
hardwareInfo.totalMemory = set.getLong("totalMemory");
|
||||||
|
hardwareInfo.logicalProcessors = set.getInt("logicalProcessors");
|
||||||
|
hardwareInfo.physicalProcessors = set.getInt("physicalProcessors");
|
||||||
|
hardwareInfo.processorMaxFreq = set.getLong("processorMaxFreq");
|
||||||
|
hardwareInfo.battery = set.getBoolean("battery");
|
||||||
|
hardwareInfo.graphicCard = set.getString("graphicCard");
|
||||||
|
Blob publicKey = set.getBlob("publicKey");
|
||||||
|
long id = set.getLong("id");
|
||||||
|
boolean banned = set.getBoolean("banned");
|
||||||
|
return new MySQLUserHardware(hardwareInfo, publicKey == null ? null : IOHelper.read(publicKey.getBinaryStream()), id, banned);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUserHardwareId(Connection connection, UUID uuid, long hwidId) throws SQLException {
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlUpdateUsers);
|
||||||
|
s.setLong(1, hwidId);
|
||||||
|
s.setString(2, uuid.toString());
|
||||||
|
s.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserHardware getHardwareInfoByPublicKey(byte[] publicKey) {
|
||||||
|
try (Connection connection = mySQLHolder.getConnection()) {
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlFindHardwareByPublicKey);
|
||||||
|
s.setBlob(1, new ByteArrayInputStream(publicKey));
|
||||||
|
try (ResultSet set = s.executeQuery()) {
|
||||||
|
if (set.next()) {
|
||||||
|
return fetchHardwareInfo(set);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | IOException throwables) {
|
||||||
|
logger.error("SQL Error", throwables);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserHardware getHardwareInfoByData(HardwareReportRequest.HardwareInfo info) {
|
||||||
|
try (Connection connection = mySQLHolder.getConnection()) {
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlFindHardwareByData);
|
||||||
|
try (ResultSet set = s.executeQuery()) {
|
||||||
|
while (set.next()) {
|
||||||
|
MySQLUserHardware hw = fetchHardwareInfo(set);
|
||||||
|
HardwareInfoCompareResult result = compareHardwareInfo(hw.getHardwareInfo(), info);
|
||||||
|
if (result.compareLevel > criticalCompareLevel) {
|
||||||
|
return hw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | IOException throwables) {
|
||||||
|
logger.error("SQL Error", throwables);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserHardware getHardwareInfoById(String id) {
|
||||||
|
try (Connection connection = mySQLHolder.getConnection()) {
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlFindHardwareById);
|
||||||
|
s.setLong(1, Long.parseLong(id));
|
||||||
|
try (ResultSet set = s.executeQuery()) {
|
||||||
|
if (set.next()) {
|
||||||
|
return fetchHardwareInfo(set);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException | IOException throwables) {
|
||||||
|
logger.error("SQL Error", throwables);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserHardware createHardwareInfo(HardwareReportRequest.HardwareInfo hardwareInfo, byte[] publicKey) {
|
||||||
|
try (Connection connection = mySQLHolder.getConnection()) {
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlCreateHardware, Statement.RETURN_GENERATED_KEYS);
|
||||||
|
s.setBlob(1, new ByteArrayInputStream(publicKey));
|
||||||
|
s.setString(2, hardwareInfo.hwDiskId);
|
||||||
|
s.setString(3, hardwareInfo.baseboardSerialNumber);
|
||||||
|
s.setBlob(4, hardwareInfo.displayId == null ? null : new ByteArrayInputStream(hardwareInfo.displayId));
|
||||||
|
s.setInt(5, hardwareInfo.bitness);
|
||||||
|
s.setLong(6, hardwareInfo.totalMemory);
|
||||||
|
s.setInt(7, hardwareInfo.logicalProcessors);
|
||||||
|
s.setInt(8, hardwareInfo.physicalProcessors);
|
||||||
|
s.setLong(9, hardwareInfo.processorMaxFreq);
|
||||||
|
s.setString(10, hardwareInfo.graphicCard);
|
||||||
|
s.setBoolean(11, hardwareInfo.battery);
|
||||||
|
s.executeUpdate();
|
||||||
|
try (ResultSet generatedKeys = s.getGeneratedKeys()) {
|
||||||
|
if (generatedKeys.next()) {
|
||||||
|
//writeHwidLog(connection, generatedKeys.getLong(1), publicKey);
|
||||||
|
long id = generatedKeys.getLong(1);
|
||||||
|
return new MySQLUserHardware(hardwareInfo, publicKey, id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch (SQLException throwables) {
|
||||||
|
logger.error("SQL Error", throwables);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectUserAndHardware(UserSession userSession, UserHardware hardware) {
|
||||||
|
SQLUserSession mySQLUserSession = (SQLUserSession) userSession;
|
||||||
|
MySQLUser mySQLUser = (MySQLUser) mySQLUserSession.getUser();
|
||||||
|
MySQLUserHardware mySQLUserHardware = (MySQLUserHardware) hardware;
|
||||||
|
if (mySQLUser.hwidId == mySQLUserHardware.id) return;
|
||||||
|
mySQLUser.hwidId = mySQLUserHardware.id;
|
||||||
|
try (Connection connection = mySQLHolder.getConnection()) {
|
||||||
|
setUserHardwareId(connection, mySQLUser.getUUID(), mySQLUserHardware.id);
|
||||||
|
} catch (SQLException throwables) {
|
||||||
|
logger.error("SQL Error", throwables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addPublicKeyToHardwareInfo(UserHardware hardware, byte[] publicKey) {
|
||||||
|
MySQLUserHardware mySQLUserHardware = (MySQLUserHardware) hardware;
|
||||||
|
mySQLUserHardware.publicKey = publicKey;
|
||||||
|
try (Connection connection = mySQLHolder.getConnection()) {
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlUpdateHardwarePublicKey);
|
||||||
|
s.setBlob(1, new ByteArrayInputStream(publicKey));
|
||||||
|
s.setLong(2, mySQLUserHardware.id);
|
||||||
|
s.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("SQL error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterable<User> getUsersByHardwareInfo(UserHardware hardware) {
|
||||||
|
List<User> users = new LinkedList<>();
|
||||||
|
try (Connection c = mySQLHolder.getConnection()) {
|
||||||
|
PreparedStatement s = c.prepareStatement(sqlUsersByHwidId);
|
||||||
|
s.setLong(1, Long.parseLong(hardware.getId()));
|
||||||
|
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||||
|
try (ResultSet set = s.executeQuery()) {
|
||||||
|
while (!set.isLast()) {
|
||||||
|
users.add(constructUser(set));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("SQL error", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void banHardware(UserHardware hardware) {
|
||||||
|
MySQLUserHardware mySQLUserHardware = (MySQLUserHardware) hardware;
|
||||||
|
mySQLUserHardware.banned = true;
|
||||||
|
try (Connection connection = mySQLHolder.getConnection()) {
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlUpdateHardwareBanned);
|
||||||
|
s.setBoolean(1, true);
|
||||||
|
s.setLong(2, mySQLUserHardware.id);
|
||||||
|
s.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("SQL Error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unbanHardware(UserHardware hardware) {
|
||||||
|
MySQLUserHardware mySQLUserHardware = (MySQLUserHardware) hardware;
|
||||||
|
mySQLUserHardware.banned = false;
|
||||||
|
try (Connection connection = mySQLHolder.getConnection()) {
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlUpdateHardwareBanned);
|
||||||
|
s.setBoolean(1, false);
|
||||||
|
s.setLong(2, mySQLUserHardware.id);
|
||||||
|
s.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("SQL error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SQLUserSession createSession(SQLUser user) {
|
||||||
|
return new MySQLUserSession(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class MySQLUserSession extends SQLUserSession implements UserSessionSupportHardware {
|
||||||
|
private transient MySQLUser mySQLUser;
|
||||||
|
protected transient MySQLUserHardware hardware;
|
||||||
|
|
||||||
|
public MySQLUserSession(SQLUser user) {
|
||||||
|
super(user);
|
||||||
|
mySQLUser = (MySQLUser) user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHardwareId() {
|
||||||
|
return mySQLUser.hwidId == 0 ? null : String.valueOf(mySQLUser.hwidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserHardware getHardware() {
|
||||||
|
if(hardware == null) {
|
||||||
|
hardware = (MySQLUserHardware) getHardwareInfoById(String.valueOf(mySQLUser.hwidId));
|
||||||
|
}
|
||||||
|
return hardware;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MySQLUserHardware implements UserHardware {
|
||||||
|
private final HardwareReportRequest.HardwareInfo hardwareInfo;
|
||||||
|
private final long id;
|
||||||
|
private byte[] publicKey;
|
||||||
|
private boolean banned;
|
||||||
|
|
||||||
|
public MySQLUserHardware(HardwareReportRequest.HardwareInfo hardwareInfo, byte[] publicKey, long id, boolean banned) {
|
||||||
|
this.hardwareInfo = hardwareInfo;
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
this.id = id;
|
||||||
|
this.banned = banned;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HardwareReportRequest.HardwareInfo getHardwareInfo() {
|
||||||
|
return hardwareInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getPublicKey() {
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return String.valueOf(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBanned() {
|
||||||
|
return banned;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "MySQLUserHardware{" +
|
||||||
|
"hardwareInfo=" + hardwareInfo +
|
||||||
|
", publicKey=" + (publicKey == null ? null : new String(Base64.getEncoder().encode(publicKey))) +
|
||||||
|
", id=" + id +
|
||||||
|
", banned=" + banned +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MySQLUser extends SQLUser {
|
||||||
|
protected long hwidId;
|
||||||
|
|
||||||
|
public MySQLUser(UUID uuid, String username, String accessToken, String serverId, String password, long hwidId) {
|
||||||
|
super(uuid, username, accessToken, serverId, password);
|
||||||
|
this.hwidId = hwidId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "MySQLUser{" +
|
||||||
|
"uuid=" + uuid +
|
||||||
|
", username='" + username + '\'' +
|
||||||
|
", permissions=" + permissions +
|
||||||
|
", hwidId=" + hwidId +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package pro.gravit.launchserver.auth.core;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.auth.AuthProviderPair;
|
||||||
|
import pro.gravit.launchserver.auth.PostgreSQLSourceConfig;
|
||||||
|
import pro.gravit.launchserver.auth.SQLSourceConfig;
|
||||||
|
|
||||||
|
public class PostgresSQLCoreProvider extends AbstractSQLCoreProvider {
|
||||||
|
public PostgreSQLSourceConfig postgresSQLHolder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLSourceConfig getSQLConfig() {
|
||||||
|
return postgresSQLHolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(LaunchServer server, AuthProviderPair pair) {
|
||||||
|
super.init(server, pair);
|
||||||
|
logger.warn("Method 'postgresql' deprecated and may be removed in future release. Please use new 'sql' method: https://gravitlauncher.com/auth");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package pro.gravit.launchserver.auth.core;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.request.auth.AuthRequest;
|
||||||
|
import pro.gravit.launchserver.auth.AuthException;
|
||||||
|
import pro.gravit.launchserver.manangers.AuthManager;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.response.auth.AuthResponse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class RejectAuthCoreProvider extends AuthCoreProvider {
|
||||||
|
@Override
|
||||||
|
public User getUserByUsername(String username) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUserByUUID(UUID uuid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserSession getUserSessionByOAuthAccessToken(String accessToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthManager.AuthReport refreshAccessToken(String refreshToken, AuthResponse.AuthContext context) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void verifyAuth(AuthResponse.AuthContext context) throws AuthException {
|
||||||
|
throw new AuthException("Please configure AuthCoreProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthManager.AuthReport authorize(String login, AuthResponse.AuthContext context, AuthRequest.AuthPasswordInterface password, boolean minecraftAccess) throws IOException {
|
||||||
|
throw new AuthException("Please configure AuthCoreProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User checkServer(Client client, String username, String serverID) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean joinServer(Client client, String username, UUID uuid, String accessToken, String serverID) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,391 @@
|
||||||
|
package pro.gravit.launchserver.auth.core;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.request.secure.HardwareReportRequest;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.auth.AuthProviderPair;
|
||||||
|
import pro.gravit.launchserver.auth.HikariSQLSourceConfig;
|
||||||
|
import pro.gravit.launchserver.auth.MySQLSourceConfig;
|
||||||
|
import pro.gravit.launchserver.auth.SQLSourceConfig;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.UserHardware;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportExtendedCheckServer;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportHardware;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.session.UserSessionSupportHardware;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.*;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SQLCoreProvider extends AbstractSQLCoreProvider implements AuthSupportHardware, AuthSupportExtendedCheckServer {
|
||||||
|
public HikariSQLSourceConfig holder;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
super.close();
|
||||||
|
holder.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SQLSourceConfig getSQLConfig() {
|
||||||
|
return holder;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String hardwareIdColumn;
|
||||||
|
public String tableHWID = "hwids";
|
||||||
|
public String tableHWIDLog = "hwidLog";
|
||||||
|
public double criticalCompareLevel = 1.0;
|
||||||
|
private transient String sqlFindHardwareByPublicKey;
|
||||||
|
private transient String sqlFindHardwareByData;
|
||||||
|
private transient String sqlFindHardwareById;
|
||||||
|
private transient String sqlCreateHardware;
|
||||||
|
private transient String sqlCreateHWIDLog;
|
||||||
|
private transient String sqlUpdateHardwarePublicKey;
|
||||||
|
private transient String sqlUpdateHardwareBanned;
|
||||||
|
private transient String sqlUpdateUsers;
|
||||||
|
private transient String sqlUsersByHwidId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(LaunchServer server, AuthProviderPair pair) {
|
||||||
|
holder.init();
|
||||||
|
super.init(server, pair);
|
||||||
|
String userInfoCols = makeUserCols();
|
||||||
|
String hardwareInfoCols = "id, hwDiskId, baseboardSerialNumber, displayId, bitness, totalMemory, logicalProcessors, physicalProcessors, processorMaxFreq, battery, id, graphicCard, banned, publicKey";
|
||||||
|
if (sqlFindHardwareByPublicKey == null)
|
||||||
|
sqlFindHardwareByPublicKey = "SELECT %s FROM %s WHERE publicKey = ?".formatted(hardwareInfoCols, tableHWID);
|
||||||
|
if (sqlFindHardwareById == null)
|
||||||
|
sqlFindHardwareById = "SELECT %s FROM %s WHERE id = ?".formatted(hardwareInfoCols, tableHWID);
|
||||||
|
if (sqlUsersByHwidId == null)
|
||||||
|
sqlUsersByHwidId = "SELECT %s FROM %s WHERE %s = ?".formatted(userInfoCols, table, hardwareIdColumn);
|
||||||
|
if (sqlFindHardwareByData == null)
|
||||||
|
sqlFindHardwareByData = "SELECT %s FROM %s".formatted(hardwareInfoCols, tableHWID);
|
||||||
|
if (sqlCreateHardware == null)
|
||||||
|
sqlCreateHardware = "INSERT INTO %s (publickey, hwDiskId, baseboardSerialNumber, displayId, bitness, totalMemory, logicalProcessors, physicalProcessors, processorMaxFreq, graphicCard, battery, banned) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '0')".formatted(tableHWID);
|
||||||
|
if (sqlCreateHWIDLog == null)
|
||||||
|
sqlCreateHWIDLog = "INSERT INTO %s (hwidId, newPublicKey) VALUES (?, ?)".formatted(tableHWIDLog);
|
||||||
|
if (sqlUpdateHardwarePublicKey == null)
|
||||||
|
sqlUpdateHardwarePublicKey = "UPDATE %s SET publicKey = ? WHERE id = ?".formatted(tableHWID);
|
||||||
|
sqlUpdateHardwareBanned = "UPDATE %s SET banned = ? WHERE id = ?".formatted(tableHWID);
|
||||||
|
sqlUpdateUsers = "UPDATE %s SET %s = ? WHERE %s = ?".formatted(table, hardwareIdColumn, uuidColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String makeUserCols() {
|
||||||
|
return super.makeUserCols().concat(", ").concat(hardwareIdColumn);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected SQLUser constructUser(ResultSet set) throws SQLException {
|
||||||
|
return set.next() ? new SQLUser(UUID.fromString(set.getString(uuidColumn)), set.getString(usernameColumn),
|
||||||
|
set.getString(accessTokenColumn), set.getString(serverIDColumn), set.getString(passwordColumn), set.getLong(hardwareIdColumn)) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SQLUserHardware fetchHardwareInfo(ResultSet set) throws SQLException {
|
||||||
|
HardwareReportRequest.HardwareInfo hardwareInfo = new HardwareReportRequest.HardwareInfo();
|
||||||
|
hardwareInfo.hwDiskId = set.getString("hwDiskId");
|
||||||
|
hardwareInfo.baseboardSerialNumber = set.getString("baseboardSerialNumber");
|
||||||
|
byte[] displayId = set.getBytes("displayId");
|
||||||
|
hardwareInfo.displayId = displayId == null ? null : displayId;
|
||||||
|
hardwareInfo.bitness = set.getInt("bitness");
|
||||||
|
hardwareInfo.totalMemory = set.getLong("totalMemory");
|
||||||
|
hardwareInfo.logicalProcessors = set.getInt("logicalProcessors");
|
||||||
|
hardwareInfo.physicalProcessors = set.getInt("physicalProcessors");
|
||||||
|
hardwareInfo.processorMaxFreq = set.getLong("processorMaxFreq");
|
||||||
|
hardwareInfo.battery = set.getBoolean("battery");
|
||||||
|
hardwareInfo.graphicCard = set.getString("graphicCard");
|
||||||
|
byte[] publicKey = set.getBytes("publicKey");
|
||||||
|
long id = set.getLong("id");
|
||||||
|
boolean banned = set.getBoolean("banned");
|
||||||
|
return new SQLUserHardware(hardwareInfo, publicKey, id, banned);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setUserHardwareId(Connection connection, UUID uuid, long hwidId) throws SQLException {
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlUpdateUsers);
|
||||||
|
s.setLong(1, hwidId);
|
||||||
|
s.setString(2, uuid.toString());
|
||||||
|
s.executeUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserHardware getHardwareInfoByPublicKey(byte[] publicKey) {
|
||||||
|
try (Connection connection = holder.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlFindHardwareByPublicKey);
|
||||||
|
s.setBytes(1, publicKey);
|
||||||
|
try (ResultSet set = s.executeQuery()) {
|
||||||
|
if (set.next()) {
|
||||||
|
connection.commit();
|
||||||
|
return fetchHardwareInfo(set);
|
||||||
|
} else {
|
||||||
|
connection.commit();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException throwables) {
|
||||||
|
logger.error("SQL Error", throwables);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserHardware getHardwareInfoByData(HardwareReportRequest.HardwareInfo info) {
|
||||||
|
try (Connection connection = holder.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlFindHardwareByData);
|
||||||
|
try (ResultSet set = s.executeQuery()) {
|
||||||
|
while (set.next()) {
|
||||||
|
SQLUserHardware hw = fetchHardwareInfo(set);
|
||||||
|
AuthSupportHardware.HardwareInfoCompareResult result = compareHardwareInfo(hw.getHardwareInfo(), info);
|
||||||
|
if (result.compareLevel > criticalCompareLevel) {
|
||||||
|
connection.commit();
|
||||||
|
return hw;
|
||||||
|
} else {
|
||||||
|
connection.commit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException throwables) {
|
||||||
|
logger.error("SQL Error", throwables);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserHardware getHardwareInfoById(String id) {
|
||||||
|
try (Connection connection = holder.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlFindHardwareById);
|
||||||
|
s.setLong(1, Long.parseLong(id));
|
||||||
|
try (ResultSet set = s.executeQuery()) {
|
||||||
|
if (set.next()) {
|
||||||
|
connection.commit();
|
||||||
|
return fetchHardwareInfo(set);
|
||||||
|
} else {
|
||||||
|
connection.commit();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException throwables) {
|
||||||
|
logger.error("SQL Error", throwables);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserHardware createHardwareInfo(HardwareReportRequest.HardwareInfo hardwareInfo, byte[] publicKey) {
|
||||||
|
try (Connection connection = holder.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlCreateHardware, Statement.RETURN_GENERATED_KEYS);
|
||||||
|
s.setBytes(1, publicKey);
|
||||||
|
s.setString(2, hardwareInfo.hwDiskId);
|
||||||
|
s.setString(3, hardwareInfo.baseboardSerialNumber);
|
||||||
|
s.setBytes(4, hardwareInfo.displayId == null ? null : hardwareInfo.displayId);
|
||||||
|
s.setInt(5, hardwareInfo.bitness);
|
||||||
|
s.setLong(6, hardwareInfo.totalMemory);
|
||||||
|
s.setInt(7, hardwareInfo.logicalProcessors);
|
||||||
|
s.setInt(8, hardwareInfo.physicalProcessors);
|
||||||
|
s.setLong(9, hardwareInfo.processorMaxFreq);
|
||||||
|
s.setString(10, hardwareInfo.graphicCard);
|
||||||
|
s.setBoolean(11, hardwareInfo.battery);
|
||||||
|
s.executeUpdate();
|
||||||
|
try (ResultSet generatedKeys = s.getGeneratedKeys()) {
|
||||||
|
if (generatedKeys.next()) {
|
||||||
|
//writeHwidLog(connection, generatedKeys.getLong(1), publicKey);
|
||||||
|
long id = generatedKeys.getLong(1);
|
||||||
|
connection.commit();
|
||||||
|
return new SQLUserHardware(hardwareInfo, publicKey, id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
connection.commit();
|
||||||
|
return null;
|
||||||
|
} catch (SQLException throwables) {
|
||||||
|
logger.error("SQL Error", throwables);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void connectUserAndHardware(UserSession userSession, UserHardware hardware) {
|
||||||
|
AbstractSQLCoreProvider.SQLUserSession SQLUserSession = (AbstractSQLCoreProvider.SQLUserSession) userSession;
|
||||||
|
SQLUser SQLUser = (SQLUser) SQLUserSession.getUser();
|
||||||
|
SQLUserHardware SQLUserHardware = (SQLUserHardware) hardware;
|
||||||
|
if (SQLUser.hwidId == SQLUserHardware.id) return;
|
||||||
|
SQLUser.hwidId = SQLUserHardware.id;
|
||||||
|
try (Connection connection = holder.getConnection()) {
|
||||||
|
setUserHardwareId(connection, SQLUser.getUUID(), SQLUserHardware.id);
|
||||||
|
} catch (SQLException throwables) {
|
||||||
|
logger.error("SQL Error", throwables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addPublicKeyToHardwareInfo(UserHardware hardware, byte[] publicKey) {
|
||||||
|
SQLUserHardware SQLUserHardware = (SQLUserHardware) hardware;
|
||||||
|
SQLUserHardware.publicKey = publicKey;
|
||||||
|
try (Connection connection = holder.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlUpdateHardwarePublicKey);
|
||||||
|
s.setBytes(1, publicKey);
|
||||||
|
s.setLong(2, SQLUserHardware.id);
|
||||||
|
s.executeUpdate();
|
||||||
|
connection.commit();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("SQL error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Iterable<User> getUsersByHardwareInfo(UserHardware hardware) {
|
||||||
|
List<User> users = new LinkedList<>();
|
||||||
|
try (Connection c = holder.getConnection()) {
|
||||||
|
c.setAutoCommit(false);
|
||||||
|
PreparedStatement s = c.prepareStatement(sqlUsersByHwidId);
|
||||||
|
s.setLong(1, Long.parseLong(hardware.getId()));
|
||||||
|
s.setQueryTimeout(MySQLSourceConfig.TIMEOUT);
|
||||||
|
try (ResultSet set = s.executeQuery()) {
|
||||||
|
while (!set.isLast()) {
|
||||||
|
users.add(constructUser(set));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.commit();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("SQL error", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void banHardware(UserHardware hardware) {
|
||||||
|
SQLUserHardware SQLUserHardware = (SQLUserHardware) hardware;
|
||||||
|
SQLUserHardware.banned = true;
|
||||||
|
try (Connection connection = holder.getConnection()) {
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlUpdateHardwareBanned);
|
||||||
|
s.setBoolean(1, true);
|
||||||
|
s.setLong(2, SQLUserHardware.id);
|
||||||
|
s.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("SQL Error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unbanHardware(UserHardware hardware) {
|
||||||
|
SQLUserHardware SQLUserHardware = (SQLUserHardware) hardware;
|
||||||
|
SQLUserHardware.banned = false;
|
||||||
|
try (Connection connection = holder.getConnection()) {
|
||||||
|
PreparedStatement s = connection.prepareStatement(sqlUpdateHardwareBanned);
|
||||||
|
s.setBoolean(1, false);
|
||||||
|
s.setLong(2, SQLUserHardware.id);
|
||||||
|
s.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("SQL error", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AbstractSQLCoreProvider.SQLUserSession createSession(AbstractSQLCoreProvider.SQLUser user) {
|
||||||
|
return new SQLUserSession(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserSession extendedCheckServer(Client client, String username, String serverID) {
|
||||||
|
AbstractSQLCoreProvider.SQLUser user = (AbstractSQLCoreProvider.SQLUser) getUserByUsername(username);
|
||||||
|
if (user == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (user.getUsername().equals(username) && user.getServerId().equals(serverID)) {
|
||||||
|
return createSession(user);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SQLUserSession extends AbstractSQLCoreProvider.SQLUserSession implements UserSessionSupportHardware {
|
||||||
|
private transient SQLUser SQLUser;
|
||||||
|
protected transient SQLUserHardware hardware;
|
||||||
|
|
||||||
|
public SQLUserSession(AbstractSQLCoreProvider.SQLUser user) {
|
||||||
|
super(user);
|
||||||
|
SQLUser = (SQLUser) user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHardwareId() {
|
||||||
|
return SQLUser.hwidId == 0 ? null : String.valueOf(SQLUser.hwidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserHardware getHardware() {
|
||||||
|
if(hardware == null) {
|
||||||
|
hardware = (SQLUserHardware) getHardwareInfoById(String.valueOf(SQLUser.hwidId));
|
||||||
|
}
|
||||||
|
return hardware;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SQLUserHardware implements UserHardware {
|
||||||
|
private final HardwareReportRequest.HardwareInfo hardwareInfo;
|
||||||
|
private final long id;
|
||||||
|
private byte[] publicKey;
|
||||||
|
private boolean banned;
|
||||||
|
|
||||||
|
public SQLUserHardware(HardwareReportRequest.HardwareInfo hardwareInfo, byte[] publicKey, long id, boolean banned) {
|
||||||
|
this.hardwareInfo = hardwareInfo;
|
||||||
|
this.publicKey = publicKey;
|
||||||
|
this.id = id;
|
||||||
|
this.banned = banned;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HardwareReportRequest.HardwareInfo getHardwareInfo() {
|
||||||
|
return hardwareInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getPublicKey() {
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getId() {
|
||||||
|
return String.valueOf(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isBanned() {
|
||||||
|
return banned;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SQLUserHardware{" +
|
||||||
|
"hardwareInfo=" + hardwareInfo +
|
||||||
|
", publicKey=" + (publicKey == null ? null : new String(Base64.getEncoder().encode(publicKey))) +
|
||||||
|
", id=" + id +
|
||||||
|
", banned=" + banned +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SQLUser extends AbstractSQLCoreProvider.SQLUser {
|
||||||
|
protected long hwidId;
|
||||||
|
|
||||||
|
public SQLUser(UUID uuid, String username, String accessToken, String serverId, String password, long hwidId) {
|
||||||
|
super(uuid, username, accessToken, serverId, password);
|
||||||
|
this.hwidId = hwidId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "SQLUser{" +
|
||||||
|
"uuid=" + uuid +
|
||||||
|
", username='" + username + '\'' +
|
||||||
|
", permissions=" + permissions +
|
||||||
|
", hwidId=" + hwidId +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package pro.gravit.launchserver.auth.core;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.ClientPermissions;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface User {
|
||||||
|
String getUsername();
|
||||||
|
|
||||||
|
UUID getUUID();
|
||||||
|
|
||||||
|
ClientPermissions getPermissions();
|
||||||
|
|
||||||
|
default boolean isBanned() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package pro.gravit.launchserver.auth.core;
|
||||||
|
|
||||||
|
public interface UserSession {
|
||||||
|
String getID();
|
||||||
|
|
||||||
|
User getUser();
|
||||||
|
|
||||||
|
String getMinecraftAccessToken();
|
||||||
|
|
||||||
|
long getExpireIn();
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.request.secure.HardwareReportRequest;
|
||||||
|
|
||||||
|
public interface UserHardware {
|
||||||
|
HardwareReportRequest.HardwareInfo getHardwareInfo();
|
||||||
|
|
||||||
|
byte[] getPublicKey();
|
||||||
|
|
||||||
|
String getId();
|
||||||
|
|
||||||
|
boolean isBanned();
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.provider;
|
||||||
|
|
||||||
|
public interface AuthSupport {
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.provider;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.events.request.AssetUploadInfoRequestEvent;
|
||||||
|
import pro.gravit.launcher.base.events.request.AuthRequestEvent;
|
||||||
|
import pro.gravit.launcher.base.events.request.GetAssetUploadUrlRequestEvent;
|
||||||
|
import pro.gravit.launchserver.auth.Feature;
|
||||||
|
import pro.gravit.launchserver.auth.core.User;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Feature(GetAssetUploadUrlRequestEvent.FEATURE_NAME)
|
||||||
|
public interface AuthSupportAssetUpload extends AuthSupport {
|
||||||
|
String getAssetUploadUrl(String name, User user);
|
||||||
|
|
||||||
|
default AuthRequestEvent.OAuthRequestEvent getAssetUploadToken(String name, User user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
default AssetUploadInfoRequestEvent getAssetUploadInfo(User user) {
|
||||||
|
return new AssetUploadInfoRequestEvent(Set.of("SKIN", "CAPE"), AssetUploadInfoRequestEvent.SlimSupportConf.USER);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.provider;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.auth.core.User;
|
||||||
|
import pro.gravit.launchserver.auth.core.UserSession;
|
||||||
|
|
||||||
|
public interface AuthSupportExit extends AuthSupport {
|
||||||
|
void deleteSession(UserSession session);
|
||||||
|
|
||||||
|
void exitUser(User user);
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.provider;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.auth.core.UserSession;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface AuthSupportExtendedCheckServer {
|
||||||
|
UserSession extendedCheckServer(Client client, String username, String serverID);
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.provider;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.auth.Feature;
|
||||||
|
import pro.gravit.launchserver.auth.core.User;
|
||||||
|
|
||||||
|
@Feature("users")
|
||||||
|
public interface AuthSupportGetAllUsers extends AuthSupport {
|
||||||
|
Iterable<User> getAllUsers();
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.provider;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.request.secure.HardwareReportRequest;
|
||||||
|
import pro.gravit.launchserver.auth.core.User;
|
||||||
|
import pro.gravit.launchserver.auth.core.UserSession;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.UserHardware;
|
||||||
|
import pro.gravit.launchserver.helper.DamerauHelper;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public interface AuthSupportHardware extends AuthSupport {
|
||||||
|
UserHardware getHardwareInfoByPublicKey(byte[] publicKey);
|
||||||
|
|
||||||
|
UserHardware getHardwareInfoByData(HardwareReportRequest.HardwareInfo info);
|
||||||
|
|
||||||
|
UserHardware getHardwareInfoById(String id);
|
||||||
|
|
||||||
|
UserHardware createHardwareInfo(HardwareReportRequest.HardwareInfo info, byte[] publicKey);
|
||||||
|
|
||||||
|
void connectUserAndHardware(UserSession userSession, UserHardware hardware);
|
||||||
|
|
||||||
|
void addPublicKeyToHardwareInfo(UserHardware hardware, byte[] publicKey);
|
||||||
|
|
||||||
|
Iterable<User> getUsersByHardwareInfo(UserHardware hardware);
|
||||||
|
|
||||||
|
void banHardware(UserHardware hardware);
|
||||||
|
|
||||||
|
void unbanHardware(UserHardware hardware);
|
||||||
|
|
||||||
|
default void normalizeHardwareInfo(HardwareReportRequest.HardwareInfo hardwareInfo) {
|
||||||
|
if (hardwareInfo.baseboardSerialNumber != null)
|
||||||
|
hardwareInfo.baseboardSerialNumber = hardwareInfo.baseboardSerialNumber.trim();
|
||||||
|
if (hardwareInfo.hwDiskId != null) hardwareInfo.hwDiskId = hardwareInfo.hwDiskId.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
//Required normalize HardwareInfo
|
||||||
|
default HardwareInfoCompareResult compareHardwareInfo(HardwareReportRequest.HardwareInfo first, HardwareReportRequest.HardwareInfo second) {
|
||||||
|
HardwareInfoCompareResult result = new HardwareInfoCompareResult();
|
||||||
|
if (first.hwDiskId == null || first.hwDiskId.isEmpty()) result.firstSpoofingLevel += 0.9;
|
||||||
|
if (first.displayId == null || first.displayId.length < 4) result.firstSpoofingLevel += 0.3;
|
||||||
|
if (first.baseboardSerialNumber == null || first.baseboardSerialNumber.trim().isEmpty())
|
||||||
|
result.firstSpoofingLevel += 0.2;
|
||||||
|
if (second.hwDiskId == null || second.hwDiskId.trim().isEmpty()) result.secondSpoofingLevel += 0.9;
|
||||||
|
if (second.displayId == null || second.displayId.length < 4) result.secondSpoofingLevel += 0.3;
|
||||||
|
if (second.baseboardSerialNumber == null || second.baseboardSerialNumber.trim().isEmpty())
|
||||||
|
result.secondSpoofingLevel += 0.2;
|
||||||
|
if (first.hwDiskId != null && second.hwDiskId != null) {
|
||||||
|
int hwDIskIdRate = DamerauHelper.calculateDistance(first.hwDiskId.toLowerCase(), second.hwDiskId.toLowerCase());
|
||||||
|
if (hwDIskIdRate == 0) // 100% compare
|
||||||
|
{
|
||||||
|
result.compareLevel += 0.99;
|
||||||
|
} else if (hwDIskIdRate < 3) //Very small change
|
||||||
|
{
|
||||||
|
result.compareLevel += 0.85;
|
||||||
|
} else if (hwDIskIdRate < (first.hwDiskId.length() + second.hwDiskId.length()) / 4) {
|
||||||
|
double addLevel = hwDIskIdRate / ((double) (first.hwDiskId.length() + second.hwDiskId.length()) / 2.0);
|
||||||
|
if (addLevel > 0.0 && addLevel < 0.85) result.compareLevel += addLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (first.baseboardSerialNumber != null && second.baseboardSerialNumber != null) {
|
||||||
|
int baseboardSerialRate = DamerauHelper.calculateDistance(first.baseboardSerialNumber.toLowerCase(), second.baseboardSerialNumber.toLowerCase());
|
||||||
|
if (baseboardSerialRate == 0) // 100% compare
|
||||||
|
{
|
||||||
|
result.compareLevel += 0.3;
|
||||||
|
} else if (baseboardSerialRate < 3) //Very small change
|
||||||
|
{
|
||||||
|
result.compareLevel += 0.15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (first.displayId != null && second.displayId != null) {
|
||||||
|
if (Arrays.equals(first.displayId, second.displayId)) {
|
||||||
|
result.compareLevel += 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Check statistic info
|
||||||
|
if (first.logicalProcessors == 0 || first.physicalProcessors == 0 || first.logicalProcessors < first.physicalProcessors) //WTF
|
||||||
|
result.firstSpoofingLevel += 0.9;
|
||||||
|
if (second.logicalProcessors == 0 || second.physicalProcessors == 0 || second.logicalProcessors < second.physicalProcessors) //WTF
|
||||||
|
result.secondSpoofingLevel += 0.9;
|
||||||
|
if (first.physicalProcessors == second.physicalProcessors && first.logicalProcessors == second.logicalProcessors)
|
||||||
|
result.compareLevel += 0.05;
|
||||||
|
if (first.battery != second.battery)
|
||||||
|
result.compareLevel -= 0.05;
|
||||||
|
if (first.processorMaxFreq == second.processorMaxFreq)
|
||||||
|
result.compareLevel += 0.1;
|
||||||
|
if (first.totalMemory == second.totalMemory)
|
||||||
|
result.compareLevel += 0.1;
|
||||||
|
if (Math.abs(first.totalMemory - second.totalMemory) < 32 * 1024)
|
||||||
|
result.compareLevel += 0.05;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
class HardwareInfoCompareResult {
|
||||||
|
public double firstSpoofingLevel = 0.0;
|
||||||
|
public double secondSpoofingLevel = 0.0;
|
||||||
|
public double compareLevel;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.provider;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.request.auth.AuthRequest;
|
||||||
|
import pro.gravit.launchserver.auth.Feature;
|
||||||
|
import pro.gravit.launchserver.auth.core.User;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Feature("registration")
|
||||||
|
public interface AuthSupportRegistration extends AuthSupport {
|
||||||
|
User registration(String login, String email, AuthRequest.AuthPasswordInterface password, Map<String, String> properties);
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.provider;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.auth.core.User;
|
||||||
|
import pro.gravit.launchserver.manangers.AuthManager;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public interface AuthSupportSudo {
|
||||||
|
AuthManager.AuthReport sudo(User user, boolean shadow) throws IOException;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.session;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.UserHardware;
|
||||||
|
|
||||||
|
public interface UserSessionSupportHardware {
|
||||||
|
String getHardwareId();
|
||||||
|
UserHardware getHardware();
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.session;
|
||||||
|
|
||||||
|
import java.security.PrivateKey;
|
||||||
|
import java.security.PublicKey;
|
||||||
|
|
||||||
|
public interface UserSessionSupportKeys {
|
||||||
|
ClientProfileKeys getClientProfileKeys();
|
||||||
|
|
||||||
|
record ClientProfileKeys(PublicKey publicKey, PrivateKey privateKey, byte[] signature /* V2 */, long expiresAt,
|
||||||
|
long refreshedAfter) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.session;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface UserSessionSupportProperties {
|
||||||
|
Map<String, String> getProperties();
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.user;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface UserSupportAdditionalData {
|
||||||
|
String getProperty(String name);
|
||||||
|
|
||||||
|
default String getPropertyUnprivileged(String name) {
|
||||||
|
return getProperty(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
default String getPropertyUnprivilegedSelf(String name) {
|
||||||
|
return getProperty(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> getPropertiesMap();
|
||||||
|
|
||||||
|
default Map<String, String> getPropertiesMapUnprivileged() {
|
||||||
|
return getPropertiesMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
default Map<String, String> getPropertiesMapUnprivilegedSelf() {
|
||||||
|
return getPropertiesMapUnprivileged();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.user;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface UserSupportProperties {
|
||||||
|
Map<String, String> getProperties();
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.interfaces.user;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.profiles.ClientProfile;
|
||||||
|
import pro.gravit.launcher.base.profiles.Texture;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public interface UserSupportTextures {
|
||||||
|
Texture getSkinTexture();
|
||||||
|
|
||||||
|
Texture getCloakTexture();
|
||||||
|
|
||||||
|
default Texture getSkinTexture(ClientProfile profile) {
|
||||||
|
return getSkinTexture();
|
||||||
|
}
|
||||||
|
|
||||||
|
default Texture getCloakTexture(ClientProfile profile) {
|
||||||
|
return getCloakTexture();
|
||||||
|
}
|
||||||
|
|
||||||
|
default Map<String, Texture> getUserAssets() {
|
||||||
|
var skin = getSkinTexture();
|
||||||
|
var cape = getCloakTexture();
|
||||||
|
Map<String, Texture> map = new HashMap<>();
|
||||||
|
if (skin != null) {
|
||||||
|
map.put("SKIN", skin);
|
||||||
|
}
|
||||||
|
if (cape != null) {
|
||||||
|
map.put("CAPE", cape);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.openid;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
|
||||||
|
public record AccessTokenResponse(@SerializedName("access_token") String accessToken,
|
||||||
|
@SerializedName("expires_in") Long expiresIn,
|
||||||
|
@SerializedName("refresh_expires_in") Long refreshExpiresIn,
|
||||||
|
@SerializedName("refresh_token") String refreshToken,
|
||||||
|
@SerializedName("token_type") String tokenType,
|
||||||
|
@SerializedName("id_token") String idToken,
|
||||||
|
@SerializedName("not-before-policy") Integer notBeforePolicy,
|
||||||
|
@SerializedName("session_state") String sessionState,
|
||||||
|
@SerializedName("scope") String scope) {
|
||||||
|
}
|
|
@ -0,0 +1,178 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.openid;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.JwtException;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import pro.gravit.launcher.base.ClientPermissions;
|
||||||
|
import pro.gravit.launcher.base.events.request.GetAvailabilityAuthRequestEvent;
|
||||||
|
import pro.gravit.launcher.base.request.auth.AuthRequest;
|
||||||
|
import pro.gravit.launcher.base.request.auth.password.AuthCodePassword;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.auth.AuthException;
|
||||||
|
import pro.gravit.launchserver.auth.AuthProviderPair;
|
||||||
|
import pro.gravit.launchserver.auth.HikariSQLSourceConfig;
|
||||||
|
import pro.gravit.launchserver.auth.core.AuthCoreProvider;
|
||||||
|
import pro.gravit.launchserver.auth.core.User;
|
||||||
|
import pro.gravit.launchserver.auth.core.UserSession;
|
||||||
|
import pro.gravit.launchserver.manangers.AuthManager;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.response.auth.AuthResponse;
|
||||||
|
import pro.gravit.utils.helper.LogHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.temporal.ChronoUnit;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public class OpenIDAuthCoreProvider extends AuthCoreProvider {
|
||||||
|
private transient SQLUserStore sqlUserStore;
|
||||||
|
private transient SQLServerSessionStore sqlSessionStore;
|
||||||
|
private transient OpenIDAuthenticator openIDAuthenticator;
|
||||||
|
|
||||||
|
private OpenIDConfig openIDConfig;
|
||||||
|
private HikariSQLSourceConfig sqlSourceConfig;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<GetAvailabilityAuthRequestEvent.AuthAvailabilityDetails> getDetails(Client client) {
|
||||||
|
return openIDAuthenticator.getDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUserByUsername(String username) {
|
||||||
|
return sqlUserStore.getByUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUserByUUID(UUID uuid) {
|
||||||
|
return sqlUserStore.getUserByUUID(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserSession getUserSessionByOAuthAccessToken(String accessToken) throws OAuthAccessTokenExpired {
|
||||||
|
return openIDAuthenticator.getUserSessionByOAuthAccessToken(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthManager.AuthReport refreshAccessToken(String oldRefreshToken, AuthResponse.AuthContext context) {
|
||||||
|
var tokens = openIDAuthenticator.refreshAccessToken(oldRefreshToken);
|
||||||
|
var accessToken = tokens.accessToken();
|
||||||
|
var refreshToken = tokens.refreshToken();
|
||||||
|
long expiresIn = TimeUnit.SECONDS.toMillis(tokens.accessTokenExpiresIn());
|
||||||
|
|
||||||
|
UserSession session;
|
||||||
|
try {
|
||||||
|
session = openIDAuthenticator.getUserSessionByOAuthAccessToken(accessToken);
|
||||||
|
} catch (OAuthAccessTokenExpired e) {
|
||||||
|
throw new RuntimeException("invalid token", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return AuthManager.AuthReport.ofOAuth(accessToken, refreshToken,
|
||||||
|
expiresIn, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthManager.AuthReport authorize(String login, AuthResponse.AuthContext context, AuthRequest.AuthPasswordInterface password, boolean minecraftAccess) throws IOException {
|
||||||
|
if (password == null) {
|
||||||
|
throw AuthException.wrongPassword();
|
||||||
|
}
|
||||||
|
var authCodePassword = (AuthCodePassword) password;
|
||||||
|
|
||||||
|
var tokens = openIDAuthenticator.authorize(authCodePassword);
|
||||||
|
|
||||||
|
var accessToken = tokens.accessToken();
|
||||||
|
var refreshToken = tokens.refreshToken();
|
||||||
|
var user = openIDAuthenticator.createUserFromToken(accessToken);
|
||||||
|
long expiresIn = TimeUnit.SECONDS.toMillis(tokens.accessTokenExpiresIn());
|
||||||
|
|
||||||
|
sqlUserStore.createOrUpdateUser(user);
|
||||||
|
|
||||||
|
UserSession session;
|
||||||
|
try {
|
||||||
|
session = openIDAuthenticator.getUserSessionByOAuthAccessToken(accessToken);
|
||||||
|
} catch (OAuthAccessTokenExpired e) {
|
||||||
|
throw new AuthException("invalid token", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minecraftAccess) {
|
||||||
|
var minecraftToken = generateMinecraftToken(user);
|
||||||
|
return AuthManager.AuthReport.ofOAuthWithMinecraft(minecraftToken, accessToken, refreshToken,
|
||||||
|
expiresIn, session);
|
||||||
|
} else {
|
||||||
|
return AuthManager.AuthReport.ofOAuth(accessToken, refreshToken,
|
||||||
|
expiresIn, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateMinecraftToken(User user) {
|
||||||
|
return Jwts.builder()
|
||||||
|
.issuer("LaunchServer")
|
||||||
|
.subject(user.getUUID().toString())
|
||||||
|
.claim("preferred_username", user.getUsername())
|
||||||
|
.expiration(Date.from(Instant.now().plus(24, ChronoUnit.HOURS)))
|
||||||
|
.signWith(server.keyAgreementManager.ecdsaPrivateKey)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
private User createUserFromMinecraftToken(String accessToken) throws AuthException {
|
||||||
|
try {
|
||||||
|
var parser = Jwts.parser()
|
||||||
|
.requireIssuer("LaunchServer")
|
||||||
|
.verifyWith(server.keyAgreementManager.ecdsaPublicKey)
|
||||||
|
.build();
|
||||||
|
var claims = parser.parseSignedClaims(accessToken);
|
||||||
|
var username = claims.getPayload().get("preferred_username", String.class);
|
||||||
|
var uuid = UUID.fromString(claims.getPayload().getSubject());
|
||||||
|
return new UserEntity(username, uuid, new ClientPermissions());
|
||||||
|
} catch (JwtException e) {
|
||||||
|
throw new AuthException("Bad minecraft token", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(LaunchServer server, AuthProviderPair pair) {
|
||||||
|
super.init(server, pair);
|
||||||
|
this.sqlSourceConfig.init();
|
||||||
|
this.sqlUserStore = new SQLUserStore(sqlSourceConfig);
|
||||||
|
this.sqlUserStore.init();
|
||||||
|
this.sqlSessionStore = new SQLServerSessionStore(sqlSourceConfig);
|
||||||
|
this.sqlSessionStore.init();
|
||||||
|
this.openIDAuthenticator = new OpenIDAuthenticator(openIDConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User checkServer(Client client, String username, String serverID) {
|
||||||
|
var savedServerId = sqlSessionStore.getServerIdByUsername(username);
|
||||||
|
if (!serverID.equals(savedServerId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sqlUserStore.getByUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean joinServer(Client client, String username, UUID uuid, String accessToken, String serverID) {
|
||||||
|
User user;
|
||||||
|
try {
|
||||||
|
user = createUserFromMinecraftToken(accessToken);
|
||||||
|
} catch (AuthException e) {
|
||||||
|
LogHelper.error(e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!user.getUUID().equals(uuid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlUserStore.createOrUpdateUser(user);
|
||||||
|
|
||||||
|
return sqlSessionStore.joinServer(user.getUUID(), user.getUsername(), serverID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
sqlSourceConfig.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,232 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.openid;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.*;
|
||||||
|
import io.jsonwebtoken.security.Jwk;
|
||||||
|
import io.jsonwebtoken.security.JwkSet;
|
||||||
|
import io.jsonwebtoken.security.Jwks;
|
||||||
|
import pro.gravit.launcher.base.ClientPermissions;
|
||||||
|
import pro.gravit.launcher.base.Launcher;
|
||||||
|
import pro.gravit.launcher.base.events.request.GetAvailabilityAuthRequestEvent;
|
||||||
|
import pro.gravit.launcher.base.request.auth.details.AuthWebViewDetails;
|
||||||
|
import pro.gravit.launcher.base.request.auth.password.AuthCodePassword;
|
||||||
|
import pro.gravit.launchserver.auth.AuthException;
|
||||||
|
import pro.gravit.launchserver.auth.core.AuthCoreProvider;
|
||||||
|
import pro.gravit.launchserver.auth.core.User;
|
||||||
|
import pro.gravit.launchserver.auth.core.UserSession;
|
||||||
|
import pro.gravit.utils.helper.CommonHelper;
|
||||||
|
import pro.gravit.utils.helper.QueryHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class OpenIDAuthenticator {
|
||||||
|
private static final HttpClient CLIENT = HttpClient.newBuilder().build();
|
||||||
|
private final OpenIDConfig openIDConfig;
|
||||||
|
private final JwtParser jwtParser;
|
||||||
|
|
||||||
|
public OpenIDAuthenticator(OpenIDConfig openIDConfig) {
|
||||||
|
this.openIDConfig = openIDConfig;
|
||||||
|
var keyLocator = loadKeyLocator(openIDConfig);
|
||||||
|
this.jwtParser = Jwts.parser()
|
||||||
|
.keyLocator(keyLocator)
|
||||||
|
.requireIssuer(openIDConfig.issuer())
|
||||||
|
.require("azp", openIDConfig.clientId())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<GetAvailabilityAuthRequestEvent.AuthAvailabilityDetails> getDetails() {
|
||||||
|
var state = UUID.randomUUID().toString();
|
||||||
|
var uri = QueryBuilder.get(openIDConfig.authorizationEndpoint())
|
||||||
|
.addQuery("response_type", "code")
|
||||||
|
.addQuery("client_id", openIDConfig.clientId())
|
||||||
|
.addQuery("redirect_uri", openIDConfig.redirectUri())
|
||||||
|
.addQuery("scope", openIDConfig.scopes())
|
||||||
|
.addQuery("state", state)
|
||||||
|
.toUriString();
|
||||||
|
|
||||||
|
return List.of(new AuthWebViewDetails(uri, openIDConfig.redirectUri()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenResponse refreshAccessToken(String oldRefreshToken) {
|
||||||
|
var postBody = QueryBuilder.post()
|
||||||
|
.addQuery("grant_type", "refresh_token")
|
||||||
|
.addQuery("refresh_token", oldRefreshToken)
|
||||||
|
.addQuery("client_id", openIDConfig.clientId())
|
||||||
|
.addQuery("client_secret", openIDConfig.clientSecret())
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
var accessTokenResponse = requestToken(postBody);
|
||||||
|
var accessToken = accessTokenResponse.accessToken();
|
||||||
|
var refreshToken = accessTokenResponse.refreshToken();
|
||||||
|
|
||||||
|
try {
|
||||||
|
readAndVerifyToken(accessToken);
|
||||||
|
} catch (AuthException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
var accessTokenExpiresIn = Objects.requireNonNullElse(accessTokenResponse.expiresIn(), 0L);
|
||||||
|
var refreshTokenExpiresIn = Objects.requireNonNullElse(accessTokenResponse.refreshExpiresIn(), 0L);
|
||||||
|
|
||||||
|
return new TokenResponse(accessToken, accessTokenExpiresIn,
|
||||||
|
refreshToken, refreshTokenExpiresIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserSession getUserSessionByOAuthAccessToken(String accessToken) throws AuthCoreProvider.OAuthAccessTokenExpired {
|
||||||
|
Jws<Claims> token;
|
||||||
|
try {
|
||||||
|
token = readAndVerifyToken(accessToken);
|
||||||
|
} catch (AuthException e) {
|
||||||
|
throw new AuthCoreProvider.OAuthAccessTokenExpired("Can't read token", e);
|
||||||
|
}
|
||||||
|
var user = createUserFromToken(token);
|
||||||
|
long expiresIn = 0;
|
||||||
|
var expDate = token.getPayload().getExpiration();
|
||||||
|
if (expDate != null) {
|
||||||
|
expiresIn = expDate.toInstant().toEpochMilli();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OpenIDUserSession(user, accessToken, expiresIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenResponse authorize(AuthCodePassword authCode) throws IOException {
|
||||||
|
var uri = URI.create(authCode.uri);
|
||||||
|
var queries = QueryHelper.splitUriQuery(uri);
|
||||||
|
|
||||||
|
String code = CommonHelper.multimapFirstOrNullValue("code", queries);
|
||||||
|
String error = CommonHelper.multimapFirstOrNullValue("error", queries);
|
||||||
|
String errorDescription = CommonHelper.multimapFirstOrNullValue("error_description", queries);
|
||||||
|
|
||||||
|
if (error != null && !error.isBlank()) {
|
||||||
|
throw new AuthException("Auth error. Error: %s, description: %s".formatted(error, errorDescription));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var postBody = QueryBuilder.post()
|
||||||
|
.addQuery("grant_type", "authorization_code")
|
||||||
|
.addQuery("code", code)
|
||||||
|
.addQuery("redirect_uri", openIDConfig.redirectUri())
|
||||||
|
.addQuery("client_id", openIDConfig.clientId())
|
||||||
|
.addQuery("client_secret", openIDConfig.clientSecret())
|
||||||
|
.toString();
|
||||||
|
|
||||||
|
var accessTokenResponse = requestToken(postBody);
|
||||||
|
var accessToken = accessTokenResponse.accessToken();
|
||||||
|
var refreshToken = accessTokenResponse.refreshToken();
|
||||||
|
|
||||||
|
readAndVerifyToken(accessToken);
|
||||||
|
|
||||||
|
var accessTokenExpiresIn = Objects.requireNonNullElse(accessTokenResponse.expiresIn(), 0L);
|
||||||
|
var refreshTokenExpiresIn = Objects.requireNonNullElse(accessTokenResponse.refreshExpiresIn(), 0L);
|
||||||
|
|
||||||
|
return new TokenResponse(accessToken, accessTokenExpiresIn,
|
||||||
|
refreshToken, refreshTokenExpiresIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public User createUserFromToken(String accessToken) throws AuthException {
|
||||||
|
return createUserFromToken(readAndVerifyToken(accessToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Jws<Claims> readAndVerifyToken(String accessToken) throws AuthException {
|
||||||
|
if (accessToken == null) {
|
||||||
|
throw new AuthException("Token is null");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return jwtParser.parseSignedClaims(accessToken);
|
||||||
|
} catch (JwtException e) {
|
||||||
|
throw new AuthException("Bad token", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private User createUserFromToken(Jws<Claims> token) {
|
||||||
|
var username = token.getPayload().get(openIDConfig.extractorConfig().usernameClaim(), String.class);
|
||||||
|
var uuidStr = token.getPayload().get(openIDConfig.extractorConfig().uuidClaim(), String.class);
|
||||||
|
var uuid = UUID.fromString(uuidStr);
|
||||||
|
return new UserEntity(username, uuid, new ClientPermissions());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccessTokenResponse requestToken(String postBody) {
|
||||||
|
var request = HttpRequest.newBuilder()
|
||||||
|
.uri(openIDConfig.tokenUri())
|
||||||
|
.header("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(postBody))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
HttpResponse<String> resp;
|
||||||
|
try {
|
||||||
|
resp = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return Launcher.gsonManager.gson.fromJson(resp.body(), AccessTokenResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KeyLocator loadKeyLocator(OpenIDConfig openIDConfig) {
|
||||||
|
var request = HttpRequest.newBuilder(openIDConfig.jwksUri()).GET().build();
|
||||||
|
HttpResponse<String> response;
|
||||||
|
try {
|
||||||
|
response = CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
|
||||||
|
} catch (IOException | InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
var jwks = Jwks.setParser().build().parse(response.body());
|
||||||
|
return new KeyLocator(jwks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class KeyLocator extends LocatorAdapter<Key> {
|
||||||
|
private final Map<String, Key> keys;
|
||||||
|
|
||||||
|
public KeyLocator(JwkSet jwks) {
|
||||||
|
this.keys = jwks.getKeys().stream().collect(
|
||||||
|
Collectors.toMap(jwk -> String.valueOf(jwk.get("kid")), Jwk::toKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Key locate(JweHeader header) {
|
||||||
|
return super.locate(header);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Key locate(JwsHeader header) {
|
||||||
|
return keys.get(header.getKeyId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Key doLocate(Header header) {
|
||||||
|
return super.doLocate(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record OpenIDUserSession(User user, String token, long expiresIn) implements UserSession {
|
||||||
|
@Override
|
||||||
|
public String getID() {
|
||||||
|
return user.getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getMinecraftAccessToken() {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getExpireIn() {
|
||||||
|
return expiresIn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.openid;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
public record OpenIDConfig(URI tokenUri, String authorizationEndpoint, String clientId, String clientSecret,
|
||||||
|
String redirectUri, URI jwksUri, String scopes, String issuer,
|
||||||
|
ClaimExtractorConfig extractorConfig) {
|
||||||
|
|
||||||
|
public record ClaimExtractorConfig(String usernameClaim, String uuidClaim) {}
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.openid;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Xakep_SDK
|
||||||
|
*/
|
||||||
|
public class QueryBuilder {
|
||||||
|
private final String uri;
|
||||||
|
private final StringBuilder query = new StringBuilder();
|
||||||
|
|
||||||
|
public QueryBuilder(String uri) {
|
||||||
|
this.uri = uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static QueryBuilder get(String uri) {
|
||||||
|
Objects.requireNonNull(uri, "uri");
|
||||||
|
if (uri.endsWith("/")) {
|
||||||
|
uri = uri.substring(0, uri.length() - 1);
|
||||||
|
}
|
||||||
|
return new QueryBuilder(uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static QueryBuilder post() {
|
||||||
|
return new QueryBuilder(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QueryBuilder addQuery(String key, String value) {
|
||||||
|
if (!query.isEmpty()) {
|
||||||
|
query.append('&');
|
||||||
|
}
|
||||||
|
query.append(URLEncoder.encode(key, StandardCharsets.UTF_8))
|
||||||
|
.append('=')
|
||||||
|
.append(URLEncoder.encode(value, StandardCharsets.UTF_8));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toUriString() {
|
||||||
|
if (uri != null) {
|
||||||
|
if (query. isEmpty()) {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
return uri + '?' + query;
|
||||||
|
}
|
||||||
|
return toQueryString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toQueryString() {
|
||||||
|
return query.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return toUriString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.openid;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.auth.SQLSourceConfig;
|
||||||
|
import pro.gravit.utils.helper.LogHelper;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SQLServerSessionStore implements ServerSessionStore {
|
||||||
|
private static final String CREATE_TABLE = """
|
||||||
|
create table if not exists `gravit_server_session` (
|
||||||
|
id int auto_increment,
|
||||||
|
uuid varchar(36),
|
||||||
|
username varchar(255),
|
||||||
|
server_id varchar(41),
|
||||||
|
primary key (id),
|
||||||
|
unique (uuid),
|
||||||
|
unique (username)
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
private static final String DELETE_SERVER_ID = """
|
||||||
|
delete from `gravit_server_session` where uuid = ?
|
||||||
|
""";
|
||||||
|
private static final String INSERT_SERVER_ID = """
|
||||||
|
insert into `gravit_server_session` (uuid, username, server_id) values (?, ?, ?)
|
||||||
|
""";
|
||||||
|
private static final String SELECT_SERVER_ID_BY_USERNAME = """
|
||||||
|
select server_id from `gravit_server_session` where username = ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
private final SQLSourceConfig sqlSourceConfig;
|
||||||
|
|
||||||
|
public SQLServerSessionStore(SQLSourceConfig sqlSourceConfig) {
|
||||||
|
this.sqlSourceConfig = sqlSourceConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean joinServer(UUID uuid, String username, String serverId) {
|
||||||
|
try (var connection = sqlSourceConfig.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
var savepoint = connection.setSavepoint();
|
||||||
|
try (var deleteServerIdStmt = connection.prepareStatement(DELETE_SERVER_ID);
|
||||||
|
var insertServerIdStmt = connection.prepareStatement(INSERT_SERVER_ID)) {
|
||||||
|
deleteServerIdStmt.setString(1, uuid.toString());
|
||||||
|
deleteServerIdStmt.execute();
|
||||||
|
insertServerIdStmt.setString(1, uuid.toString());
|
||||||
|
insertServerIdStmt.setString(2, username);
|
||||||
|
insertServerIdStmt.setString(3, serverId);
|
||||||
|
insertServerIdStmt.execute();
|
||||||
|
connection.commit();
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
connection.rollback(savepoint);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LogHelper.debug("Can't join server. Username: %s".formatted(username));
|
||||||
|
LogHelper.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getServerIdByUsername(String username) {
|
||||||
|
try (var connection = sqlSourceConfig.getConnection();
|
||||||
|
var selectServerId = connection.prepareStatement(SELECT_SERVER_ID_BY_USERNAME)) {
|
||||||
|
selectServerId.setString(1, username);
|
||||||
|
try (var rs = selectServerId.executeQuery()) {
|
||||||
|
if (!rs.next()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return rs.getString("server_id");
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LogHelper.debug("Can't find server id by username. Username: %s".formatted(username));
|
||||||
|
LogHelper.error(e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init() {
|
||||||
|
try (var connection = sqlSourceConfig.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
var savepoint = connection.setSavepoint();
|
||||||
|
try (var createTableStmt = connection.prepareStatement(CREATE_TABLE)) {
|
||||||
|
createTableStmt.execute();
|
||||||
|
connection.commit();
|
||||||
|
} catch (Exception e) {
|
||||||
|
connection.rollback(savepoint);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,124 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.openid;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.ClientPermissions;
|
||||||
|
import pro.gravit.launchserver.auth.HikariSQLSourceConfig;
|
||||||
|
import pro.gravit.launchserver.auth.core.User;
|
||||||
|
import pro.gravit.utils.helper.LogHelper;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class SQLUserStore implements UserStore {
|
||||||
|
private static final String CREATE_USER_TABLE = """
|
||||||
|
create table if not exists `gravit_user` (
|
||||||
|
id int auto_increment,
|
||||||
|
uuid varchar(36),
|
||||||
|
username varchar(255),
|
||||||
|
primary key (id),
|
||||||
|
unique (uuid),
|
||||||
|
unique (username)
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
private static final String INSERT_USER = """
|
||||||
|
insert into `gravit_user` (uuid, username) values (?, ?)
|
||||||
|
""";
|
||||||
|
private static final String DELETE_USER_BY_NAME = """
|
||||||
|
delete `gravit_user` where username = ?
|
||||||
|
""";
|
||||||
|
private static final String SELECT_USER_BY_NAME = """
|
||||||
|
select uuid, username from `gravit_user` where username = ?
|
||||||
|
""";
|
||||||
|
private static final String SELECT_USER_BY_UUID = """
|
||||||
|
select uuid, username from `gravit_user` where uuid = ?
|
||||||
|
""";
|
||||||
|
|
||||||
|
private final HikariSQLSourceConfig sqlSourceConfig;
|
||||||
|
|
||||||
|
public SQLUserStore(HikariSQLSourceConfig sqlSourceConfig) {
|
||||||
|
this.sqlSourceConfig = sqlSourceConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getByUsername(String username) {
|
||||||
|
try (var connection = sqlSourceConfig.getConnection();
|
||||||
|
var selectUserStmt = connection.prepareStatement(SELECT_USER_BY_NAME)) {
|
||||||
|
selectUserStmt.setString(1, username);
|
||||||
|
try (var rs = selectUserStmt.executeQuery()) {
|
||||||
|
if (!rs.next()) {
|
||||||
|
LogHelper.debug("User not found, username: %s".formatted(username));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new UserEntity(rs.getString("username"),
|
||||||
|
UUID.fromString(rs.getString("uuid")),
|
||||||
|
new ClientPermissions());
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LogHelper.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public User getUserByUUID(UUID uuid) {
|
||||||
|
try (var connection = sqlSourceConfig.getConnection();
|
||||||
|
var selectUserStmt = connection.prepareStatement(SELECT_USER_BY_UUID)) {
|
||||||
|
selectUserStmt.setString(1, uuid.toString());
|
||||||
|
try (var rs = selectUserStmt.executeQuery()) {
|
||||||
|
if (!rs.next()) {
|
||||||
|
LogHelper.debug("User not found, UUID: %s".formatted(uuid));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new UserEntity(rs.getString("username"),
|
||||||
|
UUID.fromString(rs.getString("uuid")),
|
||||||
|
new ClientPermissions());
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LogHelper.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void createOrUpdateUser(User user) {
|
||||||
|
try (var connection = sqlSourceConfig.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
var savepoint = connection.setSavepoint();
|
||||||
|
try (var deleteUserStmt = connection.prepareStatement(DELETE_USER_BY_NAME);
|
||||||
|
var insertUserStmt = connection.prepareStatement(INSERT_USER)) {
|
||||||
|
deleteUserStmt.setString(1, user.getUsername());
|
||||||
|
deleteUserStmt.execute();
|
||||||
|
insertUserStmt.setString(1, user.getUUID().toString());
|
||||||
|
insertUserStmt.setString(2, user.getUsername());
|
||||||
|
insertUserStmt.execute();
|
||||||
|
connection.commit();
|
||||||
|
LogHelper.debug("User saved. UUID: %s, username: %s".formatted(user.getUUID(), user.getUsername()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
connection.rollback(savepoint);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LogHelper.debug("Failed to save user");
|
||||||
|
LogHelper.error(e);
|
||||||
|
throw new RuntimeException("Failed to save user", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init() {
|
||||||
|
try (var connection = sqlSourceConfig.getConnection()) {
|
||||||
|
connection.setAutoCommit(false);
|
||||||
|
var savepoint = connection.setSavepoint();
|
||||||
|
try (var createUserTableStmt = connection.prepareStatement(CREATE_USER_TABLE)) {
|
||||||
|
createUserTableStmt.execute();
|
||||||
|
connection.commit();
|
||||||
|
} catch (Exception e) {
|
||||||
|
connection.rollback(savepoint);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.openid;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface ServerSessionStore {
|
||||||
|
boolean joinServer(UUID uuid, String username, String serverId);
|
||||||
|
String getServerIdByUsername(String username);
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.openid;
|
||||||
|
|
||||||
|
public record TokenResponse(String accessToken, long accessTokenExpiresIn,
|
||||||
|
String refreshToken, long refreshTokenExpiresIn) {
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.openid;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.ClientPermissions;
|
||||||
|
import pro.gravit.launchserver.auth.core.User;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
record UserEntity(String username, UUID uuid, ClientPermissions permissions) implements User {
|
||||||
|
@Override
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UUID getUUID() {
|
||||||
|
return uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ClientPermissions getPermissions() {
|
||||||
|
return permissions;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package pro.gravit.launchserver.auth.core.openid;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.auth.core.User;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface UserStore {
|
||||||
|
User getByUsername(String username);
|
||||||
|
|
||||||
|
User getUserByUUID(UUID uuid);
|
||||||
|
|
||||||
|
void createOrUpdateUser(User user);
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package pro.gravit.launchserver.auth.mix;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.auth.core.AuthCoreProvider;
|
||||||
|
import pro.gravit.utils.ProviderMap;
|
||||||
|
|
||||||
|
public abstract class MixProvider implements AutoCloseable{
|
||||||
|
public static final ProviderMap<MixProvider> providers = new ProviderMap<>("MixProvider");
|
||||||
|
private static final Logger logger = LogManager.getLogger();
|
||||||
|
private static boolean registredProviders = false;
|
||||||
|
|
||||||
|
public static void registerProviders() {
|
||||||
|
if (!registredProviders) {
|
||||||
|
providers.register("uploadAsset", UploadAssetMixProvider.class);
|
||||||
|
registredProviders = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void init(LaunchServer server, AuthCoreProvider core);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T isSupport(Class<T> clazz) {
|
||||||
|
if (clazz.isAssignableFrom(getClass())) return (T) this;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract void close();
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package pro.gravit.launchserver.auth.mix;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.events.request.AssetUploadInfoRequestEvent;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.auth.core.AuthCoreProvider;
|
||||||
|
import pro.gravit.launchserver.auth.core.User;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportAssetUpload;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public class UploadAssetMixProvider extends MixProvider implements AuthSupportAssetUpload {
|
||||||
|
public Map<String, String> urls;
|
||||||
|
public AssetUploadInfoRequestEvent.SlimSupportConf slimSupportConf;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getAssetUploadUrl(String name, User user) {
|
||||||
|
return urls.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AssetUploadInfoRequestEvent getAssetUploadInfo(User user) {
|
||||||
|
return new AssetUploadInfoRequestEvent(urls.keySet(), slimSupportConf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(LaunchServer server, AuthCoreProvider core) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package pro.gravit.launchserver.auth.password;
|
||||||
|
|
||||||
|
public class AcceptPasswordVerifier extends PasswordVerifier {
|
||||||
|
@Override
|
||||||
|
public boolean check(String encryptedPassword, String password) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encrypt(String password) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package pro.gravit.launchserver.auth.password;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.generators.OpenBSDBCrypt;
|
||||||
|
import pro.gravit.utils.helper.SecurityHelper;
|
||||||
|
|
||||||
|
public class BCryptPasswordVerifier extends PasswordVerifier {
|
||||||
|
public int cost = 10;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean check(String encryptedPassword, String password) {
|
||||||
|
return OpenBSDBCrypt.checkPassword(encryptedPassword, password.toCharArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encrypt(String password) {
|
||||||
|
return OpenBSDBCrypt.generate(password.toCharArray(), SecurityHelper.randomBytes(16), cost);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package pro.gravit.launchserver.auth.password;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
import pro.gravit.utils.helper.SecurityHelper;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class DigestPasswordVerifier extends PasswordVerifier {
|
||||||
|
private transient final Logger logger = LogManager.getLogger();
|
||||||
|
public String algo;
|
||||||
|
|
||||||
|
private byte[] digest(String text) throws NoSuchAlgorithmException {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance(algo);
|
||||||
|
return digest.digest(IOHelper.encode(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean check(String encryptedPassword, String password) {
|
||||||
|
try {
|
||||||
|
byte[] bytes = SecurityHelper.fromHex(encryptedPassword);
|
||||||
|
return Arrays.equals(bytes, digest(password));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
logger.error("Digest algorithm {} not supported", algo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encrypt(String password) {
|
||||||
|
try {
|
||||||
|
return SecurityHelper.toHex(digest(password));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
logger.error("Digest algorithm {} not supported", algo);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package pro.gravit.launchserver.auth.password;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
import pro.gravit.utils.helper.SecurityHelper;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class DoubleDigestPasswordVerifier extends PasswordVerifier {
|
||||||
|
private transient final Logger logger = LogManager.getLogger();
|
||||||
|
public String algo;
|
||||||
|
public boolean toHexMode;
|
||||||
|
|
||||||
|
private byte[] digest(String text) throws NoSuchAlgorithmException {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance(algo);
|
||||||
|
byte[] firstDigest = digest.digest(IOHelper.encode(text));
|
||||||
|
return toHexMode ? digest.digest(IOHelper.encode(SecurityHelper.toHex(firstDigest))) : digest.digest(firstDigest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean check(String encryptedPassword, String password) {
|
||||||
|
try {
|
||||||
|
byte[] bytes = SecurityHelper.fromHex(encryptedPassword);
|
||||||
|
return Arrays.equals(bytes, digest(password));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
logger.error("Digest algorithm {} not supported", algo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encrypt(String password) {
|
||||||
|
try {
|
||||||
|
return SecurityHelper.toHex(digest(password));
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
logger.error("Digest algorithm {} not supported", algo);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package pro.gravit.launchserver.auth.password;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launcher.base.Launcher;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.Reader;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
public class JsonPasswordVerifier extends PasswordVerifier {
|
||||||
|
private static final Logger logger = LogManager.getLogger();
|
||||||
|
private transient final HttpClient client = HttpClient.newBuilder().build();
|
||||||
|
public String url;
|
||||||
|
public String bearerToken;
|
||||||
|
|
||||||
|
public static <T, R> R jsonRequest(T request, String url, String bearerToken, Class<R> clazz, HttpClient client) {
|
||||||
|
HttpRequest.BodyPublisher publisher;
|
||||||
|
if (request != null) {
|
||||||
|
publisher = HttpRequest.BodyPublishers.ofString(Launcher.gsonManager.gson.toJson(request));
|
||||||
|
} else {
|
||||||
|
publisher = HttpRequest.BodyPublishers.noBody();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
HttpRequest.Builder request1 = HttpRequest.newBuilder()
|
||||||
|
.method("POST", publisher)
|
||||||
|
.uri(new URI(url))
|
||||||
|
.header("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.timeout(Duration.ofMillis(10000));
|
||||||
|
if (bearerToken != null) {
|
||||||
|
request1.header("Authorization", "Bearer ".concat(bearerToken));
|
||||||
|
}
|
||||||
|
HttpResponse<InputStream> response = client.send(request1.build(), HttpResponse.BodyHandlers.ofInputStream());
|
||||||
|
int statusCode = response.statusCode();
|
||||||
|
if (200 > statusCode || statusCode > 300) {
|
||||||
|
if (statusCode >= 500) {
|
||||||
|
logger.error("JsonCoreProvider: {} return {}", url, statusCode);
|
||||||
|
} else if (statusCode >= 300 && statusCode <= 400) {
|
||||||
|
logger.error("JsonCoreProvider: {} return {}, try redirect to {}. Redirects not supported!", url, statusCode, response.headers().firstValue("Location").orElse("Unknown"));
|
||||||
|
} else if (statusCode == 403 || statusCode == 401) {
|
||||||
|
logger.error("JsonCoreProvider: {} return {}. Please set 'bearerToken'!", url, statusCode);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try (Reader reader = new InputStreamReader(response.body())) {
|
||||||
|
return Launcher.gsonManager.gson.fromJson(reader, clazz);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean check(String encryptedPassword, String password) {
|
||||||
|
JsonPasswordResponse response = jsonRequest(new JsonPasswordRequest(encryptedPassword, password), url, bearerToken, JsonPasswordResponse.class, client);
|
||||||
|
if (response != null) {
|
||||||
|
return response.success;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class JsonPasswordRequest {
|
||||||
|
public String encryptedPassword;
|
||||||
|
public String password;
|
||||||
|
|
||||||
|
public JsonPasswordRequest(String encryptedPassword, String password) {
|
||||||
|
this.encryptedPassword = encryptedPassword;
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class JsonPasswordResponse {
|
||||||
|
public boolean success;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package pro.gravit.launchserver.auth.password;
|
||||||
|
|
||||||
|
import pro.gravit.utils.ProviderMap;
|
||||||
|
|
||||||
|
public abstract class PasswordVerifier {
|
||||||
|
public static final ProviderMap<PasswordVerifier> providers = new ProviderMap<>("PasswordVerifier");
|
||||||
|
private static boolean registeredProviders = false;
|
||||||
|
|
||||||
|
public static void registerProviders() {
|
||||||
|
if (!registeredProviders) {
|
||||||
|
providers.register("plain", PlainPasswordVerifier.class);
|
||||||
|
providers.register("digest", DigestPasswordVerifier.class);
|
||||||
|
providers.register("doubleDigest", DoubleDigestPasswordVerifier.class);
|
||||||
|
providers.register("json", JsonPasswordVerifier.class);
|
||||||
|
providers.register("bcrypt", BCryptPasswordVerifier.class);
|
||||||
|
providers.register("accept", AcceptPasswordVerifier.class);
|
||||||
|
providers.register("reject", RejectPasswordVerifier.class);
|
||||||
|
registeredProviders = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract boolean check(String encryptedPassword, String password);
|
||||||
|
|
||||||
|
public String encrypt(String password) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package pro.gravit.launchserver.auth.password;
|
||||||
|
|
||||||
|
public class PlainPasswordVerifier extends PasswordVerifier {
|
||||||
|
@Override
|
||||||
|
public boolean check(String encryptedPassword, String password) {
|
||||||
|
return encryptedPassword.equals(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String encrypt(String password) {
|
||||||
|
return super.encrypt(password);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package pro.gravit.launchserver.auth.password;
|
||||||
|
|
||||||
|
public class RejectPasswordVerifier extends PasswordVerifier {
|
||||||
|
@Override
|
||||||
|
public boolean check(String encryptedPassword, String password) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
package pro.gravit.launchserver.auth.profiles;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launcher.base.Launcher;
|
||||||
|
import pro.gravit.launcher.base.profiles.ClientProfile;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class LocalProfileProvider extends ProfileProvider {
|
||||||
|
public String profilesDir = "profiles";
|
||||||
|
private transient volatile Map<Path, ClientProfile> profilesMap;
|
||||||
|
private transient volatile Set<ClientProfile> profilesList; // Cache
|
||||||
|
@Override
|
||||||
|
public void sync() throws IOException {
|
||||||
|
Path profilesDirPath = Path.of(profilesDir);
|
||||||
|
if (!IOHelper.isDir(profilesDirPath))
|
||||||
|
Files.createDirectory(profilesDirPath);
|
||||||
|
Map<Path, ClientProfile> newProfiles = new HashMap<>();
|
||||||
|
IOHelper.walk(profilesDirPath, new ProfilesFileVisitor(newProfiles), false);
|
||||||
|
Set<ClientProfile> newProfilesList = new HashSet<>(newProfiles.values());
|
||||||
|
profilesMap = newProfiles;
|
||||||
|
profilesList = newProfilesList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Set<ClientProfile> getProfiles() {
|
||||||
|
return profilesList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addProfile(ClientProfile profile) throws IOException {
|
||||||
|
Path profilesDirPath = Path.of(profilesDir);
|
||||||
|
ClientProfile oldProfile;
|
||||||
|
Path target = null;
|
||||||
|
for(var e : profilesMap.entrySet()) {
|
||||||
|
if(e.getValue().getUUID().equals(profile.getUUID())) {
|
||||||
|
target = e.getKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(target == null) {
|
||||||
|
target = profilesDirPath.resolve(profile.getTitle()+".json");
|
||||||
|
oldProfile = profilesMap.get(target);
|
||||||
|
if(oldProfile != null && !oldProfile.getUUID().equals(profile.getUUID())) {
|
||||||
|
throw new FileAlreadyExistsException(target.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try (BufferedWriter writer = IOHelper.newWriter(target)) {
|
||||||
|
Launcher.gsonManager.configGson.toJson(profile, writer);
|
||||||
|
}
|
||||||
|
addProfile(target, profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteProfile(ClientProfile profile) throws IOException {
|
||||||
|
for(var e : profilesMap.entrySet()) {
|
||||||
|
if(e.getValue().getUUID().equals(profile.getUUID())) {
|
||||||
|
Files.deleteIfExists(e.getKey());
|
||||||
|
profilesMap.remove(e.getKey());
|
||||||
|
profilesList.remove(e.getValue());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addProfile(Path path, ClientProfile profile) {
|
||||||
|
for(var e : profilesMap.entrySet()) {
|
||||||
|
if(e.getValue().getUUID().equals(profile.getUUID())) {
|
||||||
|
profilesMap.remove(e.getKey());
|
||||||
|
profilesList.remove(e.getValue());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profilesMap.put(path, profile);
|
||||||
|
profilesList.add(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class ProfilesFileVisitor extends SimpleFileVisitor<Path> {
|
||||||
|
private final Map<Path, ClientProfile> result;
|
||||||
|
private final Logger logger = LogManager.getLogger();
|
||||||
|
|
||||||
|
private ProfilesFileVisitor(Map<Path, ClientProfile> result) {
|
||||||
|
this.result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
|
logger.info("Syncing '{}' profile", IOHelper.getFileName(file));
|
||||||
|
|
||||||
|
// Read profile
|
||||||
|
ClientProfile profile;
|
||||||
|
try (BufferedReader reader = IOHelper.newReader(file)) {
|
||||||
|
profile = Launcher.gsonManager.gson.fromJson(reader, ClientProfile.class);
|
||||||
|
}
|
||||||
|
profile.verify();
|
||||||
|
|
||||||
|
// Add SIGNED profile to result list
|
||||||
|
result.put(file.toAbsolutePath(), profile);
|
||||||
|
return super.visitFile(file, attrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
package pro.gravit.launchserver.auth.profiles;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.profiles.ClientProfile;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.auth.protect.interfaces.ProfilesProtectHandler;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.utils.ProviderMap;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public abstract class ProfileProvider {
|
||||||
|
public static final ProviderMap<ProfileProvider> providers = new ProviderMap<>("ProfileProvider");
|
||||||
|
private static boolean registredProviders = false;
|
||||||
|
protected transient LaunchServer server;
|
||||||
|
|
||||||
|
public static void registerProviders() {
|
||||||
|
if (!registredProviders) {
|
||||||
|
providers.register("local", LocalProfileProvider.class);
|
||||||
|
registredProviders = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init(LaunchServer server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void sync() throws IOException;
|
||||||
|
|
||||||
|
public abstract Set<ClientProfile> getProfiles();
|
||||||
|
|
||||||
|
public abstract void addProfile(ClientProfile profile) throws IOException;
|
||||||
|
|
||||||
|
public abstract void deleteProfile(ClientProfile profile) throws IOException;
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientProfile getProfile(UUID uuid) {
|
||||||
|
for(var e : getProfiles()) {
|
||||||
|
if(e.getUUID().equals(uuid)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClientProfile getProfile(String title) {
|
||||||
|
for(var e : getProfiles()) {
|
||||||
|
if(e.getTitle().equals(title)) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ClientProfile> getProfiles(Client client) {
|
||||||
|
List<ClientProfile> profileList;
|
||||||
|
Set<ClientProfile> serverProfiles = getProfiles();
|
||||||
|
if (server.config.protectHandler instanceof ProfilesProtectHandler protectHandler) {
|
||||||
|
profileList = new ArrayList<>(4);
|
||||||
|
for (ClientProfile profile : serverProfiles) {
|
||||||
|
if (protectHandler.canGetProfile(profile, client)) {
|
||||||
|
profileList.add(profile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
profileList = List.copyOf(serverProfiles);
|
||||||
|
}
|
||||||
|
return profileList;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
package pro.gravit.launchserver.auth.protect;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.JwtParser;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launcher.base.events.request.GetSecureLevelInfoRequestEvent;
|
||||||
|
import pro.gravit.launcher.base.events.request.HardwareReportRequestEvent;
|
||||||
|
import pro.gravit.launcher.base.events.request.VerifySecureLevelKeyRequestEvent;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.auth.AuthProviderPair;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.UserHardware;
|
||||||
|
import pro.gravit.launchserver.auth.core.interfaces.provider.AuthSupportHardware;
|
||||||
|
import pro.gravit.launchserver.auth.protect.interfaces.HardwareProtectHandler;
|
||||||
|
import pro.gravit.launchserver.auth.protect.interfaces.JoinServerProtectHandler;
|
||||||
|
import pro.gravit.launchserver.auth.protect.interfaces.SecureProtectHandler;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.response.auth.RestoreResponse;
|
||||||
|
import pro.gravit.launchserver.socket.response.secure.HardwareReportResponse;
|
||||||
|
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||||
|
|
||||||
|
public class AdvancedProtectHandler extends StdProtectHandler implements SecureProtectHandler, HardwareProtectHandler, JoinServerProtectHandler {
|
||||||
|
private transient final Logger logger = LogManager.getLogger();
|
||||||
|
public boolean enableHardwareFeature;
|
||||||
|
private transient LaunchServer server;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GetSecureLevelInfoRequestEvent onGetSecureLevelInfo(GetSecureLevelInfoRequestEvent event) {
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean allowGetSecureLevelInfo(Client client) {
|
||||||
|
return client.checkSign;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onHardwareReport(HardwareReportResponse response, Client client) {
|
||||||
|
if (!enableHardwareFeature) {
|
||||||
|
response.sendResult(new HardwareReportRequestEvent());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!client.isAuth || client.trustLevel == null || client.trustLevel.publicKey == null) {
|
||||||
|
response.sendError("Access denied");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(client.trustLevel.hardwareInfo != null) {
|
||||||
|
response.sendResult(new HardwareReportRequestEvent(createHardwareToken(client.username, client.trustLevel.hardwareInfo), SECONDS.toMillis(server.config.netty.security.hardwareTokenExpire)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.debug("HardwareInfo received");
|
||||||
|
{
|
||||||
|
var authSupportHardware = client.auth.isSupport(AuthSupportHardware.class);
|
||||||
|
if (authSupportHardware != null) {
|
||||||
|
UserHardware hardware = authSupportHardware.getHardwareInfoByData(response.hardware);
|
||||||
|
if (hardware == null) {
|
||||||
|
hardware = authSupportHardware.createHardwareInfo(response.hardware, client.trustLevel.publicKey);
|
||||||
|
} else {
|
||||||
|
authSupportHardware.addPublicKeyToHardwareInfo(hardware, client.trustLevel.publicKey);
|
||||||
|
}
|
||||||
|
authSupportHardware.connectUserAndHardware(client.sessionObject, hardware);
|
||||||
|
if (hardware.isBanned()) {
|
||||||
|
throw new SecurityException("Your hardware banned");
|
||||||
|
}
|
||||||
|
client.trustLevel.hardwareInfo = hardware;
|
||||||
|
response.sendResult(new HardwareReportRequestEvent(createHardwareToken(client.username, hardware), SECONDS.toMillis(server.config.netty.security.hardwareTokenExpire)));
|
||||||
|
} else {
|
||||||
|
logger.error("AuthCoreProvider not supported hardware");
|
||||||
|
response.sendError("AuthCoreProvider not supported hardware");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public VerifySecureLevelKeyRequestEvent onSuccessVerify(Client client) {
|
||||||
|
if (enableHardwareFeature) {
|
||||||
|
var authSupportHardware = client.auth.isSupport(AuthSupportHardware.class);
|
||||||
|
if (authSupportHardware != null) {
|
||||||
|
UserHardware hardware = authSupportHardware.getHardwareInfoByPublicKey(client.trustLevel.publicKey);
|
||||||
|
if (hardware == null) //HWID not found?
|
||||||
|
return new VerifySecureLevelKeyRequestEvent(true, false, createPublicKeyToken(client.username, client.trustLevel.publicKey), SECONDS.toMillis(server.config.netty.security.publicKeyTokenExpire));
|
||||||
|
if (hardware.isBanned()) {
|
||||||
|
throw new SecurityException("Your hardware banned");
|
||||||
|
}
|
||||||
|
client.trustLevel.hardwareInfo = hardware;
|
||||||
|
authSupportHardware.connectUserAndHardware(client.sessionObject, hardware);
|
||||||
|
return new VerifySecureLevelKeyRequestEvent(false, false, createPublicKeyToken(client.username, client.trustLevel.publicKey), SECONDS.toMillis(server.config.netty.security.publicKeyTokenExpire));
|
||||||
|
} else {
|
||||||
|
logger.warn("AuthCoreProvider not supported hardware. HardwareInfo not checked!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new VerifySecureLevelKeyRequestEvent(false, false, createPublicKeyToken(client.username, client.trustLevel.publicKey), SECONDS.toMillis(server.config.netty.security.publicKeyTokenExpire));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onJoinServer(String serverID, String username, UUID uuid, Client client) {
|
||||||
|
return !enableHardwareFeature || (client.trustLevel != null && client.trustLevel.hardwareInfo != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(LaunchServer server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createHardwareToken(String username, UserHardware hardware) {
|
||||||
|
return Jwts.builder()
|
||||||
|
.setIssuer("LaunchServer")
|
||||||
|
.setSubject(username)
|
||||||
|
.setExpiration(new Date(System.currentTimeMillis() + SECONDS.toMillis(server.config.netty.security.hardwareTokenExpire)))
|
||||||
|
.claim("hardware", hardware.getId())
|
||||||
|
.signWith(server.keyAgreementManager.ecdsaPrivateKey)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String createPublicKeyToken(String username, byte[] publicKey) {
|
||||||
|
return Jwts.builder()
|
||||||
|
.setIssuer("LaunchServer")
|
||||||
|
.setSubject(username)
|
||||||
|
.setExpiration(new Date(System.currentTimeMillis() + SECONDS.toMillis(server.config.netty.security.publicKeyTokenExpire)))
|
||||||
|
.claim("publicKey", Base64.getEncoder().encodeToString(publicKey))
|
||||||
|
.signWith(server.keyAgreementManager.ecdsaPrivateKey)
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class HardwareInfoTokenVerifier implements RestoreResponse.ExtendedTokenProvider {
|
||||||
|
private transient final Logger logger = LogManager.getLogger();
|
||||||
|
private final JwtParser parser;
|
||||||
|
|
||||||
|
public HardwareInfoTokenVerifier(LaunchServer server) {
|
||||||
|
this.parser = Jwts.parser()
|
||||||
|
.requireIssuer("LaunchServer")
|
||||||
|
.verifyWith(server.keyAgreementManager.ecdsaPublicKey)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean accept(Client client, AuthProviderPair pair, String extendedToken) {
|
||||||
|
try {
|
||||||
|
var parse = parser.parseClaimsJws(extendedToken);
|
||||||
|
String hardwareInfoId = parse.getBody().get("hardware", String.class);
|
||||||
|
if (hardwareInfoId == null) return false;
|
||||||
|
if (client.auth == null) return false;
|
||||||
|
var hardwareSupport = client.auth.core.isSupport(AuthSupportHardware.class);
|
||||||
|
if (hardwareSupport == null) return false;
|
||||||
|
UserHardware hardware = hardwareSupport.getHardwareInfoById(hardwareInfoId);
|
||||||
|
if (client.trustLevel == null) client.trustLevel = new Client.TrustLevel();
|
||||||
|
client.trustLevel.hardwareInfo = hardware;
|
||||||
|
return true;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.error("Hardware JWT error", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class PublicKeyTokenVerifier implements RestoreResponse.ExtendedTokenProvider {
|
||||||
|
private transient final Logger logger = LogManager.getLogger();
|
||||||
|
private final JwtParser parser;
|
||||||
|
|
||||||
|
public PublicKeyTokenVerifier(LaunchServer server) {
|
||||||
|
this.parser = Jwts.parser()
|
||||||
|
.requireIssuer("LaunchServer")
|
||||||
|
.verifyWith(server.keyAgreementManager.ecdsaPublicKey)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean accept(Client client, AuthProviderPair pair, String extendedToken) {
|
||||||
|
try {
|
||||||
|
var parse = parser.parseClaimsJws(extendedToken);
|
||||||
|
String publicKey = parse.getBody().get("publicKey", String.class);
|
||||||
|
if (publicKey == null) return false;
|
||||||
|
if (client.trustLevel == null) client.trustLevel = new Client.TrustLevel();
|
||||||
|
client.trustLevel.publicKey = Base64.getDecoder().decode(publicKey);
|
||||||
|
return true;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.error("Public Key JWT error", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package pro.gravit.launchserver.auth.protect;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.response.auth.AuthResponse;
|
||||||
|
|
||||||
|
public class NoProtectHandler extends ProtectHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean allowGetAccessToken(AuthResponse.AuthContext context) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean allowJoinServer(Client client) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package pro.gravit.launchserver.auth.protect;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.response.auth.AuthResponse;
|
||||||
|
import pro.gravit.utils.ProviderMap;
|
||||||
|
|
||||||
|
public abstract class ProtectHandler {
|
||||||
|
public static final ProviderMap<ProtectHandler> providers = new ProviderMap<>("ProtectHandler");
|
||||||
|
private static boolean registredHandl = false;
|
||||||
|
|
||||||
|
|
||||||
|
public static void registerHandlers() {
|
||||||
|
if (!registredHandl) {
|
||||||
|
providers.register("none", NoProtectHandler.class);
|
||||||
|
providers.register("std", StdProtectHandler.class);
|
||||||
|
providers.register("advanced", AdvancedProtectHandler.class);
|
||||||
|
registredHandl = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract boolean allowGetAccessToken(AuthResponse.AuthContext context);
|
||||||
|
public boolean allowJoinServer(Client client) {
|
||||||
|
return client.isAuth && client.type == AuthResponse.ConnectTypes.CLIENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init(LaunchServer server) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
//public abstract
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package pro.gravit.launchserver.auth.protect;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launcher.base.profiles.ClientProfile;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.auth.protect.interfaces.ProfilesProtectHandler;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.response.auth.AuthResponse;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class StdProtectHandler extends ProtectHandler implements ProfilesProtectHandler {
|
||||||
|
private transient final Logger logger = LogManager.getLogger();
|
||||||
|
public Map<String, List<String>> profileWhitelist = new HashMap<>();
|
||||||
|
public List<String> allowUpdates = new ArrayList<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean allowGetAccessToken(AuthResponse.AuthContext context) {
|
||||||
|
return (context.authType == AuthResponse.ConnectTypes.CLIENT) && context.client.checkSign;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(LaunchServer server) {
|
||||||
|
if (profileWhitelist != null && !profileWhitelist.isEmpty()) {
|
||||||
|
logger.warn("profileWhitelist deprecated. Please use permission 'launchserver.profile.PROFILE_UUID.show' and 'launchserver.profile.PROFILE_UUID.enter'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canGetProfile(ClientProfile profile, Client client) {
|
||||||
|
return (client.isAuth && !profile.isLimited()) || isWhitelisted("launchserver.profile.%s.show", profile, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canChangeProfile(ClientProfile profile, Client client) {
|
||||||
|
return (client.isAuth && !profile.isLimited()) || isWhitelisted("launchserver.profile.%s.enter", profile, client);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canGetUpdates(String updatesDirName, Client client) {
|
||||||
|
return client.profile != null && (client.profile.getDir().equals(updatesDirName) || client.profile.getAssetDir().equals(updatesDirName) || allowUpdates.contains(updatesDirName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWhitelisted(String property, ClientProfile profile, Client client) {
|
||||||
|
if (client.permissions != null) {
|
||||||
|
String permByUUID = property.formatted(profile.getUUID());
|
||||||
|
if (client.permissions.hasPerm(permByUUID)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String permByTitle = property.formatted(profile.getTitle().toLowerCase(Locale.ROOT));
|
||||||
|
if (client.permissions.hasPerm(permByTitle)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> allowedUsername = profileWhitelist.get(profile.getTitle());
|
||||||
|
return allowedUsername != null && allowedUsername.contains(client.username);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package pro.gravit.launchserver.auth.protect.interfaces;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.response.secure.HardwareReportResponse;
|
||||||
|
|
||||||
|
public interface HardwareProtectHandler {
|
||||||
|
void onHardwareReport(HardwareReportResponse response, Client client);
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package pro.gravit.launchserver.auth.protect.interfaces;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface JoinServerProtectHandler {
|
||||||
|
default boolean onJoinServer(String serverID, String username, UUID uuid, Client client) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package pro.gravit.launchserver.auth.protect.interfaces;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.profiles.ClientProfile;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
|
||||||
|
public interface ProfilesProtectHandler {
|
||||||
|
default boolean canGetProfiles(Client client) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean canGetProfile(ClientProfile profile, Client client) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean canChangeProfile(ClientProfile profile, Client client) {
|
||||||
|
return client.isAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
default boolean canGetUpdates(String updatesDirName, Client client) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package pro.gravit.launchserver.auth.protect.interfaces;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.events.request.GetSecureLevelInfoRequestEvent;
|
||||||
|
import pro.gravit.launcher.base.events.request.SecurityReportRequestEvent;
|
||||||
|
import pro.gravit.launcher.base.events.request.VerifySecureLevelKeyRequestEvent;
|
||||||
|
import pro.gravit.launchserver.socket.Client;
|
||||||
|
import pro.gravit.launchserver.socket.response.secure.SecurityReportResponse;
|
||||||
|
import pro.gravit.utils.helper.SecurityHelper;
|
||||||
|
|
||||||
|
import java.security.Signature;
|
||||||
|
import java.security.SignatureException;
|
||||||
|
import java.security.interfaces.ECPublicKey;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
|
||||||
|
public interface SecureProtectHandler {
|
||||||
|
default byte[] generateSecureLevelKey() {
|
||||||
|
return SecurityHelper.randomBytes(128);
|
||||||
|
}
|
||||||
|
|
||||||
|
default void verifySecureLevelKey(byte[] publicKey, byte[] data, byte[] signature) throws InvalidKeySpecException, SignatureException {
|
||||||
|
if (publicKey == null || signature == null) throw new InvalidKeySpecException();
|
||||||
|
ECPublicKey pubKey = SecurityHelper.toPublicECDSAKey(publicKey);
|
||||||
|
Signature sign = SecurityHelper.newECVerifySignature(pubKey);
|
||||||
|
sign.update(data);
|
||||||
|
sign.verify(signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
GetSecureLevelInfoRequestEvent onGetSecureLevelInfo(GetSecureLevelInfoRequestEvent event);
|
||||||
|
|
||||||
|
boolean allowGetSecureLevelInfo(Client client);
|
||||||
|
|
||||||
|
default SecurityReportRequestEvent onSecurityReport(SecurityReportResponse report, Client client) {
|
||||||
|
return new SecurityReportRequestEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
default VerifySecureLevelKeyRequestEvent onSuccessVerify(Client client) {
|
||||||
|
return new VerifySecureLevelKeyRequestEvent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package pro.gravit.launchserver.auth.texture;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launcher.base.profiles.Texture;
|
||||||
|
import pro.gravit.launchserver.HttpRequester;
|
||||||
|
import pro.gravit.utils.helper.SecurityHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class JsonTextureProvider extends TextureProvider {
|
||||||
|
private static final Type MAP_TYPE = new TypeToken<Map<String, JsonTexture>>() {
|
||||||
|
}.getType();
|
||||||
|
private transient final Logger logger = LogManager.getLogger();
|
||||||
|
private transient final HttpRequester requester = new HttpRequester();
|
||||||
|
public String url;
|
||||||
|
public String bearerToken;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
//None
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Texture getCloakTexture(UUID uuid, String username, String client) {
|
||||||
|
logger.warn("Ineffective get cloak texture for {}", username);
|
||||||
|
return getAssets(uuid, username, client).get("CAPE");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Texture getSkinTexture(UUID uuid, String username, String client) {
|
||||||
|
logger.warn("Ineffective get skin texture for {}", username);
|
||||||
|
return getAssets(uuid, username, client).get("SKIN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Texture> getAssets(UUID uuid, String username, String client) {
|
||||||
|
try {
|
||||||
|
Map<String, JsonTexture> map = requester.<Map<String, JsonTexture>>send(requester.get(RequestTextureProvider.getTextureURL(url, uuid, username, client), bearerToken), MAP_TYPE).getOrThrow();
|
||||||
|
return JsonTexture.convertMap(map);
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("JsonTextureProvider", e);
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record JsonTexture(String url, String digest, Map<String, String> metadata) {
|
||||||
|
public Texture toTexture() {
|
||||||
|
return new Texture(url, digest == null ? null : SecurityHelper.fromHex(digest), metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<String, Texture> convertMap(Map<String, JsonTexture> map) {
|
||||||
|
if (map == null) {
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
Map<String, Texture> res = new HashMap<>();
|
||||||
|
for(var e : map.entrySet()) {
|
||||||
|
res.put(e.getKey(), e.getValue().toTexture());
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,15 @@
|
||||||
package ru.gravit.launchserver.texture;
|
package pro.gravit.launchserver.auth.texture;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.profiles.Texture;
|
||||||
|
import pro.gravit.utils.helper.VerifyHelper;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import ru.gravit.launcher.LauncherAPI;
|
|
||||||
import ru.gravit.utils.helper.VerifyHelper;
|
|
||||||
import ru.gravit.launcher.profiles.Texture;
|
|
||||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
|
||||||
|
|
||||||
public final class NullTextureProvider extends TextureProvider {
|
public final class NullTextureProvider extends TextureProvider {
|
||||||
private volatile TextureProvider provider;
|
private volatile TextureProvider provider;
|
||||||
|
|
||||||
public NullTextureProvider(BlockConfigEntry block) {
|
|
||||||
super(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
TextureProvider provider = this.provider;
|
TextureProvider provider = this.provider;
|
||||||
|
@ -37,7 +31,7 @@ public Texture getSkinTexture(UUID uuid, String username, String client) throws
|
||||||
return getProvider().getSkinTexture(uuid, username, client);
|
return getProvider().getSkinTexture(uuid, username, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
@LauncherAPI
|
|
||||||
public void setBackend(TextureProvider provider) {
|
public void setBackend(TextureProvider provider) {
|
||||||
this.provider = provider;
|
this.provider = provider;
|
||||||
}
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package pro.gravit.launchserver.auth.texture;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.Launcher;
|
||||||
|
import pro.gravit.launcher.base.profiles.Texture;
|
||||||
|
import pro.gravit.utils.helper.CommonHelper;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class RequestTextureProvider extends TextureProvider {
|
||||||
|
// Instance
|
||||||
|
public String skinURL;
|
||||||
|
public String cloakURL;
|
||||||
|
|
||||||
|
public String skinLocalPath;
|
||||||
|
public String cloakLocalPath;
|
||||||
|
|
||||||
|
public RequestTextureProvider() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public RequestTextureProvider(String skinURL, String cloakURL) {
|
||||||
|
this.skinURL = skinURL;
|
||||||
|
this.cloakURL = cloakURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Texture getTexture(String url, boolean cloak) throws IOException {
|
||||||
|
try {
|
||||||
|
return new Texture(url, cloak, null);
|
||||||
|
} catch (FileNotFoundException ignored) {
|
||||||
|
return null; // Simply not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Texture getTexture(String url, Path local, boolean cloak) throws IOException {
|
||||||
|
try {
|
||||||
|
return new Texture(url, local, cloak, null);
|
||||||
|
} catch (FileNotFoundException ignored) {
|
||||||
|
return null; // Simply not found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTextureURL(String url, UUID uuid, String username, String client) {
|
||||||
|
return CommonHelper.replace(url, "username", IOHelper.urlEncode(username),
|
||||||
|
"uuid", IOHelper.urlEncode(uuid.toString()), "hash", IOHelper.urlEncode(Launcher.toHash(uuid)),
|
||||||
|
"client", IOHelper.urlEncode(client == null ? "unknown" : client));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Texture getCloakTexture(UUID uuid, String username, String client) throws IOException {
|
||||||
|
String textureUrl = getTextureURL(cloakURL, uuid, username, client);
|
||||||
|
if (cloakLocalPath == null) {
|
||||||
|
return getTexture(textureUrl, true);
|
||||||
|
} else {
|
||||||
|
String path = getTextureURL(cloakLocalPath, uuid, username, client);
|
||||||
|
return getTexture(textureUrl, Paths.get(path), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Texture getSkinTexture(UUID uuid, String username, String client) throws IOException {
|
||||||
|
String textureUrl = getTextureURL(skinURL, uuid, username, client);
|
||||||
|
if (skinLocalPath == null) {
|
||||||
|
return getTexture(textureUrl, false);
|
||||||
|
} else {
|
||||||
|
String path = getTextureURL(skinLocalPath, uuid, username, client);
|
||||||
|
return getTexture(textureUrl, Paths.get(path), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package pro.gravit.launchserver.auth.texture;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.profiles.Texture;
|
||||||
|
import pro.gravit.utils.ProviderMap;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public abstract class TextureProvider implements AutoCloseable {
|
||||||
|
public static final ProviderMap<TextureProvider> providers = new ProviderMap<>("TextureProvider");
|
||||||
|
private static boolean registredProv = false;
|
||||||
|
|
||||||
|
public static void registerProviders() {
|
||||||
|
if (!registredProv) {
|
||||||
|
providers.register("null", NullTextureProvider.class);
|
||||||
|
providers.register("void", VoidTextureProvider.class);
|
||||||
|
|
||||||
|
// Auth providers that doesn't do nothing :D
|
||||||
|
providers.register("request", RequestTextureProvider.class);
|
||||||
|
providers.register("json", JsonTextureProvider.class);
|
||||||
|
registredProv = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract void close() throws IOException;
|
||||||
|
|
||||||
|
|
||||||
|
public abstract Texture getCloakTexture(UUID uuid, String username, String client) throws IOException;
|
||||||
|
|
||||||
|
|
||||||
|
public abstract Texture getSkinTexture(UUID uuid, String username, String client) throws IOException;
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
public SkinAndCloakTextures getTextures(UUID uuid, String username, String client) {
|
||||||
|
|
||||||
|
Texture skin;
|
||||||
|
try {
|
||||||
|
skin = getSkinTexture(uuid, username, client);
|
||||||
|
} catch (IOException e) {
|
||||||
|
skin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cloak texture
|
||||||
|
Texture cloak;
|
||||||
|
try {
|
||||||
|
cloak = getCloakTexture(uuid, username, client);
|
||||||
|
} catch (IOException e) {
|
||||||
|
cloak = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SkinAndCloakTextures(skin, cloak);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Texture> getAssets(UUID uuid, String username, String client) {
|
||||||
|
|
||||||
|
Texture skin;
|
||||||
|
try {
|
||||||
|
skin = getSkinTexture(uuid, username, client);
|
||||||
|
} catch (IOException e) {
|
||||||
|
skin = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get cloak texture
|
||||||
|
Texture cloak;
|
||||||
|
try {
|
||||||
|
cloak = getCloakTexture(uuid, username, client);
|
||||||
|
} catch (IOException e) {
|
||||||
|
cloak = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, Texture> map = new HashMap<>();
|
||||||
|
if (skin != null) {
|
||||||
|
map.put("SKIN", skin);
|
||||||
|
}
|
||||||
|
if (cloak != null) {
|
||||||
|
map.put("CAPE", cloak);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
|
public static class SkinAndCloakTextures {
|
||||||
|
public final Texture skin;
|
||||||
|
public final Texture cloak;
|
||||||
|
|
||||||
|
public SkinAndCloakTextures(Texture skin, Texture cloak) {
|
||||||
|
this.skin = skin;
|
||||||
|
this.cloak = cloak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,10 @@
|
||||||
package ru.gravit.launchserver.texture;
|
package pro.gravit.launchserver.auth.texture;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.profiles.Texture;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import ru.gravit.launcher.profiles.Texture;
|
|
||||||
import ru.gravit.launcher.serialize.config.entry.BlockConfigEntry;
|
|
||||||
|
|
||||||
public final class VoidTextureProvider extends TextureProvider {
|
public final class VoidTextureProvider extends TextureProvider {
|
||||||
public VoidTextureProvider(BlockConfigEntry block) {
|
|
||||||
super(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() {
|
public void close() {
|
|
@ -0,0 +1,186 @@
|
||||||
|
package pro.gravit.launchserver.auth.updates;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launcher.core.hasher.HashedDir;
|
||||||
|
import pro.gravit.launcher.core.serialize.HInput;
|
||||||
|
import pro.gravit.launcher.core.serialize.HOutput;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.modules.events.LaunchServerUpdatesSyncEvent;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class LocalUpdatesProvider extends UpdatesProvider {
|
||||||
|
private final transient Logger logger = LogManager.getLogger();
|
||||||
|
public String cacheFile = ".updates-cache";
|
||||||
|
public String updatesDir = "updates";
|
||||||
|
public boolean cacheUpdates = true;
|
||||||
|
private volatile transient Map<String, HashedDir> updatesDirMap;
|
||||||
|
|
||||||
|
private void writeCache(Path file) throws IOException {
|
||||||
|
try (HOutput output = new HOutput(IOHelper.newOutput(file))) {
|
||||||
|
output.writeLength(updatesDirMap.size(), 0);
|
||||||
|
for (Map.Entry<String, HashedDir> entry : updatesDirMap.entrySet()) {
|
||||||
|
output.writeString(entry.getKey(), 0);
|
||||||
|
entry.getValue().write(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("Saved {} updates to cache", updatesDirMap.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void readCache(Path file) throws IOException {
|
||||||
|
Map<String, HashedDir> updatesDirMap = new HashMap<>(16);
|
||||||
|
try (HInput input = new HInput(IOHelper.newInput(file))) {
|
||||||
|
int size = input.readLength(0);
|
||||||
|
for (int i = 0; i < size; ++i) {
|
||||||
|
String name = input.readString(0);
|
||||||
|
HashedDir dir = new HashedDir(input);
|
||||||
|
updatesDirMap.put(name, dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("Found {} updates from cache", updatesDirMap.size());
|
||||||
|
this.updatesDirMap = Collections.unmodifiableMap(updatesDirMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readUpdatesFromCache() throws IOException {
|
||||||
|
readCache(Path.of(cacheFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void readUpdatesDir() throws IOException {
|
||||||
|
var cacheFilePath = Path.of(cacheFile);
|
||||||
|
if (cacheUpdates) {
|
||||||
|
if (Files.exists(cacheFilePath)) {
|
||||||
|
try {
|
||||||
|
readCache(cacheFilePath);
|
||||||
|
return;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.error("Read updates cache failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sync(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(LaunchServer server) {
|
||||||
|
super.init(server);
|
||||||
|
try {
|
||||||
|
if (!IOHelper.isDir(Path.of(updatesDir)))
|
||||||
|
Files.createDirectory(Path.of(updatesDir));
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Updates not synced", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void syncInitially() throws IOException {
|
||||||
|
readUpdatesDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sync(Collection<String> dirs) throws IOException {
|
||||||
|
logger.info("Syncing updates dir");
|
||||||
|
Map<String, HashedDir> newUpdatesDirMap = new HashMap<>(16);
|
||||||
|
try (DirectoryStream<Path> dirStream = Files.newDirectoryStream(Path.of(updatesDir))) {
|
||||||
|
for (final Path updateDir : dirStream) {
|
||||||
|
if (Files.isHidden(updateDir))
|
||||||
|
continue; // Skip hidden
|
||||||
|
|
||||||
|
// Resolve name and verify is dir
|
||||||
|
String name = IOHelper.getFileName(updateDir);
|
||||||
|
if (!IOHelper.isDir(updateDir)) {
|
||||||
|
if (!IOHelper.isFile(updateDir) && Stream.of(".jar", ".exe", ".hash").noneMatch(e -> updateDir.toString().endsWith(e)))
|
||||||
|
logger.warn("Not update dir: '{}'", name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add from previous map (it's guaranteed to be non-null)
|
||||||
|
if (dirs != null && !dirs.contains(name)) {
|
||||||
|
HashedDir hdir = updatesDirMap.get(name);
|
||||||
|
if (hdir != null) {
|
||||||
|
newUpdatesDirMap.put(name, hdir);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync and sign update dir
|
||||||
|
logger.info("Syncing '{}' update dir", name);
|
||||||
|
HashedDir updateHDir = new HashedDir(updateDir, null, true, true);
|
||||||
|
newUpdatesDirMap.put(name, updateHDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatesDirMap = Collections.unmodifiableMap(newUpdatesDirMap);
|
||||||
|
if (cacheUpdates) {
|
||||||
|
try {
|
||||||
|
writeCache(Path.of(cacheFile));
|
||||||
|
} catch (Throwable e) {
|
||||||
|
logger.error("Write updates cache failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server.modulesManager.invokeEvent(new LaunchServerUpdatesSyncEvent(server));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HashedDir getUpdatesDir(String updateName) {
|
||||||
|
return updatesDirMap.get(updateName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveUpdateName(String updateName) {
|
||||||
|
if(updateName == null) {
|
||||||
|
return Path.of(updatesDir);
|
||||||
|
}
|
||||||
|
return Path.of(updatesDir).resolve(updateName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void upload(String updateName, Map<String, Path> files, boolean deleteAfterUpload) throws IOException {
|
||||||
|
var path = resolveUpdateName(updateName);
|
||||||
|
for(var e : files.entrySet()) {
|
||||||
|
var target = path.resolve(e.getKey());
|
||||||
|
var source = e.getValue();
|
||||||
|
IOHelper.createParentDirs(target);
|
||||||
|
if(deleteAfterUpload) {
|
||||||
|
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
} else {
|
||||||
|
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Path> download(String updateName, List<String> files) {
|
||||||
|
var path = resolveUpdateName(updateName);
|
||||||
|
Map<String, Path> map = new HashMap<>();
|
||||||
|
for(var e : files) {
|
||||||
|
map.put(e, path.resolve(e));
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String updateName, List<String> files) throws IOException {
|
||||||
|
var path = resolveUpdateName(updateName);
|
||||||
|
for(var e : files) {
|
||||||
|
var target = path.resolve(e);
|
||||||
|
Files.delete(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(String updateName) throws IOException {
|
||||||
|
var path = resolveUpdateName(updateName);
|
||||||
|
IOHelper.deleteDir(path, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void create(String updateName) throws IOException {
|
||||||
|
var path = resolveUpdateName(updateName);
|
||||||
|
Files.createDirectories(path);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package pro.gravit.launchserver.auth.updates;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.core.hasher.HashedDir;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.utils.ProviderMap;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public abstract class UpdatesProvider {
|
||||||
|
public static final ProviderMap<UpdatesProvider> providers = new ProviderMap<>("UpdatesProvider");
|
||||||
|
private static boolean registredProviders = false;
|
||||||
|
protected transient LaunchServer server;
|
||||||
|
|
||||||
|
public static void registerProviders() {
|
||||||
|
if (!registredProviders) {
|
||||||
|
providers.register("local", LocalUpdatesProvider.class);
|
||||||
|
registredProviders = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init(LaunchServer server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sync() throws IOException {
|
||||||
|
sync(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void syncInitially() throws IOException;
|
||||||
|
|
||||||
|
public abstract void sync(Collection<String> updateNames) throws IOException;
|
||||||
|
|
||||||
|
public abstract HashedDir getUpdatesDir(String updateName);
|
||||||
|
|
||||||
|
public abstract void upload(String updateName, Map<String, Path> files, boolean deleteAfterUpload) throws IOException;
|
||||||
|
|
||||||
|
public abstract Map<String, Path> download(String updateName, List<String> files);
|
||||||
|
|
||||||
|
public abstract void delete(String updateName, List<String> files) throws IOException;
|
||||||
|
|
||||||
|
public abstract void delete(String updateName) throws IOException;
|
||||||
|
|
||||||
|
public abstract void create(String updateName) throws IOException;
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package pro.gravit.launchserver.binary;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launchserver.binary.tasks.LauncherBuildTask;
|
||||||
|
import pro.gravit.utils.helper.CommonHelper;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public abstract class BinaryPipeline {
|
||||||
|
public final List<LauncherBuildTask> tasks = new ArrayList<>();
|
||||||
|
public final Path buildDir;
|
||||||
|
public final String nameFormat;
|
||||||
|
protected transient final Logger logger = LogManager.getLogger();
|
||||||
|
|
||||||
|
public BinaryPipeline(Path buildDir, String nameFormat) {
|
||||||
|
this.buildDir = buildDir;
|
||||||
|
this.nameFormat = nameFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCounted(int count, Predicate<LauncherBuildTask> pred, LauncherBuildTask taskAdd) {
|
||||||
|
List<LauncherBuildTask> indexes = new ArrayList<>();
|
||||||
|
tasks.stream().filter(pred).forEach(indexes::add);
|
||||||
|
indexes.forEach(e -> tasks.add(tasks.indexOf(e) + count, taskAdd));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replaceCounted(int count, Predicate<LauncherBuildTask> pred, LauncherBuildTask taskRep) {
|
||||||
|
List<LauncherBuildTask> indexes = new ArrayList<>();
|
||||||
|
tasks.stream().filter(pred).forEach(indexes::add);
|
||||||
|
indexes.forEach(e -> tasks.set(tasks.indexOf(e) + count, taskRep));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addPre(Predicate<LauncherBuildTask> pred, LauncherBuildTask taskAdd) {
|
||||||
|
addCounted(-1, pred, taskAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(Predicate<LauncherBuildTask> pred, LauncherBuildTask taskAdd) {
|
||||||
|
addCounted(0, pred, taskAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addAfter(Predicate<LauncherBuildTask> pred, LauncherBuildTask taskAdd) {
|
||||||
|
addCounted(1, pred, taskAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replacePre(Predicate<LauncherBuildTask> pred, LauncherBuildTask taskRep) {
|
||||||
|
replaceCounted(-1, pred, taskRep);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replace(Predicate<LauncherBuildTask> pred, LauncherBuildTask taskRep) {
|
||||||
|
replaceCounted(0, pred, taskRep);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replaceAfter(Predicate<LauncherBuildTask> pred, LauncherBuildTask taskRep) {
|
||||||
|
replaceCounted(1, pred, taskRep);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends LauncherBuildTask> List<T> getTasksByClass(Class<T> taskClass) {
|
||||||
|
return tasks.stream().filter(taskClass::isInstance).map(taskClass::cast).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends LauncherBuildTask> Optional<T> getTaskByClass(Class<T> taskClass) {
|
||||||
|
return tasks.stream().filter(taskClass::isInstance).map(taskClass::cast).findFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<LauncherBuildTask> getTaskBefore(Predicate<LauncherBuildTask> pred) {
|
||||||
|
LauncherBuildTask last = null;
|
||||||
|
for(var e : tasks) {
|
||||||
|
if(pred.test(e)) {
|
||||||
|
return Optional.ofNullable(last);
|
||||||
|
}
|
||||||
|
last = e;
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String nextName(String taskName) {
|
||||||
|
return nameFormat.formatted(taskName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path nextPath(String taskName) {
|
||||||
|
return buildDir.resolve(nextName(taskName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path nextPath(LauncherBuildTask task) {
|
||||||
|
return nextPath(task.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path nextLowerPath(LauncherBuildTask task) {
|
||||||
|
return nextPath(CommonHelper.low(task.getName()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,281 @@
|
||||||
|
package pro.gravit.launchserver.binary;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import pro.gravit.launcher.base.Launcher;
|
||||||
|
import pro.gravit.launcher.core.serialize.HOutput;
|
||||||
|
import pro.gravit.launcher.core.serialize.stream.StreamObject;
|
||||||
|
import pro.gravit.launchserver.binary.tasks.MainBuildTask;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
import pro.gravit.utils.helper.SecurityHelper;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.CipherOutputStream;
|
||||||
|
import javax.crypto.NoSuchPaddingException;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.FileVisitResult;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.SimpleFileVisitor;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipException;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
import static pro.gravit.utils.helper.IOHelper.newZipEntry;
|
||||||
|
|
||||||
|
public class BuildContext {
|
||||||
|
public final ZipOutputStream output;
|
||||||
|
public final List<JarFile> readerClassPath;
|
||||||
|
public final MainBuildTask task;
|
||||||
|
public final HashSet<String> fileList;
|
||||||
|
public final HashSet<String> clientModules;
|
||||||
|
public final HashSet<String> legacyClientModules;
|
||||||
|
private Path runtimeDir;
|
||||||
|
private boolean deleteRuntimeDir;
|
||||||
|
|
||||||
|
public BuildContext(ZipOutputStream output, List<JarFile> readerClassPath, MainBuildTask task, Path runtimeDir) {
|
||||||
|
this.output = output;
|
||||||
|
this.readerClassPath = readerClassPath;
|
||||||
|
this.task = task;
|
||||||
|
this.runtimeDir = runtimeDir;
|
||||||
|
fileList = new HashSet<>(1024);
|
||||||
|
clientModules = new HashSet<>();
|
||||||
|
legacyClientModules = new HashSet<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pushFile(String filename, InputStream inputStream) throws IOException {
|
||||||
|
ZipEntry zip = IOHelper.newZipEntry(filename);
|
||||||
|
output.putNextEntry(zip);
|
||||||
|
IOHelper.transfer(inputStream, output);
|
||||||
|
output.closeEntry();
|
||||||
|
fileList.add(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pushFile(String filename, StreamObject object) throws IOException {
|
||||||
|
ZipEntry zip = IOHelper.newZipEntry(filename);
|
||||||
|
output.putNextEntry(zip);
|
||||||
|
object.write(new HOutput(output));
|
||||||
|
output.closeEntry();
|
||||||
|
fileList.add(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pushFile(String filename, Object object, Type type) throws IOException {
|
||||||
|
ZipEntry zip = IOHelper.newZipEntry(filename);
|
||||||
|
output.putNextEntry(zip);
|
||||||
|
try (BufferedWriter w = IOHelper.newWriter(IOHelper.nonClosing(output))) {
|
||||||
|
Launcher.gsonManager.gson.toJson(object, type);
|
||||||
|
}
|
||||||
|
output.closeEntry();
|
||||||
|
fileList.add(filename);
|
||||||
|
pushBytes(filename, IOHelper.encode(Launcher.gsonManager.gson.toJson(object, type)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pushDir(Path dir, String targetDir, Map<String, byte[]> hashMap, boolean hidden) throws IOException {
|
||||||
|
IOHelper.walk(dir, new RuntimeDirVisitor(output, hashMap, dir, targetDir), hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pushEncryptedDir(Path dir, String targetDir, String aesHexKey, Map<String, byte[]> hashMap, boolean hidden) throws IOException {
|
||||||
|
IOHelper.walk(dir, new EncryptedRuntimeDirVisitor(output, aesHexKey, hashMap, dir, targetDir), hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pushBytes(String filename, byte[] bytes) throws IOException {
|
||||||
|
ZipEntry zip = IOHelper.newZipEntry(filename);
|
||||||
|
output.putNextEntry(zip);
|
||||||
|
output.write(bytes);
|
||||||
|
output.closeEntry();
|
||||||
|
fileList.add(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pushJarFile(Path jarfile, Predicate<ZipEntry> filter, Predicate<String> needTransform) throws IOException {
|
||||||
|
pushJarFile(jarfile.toUri().toURL(), filter, needTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Path getRuntimeDir() {
|
||||||
|
return runtimeDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRuntimeDir(Path runtimeDir) {
|
||||||
|
this.runtimeDir = runtimeDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pushJarFile(URL jarfile, Predicate<ZipEntry> filter, Predicate<String> needTransform) throws IOException {
|
||||||
|
try (ZipInputStream input = new ZipInputStream(IOHelper.newInput(jarfile))) {
|
||||||
|
ZipEntry e = input.getNextEntry();
|
||||||
|
while (e != null) {
|
||||||
|
String filename = e.getName();
|
||||||
|
if (e.isDirectory() || fileList.contains(filename) || filter.test(e)) {
|
||||||
|
e = input.getNextEntry();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
output.putNextEntry(IOHelper.newZipEntry(e));
|
||||||
|
if (filename.endsWith(".class")) {
|
||||||
|
String classname = filename.replace('/', '.').substring(0,
|
||||||
|
filename.length() - ".class".length());
|
||||||
|
if (!needTransform.test(classname)) {
|
||||||
|
IOHelper.transfer(input, output);
|
||||||
|
} else {
|
||||||
|
byte[] bytes = IOHelper.read(input);
|
||||||
|
bytes = task.transformClass(bytes, classname, this);
|
||||||
|
output.write(bytes);
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
IOHelper.transfer(input, output);
|
||||||
|
fileList.add(filename);
|
||||||
|
e = input.getNextEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isDeleteRuntimeDir() {
|
||||||
|
return deleteRuntimeDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDeleteRuntimeDir(boolean deleteRuntimeDir) {
|
||||||
|
this.deleteRuntimeDir = deleteRuntimeDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final static class RuntimeDirVisitor extends SimpleFileVisitor<Path> {
|
||||||
|
private final ZipOutputStream output;
|
||||||
|
private final Map<String, byte[]> hashs;
|
||||||
|
private final Path sourceDir;
|
||||||
|
private final String targetDir;
|
||||||
|
|
||||||
|
private RuntimeDirVisitor(ZipOutputStream output, Map<String, byte[]> hashs, Path sourceDir, String targetDir) {
|
||||||
|
this.output = output;
|
||||||
|
this.hashs = hashs;
|
||||||
|
this.sourceDir = sourceDir;
|
||||||
|
this.targetDir = targetDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||||
|
String dirName = IOHelper.toString(sourceDir.relativize(dir));
|
||||||
|
output.putNextEntry(newEntry(dirName + '/'));
|
||||||
|
return super.preVisitDirectory(dir, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
|
String fileName = IOHelper.toString(sourceDir.relativize(file));
|
||||||
|
if (hashs != null)
|
||||||
|
hashs.put(fileName, SecurityHelper.digest(SecurityHelper.DigestAlgorithm.MD5, file));
|
||||||
|
|
||||||
|
// Create zip entry and transfer contents
|
||||||
|
output.putNextEntry(newEntry(fileName));
|
||||||
|
IOHelper.transfer(file, output);
|
||||||
|
|
||||||
|
// Return result
|
||||||
|
return super.visitFile(file, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ZipEntry newEntry(String fileName) {
|
||||||
|
return newZipEntry(targetDir + IOHelper.CROSS_SEPARATOR + fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final static class EncryptedRuntimeDirVisitor extends SimpleFileVisitor<Path> {
|
||||||
|
private final ZipOutputStream output;
|
||||||
|
private final Map<String, byte[]> hashs;
|
||||||
|
private final Path sourceDir;
|
||||||
|
private final String targetDir;
|
||||||
|
private final SecretKeySpec sKeySpec;
|
||||||
|
private final IvParameterSpec iKeySpec;
|
||||||
|
private final transient Logger logger = LogManager.getLogger();
|
||||||
|
|
||||||
|
private EncryptedRuntimeDirVisitor(ZipOutputStream output, String aesKey, Map<String, byte[]> hashs, Path sourceDir, String targetDir) {
|
||||||
|
this.output = output;
|
||||||
|
this.hashs = hashs;
|
||||||
|
this.sourceDir = sourceDir;
|
||||||
|
this.targetDir = targetDir;
|
||||||
|
try {
|
||||||
|
byte[] key = SecurityHelper.fromHex(aesKey);
|
||||||
|
byte[] compatKey = SecurityHelper.getAESKey(key);
|
||||||
|
sKeySpec = new SecretKeySpec(compatKey, "AES/CBC/PKCS5Padding");
|
||||||
|
iKeySpec = new IvParameterSpec(IOHelper.encode("8u3d90ikr7o67lsq"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||||
|
byte[] digest = SecurityHelper.digest(SecurityHelper.DigestAlgorithm.MD5, file);
|
||||||
|
String fileName = IOHelper.toString(sourceDir.relativize(file));
|
||||||
|
if (hashs != null) {
|
||||||
|
hashs.put(fileName, digest);
|
||||||
|
}
|
||||||
|
// Create zip entry and transfer contents
|
||||||
|
try {
|
||||||
|
output.putNextEntry(newEntry(SecurityHelper.toHex(digest)));
|
||||||
|
} catch (ZipException e) {
|
||||||
|
return super.visitFile(file, attrs); // fix duplicate files
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Cipher cipher = null;
|
||||||
|
try {
|
||||||
|
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, sKeySpec, iKeySpec);
|
||||||
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
|
||||||
|
InvalidAlgorithmParameterException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
try (OutputStream stream = new CipherOutputStream(new NoCloseOutputStream(output), cipher)) {
|
||||||
|
IOHelper.transfer(file, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return result
|
||||||
|
return super.visitFile(file, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ZipEntry newEntry(String fileName) {
|
||||||
|
return newZipEntry(targetDir + IOHelper.CROSS_SEPARATOR + fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NoCloseOutputStream extends OutputStream {
|
||||||
|
private final OutputStream stream;
|
||||||
|
|
||||||
|
private NoCloseOutputStream(OutputStream stream) {
|
||||||
|
this.stream = stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(int i) throws IOException {
|
||||||
|
stream.write(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] b) throws IOException {
|
||||||
|
stream.write(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(byte[] b, int off, int len) throws IOException {
|
||||||
|
stream.write(b, off, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void flush() throws IOException {
|
||||||
|
stream.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package pro.gravit.launchserver.binary;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
|
||||||
|
public class EXELauncherBinary extends LauncherBinary {
|
||||||
|
|
||||||
|
public EXELauncherBinary(LaunchServer server) {
|
||||||
|
super(server, LauncherBinary.resolve(server, ".exe"), "Launcher-%s.exe");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void build() throws IOException {
|
||||||
|
if (IOHelper.isFile(syncBinaryFile)) {
|
||||||
|
Files.delete(syncBinaryFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package pro.gravit.launchserver.binary;
|
||||||
|
|
||||||
|
import pro.gravit.launcher.base.Launcher;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.binary.tasks.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
|
||||||
|
public final class JARLauncherBinary extends LauncherBinary {
|
||||||
|
public final AtomicLong count;
|
||||||
|
public final Path runtimeDir;
|
||||||
|
public final Path buildDir;
|
||||||
|
public final List<Path> coreLibs;
|
||||||
|
public final List<Path> addonLibs;
|
||||||
|
|
||||||
|
public final Map<String, Path> files;
|
||||||
|
|
||||||
|
public JARLauncherBinary(LaunchServer server) throws IOException {
|
||||||
|
super(server, resolve(server, ".jar"), "Launcher-%s.jar");
|
||||||
|
count = new AtomicLong(0);
|
||||||
|
runtimeDir = server.dir.resolve(Launcher.RUNTIME_DIR);
|
||||||
|
buildDir = server.dir.resolve("build");
|
||||||
|
coreLibs = new ArrayList<>();
|
||||||
|
addonLibs = new ArrayList<>();
|
||||||
|
files = new HashMap<>();
|
||||||
|
if (!Files.isDirectory(buildDir)) {
|
||||||
|
Files.deleteIfExists(buildDir);
|
||||||
|
Files.createDirectory(buildDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() {
|
||||||
|
tasks.add(new PrepareBuildTask(server));
|
||||||
|
if (!server.config.sign.enabled) tasks.add(new CertificateAutogenTask(server));
|
||||||
|
tasks.add(new MainBuildTask(server));
|
||||||
|
tasks.add(new AttachJarsTask(server));
|
||||||
|
tasks.add(new AdditionalFixesApplyTask(server));
|
||||||
|
if (server.config.launcher.compress) tasks.add(new CompressBuildTask(server));
|
||||||
|
tasks.add(new SignJarTask(server.config.sign, server));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package pro.gravit.launchserver.binary;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.binary.tasks.LauncherBuildTask;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
import pro.gravit.utils.helper.SecurityHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
public abstract class LauncherBinary extends BinaryPipeline {
|
||||||
|
public final LaunchServer server;
|
||||||
|
public final Path syncBinaryFile;
|
||||||
|
private volatile byte[] digest;
|
||||||
|
|
||||||
|
protected LauncherBinary(LaunchServer server, Path binaryFile, String nameFormat) {
|
||||||
|
super(server.tmpDir.resolve("build"), nameFormat);
|
||||||
|
this.server = server;
|
||||||
|
syncBinaryFile = binaryFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Path resolve(LaunchServer server, String ext) {
|
||||||
|
return Path.of(server.config.binaryName + ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build() throws IOException {
|
||||||
|
logger.info("Building launcher binary file");
|
||||||
|
Path thisPath = null;
|
||||||
|
long time_start = System.currentTimeMillis();
|
||||||
|
long time_this = time_start;
|
||||||
|
for (LauncherBuildTask task : tasks) {
|
||||||
|
logger.info("Task {}", task.getName());
|
||||||
|
Path oldPath = thisPath;
|
||||||
|
thisPath = task.process(oldPath);
|
||||||
|
long time_task_end = System.currentTimeMillis();
|
||||||
|
long time_task = time_task_end - time_this;
|
||||||
|
time_this = time_task_end;
|
||||||
|
logger.info("Task {} processed from {} millis", task.getName(), time_task);
|
||||||
|
}
|
||||||
|
long time_end = System.currentTimeMillis();
|
||||||
|
server.config.updatesProvider.upload(null, Map.of(syncBinaryFile.toString(), thisPath), true);
|
||||||
|
IOHelper.deleteDir(buildDir, false);
|
||||||
|
logger.info("Build successful from {} millis", time_end - time_start);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final boolean exists() {
|
||||||
|
return syncBinaryFile != null && IOHelper.isFile(syncBinaryFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final byte[] getDigest() {
|
||||||
|
return digest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public final boolean sync() {
|
||||||
|
try {
|
||||||
|
var target = syncBinaryFile.toString();
|
||||||
|
var path = server.config.updatesProvider.download(null, List.of(target)).get(target);
|
||||||
|
digest = SecurityHelper.digest(SecurityHelper.DigestAlgorithm.SHA512, IOHelper.read(path));
|
||||||
|
return true;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,45 +1,25 @@
|
||||||
package ru.gravit.launchserver.binary;
|
package pro.gravit.launchserver.binary;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PrivateKey;
|
|
||||||
import java.security.Security;
|
|
||||||
import java.security.cert.Certificate;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.jar.Attributes;
|
|
||||||
import java.util.jar.Manifest;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipOutputStream;
|
|
||||||
|
|
||||||
import org.bouncycastle.cert.jcajce.JcaCertStore;
|
|
||||||
import org.bouncycastle.cms.CMSProcessableByteArray;
|
import org.bouncycastle.cms.CMSProcessableByteArray;
|
||||||
import org.bouncycastle.cms.CMSSignedData;
|
import org.bouncycastle.cms.CMSSignedData;
|
||||||
import org.bouncycastle.cms.CMSSignedDataGenerator;
|
import org.bouncycastle.cms.CMSSignedDataGenerator;
|
||||||
import org.bouncycastle.cms.CMSTypedData;
|
import org.bouncycastle.cms.CMSTypedData;
|
||||||
import org.bouncycastle.cms.SignerInfoGenerator;
|
import pro.gravit.launchserver.helper.SignHelper;
|
||||||
import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
|
||||||
import org.bouncycastle.operator.ContentSigner;
|
|
||||||
import org.bouncycastle.operator.DigestCalculatorProvider;
|
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
|
||||||
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
|
||||||
import org.bouncycastle.util.Store;
|
|
||||||
|
|
||||||
import ru.gravit.utils.helper.IOHelper;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,104 +39,28 @@
|
||||||
* </pre>
|
* </pre>
|
||||||
*/
|
*/
|
||||||
public class SignerJar implements AutoCloseable {
|
public class SignerJar implements AutoCloseable {
|
||||||
/**
|
|
||||||
* Helper output stream that also sends the data to the given {@link com.google.common.hash.Hasher}.
|
|
||||||
*/
|
|
||||||
private static class HashingOutputStream extends OutputStream {
|
|
||||||
private final OutputStream out;
|
|
||||||
private final MessageDigest hasher;
|
|
||||||
|
|
||||||
public HashingOutputStream(OutputStream out, MessageDigest hasher) {
|
|
||||||
this.out = out;
|
|
||||||
this.hasher = hasher;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
out.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void flush() throws IOException {
|
|
||||||
out.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b) throws IOException {
|
|
||||||
out.write(b);
|
|
||||||
hasher.update(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b, int off, int len) throws IOException {
|
|
||||||
out.write(b, off, len);
|
|
||||||
hasher.update(b, off, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(int b) throws IOException {
|
|
||||||
out.write(b);
|
|
||||||
hasher.update((byte) b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String MANIFEST_FN = "META-INF/MANIFEST.MF";
|
private static final String MANIFEST_FN = "META-INF/MANIFEST.MF";
|
||||||
private static final String SIG_FN = "META-INF/SIGNUMO.SF";
|
private static final String DIGEST_HASH = SignHelper.hashFunctionName + "-Digest";
|
||||||
|
private final String SIG_FN;
|
||||||
private static final String SIG_RSA_FN = "META-INF/SIGNUMO.RSA";
|
private final String SIG_KEY_FN;
|
||||||
|
|
||||||
private static final String hashFunctionName = "SHA-256";
|
|
||||||
|
|
||||||
public static KeyStore getStore(Path file, String storepass, String algo) throws IOException {
|
|
||||||
try {
|
|
||||||
KeyStore st = KeyStore.getInstance(algo);
|
|
||||||
st.load(IOHelper.newInput(file), storepass != null ? storepass.toCharArray() : null);
|
|
||||||
return st;
|
|
||||||
} catch (NoSuchAlgorithmException | CertificateException | KeyStoreException e) {
|
|
||||||
throw new IOException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static MessageDigest hasher() {
|
|
||||||
try {
|
|
||||||
return MessageDigest.getInstance(hashFunctionName);
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final ZipOutputStream zos;
|
private final ZipOutputStream zos;
|
||||||
|
|
||||||
private final KeyStore keyStore;
|
|
||||||
|
|
||||||
private final String keyAlias;
|
|
||||||
|
|
||||||
private final String password;
|
|
||||||
private final Map<String, String> manifestAttributes;
|
private final Map<String, String> manifestAttributes;
|
||||||
|
private final Map<String, String> fileDigests;
|
||||||
|
private final Map<String, String> sectionDigests;
|
||||||
|
private final Supplier<CMSSignedDataGenerator> gen;
|
||||||
private String manifestHash;
|
private String manifestHash;
|
||||||
private String manifestMainHash;
|
private String manifestMainHash;
|
||||||
|
|
||||||
private final Map<String, String> fileDigests;
|
public SignerJar(ZipOutputStream out, Supplier<CMSSignedDataGenerator> gen, String sig_fn, String sig_key_fn) {
|
||||||
|
zos = out;
|
||||||
private final Map<String, String> sectionDigests;
|
this.gen = gen;
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor.
|
|
||||||
*
|
|
||||||
* @param out the output stream to write JAR data to
|
|
||||||
* @param keyStore the key store to load given key from
|
|
||||||
* @param keyAlias the name of the key in the store, this key is used to sign the JAR
|
|
||||||
* @param keyPassword the password to access the key
|
|
||||||
*/
|
|
||||||
public SignerJar(OutputStream out, KeyStore keyStore, String keyAlias, String keyPassword) {
|
|
||||||
zos = new ZipOutputStream(out);
|
|
||||||
this.keyStore = keyStore;
|
|
||||||
this.keyAlias = keyAlias;
|
|
||||||
password = keyPassword;
|
|
||||||
|
|
||||||
manifestAttributes = new LinkedHashMap<>();
|
manifestAttributes = new LinkedHashMap<>();
|
||||||
fileDigests = new LinkedHashMap<>();
|
fileDigests = new LinkedHashMap<>();
|
||||||
sectionDigests = new LinkedHashMap<>();
|
sectionDigests = new LinkedHashMap<>();
|
||||||
|
SIG_FN = "META-INF/".concat(sig_fn);
|
||||||
|
SIG_KEY_FN = "META-INF/".concat(sig_key_fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -165,16 +69,10 @@ public SignerJar(OutputStream out, KeyStore keyStore, String keyAlias, String ke
|
||||||
*
|
*
|
||||||
* @param filename name of the file to add (use forward slash as a path separator)
|
* @param filename name of the file to add (use forward slash as a path separator)
|
||||||
* @param contents contents of the file
|
* @param contents contents of the file
|
||||||
* @throws java.io.IOException
|
|
||||||
* @throws NullPointerException if any of the arguments is {@code null}
|
* @throws NullPointerException if any of the arguments is {@code null}
|
||||||
*/
|
*/
|
||||||
public void addFileContents(String filename, byte[] contents) throws IOException {
|
public void addFileContents(String filename, byte[] contents) throws IOException {
|
||||||
zos.putNextEntry(new ZipEntry(filename));
|
addFileContents(filename, new ByteArrayInputStream(contents));
|
||||||
zos.write(contents);
|
|
||||||
zos.closeEntry();
|
|
||||||
|
|
||||||
String hashCode64 = Base64.getEncoder().encodeToString(hasher().digest(contents));
|
|
||||||
fileDigests.put(filename, hashCode64);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -183,17 +81,10 @@ public void addFileContents(String filename, byte[] contents) throws IOException
|
||||||
*
|
*
|
||||||
* @param filename name of the file to add (use forward slash as a path separator)
|
* @param filename name of the file to add (use forward slash as a path separator)
|
||||||
* @param contents contents of the file
|
* @param contents contents of the file
|
||||||
* @throws java.io.IOException
|
|
||||||
* @throws NullPointerException if any of the arguments is {@code null}
|
* @throws NullPointerException if any of the arguments is {@code null}
|
||||||
*/
|
*/
|
||||||
public void addFileContents(String filename, InputStream contents) throws IOException {
|
public void addFileContents(String filename, InputStream contents) throws IOException {
|
||||||
zos.putNextEntry(new ZipEntry(filename));
|
addFileContents(IOHelper.newZipEntry(filename), contents);
|
||||||
byte[] arr = IOHelper.toByteArray(contents);
|
|
||||||
zos.write(arr);
|
|
||||||
zos.closeEntry();
|
|
||||||
|
|
||||||
String hashCode64 = Base64.getEncoder().encodeToString(hasher().digest(arr));
|
|
||||||
fileDigests.put(filename, hashCode64);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -202,16 +93,10 @@ public void addFileContents(String filename, InputStream contents) throws IOExce
|
||||||
*
|
*
|
||||||
* @param entry name of the file to add (use forward slash as a path separator)
|
* @param entry name of the file to add (use forward slash as a path separator)
|
||||||
* @param contents contents of the file
|
* @param contents contents of the file
|
||||||
* @throws java.io.IOException
|
|
||||||
* @throws NullPointerException if any of the arguments is {@code null}
|
* @throws NullPointerException if any of the arguments is {@code null}
|
||||||
*/
|
*/
|
||||||
public void addFileContents(ZipEntry entry, byte[] contents) throws IOException {
|
public void addFileContents(ZipEntry entry, byte[] contents) throws IOException {
|
||||||
zos.putNextEntry(entry);
|
addFileContents(entry, new ByteArrayInputStream(contents));
|
||||||
zos.write(contents);
|
|
||||||
zos.closeEntry();
|
|
||||||
|
|
||||||
String hashCode64 = Base64.getEncoder().encodeToString(hasher().digest(contents));
|
|
||||||
fileDigests.put(entry.getName(), hashCode64);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -220,24 +105,20 @@ public void addFileContents(ZipEntry entry, byte[] contents) throws IOException
|
||||||
*
|
*
|
||||||
* @param entry name of the file to add (use forward slash as a path separator)
|
* @param entry name of the file to add (use forward slash as a path separator)
|
||||||
* @param contents contents of the file
|
* @param contents contents of the file
|
||||||
* @throws java.io.IOException
|
|
||||||
* @throws NullPointerException if any of the arguments is {@code null}
|
* @throws NullPointerException if any of the arguments is {@code null}
|
||||||
*/
|
*/
|
||||||
public void addFileContents(ZipEntry entry, InputStream contents) throws IOException {
|
public void addFileContents(ZipEntry entry, InputStream contents) throws IOException {
|
||||||
zos.putNextEntry(entry);
|
zos.putNextEntry(entry);
|
||||||
byte[] arr = IOHelper.toByteArray(contents);
|
SignHelper.HashingOutputStream out = new SignHelper.HashingNonClosingOutputStream(zos, SignHelper.hasher());
|
||||||
zos.write(arr);
|
IOHelper.transfer(contents, out);
|
||||||
zos.closeEntry();
|
zos.closeEntry();
|
||||||
|
fileDigests.put(entry.getName(), Base64.getEncoder().encodeToString(out.digest()));
|
||||||
String hashCode64 = Base64.getEncoder().encodeToString(hasher().digest(arr));
|
|
||||||
fileDigests.put(entry.getName(), hashCode64);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a header to the manifest of the JAR.
|
* Adds a header to the manifest of the JAR.
|
||||||
*
|
*
|
||||||
* @param name name of the attribute, it is placed into the main section of the manifest file, it cannot be longer
|
* @param name name of the attribute, it is placed into the main section of the manifest file
|
||||||
* than {@value #MANIFEST_ATTR_MAX_LEN} bytes (in utf-8 encoding)
|
|
||||||
* @param value value of the attribute
|
* @param value value of the attribute
|
||||||
*/
|
*/
|
||||||
public void addManifestAttribute(String name, String value) {
|
public void addManifestAttribute(String name, String value) {
|
||||||
|
@ -249,7 +130,6 @@ public void addManifestAttribute(String name, String value) {
|
||||||
* Closes the JAR file by writing the manifest and signature data to it and finishing the ZIP entries. It closes the
|
* Closes the JAR file by writing the manifest and signature data to it and finishing the ZIP entries. It closes the
|
||||||
* underlying stream.
|
* underlying stream.
|
||||||
*
|
*
|
||||||
* @throws java.io.IOException
|
|
||||||
* @throws RuntimeException if the signing goes wrong
|
* @throws RuntimeException if the signing goes wrong
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
|
@ -258,36 +138,16 @@ public void close() throws IOException {
|
||||||
zos.close();
|
zos.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the beast that can actually sign the data.
|
|
||||||
*/
|
|
||||||
private CMSSignedDataGenerator createSignedDataGenerator() throws Exception {
|
|
||||||
Security.addProvider(new BouncyCastleProvider());
|
|
||||||
|
|
||||||
List<Certificate> certChain = new ArrayList<>(Arrays.asList(keyStore.getCertificateChain(keyAlias)));
|
|
||||||
Store certStore = new JcaCertStore(certChain);
|
|
||||||
Certificate cert = keyStore.getCertificate(keyAlias);
|
|
||||||
PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, password != null ? password.toCharArray() : null);
|
|
||||||
ContentSigner signer = new JcaContentSignerBuilder("SHA256WITHRSA").setProvider("BC").build(privateKey);
|
|
||||||
CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
|
|
||||||
DigestCalculatorProvider dcp = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
|
|
||||||
SignerInfoGenerator sig = new JcaSignerInfoGeneratorBuilder(dcp).build(signer, (X509Certificate) cert);
|
|
||||||
generator.addSignerInfoGenerator(sig);
|
|
||||||
generator.addCertificates(certStore);
|
|
||||||
return generator;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finishes the JAR file by writing the manifest and signature data to it and finishing the ZIP entries. It leaves the
|
* Finishes the JAR file by writing the manifest and signature data to it and finishing the ZIP entries. It leaves the
|
||||||
* underlying stream open.
|
* underlying stream open.
|
||||||
*
|
*
|
||||||
* @throws java.io.IOException
|
|
||||||
* @throws RuntimeException if the signing goes wrong
|
* @throws RuntimeException if the signing goes wrong
|
||||||
*/
|
*/
|
||||||
public void finish() throws IOException {
|
public void finish() throws IOException {
|
||||||
writeManifest();
|
writeManifest();
|
||||||
byte sig[] = writeSigFile();
|
byte[] sig = writeSigFile();
|
||||||
writeSignature(sig);
|
writeSignature(sig);
|
||||||
zos.finish();
|
zos.finish();
|
||||||
}
|
}
|
||||||
|
@ -311,7 +171,7 @@ private String hashEntrySection(String name, Attributes attributes) throws IOExc
|
||||||
manifest.write(o);
|
manifest.write(o);
|
||||||
byte[] ob = o.toByteArray();
|
byte[] ob = o.toByteArray();
|
||||||
ob = Arrays.copyOfRange(ob, emptyLen, ob.length);
|
ob = Arrays.copyOfRange(ob, emptyLen, ob.length);
|
||||||
return Base64.getEncoder().encodeToString(hasher().digest(ob));
|
return Base64.getEncoder().encodeToString(SignHelper.hasher().digest(ob));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -320,39 +180,18 @@ private String hashEntrySection(String name, Attributes attributes) throws IOExc
|
||||||
private String hashMainSection(Attributes attributes) throws IOException {
|
private String hashMainSection(Attributes attributes) throws IOException {
|
||||||
Manifest manifest = new Manifest();
|
Manifest manifest = new Manifest();
|
||||||
manifest.getMainAttributes().putAll(attributes);
|
manifest.getMainAttributes().putAll(attributes);
|
||||||
MessageDigest hasher = hasher();
|
SignHelper.HashingOutputStream o = new SignHelper.HashingNonClosingOutputStream(SignHelper.NULL, SignHelper.hasher());
|
||||||
SignerJar.HashingOutputStream o = new SignerJar.HashingOutputStream(new OutputStream() {
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "NullOutputStream";
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Discards the specified byte array. */
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Discards the specified byte array. */
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b, int off, int len) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Discards the specified byte. */
|
|
||||||
@Override
|
|
||||||
public void write(int b) {
|
|
||||||
}
|
|
||||||
}, hasher);
|
|
||||||
manifest.write(o);
|
manifest.write(o);
|
||||||
return Base64.getEncoder().encodeToString(hasher.digest());
|
return Base64.getEncoder().encodeToString(o.digest());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the CMS signed data.
|
* Returns the CMS signed data.
|
||||||
*/
|
*/
|
||||||
private byte[] signSigFile(byte[] sigContents) throws Exception {
|
private byte[] signSigFile(byte[] sigContents) throws Exception {
|
||||||
CMSSignedDataGenerator gen = createSignedDataGenerator();
|
CMSSignedDataGenerator gen = this.gen.get();
|
||||||
CMSTypedData cmsData = new CMSProcessableByteArray(sigContents);
|
CMSTypedData cmsData = new CMSProcessableByteArray(sigContents);
|
||||||
CMSSignedData signedData = gen.generate(cmsData, true);
|
CMSSignedData signedData = gen.generate(cmsData, false);
|
||||||
return signedData.getEncoded();
|
return signedData.getEncoded();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,10 +199,9 @@ private byte[] signSigFile(byte[] sigContents) throws Exception {
|
||||||
* Writes the manifest to the JAR. It also calculates the digests that are required to be placed in the the signature
|
* Writes the manifest to the JAR. It also calculates the digests that are required to be placed in the the signature
|
||||||
* file.
|
* file.
|
||||||
*
|
*
|
||||||
* @throws java.io.IOException
|
|
||||||
*/
|
*/
|
||||||
private void writeManifest() throws IOException {
|
private void writeManifest() throws IOException {
|
||||||
zos.putNextEntry(new ZipEntry(MANIFEST_FN));
|
zos.putNextEntry(IOHelper.newZipEntry(MANIFEST_FN));
|
||||||
Manifest man = new Manifest();
|
Manifest man = new Manifest();
|
||||||
|
|
||||||
// main section
|
// main section
|
||||||
|
@ -374,7 +212,7 @@ private void writeManifest() throws IOException {
|
||||||
mainAttributes.put(new Attributes.Name(entry.getKey()), entry.getValue());
|
mainAttributes.put(new Attributes.Name(entry.getKey()), entry.getValue());
|
||||||
|
|
||||||
// individual files sections
|
// individual files sections
|
||||||
Attributes.Name digestAttr = new Attributes.Name(hashFunctionName + "-Digest");
|
Attributes.Name digestAttr = new Attributes.Name(DIGEST_HASH);
|
||||||
for (Map.Entry<String, String> entry : fileDigests.entrySet()) {
|
for (Map.Entry<String, String> entry : fileDigests.entrySet()) {
|
||||||
Attributes attributes = new Attributes();
|
Attributes attributes = new Attributes();
|
||||||
man.getEntries().put(entry.getKey(), attributes);
|
man.getEntries().put(entry.getKey(), attributes);
|
||||||
|
@ -382,12 +220,11 @@ private void writeManifest() throws IOException {
|
||||||
sectionDigests.put(entry.getKey(), hashEntrySection(entry.getKey(), attributes));
|
sectionDigests.put(entry.getKey(), hashEntrySection(entry.getKey(), attributes));
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDigest hasher = hasher();
|
SignHelper.HashingOutputStream out = new SignHelper.HashingNonClosingOutputStream(zos, SignHelper.hasher());
|
||||||
OutputStream out = new SignerJar.HashingOutputStream(zos, hasher);
|
|
||||||
man.write(out);
|
man.write(out);
|
||||||
zos.closeEntry();
|
zos.closeEntry();
|
||||||
|
|
||||||
manifestHash = Base64.getEncoder().encodeToString(hasher.digest());
|
manifestHash = Base64.getEncoder().encodeToString(out.digest());
|
||||||
manifestMainHash = hashMainSection(man.getMainAttributes());
|
manifestMainHash = hashMainSection(man.getMainAttributes());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -397,16 +234,16 @@ private void writeManifest() throws IOException {
|
||||||
* @return the contents of the file as bytes
|
* @return the contents of the file as bytes
|
||||||
*/
|
*/
|
||||||
private byte[] writeSigFile() throws IOException {
|
private byte[] writeSigFile() throws IOException {
|
||||||
zos.putNextEntry(new ZipEntry(SIG_FN));
|
zos.putNextEntry(IOHelper.newZipEntry(SIG_FN));
|
||||||
Manifest man = new Manifest();
|
Manifest man = new Manifest();
|
||||||
// main section
|
// main section
|
||||||
Attributes mainAttributes = man.getMainAttributes();
|
Attributes mainAttributes = man.getMainAttributes();
|
||||||
mainAttributes.put(Attributes.Name.SIGNATURE_VERSION, "1.0");
|
mainAttributes.put(Attributes.Name.SIGNATURE_VERSION, "1.0");
|
||||||
mainAttributes.put(new Attributes.Name(hashFunctionName + "-Digest-Manifest"), manifestHash);
|
mainAttributes.put(new Attributes.Name(DIGEST_HASH + "-Manifest"), manifestHash);
|
||||||
mainAttributes.put(new Attributes.Name(hashFunctionName + "-Digest-Manifest-Main-Attributes"), manifestMainHash);
|
mainAttributes.put(new Attributes.Name(DIGEST_HASH + "-Manifest-Main-Attributes"), manifestMainHash);
|
||||||
|
|
||||||
// individual files sections
|
// individual files sections
|
||||||
Attributes.Name digestAttr = new Attributes.Name(hashFunctionName + "-Digest");
|
Attributes.Name digestAttr = new Attributes.Name(DIGEST_HASH);
|
||||||
for (Map.Entry<String, String> entry : sectionDigests.entrySet()) {
|
for (Map.Entry<String, String> entry : sectionDigests.entrySet()) {
|
||||||
Attributes attributes = new Attributes();
|
Attributes attributes = new Attributes();
|
||||||
man.getEntries().put(entry.getKey(), attributes);
|
man.getEntries().put(entry.getKey(), attributes);
|
||||||
|
@ -424,11 +261,10 @@ private byte[] writeSigFile() throws IOException {
|
||||||
/**
|
/**
|
||||||
* Signs the .SIG file and writes the signature (.RSA file) to the JAR.
|
* Signs the .SIG file and writes the signature (.RSA file) to the JAR.
|
||||||
*
|
*
|
||||||
* @throws java.io.IOException
|
|
||||||
* @throws RuntimeException if the signing failed
|
* @throws RuntimeException if the signing failed
|
||||||
*/
|
*/
|
||||||
private void writeSignature(byte[] sigFile) throws IOException {
|
private void writeSignature(byte[] sigFile) throws IOException {
|
||||||
zos.putNextEntry(new ZipEntry(SIG_RSA_FN));
|
zos.putNextEntry(IOHelper.newZipEntry(SIG_KEY_FN));
|
||||||
try {
|
try {
|
||||||
byte[] signature = signSigFile(sigFile);
|
byte[] signature = signSigFile(sigFile);
|
||||||
zos.write(signature);
|
zos.write(signature);
|
|
@ -0,0 +1,77 @@
|
||||||
|
package pro.gravit.launchserver.binary.tasks;
|
||||||
|
|
||||||
|
import org.objectweb.asm.ClassReader;
|
||||||
|
import org.objectweb.asm.ClassWriter;
|
||||||
|
import org.objectweb.asm.tree.ClassNode;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.asm.ClassMetadataReader;
|
||||||
|
import pro.gravit.launchserver.asm.SafeClassWriter;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import java.util.jar.JarFile;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
public class AdditionalFixesApplyTask implements LauncherBuildTask {
|
||||||
|
private final LaunchServer server;
|
||||||
|
|
||||||
|
public AdditionalFixesApplyTask(LaunchServer server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void apply(Path inputFile, Path addFile, ZipOutputStream output, LaunchServer srv, Predicate<ZipEntry> excluder, boolean needFixes) throws IOException {
|
||||||
|
try (ClassMetadataReader reader = new ClassMetadataReader()) {
|
||||||
|
reader.getCp().add(new JarFile(inputFile.toFile()));
|
||||||
|
try (ZipInputStream input = IOHelper.newZipInput(addFile)) {
|
||||||
|
ZipEntry e = input.getNextEntry();
|
||||||
|
while (e != null) {
|
||||||
|
if (e.isDirectory() || excluder.test(e)) {
|
||||||
|
e = input.getNextEntry();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String filename = e.getName();
|
||||||
|
output.putNextEntry(IOHelper.newZipEntry(e));
|
||||||
|
if (filename.endsWith(".class")) {
|
||||||
|
byte[] bytes;
|
||||||
|
if (needFixes) {
|
||||||
|
bytes = classFix(input, reader, srv.config.launcher.stripLineNumbers);
|
||||||
|
output.write(bytes);
|
||||||
|
} else
|
||||||
|
IOHelper.transfer(input, output);
|
||||||
|
} else
|
||||||
|
IOHelper.transfer(input, output);
|
||||||
|
e = input.getNextEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] classFix(InputStream input, ClassMetadataReader reader, boolean stripNumbers) throws IOException {
|
||||||
|
ClassReader cr = new ClassReader(input);
|
||||||
|
ClassNode cn = new ClassNode();
|
||||||
|
cr.accept(cn, stripNumbers ? (ClassReader.SKIP_DEBUG) : 0);
|
||||||
|
ClassWriter cw = new SafeClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
|
||||||
|
cn.accept(cw);
|
||||||
|
return cw.toByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "AdditionalFixesApply";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path process(Path inputFile) throws IOException {
|
||||||
|
Path out = server.launcherBinary.nextPath("post-fixed");
|
||||||
|
try (ZipOutputStream output = new ZipOutputStream(IOHelper.newOutput(out))) {
|
||||||
|
apply(inputFile, inputFile, output, server, (e) -> false, true);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package pro.gravit.launchserver.binary.tasks;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
public class AttachJarsTask implements LauncherBuildTask {
|
||||||
|
private final LaunchServer srv;
|
||||||
|
private final List<Path> jars;
|
||||||
|
private final List<String> exclusions;
|
||||||
|
|
||||||
|
public AttachJarsTask(LaunchServer srv) {
|
||||||
|
this.srv = srv;
|
||||||
|
jars = new ArrayList<>();
|
||||||
|
exclusions = new ArrayList<>();
|
||||||
|
exclusions.add("META-INF");
|
||||||
|
exclusions.add("module-info.class");
|
||||||
|
exclusions.add("LICENSE");
|
||||||
|
exclusions.add("LICENSE.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "AttachJars";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path process(Path inputFile) throws IOException {
|
||||||
|
Path outputFile = srv.launcherBinary.nextPath("attached");
|
||||||
|
try (ZipInputStream input = IOHelper.newZipInput(inputFile);
|
||||||
|
ZipOutputStream output = new ZipOutputStream(IOHelper.newOutput(outputFile))) {
|
||||||
|
ZipEntry e = input.getNextEntry();
|
||||||
|
while (e != null) {
|
||||||
|
if (e.isDirectory()) {
|
||||||
|
e = input.getNextEntry();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
output.putNextEntry(IOHelper.newZipEntry(e));
|
||||||
|
IOHelper.transfer(input, output);
|
||||||
|
e = input.getNextEntry();
|
||||||
|
}
|
||||||
|
attach(output, inputFile, srv.launcherBinary.coreLibs);
|
||||||
|
attach(output, inputFile, jars);
|
||||||
|
for(var entry : srv.launcherBinary.files.entrySet()) {
|
||||||
|
ZipEntry newEntry = IOHelper.newZipEntry(entry.getKey());
|
||||||
|
output.putNextEntry(newEntry);
|
||||||
|
IOHelper.transfer(entry.getValue(), output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outputFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void attach(ZipOutputStream output, Path inputFile, List<Path> lst) throws IOException {
|
||||||
|
for (Path p : lst) {
|
||||||
|
AdditionalFixesApplyTask.apply(inputFile, p, output, srv, (e) -> filter(e.getName()), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean filter(String name) {
|
||||||
|
if (name.startsWith("META-INF/services")) return false;
|
||||||
|
return exclusions.stream().anyMatch(name::startsWith);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Path> getJars() {
|
||||||
|
return jars;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getExclusions() {
|
||||||
|
return exclusions;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,84 @@
|
||||||
|
package pro.gravit.launchserver.binary.tasks;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.bouncycastle.asn1.x500.X500Name;
|
||||||
|
import org.bouncycastle.asn1.x500.X500NameBuilder;
|
||||||
|
import org.bouncycastle.asn1.x500.style.BCStyle;
|
||||||
|
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
|
||||||
|
import org.bouncycastle.asn1.x509.Extension;
|
||||||
|
import org.bouncycastle.asn1.x509.KeyPurposeId;
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
|
||||||
|
import org.bouncycastle.cert.X509CertificateHolder;
|
||||||
|
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||||
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
||||||
|
import org.bouncycastle.cms.CMSException;
|
||||||
|
import org.bouncycastle.cms.CMSSignedDataGenerator;
|
||||||
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
|
import org.bouncycastle.operator.OperatorCreationException;
|
||||||
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.launchserver.helper.SignHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.cert.Certificate;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
public class CertificateAutogenTask implements LauncherBuildTask {
|
||||||
|
private final LaunchServer server;
|
||||||
|
private transient final Logger logger = LogManager.getLogger();
|
||||||
|
public X509Certificate certificate;
|
||||||
|
public X509CertificateHolder bcCertificate;
|
||||||
|
public CMSSignedDataGenerator signedDataGenerator;
|
||||||
|
|
||||||
|
public CertificateAutogenTask(LaunchServer server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "CertificateAutogen";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path process(Path inputFile) throws IOException {
|
||||||
|
if (signedDataGenerator != null) return inputFile;
|
||||||
|
try {
|
||||||
|
logger.warn("You are using an auto-generated certificate (sign.enabled false). It is not good");
|
||||||
|
logger.warn("It is highly recommended that you use the correct certificate (sign.enabled true)");
|
||||||
|
logger.warn("You can use GenerateCertificateModule or your own certificate.");
|
||||||
|
X500NameBuilder subject = new X500NameBuilder();
|
||||||
|
subject.addRDN(BCStyle.CN, server.config.projectName.concat(" Autogenerated"));
|
||||||
|
subject.addRDN(BCStyle.O, server.config.projectName);
|
||||||
|
LocalDateTime startDate = LocalDate.now().atStartOfDay();
|
||||||
|
X509v3CertificateBuilder builder = new X509v3CertificateBuilder(
|
||||||
|
subject.build(),
|
||||||
|
new BigInteger("0"),
|
||||||
|
Date.from(startDate.atZone(ZoneId.systemDefault()).toInstant()),
|
||||||
|
Date.from(startDate.plusDays(3650).atZone(ZoneId.systemDefault()).toInstant()),
|
||||||
|
new X500Name("CN=ca"),
|
||||||
|
SubjectPublicKeyInfo.getInstance(server.keyAgreementManager.ecdsaPublicKey.getEncoded()));
|
||||||
|
builder.addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(KeyPurposeId.id_kp_codeSigning));
|
||||||
|
//builder.addExtension(Extension.keyUsage, false, new KeyUsage(1));
|
||||||
|
JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder("SHA256WITHECDSA");
|
||||||
|
ContentSigner signer = csBuilder.build(server.keyAgreementManager.ecdsaPrivateKey);
|
||||||
|
bcCertificate = builder.build(signer);
|
||||||
|
certificate = new JcaX509CertificateConverter().setProvider("BC")
|
||||||
|
.getCertificate(bcCertificate);
|
||||||
|
ArrayList<Certificate> chain = new ArrayList<>();
|
||||||
|
chain.add(certificate);
|
||||||
|
signedDataGenerator = SignHelper.createSignedDataGenerator(server.keyAgreementManager.ecdsaPrivateKey, certificate, chain, "SHA256WITHECDSA");
|
||||||
|
} catch (OperatorCreationException | CMSException | CertificateException e) {
|
||||||
|
logger.error("Certificate generate failed", e);
|
||||||
|
}
|
||||||
|
return inputFile;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package pro.gravit.launchserver.binary.tasks;
|
||||||
|
|
||||||
|
import pro.gravit.launchserver.LaunchServer;
|
||||||
|
import pro.gravit.utils.helper.IOHelper;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.zip.Deflater;
|
||||||
|
import java.util.zip.ZipEntry;
|
||||||
|
import java.util.zip.ZipInputStream;
|
||||||
|
import java.util.zip.ZipOutputStream;
|
||||||
|
|
||||||
|
public class CompressBuildTask implements LauncherBuildTask {
|
||||||
|
public transient final LaunchServer server;
|
||||||
|
|
||||||
|
public CompressBuildTask(LaunchServer server) {
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "compress";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Path process(Path inputFile) throws IOException {
|
||||||
|
Path output = server.launcherBinary.nextPath(this);
|
||||||
|
try (ZipOutputStream outputStream = new ZipOutputStream(IOHelper.newOutput(output))) {
|
||||||
|
outputStream.setMethod(ZipOutputStream.DEFLATED);
|
||||||
|
outputStream.setLevel(Deflater.BEST_COMPRESSION);
|
||||||
|
try (ZipInputStream input = IOHelper.newZipInput(inputFile)) {
|
||||||
|
ZipEntry e = input.getNextEntry();
|
||||||
|
while (e != null) {
|
||||||
|
if (e.isDirectory()) {
|
||||||
|
e = input.getNextEntry();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
outputStream.putNextEntry(IOHelper.newZipEntry(e));
|
||||||
|
IOHelper.transfer(input, outputStream);
|
||||||
|
e = input.getNextEntry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue