mirror of
https://github.com/dkmstr/openuds.git
synced 2025-10-07 15:33:51 +03:00
Compare commits
610 Commits
nonMultiCl
...
release-v3
Author | SHA1 | Date | |
---|---|---|---|
|
a1cd0dc047 | ||
|
bd7faf7867 | ||
|
708c54878f | ||
|
18c0d0cb9a | ||
|
53c8d0fbc9 | ||
|
03b068a64e | ||
|
667deb7011 | ||
|
a9eae0e2a3 | ||
|
3d1f046c1a | ||
|
78af8b262f | ||
|
cba5fca237 | ||
|
7d8d575e2e | ||
|
9b4827453c | ||
|
3073ac0cc0 | ||
|
35f27cbbd9 | ||
|
f1ed1f5cf4 | ||
|
09e9d70e08 | ||
|
d6691908b3 | ||
|
f2fa4de21d | ||
|
3110d070e6 | ||
|
16e0064d56 | ||
|
20e44615e4 | ||
|
cae457a85e | ||
|
b3a7718942 | ||
|
35c375e3bd | ||
|
2b21b22fe8 | ||
|
2f246d49b9 | ||
|
1c01e9aba0 | ||
|
d135e4e1e2 | ||
|
756ac04d90 | ||
|
1d58ffb3b3 | ||
|
0363ac3a6a | ||
|
45a4dec18f | ||
|
23a9465ae1 | ||
|
084e0cc2a0 | ||
|
2c77d361d7 | ||
|
392cb6e406 | ||
|
4df4892111 | ||
|
8e4615de19 | ||
|
d8ad7ddd22 | ||
|
4d26df9580 | ||
|
ddf07eb68b | ||
|
ba28ab78ed | ||
|
e42ab76088 | ||
|
d72723d6f2 | ||
|
8feef1d3f9 | ||
|
0c2ee7906b | ||
|
8891da5987 | ||
|
8c9b326c3c | ||
|
db70f02df0 | ||
|
f502f4ceb9 | ||
|
a2bfcd3d5a | ||
|
0f41544830 | ||
|
eaa05ead0b | ||
|
2d6a381321 | ||
|
84b0bd1de2 | ||
|
0aeb9b923f | ||
|
55ed118ae9 | ||
|
c4690a25bb | ||
|
902f838178 | ||
|
36a4f9a68b | ||
|
59141a9f03 | ||
|
7a6c5966d9 | ||
|
48aec57256 | ||
|
e81982dd41 | ||
|
c8982cf677 | ||
|
9b4d1139d1 | ||
|
4756437d9f | ||
|
9e61d142e2 | ||
|
d98be68d96 | ||
|
4c759c3367 | ||
|
e70146fad6 | ||
|
c7e1f36cb3 | ||
|
f78053fc0c | ||
|
e15746b4a4 | ||
|
14dd5aca64 | ||
|
7bf4859399 | ||
|
846f9225f1 | ||
|
09c44ac0b6 | ||
|
9db8e8d7ec | ||
|
15bc3a0b6f | ||
|
321255a1b0 | ||
|
59d578f292 | ||
|
ea343659ff | ||
|
636b72a471 | ||
|
31104c3fc2 | ||
|
9d9a764a81 | ||
|
8aa04c6a9c | ||
|
1380cbde3e | ||
|
0ac4fe60a8 | ||
|
9d5e983847 | ||
|
c11ea77f22 | ||
|
6cbb497902 | ||
|
8fc9495d5e | ||
|
d43167707c | ||
|
3fc86482dc | ||
|
7eaf0c8126 | ||
|
df0e1bde96 | ||
|
14a8f1f5e1 | ||
|
8f132e7524 | ||
|
ab7b4c78ef | ||
|
f5af2b12d2 | ||
|
f11da32f0d | ||
|
55b8763f72 | ||
|
5694420f89 | ||
|
76f7b36508 | ||
|
2269f8c770 | ||
|
6f4d84a08e | ||
|
b983d5d409 | ||
|
562e9201c8 | ||
|
e2814f2674 | ||
|
ef9a0ce0b2 | ||
|
77bc47671e | ||
|
f7886abfbc | ||
|
5c9dd741d3 | ||
|
0ba381dbc4 | ||
|
8abe2ad31b | ||
|
4386c5567a | ||
|
5da71a4f6e | ||
|
6bb4c3bd5e | ||
|
b9a01e686f | ||
|
011223ec05 | ||
|
a12aa1f3d4 | ||
|
666b982c50 | ||
|
cf1048afcb | ||
|
7985f44389 | ||
|
4517b781cf | ||
|
9de5387fd6 | ||
|
304f5dd686 | ||
|
2699c090f8 | ||
|
43e1353154 | ||
|
ba5be7e2fb | ||
|
62a401f9b4 | ||
|
ddc9d5a434 | ||
|
8f2b9bf136 | ||
|
f4d15e0fca | ||
|
002321c339 | ||
|
bcdbfa67d7 | ||
|
e8022389fb | ||
|
12c59f66e2 | ||
|
2541642160 | ||
|
56d7619e9f | ||
|
a7ae7d3771 | ||
|
e47e0ee69f | ||
|
9d6a74faa3 | ||
|
56ab199856 | ||
|
9be108dbd5 | ||
|
165bd89829 | ||
|
52096b1eff | ||
|
ac49786492 | ||
|
b14581c522 | ||
|
98954b5e3b | ||
|
762c0e5392 | ||
|
0aaa734030 | ||
|
2d48320ac8 | ||
|
9192a0a822 | ||
|
5907985719 | ||
|
cf0f6a0cce | ||
|
c8df5de095 | ||
|
d2d13d1089 | ||
|
978c39edd1 | ||
|
fe91fa635b | ||
|
a75bc4a4b5 | ||
|
8aa94fd0c5 | ||
|
0db41e1a14 | ||
|
4f12602db3 | ||
|
1fbc4b9bfd | ||
|
d8fce3bb73 | ||
|
e94b558ae9 | ||
|
58c4c4e68f | ||
|
5f8854335e | ||
|
f2f7edbd67 | ||
|
56cf0049db | ||
|
9478a86b02 | ||
|
e15ba650f9 | ||
|
86e8c759ec | ||
|
48cd8240e9 | ||
|
75005399aa | ||
|
da02ad0c7c | ||
|
0a15f7bdce | ||
|
2189267358 | ||
|
1a9517675d | ||
|
aa97309db9 | ||
|
3e947e1d82 | ||
|
dcedb268dd | ||
|
645f61c8e6 | ||
|
d462ecbb32 | ||
|
c1e4c5b81a | ||
|
d707771fe5 | ||
|
544fb21a01 | ||
|
159aa3c6ec | ||
|
8cc17464c3 | ||
|
adeb6b2a46 | ||
|
b7962a24f1 | ||
|
94cab0c204 | ||
|
fa3e6cb5a2 | ||
|
2a9900d2eb | ||
|
ccf75e7104 | ||
|
c208814dd0 | ||
|
6081122311 | ||
|
3d421ac38c | ||
|
d22f606f49 | ||
|
859f09aa0b | ||
|
dba2526ffb | ||
|
d17dae8bdd | ||
|
c6b9817749 | ||
|
b34f09e58e | ||
|
053fcfd3e3 | ||
|
36e3c7eea9 | ||
|
6772455111 | ||
|
b69efb5426 | ||
|
5c4141f9a2 | ||
|
d8e8ddd1bb | ||
|
c8770e5f4f | ||
|
f2e3a2468a | ||
|
106c28cb34 | ||
|
2165798408 | ||
|
271351b3d6 | ||
|
d0d6892858 | ||
|
80e53615c5 | ||
|
899156843e | ||
|
2b917839c7 | ||
|
adec02dc3f | ||
|
d96a639a3d | ||
|
721d32c972 | ||
|
1cf2e2cd76 | ||
|
40364cdcce | ||
|
39fd5dbf3f | ||
|
396e0f0c38 | ||
|
fc6224dada | ||
|
1d8d1fe2c9 | ||
|
0923a3fbca | ||
|
43458cbf99 | ||
|
114247e407 | ||
|
ec2645b0a2 | ||
|
dd08257fb9 | ||
|
9d0df6cfae | ||
|
7bd0d571e6 | ||
|
ad269b3c28 | ||
|
f3dd5753a3 | ||
|
13336b966e | ||
|
a76989d885 | ||
|
5f0e5a5dfe | ||
|
cfbce5aef5 | ||
|
d2cb4356f0 | ||
|
4f4f1f24fd | ||
|
65d38d8722 | ||
|
b16cea984c | ||
|
7769351d42 | ||
|
bf635a5e9a | ||
|
ae2ffccbc3 | ||
|
a005bf1ca0 | ||
|
4de443395d | ||
|
9f2bc5417f | ||
|
c6d1bf450c | ||
|
cf21936f41 | ||
|
5d9c8ee53f | ||
|
7d3bfb5d3b | ||
|
b474e63924 | ||
|
d48747abff | ||
|
8b3ad295cc | ||
|
aa677353ad | ||
|
9c6c4078b1 | ||
|
9fba2b45ad | ||
|
71582fc415 | ||
|
0d1d38c18a | ||
|
4ec8841a57 | ||
|
8c6390733c | ||
|
98f56ee58b | ||
|
1c01c35a87 | ||
|
673d1b6813 | ||
|
1ba12bb82d | ||
|
f90f108869 | ||
|
88c3f9077b | ||
|
2a01df542d | ||
|
2733444355 | ||
|
cf6820aa2b | ||
|
6692e5ce6d | ||
|
1a85f60f4f | ||
|
38b3318704 | ||
|
ccec281e0d | ||
|
230187d9ee | ||
|
092bb83001 | ||
|
ac62aed420 | ||
|
e16be78ad5 | ||
|
28319b216f | ||
|
739b0c7f81 | ||
|
e5e8ad5fbd | ||
|
86ebd7766e | ||
|
83394f0d34 | ||
|
4f0ea76666 | ||
|
c34fc41f56 | ||
|
18e9cab9ef | ||
|
6053e34d1d | ||
|
90aa455586 | ||
|
11041ff44f | ||
|
bc2328a239 | ||
|
98826504d6 | ||
|
3a990e19a6 | ||
|
d9d3bc452c | ||
|
8a150439ae | ||
|
08f14bff57 | ||
|
653bff420f | ||
|
73a3c89e04 | ||
|
e79753748e | ||
|
adaabf9d83 | ||
|
a8a9b24596 | ||
|
f24c77f20a | ||
|
d2fa5e38d0 | ||
|
ada5374db5 | ||
|
93ba05f6cb | ||
|
94cf5582e2 | ||
|
afcfffbd29 | ||
|
3cfbdc86e0 | ||
|
d1329849f3 | ||
|
ba759b3652 | ||
|
1e3478314b | ||
|
f5d2776478 | ||
|
0496117fc1 | ||
|
89864b11c2 | ||
|
fcdf599e18 | ||
|
05b6bebf36 | ||
|
cdbc8d7ba1 | ||
|
072a722b09 | ||
|
2d2e2d7b1f | ||
|
f4da75cea9 | ||
|
1c65722d24 | ||
|
8783db925f | ||
|
5e61871091 | ||
|
80b26446f6 | ||
|
a0ac50d9c2 | ||
|
6094f55182 | ||
|
11d9c77a79 | ||
|
76e67b1f63 | ||
|
64fc61a2d6 | ||
|
57b19757b9 | ||
|
aec2f5b57f | ||
|
77e021a371 | ||
|
4db98684d3 | ||
|
a948d5eeb1 | ||
|
c7e6857492 | ||
|
aaa4216862 | ||
|
098396be87 | ||
|
d02c693202 | ||
|
cb11a26fbe | ||
|
43934d425f | ||
|
5b499de983 | ||
|
00d9f5759d | ||
|
c6a40ac182 | ||
|
7d9ffca559 | ||
|
ec02f63cac | ||
|
0de655d14f | ||
|
68e327847b | ||
|
81ea07f0a0 | ||
|
d7540c3305 | ||
|
f43b9c7bfd | ||
|
972c48ddee | ||
|
118e642700 | ||
|
dfa441871b | ||
|
18c5e3a242 | ||
|
3a4d571a6c | ||
|
3cc42e1e73 | ||
|
ffe9baa9a5 | ||
|
0b05009d3f | ||
|
b34b12ec9f | ||
|
fb70524cb3 | ||
|
4c66401e4f | ||
|
364ebd6f3a | ||
|
493cbbb4e7 | ||
|
5277a74c1c | ||
|
1e01339b93 | ||
|
9343f7c263 | ||
|
7775964d62 | ||
|
a207e8f65f | ||
|
0a0f2771ae | ||
|
ceb5fd9bde | ||
|
7bfa6a6c4f | ||
|
858b79614b | ||
|
45b47ce702 | ||
|
dd98ba5653 | ||
|
0fe5b32224 | ||
|
a0adc1ded3 | ||
|
4f5cc505d3 | ||
|
8bac68b55b | ||
|
b5412e70fd | ||
|
75cd3c4845 | ||
|
e8c45b568d | ||
|
540a2b83be | ||
|
aa4d157c30 | ||
|
69ca93586a | ||
|
cf283bba0f | ||
|
9abaada7cb | ||
|
b359892454 | ||
|
927a86c835 | ||
|
2b5aa9c9a4 | ||
|
b3047e366d | ||
|
d2ef6e3704 | ||
|
5fb4461934 | ||
|
2f5f87e122 | ||
|
d9be83863c | ||
|
5fed04d64d | ||
|
8a2e2deaf1 | ||
|
86990638dc | ||
|
40b9572233 | ||
|
5836b33299 | ||
|
282495ce0f | ||
|
2b33ffc656 | ||
|
e0149900a7 | ||
|
7bed6ac171 | ||
|
0d77e86af2 | ||
|
a179522f4c | ||
|
21c2976d82 | ||
|
ee30ab4604 | ||
|
1fba4d3f9f | ||
|
5084fec43f | ||
|
04e24d406f | ||
|
f58ef9b6d3 | ||
|
18d4147d59 | ||
|
ccd429454e | ||
|
5ce7ddc3a7 | ||
|
3dd73f4723 | ||
|
c3531f3e7e | ||
|
ba7b1c0198 | ||
|
ba90dae5d6 | ||
|
f7cd474264 | ||
|
a255b52628 | ||
|
8d93144e24 | ||
|
27d158f514 | ||
|
2b4e771709 | ||
|
3ebc0dd26f | ||
|
79739bf9b8 | ||
|
f702c144fc | ||
|
ce2d2b1c2e | ||
|
790c204b6a | ||
|
d80cf4052e | ||
|
6a86b0ff04 | ||
|
0d412c4a9a | ||
|
ac9e6dafdf | ||
|
efd0ca3f88 | ||
|
edb4a32496 | ||
|
b239ff6cab | ||
|
d55d1bc619 | ||
|
917a201483 | ||
|
4809252434 | ||
|
8be0d9702a | ||
|
36acb0b0c0 | ||
|
420b78d45d | ||
|
e1ccc62dab | ||
|
6b0d98d4eb | ||
|
7bec7bd7cc | ||
|
270957fab5 | ||
|
47c6ca42f1 | ||
|
c1f6ed376b | ||
|
250ade6aee | ||
|
bde63f7b4f | ||
|
eb4be53508 | ||
|
3003066a91 | ||
|
10805ded7e | ||
|
21c221a6db | ||
|
1857134f42 | ||
|
835dc05e63 | ||
|
4cc4af5bd1 | ||
|
986a82f225 | ||
|
90b64c1721 | ||
|
f403d4ff3e | ||
|
c5071cf348 | ||
|
679956702b | ||
|
98d7a24656 | ||
|
b67771d5f3 | ||
|
672c35c903 | ||
|
6df1bc0a50 | ||
|
01119d1914 | ||
|
a4d1ecb95f | ||
|
237f7e5b77 | ||
|
edb74ab9c6 | ||
|
86eb1a9421 | ||
|
c09ea0eb63 | ||
|
ea79ccbee1 | ||
|
da82a26dd8 | ||
|
c129c83ca0 | ||
|
d8e6de8c1e | ||
|
e0d79cb590 | ||
|
59bd6c1649 | ||
|
564f0e17de | ||
|
842212f186 | ||
|
e4b609c4ce | ||
|
741855030f | ||
|
293b7f02ad | ||
|
fddd54fa99 | ||
|
cd640af37f | ||
|
6f99b63731 | ||
|
6b3355f819 | ||
|
660cfdcd0e | ||
|
47df6c58fc | ||
|
91c90766a3 | ||
|
2a834460d1 | ||
|
5bd77676ca | ||
|
8ef97a7773 | ||
|
abafa7bfac | ||
|
dcb7b3e28e | ||
|
41aa22fadd | ||
|
d02974ad87 | ||
|
b2a067300c | ||
|
afbc75bff0 | ||
|
4c453d2b1f | ||
|
26f33626c2 | ||
|
cb8284d076 | ||
|
ef3dd893d9 | ||
|
d531a1612a | ||
|
de9c06bc2c | ||
|
2400cc99cd | ||
|
7f5c3c3bbd | ||
|
710f2fb0e4 | ||
|
ede23ad793 | ||
|
9a3913cc42 | ||
|
5bf98782ea | ||
|
3a69c9205e | ||
|
3615db877e | ||
|
2286ccaca1 | ||
|
f90bf3a421 | ||
|
df815776da | ||
|
54f7fd21dc | ||
|
8e3d90e7f3 | ||
|
afa9e0aab6 | ||
|
77b0c7c8e1 | ||
|
23afd01004 | ||
|
c30a67d363 | ||
|
aa2d268453 | ||
|
de40c72d9e | ||
|
d0b30b561c | ||
|
e485374836 | ||
|
3934f2b88d | ||
|
c72bcf4200 | ||
|
1b7076e645 | ||
|
e637f208bd | ||
|
75e54618bb | ||
|
04864e3846 | ||
|
a52be141ea | ||
|
afcbd058d1 | ||
|
8285e2daad | ||
|
03bfb3efbb | ||
|
8c4b84e7db | ||
|
4f8fe793cc | ||
|
286b320257 | ||
|
68411f0726 | ||
|
1be49a6e0e | ||
|
c21c0b44ce | ||
|
46aa9139a0 | ||
|
574b19a905 | ||
|
612646bd1c | ||
|
10d9279b89 | ||
|
a8a5063083 | ||
|
29b6613c95 | ||
|
8aa7dc3c6f | ||
|
e75d373d03 | ||
|
91d2398ade | ||
|
f4e953c9c9 | ||
|
f14f36b0d0 | ||
|
d1e51c0103 | ||
|
d38347c534 | ||
|
6fd307e86e | ||
|
51407b54ee | ||
|
91f90c8630 | ||
|
ca5b54c8e2 | ||
|
8d74055357 | ||
|
8e81d51a43 | ||
|
5ff6cdaf69 | ||
|
13cbfe26c7 | ||
|
d497235eeb | ||
|
7d8bcf2168 | ||
|
5706f9d681 | ||
|
cd06597918 | ||
|
49ce5622d6 | ||
|
de5031febf | ||
|
b29baf2a29 | ||
|
aaa909fff0 | ||
|
99ee0b00fc | ||
|
f2643df05f | ||
|
2520cce429 | ||
|
962015c355 | ||
|
582ba01014 | ||
|
eec8588628 | ||
|
37f59e952d | ||
|
46bab75a92 | ||
|
8f7421ef9d | ||
|
a7584f9e8e | ||
|
fad735bb87 | ||
|
5ba704ac8a | ||
|
3c5ef5817f | ||
|
de0db84a5d | ||
|
548b6e813d | ||
|
31b513a7ef | ||
|
fa7ce3de0b | ||
|
3a7e7b8dfc | ||
|
c9488329b9 | ||
|
55c4574021 | ||
|
59179584f2 | ||
|
92de3b01dd | ||
|
c62d62dd65 | ||
|
e02318e665 | ||
|
612ae63cf2 | ||
|
cb44662134 | ||
|
a359ff2263 | ||
|
9ca3a7cdeb | ||
|
1736cae1c1 | ||
|
727ffe0365 | ||
|
b031e0aa3c | ||
|
d7886a1281 | ||
|
09e88b60f5 | ||
|
6af0617c2a |
@@ -1,4 +0,0 @@
|
||||
Linux:
|
||||
python3-prctl (recommended, but not required in fact)
|
||||
python3-pyqt5
|
||||
|
@@ -11,6 +11,9 @@ dpkg-buildpackage -b
|
||||
cat udsactor-template.spec |
|
||||
sed -e s/"version 0.0.0"/"version ${VERSION}"/g |
|
||||
sed -e s/"release 1"/"release ${RELEASE}"/g > udsactor-$VERSION.spec
|
||||
cat udsactor-unmanaged-template.spec |
|
||||
sed -e s/"version 0.0.0"/"version ${VERSION}"/g |
|
||||
sed -e s/"release 1"/"release ${RELEASE}"/g > udsactor-unmanaged-$VERSION.spec
|
||||
|
||||
# Now fix dependencies for opensuse
|
||||
# Note that, although on opensuse the library is "libXss1" on newer,
|
||||
@@ -22,7 +25,7 @@ cat udsactor-template.spec |
|
||||
# sed -e s/"libXScrnSaver"/"libXss1"/g > udsactor-opensuse-$VERSION.spec
|
||||
|
||||
#for pkg in udsactor-$VERSION.spec udsactor-opensuse-$VERSION.spec; do
|
||||
for pkg in udsactor-$VERSION.spec; do
|
||||
for pkg in udsactor-*$VERSION.spec; do
|
||||
|
||||
rm -rf rpm
|
||||
for folder in SOURCES BUILD RPMS SPECS SRPMS; do
|
||||
|
@@ -1,3 +1,9 @@
|
||||
udsactor (3.6.0) stable; urgency=medium
|
||||
|
||||
* Upgraded to 3.6.0 release
|
||||
|
||||
-- Adolfo Gómez García <agomez@virtualcable.es> Fri, 1 Jul 2022 14:00:00 +0200
|
||||
|
||||
udsactor (3.5.0) stable; urgency=medium
|
||||
|
||||
* Upgraded to 3.5.0 release
|
||||
|
@@ -1,3 +1,3 @@
|
||||
udsactor-unmanaged_3.5.0_all.deb admin optional
|
||||
udsactor_3.5.0_all.deb admin optional
|
||||
udsactor_3.5.0_amd64.buildinfo admin optional
|
||||
udsactor-unmanaged_3.6.0_all.deb admin optional
|
||||
udsactor_3.6.0_all.deb admin optional
|
||||
udsactor_3.6.0_amd64.buildinfo admin optional
|
||||
|
@@ -3,4 +3,4 @@
|
||||
FOLDER=/usr/share/UDSActor
|
||||
|
||||
cd $FOLDER
|
||||
exec python3 actor_client.py -platform xcb $@
|
||||
exec python3 -s actor_client.py -platform xcb $@
|
||||
|
@@ -11,7 +11,7 @@ Release: %{release}
|
||||
Summary: Actor for Universal Desktop Services (UDS) Broker
|
||||
License: BSD3
|
||||
Group: Admin
|
||||
Requires: python3-six python3-requests python3-qt5 libXScrnSaver
|
||||
Requires: python3-six python3-requests python3-qt5 libXScrnSaver xset
|
||||
Vendor: Virtual Cable S.L.U.
|
||||
URL: http://www.udsenterprise.com
|
||||
Provides: udsactor
|
||||
|
70
actor/linux/udsactor-unmanaged-template.spec
Normal file
70
actor/linux/udsactor-unmanaged-template.spec
Normal file
@@ -0,0 +1,70 @@
|
||||
%define _topdir %(echo $PWD)/rpm
|
||||
%define name udsactor-unmanaged
|
||||
%define version 0.0.0
|
||||
%define release 1
|
||||
%define buildroot %{_topdir}/%{name}-%{version}-%{release}-root
|
||||
|
||||
BuildRoot: %{buildroot}
|
||||
Name: %{name}
|
||||
Version: %{version}
|
||||
Release: %{release}
|
||||
Summary: Actor for Universal Desktop Services (UDS) Broker
|
||||
License: BSD3
|
||||
Group: Admin
|
||||
Requires: python3-six python3-requests python3-qt5 libXScrnSaver
|
||||
Vendor: Virtual Cable S.L.U.
|
||||
URL: http://www.udsenterprise.com
|
||||
Provides: udsactor
|
||||
|
||||
%define _rpmdir ../
|
||||
%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm
|
||||
|
||||
|
||||
%install
|
||||
curdir=`pwd`
|
||||
cd ../..
|
||||
make DESTDIR=$RPM_BUILD_ROOT DISTRO=rh install-udsactor-unmanaged
|
||||
cd $curdir
|
||||
|
||||
%clean
|
||||
rm -rf $RPM_BUILD_ROOT
|
||||
curdir=`pwd`
|
||||
cd ../..
|
||||
make DESTDIR=$RPM_BUILD_ROOT DISTRO=rh clean
|
||||
cd $curdir
|
||||
|
||||
|
||||
%post
|
||||
systemctl enable udsactor.service > /dev/null 2>&1
|
||||
|
||||
%preun
|
||||
systemctl disable udsactor.service > /dev/null 2>&1
|
||||
systemctl stop udsactor.service > /dev/null 2>&1
|
||||
|
||||
%postun
|
||||
# $1 == 0 on uninstall, == 1 on upgrade for preun and postun (just a reminder for me... :) )
|
||||
if [ $1 -eq 0 ]; then
|
||||
rm -rf /etc/udsactor
|
||||
rm /var/log/udsactor.log
|
||||
fi
|
||||
# And, posibly, the .pyc leaved behind on /usr/share/UDSActor
|
||||
rm -rf /usr/share/UDSActor > /dev/null 2>&1
|
||||
|
||||
%description
|
||||
This package provides the required components to allow this unmanaged machine to work on an environment managed by UDS Broker.
|
||||
|
||||
%files
|
||||
%defattr(-,root,root)
|
||||
/etc/udsactor
|
||||
/etc/xdg/autostart/UDSActorTool.desktop
|
||||
/etc/systemd/system/udsactor.service
|
||||
/usr/bin/UDSActorTool-startup
|
||||
/usr/bin/udsactor
|
||||
/usr/bin/udsvapp
|
||||
/usr/bin/UDSActorTool
|
||||
/usr/sbin/UDSActorConfig
|
||||
/usr/sbin/UDSActorConfig-pkexec
|
||||
/usr/share/UDSActor/*
|
||||
/usr/share/applications/UDS_Actor_Configuration.desktop
|
||||
/usr/share/autostart/UDSActorTool.desktop
|
||||
/usr/share/polkit-1/actions/org.openuds.pkexec.UDSActorConfig.policy
|
@@ -69,7 +69,7 @@ if __name__ == "__main__":
|
||||
timer.start(1000)
|
||||
timer.timeout.connect(lambda *a: None) # type: ignore # timeout can be connected to a callable
|
||||
|
||||
qApp.exec_()
|
||||
qApp.exec()
|
||||
|
||||
# On windows, if no window is created, this point will never be reached.
|
||||
qApp.end()
|
||||
|
@@ -187,9 +187,9 @@ if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
if udsactor.platform.operations.checkPermissions() is False:
|
||||
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok)
|
||||
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok) # type: ignore
|
||||
sys.exit(1)
|
||||
|
||||
myapp = UDSConfigDialog()
|
||||
myapp.show()
|
||||
sys.exit(app.exec_())
|
||||
sys.exit(app.exec())
|
||||
|
@@ -40,6 +40,7 @@ import PyQt5 # pylint: disable=unused-import
|
||||
from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox
|
||||
|
||||
import udsactor
|
||||
import udsactor.tools
|
||||
|
||||
from ui.setup_dialog_unmanaged_ui import Ui_UdsActorSetupDialog
|
||||
|
||||
@@ -49,6 +50,7 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger('actor')
|
||||
|
||||
|
||||
class UDSConfigDialog(QDialog):
|
||||
_host: str = ''
|
||||
_config: udsactor.types.ActorConfigurationType
|
||||
@@ -60,65 +62,99 @@ class UDSConfigDialog(QDialog):
|
||||
self.ui = Ui_UdsActorSetupDialog()
|
||||
self.ui.setupUi(self)
|
||||
self.ui.host.setText(self._config.host)
|
||||
self.ui.validateCertificate.setCurrentIndex(1 if self._config.validateCertificate else 0)
|
||||
self.ui.validateCertificate.setCurrentIndex(
|
||||
1 if self._config.validateCertificate else 0
|
||||
)
|
||||
self.ui.logLevelComboBox.setCurrentIndex(self._config.log_level)
|
||||
self.ui.serviceToken.setText(self._config.master_token)
|
||||
self.ui.serviceToken.setText(self._config.master_token or '')
|
||||
self.ui.restrictNet.setText(self._config.restrict_net or '')
|
||||
|
||||
self.ui.testButton.setEnabled(bool(self._config.master_token and self._config.host))
|
||||
self.ui.testButton.setEnabled(
|
||||
bool(self._config.master_token and self._config.host)
|
||||
)
|
||||
|
||||
@property
|
||||
def api(self) -> udsactor.rest.UDSServerApi:
|
||||
return udsactor.rest.UDSServerApi(self.ui.host.text(), self.ui.validateCertificate.currentIndex() == 1)
|
||||
return udsactor.rest.UDSServerApi(
|
||||
self.ui.host.text(), self.ui.validateCertificate.currentIndex() == 1
|
||||
)
|
||||
|
||||
def finish(self) -> None:
|
||||
self.close()
|
||||
|
||||
def configChanged(self, text: str) -> None:
|
||||
self.ui.testButton.setEnabled(self.ui.host.text() == self._config.host and self.ui.serviceToken.text() == self._config.master_token)
|
||||
self.ui.testButton.setEnabled(
|
||||
self.ui.host.text() == self._config.host
|
||||
and self.ui.serviceToken.text() == self._config.master_token
|
||||
and self.ui.restrictNet.text() == self._config.restrict_net
|
||||
)
|
||||
|
||||
def testUDSServer(self) -> None:
|
||||
if not self._config.master_token or not self._config.host:
|
||||
self.ui.testButton.setEnabled(False)
|
||||
return
|
||||
try:
|
||||
api = udsactor.rest.UDSServerApi(self._config.host, self._config.validateCertificate)
|
||||
api = udsactor.rest.UDSServerApi(
|
||||
self._config.host, self._config.validateCertificate
|
||||
)
|
||||
if not api.test(self._config.master_token, udsactor.types.UNMANAGED):
|
||||
QMessageBox.information(
|
||||
self,
|
||||
'UDS Test',
|
||||
'Service token seems to be invalid . Please, check token validity.',
|
||||
QMessageBox.Ok
|
||||
QMessageBox.Ok,
|
||||
)
|
||||
else:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
'UDS Test',
|
||||
'Configuration for {} seems to be correct.'.format(self._config.host),
|
||||
QMessageBox.Ok
|
||||
'Configuration for {} seems to be correct.'.format(
|
||||
self._config.host
|
||||
),
|
||||
QMessageBox.Ok,
|
||||
)
|
||||
except Exception:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
'UDS Test',
|
||||
'Configured host {} seems to be inaccesible.'.format(self._config.host),
|
||||
QMessageBox.Ok
|
||||
QMessageBox.Ok,
|
||||
)
|
||||
|
||||
def saveConfig(self) -> None:
|
||||
# Ensure restrict_net is empty or a valid subnet
|
||||
restrictNet = self.ui.restrictNet.text().strip()
|
||||
if restrictNet:
|
||||
try:
|
||||
subnet = udsactor.tools.strToNoIPV4Network(restrictNet)
|
||||
if not subnet:
|
||||
raise Exception('Invalid subnet')
|
||||
except Exception:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
'Invalid subnet',
|
||||
'Invalid subnet {}. Please, check it.'.format(restrictNet),
|
||||
QMessageBox.Ok,
|
||||
)
|
||||
return
|
||||
|
||||
# Store parameters on register for later use, notify user of registration
|
||||
self._config = udsactor.types.ActorConfigurationType(
|
||||
actorType=udsactor.types.UNMANAGED,
|
||||
host=self.ui.host.text(),
|
||||
validateCertificate=self.ui.validateCertificate.currentIndex() == 1,
|
||||
master_token=self.ui.serviceToken.text(),
|
||||
log_level=self.ui.logLevelComboBox.currentIndex()
|
||||
master_token=self.ui.serviceToken.text().strip(),
|
||||
restrict_net=restrictNet,
|
||||
log_level=self.ui.logLevelComboBox.currentIndex(),
|
||||
)
|
||||
|
||||
udsactor.platform.store.writeConfig(self._config)
|
||||
# Enables test button
|
||||
self.ui.testButton.setEnabled(True)
|
||||
# Informs the user
|
||||
QMessageBox.information(self, 'UDS Configuration', 'Configuration saved.', QMessageBox.Ok)
|
||||
QMessageBox.information(
|
||||
self, 'UDS Configuration', 'Configuration saved.', QMessageBox.Ok
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -127,9 +163,9 @@ if __name__ == "__main__":
|
||||
os.environ['QT_X11_NO_MITSHM'] = '1'
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
|
||||
if udsactor.platform.operations.checkPermissions() is False:
|
||||
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok)
|
||||
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok) # type: ignore
|
||||
sys.exit(1)
|
||||
|
||||
if len(sys.argv) > 2:
|
||||
@@ -153,4 +189,4 @@ if __name__ == "__main__":
|
||||
|
||||
myapp = UDSConfigDialog()
|
||||
myapp.show()
|
||||
sys.exit(app.exec_())
|
||||
sys.exit(app.exec())
|
||||
|
@@ -10,8 +10,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>595</width>
|
||||
<height>220</height>
|
||||
<width>601</width>
|
||||
<height>243</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
@@ -55,7 +55,7 @@
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>180</y>
|
||||
<y>210</y>
|
||||
<width>181</width>
|
||||
<height>23</height>
|
||||
</rect>
|
||||
@@ -83,7 +83,7 @@
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>410</x>
|
||||
<y>180</y>
|
||||
<y>210</y>
|
||||
<width>171</width>
|
||||
<height>23</height>
|
||||
</rect>
|
||||
@@ -117,7 +117,7 @@
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>210</x>
|
||||
<y>180</y>
|
||||
<y>210</y>
|
||||
<width>181</width>
|
||||
<height>23</height>
|
||||
</rect>
|
||||
@@ -144,7 +144,7 @@
|
||||
<x>10</x>
|
||||
<y>10</y>
|
||||
<width>571</width>
|
||||
<height>161</height>
|
||||
<height>191</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
@@ -214,21 +214,21 @@
|
||||
<item row="2" column="1">
|
||||
<widget class="QLineEdit" name="serviceToken">
|
||||
<property name="toolTip">
|
||||
<string>UDS user with administration rights (Will not be stored on template)</string>
|
||||
<string>UDS Service Token</string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p>Administrator user on UDS Server.</p><p>Note: This credential will not be stored on client. Will be used to obtain an unique token for this image.</p></body></html></string>
|
||||
<string><html><head/><body><p>Token of the service on UDS platform</p><p>This token can be obtainend from the service configuration on UDS.</p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_loglevel">
|
||||
<property name="text">
|
||||
<string>Log Level</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="logLevelComboBox">
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
@@ -258,6 +258,23 @@
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_restrictNet">
|
||||
<property name="text">
|
||||
<string>Restrict Net</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="restrictNet">
|
||||
<property name="toolTip">
|
||||
<string>Restrict valid detection of network interfaces to this network.</string>
|
||||
</property>
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p>Restrics valid detection of network interfaces.</p><p>Note: Use this field only in case of several network interfaces, so UDS knows which one is the interface where the user will be connected..</p></body></html></string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<zorder>label_host</zorder>
|
||||
<zorder>host</zorder>
|
||||
@@ -267,6 +284,8 @@
|
||||
<zorder>label_security</zorder>
|
||||
<zorder>label_loglevel</zorder>
|
||||
<zorder>logLevelComboBox</zorder>
|
||||
<zorder>label_restrictNet</zorder>
|
||||
<zorder>restrictNet</zorder>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources>
|
||||
@@ -353,6 +372,22 @@
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>restrictNet</sender>
|
||||
<signal>textChanged(QString)</signal>
|
||||
<receiver>UdsActorSetupDialog</receiver>
|
||||
<slot>configChanged()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>341</x>
|
||||
<y>139</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>295</x>
|
||||
<y>121</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
<slots>
|
||||
<slot>finish()</slot>
|
||||
|
@@ -224,6 +224,9 @@
|
||||
<property name="whatsThis">
|
||||
<string><html><head/><body><p>Select the security for communications with UDS Broker.</p><p>The recommended method of communication is <span style=" font-weight:600;">Use SSL</span>, but selection needs to be acording to your broker configuration.</p></body></html></string>
|
||||
</property>
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Ignore certificate</string>
|
||||
|
@@ -35,4 +35,4 @@ from . import platform
|
||||
__title__ = 'udsactor'
|
||||
__author__ = 'Adolfo Gómez <dkmaster@dkmon.com>'
|
||||
__license__ = "BSD 3-clause"
|
||||
__copyright__ = "Copyright 2014-2020 VirtualCable S.L.U."
|
||||
__copyright__ = "Copyright 2014-2022 VirtualCable S.L.U."
|
||||
|
@@ -185,7 +185,8 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
|
||||
|
||||
try:
|
||||
# Notify loging and mark it
|
||||
self._loginInfo = self.api.login(platform.operations.getCurrentUser(), platform.operations.getSessionType())
|
||||
user, sessionType = platform.operations.getCurrentUser(), platform.operations.getSessionType()
|
||||
self._loginInfo = self.api.login(user, sessionType)
|
||||
|
||||
if self._loginInfo.max_idle:
|
||||
platform.operations.initIdleDuration(self._loginInfo.max_idle)
|
||||
@@ -197,8 +198,11 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
|
||||
|
||||
time.sleep(1.3) # Sleeps between loop iterations
|
||||
|
||||
self.api.logout(user + self._extraLogoff, sessionType)
|
||||
logger.info('Notified logout for %s (%s)', user, sessionType) # Log logout
|
||||
|
||||
# Clean up login info
|
||||
self._loginInfo = None
|
||||
self.api.logout(platform.operations.getCurrentUser() + self._extraLogoff)
|
||||
except Exception as e:
|
||||
logger.error('Error on client loop: %s', e)
|
||||
|
||||
@@ -235,7 +239,7 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
|
||||
pixmap: 'QPixmap' = self._qApp.primaryScreen().grabWindow(0) # type: ignore
|
||||
ba = QByteArray()
|
||||
buffer = QBuffer(ba)
|
||||
buffer.open(QIODevice.WriteOnly)
|
||||
buffer.open(QIODevice.OpenModeFlag.WriteOnly)
|
||||
pixmap.save(buffer, 'PNG')
|
||||
buffer.close()
|
||||
scrBase64 = bytes(ba.toBase64()).decode() # type: ignore # there are problems with Pylance and connects on PyQt5... :)
|
||||
|
@@ -1,7 +1,10 @@
|
||||
from .. import types
|
||||
|
||||
# Default certificate, will be overwritten by the first call to Broker, it's needed to wake up the server part of the actor
|
||||
# at the beginning, but will be replaced by the real certificate.
|
||||
defaultCertificate = types.CertificateInfoType(
|
||||
private_key='-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIFHTBPBgkqhkiG9w0BBQ0wQjApBgkqhkiG9w0BBQwwHAQIfG2+iMYJBswCAggA\nMAwGCCqGSIb3DQIJBQAwFQYJKwYBBAGXVQECBAhCusU5R8ulZQSCBMgheyZ81Qkq\n+TcbPeBlUGCFllSUOo7xQ/OuwYSmzLx8LpN0hQNv4azF6MYH+I8eMSPd3A547yW3\nJE4GjIBfRvcq2X1UZ2FQfECU9UP0ShPuPrVhIh6ZZklmlRjbIF8hGfSzXAuafQb+\n4wXXsofahi/SPgqK1Gw65nRiMcoeRZchJkx8pBgKVWED6Cbh6aAkeqkVKPnsebiV\n6kE+0C7+hgNUbyRd46R+/5NXzPjg4ItfSak+PLzQ1KeRv4Cu6DdzRKJ4V9/MlNdU\nNNEkSVSEaRn4sv+eByU4uxBMaSmD1tLc/A7OmaAeRpIQvls3Zcf2+V0+anAtjbjd\n6eIb2nceey+dKFm4ewlR4mXuzj1QowRTHceOIkvKIrOODxdy9M5hNBZ7VLum29tY\nRhqtmEH2BZZJ8SpM2SsEZzPxqJFiVZbvpeOKjxlMyn1dFWn1rP8uMnfuMKqBaj5D\nd5clOPlwebYw5UpM6Vvawu4nGqxECTSWcfNlDYO5U/0Fsm9+JIrJ7Buukgv2+rhs\nD/6oUK9NB8AW9qnDr7UxbC/ujhkKQG3woaZlPbiMs5WQaS+DrTg4N49wPzS0h+ME\nF8ZzuPnd6+sMGQioCIrQAZ08rk54oCijBhFh8/EQhQKGsMFw2swi9t6+FVU5Bvil\nlhmBd3LA5EuQ5y1X0jRL/+GDiUiZw1gOJP8d/XzhUJL9AmamdqJ6/rAU7lUTNWkM\ndzmFonUO2Mh2zgEEudHsTOH8udZ2l64LIHc6fCkDmM8QzghjrEFyci6R8333DSSM\nwbM0MvyTLM7TTqZUD60EgD+Ihyr/wJcBZY7GVn7hTq7ee14zeI+dZFmTMYOnt0mA\ngof19t0naPPZU+zyl/ambNF5mmSkGOAl4IBHNvPt5ztEVbNpwW3DHbmdYW71Ax+z\nCDlr4iKZahv21o1PCesPV2IlaHZFD6aBRt0DxzMqtq9cpWsI1g7aEaAjRbSvqhMY\npUeqFXz/GfR9rjRkufr48//ll0/Q/Ogx7m1TjQ6mAEQrklI7pa2W0u3H0BpSZSis\nR6ST3ulE+wfsp8cau6q2er+BSsDhBjSn9FeCUjHzY56u9ud/kb6/jLEdgxNpj0na\n3WVqCCCL/dAFSWznBmdracZsRMXapXInHCiiOEkXXbXIXvRKiTPJXdN+w2/U2j2B\nwXZuazVSpmM+xAZTAS9dtBUQJo+5px9b6P09uagvTA32ezbpPXf+hSfmTdUwbmAY\nrmE9SW85tzX+cD17loygBBRrjOr4uQy/s/9FqLx8bM73jly05rdOmX28ECKwEA05\n8aCFkfqrl9J9doVapaUlywpJVPFtE6W6tCF+ULMfb16vEjT1du1+epEnbGGLRQxg\n3aFLyKlvFaNvR38fiQFUGtBgGOaBN3rhGpbMwjch3oReXv9X/4UCL6sVIiOH2H3c\nVSZdC3O5g6CMVe4zckUe1k9mLDb5524IHDFfptZ6Bw+uzrqIy3GHW8dJF2AK471b\nMUnCojTpdbFHaUs2u/rNKVUyY+vLf8hkyP+znBUoPxSJtty53EWNukxjjsxx0lx3\niZGqN72lXlXuSFZAIxi307+xxE21cbzDsMidyJkbKKGm/F4BOKvX9jWmAyYmBG6A\n1L3yNRouFWsYDwYAX2nZ1is=\n-----END ENCRYPTED PRIVATE KEY-----\n',
|
||||
server_certificate='-----BEGIN CERTIFICATE-----\nMIIDcTCCAlkCBDfnXU8wDQYJKoZIhvcNAQELBQAwfTELMAkGA1UEBhMCRVMxDzAN\nBgNVBAgMBk1hZHJpZDEPMA0GA1UEBwwGTWFkcmlkMREwDwYDVQQKDAhVRFMgQ2Vy\ndDERMA8GA1UECwwIVURTIENlcnQxEjAQBgNVBAMMCTEyNy4wLjAuMTESMBAGA1Ud\nEQwJMTI3LjAuMC4xMB4XDTIwMDIxNzExNTkzMloXDTMwMDIxNDExNTkzMlowfTEL\nMAkGA1UEBhMCRVMxDzANBgNVBAgMBk1hZHJpZDEPMA0GA1UEBwwGTWFkcmlkMREw\nDwYDVQQKDAhVRFMgQ2VydDERMA8GA1UECwwIVURTIENlcnQxEjAQBgNVBAMMCTEy\nNy4wLjAuMTESMBAGA1UdEQwJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0BAQEFAAOC\nAQ8AMIIBCgKCAQEA2e1cW7YtRpNLazR3f/LqLv8OB0rKh8cUPH4wuQhbBTkee8Wu\n5eMSadRCIyRbKj4b8dtVfI9QW0SrmhGuMx1KCh3CsYd9XsWiKbGkiRBHIDOn5pkF\n6PUayDJ8KjnGbfnZjp0AmxXP4r1OO8jUPqzKS9Ubf5PgwcwdFiUKVfVPwGwctwt5\nt9YpSRONw0rTsCjVHvO2dd9h6EopskLCWxpN8l9kNLwLM/6t0IqVKmn5/IYPKKN2\nCX8a7IXpxwoiUs4sBZYhUMBWikB1hKQRSYafp1Xvc5PeTFXTFqGANnqz0NoZ8tqL\n8qjQUN/PCdtzhfcP5RgT2g1qyS2RBCMYH7Zs0wIDAQABMA0GCSqGSIb3DQEBCwUA\nA4IBAQCUt+qlLA1N9VXMwDQAYG4Kt6/UlMHCXAajHQQGtjdyGJ4++m7EIjI96hMU\n3Cx2gp2ggR3JGnuSR+DdBvPl5iGku7J8KV0JiJg30gTY8JuUIy/PMLZWloYKrBHV\nlin2GujQ4OsIt3dbr4XtcKW1Wd7L6fBzHlq7Xyxh+gcTzTvTmq67Q9XKlBWsegMf\nv4FKy0lfcSFK3vTzswQtuTontG4TqLiT/4AnMt3D0cTQ6b6KoZwUUX/TDNhau06d\nQ4Ilz8X61ka+4HBkFSR5ahP9noCVhwO329h+6epO141E5Tep3OLc/GCF4oaKOlMR\nfqxf5f2bghU0fxmtEoNJTZkBsN1S\n-----END CERTIFICATE-----\n',
|
||||
password='Pw7qbatz5u-y-Z5ora2D2ZuBCm95AHnKRcpze53k8tw'
|
||||
password='Pw7qbatz5u-y-Z5ora2D2ZuBCm95AHnKRcpze53k8tw',
|
||||
ciphers=''
|
||||
)
|
||||
|
@@ -37,9 +37,9 @@ import requests
|
||||
from ..log import logger
|
||||
|
||||
# For avoid proxy on localhost connections
|
||||
NO_PROXY = {
|
||||
'http': None,
|
||||
'https': None,
|
||||
NO_PROXY: typing.Dict[str, str] = {
|
||||
'http': '',
|
||||
'https': '',
|
||||
}
|
||||
|
||||
class UDSActorClientPool:
|
||||
|
@@ -42,7 +42,7 @@ class LocalProvider(handler.Handler):
|
||||
return result._asdict()
|
||||
|
||||
def post_logout(self) -> typing.Any:
|
||||
self._service.logout(self._params['username'])
|
||||
self._service.logout(self._params['username'], self._params['session_type'])
|
||||
return 'ok'
|
||||
|
||||
def post_ping(self) -> typing.Any:
|
||||
|
@@ -38,6 +38,7 @@ from ..log import logger
|
||||
if typing.TYPE_CHECKING:
|
||||
from ..service import CommonService
|
||||
|
||||
|
||||
class PublicProvider(handler.Handler):
|
||||
def post_logout(self) -> typing.Any:
|
||||
logger.debug('Sending LOGOFF to clients')
|
||||
@@ -51,7 +52,9 @@ class PublicProvider(handler.Handler):
|
||||
logger.debug('Sending MESSAGE to clients')
|
||||
if 'message' not in self._params:
|
||||
raise Exception('Invalid message parameters')
|
||||
self._service._clientsPool.message(self._params['message']) # pylint: disable=protected-access
|
||||
self._service._clientsPool.message(
|
||||
self._params['message']
|
||||
) # pylint: disable=protected-access
|
||||
return 'ok'
|
||||
|
||||
def post_script(self) -> typing.Any:
|
||||
@@ -60,7 +63,9 @@ class PublicProvider(handler.Handler):
|
||||
raise Exception('Invalid script parameters')
|
||||
if self._params.get('user', False):
|
||||
logger.debug('Sending SCRIPT to client')
|
||||
self._service._clientsPool.executeScript(self._params['script']) # pylint: disable=protected-access
|
||||
self._service._clientsPool.executeScript(
|
||||
self._params['script']
|
||||
) # pylint: disable=protected-access
|
||||
else:
|
||||
# Execute script at server space, that is, here
|
||||
# as a parallel thread
|
||||
@@ -72,14 +77,22 @@ class PublicProvider(handler.Handler):
|
||||
logger.debug('Received Pre connection')
|
||||
if 'user' not in self._params or 'protocol' not in self._params:
|
||||
raise Exception('Invalid preConnect parameters')
|
||||
return self._service.preConnect(self._params['user'], self._params['protocol'], self._params.get('ip', 'unknown'), self._params.get('hostname', 'unknown'))
|
||||
return self._service.preConnect(
|
||||
self._params['user'],
|
||||
self._params['protocol'],
|
||||
self._params.get('ip', 'unknown'),
|
||||
self._params.get('hostname', 'unknown'),
|
||||
self._params.get('udsuser', 'unknown'),
|
||||
)
|
||||
|
||||
def get_information(self) -> typing.Any:
|
||||
# Return something useful? :)
|
||||
return 'UDS Actor Secure Server'
|
||||
|
||||
def get_screenshot(self) -> typing.Any:
|
||||
return self._service._clientsPool.screenshot() # pylint: disable=protected-access
|
||||
return (
|
||||
self._service._clientsPool.screenshot()
|
||||
) # pylint: disable=protected-access
|
||||
|
||||
def get_uuid(self) -> typing.Any:
|
||||
if self._service.isManaged():
|
||||
|
@@ -42,11 +42,18 @@ from .. import rest
|
||||
from .public import PublicProvider
|
||||
from .local import LocalProvider
|
||||
|
||||
# a couple of 1.2 ciphers + 1.3 ciphers (implicit)
|
||||
DEFAULT_CIPHERS = (
|
||||
'ECDHE-RSA-AES128-GCM-SHA256'
|
||||
':ECDHE-RSA-AES256-GCM-SHA384'
|
||||
)
|
||||
|
||||
# Not imported at runtime, just for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
from ..service import CommonService
|
||||
from .handler import Handler
|
||||
|
||||
|
||||
class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
|
||||
protocol_version = 'HTTP/1.0'
|
||||
server_version = 'UDS Actor Server'
|
||||
@@ -54,7 +61,12 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
_service: typing.Optional['CommonService'] = None
|
||||
|
||||
def sendJsonResponse(self, result: typing.Optional[typing.Any] = None, error: typing.Optional[str] = None, code: int = 200) -> None:
|
||||
def sendJsonResponse(
|
||||
self,
|
||||
result: typing.Optional[typing.Any] = None,
|
||||
error: typing.Optional[str] = None,
|
||||
code: int = 200,
|
||||
) -> None:
|
||||
data = json.dumps({'result': result, 'error': error})
|
||||
self.send_response(code)
|
||||
self.send_header('Content-type', 'application/json')
|
||||
@@ -71,11 +83,13 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
|
||||
# Very simple path & params splitter
|
||||
path = self.path.split('?')[0][1:].split('/')
|
||||
|
||||
logger.debug('Path: %s, params: %s', path, params)
|
||||
logger.debug('Path: %s, ip: %s, params: %s', path, self.client_address, params)
|
||||
|
||||
handlerType: typing.Optional[typing.Type['Handler']] = None
|
||||
|
||||
if len(path) == 3 and path[0] == 'actor' and path[1] == self._service._secret: # pylint: disable=protected-access
|
||||
if (
|
||||
len(path) == 3 and path[0] == 'actor' and path[1] == self._service._secret
|
||||
): # pylint: disable=protected-access
|
||||
# public method
|
||||
handlerType = PublicProvider
|
||||
elif len(path) == 2 and path[0] == 'ui':
|
||||
@@ -88,12 +102,18 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
|
||||
return
|
||||
|
||||
try:
|
||||
result = getattr(handlerType(self._service, method, params), method + '_' + path[-1])() # last part of path is method
|
||||
result = getattr(
|
||||
handlerType(self._service, method, params), method + '_' + path[-1]
|
||||
)() # last part of path is method
|
||||
except AttributeError:
|
||||
self.sendJsonResponse(error='Method not found', code=404)
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error('Got exception executing {} {}: {}'.format(method, '/'.join(path), str(e)))
|
||||
logger.error(
|
||||
'Got exception executing {} {}: {}'.format(
|
||||
method, '/'.join(path), str(e)
|
||||
)
|
||||
)
|
||||
self.sendJsonResponse(error=str(e), code=500)
|
||||
return
|
||||
|
||||
@@ -101,7 +121,10 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
def do_GET(self) -> None:
|
||||
try:
|
||||
params = {v.split('=')[0]: v.split('=')[1] for v in self.path.split('?')[1].split('&')}
|
||||
params = {
|
||||
v.split('=')[0]: v.split('=')[1]
|
||||
for v in self.path.split('?')[1].split('&')
|
||||
}
|
||||
except Exception:
|
||||
params = {}
|
||||
|
||||
@@ -113,7 +136,9 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
|
||||
content = self.rfile.read(length)
|
||||
params: typing.MutableMapping[str, str] = json.loads(content)
|
||||
except Exception as e:
|
||||
logger.error('Got exception executing POST {}: {}'.format(self.path, str(e)))
|
||||
logger.error(
|
||||
'Got exception executing POST {}: {}'.format(self.path, str(e))
|
||||
)
|
||||
self.sendJsonResponse(error='Invalid parameters', code=400)
|
||||
return
|
||||
|
||||
@@ -125,6 +150,7 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
|
||||
def log_message(self, format, *args): # pylint: disable=redefined-builtin
|
||||
logger.debug(format, *args)
|
||||
|
||||
|
||||
class HTTPServerThread(threading.Thread):
|
||||
_server: typing.Optional[http.server.HTTPServer]
|
||||
_service: 'CommonService'
|
||||
@@ -153,13 +179,22 @@ class HTTPServerThread(threading.Thread):
|
||||
def run(self):
|
||||
HTTPServerHandler._service = self._service # pylint: disable=protected-access
|
||||
|
||||
self._certFile, password = certs.saveCertificate(self._service._certificate) # pylint: disable=protected-access
|
||||
self._certFile, password = certs.saveCertificate(
|
||||
self._service._certificate
|
||||
) # pylint: disable=protected-access
|
||||
|
||||
self._server = http.server.HTTPServer(('0.0.0.0', rest.LISTEN_PORT), HTTPServerHandler)
|
||||
self._server = http.server.HTTPServer(
|
||||
('0.0.0.0', rest.LISTEN_PORT), HTTPServerHandler
|
||||
)
|
||||
# self._server.socket = ssl.wrap_socket(self._server.socket, certfile=self.certFile, server_side=True)
|
||||
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.options = ssl.CERT_NONE
|
||||
# Disable TLSv1.0 and TLSv1.1, use only TLSv1.3 or TLSv1.2 with allowed ciphers
|
||||
context.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
|
||||
# If a configures ciphers are provided, use them, otherwise use the default ones
|
||||
context.set_ciphers(self._service._certificate.ciphers or DEFAULT_CIPHERS)
|
||||
|
||||
context.load_cert_chain(certfile=self._certFile, password=password)
|
||||
self._server.socket = context.wrap_socket(self._server.socket, server_side=True)
|
||||
|
||||
|
@@ -1 +0,0 @@
|
||||
VERSION = '3.0.0'
|
@@ -37,6 +37,7 @@ import typing
|
||||
class LocalLogger: # pylint: disable=too-few-public-methods
|
||||
linux = False
|
||||
windows = True
|
||||
serviceLogger = False
|
||||
|
||||
logger: typing.Optional[logging.Logger]
|
||||
|
||||
|
@@ -41,10 +41,11 @@ import typing
|
||||
|
||||
from .. import types
|
||||
|
||||
|
||||
from udsactor.log import logger
|
||||
from .renamer import rename
|
||||
from . import xss
|
||||
|
||||
|
||||
def _getMacAddr(ifname: str) -> typing.Optional[str]:
|
||||
'''
|
||||
Returns the mac address of an interface
|
||||
@@ -91,12 +92,12 @@ def _getInterfaces() -> typing.List[str]:
|
||||
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
names = array.array(str('B'), b'\0' * space)
|
||||
outbytes = struct.unpack(str('iL'), fcntl.ioctl(
|
||||
outbytes = struct.unpack('iL', fcntl.ioctl(
|
||||
s.fileno(),
|
||||
0x8912, # SIOCGIFCONF
|
||||
struct.pack(str('iL'), space, names.buffer_info()[0])
|
||||
struct.pack('iL', space, names.buffer_info()[0])
|
||||
))[0]
|
||||
namestr = names.tostring()
|
||||
namestr = names.tobytes()
|
||||
# return namestr, outbytes
|
||||
return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)]
|
||||
|
||||
@@ -106,6 +107,7 @@ def _getIpAndMac(ifname: str) -> typing.Tuple[typing.Optional[str], typing.Optio
|
||||
return (ip, mac)
|
||||
|
||||
def checkPermissions() -> bool:
|
||||
return True
|
||||
return os.getuid() == 0 # getuid only available on linux. Expect "complaioins" if edited from Windows
|
||||
|
||||
def getComputerName() -> str:
|
||||
@@ -137,14 +139,19 @@ def reboot(flags: int = 0):
|
||||
'''
|
||||
Simple reboot using os command
|
||||
'''
|
||||
subprocess.call(['/sbin/shutdown', 'now', '-r'])
|
||||
|
||||
try:
|
||||
subprocess.call(['/sbin/shutdown', 'now', '-r'])
|
||||
except Exception as e:
|
||||
logger.error('Error rebooting: %s', e)
|
||||
|
||||
def loggoff() -> None:
|
||||
'''
|
||||
Right now restarts the machine...
|
||||
'''
|
||||
subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']])
|
||||
try:
|
||||
subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']])
|
||||
except Exception as e:
|
||||
logger.error('Error killing user processes: %s', e)
|
||||
# subprocess.call(['/sbin/shutdown', 'now', '-r'])
|
||||
# subprocess.call(['/usr/bin/systemctl', 'reboot', '-i'])
|
||||
|
||||
@@ -155,7 +162,7 @@ def renameComputer(newName: str) -> bool:
|
||||
Returns True if reboot needed
|
||||
'''
|
||||
rename(newName)
|
||||
return True # Always reboot right now. Not much slower but much more better
|
||||
return True # Always reboot right now. Not much slower but much more convenient
|
||||
|
||||
|
||||
def joinDomain(domain: str, ou: str, account: str, password: str, executeInOneStep: bool = False):
|
||||
|
@@ -53,9 +53,10 @@ def readConfig() -> types.ActorConfigurationType:
|
||||
return types.ActorConfigurationType(
|
||||
actorType=uds.get('type', types.MANAGED),
|
||||
host=uds.get('host', ''),
|
||||
validateCertificate=uds.getboolean('validate', fallback=False),
|
||||
validateCertificate=uds.getboolean('validate', fallback=True),
|
||||
master_token=uds.get('master_token', None),
|
||||
own_token=uds.get('own_token', None),
|
||||
restrict_net=uds.get('restrict_net', None),
|
||||
pre_command=uds.get('pre_command', None),
|
||||
runonce_command=uds.get('runonce_command', None),
|
||||
post_command=uds.get('post_command', None),
|
||||
@@ -78,6 +79,7 @@ def writeConfig(config: types.ActorConfigurationType) -> None:
|
||||
writeIfValue(config.actorType, 'type')
|
||||
writeIfValue(config.master_token, 'master_token')
|
||||
writeIfValue(config.own_token, 'own_token')
|
||||
writeIfValue(config.restrict_net, 'restrict_net')
|
||||
writeIfValue(config.pre_command, 'pre_command')
|
||||
writeIfValue(config.post_command, 'post_command')
|
||||
writeIfValue(config.runonce_command, 'runonce_command')
|
||||
|
@@ -33,6 +33,10 @@ import ctypes
|
||||
import ctypes.util
|
||||
import subprocess
|
||||
|
||||
|
||||
from udsactor.log import logger
|
||||
|
||||
|
||||
xlib = None
|
||||
xss = None
|
||||
display = None
|
||||
@@ -107,9 +111,12 @@ def _ensureInitialized():
|
||||
def initIdleDuration(atLeastSeconds: int) -> None:
|
||||
_ensureInitialized()
|
||||
if atLeastSeconds:
|
||||
subprocess.call(['/usr/bin/xset', 's', '{}'.format(atLeastSeconds + 30)])
|
||||
# And now reset it
|
||||
subprocess.call(['/usr/bin/xset', 's', 'reset'])
|
||||
try:
|
||||
subprocess.call(['/usr/bin/xset', 's', '{}'.format(atLeastSeconds + 30)])
|
||||
# And now reset it
|
||||
subprocess.call(['/usr/bin/xset', 's', 'reset'])
|
||||
except Exception as e:
|
||||
logger.error('Error setting screensaver time: %s', e)
|
||||
|
||||
|
||||
def getIdleDuration() -> float:
|
||||
|
@@ -31,47 +31,60 @@
|
||||
# pylint: disable=invalid-name
|
||||
import warnings
|
||||
import json
|
||||
import ssl
|
||||
import logging
|
||||
import typing
|
||||
|
||||
import requests
|
||||
import requests.adapters
|
||||
|
||||
from . import types
|
||||
from .info import VERSION
|
||||
from .version import VERSION
|
||||
|
||||
# Default public listen port
|
||||
LISTEN_PORT = 43910
|
||||
|
||||
# Default timeout
|
||||
TIMEOUT = 5 # 5 seconds is more than enought
|
||||
TIMEOUT = 5 # 5 seconds is more than enought
|
||||
|
||||
# Constants
|
||||
UNKNOWN = 'unknown'
|
||||
|
||||
|
||||
class RESTError(Exception):
|
||||
ERRCODE = 0
|
||||
|
||||
|
||||
class RESTConnectionError(RESTError):
|
||||
ERRCODE = -1
|
||||
|
||||
|
||||
# Errors ""raised"" from broker
|
||||
class RESTInvalidKeyError(RESTError):
|
||||
ERRCODE = 1
|
||||
|
||||
|
||||
class RESTUnmanagedHostError(RESTError):
|
||||
ERRCODE = 2
|
||||
|
||||
|
||||
class RESTUserServiceNotFoundError(RESTError):
|
||||
ERRCODE = 3
|
||||
|
||||
|
||||
class RESTOsManagerError(RESTError):
|
||||
ERRCODE = 4
|
||||
|
||||
|
||||
# For avoid proxy on localhost connections
|
||||
NO_PROXY = {
|
||||
'http': None,
|
||||
'https': None,
|
||||
}
|
||||
|
||||
UDS_BASE_URL = 'https://{}/uds/rest/'
|
||||
|
||||
|
||||
#
|
||||
# Basic UDS Api
|
||||
#
|
||||
@@ -79,48 +92,74 @@ class UDSApi: # pylint: disable=too-few-public-methods
|
||||
"""
|
||||
Base for remote api accesses
|
||||
"""
|
||||
|
||||
_host: str
|
||||
_validateCert: bool
|
||||
_url: str
|
||||
_session: 'requests.Session'
|
||||
|
||||
def __init__(self, host: str, validateCert: bool) -> None:
|
||||
self._host = host
|
||||
self._validateCert = validateCert
|
||||
self._url = "https://{}/uds/rest/".format(self._host)
|
||||
self._url = UDS_BASE_URL.format(self._host)
|
||||
# Disable logging requests messages except for errors, ...
|
||||
logging.getLogger("requests").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("urllib3").setLevel(logging.ERROR)
|
||||
logging.getLogger('request').setLevel(logging.CRITICAL)
|
||||
logging.getLogger('urllib3').setLevel(logging.ERROR)
|
||||
try:
|
||||
warnings.simplefilter("ignore") # Disables all warnings
|
||||
warnings.simplefilter('ignore') # Disables all warnings
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
context = (
|
||||
ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
|
||||
if validateCert
|
||||
else ssl._create_unverified_context(purpose=ssl.Purpose.SERVER_AUTH, check_hostname=False)
|
||||
)
|
||||
# Disable SSLv2, SSLv3, TLSv1, TLSv1.1, TLSv1.2
|
||||
context.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||
|
||||
# Configure session security
|
||||
class UDSHTTPAdapter(requests.adapters.HTTPAdapter):
|
||||
def init_poolmanager(self, *args, **kwargs) -> None:
|
||||
kwargs["ssl_context"] = context
|
||||
|
||||
return super().init_poolmanager(*args, **kwargs)
|
||||
|
||||
def cert_verify(self, conn, url, verify, cert): # pylint: disable=unused-argument
|
||||
# Overridden to do nothing
|
||||
return super().cert_verify(conn, url, validateCert, cert)
|
||||
|
||||
self._session = requests.Session()
|
||||
self._session.mount("https://", UDSHTTPAdapter())
|
||||
|
||||
@property
|
||||
def _headers(self) -> typing.MutableMapping[str, str]:
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'UDS Actor v{}'.format(VERSION)
|
||||
'User-Agent': 'UDS Actor v{}'.format(VERSION),
|
||||
}
|
||||
|
||||
def _apiURL(self, method: str) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def _doPost(
|
||||
self,
|
||||
method: str, # i.e. 'initialize', 'ready', ....
|
||||
payLoad: typing.MutableMapping[str, typing.Any],
|
||||
headers: typing.Optional[typing.MutableMapping[str, str]] = None,
|
||||
disableProxy: bool = False
|
||||
) -> typing.Any:
|
||||
self,
|
||||
method: str, # i.e. 'initialize', 'ready', ....
|
||||
payLoad: typing.MutableMapping[str, typing.Any],
|
||||
headers: typing.Optional[typing.MutableMapping[str, str]] = None,
|
||||
disableProxy: bool = False,
|
||||
) -> typing.Any:
|
||||
headers = headers or self._headers
|
||||
try:
|
||||
result = requests.post(
|
||||
result = self._session.post(
|
||||
self._apiURL(method),
|
||||
data=json.dumps(payLoad),
|
||||
headers=headers,
|
||||
verify=self._validateCert,
|
||||
# verify=self._validateCert, Not needed, already in session
|
||||
timeout=TIMEOUT,
|
||||
proxies=NO_PROXY if disableProxy else None # if not proxies wanted, enforce it
|
||||
proxies=NO_PROXY # type: ignore
|
||||
if disableProxy
|
||||
else None, # if not proxies wanted, enforce it
|
||||
)
|
||||
|
||||
if result.ok:
|
||||
@@ -139,6 +178,7 @@ class UDSApi: # pylint: disable=too-few-public-methods
|
||||
|
||||
raise RESTError(data)
|
||||
|
||||
|
||||
#
|
||||
# UDS Broker API access
|
||||
#
|
||||
@@ -148,7 +188,12 @@ class UDSServerApi(UDSApi):
|
||||
|
||||
def enumerateAuthenticators(self) -> typing.Iterable[types.AuthenticatorType]:
|
||||
try:
|
||||
result = requests.get(self._url + 'auth/auths', headers=self._headers, verify=self._validateCert, timeout=4)
|
||||
result = self._session.get(
|
||||
self._url + 'auth/auths',
|
||||
headers=self._headers,
|
||||
# verify=self._validateCert,
|
||||
timeout=4,
|
||||
)
|
||||
if result.ok:
|
||||
for v in sorted(result.json(), key=lambda x: x['priority']):
|
||||
yield types.AuthenticatorType(
|
||||
@@ -157,9 +202,9 @@ class UDSServerApi(UDSApi):
|
||||
auth=v['auth'],
|
||||
type=v['type'],
|
||||
priority=v['priority'],
|
||||
isCustom=v['isCustom']
|
||||
isCustom=v['isCustom'],
|
||||
)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def register( # pylint: disable=too-many-arguments, too-many-locals
|
||||
@@ -173,7 +218,7 @@ class UDSServerApi(UDSApi):
|
||||
preCommand: str,
|
||||
runOnceCommand: str,
|
||||
postCommand: str,
|
||||
logLevel: int
|
||||
logLevel: int,
|
||||
) -> str:
|
||||
"""
|
||||
Raises an exception if could not register, or registers and returns the "authorization token"
|
||||
@@ -186,7 +231,7 @@ class UDSServerApi(UDSApi):
|
||||
'pre_command': preCommand,
|
||||
'run_once_command': runOnceCommand,
|
||||
'post_command': postCommand,
|
||||
'log_level': logLevel
|
||||
'log_level': logLevel,
|
||||
}
|
||||
|
||||
# First, try to login to REST api
|
||||
@@ -194,13 +239,23 @@ class UDSServerApi(UDSApi):
|
||||
# First, try to login
|
||||
authInfo = {'auth': auth, 'username': username, 'password': password}
|
||||
headers = self._headers
|
||||
result = requests.post(self._url + 'auth/login', data=json.dumps(authInfo), headers=headers, verify=self._validateCert)
|
||||
result = self._session.post(
|
||||
self._url + 'auth/login',
|
||||
data=json.dumps(authInfo),
|
||||
headers=headers,
|
||||
# verify=self._validateCert,
|
||||
)
|
||||
if not result.ok or result.json()['result'] == 'error':
|
||||
raise Exception() # Invalid credentials
|
||||
|
||||
headers['X-Auth-Token'] = result.json()['token']
|
||||
|
||||
result = requests.post(self._apiURL('register'), data=json.dumps(data), headers=headers, verify=self._validateCert)
|
||||
result = self._session.post(
|
||||
self._apiURL('register'),
|
||||
data=json.dumps(data),
|
||||
headers=headers,
|
||||
# verify=self._validateCert,
|
||||
)
|
||||
if result.ok:
|
||||
return result.json()['result']
|
||||
except requests.ConnectionError as e:
|
||||
@@ -212,13 +267,18 @@ class UDSServerApi(UDSApi):
|
||||
|
||||
raise RESTError(result.content.decode())
|
||||
|
||||
def initialize(self, token: str, interfaces: typing.Iterable[types.InterfaceInfoType], actor_type: typing.Optional[str]) -> types.InitializationResultType:
|
||||
def initialize(
|
||||
self,
|
||||
token: str,
|
||||
interfaces: typing.Iterable[types.InterfaceInfoType],
|
||||
actor_type: typing.Optional[str],
|
||||
) -> types.InitializationResultType:
|
||||
# Generate id list from netork cards
|
||||
payload = {
|
||||
'type': actor_type or types.MANAGED,
|
||||
'token': token,
|
||||
'version': VERSION,
|
||||
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces]
|
||||
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
|
||||
}
|
||||
r = self._doPost('initialize', payload)
|
||||
os = r['os']
|
||||
@@ -232,53 +292,58 @@ class UDSServerApi(UDSApi):
|
||||
password=os.get('password'),
|
||||
new_password=os.get('new_password'),
|
||||
ad=os.get('ad'),
|
||||
ou=os.get('ou')
|
||||
) if r['os'] else None
|
||||
ou=os.get('ou'),
|
||||
)
|
||||
if r['os']
|
||||
else None,
|
||||
)
|
||||
|
||||
def ready(self, own_token: str, secret: str, ip: str, port: int) -> types.CertificateInfoType:
|
||||
payload = {
|
||||
'token': own_token,
|
||||
'secret': secret,
|
||||
'ip': ip,
|
||||
'port': port
|
||||
}
|
||||
def ready(
|
||||
self, own_token: str, secret: str, ip: str, port: int
|
||||
) -> types.CertificateInfoType:
|
||||
payload = {'token': own_token, 'secret': secret, 'ip': ip, 'port': port}
|
||||
result = self._doPost('ready', payload)
|
||||
|
||||
return types.CertificateInfoType(
|
||||
private_key=result['private_key'],
|
||||
server_certificate=result['server_certificate'],
|
||||
password=result['password']
|
||||
password=result['password'],
|
||||
ciphers=result.get('ciphers', ''),
|
||||
)
|
||||
|
||||
def notifyIpChange(self, own_token: str, secret: str, ip: str, port: int) -> types.CertificateInfoType:
|
||||
payload = {
|
||||
'token': own_token,
|
||||
'secret': secret,
|
||||
'ip': ip,
|
||||
'port': port
|
||||
}
|
||||
def notifyIpChange(
|
||||
self, own_token: str, secret: str, ip: str, port: int
|
||||
) -> types.CertificateInfoType:
|
||||
payload = {'token': own_token, 'secret': secret, 'ip': ip, 'port': port}
|
||||
result = self._doPost('ipchange', payload)
|
||||
|
||||
return types.CertificateInfoType(
|
||||
private_key=result['private_key'],
|
||||
server_certificate=result['server_certificate'],
|
||||
password=result['password']
|
||||
password=result['password'],
|
||||
ciphers=result.get('ciphers', ''),
|
||||
)
|
||||
|
||||
def notifyUnmanagedCallback(self, master_token: str, secret: str, interfaces: typing.Iterable[types.InterfaceInfoType], port: int) -> types.CertificateInfoType:
|
||||
def notifyUnmanagedCallback(
|
||||
self,
|
||||
master_token: str,
|
||||
secret: str,
|
||||
interfaces: typing.Iterable[types.InterfaceInfoType],
|
||||
port: int,
|
||||
) -> types.CertificateInfoType:
|
||||
payload = {
|
||||
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
|
||||
'token': master_token,
|
||||
'secret': secret,
|
||||
'port': port
|
||||
'port': port,
|
||||
}
|
||||
result = self._doPost('unmanaged', payload)
|
||||
|
||||
return types.CertificateInfoType(
|
||||
private_key=result['private_key'],
|
||||
server_certificate=result['server_certificate'],
|
||||
password=result['password']
|
||||
password=result['password'],
|
||||
ciphers=result.get('ciphers', ''),
|
||||
)
|
||||
|
||||
def login(
|
||||
@@ -288,14 +353,11 @@ class UDSServerApi(UDSApi):
|
||||
username: str,
|
||||
sessionType: str,
|
||||
interfaces: typing.Iterable[types.InterfaceInfoType],
|
||||
secret: typing.Optional[str]
|
||||
secret: typing.Optional[str],
|
||||
) -> types.LoginResultInfoType:
|
||||
if not token:
|
||||
return types.LoginResultInfoType(
|
||||
ip='0.0.0.0',
|
||||
hostname=UNKNOWN,
|
||||
dead_line=None,
|
||||
max_idle=None
|
||||
ip='0.0.0.0', hostname=UNKNOWN, dead_line=None, max_idle=None
|
||||
)
|
||||
payload = {
|
||||
'type': actor_type or types.MANAGED,
|
||||
@@ -310,7 +372,7 @@ class UDSServerApi(UDSApi):
|
||||
ip=result['ip'],
|
||||
hostname=result['hostname'],
|
||||
dead_line=result['dead_line'],
|
||||
max_idle=result['max_idle']
|
||||
max_idle=result['max_idle'],
|
||||
)
|
||||
|
||||
def logout(
|
||||
@@ -318,29 +380,26 @@ class UDSServerApi(UDSApi):
|
||||
actor_type: typing.Optional[str],
|
||||
token: str,
|
||||
username: str,
|
||||
sessionType: str,
|
||||
interfaces: typing.Iterable[types.InterfaceInfoType],
|
||||
secret: typing.Optional[str]
|
||||
) -> None:
|
||||
secret: typing.Optional[str],
|
||||
) -> typing.Optional[str]:
|
||||
if not token:
|
||||
return
|
||||
return None
|
||||
payload = {
|
||||
'type': actor_type or types.MANAGED,
|
||||
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
|
||||
'token': token,
|
||||
'username': username,
|
||||
'secret': secret or ''
|
||||
'session_type': sessionType,
|
||||
'secret': secret or '',
|
||||
}
|
||||
self._doPost('logout', payload)
|
||||
|
||||
return self._doPost('logout', payload) # Can be 'ok' or 'notified'
|
||||
|
||||
def log(self, own_token: str, level: int, message: str) -> None:
|
||||
if not own_token:
|
||||
return
|
||||
payLoad = {
|
||||
'token': own_token,
|
||||
'level': level,
|
||||
'message': message
|
||||
}
|
||||
payLoad = {'token': own_token, 'level': level, 'message': message}
|
||||
self._doPost('log', payLoad) # Ignores result...
|
||||
|
||||
def test(self, master_token: str, actorType: typing.Optional[str]) -> bool:
|
||||
@@ -359,26 +418,25 @@ class UDSClientApi(UDSApi):
|
||||
|
||||
def _apiURL(self, method: str) -> str:
|
||||
return self._url + method
|
||||
|
||||
def post(
|
||||
self,
|
||||
method: str, # i.e. 'initialize', 'ready', ....
|
||||
payLoad: typing.MutableMapping[str, typing.Any]
|
||||
) -> typing.Any:
|
||||
self,
|
||||
method: str, # i.e. 'initialize', 'ready', ....
|
||||
payLoad: typing.MutableMapping[str, typing.Any],
|
||||
) -> typing.Any:
|
||||
return self._doPost(method=method, payLoad=payLoad, disableProxy=True)
|
||||
|
||||
def register(self, callbackUrl: str) -> None:
|
||||
payLoad = {
|
||||
'callback_url': callbackUrl
|
||||
}
|
||||
payLoad = {'callback_url': callbackUrl}
|
||||
self.post('register', payLoad)
|
||||
|
||||
def unregister(self, callbackUrl: str) -> None:
|
||||
payLoad = {
|
||||
'callback_url': callbackUrl
|
||||
}
|
||||
payLoad = {'callback_url': callbackUrl}
|
||||
self.post('unregister', payLoad)
|
||||
|
||||
def login(self, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType:
|
||||
def login(
|
||||
self, username: str, sessionType: typing.Optional[str] = None
|
||||
) -> types.LoginResultInfoType:
|
||||
payLoad = {
|
||||
'username': username,
|
||||
'session_type': sessionType or UNKNOWN,
|
||||
@@ -388,13 +446,11 @@ class UDSClientApi(UDSApi):
|
||||
ip=result['ip'],
|
||||
hostname=result['hostname'],
|
||||
dead_line=result['dead_line'],
|
||||
max_idle=result['max_idle']
|
||||
max_idle=result['max_idle'],
|
||||
)
|
||||
|
||||
def logout(self, username: str) -> None:
|
||||
payLoad = {
|
||||
'username': username
|
||||
}
|
||||
def logout(self, username: str, sessionType: typing.Optional[str]) -> None:
|
||||
payLoad = {'username': username, 'session_type': sessionType or UNKNOWN}
|
||||
self.post('logout', payLoad)
|
||||
|
||||
def ping(self) -> bool:
|
||||
|
@@ -39,6 +39,7 @@ import typing
|
||||
from . import platform
|
||||
from . import rest
|
||||
from . import types
|
||||
from . import tools
|
||||
|
||||
from .log import logger, DEBUG, INFO, ERROR, FATAL
|
||||
from .http import clients_pool, server, cert
|
||||
@@ -55,6 +56,7 @@ from .http import clients_pool, server, cert
|
||||
# else:
|
||||
# logger.setLevel(20000)
|
||||
|
||||
|
||||
class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
_isAlive: bool = True
|
||||
_rebootRequested: bool = False
|
||||
@@ -75,7 +77,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
logger.debug('Executing command on {}: {}'.format(section, cmdLine))
|
||||
res = subprocess.check_call(cmdLine, shell=True)
|
||||
except Exception as e:
|
||||
logger.error('Got exception executing: {} - {} - {}'.format(section, cmdLine, e))
|
||||
logger.error(
|
||||
'Got exception executing: {} - {} - {}'.format(section, cmdLine, e)
|
||||
)
|
||||
return False
|
||||
logger.debug('Result of executing cmd for {} was {}'.format(section, res))
|
||||
return True
|
||||
@@ -86,7 +90,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
self._api = rest.UDSServerApi(self._cfg.host, self._cfg.validateCertificate)
|
||||
self._secret = secrets.token_urlsafe(33)
|
||||
self._clientsPool = clients_pool.UDSActorClientPool()
|
||||
self._certificate = cert.defaultCertificate # For being used on "unmanaged" hosts only
|
||||
self._certificate = (
|
||||
cert.defaultCertificate
|
||||
) # For being used on "unmanaged" hosts only, and prior to first login
|
||||
self._http = None
|
||||
|
||||
# Initialzies loglevel and serviceLogger
|
||||
@@ -112,16 +118,24 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
self._http.start()
|
||||
|
||||
def isManaged(self) -> bool:
|
||||
return self._cfg.actorType != types.UNMANAGED # Only "unmanaged" hosts are unmanaged, the rest are "managed"
|
||||
return (
|
||||
self._cfg.actorType != types.UNMANAGED
|
||||
) # Only "unmanaged" hosts are unmanaged, the rest are "managed"
|
||||
|
||||
def serviceInterfaceInfo(self, interfaces: typing.Optional[typing.List[types.InterfaceInfoType]] = None) -> typing.Optional[types.InterfaceInfoType]:
|
||||
def serviceInterfaceInfo(
|
||||
self, interfaces: typing.Optional[typing.List[types.InterfaceInfoType]] = None
|
||||
) -> typing.Optional[types.InterfaceInfoType]:
|
||||
"""
|
||||
returns the inteface with unique_id mac or first interface or None if no interfaces...
|
||||
"""
|
||||
interfaces = interfaces or self._interfaces # Emty interfaces is like "no ip change" because cannot be notified
|
||||
interfaces = (
|
||||
interfaces or self._interfaces
|
||||
) # Emty interfaces is like "no ip change" because cannot be notified
|
||||
if self._cfg.config and interfaces:
|
||||
try:
|
||||
return next(x for x in interfaces if x.mac.lower() == self._cfg.config.unique_id)
|
||||
return next(
|
||||
x for x in interfaces if x.mac.lower() == self._cfg.config.unique_id
|
||||
)
|
||||
except StopIteration:
|
||||
return interfaces[0]
|
||||
|
||||
@@ -152,7 +166,12 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
while self._isAlive:
|
||||
counter -= 1
|
||||
try:
|
||||
self._certificate = self._api.ready(self._cfg.own_token, self._secret, srvInterface.ip, rest.LISTEN_PORT)
|
||||
self._certificate = self._api.ready(
|
||||
self._cfg.own_token,
|
||||
self._secret,
|
||||
srvInterface.ip,
|
||||
rest.LISTEN_PORT,
|
||||
)
|
||||
except rest.RESTConnectionError as e:
|
||||
if not logged: # Only log connection problems ONCE
|
||||
logged = True
|
||||
@@ -168,7 +187,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
# Success or any error that is not recoverable (retunerd by UDS). if Error, service will be cleaned in a while.
|
||||
break
|
||||
else:
|
||||
logger.error('Could not locate IP address!!!. (Not registered with UDS)')
|
||||
logger.error(
|
||||
'Could not locate IP address!!!. (Not registered with UDS)'
|
||||
)
|
||||
|
||||
# Do not continue if not alive...
|
||||
if not self._isAlive:
|
||||
@@ -176,7 +197,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
|
||||
# Cleans sensible data
|
||||
if self._cfg.config:
|
||||
self._cfg = self._cfg._replace(config=self._cfg.config._replace(os=None), data=None)
|
||||
self._cfg = self._cfg._replace(
|
||||
config=self._cfg.config._replace(os=None), data=None
|
||||
)
|
||||
platform.store.writeConfig(self._cfg)
|
||||
|
||||
logger.info('Service ready')
|
||||
@@ -195,10 +218,10 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
self._cfg = self._cfg._replace(runonce_command=None)
|
||||
platform.store.writeConfig(self._cfg)
|
||||
if self.execute(runOnce, "runOnce"):
|
||||
# If runonce is present, will not do anythin more
|
||||
# So we have to ensure that, when runonce command is finished, reboots the machine.
|
||||
# That is, the COMMAND itself has to restart the machine!
|
||||
return False # If the command fails, continue with the rest of the operations...
|
||||
# If runonce is present, will not do anythin more
|
||||
# So we have to ensure that, when runonce command is finished, reboots the machine.
|
||||
# That is, the COMMAND itself has to restart the machine!
|
||||
return False # If the command fails, continue with the rest of the operations...
|
||||
|
||||
# Retry configuration while not stop service, config in case of error 10 times, reboot vm
|
||||
counter = 10
|
||||
@@ -208,9 +231,20 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
if self._cfg.config and self._cfg.config.os:
|
||||
osData = self._cfg.config.os
|
||||
if osData.action == 'rename':
|
||||
self.rename(osData.name, osData.username, osData.password, osData.new_password)
|
||||
self.rename(
|
||||
osData.name,
|
||||
osData.username,
|
||||
osData.password,
|
||||
osData.new_password,
|
||||
)
|
||||
elif osData.action == 'rename_ad':
|
||||
self.joinDomain(osData.name, osData.ad or '', osData.ou or '', osData.username or '', osData.password or '')
|
||||
self.joinDomain(
|
||||
osData.name,
|
||||
osData.ad or '',
|
||||
osData.ou or '',
|
||||
osData.username or '',
|
||||
osData.password or '',
|
||||
)
|
||||
|
||||
if self._rebootRequested:
|
||||
try:
|
||||
@@ -234,7 +268,12 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
self.getInterfaces() # Ensure we have interfaces
|
||||
if self._cfg.master_token:
|
||||
try:
|
||||
self._certificate = self._api.notifyUnmanagedCallback(self._cfg.master_token, self._secret, self._interfaces, rest.LISTEN_PORT)
|
||||
self._certificate = self._api.notifyUnmanagedCallback(
|
||||
self._cfg.master_token,
|
||||
self._secret,
|
||||
self._interfaces,
|
||||
rest.LISTEN_PORT,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error('Couuld not notify unmanaged callback: %s', e)
|
||||
|
||||
@@ -245,13 +284,17 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
return
|
||||
|
||||
while self._isAlive:
|
||||
self._interfaces = list(platform.operations.getNetworkInfo())
|
||||
self._interfaces = tools.validNetworkCards(
|
||||
self._cfg.restrict_net, platform.operations.getNetworkInfo()
|
||||
)
|
||||
if self._interfaces:
|
||||
break
|
||||
self.doWait(5000)
|
||||
|
||||
def initialize(self) -> bool:
|
||||
if self._initialized or not self._cfg.host or not self._isAlive: # Not configured or not running
|
||||
if (
|
||||
self._initialized or not self._cfg.host or not self._isAlive
|
||||
): # Not configured or not running
|
||||
return False
|
||||
|
||||
self._initialized = True
|
||||
@@ -268,9 +311,15 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
try:
|
||||
# If master token is present, initialize and get configuration data
|
||||
if self._cfg.master_token:
|
||||
initResult: types.InitializationResultType = self._api.initialize(self._cfg.master_token, self._interfaces, self._cfg.actorType)
|
||||
initResult: types.InitializationResultType = self._api.initialize(
|
||||
self._cfg.master_token, self._interfaces, self._cfg.actorType
|
||||
)
|
||||
if not initResult.own_token: # Not managed
|
||||
logger.debug('This host is not managed by UDS Broker (ids: {})'.format(self._interfaces))
|
||||
logger.debug(
|
||||
'This host is not managed by UDS Broker (ids: {})'.format(
|
||||
self._interfaces
|
||||
)
|
||||
)
|
||||
return False
|
||||
|
||||
# Only removes master token for managed machines (will need it on next client execution)
|
||||
@@ -279,9 +328,8 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
master_token=master_token,
|
||||
own_token=initResult.own_token,
|
||||
config=types.ActorDataConfigurationType(
|
||||
unique_id=initResult.unique_id,
|
||||
os=initResult.os
|
||||
)
|
||||
unique_id=initResult.unique_id, os=initResult.os
|
||||
),
|
||||
)
|
||||
|
||||
# On first successfull initialization request, master token will dissapear for managed hosts so it will be no more available (not needed anyway)
|
||||
@@ -294,10 +342,16 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
|
||||
break # Initial configuration done..
|
||||
except rest.RESTConnectionError as e:
|
||||
logger.info('Trying to inititialize connection with broker (last error: {})'.format(e))
|
||||
logger.info(
|
||||
'Trying to inititialize connection with broker (last error: {})'.format(
|
||||
e
|
||||
)
|
||||
)
|
||||
self.doWait(5000) # Wait a bit and retry
|
||||
except rest.RESTError as e: # Invalid key?
|
||||
logger.error('Error validating with broker. (Invalid token?): {}'.format(e))
|
||||
except rest.RESTError as e: # Invalid key?
|
||||
logger.error(
|
||||
'Error validating with broker. (Invalid token?): {}'.format(e)
|
||||
)
|
||||
return False
|
||||
except Exception:
|
||||
logger.exception()
|
||||
@@ -307,7 +361,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
|
||||
def uninitialize(self):
|
||||
self._initialized = False
|
||||
self._cfg = self._cfg._replace(own_token=None) # Ensures assigned token is cleared
|
||||
self._cfg = self._cfg._replace(
|
||||
own_token=None
|
||||
) # Ensures assigned token is cleared
|
||||
|
||||
def finish(self) -> None:
|
||||
if self._http:
|
||||
@@ -321,8 +377,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
self._cfg.actorType,
|
||||
self._cfg.own_token,
|
||||
'',
|
||||
'',
|
||||
self._interfaces,
|
||||
self._secret
|
||||
self._secret,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error('Error notifying final logout to UDS: %s', e)
|
||||
@@ -334,19 +391,33 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
return # Unamanaged hosts does not changes ips. (The full initialize-login-logout process is done in a row, so at login the IP is correct)
|
||||
|
||||
try:
|
||||
if not self._cfg.own_token or not self._cfg.config or not self._cfg.config.unique_id:
|
||||
if (
|
||||
not self._cfg.own_token
|
||||
or not self._cfg.config
|
||||
or not self._cfg.config.unique_id
|
||||
):
|
||||
# Not enouth data do check
|
||||
return
|
||||
currentInterfaces = list(platform.operations.getNetworkInfo())
|
||||
currentInterfaces = tools.validNetworkCards(
|
||||
self._cfg.restrict_net, platform.operations.getNetworkInfo()
|
||||
)
|
||||
old = self.serviceInterfaceInfo()
|
||||
new = self.serviceInterfaceInfo(currentInterfaces)
|
||||
if not new or not old:
|
||||
raise Exception('No ip currently available for {}'.format(self._cfg.config.unique_id))
|
||||
raise Exception(
|
||||
'No ip currently available for {}'.format(
|
||||
self._cfg.config.unique_id
|
||||
)
|
||||
)
|
||||
if old.ip != new.ip:
|
||||
self._certificate = self._api.notifyIpChange(self._cfg.own_token, self._secret, new.ip, rest.LISTEN_PORT)
|
||||
self._certificate = self._api.notifyIpChange(
|
||||
self._cfg.own_token, self._secret, new.ip, rest.LISTEN_PORT
|
||||
)
|
||||
# Now store new addresses & interfaces...
|
||||
self._interfaces = currentInterfaces
|
||||
logger.info('Ip changed from {} to {}. Notified to UDS'.format(old.ip, new.ip))
|
||||
logger.info(
|
||||
'Ip changed from {} to {}. Notified to UDS'.format(old.ip, new.ip)
|
||||
)
|
||||
# Stop the running HTTP Thread and start a new one, with new generated cert
|
||||
self.startHttpServer()
|
||||
except Exception as e:
|
||||
@@ -354,29 +425,34 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
logger.warn('Checking ips failed: {}'.format(e))
|
||||
|
||||
def rename(
|
||||
self,
|
||||
name: str,
|
||||
userName: typing.Optional[str] = None,
|
||||
oldPassword: typing.Optional[str] = None,
|
||||
newPassword: typing.Optional[str] = None
|
||||
) -> None:
|
||||
self,
|
||||
name: str,
|
||||
userName: typing.Optional[str] = None,
|
||||
oldPassword: typing.Optional[str] = None,
|
||||
newPassword: typing.Optional[str] = None,
|
||||
) -> None:
|
||||
'''
|
||||
Invoked when broker requests a rename action
|
||||
default does nothing
|
||||
'''
|
||||
hostName = platform.operations.getComputerName()
|
||||
|
||||
if hostName.lower() == name.lower():
|
||||
logger.info('Computer name is already {}'.format(hostName))
|
||||
return
|
||||
|
||||
# Check for password change request for an user
|
||||
if userName and newPassword:
|
||||
logger.info('Setting password for configured user')
|
||||
try:
|
||||
platform.operations.changeUserPassword(userName, oldPassword or '', newPassword)
|
||||
platform.operations.changeUserPassword(
|
||||
userName, oldPassword or '', newPassword
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception('Could not change password for user {} (maybe invalid current password is configured at broker): {} '.format(userName, str(e)))
|
||||
# Logs error, but continue renaming computer
|
||||
logger.error(
|
||||
'Could not change password for user {}: {}'.format(userName, e)
|
||||
)
|
||||
|
||||
if hostName.lower() == name.lower():
|
||||
logger.info('Computer name is already {}'.format(hostName))
|
||||
return
|
||||
|
||||
if platform.operations.renameComputer(name):
|
||||
self.reboot()
|
||||
@@ -389,7 +465,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
|
||||
# Now check if every registered client is already there (if logged in OFC)
|
||||
if self._loggedIn and not self._clientsPool.ping():
|
||||
self.logout('client_unavailable')
|
||||
self.logout('client_unavailable', '')
|
||||
except Exception as e:
|
||||
logger.error('Exception on main service loop: %s', e)
|
||||
|
||||
@@ -397,13 +473,8 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
# Methods that can be overriden by linux & windows Actor
|
||||
# ******************************************************
|
||||
def joinDomain( # pylint: disable=unused-argument, too-many-arguments
|
||||
self,
|
||||
name: str,
|
||||
domain: str,
|
||||
ou: str,
|
||||
account: str,
|
||||
password: str
|
||||
) -> None:
|
||||
self, name: str, domain: str, ou: str, account: str, password: str
|
||||
) -> None:
|
||||
'''
|
||||
Invoked when broker requests a "domain" action
|
||||
default does nothing
|
||||
@@ -411,19 +482,24 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
logger.debug('Base join invoked: {} on {}, {}'.format(name, domain, ou))
|
||||
|
||||
# Client notifications
|
||||
def login(self, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType:
|
||||
result = types.LoginResultInfoType(ip='', hostname='', dead_line=None, max_idle=None)
|
||||
self._loggedIn = True
|
||||
|
||||
def login(
|
||||
self, username: str, sessionType: typing.Optional[str] = None
|
||||
) -> types.LoginResultInfoType:
|
||||
result = types.LoginResultInfoType(
|
||||
ip='', hostname='', dead_line=None, max_idle=None
|
||||
)
|
||||
master_token = None
|
||||
secret = None
|
||||
# If unmanaged, do initialization now, because we don't know before this
|
||||
# Also, even if not initialized, get a "login" notification token
|
||||
if not self.isManaged():
|
||||
self.initialize()
|
||||
self._initialized = (
|
||||
self.initialize()
|
||||
) # Maybe it's a local login by an unmanaged host.... On real login, will execute initilize again
|
||||
# Unamanaged, need the master token
|
||||
master_token = self._cfg.master_token
|
||||
secret = self._secret
|
||||
|
||||
|
||||
# Own token will not be set if UDS did not assigned the initialized VM to an user
|
||||
# In that case, take master token (if machine is Unamanaged version)
|
||||
token = self._cfg.own_token or master_token
|
||||
@@ -434,33 +510,43 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
username,
|
||||
sessionType or '',
|
||||
self._interfaces,
|
||||
secret
|
||||
secret,
|
||||
)
|
||||
|
||||
script = platform.store.invokeScriptOnLogin()
|
||||
if script:
|
||||
script += f'{username} {sessionType or "unknown"} {self._cfg.actorType}'
|
||||
self.execute(script, 'Logon')
|
||||
if result.logged_in:
|
||||
logger.debug('Login successful')
|
||||
self._loggedIn = True
|
||||
script = platform.store.invokeScriptOnLogin()
|
||||
if script:
|
||||
logger.info('Executing script on login: {}'.format(script))
|
||||
script += f'{username} {sessionType or "unknown"} {self._cfg.actorType}'
|
||||
self.execute(script, 'Logon')
|
||||
|
||||
return result
|
||||
|
||||
def logout(self, username: str) -> None:
|
||||
self._loggedIn = False
|
||||
|
||||
master_token = self._cfg.master_token if self.isManaged() else None
|
||||
def logout(self, username: str, sessionType: typing.Optional[str]) -> None:
|
||||
master_token = self._cfg.master_token
|
||||
|
||||
# Own token will not be set if UDS did not assigned the initialized VM to an user
|
||||
# In that case, take master token (if machine is Unamanaged version)
|
||||
token = self._cfg.own_token or master_token
|
||||
if token:
|
||||
self._api.logout(
|
||||
self._cfg.actorType,
|
||||
token,
|
||||
username,
|
||||
self._interfaces,
|
||||
self._secret
|
||||
)
|
||||
# If logout is not processed (that is, not ok result), the logout has not been processed
|
||||
if (
|
||||
self._api.logout(
|
||||
self._cfg.actorType,
|
||||
token,
|
||||
username,
|
||||
sessionType or '',
|
||||
self._interfaces,
|
||||
self._secret,
|
||||
)
|
||||
!= 'ok'
|
||||
):
|
||||
logger.info('Logout from %s ignored as required by uds broker', username)
|
||||
return
|
||||
|
||||
self._loggedIn = False
|
||||
self.onLogout(username)
|
||||
|
||||
if not self.isManaged():
|
||||
@@ -487,13 +573,25 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
'''
|
||||
logger.info('Service stopped')
|
||||
|
||||
def preConnect(self, userName: str, protocol: str, ip: str, hostname: str) -> str: # pylint: disable=unused-argument
|
||||
def preConnect(
|
||||
self, userName: str, protocol: str, ip: str, hostname: str, udsUserName: str
|
||||
) -> str:
|
||||
'''
|
||||
Invoked when received a PRE Connection request via REST
|
||||
Base preconnect executes the preconnect command
|
||||
'''
|
||||
if self._cfg.pre_command:
|
||||
self.execute(self._cfg.pre_command + ' {} {} {} {}'.format(userName.replace('"', '%22'), protocol, ip, hostname), 'preConnect')
|
||||
self.execute(
|
||||
self._cfg.pre_command
|
||||
+ ' {} {} {} {} {}'.format(
|
||||
userName.replace('"', '%22'),
|
||||
protocol,
|
||||
ip,
|
||||
hostname,
|
||||
udsUserName.replace('"', '%22'),
|
||||
),
|
||||
'preConnect',
|
||||
)
|
||||
|
||||
return 'ok'
|
||||
|
||||
|
@@ -28,20 +28,58 @@
|
||||
'''
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
'''
|
||||
# pylint: disable=invalid-name
|
||||
import threading
|
||||
import ipaddress
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from udsactor.types import InterfaceInfoType
|
||||
|
||||
from udsactor.log import logger
|
||||
|
||||
class ScriptExecutorThread(threading.Thread):
|
||||
|
||||
def __init__(self, script: str) -> None:
|
||||
super(ScriptExecutorThread, self).__init__()
|
||||
self.script = script
|
||||
|
||||
def run(self) -> None:
|
||||
from udsactor.log import logger
|
||||
|
||||
try:
|
||||
logger.debug('Executing script: {}'.format(self.script))
|
||||
exec(self.script, globals(), None) # pylint: disable=exec-used
|
||||
except Exception as e:
|
||||
logger.error('Error executing script: {}'.format(e))
|
||||
logger.exception()
|
||||
|
||||
|
||||
# Convert "X.X.X.X/X" to ipaddress.IPv4Network
|
||||
def strToNoIPV4Network(net: typing.Optional[str]) -> typing.Optional[ipaddress.IPv4Network]:
|
||||
if not net: # Empty or None
|
||||
return None
|
||||
try:
|
||||
return ipaddress.IPv4Interface(net).network
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def validNetworkCards(
|
||||
net: typing.Optional[str], cards: typing.Iterable['InterfaceInfoType']
|
||||
) -> typing.List['InterfaceInfoType']:
|
||||
try:
|
||||
subnet = strToNoIPV4Network(net)
|
||||
except Exception as e:
|
||||
subnet = None
|
||||
|
||||
if subnet is None:
|
||||
return list(cards)
|
||||
|
||||
def isValid(ip: str, subnet: ipaddress.IPv4Network) -> bool:
|
||||
if not ip:
|
||||
return False
|
||||
try:
|
||||
return ipaddress.IPv4Address(ip) in subnet
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return [c for c in cards if isValid(c.ip, subnet)]
|
||||
|
@@ -35,6 +35,7 @@ class ActorConfigurationType(typing.NamedTuple):
|
||||
actorType: typing.Optional[str] = None
|
||||
master_token: typing.Optional[str] = None
|
||||
own_token: typing.Optional[str] = None
|
||||
restrict_net: typing.Optional[str] = None
|
||||
|
||||
pre_command: typing.Optional[str] = None
|
||||
runonce_command: typing.Optional[str] = None
|
||||
@@ -57,7 +58,12 @@ class LoginResultInfoType(typing.NamedTuple):
|
||||
dead_line: typing.Optional[int]
|
||||
max_idle: typing.Optional[int] # Not provided by broker
|
||||
|
||||
@property
|
||||
def logged_in(self) -> bool:
|
||||
return self.hostname != '' or self.ip != ''
|
||||
|
||||
class CertificateInfoType(typing.NamedTuple):
|
||||
private_key: str
|
||||
server_certificate: str
|
||||
password: str
|
||||
ciphers: str
|
||||
|
1
actor/src/udsactor/version.py
Normal file
1
actor/src/udsactor/version.py
Normal file
@@ -0,0 +1 @@
|
||||
VERSION = '3.6.0'
|
@@ -34,7 +34,7 @@ import os
|
||||
import tempfile
|
||||
import typing
|
||||
|
||||
import servicemanager # pylint: disable=import-error
|
||||
import servicemanager
|
||||
|
||||
# Valid logging levels, from UDS Broker (uds.core.utils.log).
|
||||
from .. import loglevel
|
||||
@@ -42,6 +42,7 @@ from .. import loglevel
|
||||
class LocalLogger: # pylint: disable=too-few-public-methods
|
||||
linux = False
|
||||
windows = True
|
||||
serviceLogger = False
|
||||
|
||||
logger: typing.Optional[logging.Logger]
|
||||
|
||||
|
@@ -41,6 +41,8 @@ from .service import UDSActorSvc
|
||||
def setupRecoverService():
|
||||
svc_name = UDSActorSvc._svc_name_ # pylint: disable=protected-access
|
||||
|
||||
hs = None
|
||||
hscm = None
|
||||
try:
|
||||
hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS)
|
||||
|
||||
@@ -57,9 +59,11 @@ def setupRecoverService():
|
||||
}
|
||||
win32service.ChangeServiceConfig2(hs, win32service.SERVICE_CONFIG_FAILURE_ACTIONS, service_failure_actions)
|
||||
finally:
|
||||
win32service.CloseServiceHandle(hs)
|
||||
if hs:
|
||||
win32service.CloseServiceHandle(hs)
|
||||
finally:
|
||||
win32service.CloseServiceHandle(hscm)
|
||||
if hscm:
|
||||
win32service.CloseServiceHandle(hscm)
|
||||
|
||||
|
||||
def run() -> None:
|
||||
|
@@ -139,7 +139,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
|
||||
logger.info('Using multiple step join because configuration requests to do so')
|
||||
self.multiStepJoin(name, domain, ou, account, password)
|
||||
|
||||
def preConnect(self, userName: str, protocol: str, ip: str, hostname: str) -> str:
|
||||
def preConnect(self, userName: str, protocol: str, ip: str, hostname: str, udsUserName: str) -> str:
|
||||
logger.debug('Pre connect invoked')
|
||||
|
||||
if protocol == 'rdp': # If connection is not using rdp, skip adding user
|
||||
@@ -168,7 +168,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
|
||||
self._user = None
|
||||
logger.debug('User {} already in group'.format(userName))
|
||||
|
||||
return super().preConnect(userName, protocol, ip, hostname)
|
||||
return super().preConnect(userName, protocol, ip, hostname, udsUserName)
|
||||
|
||||
def ovLogon(self, username: str, password: str) -> str:
|
||||
"""
|
||||
|
@@ -2,9 +2,10 @@
|
||||
|
||||
# Form implementation generated from reading ui file 'setup-dialog.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.13.2
|
||||
# Created by: PyQt5 UI code generator 5.15.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||
# run again. Do not edit this file unless you know what you are doing.
|
||||
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
@@ -191,6 +192,7 @@ class Ui_UdsActorSetupDialog(object):
|
||||
|
||||
self.retranslateUi(UdsActorSetupDialog)
|
||||
self.tabWidget.setCurrentIndex(0)
|
||||
self.validateCertificate.setCurrentIndex(1)
|
||||
self.logLevelComboBox.setCurrentIndex(1)
|
||||
self.closeButton.clicked.connect(UdsActorSetupDialog.finish)
|
||||
self.registerButton.clicked.connect(UdsActorSetupDialog.registerWithUDS)
|
||||
|
@@ -2,9 +2,10 @@
|
||||
|
||||
# Form implementation generated from reading ui file 'setup-dialog-unmanaged.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.13.2
|
||||
# Created by: PyQt5 UI code generator 5.15.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||
# run again. Do not edit this file unless you know what you are doing.
|
||||
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
@@ -14,7 +15,7 @@ class Ui_UdsActorSetupDialog(object):
|
||||
def setupUi(self, UdsActorSetupDialog):
|
||||
UdsActorSetupDialog.setObjectName("UdsActorSetupDialog")
|
||||
UdsActorSetupDialog.setWindowModality(QtCore.Qt.WindowModal)
|
||||
UdsActorSetupDialog.resize(595, 220)
|
||||
UdsActorSetupDialog.resize(601, 243)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
@@ -34,12 +35,12 @@ class Ui_UdsActorSetupDialog(object):
|
||||
UdsActorSetupDialog.setModal(True)
|
||||
self.saveButton = QtWidgets.QPushButton(UdsActorSetupDialog)
|
||||
self.saveButton.setEnabled(True)
|
||||
self.saveButton.setGeometry(QtCore.QRect(10, 180, 181, 23))
|
||||
self.saveButton.setGeometry(QtCore.QRect(10, 210, 181, 23))
|
||||
self.saveButton.setMinimumSize(QtCore.QSize(181, 0))
|
||||
self.saveButton.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
|
||||
self.saveButton.setObjectName("saveButton")
|
||||
self.closeButton = QtWidgets.QPushButton(UdsActorSetupDialog)
|
||||
self.closeButton.setGeometry(QtCore.QRect(410, 180, 171, 23))
|
||||
self.closeButton.setGeometry(QtCore.QRect(410, 210, 171, 23))
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
@@ -49,11 +50,11 @@ class Ui_UdsActorSetupDialog(object):
|
||||
self.closeButton.setObjectName("closeButton")
|
||||
self.testButton = QtWidgets.QPushButton(UdsActorSetupDialog)
|
||||
self.testButton.setEnabled(False)
|
||||
self.testButton.setGeometry(QtCore.QRect(210, 180, 181, 23))
|
||||
self.testButton.setGeometry(QtCore.QRect(210, 210, 181, 23))
|
||||
self.testButton.setMinimumSize(QtCore.QSize(181, 0))
|
||||
self.testButton.setObjectName("testButton")
|
||||
self.layoutWidget = QtWidgets.QWidget(UdsActorSetupDialog)
|
||||
self.layoutWidget.setGeometry(QtCore.QRect(10, 10, 571, 161))
|
||||
self.layoutWidget.setGeometry(QtCore.QRect(10, 10, 571, 191))
|
||||
self.layoutWidget.setObjectName("layoutWidget")
|
||||
self.formLayout = QtWidgets.QFormLayout(self.layoutWidget)
|
||||
self.formLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
|
||||
@@ -84,7 +85,7 @@ class Ui_UdsActorSetupDialog(object):
|
||||
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.serviceToken)
|
||||
self.label_loglevel = QtWidgets.QLabel(self.layoutWidget)
|
||||
self.label_loglevel.setObjectName("label_loglevel")
|
||||
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_loglevel)
|
||||
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.label_loglevel)
|
||||
self.logLevelComboBox = QtWidgets.QComboBox(self.layoutWidget)
|
||||
self.logLevelComboBox.setFrame(True)
|
||||
self.logLevelComboBox.setObjectName("logLevelComboBox")
|
||||
@@ -96,7 +97,13 @@ class Ui_UdsActorSetupDialog(object):
|
||||
self.logLevelComboBox.setItemText(2, "ERROR")
|
||||
self.logLevelComboBox.addItem("")
|
||||
self.logLevelComboBox.setItemText(3, "FATAL")
|
||||
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.logLevelComboBox)
|
||||
self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.logLevelComboBox)
|
||||
self.label_restrictNet = QtWidgets.QLabel(self.layoutWidget)
|
||||
self.label_restrictNet.setObjectName("label_restrictNet")
|
||||
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_restrictNet)
|
||||
self.restrictNet = QtWidgets.QLineEdit(self.layoutWidget)
|
||||
self.restrictNet.setObjectName("restrictNet")
|
||||
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.restrictNet)
|
||||
self.label_host.raise_()
|
||||
self.host.raise_()
|
||||
self.label_serviceToken.raise_()
|
||||
@@ -105,6 +112,8 @@ class Ui_UdsActorSetupDialog(object):
|
||||
self.label_security.raise_()
|
||||
self.label_loglevel.raise_()
|
||||
self.logLevelComboBox.raise_()
|
||||
self.label_restrictNet.raise_()
|
||||
self.restrictNet.raise_()
|
||||
|
||||
self.retranslateUi(UdsActorSetupDialog)
|
||||
self.logLevelComboBox.setCurrentIndex(1)
|
||||
@@ -113,6 +122,7 @@ class Ui_UdsActorSetupDialog(object):
|
||||
self.saveButton.clicked.connect(UdsActorSetupDialog.saveConfig)
|
||||
self.host.textChanged['QString'].connect(UdsActorSetupDialog.configChanged)
|
||||
self.serviceToken.textChanged['QString'].connect(UdsActorSetupDialog.configChanged)
|
||||
self.restrictNet.textChanged['QString'].connect(UdsActorSetupDialog.configChanged)
|
||||
QtCore.QMetaObject.connectSlotsByName(UdsActorSetupDialog)
|
||||
|
||||
def retranslateUi(self, UdsActorSetupDialog):
|
||||
@@ -136,7 +146,10 @@ class Ui_UdsActorSetupDialog(object):
|
||||
self.host.setToolTip(_translate("UdsActorSetupDialog", "Uds Broker Server Addres. Use IP or FQDN"))
|
||||
self.host.setWhatsThis(_translate("UdsActorSetupDialog", "Enter here the UDS Broker Addres using either its IP address or its FQDN address"))
|
||||
self.label_serviceToken.setText(_translate("UdsActorSetupDialog", "Service Token"))
|
||||
self.serviceToken.setToolTip(_translate("UdsActorSetupDialog", "UDS user with administration rights (Will not be stored on template)"))
|
||||
self.serviceToken.setWhatsThis(_translate("UdsActorSetupDialog", "<html><head/><body><p>Administrator user on UDS Server.</p><p>Note: This credential will not be stored on client. Will be used to obtain an unique token for this image.</p></body></html>"))
|
||||
self.serviceToken.setToolTip(_translate("UdsActorSetupDialog", "UDS Service Token"))
|
||||
self.serviceToken.setWhatsThis(_translate("UdsActorSetupDialog", "<html><head/><body><p>Token of the service on UDS platform</p><p>This token can be obtainend from the service configuration on UDS.</p></body></html>"))
|
||||
self.label_loglevel.setText(_translate("UdsActorSetupDialog", "Log Level"))
|
||||
self.label_restrictNet.setText(_translate("UdsActorSetupDialog", "Restrict Net"))
|
||||
self.restrictNet.setToolTip(_translate("UdsActorSetupDialog", "Restrict valid detection of network interfaces to this network."))
|
||||
self.restrictNet.setWhatsThis(_translate("UdsActorSetupDialog", "<html><head/><body><p>Restrics valid detection of network interfaces.</p><p>Note: Use this field only in case of several network interfaces, so UDS knows which one is the interface where the user will be connected..</p></body></html>"))
|
||||
from ui import uds_rc
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
# Resource object code
|
||||
#
|
||||
# Created by: The Resource Compiler for PyQt5 (Qt v5.13.2)
|
||||
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -175,7 +175,7 @@ qt_resource_struct_v2 = b"\
|
||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x0c\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
||||
\x00\x00\x01\x6e\x86\x31\xef\xa3\
|
||||
\x00\x00\x01\x81\xce\x8a\xac\xf2\
|
||||
"
|
||||
|
||||
qt_version = [int(v) for v in QtCore.qVersion().split('.')]
|
||||
|
1
client-py3/full/linux/.gitignore
vendored
1
client-py3/full/linux/.gitignore
vendored
@@ -6,3 +6,4 @@
|
||||
/UDSClient*.AppImage
|
||||
/appimage*
|
||||
/UDSClient.desktop
|
||||
*.zsync
|
@@ -14,6 +14,8 @@ APPSDIR := $(DESTDIR)/usr/share/applications
|
||||
PYC := $(shell find $(SOURCEDIR) -name '*.py[co]')
|
||||
CACHES := $(shell find $(SOURCEDIR) -name '__pycache__')
|
||||
|
||||
|
||||
|
||||
clean:
|
||||
rm -rf $(PYC) $(CACHES) $(DESTDIR)
|
||||
install:
|
||||
@@ -55,16 +57,16 @@ build-appimage:
|
||||
ifeq ($(DISTRO),x86_64)
|
||||
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g > appimage.recipe
|
||||
endif
|
||||
ifeq ($(DISTRO),armf)
|
||||
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g | sed -e s/amd64\\\|x86_64/armhf/g > appimage.recipe
|
||||
ifeq ($(DISTRO),armhf)
|
||||
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g | sed -e s/amd64/armhf/g | sed -e s/x86_64/armhf/g > appimage.recipe
|
||||
endif
|
||||
ifeq ($(DISTRO),i686)
|
||||
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g | sed -e s/amd64/i386/g | sed -e s/x86_64/i686/g > appimage.recipe
|
||||
endif
|
||||
# Ensure all working folders are "clean"
|
||||
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir
|
||||
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir appimage-build AppDir
|
||||
|
||||
appimage-builder --recipe appimage.recipe
|
||||
appimage-builder --recipe appimage.recipe --appdir /tmp/UDSClientDir
|
||||
# Now create dist and move appimage
|
||||
rm -rf $(DESTDIR)
|
||||
mkdir -p $(DESTDIR)
|
||||
@@ -77,4 +79,29 @@ endif
|
||||
tar czvf ../udsclient3-$(DISTRO)-$(VERSION).tar.gz -C $(DESTDIR) .
|
||||
|
||||
# cleanup
|
||||
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir
|
||||
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir appimage-build AppDir
|
||||
|
||||
build-igel:
|
||||
rm -rf $(DESTDIR)
|
||||
mkdir -p $(DESTDIR)
|
||||
# Calculate the size of the custom partition (15 megas more than the appimage size)
|
||||
@$(eval APPIMAGE_SIZE=$(shell du -sm UDSClient-$(VERSION)-x86_64.AppImage | cut -f1))
|
||||
@$(eval APPIMAGE_SIZE=$(shell expr $(APPIMAGE_SIZE) + 15))
|
||||
cat igel/UDSClient-Profile-template.xml | sed -e s/"_SIZE_"/"$(APPIMAGE_SIZE)M"/g > $(DESTDIR)/UDSClient-Profile.xml
|
||||
cat igel/UDSClient-template.inf | sed -e s/"_SIZE_"/"$(APPIMAGE_SIZE)M"/g > $(DESTDIR)/UDSClient.inf
|
||||
cp UDSClient-$(VERSION)-x86_64.AppImage $(DESTDIR)/UDSClient
|
||||
cp igel/UDSClient.desktop $(DESTDIR)/UDSClient.desktop
|
||||
cp igel/init.sh $(DESTDIR)/init.sh
|
||||
tar cjvf $(DESTDIR)/UDSClient.tar.bz2 -C $(DESTDIR) UDSClient UDSClient.desktop init.sh
|
||||
zip -j ../udsclient3-$(VERSION)-igel.zip $(DESTDIR)/UDSClient-Profile.xml $(DESTDIR)/UDSClient.inf $(DESTDIR)/UDSClient.tar.bz2
|
||||
cd ..
|
||||
rm -rf $(DESTDIR)
|
||||
|
||||
build-thinpro:
|
||||
rm -rf $(DESTDIR)
|
||||
mkdir -p $(DESTDIR)
|
||||
cp -r thinpro/* $(DESTDIR)
|
||||
cp UDSClient-$(VERSION)-x86_64.AppImage $(DESTDIR)/UDSClient
|
||||
tar czvf ../udsclient3-$(VERSION)-thinpro.tar.gz -C $(DESTDIR) .
|
||||
rm -rf $(DESTDIR)
|
||||
|
||||
|
@@ -12,9 +12,6 @@ cat udsclient-template.spec |
|
||||
sed -e s/"version 0.0.0"/"version ${VERSION}"/g |
|
||||
sed -e s/"release 1"/"release ${RELEASE}"/g > udsclient-$VERSION.spec
|
||||
|
||||
cat appimage-udsclient.recipe |
|
||||
sed -e s/"version: 0.0.0"/"version: ${VERSION}"/g > appimage.recipe
|
||||
|
||||
# Now fix dependencies for opensuse
|
||||
# Note: Right now, opensuse & rh seems to have same dependencies, only 1 package needed
|
||||
# cat udsclient-template.spec |
|
||||
@@ -43,5 +40,11 @@ make DESTDIR=appimage DISTRO=x86_64 VERSION=${VERSION} build-appimage
|
||||
make DESTDIR=appimage DISTRO=armhf VERSION=${VERSION} build-appimage
|
||||
make DESTDIR=appimage DISTRO=i686 VERSION=${VERSION} build-appimage
|
||||
|
||||
# Now create igel version
|
||||
# we need first to create the Appimage for x86_64
|
||||
make DESTDIR=igelimage DISTRO=x86_64 VERSION=${VERSION} build-igel
|
||||
|
||||
# Create the thinpro version
|
||||
make DESTDIR=thinproimage DISTRO=x86_64 VERSION=${VERSION} build-thinpro
|
||||
|
||||
rpm --addsign ../*rpm
|
||||
|
@@ -1,3 +1,9 @@
|
||||
udsclient3 (3.6.0) stable; urgency=medium
|
||||
|
||||
* Upgraded to 3.6.0 release
|
||||
|
||||
-- Adolfo Gómez García <agomez@virtualcable.es> Fri, 1 Jul 2022 14:12:10 +0200
|
||||
|
||||
udsclient3 (3.5.0) stable; urgency=medium
|
||||
|
||||
* Upgraded to 3.5.0 release
|
||||
|
@@ -1 +1 @@
|
||||
9
|
||||
10
|
||||
|
@@ -1,10 +1,10 @@
|
||||
Source: udsclient3
|
||||
Section: admin
|
||||
Priority: optional
|
||||
Maintainer: Adolfo Gómez García <agomez@virtualcable.es>
|
||||
Maintainer: Adolfo Gómez García <agomez@virtualcable.net>
|
||||
Build-Depends: debhelper (>= 7), po-debconf
|
||||
Standards-Version: 3.9.2
|
||||
Homepage: http://www.virtualcable.es
|
||||
Homepage: http://www.udsenterprise.com
|
||||
|
||||
Package: udsclient3
|
||||
Section: admin
|
||||
|
@@ -1,26 +1,38 @@
|
||||
Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135
|
||||
Name: udsclient3
|
||||
Maintainer: Adolfo Gómez García
|
||||
Source: http://www.udsenterprise.com/
|
||||
Source: http://github.com/dkmstr/openuds/client-py3
|
||||
|
||||
Copyright: 2014 Virtual Cable S.L.U.
|
||||
Files: *
|
||||
Copyright: (c) 2014-2022, Virtual Cable S.L.U.
|
||||
License: BSD-3-clause
|
||||
|
||||
License: GPL-2+
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License along
|
||||
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
.
|
||||
On Debian systems, the full text of the GNU General Public
|
||||
License version 2 can be found in the file
|
||||
`/usr/share/common-licenses/GPL-2'.
|
||||
License: BSD-3-clause
|
||||
All rights reserved.
|
||||
.
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
.
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
.
|
||||
* Neither the name of pg_query nor the names of its contributors may be used
|
||||
to endorse or promote products derived from this software without specific
|
||||
prior written permission.
|
||||
.
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
@@ -1,2 +1,2 @@
|
||||
udsclient3_3.5.0_all.deb admin optional
|
||||
udsclient3_3.5.0_amd64.buildinfo admin optional
|
||||
udsclient3_3.6.0_all.deb admin optional
|
||||
udsclient3_3.6.0_amd64.buildinfo admin optional
|
||||
|
69
client-py3/full/linux/igel/UDSClient-Profile-template.xml
Executable file
69
client-py3/full/linux/igel/UDSClient-Profile-template.xml
Executable file
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<profile>
|
||||
<profile_id>1126</profile_id>
|
||||
<profilename>UDSClient</profilename>
|
||||
<firmware>
|
||||
<model>IGEL OS 11</model>
|
||||
<version>11.05.120.01</version>
|
||||
</firmware>
|
||||
<description></description>
|
||||
<overwritesessions>false</overwritesessions>
|
||||
<is_master_profile>false</is_master_profile>
|
||||
<is_igel_os>true</is_igel_os>
|
||||
<settings>
|
||||
<pclass name="custom_partition.enabled">
|
||||
<pvalue instancenr="-1" variableExpression="" variableSubstitutionActive="false">true</pvalue>
|
||||
<variableSubstitutionActive>false</variableSubstitutionActive>
|
||||
</pclass>
|
||||
<pclass name="system.security.apparmor">
|
||||
<pvalue instancenr="-1" variableExpression="" variableSubstitutionActive="false">false</pvalue>
|
||||
<variableSubstitutionActive>false</variableSubstitutionActive>
|
||||
</pclass>
|
||||
<pclass name="custom_partition.mountpoint">
|
||||
<pvalue instancenr="-1" variableExpression="" variableSubstitutionActive="false">/UDSClient</pvalue>
|
||||
<variableSubstitutionActive>false</variableSubstitutionActive>
|
||||
</pclass>
|
||||
<pclass name="custom_partition.size">
|
||||
<pvalue instancenr="-1" variableExpression="" variableSubstitutionActive="false">_SIZE_</pvalue>
|
||||
<variableSubstitutionActive>false</variableSubstitutionActive>
|
||||
</pclass>
|
||||
</settings>
|
||||
<instancesettings>
|
||||
<instance classprefix="custom_partition.source%" serialnumber="-719cadfe:17ca470644a:-7fff127.0.1.1">
|
||||
<ivalue classname="custom_partition.source%.autoupdate" variableExpression="" variableSubstitutionActive="false">false</ivalue>
|
||||
<ivalue classname="custom_partition.source%.crypt_password" variableExpression="" variableSubstitutionActive="false">000d4317311f2c0031133c4d3e4c3d</ivalue>
|
||||
<ivalue classname="custom_partition.source%.final_action" variableExpression="" variableSubstitutionActive="false"></ivalue>
|
||||
<ivalue classname="custom_partition.source%.init_action" variableExpression="" variableSubstitutionActive="false">/UDSClient/init.sh</ivalue>
|
||||
<ivalue classname="custom_partition.source%.password" variableExpression="" variableSubstitutionActive="false"></ivalue>
|
||||
<ivalue classname="custom_partition.source%.url" variableExpression="" variableSubstitutionActive="false">https://[UMS_SERVER]:8443/ums_filetransfer/UDSClient-igel.inf</ivalue>
|
||||
<ivalue classname="custom_partition.source%.username" variableExpression="" variableSubstitutionActive="false">[UMS_USERNAME]</ivalue>
|
||||
</instance>
|
||||
<instance classprefix="sessions.chromium%" serialnumber="-6b5264e9:17ca6f65505:-8000127.0.1.1">
|
||||
<ivalue classname="sessions.chromium%.app.browser_startup_page" variableExpression="" variableSubstitutionActive="false">1</ivalue>
|
||||
<ivalue classname="sessions.chromium%.app.homepage" variableExpression="" variableSubstitutionActive="false">https://demo.udsenterprise.com</ivalue>
|
||||
<ivalue classname="sessions.chromium%.applaunch" variableExpression="" variableSubstitutionActive="false">true</ivalue>
|
||||
<ivalue classname="sessions.chromium%.applaunch_path" variableExpression="" variableSubstitutionActive="false"></ivalue>
|
||||
<ivalue classname="sessions.chromium%.applaunch_system" variableExpression="" variableSubstitutionActive="false">false</ivalue>
|
||||
<ivalue classname="sessions.chromium%.autostart" variableExpression="" variableSubstitutionActive="false">false</ivalue>
|
||||
<ivalue classname="sessions.chromium%.autostartnotify" variableExpression="" variableSubstitutionActive="false">false</ivalue>
|
||||
<ivalue classname="sessions.chromium%.desktop" variableExpression="" variableSubstitutionActive="false">true</ivalue>
|
||||
<ivalue classname="sessions.chromium%.desktop_path" variableExpression="" variableSubstitutionActive="false"></ivalue>
|
||||
<ivalue classname="sessions.chromium%.hotkey" variableExpression="" variableSubstitutionActive="false"></ivalue>
|
||||
<ivalue classname="sessions.chromium%.hotkeymodifier" variableExpression="" variableSubstitutionActive="false">None</ivalue>
|
||||
<ivalue classname="sessions.chromium%.icon" variableExpression="" variableSubstitutionActive="false">chromium</ivalue>
|
||||
<ivalue classname="sessions.chromium%.menu_path" variableExpression="" variableSubstitutionActive="false"></ivalue>
|
||||
<ivalue classname="sessions.chromium%.name" variableExpression="UDS" variableSubstitutionActive="true">###LOC_DEFAULT###</ivalue>
|
||||
<ivalue classname="sessions.chromium%.position" variableExpression="" variableSubstitutionActive="false">0</ivalue>
|
||||
<ivalue classname="sessions.chromium%.pulldown" variableExpression="" variableSubstitutionActive="false">false</ivalue>
|
||||
<ivalue classname="sessions.chromium%.pwprotected" variableExpression="" variableSubstitutionActive="false">none</ivalue>
|
||||
<ivalue classname="sessions.chromium%.quick_start" variableExpression="" variableSubstitutionActive="false">false</ivalue>
|
||||
<ivalue classname="sessions.chromium%.scardautostart" variableExpression="" variableSubstitutionActive="false">false</ivalue>
|
||||
<ivalue classname="sessions.chromium%.snotify" variableExpression="" variableSubstitutionActive="false">true</ivalue>
|
||||
<ivalue classname="sessions.chromium%.startmenu" variableExpression="" variableSubstitutionActive="false">true</ivalue>
|
||||
<ivalue classname="sessions.chromium%.startmenu_system" variableExpression="" variableSubstitutionActive="false">false</ivalue>
|
||||
<ivalue classname="sessions.chromium%.usehotkey" variableExpression="" variableSubstitutionActive="false">false</ivalue>
|
||||
<ivalue classname="sessions.chromium%.waittime2autostart" variableExpression="" variableSubstitutionActive="false">0</ivalue>
|
||||
<ivalue classname="sessions.chromium%.waittime2restart" variableExpression="" variableSubstitutionActive="false">0</ivalue>
|
||||
</instance>
|
||||
</instancesettings>
|
||||
</profile>
|
7
client-py3/full/linux/igel/UDSClient-template.inf
Executable file
7
client-py3/full/linux/igel/UDSClient-template.inf
Executable file
@@ -0,0 +1,7 @@
|
||||
[INFO]
|
||||
[PART]
|
||||
file="UDSClient.tar.bz2"
|
||||
version="1.1_igel1"
|
||||
size="_SIZE_"
|
||||
name="UDSClient"
|
||||
minfw="11.05.120"
|
11
client-py3/full/linux/igel/UDSClient.desktop
Executable file
11
client-py3/full/linux/igel/UDSClient.desktop
Executable file
@@ -0,0 +1,11 @@
|
||||
[Desktop Entry]
|
||||
Name=UDSClient
|
||||
Comment=UDS Helper
|
||||
Keywords=uds;client;vdi;
|
||||
Exec=/UDSClient/UDSClient %u
|
||||
Icon=help-browser
|
||||
StartupNotify=true
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;
|
||||
MimeType=x-scheme-handler/uds;x-scheme-handler/udss;
|
4
client-py3/full/linux/igel/init.sh
Executable file
4
client-py3/full/linux/igel/init.sh
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
cp /UDSClient/UDSClient.desktop /usr/share/applications.mime
|
||||
chmod 755 /UDSClient/UDSClient
|
||||
|
@@ -9,6 +9,7 @@ fi
|
||||
echo "Installing UDSClient Portable..."
|
||||
|
||||
cp UDSClient-0.0.0-x86_64.AppImage /usr/bin
|
||||
chmod 755 /usr/bin/UDSClient-0.0.0-x86_64.AppImage
|
||||
cp UDSClient.desktop /usr/share/applications
|
||||
update-desktop-database
|
||||
|
||||
|
@@ -8,6 +8,8 @@ echo "Installation process done."
|
||||
echo "Remember that the following packages must be installed on system:"
|
||||
echo "* Python3 paramiko"
|
||||
echo "* Python3 PyQt5"
|
||||
echo "* Python3 six"
|
||||
echo "* Python3 requests"
|
||||
echo "* Python3 cryptography"
|
||||
echo "Theese packages (as their names), are dependent on your platform, so you must locate and install them"
|
||||
echo "Also, ensure that a /media folder exists on your machine, that will be redirected on RDP connections"
|
||||
|
@@ -1,3 +1,5 @@
|
||||
UDSClient is the client connector needed to get acccess to services managed by UDS Broker.
|
||||
|
||||
For raspberry Pi, AppImage does not works with 1.1.0 (works with 1.0.3)
|
||||
|
||||
Please, visit http://www.udsenterprise.com for more information
|
||||
|
4
client-py3/full/linux/thinpro/firefox/45-uds
Normal file
4
client-py3/full/linux/thinpro/firefox/45-uds
Normal file
@@ -0,0 +1,4 @@
|
||||
# UDS handlers.json
|
||||
cp "/lib/UDSClient/firefox/handlers.json" "$FIREFOX_PROFILE_HANDLERS"
|
||||
ffset "network.protocol-handler.external.uds" "true"
|
||||
ffset "network.protocol-handler.external.udss" "true"
|
98
client-py3/full/linux/thinpro/firefox/handlers.json
Normal file
98
client-py3/full/linux/thinpro/firefox/handlers.json
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"defaultHandlersVersion": {
|
||||
"en-US": 4
|
||||
},
|
||||
"mimeTypes": {
|
||||
"application/pdf": {
|
||||
"action": 3,
|
||||
"extensions": [
|
||||
"pdf"
|
||||
]
|
||||
},
|
||||
"application/x-ica": {
|
||||
"action": 2,
|
||||
"extensions": [
|
||||
"ica"
|
||||
],
|
||||
"handlers": [
|
||||
{
|
||||
"name": "wfica",
|
||||
"path": "/usr/share/hptc-firefox-mgr/handlers/citrix"
|
||||
}
|
||||
]
|
||||
},
|
||||
"application/x-rdp": {
|
||||
"action": 2,
|
||||
"extensions": [
|
||||
"rdp"
|
||||
],
|
||||
"handlers": [
|
||||
{
|
||||
"name": "HP xfreerdp",
|
||||
"path": "/usr/share/hptc-firefox-mgr/handlers/rdp"
|
||||
}
|
||||
]
|
||||
},
|
||||
"text/lic": {
|
||||
"action": 2,
|
||||
"extensions": [
|
||||
"lic"
|
||||
],
|
||||
"handlers": [
|
||||
{
|
||||
"name": "Copy license to ThinPro",
|
||||
"path": "/usr/share/hptc-firefox-mgr/handlers/copy_lic"
|
||||
}
|
||||
]
|
||||
},
|
||||
"text/xml": {
|
||||
"action": 3,
|
||||
"extensions": [
|
||||
"xml"
|
||||
]
|
||||
},
|
||||
"image/svg+xml": {
|
||||
"action": 3,
|
||||
"extensions": [
|
||||
"svg"
|
||||
]
|
||||
},
|
||||
"image/webp": {
|
||||
"action": 3,
|
||||
"extensions": [
|
||||
"webp"
|
||||
]
|
||||
}
|
||||
},
|
||||
"schemes": {
|
||||
"vmware-view": {
|
||||
"action": 2,
|
||||
"handlers": [
|
||||
{
|
||||
"name": "VMWare Horizon View",
|
||||
"path": "/usr/share/hptc-firefox-mgr/handlers/vmware"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uds": {
|
||||
"action": 2,
|
||||
"handlers": [
|
||||
{
|
||||
"name": "UDS Client for ThinPro (SSL)",
|
||||
"path": "/usr/share/hptc-firefox-mgr/handlers/uds"
|
||||
}
|
||||
]
|
||||
},
|
||||
"udss": {
|
||||
"action": 2,
|
||||
"handlers": [
|
||||
{
|
||||
"name": "UDS Client for ThinPro",
|
||||
"path": "/usr/share/hptc-firefox-mgr/handlers/uds"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
5
client-py3/full/linux/thinpro/firefox/uds
Normal file
5
client-py3/full/linux/thinpro/firefox/uds
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
export LD_PRELOAD=""
|
||||
/bin/udsclient $*
|
||||
exit 0
|
2
client-py3/full/linux/thinpro/firefox7.1/45-uds
Normal file
2
client-py3/full/linux/thinpro/firefox7.1/45-uds
Normal file
@@ -0,0 +1,2 @@
|
||||
# UDS handlers.json
|
||||
restore "/lib/UDSClient/firefox/handlers.json" "$FIREFOX_PROFILE_HANDLERS"
|
50
client-py3/full/linux/thinpro/firefox7.1/handlers.json
Normal file
50
client-py3/full/linux/thinpro/firefox7.1/handlers.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"defaultHandlersVersion":{
|
||||
"en-US":4
|
||||
},
|
||||
"mimeTypes":{
|
||||
"application/pdf":{
|
||||
"action":3,
|
||||
"extensions":["pdf"]
|
||||
},
|
||||
"application/x-ica":{
|
||||
"action":2,
|
||||
"handlers":[{
|
||||
"name":"wfica",
|
||||
"path":"/usr/bin/hptc-firefox-run-wfica.sh"
|
||||
}],
|
||||
"extensions":["ica"]
|
||||
},
|
||||
"application/x-rdp":{
|
||||
"action":2,
|
||||
"handlers":[{
|
||||
"name":"HP xfreerdp",
|
||||
"path":"/usr/bin/hptc-run-rdp-file-freerdp.sh"
|
||||
}],
|
||||
"extensions":["rdp"]
|
||||
}
|
||||
},
|
||||
"schemes":{
|
||||
"vmware-view":{
|
||||
"action":2,
|
||||
"handlers":[{
|
||||
"name":"VMWare Horizon View",
|
||||
"path":"/usr/bin/vmware-view"
|
||||
}]
|
||||
},
|
||||
"udss":{
|
||||
"action":2,
|
||||
"handlers":[{
|
||||
"name":"UDS Client",
|
||||
"path":"/bin/udsclient"
|
||||
}]
|
||||
},
|
||||
"uds":{
|
||||
"action":2,
|
||||
"handlers":[{
|
||||
"name":"UDS Client",
|
||||
"path":"/bin/udsclient"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
37
client-py3/full/linux/thinpro/firefox7.1/syspref.js
Normal file
37
client-py3/full/linux/thinpro/firefox7.1/syspref.js
Normal file
@@ -0,0 +1,37 @@
|
||||
// This file can be used to configure global preferences for Firefox
|
||||
// Example: Homepage
|
||||
//pref("browser.startup.homepage", "http://www.weebls-stuff.com/wab/");
|
||||
pref("plugin.default.state", 2);
|
||||
pref("xpinstall.signatures.required", false, locked);
|
||||
pref("extensions.autoDisableScopes", 0, locked);
|
||||
pref("extensions.pocket.enabled", false, locked);
|
||||
pref("extensions.screenshots.disabled", true, locked);
|
||||
pref("datareporting.policy.dataSubmissionEnabled", false, locked);
|
||||
pref("datareporting.policy.dataSubmissionEnabled.v2", false, locked);
|
||||
|
||||
pref("app.update.auto", false, locked);
|
||||
pref("app.update.enabled", false, locked);
|
||||
pref("browser.download.manager.closeWhenDone", true, locked);
|
||||
pref("browser.helperApps.neverAsk.openFile", "application/x-rdp, application/x-java-jnlp-file", locked);
|
||||
pref("browser.EULA.3.accepted", true, locked);
|
||||
pref("browser.rights.3.shown", true, locked);
|
||||
pref("browser.safebrowsing.enabled", false, locked);
|
||||
pref("browser.search.update", false, locked);
|
||||
pref("browser.sessionstore.enabled", false, locked);
|
||||
pref("browser.sessionhistory.cache_subframes", false, locked);
|
||||
pref("datareporting.healthreport.service.enabled", false, locked);
|
||||
pref("datareporting.healthreport.uploadEnabled", false, locked);
|
||||
pref("devtools.toolbox.host", "none", locked);
|
||||
pref("extensions.autoDisableScopes", 14, locked);
|
||||
pref("extensions.blocklist.enabled", false, locked);
|
||||
pref("extensions.update.enabled", false, locked);
|
||||
pref("intl.charsetmenu.browser.cache", "UTF-8", locked);
|
||||
|
||||
pref("network.protocol-handler.external.mailto", false, locked);
|
||||
pref("network.protocol-handler.external.news", false, locked);
|
||||
pref("network.protocol-handler.external.snews", false, locked);
|
||||
pref("network.protocol-handler.external.nntp", false, locked);
|
||||
pref("network.protocol-handler.external-default", false, locked);
|
||||
pref("network.protocol-handler.external.vmware-view", true, locked);
|
||||
pref("network.protocol-handler.external.uds", true, locked);
|
||||
pref("network.protocol-handler.external.udss", true, locked);
|
38
client-py3/full/linux/thinpro/install-uds-client.sh
Executable file
38
client-py3/full/linux/thinpro/install-uds-client.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Common part
|
||||
|
||||
# unlocks so we can write on TC
|
||||
fsunlock
|
||||
|
||||
cp UDSClient /bin/udsclient
|
||||
chmod 755 /bin/udsclient
|
||||
# RDP Script for UDSClient. Launchs udsclient using the "Template_UDS" profile
|
||||
cp udsrdp /usr/bin
|
||||
|
||||
INSTALLED=0
|
||||
# Installation for 7.1.x version
|
||||
grep -q "7.1" /etc/issue
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Installing for thinpro version 7.1"
|
||||
# Allow UDS apps without asking
|
||||
cp firefox7.1/syspref.js /etc/firefox
|
||||
# Copy handlers.json for firefox
|
||||
mkdir -p /lib/UDSClient/firefox/ > /dev/null 2>&1
|
||||
cp firefox7.1/handlers.json /lib/UDSClient/firefox/
|
||||
# and runner
|
||||
cp firefox7.1/45-uds /etc/hptc-firefox-mgr/prestart
|
||||
else
|
||||
echo "Installing for thinpro version 7.2 or later"
|
||||
# Copy handlers for firefox
|
||||
mkdir -p /lib/UDSClient/firefox/ > /dev/null 2>&1
|
||||
# Copy handlers.json for firefox
|
||||
cp firefox/handlers.json /lib/UDSClient/firefox/
|
||||
cp firefox/45-uds /etc/hptc-firefox-mgr/prestart
|
||||
# copy uds handler for firefox
|
||||
cp firefox/uds /usr/share/hptc-firefox-mgr/handlers/uds
|
||||
chmod 755 /usr/share/hptc-firefox-mgr/handlers/uds
|
||||
fi
|
||||
|
||||
# Common part
|
||||
fslock
|
390
client-py3/full/linux/thinpro/udsrdp
Executable file
390
client-py3/full/linux/thinpro/udsrdp
Executable file
@@ -0,0 +1,390 @@
|
||||
#!/bin/bash
|
||||
|
||||
function clearParams {
|
||||
mclient set $REGKEY/address ""
|
||||
mclient set $REGKEY/username ""
|
||||
mclient set $REGKEY/password ""
|
||||
mclient set $REGKEY/domain ""
|
||||
|
||||
mclient set $REGKEY/authorizations/user/execution 0
|
||||
|
||||
mclient commit
|
||||
}
|
||||
|
||||
function getRegKey {
|
||||
# Get Template_UDS
|
||||
for key in `mclient get root/ConnectionType/freerdp/connections | sed "s/dir //g"`; do
|
||||
val=`mclient get $key/label | sed "s/value //g"`
|
||||
if [ "$val" == "Template_UDS" ]; then
|
||||
REGKEY=$key
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
function createUDSConnectionTemplate {
|
||||
TMPFILE=$(mktemp /tmp/udsexport.XXXXXX)
|
||||
cat > $TMPFILE << EOF
|
||||
<Profile>
|
||||
<ProfileSettings>
|
||||
<Name>UDS Template Profile</Name>
|
||||
<RegistryRoot>root/ConnectionType/freerdp/connections/{ff064bd9-047a-45ec-b70f-04ab218186ff}</RegistryRoot>
|
||||
<Target>
|
||||
<Hardware>t420</Hardware>
|
||||
<ImageId>T7X62022</ImageId>
|
||||
<Version>6.2.0</Version>
|
||||
<Config>standard</Config>
|
||||
</Target>
|
||||
</ProfileSettings>
|
||||
<ProfileRegistry>
|
||||
<NodeDir name="{ff064bd9-047a-45ec-b70f-04ab218186ff}">
|
||||
<NodeDir name="rdWebFeed">
|
||||
<NodeKey name="keepResourcesWindowOpened">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="autoStartSingleResource">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="autoDisconnectTimeout">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
</NodeDir>
|
||||
<NodeDir name="loginfields">
|
||||
<NodeKey name="username">
|
||||
<NodeParam name="value">3</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="rememberme">
|
||||
<NodeParam name="value">2</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="password">
|
||||
<NodeParam name="value">3</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="domain">
|
||||
<NodeParam name="value">3</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
</NodeDir>
|
||||
<NodeDir name="authorizations">
|
||||
<NodeDir name="user">
|
||||
<NodeKey name="execution">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">string</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="edit">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">string</NodeParam>
|
||||
</NodeKey>
|
||||
</NodeDir>
|
||||
</NodeDir>
|
||||
<NodeKey name="address">
|
||||
<NodeParam name="value"/>
|
||||
<NodeParam name="type">string</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="username">
|
||||
<NodeParam name="value"/>
|
||||
<NodeParam name="type">string</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="password">
|
||||
<NodeParam name="value">NLCR.1</NodeParam>
|
||||
<NodeParam name="type">rc4</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="domain">
|
||||
<NodeParam name="value"/>
|
||||
<NodeParam name="type">string</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="label">
|
||||
<NodeParam name="value">Template_UDS</NodeParam>
|
||||
<NodeParam name="type">string</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="credentialsType">
|
||||
<NodeParam name="value">password</NodeParam>
|
||||
<NodeParam name="type">string</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="gatewayEnabled">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="gatewayPort">
|
||||
<NodeParam name="value">443</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="gatewayUsesSameCredentials">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="gatewayCredentialsType">
|
||||
<NodeParam name="value">password</NodeParam>
|
||||
<NodeParam name="type">string</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="remoteDesktopService">
|
||||
<NodeParam name="value">Remote Computer</NodeParam>
|
||||
<NodeParam name="type">string</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="windowMode">
|
||||
<NodeParam name="value">Remote Application</NodeParam>
|
||||
<NodeParam name="type">string</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="seamlessWindow">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="windowType">
|
||||
<NodeParam name="value">full</NodeParam>
|
||||
<NodeParam name="type">string</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="windowSizePercentage">
|
||||
<NodeParam name="value">70</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="windowSizeWidth">
|
||||
<NodeParam name="value">1024</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="windowSizeHeight">
|
||||
<NodeParam name="value">768</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="mouseMotionEvents">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="compression">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="rdpEncryption">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="offScreenBitmaps">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="attachToConsole">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="clipboardExtension">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="rdp6Buffering">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="rdpProgressiveCodec">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="securityLevel">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="tlsVersion">
|
||||
<NodeParam name="value">auto</NodeParam>
|
||||
<NodeParam name="type">string</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="sound">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="printerMapping">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="portMapping">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="usbStorageRedirection">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="localPartitionRedirection">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="scRedirection">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="usbMiscRedirection">
|
||||
<NodeParam name="value">2</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="perfFlagNoWallpaper">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="perfFlagFontSmoothing">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="perfFlagDesktopComposition">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="perfFlagNoWindowDrag">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="perfFlagNoMenuAnimations">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="perfFlagNoTheming">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="timeoutsEnabled">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="timeoutWarning">
|
||||
<NodeParam name="value">6000</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="timeoutWarningDialog">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="timeoutRecovery">
|
||||
<NodeParam name="value">30000</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="timeoutError">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="showRDPDashboard">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="showConnectionGraph">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="x11Synchronous">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="x11Logging">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="x11LogAutoflush">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="x11Capture">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="SingleSignOn">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="autostart">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">number</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="waitForNetwork">
|
||||
<NodeParam name="value">1</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="hasDesktopIcon">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
<NodeKey name="autoReconnect">
|
||||
<NodeParam name="value">0</NodeParam>
|
||||
<NodeParam name="type">bool</NodeParam>
|
||||
</NodeKey>
|
||||
</NodeDir>
|
||||
</ProfileRegistry>
|
||||
<ProfileFiles/>
|
||||
</Profile>
|
||||
EOF
|
||||
mclient import $TMPFILE
|
||||
rm $TMPFILE
|
||||
}
|
||||
|
||||
ADDRESS=
|
||||
USERNAME=
|
||||
PASSWORD=
|
||||
DOMAIN=
|
||||
REGKEY=
|
||||
CLEAR=0
|
||||
|
||||
# Try to locate registry key for UDS Template
|
||||
getRegKey
|
||||
|
||||
if [ "$REGKEY" == "" ]; then
|
||||
# Not found, create on based on our template
|
||||
createUDSConnectionTemplate
|
||||
getRegKey
|
||||
fi
|
||||
|
||||
for param in $@; do
|
||||
if [ "/u:" == "${param:0:3}" ]; then
|
||||
USERNAME=${param:3}
|
||||
CLEAR=1
|
||||
fi
|
||||
|
||||
if [ "/p:" == "${param:0:3}" ]; then
|
||||
PASSWORD=${param:3}
|
||||
CLEAR=1
|
||||
fi
|
||||
|
||||
if [ "/d:" == "${param:0:3}" ]; then
|
||||
DOMAIN=${param:3}
|
||||
CLEAR=1
|
||||
fi
|
||||
|
||||
if [ "/v:" == "${param:0:3}" ]; then
|
||||
ADDRESS=${param:3}
|
||||
CLEAR=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$CLEAR" -eq 1 ]; then
|
||||
clearParams
|
||||
fi
|
||||
|
||||
ID=`basename $REGKEY`
|
||||
RESPAWN=0
|
||||
|
||||
if [ "" != "$ADDRESS" ]; then
|
||||
mclient set $REGKEY/address "${ADDRESS}"
|
||||
RESPAWN=1
|
||||
fi
|
||||
|
||||
if [ "" != "$USERNAME" ]; then
|
||||
mclient set $REGKEY/username "${USERNAME}"
|
||||
RESPAWN=1
|
||||
fi
|
||||
|
||||
if [ "" != "$PASSWORD" ]; then
|
||||
mclient set $REGKEY/password "${PASSWORD}"
|
||||
RESPAWN=1
|
||||
fi
|
||||
|
||||
if [ "" != "$DOMAIN" ]; then
|
||||
mclient set $REGKEY/domain "${DOMAIN}"
|
||||
RESPAWN=1
|
||||
fi
|
||||
|
||||
if [ "$RESPAWN" -eq 1 ]; then
|
||||
mclient set $REGKEY/authorizations/user/execution 1
|
||||
mclient commit
|
||||
exec $0 # Restart without command line
|
||||
fi
|
||||
|
||||
process-connection $ID
|
||||
|
||||
clearParams
|
@@ -1,18 +1,17 @@
|
||||
version: 1
|
||||
script:
|
||||
# Remove any previous build
|
||||
- rm -rf /tmp/UDSClientDir | true
|
||||
- rm -rf $TARGET_APPDIR | true
|
||||
# Make usr and icons dirs
|
||||
- mkdir -p /tmp/UDSClientDir/usr/src
|
||||
- mkdir -p $TARGET_APPDIR/usr/src
|
||||
# Copy the python application code into the UDSClientDir
|
||||
- cp ../src/UDS*.py /tmp/UDSClientDir/usr/src
|
||||
- cp -r ../src/uds /tmp/UDSClientDir/usr/src
|
||||
- cp ../src/UDS*.py $TARGET_APPDIR/usr/src
|
||||
- cp -r ../src/uds $TARGET_APPDIR/usr/src
|
||||
# Remove __pycache__ and .mypy if exists
|
||||
- rm /tmp/UDSClientDir/usr/src/.mypy_cache -rf 2>&1 > /dev/null
|
||||
- rm /tmp/UDSClientDir/usr/src/uds/.mypy_cache -rf 2>&1 > /dev/null
|
||||
- rm /tmp/UDSClientDir/usr/src/__pycache__ -rf 2>&1 > /dev/null
|
||||
- rm /tmp/UDSClientDir/usr/src/uds/__pycache__ -rf 2>&1 > /dev/null
|
||||
|
||||
- rm $TARGET_APPDIR/usr/src/.mypy_cache -rf 2>&1 > /dev/null
|
||||
- rm $TARGET_APPDIR/usr/src/uds/.mypy_cache -rf 2>&1 > /dev/null
|
||||
- rm $TARGET_APPDIR/usr/src/__pycache__ -rf 2>&1 > /dev/null
|
||||
- rm $TARGET_APPDIR/usr/src/uds/__pycache__ -rf 2>&1 > /dev/null
|
||||
AppDir:
|
||||
# On /tmp, that is an ext4 filesystem. On btrfs squashfs complains with "Unrecognised xattr prefix btrfs.compression"
|
||||
path: /tmp/UDSClientDir
|
||||
@@ -31,7 +30,7 @@ AppDir:
|
||||
arch: amd64
|
||||
sources:
|
||||
- sourceline: 'deb [arch=amd64] http://ftp.de.debian.org/debian/ bullseye main contrib non-free'
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x04EE7237B7D453EC'
|
||||
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x648ACFD622F3D138'
|
||||
|
||||
include:
|
||||
- python3
|
||||
@@ -57,6 +56,6 @@ AppDir:
|
||||
PYTHONPATH: '${APPDIR}/usr/lib/python3.9/site-packages'
|
||||
|
||||
AppImage:
|
||||
update-information: None
|
||||
sign-key: None
|
||||
# update-information: None
|
||||
sign-key: 592AF43A64B8559137FA2458AA4ECFEE784E6BA7
|
||||
arch: x86_64
|
||||
|
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/env -S python3 -s
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2014-2021 Virtual Cable S.L.U.
|
||||
@@ -41,13 +41,13 @@ import typing
|
||||
from PyQt5 import QtCore, QtWidgets
|
||||
from PyQt5.QtCore import QSettings
|
||||
|
||||
from uds.rest import RestApi, RetryException, InvalidVersion, UDSException
|
||||
from uds.rest import RestApi, RetryException, InvalidVersion
|
||||
|
||||
# Just to ensure there are available on runtime
|
||||
from uds.forward import forward # type: ignore
|
||||
from uds.tunnel import forward as f2 # type: ignore
|
||||
from uds.forward import forward as ssh_forward # type: ignore # pylint: disable=unused-import
|
||||
from uds.tunnel import forward as tunnel_forwards # type: ignore # pylint: disable=unused-import
|
||||
|
||||
from uds.log import logger, DEBUG
|
||||
from uds.log import logger
|
||||
from uds import tools
|
||||
from uds import VERSION
|
||||
|
||||
@@ -55,7 +55,6 @@ from UDSWindow import Ui_MainWindow
|
||||
|
||||
|
||||
class UDSClient(QtWidgets.QMainWindow):
|
||||
|
||||
ticket: str = ''
|
||||
scrambler: str = ''
|
||||
withError = False
|
||||
@@ -149,8 +148,10 @@ class UDSClient(QtWidgets.QMainWindow):
|
||||
webbrowser.open(e.downloadUrl)
|
||||
self.closeWindow()
|
||||
return
|
||||
except Exception as e:
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
self.showError(e)
|
||||
self.closeWindow()
|
||||
return
|
||||
|
||||
self.getTransportData()
|
||||
|
||||
@@ -166,7 +167,9 @@ class UDSClient(QtWidgets.QMainWindow):
|
||||
# self.hide()
|
||||
self.closeWindow()
|
||||
|
||||
exec(script, globals(), {'parent': self, 'sp': params})
|
||||
exec(
|
||||
script, globals(), {'parent': self, 'sp': params}
|
||||
) # pylint: disable=exec-used
|
||||
|
||||
# Execute the waiting tasks...
|
||||
threading.Thread(target=endScript).start()
|
||||
@@ -175,9 +178,8 @@ class UDSClient(QtWidgets.QMainWindow):
|
||||
self.ui.info.setText(str(e) + ', retrying access...')
|
||||
# Retry operation in ten seconds
|
||||
QtCore.QTimer.singleShot(10000, self.getTransportData)
|
||||
except Exception as e:
|
||||
if DEBUG:
|
||||
logger.exception('Got exception on getTransportData')
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
logger.exception('Error getting transport data')
|
||||
self.showError(e)
|
||||
|
||||
def start(self):
|
||||
@@ -194,27 +196,27 @@ def endScript():
|
||||
try:
|
||||
# Remove early stage files...
|
||||
tools.unlinkFiles(early=True)
|
||||
except Exception as e:
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
logger.debug('Unlinking files on early stage: %s', e)
|
||||
|
||||
# After running script, wait for stuff
|
||||
try:
|
||||
logger.debug('Wating for tasks to finish...')
|
||||
tools.waitForTasks()
|
||||
except Exception as e:
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
logger.debug('Watiting for tasks to finish: %s', e)
|
||||
|
||||
try:
|
||||
logger.debug('Unlinking files')
|
||||
tools.unlinkFiles(early=False)
|
||||
except Exception as e:
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
logger.debug('Unlinking files on later stage: %s', e)
|
||||
|
||||
# Removing
|
||||
try:
|
||||
logger.debug('Executing threads before exit')
|
||||
tools.execBeforeExit()
|
||||
except Exception as e:
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
logger.debug('execBeforeExit: %s', e)
|
||||
|
||||
logger.debug('endScript done')
|
||||
@@ -305,7 +307,7 @@ def minimal(api: RestApi, ticket: str, scrambler: str):
|
||||
+ '\n\nPlease, retry again in a while.',
|
||||
QtWidgets.QMessageBox.Ok,
|
||||
)
|
||||
except Exception as e:
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
# logger.exception('Got exception on getTransportData')
|
||||
QtWidgets.QMessageBox.critical(
|
||||
None, # type: ignore
|
||||
@@ -316,12 +318,11 @@ def minimal(api: RestApi, ticket: str, scrambler: str):
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
def main(args: typing.List[str]):
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
logger.debug('Initializing connector for %s(%s)', sys.platform, platform.machine())
|
||||
|
||||
# Initialize app
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
|
||||
logger.debug('Arguments: %s', args)
|
||||
# Set several info for settings
|
||||
QtCore.QCoreApplication.setOrganizationName('Virtual Cable S.L.U.')
|
||||
QtCore.QCoreApplication.setApplicationName('UDS Connector')
|
||||
@@ -343,41 +344,48 @@ if __name__ == "__main__":
|
||||
# First parameter must be url
|
||||
useMinimal = False
|
||||
try:
|
||||
uri = sys.argv[1]
|
||||
uri = args[1]
|
||||
|
||||
if uri == '--minimal':
|
||||
useMinimal = True
|
||||
uri = sys.argv[2] # And get URI
|
||||
uri = args[2] # And get URI
|
||||
|
||||
if uri == '--test':
|
||||
sys.exit(0)
|
||||
|
||||
logger.debug('URI: %s', uri)
|
||||
if uri[:6] != 'uds://' and uri[:7] != 'udss://':
|
||||
raise Exception()
|
||||
# Shows error if using http (uds:// ) version, not supported anymore
|
||||
if uri[:6] == 'uds://':
|
||||
QtWidgets.QMessageBox.critical(
|
||||
None, # type: ignore
|
||||
'Notice',
|
||||
f'UDS Client Version {VERSION} does not support HTTP protocol Anymore.',
|
||||
QtWidgets.QMessageBox.Ok,
|
||||
)
|
||||
sys.exit(1)
|
||||
if uri[:7] != 'udss://':
|
||||
raise Exception('Not supported protocol') # Just shows "about" dialog
|
||||
|
||||
ssl = uri[3] == 's'
|
||||
host, ticket, scrambler = uri.split('//')[1].split('/') # type: ignore
|
||||
logger.debug(
|
||||
'ssl:%s, host:%s, ticket:%s, scrambler:%s',
|
||||
ssl,
|
||||
'host:%s, ticket:%s, scrambler:%s',
|
||||
host,
|
||||
UDSClient.ticket,
|
||||
UDSClient.scrambler,
|
||||
ticket,
|
||||
scrambler,
|
||||
)
|
||||
except Exception:
|
||||
except Exception: # pylint: disable=broad-except
|
||||
logger.debug('Detected execution without valid URI, exiting')
|
||||
QtWidgets.QMessageBox.critical(
|
||||
None, # type: ignore
|
||||
'Notice',
|
||||
'UDS Client Version {}'.format(VERSION),
|
||||
f'UDS Client Version {VERSION}',
|
||||
QtWidgets.QMessageBox.Ok,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# Setup REST api endpoint
|
||||
api = RestApi(
|
||||
'{}://{}/uds/rest/client'.format(['http', 'https'][ssl], host), sslError
|
||||
f'https://{host}/uds/rest/client', sslError
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -392,10 +400,10 @@ if __name__ == "__main__":
|
||||
|
||||
win.start()
|
||||
|
||||
exitVal = app.exec_()
|
||||
exitVal = app.exec()
|
||||
logger.debug('Execution finished correctly')
|
||||
|
||||
except Exception as e:
|
||||
except Exception as e: # pylint: disable=broad-exception-caught
|
||||
logger.exception('Got an exception executing client:')
|
||||
exitVal = 128
|
||||
QtWidgets.QMessageBox.critical(
|
||||
@@ -404,3 +412,7 @@ if __name__ == "__main__":
|
||||
|
||||
logger.debug('Exiting')
|
||||
sys.exit(exitVal)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv)
|
||||
|
80
client-py3/full/src/UDSClientLauncher.py
Normal file
80
client-py3/full/src/UDSClientLauncher.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import sys
|
||||
import os.path
|
||||
import subprocess
|
||||
import typing
|
||||
|
||||
from uds.log import logger
|
||||
import UDSClient
|
||||
from UDSLauncherMac import Ui_MacLauncher
|
||||
|
||||
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
|
||||
SCRIPT_NAME = 'UDSClientLauncher'
|
||||
|
||||
|
||||
class UdsApplication(QtWidgets.QApplication):
|
||||
path: str
|
||||
tunnels: typing.List[subprocess.Popen]
|
||||
|
||||
def __init__(self, argv: typing.List[str]) -> None:
|
||||
super().__init__(argv)
|
||||
self.path = os.path.join(os.path.dirname(sys.argv[0]).replace('Resources', 'MacOS'), SCRIPT_NAME)
|
||||
self.tunnels = []
|
||||
self.lastWindowClosed.connect(self.closeTunnels) # type: ignore
|
||||
|
||||
def cleanTunnels(self) -> None:
|
||||
'''
|
||||
Removes all finished tunnels from the list
|
||||
'''
|
||||
|
||||
def isRunning(p: subprocess.Popen):
|
||||
try:
|
||||
if p.poll() is None:
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.debug('Got error polling subprocess: %s', e)
|
||||
return False
|
||||
|
||||
# Remove references to finished tunnels, they will be garbage collected
|
||||
self.tunnels = [tunnel for tunnel in self.tunnels if isRunning(tunnel)]
|
||||
|
||||
def closeTunnels(self) -> None:
|
||||
'''
|
||||
Finishes all running tunnels
|
||||
'''
|
||||
logger.debug('Closing remaining tunnels')
|
||||
for tunnel in self.tunnels:
|
||||
logger.debug('Checking %s - "%s"', tunnel, tunnel.poll())
|
||||
if tunnel.poll() is None: # Running
|
||||
logger.info('Found running tunnel %s, closing it', tunnel.pid)
|
||||
tunnel.kill()
|
||||
|
||||
def event(self, evnt: QtCore.QEvent) -> bool:
|
||||
if evnt.type() == QtCore.QEvent.Type.FileOpen:
|
||||
fe = typing.cast(QtGui.QFileOpenEvent, evnt)
|
||||
logger.debug('Got url: %s', fe.url().url())
|
||||
fe.accept()
|
||||
logger.debug('Spawning %s', self.path)
|
||||
# First, remove all finished tunnel processed from check queue
|
||||
self.cleanTunnels()
|
||||
# And now add a new one
|
||||
self.tunnels.append(subprocess.Popen([self.path, fe.url().url()]))
|
||||
|
||||
return super().event(evnt)
|
||||
|
||||
|
||||
def main(args: typing.List[str]):
|
||||
if len(args) > 1:
|
||||
UDSClient.main(args)
|
||||
else:
|
||||
app = UdsApplication(sys.argv)
|
||||
window = QtWidgets.QMainWindow()
|
||||
Ui_MacLauncher().setupUi(window)
|
||||
|
||||
window.showMinimized()
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(args=sys.argv)
|
75
client-py3/full/src/UDSLauncherMac.py
Normal file
75
client-py3/full/src/UDSLauncherMac.py
Normal file
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file 'UDSLauncherMac.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.15.2
|
||||
#
|
||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||
# run again. Do not edit this file unless you know what you are doing.
|
||||
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_MacLauncher(object):
|
||||
def setupUi(self, MacLauncher):
|
||||
MacLauncher.setObjectName("MacLauncher")
|
||||
MacLauncher.setWindowModality(QtCore.Qt.NonModal)
|
||||
MacLauncher.resize(235, 120)
|
||||
MacLauncher.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
|
||||
icon = QtGui.QIcon()
|
||||
icon.addPixmap(QtGui.QPixmap(":/images/logo-uds-small"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
|
||||
MacLauncher.setWindowIcon(icon)
|
||||
MacLauncher.setWindowOpacity(1.0)
|
||||
self.centralwidget = QtWidgets.QWidget(MacLauncher)
|
||||
self.centralwidget.setAutoFillBackground(True)
|
||||
self.centralwidget.setObjectName("centralwidget")
|
||||
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
|
||||
self.verticalLayout_2.setContentsMargins(4, 4, 4, 4)
|
||||
self.verticalLayout_2.setSpacing(4)
|
||||
self.verticalLayout_2.setObjectName("verticalLayout_2")
|
||||
self.frame = QtWidgets.QFrame(self.centralwidget)
|
||||
self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
||||
self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
|
||||
self.frame.setObjectName("frame")
|
||||
self.verticalLayout = QtWidgets.QVBoxLayout(self.frame)
|
||||
self.verticalLayout.setObjectName("verticalLayout")
|
||||
self.topLabel = QtWidgets.QLabel(self.frame)
|
||||
self.topLabel.setTextFormat(QtCore.Qt.RichText)
|
||||
self.topLabel.setObjectName("topLabel")
|
||||
self.verticalLayout.addWidget(self.topLabel)
|
||||
self.image = QtWidgets.QLabel(self.frame)
|
||||
self.image.setMinimumSize(QtCore.QSize(0, 32))
|
||||
self.image.setAutoFillBackground(True)
|
||||
self.image.setText("")
|
||||
self.image.setPixmap(QtGui.QPixmap(":/images/logo-uds-small"))
|
||||
self.image.setScaledContents(False)
|
||||
self.image.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.image.setObjectName("image")
|
||||
self.verticalLayout.addWidget(self.image)
|
||||
self.label_2 = QtWidgets.QLabel(self.frame)
|
||||
self.label_2.setTextFormat(QtCore.Qt.RichText)
|
||||
self.label_2.setObjectName("label_2")
|
||||
self.verticalLayout.addWidget(self.label_2)
|
||||
self.verticalLayout_2.addWidget(self.frame)
|
||||
MacLauncher.setCentralWidget(self.centralwidget)
|
||||
|
||||
self.retranslateUi(MacLauncher)
|
||||
QtCore.QMetaObject.connectSlotsByName(MacLauncher)
|
||||
|
||||
def retranslateUi(self, MacLauncher):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
MacLauncher.setWindowTitle(_translate("MacLauncher", "UDS Launcher"))
|
||||
self.topLabel.setText(_translate("MacLauncher", "<html><head/><body><p align=\"center\"><span style=\" font-size:12pt; font-weight:600;\">UDS Launcher</span></p></body></html>"))
|
||||
self.label_2.setText(_translate("MacLauncher", "<html><head/><body><p align=\"center\"><span style=\" font-size:6pt;\">Closing this window will end all UDS tunnels</span></p></body></html>"))
|
||||
import UDSResources_rc
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
MacLauncher = QtWidgets.QMainWindow()
|
||||
ui = Ui_MacLauncher()
|
||||
ui.setupUi(MacLauncher)
|
||||
MacLauncher.show()
|
||||
sys.exit(app.exec_())
|
113
client-py3/full/src/UDSLauncherMac.ui
Normal file
113
client-py3/full/src/UDSLauncherMac.ui
Normal file
@@ -0,0 +1,113 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MacLauncher</class>
|
||||
<widget class="QMainWindow" name="MacLauncher">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::NonModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>235</width>
|
||||
<height>120</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="cursor">
|
||||
<cursorShape>ArrowCursor</cursorShape>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>UDS Launcher</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset resource="UDSResources.qrc">
|
||||
<normaloff>:/images/logo-uds-small</normaloff>:/images/logo-uds-small</iconset>
|
||||
</property>
|
||||
<property name="windowOpacity">
|
||||
<double>1.000000000000000</double>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<property name="autoFillBackground">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QFrame" name="frame">
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::StyledPanel</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Raised</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="topLabel">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p align="center"><span style=" font-size:12pt; font-weight:600;">UDS Launcher</span></p></body></html></string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="image">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>32</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true"/>
|
||||
</property>
|
||||
<property name="pixmap">
|
||||
<pixmap resource="UDSResources.qrc">:/images/logo-uds-small</pixmap>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p align="center"><span style=" font-size:6pt;">Closing this window will end all UDS tunnels</span></p></body></html></string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="UDSResources.qrc"/>
|
||||
</resources>
|
||||
<connections/>
|
||||
</ui>
|
@@ -2,7 +2,7 @@
|
||||
|
||||
# Resource object code
|
||||
#
|
||||
# Created by: The Resource Compiler for PyQt5 (Qt v5.13.2)
|
||||
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@@ -2467,9 +2467,9 @@ qt_resource_struct_v2 = b"\
|
||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
||||
\x00\x00\x01\x70\xc4\x82\x24\xd0\
|
||||
\x00\x00\x01\x81\xce\x8a\xaf\xd2\
|
||||
\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x08\xf1\
|
||||
\x00\x00\x01\x70\xc4\x82\x24\xd0\
|
||||
\x00\x00\x01\x81\xce\x8a\xaf\xd2\
|
||||
"
|
||||
|
||||
qt_version = [int(v) for v in QtCore.qVersion().split('.')]
|
||||
|
@@ -2,9 +2,10 @@
|
||||
|
||||
# Form implementation generated from reading ui file 'UDSWindow.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.13.2
|
||||
# Created by: PyQt5 UI code generator 5.15.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
|
||||
# run again. Do not edit this file unless you know what you are doing.
|
||||
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
@@ -29,13 +29,11 @@
|
||||
'''
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
'''
|
||||
from __future__ import unicode_literals
|
||||
|
||||
VERSION = '3.5.0'
|
||||
VERSION = '3.6.0'
|
||||
|
||||
__title__ = 'udclient'
|
||||
__version__ = VERSION
|
||||
__build__ = 0x010760
|
||||
__author__ = 'Adolfo Gómez'
|
||||
__build__ = 0x010712
|
||||
__author__ = 'Adolfo Gómez <dkmaster@dkmon.com>'
|
||||
__license__ = "BSD 3-clause"
|
||||
__copyright__ = "Copyright 2014-2017 VirtualCable S.L.U."
|
||||
__copyright__ = "Copyright 2014-2022 VirtualCable S.L.U."
|
||||
|
@@ -25,7 +25,11 @@ class CheckfingerPrints(paramiko.MissingHostKeyPolicy):
|
||||
if self.fingerPrints:
|
||||
remotefingerPrints = hexlify(key.get_fingerprint()).decode().lower()
|
||||
if remotefingerPrints not in self.fingerPrints.split(','):
|
||||
logger.error("Server {!r} has invalid fingerPrints. ({} vs {})".format(hostname, remotefingerPrints, self.fingerPrints))
|
||||
logger.error(
|
||||
"Server {!r} has invalid fingerPrints. ({} vs {})".format(
|
||||
hostname, remotefingerPrints, self.fingerPrints
|
||||
)
|
||||
)
|
||||
raise paramiko.SSHException(
|
||||
"Server {!r} has invalid fingerPrints".format(hostname)
|
||||
)
|
||||
@@ -47,21 +51,39 @@ class Handler(socketserver.BaseRequestHandler):
|
||||
self.thread.currentConnections += 1
|
||||
|
||||
try:
|
||||
chan = self.ssh_transport.open_channel('direct-tcpip',
|
||||
(self.chain_host, self.chain_port),
|
||||
self.request.getpeername())
|
||||
chan = self.ssh_transport.open_channel(
|
||||
'direct-tcpip',
|
||||
(self.chain_host, self.chain_port),
|
||||
self.request.getpeername(),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception('Incoming request to %s:%d failed: %s', self.chain_host, self.chain_port, repr(e))
|
||||
logger.exception(
|
||||
'Incoming request to %s:%d failed: %s',
|
||||
self.chain_host,
|
||||
self.chain_port,
|
||||
repr(e),
|
||||
)
|
||||
return
|
||||
if chan is None:
|
||||
logger.error('Incoming request to %s:%d was rejected by the SSH server.', self.chain_host, self.chain_port)
|
||||
logger.error(
|
||||
'Incoming request to %s:%d was rejected by the SSH server.',
|
||||
self.chain_host,
|
||||
self.chain_port,
|
||||
)
|
||||
return
|
||||
|
||||
logger.debug('Connected! Tunnel open %r -> %r -> %r', self.request.getpeername(), chan.getpeername(), (self.chain_host, self.chain_port))
|
||||
logger.debug(
|
||||
'Connected! Tunnel open %r -> %r -> %r',
|
||||
self.request.getpeername(),
|
||||
chan.getpeername(),
|
||||
(self.chain_host, self.chain_port),
|
||||
)
|
||||
# self.ssh_transport.set_keepalive(10) # Keep alive every 10 seconds...
|
||||
try:
|
||||
while self.event.is_set() is False:
|
||||
r, _w, _x = select.select([self.request, chan], [], [], 1) # pylint: disable=unused-variable
|
||||
r, _w, _x = select.select(
|
||||
[self.request, chan], [], [], 1
|
||||
) # pylint: disable=unused-variable
|
||||
|
||||
if self.request in r:
|
||||
data = self.request.recv(1024)
|
||||
@@ -80,7 +102,10 @@ class Handler(socketserver.BaseRequestHandler):
|
||||
peername = self.request.getpeername()
|
||||
chan.close()
|
||||
self.request.close()
|
||||
logger.debug('Tunnel closed from %r', peername,)
|
||||
logger.debug(
|
||||
'Tunnel closed from %r',
|
||||
peername,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -95,7 +120,18 @@ class ForwardThread(threading.Thread):
|
||||
client: typing.Optional[paramiko.SSHClient]
|
||||
fs: typing.Optional[ForwardServer]
|
||||
|
||||
def __init__(self, server, port, username, password, localPort, redirectHost, redirectPort, waitTime, fingerPrints):
|
||||
def __init__(
|
||||
self,
|
||||
server,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
localPort,
|
||||
redirectHost,
|
||||
redirectPort,
|
||||
waitTime,
|
||||
fingerPrints,
|
||||
):
|
||||
threading.Thread.__init__(self)
|
||||
self.client = None
|
||||
self.fs = None
|
||||
@@ -110,7 +146,7 @@ class ForwardThread(threading.Thread):
|
||||
self.redirectPort = redirectPort
|
||||
|
||||
self.waitTime = waitTime
|
||||
|
||||
|
||||
self.fingerPrints = fingerPrints
|
||||
|
||||
self.stopEvent = threading.Event()
|
||||
@@ -124,7 +160,17 @@ class ForwardThread(threading.Thread):
|
||||
if localPort is None:
|
||||
localPort = random.randrange(33000, 53000)
|
||||
|
||||
ft = ForwardThread(self.server, self.port, self.username, self.password, localPort, redirectHost, redirectPort, self.waitTime, self.fingerPrints)
|
||||
ft = ForwardThread(
|
||||
self.server,
|
||||
self.port,
|
||||
self.username,
|
||||
self.password,
|
||||
localPort,
|
||||
redirectHost,
|
||||
redirectPort,
|
||||
self.waitTime,
|
||||
self.fingerPrints,
|
||||
)
|
||||
ft.client = self.client
|
||||
self.client.useCount += 1 # type: ignore
|
||||
ft.start()
|
||||
@@ -134,7 +180,6 @@ class ForwardThread(threading.Thread):
|
||||
|
||||
return (ft, localPort)
|
||||
|
||||
|
||||
def _timerFnc(self):
|
||||
self.timer = None
|
||||
logger.debug('Timer fnc: %s', self.currentConnections)
|
||||
@@ -148,12 +193,21 @@ class ForwardThread(threading.Thread):
|
||||
self.client = paramiko.SSHClient()
|
||||
self.client.useCount = 1 # type: ignore
|
||||
self.client.load_system_host_keys()
|
||||
self.client.set_missing_host_key_policy(CheckfingerPrints(self.fingerPrints))
|
||||
self.client.set_missing_host_key_policy(
|
||||
CheckfingerPrints(self.fingerPrints)
|
||||
)
|
||||
|
||||
logger.debug('Connecting to ssh host %s:%d ...', self.server, self.port)
|
||||
|
||||
# To disable ssh-ageng asking for passwords: allow_agent=False
|
||||
self.client.connect(self.server, self.port, username=self.username, password=self.password, timeout=5, allow_agent=False)
|
||||
self.client.connect(
|
||||
self.server,
|
||||
self.port,
|
||||
username=self.username,
|
||||
password=self.password,
|
||||
timeout=5,
|
||||
allow_agent=False,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('Exception connecting: ')
|
||||
self.status = 2 # Error
|
||||
@@ -162,7 +216,7 @@ class ForwardThread(threading.Thread):
|
||||
class SubHandler(Handler):
|
||||
chain_host = self.redirectHost
|
||||
chain_port = self.redirectPort
|
||||
ssh_transport = self.client.get_transport()
|
||||
ssh_transport = self.client.get_transport() # type: ignore
|
||||
event = self.stopEvent
|
||||
thread = self
|
||||
|
||||
@@ -194,7 +248,17 @@ class ForwardThread(threading.Thread):
|
||||
logger.exception('Exception stopping')
|
||||
|
||||
|
||||
def forward(server, port, username, password, redirectHost, redirectPort, localPort=None, waitTime=10, fingerPrints=None):
|
||||
def forward(
|
||||
server,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
redirectHost,
|
||||
redirectPort,
|
||||
localPort=None,
|
||||
waitTime=10,
|
||||
fingerPrints=None,
|
||||
):
|
||||
'''
|
||||
Instantiates an ssh connection to server:port
|
||||
Returns the Thread created and the local redirected port as a list: (thread, port)
|
||||
@@ -204,10 +268,28 @@ def forward(server, port, username, password, redirectHost, redirectPort, localP
|
||||
if localPort is None:
|
||||
localPort = random.randrange(40000, 50000)
|
||||
|
||||
logger.debug('Connecting to %s:%s using %s/%s redirecting to %s:%s, listening on 127.0.0.1:%s',
|
||||
server, port, username, password, redirectHost, redirectPort, localPort)
|
||||
logger.debug(
|
||||
'Connecting to %s:%s using %s/%s redirecting to %s:%s, listening on 127.0.0.1:%s',
|
||||
server,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
redirectHost,
|
||||
redirectPort,
|
||||
localPort,
|
||||
)
|
||||
|
||||
ft = ForwardThread(server, port, username, password, localPort, redirectHost, redirectPort, waitTime, fingerPrints)
|
||||
ft = ForwardThread(
|
||||
server,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
localPort,
|
||||
redirectHost,
|
||||
redirectPort,
|
||||
waitTime,
|
||||
fingerPrints,
|
||||
)
|
||||
|
||||
ft.start()
|
||||
|
||||
|
@@ -29,11 +29,10 @@
|
||||
'''
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
'''
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import platform
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
@@ -57,9 +56,44 @@ try:
|
||||
filename=logFile,
|
||||
filemode='a',
|
||||
format='%(levelname)s %(asctime)s %(message)s',
|
||||
level=LOGLEVEL
|
||||
level=LOGLEVEL,
|
||||
)
|
||||
except Exception:
|
||||
logging.basicConfig(format='%(levelname)s %(asctime)s %(message)s', level=LOGLEVEL)
|
||||
|
||||
logger = logging.getLogger('udsclient')
|
||||
|
||||
if DEBUG:
|
||||
# Include as much as platform info as possible
|
||||
logger.debug('Platform info:')
|
||||
logger.debug(' Platform: %s', platform.platform())
|
||||
logger.debug(' Node: %s', platform.node())
|
||||
logger.debug(' System: %s', platform.system())
|
||||
logger.debug(' Release: %s', platform.release())
|
||||
logger.debug(' Version: %s', platform.version())
|
||||
logger.debug(' Machine: %s', platform.machine())
|
||||
logger.debug(' Processor: %s', platform.processor())
|
||||
logger.debug(' Architecture: %s', platform.architecture())
|
||||
logger.debug(' Python version: %s', platform.python_version())
|
||||
logger.debug(' Python implementation: %s', platform.python_implementation())
|
||||
logger.debug(' Python compiler: %s', platform.python_compiler())
|
||||
logger.debug(' Python build: %s', platform.python_build())
|
||||
# Also environment variables and any useful info
|
||||
logger.debug('Log level set to DEBUG')
|
||||
logger.debug('Environment variables:')
|
||||
for k, v in os.environ.items():
|
||||
logger.debug(' %s=%s', k, v)
|
||||
|
||||
# usefull info for debugging
|
||||
logger.debug('Python path: %s', sys.path)
|
||||
logger.debug('Python executable: %s', sys.executable)
|
||||
logger.debug('Python version: %s', sys.version)
|
||||
logger.debug('Python version info: %s', sys.version_info)
|
||||
logger.debug('Python prefix: %s', sys.prefix)
|
||||
logger.debug('Python base prefix: %s', sys.base_prefix)
|
||||
logger.debug('Python executable: %s', sys.executable)
|
||||
logger.debug('Python argv: %s', sys.argv)
|
||||
logger.debug('Python modules path: %s', sys.path)
|
||||
logger.debug('Python modules path (site): %s', sys.path_importer_cache)
|
||||
logger.debug('Python modules path (site): %s', sys.path_hooks)
|
||||
|
@@ -30,14 +30,13 @@
|
||||
'''
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
'''
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import sys
|
||||
|
||||
LINUX = 'Linux'
|
||||
WINDOWS = 'Windows'
|
||||
MAC_OS_X = 'Mac os x'
|
||||
|
||||
|
||||
def getOs():
|
||||
if sys.platform.startswith('linux'):
|
||||
return LINUX
|
||||
|
@@ -29,8 +29,6 @@
|
||||
'''
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
'''
|
||||
# pylint: disable=c-extension-no-member,no-name-in-module
|
||||
|
||||
import json
|
||||
import bz2
|
||||
import base64
|
||||
@@ -42,7 +40,6 @@ import ssl
|
||||
import socket
|
||||
import typing
|
||||
|
||||
import certifi
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
@@ -54,6 +51,19 @@ from .log import logger
|
||||
# Server before this version uses "unsigned" scripts
|
||||
OLD_METHOD_VERSION = '2.4.0'
|
||||
|
||||
SECURE_CIPHERS = (
|
||||
'TLS_AES_256_GCM_SHA384'
|
||||
':TLS_CHACHA20_POLY1305_SHA256'
|
||||
':TLS_AES_128_GCM_SHA256'
|
||||
':ECDHE-RSA-AES256-GCM-SHA384'
|
||||
':ECDHE-RSA-AES128-GCM-SHA256'
|
||||
':ECDHE-RSA-CHACHA20-POLY1305'
|
||||
':ECDHE-ECDSA-AES128-GCM-SHA256'
|
||||
':ECDHE-ECDSA-AES256-GCM-SHA384'
|
||||
':ECDHE-ECDSA-AES128-SHA256'
|
||||
':ECDHE-ECDSA-CHACHA20-POLY1305'
|
||||
)
|
||||
|
||||
# Callback for error on cert
|
||||
# parameters are hostname, serial
|
||||
# If returns True, ignores error
|
||||
@@ -63,9 +73,11 @@ CertCallbackType = typing.Callable[[str, str], bool]
|
||||
class UDSException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RetryException(UDSException):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidVersion(UDSException):
|
||||
downloadUrl: str
|
||||
|
||||
@@ -75,7 +87,7 @@ class InvalidVersion(UDSException):
|
||||
|
||||
class RestApi:
|
||||
|
||||
_restApiUrl: str # base Rest API URL
|
||||
_restApiUrl: str # base Rest API URL
|
||||
_callbackInvalidCert: typing.Optional[CertCallbackType]
|
||||
_serverVersion: str
|
||||
|
||||
@@ -90,14 +102,18 @@ class RestApi:
|
||||
self._callbackInvalidCert = callbackInvalidCert
|
||||
self._serverVersion = ''
|
||||
|
||||
def get(self, url: str, params: typing.Optional[typing.Mapping[str, str]] = None) -> typing.Any:
|
||||
def get(
|
||||
self, url: str, params: typing.Optional[typing.Mapping[str, str]] = None
|
||||
) -> typing.Any:
|
||||
if params:
|
||||
url += '?' + '&'.join(
|
||||
'{}={}'.format(k, urllib.parse.quote(str(v).encode('utf8')))
|
||||
for k, v in params.items()
|
||||
)
|
||||
|
||||
return json.loads(RestApi.getUrl(self._restApiUrl + url, self._callbackInvalidCert))
|
||||
return json.loads(
|
||||
RestApi.getUrl(self._restApiUrl + url, self._callbackInvalidCert)
|
||||
)
|
||||
|
||||
def processError(self, data: typing.Any) -> None:
|
||||
if 'error' in data:
|
||||
@@ -106,7 +122,6 @@ class RestApi:
|
||||
|
||||
raise UDSException(data['error'])
|
||||
|
||||
|
||||
def getVersion(self) -> str:
|
||||
'''Gets and stores the serverVersion.
|
||||
Also checks that the version is valid for us. If not,
|
||||
@@ -122,12 +137,16 @@ class RestApi:
|
||||
try:
|
||||
if self._serverVersion > VERSION:
|
||||
raise InvalidVersion(downloadUrl)
|
||||
|
||||
|
||||
return self._serverVersion
|
||||
except InvalidVersion:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise UDSException(e)
|
||||
|
||||
def getScriptAndParams(self, ticket: str, scrambler: str) -> typing.Tuple[str, typing.Any]:
|
||||
def getScriptAndParams(
|
||||
self, ticket: str, scrambler: str
|
||||
) -> typing.Tuple[str, typing.Any]:
|
||||
'''Gets the transport script, validates it if necesary
|
||||
and returns it'''
|
||||
try:
|
||||
@@ -145,10 +164,7 @@ class RestApi:
|
||||
params = None
|
||||
|
||||
if self._serverVersion <= OLD_METHOD_VERSION:
|
||||
script = bz2.decompress(base64.b64decode(data['result']))
|
||||
# This fixes uds 2.2 "write" string on binary streams on some transport
|
||||
script = script.replace(b'stdin.write("', b'stdin.write(b"')
|
||||
script = script.replace(b'version)', b'version.decode("utf-8"))')
|
||||
raise Exception('Server version is too old. Please, update it')
|
||||
else:
|
||||
res = data['result']
|
||||
# We have three elements on result:
|
||||
@@ -173,7 +189,6 @@ class RestApi:
|
||||
|
||||
# exec(script.decode("utf-8"), globals(), {'parent': self, 'sp': params})
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _open(
|
||||
url: str, certErrorCallback: typing.Optional[CertCallbackType] = None
|
||||
@@ -181,15 +196,27 @@ class RestApi:
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
ctx.load_verify_locations(certifi.where())
|
||||
# Disable SSLv2, SSLv3, TLSv1, TLSv1.1
|
||||
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
ctx.set_ciphers(SECURE_CIPHERS)
|
||||
|
||||
# If we have the certificates file, we use it
|
||||
if tools.getCaCertsFile() is not None:
|
||||
ctx.load_verify_locations(tools.getCaCertsFile())
|
||||
hostname = urllib.parse.urlparse(url)[1]
|
||||
serial = ''
|
||||
|
||||
port = ''
|
||||
if ':' in hostname:
|
||||
hostname, port = hostname.split(':')
|
||||
|
||||
if url.startswith('https'):
|
||||
port = port or '443'
|
||||
with ctx.wrap_socket(
|
||||
socket.socket(socket.AF_INET, socket.SOCK_STREAM), server_hostname=hostname
|
||||
socket.socket(socket.AF_INET, socket.SOCK_STREAM),
|
||||
server_hostname=hostname,
|
||||
) as s:
|
||||
s.connect((hostname, 443))
|
||||
s.connect((hostname, int(port)))
|
||||
# Get binary certificate
|
||||
binCert = s.getpeercert(True)
|
||||
if binCert:
|
||||
@@ -200,14 +227,17 @@ class RestApi:
|
||||
serial = hex(cert.serial_number)[2:]
|
||||
|
||||
response = None
|
||||
ctx.check_hostname = True
|
||||
ctx.verify_mode = ssl.CERT_REQUIRED
|
||||
ctx.check_hostname = True
|
||||
|
||||
def urlopen(url: str):
|
||||
# Generate the request with the headers
|
||||
req = urllib.request.Request(url, headers={
|
||||
'User-Agent': os_detector.getOs() + " - UDS Connector " + VERSION
|
||||
})
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={
|
||||
'User-Agent': os_detector.getOs() + " - UDS Connector " + VERSION
|
||||
},
|
||||
)
|
||||
return urllib.request.urlopen(req, context=ctx)
|
||||
|
||||
try:
|
||||
|
@@ -33,6 +33,8 @@ import tempfile
|
||||
import string
|
||||
import random
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import socket
|
||||
import stat
|
||||
import sys
|
||||
@@ -40,11 +42,19 @@ import time
|
||||
import base64
|
||||
import typing
|
||||
|
||||
import certifi
|
||||
|
||||
# For signature checking
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import utils, padding
|
||||
|
||||
try:
|
||||
import psutil
|
||||
except ImportError:
|
||||
psutil = None
|
||||
|
||||
|
||||
from .log import logger
|
||||
|
||||
_unlinkFiles: typing.List[typing.Tuple[str, bool]] = []
|
||||
@@ -72,9 +82,7 @@ nVgtClKcDDlSaBsO875WDR0CAwEAAQ==
|
||||
|
||||
def saveTempFile(content: str, filename: typing.Optional[str] = None) -> str:
|
||||
if filename is None:
|
||||
filename = ''.join(
|
||||
random.choice(string.ascii_lowercase + string.digits) for _ in range(16)
|
||||
)
|
||||
filename = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
|
||||
filename = filename + '.uds'
|
||||
|
||||
filename = os.path.join(tempfile.gettempdir(), filename)
|
||||
@@ -104,9 +112,7 @@ def testServer(host: str, port: typing.Union[str, int], timeOut: int = 4) -> boo
|
||||
return True
|
||||
|
||||
|
||||
def findApp(
|
||||
appName: str, extraPath: typing.Optional[str] = None
|
||||
) -> typing.Optional[str]:
|
||||
def findApp(appName: str, extraPath: typing.Optional[str] = None) -> typing.Optional[str]:
|
||||
searchPath = os.environ['PATH'].split(os.pathsep)
|
||||
if extraPath:
|
||||
searchPath += list(extraPath)
|
||||
@@ -135,9 +141,7 @@ def addFileToUnlink(filename: str, early: bool = False) -> None:
|
||||
'''
|
||||
Adds a file to the wait-and-unlink list
|
||||
'''
|
||||
logger.debug(
|
||||
'Added file %s to unlink on %s stage', filename, 'early' if early else 'later'
|
||||
)
|
||||
logger.debug('Added file %s to unlink on %s stage', filename, 'early' if early else 'later')
|
||||
_unlinkFiles.append((filename, early))
|
||||
|
||||
|
||||
@@ -161,7 +165,9 @@ def unlinkFiles(early: bool = False) -> None:
|
||||
|
||||
def addTaskToWait(task: typing.Any, includeSubprocess: bool = False) -> None:
|
||||
logger.debug(
|
||||
'Added task %s to wait %s', task, 'with subprocesses' if includeSubprocess else ''
|
||||
'Added task %s to wait %s',
|
||||
task,
|
||||
'with subprocesses' if includeSubprocess else '',
|
||||
)
|
||||
_tasksToWait.append((task, includeSubprocess))
|
||||
|
||||
@@ -176,11 +182,19 @@ def waitForTasks() -> None:
|
||||
elif hasattr(task, 'wait'):
|
||||
task.wait()
|
||||
# If wait for spanwed process (look for process with task pid) and we can look for them...
|
||||
logger.debug('Psutil: %s, waitForSubp: %s, hasattr: %s', psutil, waitForSubp, hasattr(task, 'pid'))
|
||||
logger.debug(
|
||||
'Psutil: %s, waitForSubp: %s, hasattr: %s',
|
||||
psutil,
|
||||
waitForSubp,
|
||||
hasattr(task, 'pid'),
|
||||
)
|
||||
if psutil and waitForSubp and hasattr(task, 'pid'):
|
||||
subProcesses = list(filter(
|
||||
lambda x: x.ppid() == task.pid, psutil.process_iter(attrs=('ppid',))
|
||||
))
|
||||
subProcesses = list(
|
||||
filter(
|
||||
lambda x: x.ppid() == task.pid, # type: ignore
|
||||
psutil.process_iter(attrs=('ppid',)),
|
||||
)
|
||||
)
|
||||
logger.debug('Waiting for subprocesses... %s, %s', task.pid, subProcesses)
|
||||
for i in subProcesses:
|
||||
logger.debug('Found %s', i)
|
||||
@@ -208,21 +222,47 @@ def verifySignature(script: bytes, signature: bytes) -> bool:
|
||||
param: signature String signature to be verified
|
||||
return: Boolean. True if the signature is valid; False otherwise.
|
||||
'''
|
||||
# For signature checking
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
from cryptography.hazmat.primitives.asymmetric import utils, padding
|
||||
|
||||
public_key = serialization.load_pem_public_key(
|
||||
data=PUBLIC_KEY, backend=default_backend()
|
||||
)
|
||||
public_key = serialization.load_pem_public_key(data=PUBLIC_KEY, backend=default_backend())
|
||||
|
||||
try:
|
||||
public_key.verify(
|
||||
base64.b64decode(signature), script, padding.PKCS1v15(), hashes.SHA256()
|
||||
public_key.verify( # type: ignore
|
||||
base64.b64decode(signature), script, padding.PKCS1v15(), hashes.SHA256() # type: ignore
|
||||
)
|
||||
except Exception: # InvalidSignature
|
||||
return False
|
||||
|
||||
# If no exception, the script was fine...
|
||||
return True
|
||||
|
||||
|
||||
def getCaCertsFile() -> typing.Optional[str]:
|
||||
# First, try certifi...
|
||||
|
||||
# If environment contains CERTIFICATE_BUNDLE_PATH, use it
|
||||
if 'CERTIFICATE_BUNDLE_PATH' in os.environ:
|
||||
return os.environ['CERTIFICATE_BUNDLE_PATH']
|
||||
|
||||
try:
|
||||
if os.path.exists(certifi.where()):
|
||||
return certifi.where()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info('Certifi file does not exists: %s', certifi.where())
|
||||
|
||||
# Check if "standard" paths are valid for linux systems
|
||||
if 'linux' in sys.platform:
|
||||
for path in (
|
||||
'/etc/pki/tls/certs/ca-bundle.crt',
|
||||
'/etc/ssl/certs/ca-certificates.crt',
|
||||
'/etc/ssl/ca-bundle.pem',
|
||||
):
|
||||
if os.path.exists(path):
|
||||
logger.info('Found certifi path: %s', path)
|
||||
return path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def isMac() -> bool:
|
||||
return 'darwin' in sys.platform
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2021 Virtual Cable S.L.U.
|
||||
# Copyright (c) 2022 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
@@ -32,14 +32,12 @@ import socket
|
||||
import socketserver
|
||||
import ssl
|
||||
import threading
|
||||
import time
|
||||
import random
|
||||
import threading
|
||||
import select
|
||||
import typing
|
||||
import logging
|
||||
|
||||
import certifi
|
||||
from . import tools
|
||||
|
||||
HANDSHAKE_V1 = b'\x5AMGB\xA5\x01\x00'
|
||||
BUFFER_SIZE = 1024 * 16 # Max buffer length
|
||||
@@ -49,8 +47,10 @@ LISTEN_ADDRESS = '0.0.0.0' if DEBUG else '127.0.0.1'
|
||||
# ForwarServer states
|
||||
TUNNEL_LISTENING, TUNNEL_OPENING, TUNNEL_PROCESSING, TUNNEL_ERROR = 0, 1, 2, 3
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PayLoadType = typing.Optional[typing.Tuple[typing.Optional[bytes], typing.Optional[bytes]]]
|
||||
|
||||
class ForwardServer(socketserver.ThreadingTCPServer):
|
||||
daemon_threads = True
|
||||
@@ -60,9 +60,9 @@ class ForwardServer(socketserver.ThreadingTCPServer):
|
||||
ticket: str
|
||||
stop_flag: threading.Event
|
||||
can_stop: bool
|
||||
timeout: int
|
||||
timer: typing.Optional[threading.Timer]
|
||||
check_certificate: bool
|
||||
keep_listening: bool
|
||||
current_connections: int
|
||||
status: int
|
||||
|
||||
@@ -73,30 +73,29 @@ class ForwardServer(socketserver.ThreadingTCPServer):
|
||||
timeout: int = 0,
|
||||
local_port: int = 0,
|
||||
check_certificate: bool = True,
|
||||
keep_listening: bool = False,
|
||||
) -> None:
|
||||
|
||||
local_port = local_port or random.randrange(33000, 53000)
|
||||
|
||||
super().__init__(
|
||||
server_address=(LISTEN_ADDRESS, local_port), RequestHandlerClass=Handler
|
||||
)
|
||||
self.remote = remote
|
||||
self.ticket = ticket
|
||||
# Negative values for timeout, means "accept always connections"
|
||||
# "but if no connection is stablished on timeout (positive)"
|
||||
# "stop the listener"
|
||||
self.timeout = int(time.time()) + timeout if timeout > 0 else 0
|
||||
# Note that this is for backwards compatibility, better use "keep_listening"
|
||||
if timeout < 0:
|
||||
keep_listening = True
|
||||
timeout = abs(timeout)
|
||||
|
||||
super().__init__(server_address=(LISTEN_ADDRESS, local_port), RequestHandlerClass=Handler)
|
||||
self.remote = remote
|
||||
self.ticket = ticket
|
||||
self.check_certificate = check_certificate
|
||||
self.keep_listening = keep_listening
|
||||
self.stop_flag = threading.Event() # False initial
|
||||
self.current_connections = 0
|
||||
|
||||
self.status = TUNNEL_LISTENING
|
||||
self.can_stop = False
|
||||
|
||||
timeout = abs(timeout) or 60
|
||||
self.timer = threading.Timer(
|
||||
abs(timeout), ForwardServer.__checkStarted, args=(self,)
|
||||
)
|
||||
timeout = timeout or 60
|
||||
self.timer = threading.Timer(abs(timeout), ForwardServer.__checkStarted, args=(self,))
|
||||
self.timer.start()
|
||||
|
||||
def stop(self) -> None:
|
||||
@@ -114,11 +113,19 @@ class ForwardServer(socketserver.ThreadingTCPServer):
|
||||
|
||||
rsocket.connect(self.remote)
|
||||
|
||||
rsocket.sendall(HANDSHAKE_V1) # No response expected, just the handshake
|
||||
|
||||
context = ssl.create_default_context()
|
||||
|
||||
|
||||
# Do not "recompress" data, use only "base protocol" compression
|
||||
context.options |= ssl.OP_NO_COMPRESSION
|
||||
context.load_verify_locations(certifi.where()) # Load certifi certificates
|
||||
# Macs with default installed python, does not support mininum tls version set to TLSv1.3
|
||||
# USe "brew" version instead, or uncomment next line and comment the next one
|
||||
# context.minimum_version = ssl.TLSVersion.TLSv1_2 if tools.isMac() else ssl.TLSVersion.TLSv1_3
|
||||
context.minimum_version = ssl.TLSVersion.TLSv1_3
|
||||
|
||||
if tools.getCaCertsFile() is not None:
|
||||
context.load_verify_locations(tools.getCaCertsFile()) # Load certifi certificates
|
||||
|
||||
# If ignore remote certificate
|
||||
if self.check_certificate is False:
|
||||
@@ -136,25 +143,27 @@ class ForwardServer(socketserver.ThreadingTCPServer):
|
||||
|
||||
try:
|
||||
with self.connect() as ssl_socket:
|
||||
ssl_socket.sendall(HANDSHAKE_V1 + b'TEST')
|
||||
ssl_socket.sendall(b'TEST')
|
||||
resp = ssl_socket.recv(2)
|
||||
if resp != b'OK':
|
||||
raise Exception({'Invalid tunnelresponse: {resp}'})
|
||||
logger.debug('Tunnel is available!')
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
'Error connecting to tunnel server %s: %s', self.server_address, e
|
||||
)
|
||||
logger.error('Error connecting to tunnel server %s: %s', self.server_address, e)
|
||||
return False
|
||||
|
||||
@property
|
||||
def stoppable(self) -> bool:
|
||||
logger.debug('Is stoppable: %s', self.can_stop)
|
||||
return self.can_stop or (self.timeout != 0 and int(time.time()) > self.timeout)
|
||||
return self.can_stop
|
||||
|
||||
@staticmethod
|
||||
def __checkStarted(fs: 'ForwardServer') -> None:
|
||||
# As soon as the timer is fired, the server can be stopped
|
||||
# This means that:
|
||||
# * If not connections are stablished, the server will be stopped
|
||||
# * If no "keep_listening" is set, the server will not allow any new connections
|
||||
logger.debug('New connection limit reached')
|
||||
fs.timer = None
|
||||
fs.can_stop = True
|
||||
@@ -170,8 +179,8 @@ class Handler(socketserver.BaseRequestHandler):
|
||||
def handle(self) -> None:
|
||||
self.server.status = TUNNEL_OPENING
|
||||
|
||||
# If server processing is over time
|
||||
if self.server.stoppable:
|
||||
# If server processing is over time, and don't allow more connections
|
||||
if self.server.stoppable and not self.server.keep_listening:
|
||||
self.server.status = TUNNEL_ERROR
|
||||
logger.info('Rejected timedout connection')
|
||||
self.request.close() # End connection without processing it
|
||||
@@ -184,16 +193,15 @@ class Handler(socketserver.BaseRequestHandler):
|
||||
logger.debug('Ticket %s', self.server.ticket)
|
||||
with self.server.connect() as ssl_socket:
|
||||
# Send handhshake + command + ticket
|
||||
ssl_socket.sendall(HANDSHAKE_V1 + b'OPEN' + self.server.ticket.encode())
|
||||
ssl_socket.sendall(b'OPEN' + self.server.ticket.encode())
|
||||
# Check response is OK
|
||||
data = ssl_socket.recv(2)
|
||||
if data != b'OK':
|
||||
data += ssl_socket.recv(128)
|
||||
raise Exception(
|
||||
f'Error received: {data.decode(errors="ignore")}'
|
||||
) # Notify error
|
||||
raise Exception(f'Error received: {data.decode(errors="ignore")}') # Notify error
|
||||
|
||||
# All is fine, now we can tunnel data
|
||||
|
||||
self.process(remote=ssl_socket)
|
||||
except Exception as e:
|
||||
logger.error(f'Error connecting to {self.server.remote!s}: {e!s}')
|
||||
@@ -230,10 +238,9 @@ class Handler(socketserver.BaseRequestHandler):
|
||||
|
||||
def _run(server: ForwardServer) -> None:
|
||||
logger.debug(
|
||||
'Starting forwarder: %s -> %s, timeout: %d',
|
||||
'Starting forwarder: %s -> %s',
|
||||
server.server_address,
|
||||
server.remote,
|
||||
server.timeout,
|
||||
)
|
||||
server.serve_forever()
|
||||
logger.debug('Stoped forwarder %s -> %s', server.server_address, server.remote)
|
||||
@@ -245,14 +252,15 @@ def forward(
|
||||
timeout: int = 0,
|
||||
local_port: int = 0,
|
||||
check_certificate=True,
|
||||
keep_listening=True,
|
||||
) -> ForwardServer:
|
||||
|
||||
fs = ForwardServer(
|
||||
remote=remote,
|
||||
ticket=ticket,
|
||||
timeout=timeout,
|
||||
local_port=local_port,
|
||||
check_certificate=check_certificate,
|
||||
keep_listening=keep_listening,
|
||||
)
|
||||
# Starts a new thread
|
||||
threading.Thread(target=_run, args=(fs,)).start()
|
||||
@@ -267,18 +275,26 @@ if __name__ == "__main__":
|
||||
log.setLevel(logging.DEBUG)
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setLevel(logging.DEBUG)
|
||||
formatter = logging.Formatter(
|
||||
'%(levelname)s - %(message)s'
|
||||
) # Basic log format, nice for syslog
|
||||
formatter = logging.Formatter('%(levelname)s - %(message)s') # Basic log format, nice for syslog
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
|
||||
ticket = 'mffqg7q4s61fvx0ck2pe0zke6k0c5ipb34clhbkbs4dasb4g'
|
||||
|
||||
fs = forward(
|
||||
('172.27.0.1', 7777),
|
||||
('demoaslan.udsenterprise.com', 11443),
|
||||
ticket,
|
||||
local_port=49999,
|
||||
local_port=0,
|
||||
timeout=-20,
|
||||
check_certificate=False,
|
||||
)
|
||||
print('Listening on port', fs.server_address)
|
||||
import socket
|
||||
# Open a socket to local fs.server_address and send some random data
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.connect(fs.server_address)
|
||||
s.sendall(b'Hello world!')
|
||||
data = s.recv(1024)
|
||||
print('Received', repr(data))
|
||||
fs.stop()
|
||||
|
||||
|
@@ -7,9 +7,9 @@
|
||||
<groupId>org.openuds.server</groupId>
|
||||
<artifactId>guacamole-auth-uds</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<version>2.5.0</version>
|
||||
<version>4.0.0</version>
|
||||
<name>UDS Integration Extension for Apache Guacamole</name>
|
||||
<url>https://github.com/dkmstr/openuds</url>
|
||||
<url>https://github.com/VirtualCable/openuds</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
@@ -18,11 +18,11 @@
|
||||
<build>
|
||||
<plugins>
|
||||
|
||||
<!-- Compile using Java 1.8 -->
|
||||
<!-- Compile using Java 1.8, as guacamole-client -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.3</version>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>1.8</source>
|
||||
<target>1.8</target>
|
||||
@@ -38,7 +38,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>2.10</version>
|
||||
<version>3.1.2</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>unpack-dependencies</id>
|
||||
@@ -70,15 +70,15 @@
|
||||
<dependency>
|
||||
<groupId>javax.ws.rs</groupId>
|
||||
<artifactId>jsr311-api</artifactId>
|
||||
<version>1.1.1</version>
|
||||
<scope>provided</scope>
|
||||
<version>1.1.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Guacamole extension API -->
|
||||
<dependency>
|
||||
<groupId>org.apache.guacamole</groupId>
|
||||
<artifactId>guacamole-ext</artifactId>
|
||||
<version>1.2.0</version>
|
||||
<version>1.5.2</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
<version>3.0</version>
|
||||
<version>5.1.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
@@ -55,7 +55,7 @@ public class UDSModule extends AbstractModule {
|
||||
* If the guacamole.properties file cannot be read.
|
||||
*/
|
||||
public UDSModule() throws GuacamoleException {
|
||||
this.environment = new LocalEnvironment();
|
||||
this.environment = LocalEnvironment.getInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -30,13 +30,8 @@ package org.openuds.guacamole.config;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import org.apache.guacamole.GuacamoleException;
|
||||
import org.apache.guacamole.GuacamoleServerException;
|
||||
import org.apache.guacamole.environment.Environment;
|
||||
import org.apache.guacamole.properties.URIGuacamoleProperty;
|
||||
|
||||
|
159
server/samples/REST4.py
Normal file
159
server/samples/REST4.py
Normal file
@@ -0,0 +1,159 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2021 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
'''
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
'''
|
||||
|
||||
import json
|
||||
import sys
|
||||
import typing
|
||||
|
||||
import requests
|
||||
|
||||
rest_url = 'http://172.27.0.1:8000/uds/rest/'
|
||||
|
||||
session = requests.Session()
|
||||
session.headers.update({'Content-Type': 'application/json'})
|
||||
|
||||
|
||||
class RESTException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AuthException(RESTException):
|
||||
pass
|
||||
|
||||
|
||||
class LogoutException(RESTException):
|
||||
pass
|
||||
|
||||
|
||||
# Hace login con el root, puede usarse cualquier autenticador y cualquier usuario, pero en la 1.5 solo está implementado poder hacer
|
||||
# este tipo de login con el usuario "root"
|
||||
def login():
|
||||
# parameters = '{ "auth": "admin", "username": "root", "password": "temporal" }'
|
||||
# parameters = '{ "auth": "interna", "username": "admin", "password": "temporal" }'
|
||||
parameters = {'auth': 'interna', 'username': 'admin', 'password': 'temporal'}
|
||||
|
||||
response = session.post(rest_url + 'auth/login', json=parameters)
|
||||
|
||||
if not response.ok:
|
||||
raise AuthException('Error logging in')
|
||||
|
||||
# resp contiene las cabeceras, content el contenido de la respuesta (que es json), pero aún está en formato texto
|
||||
res = response.json()
|
||||
print(res)
|
||||
|
||||
if res['result'] != 'ok': # Authentication error
|
||||
raise AuthException('Authentication error')
|
||||
|
||||
session.headers.update({'X-Auth-Token': res['token']})
|
||||
|
||||
|
||||
def logout():
|
||||
response = session.get(rest_url + 'auth/logout')
|
||||
|
||||
if not response.ok:
|
||||
raise LogoutException('Error logging out')
|
||||
|
||||
|
||||
# Sample response from request_pools
|
||||
# [
|
||||
# {
|
||||
# u'initial_srvs': 0,
|
||||
# u'name': u'WinAdolfo',
|
||||
# u'max_srvs': 0,
|
||||
# u'comments': u'',
|
||||
# u'id': 6,
|
||||
# u'state': u'A',
|
||||
# u'user_services_count': 3,
|
||||
# u'cache_l2_srvs': 0,
|
||||
# u'service_id': 9,
|
||||
# u'provider_id': 2,
|
||||
# u'cache_l1_srvs': 0,
|
||||
# u'restrained': False}
|
||||
# ]
|
||||
|
||||
|
||||
def request_pools() -> typing.List[typing.MutableMapping[str, typing.Any]]:
|
||||
response = session.get(rest_url + 'servicespools/overview')
|
||||
if not response.ok:
|
||||
raise RESTException('Error requesting pools')
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
def request_ticket(
|
||||
username: str,
|
||||
authSmallName: str,
|
||||
groups: typing.Union[typing.List[str], str],
|
||||
servicePool: str,
|
||||
realName: typing.Optional[str] = None,
|
||||
transport: typing.Optional[str] = None,
|
||||
force: bool = False
|
||||
) -> typing.MutableMapping[str, typing.Any]:
|
||||
data = {
|
||||
'username': username,
|
||||
'authSmallName': authSmallName,
|
||||
'groups': groups,
|
||||
'servicePool': servicePool,
|
||||
'force': 'true' if force else 'false'
|
||||
}
|
||||
if realName:
|
||||
data['realname'] = realName
|
||||
if transport:
|
||||
data['transport'] = transport
|
||||
response = session.put(
|
||||
rest_url + 'tickets/create',
|
||||
json=data
|
||||
)
|
||||
if not response.ok:
|
||||
raise RESTException('Error requesting ticket')
|
||||
|
||||
return response.json()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# request_pools() # Not logged in, this will generate an error
|
||||
login() # Will raise an exception if error
|
||||
#pools = request_pools()
|
||||
#for i in pools:
|
||||
# print(i['id'], i['name'])
|
||||
ticket = request_ticket(
|
||||
username='adolfo',
|
||||
authSmallName='172.27.0.1:8000',
|
||||
groups=['adolfo', 'dkmaster'],
|
||||
servicePool='5d045a19-54b5-541b-ba56-447b0622191c',
|
||||
realName='Adolfo Gómez',
|
||||
force=True
|
||||
)
|
||||
print(ticket)
|
||||
|
||||
logout()
|
16
server/src/server/asgi.py
Normal file
16
server/src/server/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for server project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
|
||||
|
||||
application = get_asgi_application()
|
@@ -8,12 +8,17 @@ import django
|
||||
# calculated paths for django and the site
|
||||
# used as starting points for various other paths
|
||||
DJANGO_ROOT = os.path.dirname(os.path.realpath(django.__file__))
|
||||
BASE_DIR = '/'.join(os.path.dirname(os.path.abspath(__file__)).split('/')[:-1]) # If used 'relpath' instead of abspath, returns path of "enterprise" instead of "openuds"
|
||||
BASE_DIR = '/'.join(
|
||||
os.path.dirname(os.path.abspath(__file__)).split('/')[:-1]
|
||||
) # If used 'relpath' instead of abspath, returns path of "enterprise" instead of "openuds"
|
||||
|
||||
DEBUG = True
|
||||
|
||||
# USE_X_FORWARDED_HOST = True
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') # For testing behind a reverse proxy
|
||||
SECURE_PROXY_SSL_HEADER = (
|
||||
'HTTP_X_FORWARDED_PROTO',
|
||||
'https',
|
||||
) # For testing behind a reverse proxy
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
@@ -29,12 +34,12 @@ DATABASES = {
|
||||
'PASSWORD': 'PASSWOR', # Not used with sqlite3.
|
||||
'HOST': 'localhost', # Set to empty string for localhost. Not used with sqlite3.
|
||||
'PORT': '3306', # Set to empty string for default. Not used with sqlite3.
|
||||
# 'CONN_MAX_AGE': 600, # Enable DB Pooling, 10 minutes max connection duration
|
||||
# 'CONN_MAX_AGE': 600, # Enable DB Pooling, 10 minutes max connection duration
|
||||
}
|
||||
}
|
||||
ALLOWED_HOSTS = '*'
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
DEFAULT_AUTO_FIELD='django.db.models.AutoField'
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
|
||||
# Local time zone for this installation. Choices can be found here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# although not all choices may be available on all operating systems.
|
||||
@@ -54,17 +59,17 @@ LANGUAGE_CODE = 'en'
|
||||
ugettext = lambda s: s
|
||||
|
||||
LANGUAGES = (
|
||||
('es', ugettext('Spanish')),
|
||||
('en', ugettext('English')),
|
||||
('fr', ugettext('French')),
|
||||
('de', ugettext('German')),
|
||||
('pt', ugettext('Portuguese')),
|
||||
('it', ugettext('Italian')),
|
||||
('ar', ugettext('Arabic')),
|
||||
('eu', ugettext('Basque')),
|
||||
('ar', ugettext('Arabian')),
|
||||
('ca', ugettext('Catalan')),
|
||||
('zh-hans', ugettext('Chinese')),
|
||||
('es', ugettext('Spanish')),
|
||||
('en', ugettext('English')),
|
||||
('fr', ugettext('French')),
|
||||
('de', ugettext('German')),
|
||||
('pt', ugettext('Portuguese')),
|
||||
('it', ugettext('Italian')),
|
||||
('ar', ugettext('Arabic')),
|
||||
('eu', ugettext('Basque')),
|
||||
('ar', ugettext('Arabian')),
|
||||
('ca', ugettext('Catalan')),
|
||||
('zh-hans', ugettext('Chinese')),
|
||||
)
|
||||
|
||||
LANGUAGE_COOKIE_NAME = 'uds_lang'
|
||||
@@ -123,15 +128,15 @@ CACHES = {
|
||||
'OPTIONS': {
|
||||
'MAX_ENTRIES': 5000,
|
||||
'CULL_FREQUENCY': 3, # 0 = Entire cache will be erased once MAX_ENTRIES is reached, this is faster on DB. if other value, will remove 1/this number items fromm cache
|
||||
}
|
||||
},
|
||||
},
|
||||
# 'memory': {
|
||||
# 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
# }
|
||||
# 'memory': {
|
||||
# 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
# }
|
||||
'memory': {
|
||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||
'LOCATION': '127.0.0.1:11211',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# Related to file uploading
|
||||
@@ -144,6 +149,32 @@ SECRET_KEY = 's5ky!7b5f#s35!e38xv%e-+iey6yi-#630x)kk3kk5_j8rie2*'
|
||||
# This is a very long string, an RSA KEY (this can be changed, but if u loose it, all encription will be lost)
|
||||
RSA_KEY = '-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQC0qe1GlriQbHFYdKYRPBFDSS8Ne/TEKI2mtPKJf36XZTy6rIyH\nvUpT1gMScVjHjOISLNJQqktyv0G+ZGzLDmfkCUBev6JBlFwNeX3Dv/97Q0BsEzJX\noYHiDANUkuB30ukmGvG0sg1v4ccl+xs2Su6pFSc5bGINBcQ5tO0ZI6Q1nQIDAQAB\nAoGBAKA7Octqb+T/mQOX6ZXNjY38wXOXJb44LXHWeGnEnvUNf/Aci0L0epCidfUM\nfG33oKX4BMwwTVxHDrsa/HaXn0FZtbQeBVywZqMqWpkfL/Ho8XJ8Rsq8OfElrwek\nOCPXgxMzQYxoNHw8V97k5qhfupQ+h878BseN367xSyQ8plahAkEAuPgAi6aobwZ5\nFZhx/+6rmQ8sM8FOuzzm6bclrvfuRAUFa9+kMM2K48NAneAtLPphofqI8wDPCYgQ\nTl7O96GXVQJBAPoKtWIMuBHJXKCdUNOISmeEvEzJMPKduvyqnUYv17tM0JTV0uzO\nuDpJoNIwVPq5c3LJaORKeCZnt3dBrdH1FSkCQQC3DK+1hIvhvB0uUvxWlIL7aTmM\nSny47Y9zsc04N6JzbCiuVdeueGs/9eXHl6f9gBgI7eCD48QAocfJVygphqA1AkEA\nrvzZjcIK+9+pJHqUO0XxlFrPkQloaRK77uHUaW9IEjui6dZu4+2T/q7SjubmQgWR\nZy7Pap03UuFZA2wCoqJbaQJAUG0FVrnyUORUnMQvdDjAWps2sXoPvA8sbQY1W8dh\nR2k4TCFl2wD7LutvsdgdkiH0gWdh5tc1c4dRmSX1eQ27nA==\n-----END RSA PRIVATE KEY-----'
|
||||
|
||||
# Trusted cyphers
|
||||
SECURE_CIPHERS = (
|
||||
'AES-256-GCM-SHA384'
|
||||
':CHACHA20-POLY1305-SHA256'
|
||||
':AES-128-GCM-SHA256'
|
||||
':ECDHE-RSA-AES256-GCM-SHA384'
|
||||
':ECDHE-RSA-AES128-GCM-SHA256'
|
||||
':ECDHE-RSA-CHACHA20-POLY1305'
|
||||
':ECDHE-ECDSA-AES128-GCM-SHA256'
|
||||
':ECDHE-ECDSA-AES256-GCM-SHA384'
|
||||
':ECDHE-ECDSA-AES128-SHA256'
|
||||
':ECDHE-ECDSA-CHACHA20-POLY1305'
|
||||
)
|
||||
# Min TLS version
|
||||
SECURE_MIN_TLS_VERSION = '1.2'
|
||||
|
||||
# LDAP CIFHER SUITE can be enforced here. Use GNU TLS cipher suite names in this case
|
||||
# i.e.:
|
||||
# * NORMAL
|
||||
# * NORMAL:-VERS-TLS-ALL:+VERS-TLS1.2:+VERS-TLS1.3
|
||||
# * PFS
|
||||
# * SECURE256
|
||||
# If omitted, defaults to PFS:-VERS-TLS-ALL:+VERS-TLS1.2:+VERS-TLS1.3:-AES-128-CBC:-AES-256-CBC:-DHE-RSA
|
||||
# Example:
|
||||
LDAP_CIPHER_SUITE = 'PFS:-VERS-TLS-ALL:+VERS-TLS1.2:+VERS-TLS1.3:-AES-128-CBC:-AES-256-CBC:-DHE-RSA'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
@@ -175,6 +206,7 @@ MIDDLEWARE = [
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'uds.core.util.middleware.security.UDSSecurityMiddleware',
|
||||
'uds.core.util.middleware.request.GlobalRequestMiddleware',
|
||||
'uds.core.util.middleware.xua.XUACompatibleMiddleware',
|
||||
'uds.core.util.middleware.redirect.RedirectMiddleware',
|
||||
@@ -229,25 +261,16 @@ LOGGING = {
|
||||
'simple': {
|
||||
'format': '%(levelname)s %(asctime)s %(module)s %(funcName)s %(lineno)d %(message)s'
|
||||
},
|
||||
'database': {
|
||||
'format': '%(levelname)s %(asctime)s Database %(message)s'
|
||||
},
|
||||
'auth': {
|
||||
'format': '%(asctime)s %(message)s'
|
||||
},
|
||||
'use': {
|
||||
'format': '%(asctime)s %(message)s'
|
||||
},
|
||||
'trace': {
|
||||
'format': '%(levelname)s %(asctime)s %(message)s'
|
||||
}
|
||||
'database': {'format': '%(levelname)s %(asctime)s Database %(message)s'},
|
||||
'auth': {'format': '%(asctime)s %(message)s'},
|
||||
'use': {'format': '%(asctime)s %(message)s'},
|
||||
'trace': {'format': '%(levelname)s %(asctime)s %(message)s'},
|
||||
},
|
||||
'handlers': {
|
||||
'null': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.NullHandler',
|
||||
},
|
||||
|
||||
'file': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
@@ -256,9 +279,8 @@ LOGGING = {
|
||||
'mode': 'a',
|
||||
'maxBytes': ROTATINGSIZE,
|
||||
'backupCount': 3,
|
||||
'encoding': 'utf-8'
|
||||
'encoding': 'utf-8',
|
||||
},
|
||||
|
||||
'database': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
@@ -267,9 +289,8 @@ LOGGING = {
|
||||
'mode': 'a',
|
||||
'maxBytes': ROTATINGSIZE,
|
||||
'backupCount': 3,
|
||||
'encoding': 'utf-8'
|
||||
'encoding': 'utf-8',
|
||||
},
|
||||
|
||||
'servicesFile': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
@@ -278,9 +299,8 @@ LOGGING = {
|
||||
'mode': 'a',
|
||||
'maxBytes': ROTATINGSIZE,
|
||||
'backupCount': 3,
|
||||
'encoding': 'utf-8'
|
||||
'encoding': 'utf-8',
|
||||
},
|
||||
|
||||
'workersFile': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
@@ -289,9 +309,8 @@ LOGGING = {
|
||||
'mode': 'a',
|
||||
'maxBytes': ROTATINGSIZE,
|
||||
'backupCount': 3,
|
||||
'encoding': 'utf-8'
|
||||
'encoding': 'utf-8',
|
||||
},
|
||||
|
||||
'authFile': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
@@ -300,9 +319,8 @@ LOGGING = {
|
||||
'mode': 'a',
|
||||
'maxBytes': ROTATINGSIZE,
|
||||
'backupCount': 3,
|
||||
'encoding': 'utf-8'
|
||||
'encoding': 'utf-8',
|
||||
},
|
||||
|
||||
'useFile': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
@@ -311,9 +329,8 @@ LOGGING = {
|
||||
'mode': 'a',
|
||||
'maxBytes': ROTATINGSIZE,
|
||||
'backupCount': 3,
|
||||
'encoding': 'utf-8'
|
||||
'encoding': 'utf-8',
|
||||
},
|
||||
|
||||
'traceFile': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
@@ -322,19 +339,18 @@ LOGGING = {
|
||||
'mode': 'a',
|
||||
'maxBytes': ROTATINGSIZE,
|
||||
'backupCount': 3,
|
||||
'encoding': 'utf-8'
|
||||
'encoding': 'utf-8',
|
||||
},
|
||||
|
||||
'console': {
|
||||
'level': 'DEBUG',
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'simple'
|
||||
'formatter': 'simple',
|
||||
},
|
||||
'mail_admins': {
|
||||
'level': 'ERROR',
|
||||
'class': 'django.utils.log.AdminEmailHandler',
|
||||
'filters': ['require_debug_false']
|
||||
}
|
||||
'filters': ['require_debug_false'],
|
||||
},
|
||||
},
|
||||
'loggers': {
|
||||
'': {
|
||||
@@ -356,12 +372,16 @@ LOGGING = {
|
||||
'level': 'DEBUG',
|
||||
'propagate': False,
|
||||
},
|
||||
|
||||
# Disable fonttools (used by reports) logging (too verbose)
|
||||
'fontTools': {
|
||||
'handlers': ['null'],
|
||||
'propagate': True,
|
||||
'level': 'ERROR',
|
||||
},
|
||||
'uds': {
|
||||
'handlers': ['file'],
|
||||
'level': LOGLEVEL,
|
||||
},
|
||||
|
||||
'uds.core.workers': {
|
||||
'handlers': ['workersFile'],
|
||||
'level': LOGLEVEL,
|
||||
@@ -372,7 +392,6 @@ LOGGING = {
|
||||
'level': LOGLEVEL,
|
||||
'propagate': False,
|
||||
},
|
||||
|
||||
'uds.services': {
|
||||
'handlers': ['servicesFile'],
|
||||
'level': LOGLEVEL,
|
||||
@@ -395,7 +414,6 @@ LOGGING = {
|
||||
'handlers': ['traceFile'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@@ -1,34 +1,16 @@
|
||||
"""
|
||||
WSGI config for server project.
|
||||
|
||||
This module contains the WSGI application used by Django's development server
|
||||
and any production WSGI deployments. It should expose a module-level variable
|
||||
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
|
||||
this application via the ``WSGI_APPLICATION`` setting.
|
||||
|
||||
Usually you will have the standard Django WSGI application here, but it also
|
||||
might make sense to replace the whole Django WSGI application with a custom one
|
||||
that later delegates to the Django one. For example, you could introduce WSGI
|
||||
middleware here, or combine a Django application with an application of another
|
||||
framework.
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/
|
||||
"""
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
import six
|
||||
import os
|
||||
|
||||
if six.PY2:
|
||||
import sys
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
# noinspection PyCompatibility
|
||||
reload(sys)
|
||||
sys.setdefaultencoding('UTF-8') # @UndefinedVariable
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
|
||||
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings")
|
||||
|
||||
# This application object is used by any WSGI server configured to use this
|
||||
# file. This includes Django's development server, if the WSGI_APPLICATION
|
||||
# setting points here.
|
||||
application = get_wsgi_application()
|
||||
|
@@ -43,6 +43,8 @@ from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import ugettext as _
|
||||
from uds.core import VERSION, VERSION_STAMP
|
||||
|
||||
from . import log
|
||||
|
||||
from .handlers import (
|
||||
Handler,
|
||||
HandlerError,
|
||||
@@ -50,19 +52,17 @@ from .handlers import (
|
||||
NotFound,
|
||||
RequestError,
|
||||
ResponseError,
|
||||
NotSupportedError
|
||||
NotSupportedError,
|
||||
)
|
||||
|
||||
from . import processors
|
||||
|
||||
# Not imported at runtime, just for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
from uds.core.util.request import ExtendedHttpRequest
|
||||
from uds.core.util.request import ExtendedHttpRequestWithUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ['Handler', 'Dispatcher']
|
||||
|
||||
AUTH_TOKEN_HEADER = 'X-Auth-Token'
|
||||
|
||||
|
||||
@@ -70,18 +70,18 @@ class Dispatcher(View):
|
||||
"""
|
||||
This class is responsible of dispatching REST requests
|
||||
"""
|
||||
|
||||
# This attribute will contain all paths--> handler relations, filled at Initialized method
|
||||
services: typing.ClassVar[typing.Dict[str, typing.Any]] = {'': None} # Will include a default /rest handler, but rigth now this will be fine
|
||||
services: typing.ClassVar[typing.Dict[str, typing.Any]] = {
|
||||
'': None
|
||||
} # Will include a default /rest handler, but rigth now this will be fine
|
||||
|
||||
# pylint: disable=too-many-locals, too-many-return-statements, too-many-branches, too-many-statements
|
||||
@method_decorator(csrf_exempt)
|
||||
def dispatch(self, request: 'ExtendedHttpRequest', *args, **kwargs):
|
||||
def dispatch(self, request: 'ExtendedHttpRequestWithUser', *args, **kwargs):
|
||||
"""
|
||||
Processes the REST request and routes it wherever it needs to be routed
|
||||
"""
|
||||
# Remove session from request, so response middleware do nothing with this
|
||||
del request.session
|
||||
|
||||
# Now we extract method and possible variables from path
|
||||
path: typing.List[str] = kwargs['arguments'].split('/')
|
||||
del kwargs['arguments']
|
||||
@@ -98,7 +98,9 @@ class Dispatcher(View):
|
||||
content_type = path[0].split('.')[1]
|
||||
|
||||
clean_path = path[0].split('.')[0]
|
||||
if not clean_path: # Skip empty path elements, so /x/y == /x////y for example (due to some bugs detected on some clients)
|
||||
if (
|
||||
not clean_path
|
||||
): # Skip empty path elements, so /x/y == /x////y for example (due to some bugs detected on some clients)
|
||||
path = path[1:]
|
||||
continue
|
||||
|
||||
@@ -115,9 +117,13 @@ class Dispatcher(View):
|
||||
# Here, service points to the path
|
||||
cls: typing.Optional[typing.Type[Handler]] = service['']
|
||||
if cls is None:
|
||||
return http.HttpResponseNotFound('Method not found', content_type="text/plain")
|
||||
return http.HttpResponseNotFound(
|
||||
'Method not found', content_type="text/plain"
|
||||
)
|
||||
|
||||
processor = processors.available_processors_ext_dict.get(content_type, processors.default_processor)(request)
|
||||
processor = processors.available_processors_ext_dict.get(
|
||||
content_type, processors.default_processor
|
||||
)(request)
|
||||
|
||||
# Obtain method to be invoked
|
||||
http_method: str = request.method.lower() if request.method else ''
|
||||
@@ -128,24 +134,46 @@ class Dispatcher(View):
|
||||
handler = None
|
||||
|
||||
try:
|
||||
handler = cls(request, full_path, http_method, processor.processParameters(), *args, **kwargs)
|
||||
handler = cls(
|
||||
request,
|
||||
full_path,
|
||||
http_method,
|
||||
processor.processParameters(),
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
operation: typing.Callable[[], typing.Any] = getattr(handler, http_method)
|
||||
except processors.ParametersException as e:
|
||||
logger.debug('Path: %s', full_path)
|
||||
logger.debug('Error: %s', e)
|
||||
return http.HttpResponseServerError('Invalid parameters invoking {0}: {1}'.format(full_path, e), content_type="text/plain")
|
||||
log.log_operation(handler, 500, log.ERROR)
|
||||
|
||||
return http.HttpResponseServerError(
|
||||
'Invalid parameters invoking {0}: {1}'.format(full_path, e),
|
||||
content_type="text/plain",
|
||||
)
|
||||
except AttributeError:
|
||||
allowedMethods = []
|
||||
for n in ['get', 'post', 'put', 'delete']:
|
||||
if hasattr(handler, n):
|
||||
allowedMethods.append(n)
|
||||
return http.HttpResponseNotAllowed(allowedMethods, content_type="text/plain")
|
||||
log.log_operation(handler, 405, log.ERROR)
|
||||
|
||||
return http.HttpResponseNotAllowed(
|
||||
allowedMethods, content_type="text/plain"
|
||||
)
|
||||
except AccessDenied:
|
||||
return http.HttpResponseForbidden('access denied', content_type="text/plain")
|
||||
return http.HttpResponseForbidden(
|
||||
'access denied', content_type="text/plain"
|
||||
)
|
||||
except Exception:
|
||||
logger.exception('error accessing attribute')
|
||||
logger.debug('Getting attribute %s for %s', http_method, full_path)
|
||||
return http.HttpResponseServerError('Unexcepected error', content_type="text/plain")
|
||||
|
||||
log.log_operation(handler, 500, log.ERROR)
|
||||
return http.HttpResponseServerError(
|
||||
'Unexcepected error', content_type="text/plain"
|
||||
)
|
||||
|
||||
# Invokes the handler's operation, add headers to response and returns
|
||||
try:
|
||||
@@ -157,20 +185,29 @@ class Dispatcher(View):
|
||||
response['UDS-Version'] = f'{VERSION};{VERSION_STAMP}'
|
||||
for k, val in handler.headers().items():
|
||||
response[k] = val
|
||||
|
||||
log.log_operation(handler, response.status_code, log.INFO)
|
||||
return response
|
||||
except RequestError as e:
|
||||
log.log_operation(handler, 400, log.ERROR)
|
||||
return http.HttpResponseBadRequest(str(e), content_type="text/plain")
|
||||
except ResponseError as e:
|
||||
log.log_operation(handler, 500, log.ERROR)
|
||||
return http.HttpResponseServerError(str(e), content_type="text/plain")
|
||||
except NotSupportedError as e:
|
||||
return http.HttpResponseBadRequest(str(e), content_type="text/plain")
|
||||
log.log_operation(handler, 501, log.ERROR)
|
||||
return http.HttpResponseBadRequest(str(e), content_type="text/plain", status=501)
|
||||
except AccessDenied as e:
|
||||
log.log_operation(handler, 403, log.ERROR)
|
||||
return http.HttpResponseForbidden(str(e), content_type="text/plain")
|
||||
except NotFound as e:
|
||||
log.log_operation(handler, 404, log.ERROR)
|
||||
return http.HttpResponseNotFound(str(e), content_type="text/plain")
|
||||
except HandlerError as e:
|
||||
log.log_operation(handler, 500, log.ERROR)
|
||||
return http.HttpResponseBadRequest(str(e), content_type="text/plain")
|
||||
except Exception as e:
|
||||
log.log_operation(handler, 500, log.ERROR)
|
||||
logger.exception('Error processing request')
|
||||
return http.HttpResponseServerError(str(e), content_type="text/plain")
|
||||
|
||||
@@ -180,12 +217,16 @@ class Dispatcher(View):
|
||||
Try to register Handler subclasses that have not been inherited
|
||||
"""
|
||||
for cls in classes:
|
||||
if not cls.__subclasses__(): # Only classes that has not been inherited will be registered as Handlers
|
||||
if (
|
||||
not cls.__subclasses__()
|
||||
): # Only classes that has not been inherited will be registered as Handlers
|
||||
if not cls.name:
|
||||
name = cls.__name__.lower()
|
||||
else:
|
||||
name = cls.name
|
||||
logger.debug('Adding handler %s for method %s in path %s', cls, name, cls.path)
|
||||
logger.debug(
|
||||
'Adding handler %s for method %s in path %s', cls, name, cls.path
|
||||
)
|
||||
service_node = Dispatcher.services # Root path
|
||||
if cls.path:
|
||||
for k in cls.path.split('/'):
|
||||
@@ -211,10 +252,14 @@ class Dispatcher(View):
|
||||
# Dinamycally import children of this package.
|
||||
package = 'methods'
|
||||
|
||||
pkgpath = os.path.join(os.path.dirname(sys.modules[__name__].__file__), package)
|
||||
pkgpath = os.path.join(
|
||||
os.path.dirname(typing.cast(str, sys.modules[__name__].__file__)), package
|
||||
)
|
||||
for _, name, _ in pkgutil.iter_modules([pkgpath]):
|
||||
# __import__(__name__ + '.' + package + '.' + name, globals(), locals(), [], 0)
|
||||
importlib.import_module( __name__ + '.' + package + '.' + name) # import module
|
||||
importlib.import_module(
|
||||
__name__ + '.' + package + '.' + name
|
||||
) # import module
|
||||
|
||||
importlib.invalidate_caches()
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2012-2019 Virtual Cable S.L.
|
||||
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
@@ -12,7 +11,7 @@
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
|
||||
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
@@ -28,13 +27,11 @@
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
Author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import datetime
|
||||
import typing
|
||||
import logging
|
||||
|
||||
from django.utils import timezone
|
||||
from django.contrib.sessions.backends.base import SessionBase
|
||||
from django.contrib.sessions.backends.db import SessionStore
|
||||
|
||||
@@ -44,10 +41,13 @@ from uds.core.util import net
|
||||
from uds.models import Authenticator, User
|
||||
from uds.core.managers import cryptoManager
|
||||
|
||||
from . import log
|
||||
|
||||
|
||||
# Not imported at runtime, just for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
from uds.core.util.request import ExtendedHttpRequest
|
||||
|
||||
from uds.core.util.request import ExtendedHttpRequestWithUser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
AUTH_TOKEN_HEADER = 'HTTP_X_AUTH_TOKEN'
|
||||
@@ -93,41 +93,71 @@ class Handler:
|
||||
"""
|
||||
REST requests handler base class
|
||||
"""
|
||||
raw: typing.ClassVar[bool] = False # If true, Handler will return directly an HttpResponse Object
|
||||
name: typing.ClassVar[typing.Optional[str]] = None # If name is not used, name will be the class name in lower case
|
||||
path: typing.ClassVar[typing.Optional[str]] = None # Path for this method, so we can do /auth/login, /auth/logout, /auth/auths in a simple way
|
||||
authenticated: typing.ClassVar[bool] = True # By default, all handlers needs authentication. Will be overwriten if needs_admin or needs_staff,
|
||||
needs_admin: typing.ClassVar[bool] = False # By default, the methods will be accessible by anyone if nothing else indicated
|
||||
|
||||
raw: typing.ClassVar[
|
||||
bool
|
||||
] = False # If true, Handler will return directly an HttpResponse Object
|
||||
name: typing.ClassVar[
|
||||
typing.Optional[str]
|
||||
] = None # If name is not used, name will be the class name in lower case
|
||||
path: typing.ClassVar[
|
||||
typing.Optional[str]
|
||||
] = None # Path for this method, so we can do /auth/login, /auth/logout, /auth/auths in a simple way
|
||||
authenticated: typing.ClassVar[
|
||||
bool
|
||||
] = True # By default, all handlers needs authentication. Will be overwriten if needs_admin or needs_staff,
|
||||
needs_admin: typing.ClassVar[
|
||||
bool
|
||||
] = False # By default, the methods will be accessible by anyone if nothing else indicated
|
||||
needs_staff: typing.ClassVar[bool] = False # By default, staff
|
||||
|
||||
_request: 'ExtendedHttpRequest' # It's a modified HttpRequest
|
||||
_request: 'ExtendedHttpRequestWithUser' # It's a modified HttpRequest
|
||||
_path: str
|
||||
_operation: str
|
||||
_params: typing.Any # This is a deserliazied object from request. Can be anything as 'a' or {'a': 1} or ....
|
||||
_args: typing.Tuple[str, ...] # This are the "path" split by /, that is, the REST invocation arguments
|
||||
_args: typing.Tuple[
|
||||
str, ...
|
||||
] # This are the "path" split by /, that is, the REST invocation arguments
|
||||
_kwargs: typing.Dict
|
||||
_headers: typing.Dict[str, str]
|
||||
_session: typing.Optional[SessionStore]
|
||||
_authToken: typing.Optional[str]
|
||||
_user: 'User'
|
||||
|
||||
|
||||
# method names: 'get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'
|
||||
def __init__(self, request: 'ExtendedHttpRequest', path: str, operation: str, params: typing.Any, *args: str, **kwargs):
|
||||
def __init__(
|
||||
self,
|
||||
request: 'ExtendedHttpRequestWithUser',
|
||||
path: str,
|
||||
method: str,
|
||||
params: typing.MutableMapping[str, typing.Any],
|
||||
*args: str,
|
||||
**kwargs
|
||||
):
|
||||
|
||||
logger.debug('Data: %s %s %s', self.__class__, self.needs_admin, self.authenticated)
|
||||
if (self.needs_admin or self.needs_staff) and not self.authenticated: # If needs_admin, must also be authenticated
|
||||
raise Exception('class {} is not authenticated but has needs_admin or needs_staff set!!'.format(self.__class__))
|
||||
logger.debug(
|
||||
'Data: %s %s %s', self.__class__, self.needs_admin, self.authenticated
|
||||
)
|
||||
if (
|
||||
self.needs_admin or self.needs_staff
|
||||
) and not self.authenticated: # If needs_admin, must also be authenticated
|
||||
raise Exception(
|
||||
'class {} is not authenticated but has needs_admin or needs_staff set!!'.format(
|
||||
self.__class__
|
||||
)
|
||||
)
|
||||
|
||||
self._request = request
|
||||
self._path = path
|
||||
self._operation = operation
|
||||
self._operation = method
|
||||
self._params = params
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
self._headers = {}
|
||||
self._authToken = None
|
||||
if self.authenticated: # Only retrieve auth related data on authenticated handlers
|
||||
if (
|
||||
self.authenticated
|
||||
): # Only retrieve auth related data on authenticated handlers
|
||||
try:
|
||||
self._authToken = self._request.META.get(AUTH_TOKEN_HEADER, '')
|
||||
self._session = SessionStore(session_key=self._authToken)
|
||||
@@ -191,16 +221,16 @@ class Handler:
|
||||
|
||||
@staticmethod
|
||||
def storeSessionAuthdata(
|
||||
session: SessionBase,
|
||||
id_auth: int,
|
||||
username: str,
|
||||
password: str,
|
||||
locale: str,
|
||||
platform: str,
|
||||
is_admin: bool,
|
||||
staff_member: bool,
|
||||
scrambler: str
|
||||
):
|
||||
session: SessionBase,
|
||||
id_auth: int,
|
||||
username: str,
|
||||
password: str,
|
||||
locale: str,
|
||||
platform: str,
|
||||
is_admin: bool,
|
||||
staff_member: bool,
|
||||
scrambler: str,
|
||||
):
|
||||
"""
|
||||
Stores the authentication data inside current session
|
||||
:param session: session handler (Djano user session object)
|
||||
@@ -220,20 +250,20 @@ class Handler:
|
||||
'locale': locale,
|
||||
'platform': platform,
|
||||
'is_admin': is_admin,
|
||||
'staff_member': staff_member
|
||||
'staff_member': staff_member,
|
||||
}
|
||||
|
||||
def genAuthToken(
|
||||
self,
|
||||
id_auth: int,
|
||||
username: str,
|
||||
password: str,
|
||||
locale: str,
|
||||
platform: str,
|
||||
is_admin: bool,
|
||||
staf_member: bool,
|
||||
scrambler: str
|
||||
):
|
||||
self,
|
||||
id_auth: int,
|
||||
username: str,
|
||||
password: str,
|
||||
locale: str,
|
||||
platform: str,
|
||||
is_admin: bool,
|
||||
staf_member: bool,
|
||||
scrambler: str,
|
||||
):
|
||||
"""
|
||||
Generates the authentication token from a session, that is basically
|
||||
the session key itself
|
||||
@@ -244,11 +274,21 @@ class Handler:
|
||||
:param staf_member: If user is considered staff member or not
|
||||
"""
|
||||
session = SessionStore()
|
||||
Handler.storeSessionAuthdata(session, id_auth, username, password, locale, platform, is_admin, staf_member, scrambler)
|
||||
Handler.storeSessionAuthdata(
|
||||
session,
|
||||
id_auth,
|
||||
username,
|
||||
password,
|
||||
locale,
|
||||
platform,
|
||||
is_admin,
|
||||
staf_member,
|
||||
scrambler,
|
||||
)
|
||||
session.save()
|
||||
self._authToken = session.session_key
|
||||
self._session = session
|
||||
|
||||
|
||||
return self._authToken
|
||||
|
||||
def cleanAuthToken(self) -> None:
|
||||
@@ -282,13 +322,20 @@ class Handler:
|
||||
self._session.accessed = True
|
||||
self._session.save()
|
||||
except Exception:
|
||||
logger.exception('Got an exception setting session value %s to %s', key, value)
|
||||
logger.exception(
|
||||
'Got an exception setting session value %s to %s', key, value
|
||||
)
|
||||
|
||||
def validSource(self) -> bool:
|
||||
try:
|
||||
return net.ipInNetwork(self._request.ip, GlobalConfig.ADMIN_TRUSTED_SOURCES.get(True))
|
||||
return net.ipInNetwork(
|
||||
self._request.ip, GlobalConfig.ADMIN_TRUSTED_SOURCES.get(True)
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning('Error checking truted ADMIN source: "%s" does not seems to be a valid network string. Using Unrestricted access.', GlobalConfig.ADMIN_TRUSTED_SOURCES.get())
|
||||
logger.warning(
|
||||
'Error checking truted ADMIN source: "%s" does not seems to be a valid network string. Using Unrestricted access.',
|
||||
GlobalConfig.ADMIN_TRUSTED_SOURCES.get(),
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -312,8 +359,10 @@ class Handler:
|
||||
authId = self.getValue('auth')
|
||||
username = self.getValue('username')
|
||||
# Maybe it's root user??
|
||||
if (GlobalConfig.SUPER_USER_ALLOW_WEBACCESS.getBool(True) and
|
||||
username == GlobalConfig.SUPER_USER_LOGIN.get(True) and
|
||||
authId == -1):
|
||||
if (
|
||||
GlobalConfig.SUPER_USER_ALLOW_WEBACCESS.getBool(True)
|
||||
and username == GlobalConfig.SUPER_USER_LOGIN.get(True)
|
||||
and authId == -1
|
||||
):
|
||||
return getRootUser()
|
||||
return Authenticator.objects.get(pk=authId).users.get(name=username)
|
||||
|
107
server/src/uds/REST/log.py
Normal file
107
server/src/uds/REST/log.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (c) 2022 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
"""
|
||||
Author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import typing
|
||||
|
||||
from uds import models
|
||||
from uds.core.util.log import (
|
||||
REST,
|
||||
OWNER_TYPE_AUDIT,
|
||||
DEBUG,
|
||||
INFO,
|
||||
WARNING,
|
||||
ERROR,
|
||||
CRITICAL,
|
||||
)
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .handlers import Handler
|
||||
|
||||
# This structct allows us to perform the following:
|
||||
# If path has ".../providers/[uuid]/..." we will replace uuid with "provider nanme" sourrounded by []
|
||||
# If path has ".../services/[uuid]/..." we will replace uuid with "service name" sourrounded by []
|
||||
# If path has ".../users/[uuid]/..." we will replace uuid with "user name" sourrounded by []
|
||||
# If path has ".../groups/[uuid]/..." we will replace uuid with "group name" sourrounded by []
|
||||
UUID_REPLACER = (
|
||||
('providers', models.Provider),
|
||||
('services', models.Service),
|
||||
('users', models.User),
|
||||
('groups', models.Group),
|
||||
)
|
||||
|
||||
|
||||
def replacePath(path: str) -> str:
|
||||
"""Replaces uuids in path with names
|
||||
All paths are in the form .../type/uuid/...
|
||||
"""
|
||||
for type, model in UUID_REPLACER:
|
||||
if f'/{type}/' in path:
|
||||
try:
|
||||
uuid = path.split(f'/{type}/')[1].split('/')[0]
|
||||
name = model.objects.get(uuid=uuid).name # type: ignore
|
||||
path = path.replace(uuid, f'[{name}]')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def log_operation(
|
||||
handler: typing.Optional['Handler'], response_code: int, level: int = INFO
|
||||
):
|
||||
"""
|
||||
Logs a request
|
||||
"""
|
||||
if not handler:
|
||||
return # Nothing to log
|
||||
|
||||
path = handler._request.path
|
||||
|
||||
# If a common request, and no error, we don't log it because it's useless and a waste of resources
|
||||
if response_code < 400 and any(
|
||||
x in path for x in ('overview', 'tableinfo', 'gui', 'types', 'system')
|
||||
):
|
||||
return
|
||||
|
||||
path = replacePath(path)
|
||||
|
||||
username = handler._request.user.pretty_name if handler._request.user else 'Unknown'
|
||||
# Global log is used without owner nor type
|
||||
models.Log.objects.create(
|
||||
owner_id=0,
|
||||
owner_type=OWNER_TYPE_AUDIT,
|
||||
created=models.getSqlDatetime(),
|
||||
level=level,
|
||||
source=REST,
|
||||
data=f'{handler._request.ip} {username}: [{handler._request.method}/{response_code}] {path}'[
|
||||
:255
|
||||
],
|
||||
)
|
@@ -50,6 +50,7 @@ class Accounts(ModelHandler):
|
||||
"""
|
||||
Processes REST requests about accounts
|
||||
"""
|
||||
|
||||
model = Account
|
||||
detail = {'usage': AccountsUsage}
|
||||
|
||||
@@ -72,7 +73,7 @@ class Accounts(ModelHandler):
|
||||
'tags': [tag.tag for tag in item.tags.all()],
|
||||
'comments': item.comments,
|
||||
'time_mark': item.time_mark,
|
||||
'permission': permissions.getEffectivePermission(self._user, item)
|
||||
'permission': permissions.getEffectivePermission(self._user, item),
|
||||
}
|
||||
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
|
@@ -70,7 +70,7 @@ class AccountsUsage(DetailHandler): # pylint: disable=too-many-public-methods
|
||||
'running': item.user_service is not None,
|
||||
'elapsed': item.elapsed,
|
||||
'elapsed_timemark': item.elapsed_timemark,
|
||||
'permission': perm
|
||||
'permission': perm,
|
||||
}
|
||||
|
||||
return retVal
|
||||
|
@@ -141,7 +141,7 @@ class Actor(Handler):
|
||||
except Exception:
|
||||
return Actor.result({})
|
||||
|
||||
def get(self): # pylint: disable=too-many-return-statements
|
||||
def get(self) -> typing.Any: # pylint: disable=too-many-return-statements
|
||||
"""
|
||||
Processes get requests
|
||||
"""
|
||||
@@ -186,7 +186,7 @@ class Actor(Handler):
|
||||
raise RequestError('Invalid request')
|
||||
|
||||
# Must be invoked as '/rest/actor/UUID/[message], with message data in post body
|
||||
def post(self): # pylint: disable=too-many-branches
|
||||
def post(self) -> typing.Any: # pylint: disable=too-many-branches
|
||||
"""
|
||||
Processes post requests
|
||||
"""
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2020 Virtual Cable S.L.U.
|
||||
# Copyright (c) 2021 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
@@ -64,7 +64,9 @@ class ActorTokens(ModelHandler):
|
||||
def item_as_dict(self, item: ActorToken) -> typing.Dict[str, typing.Any]:
|
||||
return {
|
||||
'id': item.token,
|
||||
'name': _('Token isued by {} from {}').format(item.username, item.hostname or item.ip),
|
||||
'name': _('Token isued by {} from {}').format(
|
||||
item.username, item.hostname or item.ip
|
||||
),
|
||||
'stamp': item.stamp,
|
||||
'username': item.username,
|
||||
'ip': item.ip,
|
||||
@@ -73,7 +75,7 @@ class ActorTokens(ModelHandler):
|
||||
'pre_command': item.pre_command,
|
||||
'post_command': item.post_command,
|
||||
'runonce_command': item.runonce_command,
|
||||
'log_level': ['DEBUG', 'INFO', 'ERROR', 'FATAL'][item.log_level%4]
|
||||
'log_level': ['DEBUG', 'INFO', 'ERROR', 'FATAL'][item.log_level % 4],
|
||||
}
|
||||
|
||||
def delete(self) -> str:
|
||||
@@ -83,7 +85,9 @@ class ActorTokens(ModelHandler):
|
||||
if len(self._args) != 1:
|
||||
raise RequestError('Delete need one and only one argument')
|
||||
|
||||
self.ensureAccess(self.model(), permissions.PERMISSION_ALL, root=True) # Must have write permissions to delete
|
||||
self.ensureAccess(
|
||||
self.model(), permissions.PERMISSION_ALL, root=True
|
||||
) # Must have write permissions to delete
|
||||
|
||||
try:
|
||||
self.model.objects.get(token=self._args[0]).delete()
|
||||
|
@@ -41,10 +41,10 @@ from uds.models import (
|
||||
TicketStore,
|
||||
)
|
||||
|
||||
#from uds.core import VERSION
|
||||
# from uds.core import VERSION
|
||||
from uds.core.managers import userServiceManager
|
||||
from uds.core import osmanagers
|
||||
from uds.core.util import log, certs
|
||||
from uds.core.util import log, security
|
||||
from uds.core.util.state import State
|
||||
from uds.core.util.cache import Cache
|
||||
from uds.core.util.config import GlobalConfig
|
||||
@@ -54,6 +54,7 @@ from ..handlers import Handler, AccessDenied, RequestError
|
||||
# Not imported at runtime, just for type checking
|
||||
if typing.TYPE_CHECKING:
|
||||
from uds.core import services
|
||||
from uds.core.util.request import ExtendedHttpRequest
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -64,23 +65,29 @@ UNMANAGED = 'unmanaged' # matches the definition of UDS Actors OFC
|
||||
class BlockAccess(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# Helpers
|
||||
def fixIdsList(idsList: typing.List[str]) -> typing.List[str]:
|
||||
return [i.upper() for i in idsList] + [i.lower() for i in idsList]
|
||||
|
||||
|
||||
def checkBlockedIp(ip: str) -> None:
|
||||
def checkBlockedIp(request: 'ExtendedHttpRequest') -> None:
|
||||
if GlobalConfig.BLOCK_ACTOR_FAILURES.getBool() is False:
|
||||
return
|
||||
cache = Cache('actorv3')
|
||||
fails = cache.get(ip) or 0
|
||||
fails = cache.get(request.ip) or 0
|
||||
if fails > ALLOWED_FAILS:
|
||||
logger.info('Access to actor from %s is blocked for %s seconds since last fail', ip, GlobalConfig.LOGIN_BLOCK.getInt())
|
||||
err = f'DENIED Access to actor from {request.ip}. Blocked for {GlobalConfig.LOGIN_BLOCK.getInt()} seconds since last fail.'
|
||||
# if request.ip_proxy is not request.ip, notify so administrator can figure out what is going on
|
||||
if request.ip_proxy != request.ip:
|
||||
err += f' Proxied ip is present: {request.ip_proxy}.'
|
||||
logger.warning(err)
|
||||
raise BlockAccess()
|
||||
|
||||
|
||||
def incFailedIp(ip: str) -> None:
|
||||
def incFailedIp(request: 'ExtendedHttpRequest') -> None:
|
||||
cache = Cache('actorv3')
|
||||
fails = (cache.get(ip) or 0) + 1
|
||||
cache.put(ip, fails, GlobalConfig.LOGIN_BLOCK.getInt())
|
||||
fails = cache.get(request.ip, 0) + 1
|
||||
cache.put(request.ip, fails, GlobalConfig.LOGIN_BLOCK.getInt())
|
||||
|
||||
|
||||
class ActorV3Action(Handler):
|
||||
@@ -88,7 +95,9 @@ class ActorV3Action(Handler):
|
||||
path = 'actor/v3'
|
||||
|
||||
@staticmethod
|
||||
def actorResult(result: typing.Any = None, error: typing.Optional[str] = None) -> typing.MutableMapping[str, typing.Any]:
|
||||
def actorResult(
|
||||
result: typing.Any = None, error: typing.Optional[str] = None
|
||||
) -> typing.MutableMapping[str, typing.Any]:
|
||||
result = result or ''
|
||||
res = {'result': result, 'stamp': getSqlDatetimeAsUnix()}
|
||||
if error:
|
||||
@@ -106,6 +115,7 @@ class ActorV3Action(Handler):
|
||||
try:
|
||||
return UserService.objects.get(uuid=self._params['token'])
|
||||
except UserService.DoesNotExist:
|
||||
logger.error('User service not found (params: %s)', self._params)
|
||||
raise BlockAccess()
|
||||
|
||||
def action(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
@@ -113,13 +123,13 @@ class ActorV3Action(Handler):
|
||||
|
||||
def post(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
try:
|
||||
checkBlockedIp(self._request.ip) # pylint: disable=protected-access
|
||||
checkBlockedIp(self._request)
|
||||
result = self.action()
|
||||
logger.debug('Action result: %s', result)
|
||||
return result
|
||||
except (BlockAccess, KeyError):
|
||||
# For blocking attacks
|
||||
incFailedIp(self._request.ip) # pylint: disable=protected-access
|
||||
incFailedIp(self._request)
|
||||
except Exception as e:
|
||||
logger.exception('Posting %s: %s', self.__class__, e)
|
||||
|
||||
@@ -130,6 +140,7 @@ class Test(ActorV3Action):
|
||||
"""
|
||||
Tests UDS Broker actor connectivity & key
|
||||
"""
|
||||
|
||||
name = 'test'
|
||||
|
||||
def post(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
@@ -138,7 +149,9 @@ class Test(ActorV3Action):
|
||||
if self._params.get('type') == UNMANAGED:
|
||||
Service.objects.get(token=self._params['token'])
|
||||
else:
|
||||
ActorToken.objects.get(token=self._params['token']) # Not assigned, because only needs check
|
||||
ActorToken.objects.get(
|
||||
token=self._params['token']
|
||||
) # Not assigned, because only needs check
|
||||
except Exception:
|
||||
return ActorV3Action.actorResult('invalid token')
|
||||
|
||||
@@ -149,6 +162,7 @@ class Register(ActorV3Action):
|
||||
"""
|
||||
Registers an actor
|
||||
"""
|
||||
|
||||
authenticated = True
|
||||
needs_staff = True
|
||||
|
||||
@@ -170,6 +184,7 @@ class Register(ActorV3Action):
|
||||
actorToken.log_level = self._params['log_level']
|
||||
actorToken.stamp = getSqlDatetime()
|
||||
actorToken.save()
|
||||
logger.info('Registered actor %s', self._params)
|
||||
except Exception:
|
||||
actorToken = ActorToken.objects.create(
|
||||
username=self._user.pretty_name,
|
||||
@@ -182,16 +197,17 @@ class Register(ActorV3Action):
|
||||
runonce_command=self._params['run_once_command'],
|
||||
log_level=self._params['log_level'],
|
||||
token=secrets.token_urlsafe(36),
|
||||
stamp=getSqlDatetime()
|
||||
stamp=getSqlDatetime(),
|
||||
)
|
||||
return ActorV3Action.actorResult(actorToken.token)
|
||||
|
||||
|
||||
class Initiialize(ActorV3Action):
|
||||
class Initialize(ActorV3Action):
|
||||
"""
|
||||
Information about machine action.
|
||||
Also returns the id used for the rest of the actions. (Only this one will use actor key)
|
||||
"""
|
||||
|
||||
name = 'initialize'
|
||||
|
||||
def action(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
@@ -228,38 +244,43 @@ class Initiialize(ActorV3Action):
|
||||
"""
|
||||
# First, validate token...
|
||||
logger.debug('Args: %s, Params: %s', self._args, self._params)
|
||||
service: typing.Optional[Service] = None
|
||||
try:
|
||||
# First, try to locate an user service providing this token.
|
||||
if self._params['type'] == UNMANAGED:
|
||||
# If unmanaged, use Service locator
|
||||
service: Service = Service.objects.get(token=self._params['token'])
|
||||
service = Service.objects.get(token=self._params['token'])
|
||||
# Locate an userService that belongs to this service and which
|
||||
# Build the possible ids and make initial filter to match service
|
||||
idsList = [x['ip'] for x in self._params['id']] + [x['mac'] for x in self._params['id']][:10]
|
||||
idsList = [x['ip'] for x in self._params['id']] + [
|
||||
x['mac'] for x in self._params['id']
|
||||
][:10]
|
||||
dbFilter = UserService.objects.filter(deployed_service__service=service)
|
||||
else:
|
||||
# If not service provided token, use actor tokens
|
||||
ActorToken.objects.get(token=self._params['token']) # Not assigned, because only needs check
|
||||
ActorToken.objects.get(
|
||||
token=self._params['token']
|
||||
) # Not assigned, because only needs check
|
||||
# Build the possible ids and make initial filter to match ANY userservice with provided MAC
|
||||
idsList = [i['mac'] for i in self._params['id'][:5]]
|
||||
dbFilter = UserService.objects.all()
|
||||
|
||||
# Valid actor token, now validate access allowed. That is, look for a valid mac from the ones provided.
|
||||
try:
|
||||
userService: UserService = next(
|
||||
iter(dbFilter.filter(
|
||||
unique_id__in=idsList,
|
||||
state__in=[State.USABLE, State.PREPARING]
|
||||
))
|
||||
# ensure idsLists has upper and lower versions for case sensitive databases
|
||||
idsList = fixIdsList(idsList)
|
||||
# Set full filter
|
||||
dbFilter = dbFilter.filter(
|
||||
unique_id__in=idsList,
|
||||
state__in=[State.USABLE, State.PREPARING],
|
||||
)
|
||||
|
||||
userService: UserService = next(iter(dbFilter))
|
||||
except Exception as e:
|
||||
logger.info('Unmanaged host request: %s, %s', self._params, e)
|
||||
return ActorV3Action.actorResult({
|
||||
'own_token': None,
|
||||
'max_idle': None,
|
||||
'unique_id': None,
|
||||
'os': None
|
||||
})
|
||||
return ActorV3Action.actorResult(
|
||||
{'own_token': None, 'max_idle': None, 'unique_id': None, 'os': None}
|
||||
)
|
||||
|
||||
# Managed by UDS, get initialization data from osmanager and return it
|
||||
# Set last seen actor version
|
||||
@@ -269,11 +290,13 @@ class Initiialize(ActorV3Action):
|
||||
if osManager:
|
||||
osData = osManager.actorData(userService)
|
||||
|
||||
return ActorV3Action.actorResult({
|
||||
'own_token': userService.uuid,
|
||||
'unique_id': userService.unique_id,
|
||||
'os': osData
|
||||
})
|
||||
return ActorV3Action.actorResult(
|
||||
{
|
||||
'own_token': userService.uuid,
|
||||
'unique_id': userService.unique_id,
|
||||
'os': osData,
|
||||
}
|
||||
)
|
||||
except (ActorToken.DoesNotExist, Service.DoesNotExist):
|
||||
raise BlockAccess()
|
||||
|
||||
@@ -282,6 +305,7 @@ class BaseReadyChange(ActorV3Action):
|
||||
"""
|
||||
Records the IP change of actor
|
||||
"""
|
||||
|
||||
name = 'notused' # Not really important, this is not a "leaf" class and will not be directly available
|
||||
|
||||
def action(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
@@ -309,7 +333,12 @@ class BaseReadyChange(ActorV3Action):
|
||||
userService.updateData(userServiceInstance)
|
||||
|
||||
# Store communications url also
|
||||
ActorV3Action.setCommsUrl(userService, self._params['ip'], int(self._params['port']), self._params['secret'])
|
||||
ActorV3Action.setCommsUrl(
|
||||
userService,
|
||||
self._params['ip'],
|
||||
int(self._params['port']),
|
||||
self._params['secret'],
|
||||
)
|
||||
|
||||
if userService.os_state != State.USABLE:
|
||||
userService.setOsState(State.USABLE)
|
||||
@@ -321,19 +350,26 @@ class BaseReadyChange(ActorV3Action):
|
||||
userServiceManager().notifyReadyFromOsManager(userService, '')
|
||||
|
||||
# Generates a certificate and send it to client.
|
||||
privateKey, cert, password = certs.selfSignedCert(self._params['ip'])
|
||||
privateKey, cert, password = security.selfSignedCert(self._params['ip'])
|
||||
# Store certificate with userService
|
||||
userService.setProperty('cert', cert)
|
||||
userService.setProperty('priv', privateKey)
|
||||
userService.setProperty('priv_passwd', password)
|
||||
|
||||
return ActorV3Action.actorResult({'private_key': privateKey, 'server_certificate': cert, 'password': password})
|
||||
return ActorV3Action.actorResult(
|
||||
{
|
||||
'private_key': privateKey,
|
||||
'server_certificate': cert,
|
||||
'password': password,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class IpChange(BaseReadyChange):
|
||||
"""
|
||||
Processses IP Change.
|
||||
"""
|
||||
|
||||
name = 'ipchange'
|
||||
|
||||
|
||||
@@ -341,6 +377,7 @@ class Ready(BaseReadyChange):
|
||||
"""
|
||||
Notifies the user service is ready
|
||||
"""
|
||||
|
||||
name = 'ready'
|
||||
|
||||
def action(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
@@ -371,6 +408,7 @@ class Version(ActorV3Action):
|
||||
Notifies the version.
|
||||
Used on possible "customized" actors.
|
||||
"""
|
||||
|
||||
name = 'version'
|
||||
|
||||
def action(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
@@ -381,16 +419,26 @@ class Version(ActorV3Action):
|
||||
|
||||
return ActorV3Action.actorResult()
|
||||
|
||||
|
||||
class LoginLogout(ActorV3Action):
|
||||
name = 'notused' # Not really important, this is not a "leaf" class and will not be directly available
|
||||
|
||||
def notifyService(self, login: bool):
|
||||
def notifyService(self, isLogin: bool) -> None:
|
||||
try:
|
||||
# If unmanaged, use Service locator
|
||||
service : 'services.Service' = Service.objects.get(token=self._params['token']).getInstance()
|
||||
# Locate an userService that belongs to this service and which
|
||||
service: 'services.Service' = Service.objects.get(
|
||||
token=self._params['token']
|
||||
).getInstance()
|
||||
|
||||
# We have a valid service, now we can make notifications
|
||||
|
||||
# Build the possible ids and make initial filter to match service
|
||||
idsList = [x['ip'] for x in self._params['id']] + [x['mac'] for x in self._params['id']][:10]
|
||||
idsList = [x['ip'] for x in self._params['id']] + [
|
||||
x['mac'] for x in self._params['id']
|
||||
][:10]
|
||||
|
||||
# ensure idsLists has upper and lower versions for case sensitive databases
|
||||
idsList = fixIdsList(idsList)
|
||||
|
||||
validId: typing.Optional[str] = service.getValidId(idsList)
|
||||
|
||||
@@ -398,22 +446,21 @@ class LoginLogout(ActorV3Action):
|
||||
if not validId:
|
||||
raise Exception()
|
||||
|
||||
# Check secret if is stored
|
||||
storedInfo : typing.Optional[typing.MutableMapping[str, typing.Any]] = service.recoverIdInfo(validId)
|
||||
# If no secret valid
|
||||
if not storedInfo or self._params['secret'] != storedInfo['secret']:
|
||||
raise Exception()
|
||||
# Recover Id Info from service and validId
|
||||
# idInfo = service.recoverIdInfo(validId)
|
||||
|
||||
# Notify Service that someone logged in/out
|
||||
if login:
|
||||
is_remote = self._params.get('session_type', '')[:4] in ('xrdp', 'RDP-')
|
||||
if isLogin:
|
||||
# Try to guess if this is a remote session
|
||||
is_remote = self._params.get('session_type', '')[:3] in ('xrdp', 'RDP-')
|
||||
service.processLogin(validId, remote_login=is_remote)
|
||||
else:
|
||||
service.processLogout(validId)
|
||||
service.processLogout(validId, remote_login=is_remote)
|
||||
|
||||
# All right, service notified...
|
||||
except Exception:
|
||||
# All right, service notified..
|
||||
except Exception as e :
|
||||
# Log error and continue
|
||||
logger.error('Error notifying service: %s (%s)', e, self._params)
|
||||
raise BlockAccess()
|
||||
|
||||
|
||||
@@ -421,12 +468,29 @@ class Login(LoginLogout):
|
||||
"""
|
||||
Notifies user logged id
|
||||
"""
|
||||
|
||||
name = 'login'
|
||||
|
||||
# payload received
|
||||
# {
|
||||
# 'type': actor_type or types.MANAGED,
|
||||
# 'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
|
||||
# 'token': token,
|
||||
# 'username': username,
|
||||
# 'session_type': sessionType,
|
||||
# 'secret': secret or '',
|
||||
# }
|
||||
|
||||
@staticmethod
|
||||
def process_login(userService: UserService, username: str) -> typing.Optional[osmanagers.OSManager]:
|
||||
osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance()
|
||||
if not userService.in_use: # If already logged in, do not add a second login (windows does this i.e.)
|
||||
def process_login(
|
||||
userService: UserService, username: str
|
||||
) -> typing.Optional[osmanagers.OSManager]:
|
||||
osManager: typing.Optional[
|
||||
osmanagers.OSManager
|
||||
] = userService.getOsManagerInstance()
|
||||
if (
|
||||
not userService.in_use
|
||||
): # If already logged in, do not add a second login (windows does this i.e.)
|
||||
osmanagers.OSManager.loggedIn(userService, username)
|
||||
return osManager
|
||||
|
||||
@@ -439,7 +503,9 @@ class Login(LoginLogout):
|
||||
|
||||
try:
|
||||
userService: UserService = self.getUserService()
|
||||
osManager = Login.process_login(userService, self._params.get('username') or '')
|
||||
osManager = Login.process_login(
|
||||
userService, self._params.get('username') or ''
|
||||
)
|
||||
|
||||
maxIdle = osManager.maxIdle() if osManager else None
|
||||
|
||||
@@ -458,30 +524,31 @@ class Login(LoginLogout):
|
||||
except Exception: # If unamanaged host, lest do a bit more work looking for a service with the provided parameters...
|
||||
if isManaged:
|
||||
raise
|
||||
self.notifyService(login=True)
|
||||
self.notifyService(isLogin=True)
|
||||
|
||||
|
||||
return ActorV3Action.actorResult({
|
||||
'ip': ip,
|
||||
'hostname': hostname,
|
||||
'dead_line': deadLine,
|
||||
'max_idle': maxIdle
|
||||
})
|
||||
return ActorV3Action.actorResult(
|
||||
{'ip': ip, 'hostname': hostname, 'dead_line': deadLine, 'max_idle': maxIdle}
|
||||
)
|
||||
|
||||
|
||||
class Logout(LoginLogout):
|
||||
"""
|
||||
Notifies user logged out
|
||||
"""
|
||||
|
||||
name = 'logout'
|
||||
|
||||
@staticmethod
|
||||
def process_logout(userService: UserService, username: str) -> None:
|
||||
def process_logout(userService: UserService, username: str) -> None:
|
||||
"""
|
||||
This method is static so can be invoked from elsewhere
|
||||
"""
|
||||
osManager: typing.Optional[osmanagers.OSManager] = userService.getOsManagerInstance()
|
||||
if userService.in_use: # If already logged out, do not add a second logout (windows does this i.e.)
|
||||
osManager: typing.Optional[
|
||||
osmanagers.OSManager
|
||||
] = userService.getOsManagerInstance()
|
||||
if (
|
||||
userService.in_use
|
||||
): # If already logged out, do not add a second logout (windows does this i.e.)
|
||||
osmanagers.OSManager.loggedOut(userService, username)
|
||||
if osManager:
|
||||
if osManager.isRemovableOnLogout(userService):
|
||||
@@ -490,7 +557,6 @@ class Logout(LoginLogout):
|
||||
else:
|
||||
userService.remove()
|
||||
|
||||
|
||||
def action(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
isManaged = self._params.get('type') != UNMANAGED
|
||||
|
||||
@@ -501,7 +567,8 @@ class Logout(LoginLogout):
|
||||
except Exception: # If unamanaged host, lest do a bit more work looking for a service with the provided parameters...
|
||||
if isManaged:
|
||||
raise
|
||||
self.notifyService(login=False) # Logout notification
|
||||
self.notifyService(isLogin=False) # Logout notification
|
||||
return ActorV3Action.actorResult('notified') # Result is that we have not processed the logout in fact, but notified the service
|
||||
|
||||
return ActorV3Action.actorResult('ok')
|
||||
|
||||
@@ -510,13 +577,19 @@ class Log(ActorV3Action):
|
||||
"""
|
||||
Sends a log from the service
|
||||
"""
|
||||
|
||||
name = 'log'
|
||||
|
||||
def action(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
logger.debug('Args: %s, Params: %s', self._args, self._params)
|
||||
userService = self.getUserService()
|
||||
# Adjust loglevel to own, we start on 10000 for OTHER, and received is 0 for OTHER
|
||||
log.doLog(userService, int(self._params['level']) + 10000, self._params['message'], log.ACTOR)
|
||||
log.doLog(
|
||||
userService,
|
||||
int(self._params['level']) + 10000,
|
||||
self._params['message'],
|
||||
log.ACTOR,
|
||||
)
|
||||
|
||||
return ActorV3Action.actorResult('ok')
|
||||
|
||||
@@ -525,6 +598,7 @@ class Ticket(ActorV3Action):
|
||||
"""
|
||||
Gets an stored ticket
|
||||
"""
|
||||
|
||||
name = 'ticket'
|
||||
|
||||
def action(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
@@ -532,12 +606,16 @@ class Ticket(ActorV3Action):
|
||||
|
||||
try:
|
||||
# Simple check that token exists
|
||||
ActorToken.objects.get(token=self._params['token']) # Not assigned, because only needs check
|
||||
ActorToken.objects.get(
|
||||
token=self._params['token']
|
||||
) # Not assigned, because only needs check
|
||||
except ActorToken.DoesNotExist:
|
||||
raise BlockAccess() # If too many blocks...
|
||||
|
||||
try:
|
||||
return ActorV3Action.actorResult(TicketStore.get(self._params['ticket'], invalidate=True))
|
||||
return ActorV3Action.actorResult(
|
||||
TicketStore.get(self._params['ticket'], invalidate=True)
|
||||
)
|
||||
except TicketStore.DoesNotExist:
|
||||
return ActorV3Action.actorResult(error='Invalid ticket')
|
||||
|
||||
@@ -550,7 +628,7 @@ class Unmanaged(ActorV3Action):
|
||||
unmanaged method expect a json POST with this fields:
|
||||
* id: List[dict] -> List of dictionary containing ip and mac:
|
||||
* token: str -> Valid Actor "master_token" (if invalid, will return an error).
|
||||
* secret: Secret for commsUrl for actor
|
||||
* secret: Secret for commsUrl for actor (Cu
|
||||
* port: port of the listener (normally 43910)
|
||||
|
||||
This method will also regenerater the public-private key pair for client, that will be needed for the new ip
|
||||
@@ -570,31 +648,69 @@ class Unmanaged(ActorV3Action):
|
||||
|
||||
# Build the possible ids and ask service if it recognizes any of it
|
||||
# If not recognized, will generate anyway the certificate, but will not be saved
|
||||
idsList = [x['ip'] for x in self._params['id']] + [x['mac'] for x in self._params['id']][:10]
|
||||
idsList = [x['ip'] for x in self._params['id']] + [
|
||||
x['mac'] for x in self._params['id']
|
||||
][:10]
|
||||
validId: typing.Optional[str] = service.getValidId(idsList)
|
||||
|
||||
# ensure idsLists has upper and lower versions for case sensitive databases
|
||||
idsList = fixIdsList(idsList)
|
||||
|
||||
# Check if there is already an assigned user service
|
||||
# To notify it logout
|
||||
userService: typing.Optional[UserService]
|
||||
try:
|
||||
dbFilter = UserService.objects.filter(
|
||||
unique_id__in=idsList,
|
||||
state__in=[State.USABLE, State.PREPARING],
|
||||
)
|
||||
|
||||
userService = next(
|
||||
iter(
|
||||
dbFilter.filter(
|
||||
unique_id__in=idsList,
|
||||
state__in=[State.USABLE, State.PREPARING],
|
||||
)
|
||||
)
|
||||
)
|
||||
except StopIteration:
|
||||
userService = None
|
||||
|
||||
# Try to infer the ip from the valid id (that could be an IP or a MAC)
|
||||
ip: str
|
||||
try:
|
||||
ip = next(x['ip'] for x in self._params['id'] if x['ip'] == validId or x['mac'] == validId)
|
||||
ip = next(
|
||||
x['ip']
|
||||
for x in self._params['id']
|
||||
if x['ip'] == validId or x['mac'] == validId
|
||||
)
|
||||
except StopIteration:
|
||||
ip = self._params['id'][0]['ip'] # Get first IP if no valid ip found
|
||||
|
||||
# Generates a certificate and send it to client.
|
||||
privateKey, certificate, password = certs.selfSignedCert(ip)
|
||||
privateKey, certificate, password = security.selfSignedCert(ip)
|
||||
cert: typing.Dict[str, str] = {
|
||||
'private_key': privateKey,
|
||||
'server_certificate': certificate,
|
||||
'password': password
|
||||
'password': password,
|
||||
}
|
||||
if validId:
|
||||
# Notify service of it "just start" action
|
||||
service.notifyInitialization(validId)
|
||||
# If id is assigned to an user service, notify "logout" to it
|
||||
if userService:
|
||||
Logout.process_logout(userService, 'init')
|
||||
else:
|
||||
# If it is not assgined to an user service, notify service
|
||||
service.notifyInitialization(validId)
|
||||
|
||||
# Store certificate, secret & port with service if validId
|
||||
service.storeIdInfo(validId, {
|
||||
'cert': certificate,
|
||||
'secret': self._params['secret'],
|
||||
'port': int(self._params['port'])
|
||||
})
|
||||
service.storeIdInfo(
|
||||
validId,
|
||||
{
|
||||
'cert': certificate,
|
||||
'secret': self._params['secret'],
|
||||
'port': int(self._params['port']),
|
||||
},
|
||||
)
|
||||
|
||||
return ActorV3Action.actorResult(cert)
|
||||
|
||||
@@ -608,14 +724,18 @@ class Notify(ActorV3Action):
|
||||
|
||||
def get(self) -> typing.MutableMapping[str, typing.Any]:
|
||||
logger.debug('Args: %s, Params: %s', self._args, self._params)
|
||||
if 'action' not in self._params or 'token' not in self._params or self._params['action'] not in ('login', 'logout'):
|
||||
if (
|
||||
'action' not in self._params
|
||||
or 'token' not in self._params
|
||||
or self._params['action'] not in ('login', 'logout')
|
||||
):
|
||||
# Requested login or logout
|
||||
raise RequestError('Invalid parameters')
|
||||
|
||||
try:
|
||||
# Check block manually
|
||||
checkBlockedIp(self._request.ip) # pylint: disable=protected-access
|
||||
if 'action' == 'login':
|
||||
checkBlockedIp(self._request) # pylint: disable=protected-access
|
||||
if self._params['action'] == 'login':
|
||||
Login.action(typing.cast(Login, self))
|
||||
else:
|
||||
Logout.action(typing.cast(Logout, self))
|
||||
@@ -623,6 +743,6 @@ class Notify(ActorV3Action):
|
||||
return ActorV3Action.actorResult('ok')
|
||||
except UserService.DoesNotExist:
|
||||
# For blocking attacks
|
||||
incFailedIp(self._request.ip) # pylint: disable=protected-access
|
||||
incFailedIp(self._request) # pylint: disable=protected-access
|
||||
|
||||
raise AccessDenied('Access denied')
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2014-2019 Virtual Cable S.L.
|
||||
# Copyright (c) 2014-2022 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
@@ -30,16 +30,18 @@
|
||||
"""
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from django.utils.translation import ugettext, ugettext_lazy as _
|
||||
from uds.models import Authenticator
|
||||
from uds.models import Authenticator, MFA
|
||||
from uds.core import auths
|
||||
|
||||
from uds.REST import NotFound
|
||||
from uds.REST.model import ModelHandler
|
||||
from uds.core.util import permissions
|
||||
from uds.core.util.model import processUuid
|
||||
from uds.core.ui import gui
|
||||
|
||||
from .users_groups import Users, Groups
|
||||
@@ -58,7 +60,7 @@ class Authenticators(ModelHandler):
|
||||
# Custom get method "search" that requires authenticator id
|
||||
custom_methods = [('search', True)]
|
||||
detail = {'users': Users, 'groups': Groups}
|
||||
save_fields = ['name', 'comments', 'tags', 'priority', 'small_name', 'visible']
|
||||
save_fields = ['name', 'comments', 'tags', 'priority', 'small_name', 'visible', 'mfa_id:'] # mfa_id is optional, and defaults to '' (no mfa)
|
||||
|
||||
table_title = _('Authenticators')
|
||||
table_fields = [
|
||||
@@ -70,6 +72,7 @@ class Authenticators(ModelHandler):
|
||||
{'visible': {'title': _('Visible'), 'type': 'callback', 'width': '3em'}},
|
||||
{'small_name': {'title': _('Label')}},
|
||||
{'users_count': {'title': _('Users'), 'type': 'numeric', 'width': '5em'}},
|
||||
{'mfa_name': {'title': _('MFA'),}},
|
||||
{'tags': {'title': _('tags'), 'visible': False}},
|
||||
]
|
||||
|
||||
@@ -87,16 +90,17 @@ class Authenticators(ModelHandler):
|
||||
'passwordLabel': _(type_.passwordLabel),
|
||||
'canCreateUsers': type_.createUser != auths.Authenticator.createUser, # type: ignore
|
||||
'isExternal': type_.isExternalSource,
|
||||
'supportsMFA': type_.providesMfa(),
|
||||
}
|
||||
# Not of my type
|
||||
return {}
|
||||
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
try:
|
||||
tgui = auths.factory().lookup(type_)
|
||||
if tgui:
|
||||
authType = auths.factory().lookup(type_)
|
||||
if authType:
|
||||
g = self.addDefaultFields(
|
||||
tgui.guiDescription(),
|
||||
authType.guiDescription(),
|
||||
['name', 'comments', 'tags', 'priority', 'small_name'],
|
||||
)
|
||||
self.addField(
|
||||
@@ -106,16 +110,39 @@ class Authenticators(ModelHandler):
|
||||
'value': True,
|
||||
'label': ugettext('Visible'),
|
||||
'tooltip': ugettext(
|
||||
'If active, transport will be visible for users'
|
||||
'If active, authenticator will be visible for users'
|
||||
),
|
||||
'type': gui.InputField.CHECKBOX_TYPE,
|
||||
'order': 107,
|
||||
'tab': ugettext('Display'),
|
||||
'tab': gui.DISPLAY_TAB,
|
||||
},
|
||||
)
|
||||
# If supports mfa, add MFA provider selector field
|
||||
if authType.providesMfa():
|
||||
self.addField(
|
||||
g,
|
||||
{
|
||||
'name': 'mfa_id',
|
||||
'values': [gui.choiceItem('', _('None'))]
|
||||
+ gui.sortedChoices(
|
||||
[
|
||||
gui.choiceItem(v.uuid, v.name) # type: ignore
|
||||
for v in MFA.objects.all()
|
||||
]
|
||||
),
|
||||
'label': ugettext('MFA Provider'),
|
||||
'tooltip': ugettext(
|
||||
'MFA provider to use for this authenticator'
|
||||
),
|
||||
'type': gui.InputField.CHOICE_TYPE,
|
||||
'order': 108,
|
||||
'tab': gui.MFA_TAB,
|
||||
},
|
||||
)
|
||||
return g
|
||||
raise Exception() # Not found
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
logger.info('Type not found: %s', e)
|
||||
raise NotFound('type not found')
|
||||
|
||||
def item_as_dict(self, item: Authenticator) -> typing.Dict[str, typing.Any]:
|
||||
@@ -127,6 +154,8 @@ class Authenticators(ModelHandler):
|
||||
'tags': [tag.tag for tag in item.tags.all()],
|
||||
'comments': item.comments,
|
||||
'priority': item.priority,
|
||||
'mfa_id': item.mfa.uuid if item.mfa else '',
|
||||
'mfa_name': item.mfa.name if item.mfa else '', # For overview
|
||||
'visible': item.visible,
|
||||
'small_name': item.small_name,
|
||||
'users_count': item.users.count(),
|
||||
@@ -182,6 +211,29 @@ class Authenticators(ModelHandler):
|
||||
return self.success()
|
||||
return res[1]
|
||||
|
||||
def beforeSave(
|
||||
self, fields: typing.Dict[str, typing.Any]
|
||||
) -> None: # pylint: disable=too-many-branches,too-many-statements
|
||||
logger.debug(self._params)
|
||||
if fields.get('mfa_id'):
|
||||
try:
|
||||
mfa = MFA.objects.get(
|
||||
uuid=processUuid(fields['mfa_id'])
|
||||
)
|
||||
fields['mfa_id'] = mfa.id
|
||||
except MFA.DoesNotExist:
|
||||
pass # will set field to null
|
||||
else:
|
||||
fields['mfa_id'] = None
|
||||
|
||||
fields['small_name'] = fields['small_name'].strip().replace(' ', '-')
|
||||
# And ensure small_name chars are valid [ a-zA-Z0-9:-.]+
|
||||
if fields['small_name'] and not re.match(r'^[a-zA-Z0-9:.-]+$', fields['small_name']):
|
||||
raise self.invalidRequestException(
|
||||
_('Label must contain only letters, numbers, or symbols: - : .')
|
||||
)
|
||||
|
||||
|
||||
def deleteItem(self, item: Authenticator):
|
||||
# For every user, remove assigned services (mark them for removal)
|
||||
|
||||
|
@@ -32,7 +32,7 @@
|
||||
"""
|
||||
import logging
|
||||
|
||||
from django.core.cache import cache as djCache
|
||||
from django.core.cache import caches
|
||||
from uds.core.util.cache import Cache as uCache
|
||||
from uds.REST import Handler, RequestError
|
||||
|
||||
@@ -57,5 +57,9 @@ class Cache(Handler):
|
||||
raise RequestError('Invalid Request')
|
||||
|
||||
uCache.purge()
|
||||
djCache.clear()
|
||||
for i in ('default', 'memory'):
|
||||
try:
|
||||
caches[i].clear()
|
||||
except Exception:
|
||||
pass # Ignore non existing cache
|
||||
return 'done'
|
||||
|
@@ -75,7 +75,7 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
|
||||
'interval': item.interval,
|
||||
'duration': item.duration,
|
||||
'duration_unit': item.duration_unit,
|
||||
'permission': perm
|
||||
'permission': perm,
|
||||
}
|
||||
|
||||
return retVal
|
||||
@@ -98,7 +98,13 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
|
||||
{'name': {'title': _('Rule name')}},
|
||||
{'start': {'title': _('Starts'), 'type': 'datetime'}},
|
||||
{'end': {'title': _('Ends'), 'type': 'date'}},
|
||||
{'frequency': {'title': _('Repeats'), 'type': 'dict', 'dict': dict((v[0], str(v[1])) for v in freqs)}},
|
||||
{
|
||||
'frequency': {
|
||||
'title': _('Repeats'),
|
||||
'type': 'dict',
|
||||
'dict': dict((v[0], str(v[1])) for v in freqs),
|
||||
}
|
||||
},
|
||||
{'interval': {'title': _('Every'), 'type': 'callback'}},
|
||||
{'duration': {'title': _('Duration'), 'type': 'callback'}},
|
||||
{'comments': {'title': _('Comments')}},
|
||||
@@ -108,7 +114,18 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
|
||||
# Extract item db fields
|
||||
# We need this fields for all
|
||||
logger.debug('Saving rule %s / %s', parent, item)
|
||||
fields = self.readFieldsFromParams(['name', 'comments', 'frequency', 'start', 'end', 'interval', 'duration', 'duration_unit'])
|
||||
fields = self.readFieldsFromParams(
|
||||
[
|
||||
'name',
|
||||
'comments',
|
||||
'frequency',
|
||||
'start',
|
||||
'end',
|
||||
'interval',
|
||||
'duration',
|
||||
'duration_unit',
|
||||
]
|
||||
)
|
||||
|
||||
if int(fields['interval']) < 1:
|
||||
raise self.invalidItemException('Repeat must be greater than zero')
|
||||
|
@@ -50,6 +50,7 @@ class Calendars(ModelHandler):
|
||||
"""
|
||||
Processes REST requests about calendars
|
||||
"""
|
||||
|
||||
model = Calendar
|
||||
detail = {'rules': CalendarRules}
|
||||
|
||||
@@ -57,9 +58,19 @@ class Calendars(ModelHandler):
|
||||
|
||||
table_title = _('Calendars')
|
||||
table_fields = [
|
||||
{'name': {'title': _('Name'), 'visible': True, 'type': 'icon', 'icon': 'fa fa-calendar text-success'}},
|
||||
{
|
||||
'name': {
|
||||
'title': _('Name'),
|
||||
'visible': True,
|
||||
'type': 'icon',
|
||||
'icon': 'fa fa-calendar text-success',
|
||||
}
|
||||
},
|
||||
{'comments': {'title': _('Comments')}},
|
||||
{'modified': {'title': _('Modified'), 'type': 'datetime'}},
|
||||
{'number_rules': {'title': _('Rules')}},
|
||||
{'number_access': {'title': _('Pools with Accesses')}},
|
||||
{'number_actions': {'title': _('Pools with Actions')}},
|
||||
{'tags': {'title': _('tags'), 'visible': False}},
|
||||
]
|
||||
|
||||
@@ -70,7 +81,11 @@ class Calendars(ModelHandler):
|
||||
'tags': [tag.tag for tag in item.tags.all()],
|
||||
'comments': item.comments,
|
||||
'modified': item.modified,
|
||||
'permission': permissions.getEffectivePermission(self._user, item)
|
||||
'number_rules': item.rules.count(),
|
||||
'number_access': item.calendaraccess_set.all().values('service_pool').distinct().count(),
|
||||
'number_actions': item.calendaraction_set.all().values('service_pool').distinct().count(),
|
||||
|
||||
'permission': permissions.getEffectivePermission(self._user, item),
|
||||
}
|
||||
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
|
@@ -38,7 +38,7 @@ from django.utils.translation import ugettext as _
|
||||
from django.urls import reverse
|
||||
from uds.REST import Handler
|
||||
from uds.REST import RequestError
|
||||
from uds.models import TicketStore, user
|
||||
from uds.models import TicketStore
|
||||
from uds.models import User
|
||||
from uds.web.util import errors
|
||||
from uds.core.managers import cryptoManager, userServiceManager
|
||||
@@ -46,11 +46,14 @@ from uds.core.util.config import GlobalConfig
|
||||
from uds.core.services.exceptions import ServiceNotReadyError
|
||||
from uds.core import VERSION as UDS_VERSION
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from uds.models import UserService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CLIENT_VERSION = UDS_VERSION
|
||||
REQUIRED_CLIENT_VERSION = '3.5.0'
|
||||
REQUIRED_CLIENT_VERSION = '3.6.0'
|
||||
|
||||
|
||||
|
||||
# Enclosed methods under /client path
|
||||
@@ -58,15 +61,16 @@ class Client(Handler):
|
||||
"""
|
||||
Processes Client requests
|
||||
"""
|
||||
|
||||
authenticated = False # Client requests are not authenticated
|
||||
|
||||
@staticmethod
|
||||
def result(
|
||||
result: typing.Any = None,
|
||||
error: typing.Optional[typing.Union[str, int]] = None,
|
||||
errorCode: int = 0,
|
||||
retryable: bool = False
|
||||
) -> typing.Dict[str, typing.Any]:
|
||||
result: typing.Any = None,
|
||||
error: typing.Optional[typing.Union[str, int]] = None,
|
||||
errorCode: int = 0,
|
||||
retryable: bool = False,
|
||||
) -> typing.Dict[str, typing.Any]:
|
||||
"""
|
||||
Helper method to create a "result" set for actor response
|
||||
:param result: Result value to return (can be None, in which case it is converted to empty string '')
|
||||
@@ -84,7 +88,9 @@ class Client(Handler):
|
||||
if errorCode != 0:
|
||||
# Reformat error so it is better understood by users
|
||||
# error += ' (code {0:04X})'.format(errorCode)
|
||||
error = _('Your service is being created. Please, wait while we complete it') + ' ({}%)'.format(int(errorCode * 25))
|
||||
error = _(
|
||||
'Your service is being created. Please, wait while we complete it'
|
||||
) + ' ({}%)'.format(int(errorCode * 25))
|
||||
|
||||
res['error'] = error
|
||||
res['retryable'] = '1' if retryable else '0'
|
||||
@@ -99,25 +105,40 @@ class Client(Handler):
|
||||
"""
|
||||
return Client.result(_('Correct'))
|
||||
|
||||
def get(self): # pylint: disable=too-many-locals
|
||||
def get(self) -> typing.Any: # pylint: disable=too-many-locals
|
||||
"""
|
||||
Processes get requests
|
||||
"""
|
||||
logger.debug('Client args for GET: %s', self._args)
|
||||
|
||||
if not self._args: # Gets version
|
||||
return Client.result({
|
||||
'availableVersion': CLIENT_VERSION,
|
||||
'requiredVersion': REQUIRED_CLIENT_VERSION,
|
||||
'downloadUrl': self._request.build_absolute_uri(reverse('page.client-download'))
|
||||
})
|
||||
return Client.result(
|
||||
{
|
||||
'availableVersion': CLIENT_VERSION,
|
||||
'requiredVersion': REQUIRED_CLIENT_VERSION,
|
||||
'downloadUrl': self._request.build_absolute_uri(
|
||||
reverse('page.client-download')
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
if len(self._args) == 1: # Simple test
|
||||
return Client.result(_('Correct'))
|
||||
|
||||
userService: typing.Optional['UserService'] = None
|
||||
try:
|
||||
ticket, scrambler = self._args # If more than 2 args, got an error. pylint: disable=unbalanced-tuple-unpacking
|
||||
(
|
||||
ticket,
|
||||
scrambler,
|
||||
) = (
|
||||
self._args
|
||||
) # If more than 2 args, got an error. pylint: disable=unbalanced-tuple-unpacking
|
||||
hostname = self._params['hostname'] # Or if hostname is not included...
|
||||
|
||||
version = self._params.get('version', '0.0.0')
|
||||
if version < '3.6.0':
|
||||
return Client.result(error='Client version not supported.\n Please, upgrade it.')
|
||||
|
||||
srcIp = self._request.ip
|
||||
|
||||
# Ip is optional,
|
||||
@@ -127,7 +148,13 @@ class Client(Handler):
|
||||
except Exception:
|
||||
raise RequestError('Invalid request')
|
||||
|
||||
logger.debug('Got Ticket: %s, scrambled: %s, Hostname: %s, Ip: %s', ticket, scrambler, hostname, srcIp)
|
||||
logger.debug(
|
||||
'Got Ticket: %s, scrambled: %s, Hostname: %s, Ip: %s',
|
||||
ticket,
|
||||
scrambler,
|
||||
hostname,
|
||||
srcIp,
|
||||
)
|
||||
|
||||
try:
|
||||
data = TicketStore.get(ticket)
|
||||
@@ -138,33 +165,76 @@ class Client(Handler):
|
||||
|
||||
try:
|
||||
logger.debug(data)
|
||||
ip, userService, userServiceInstance, transport, transportInstance = userServiceManager().getService(
|
||||
self._request.user, self._request.os, self._request.ip, data['service'], data['transport'], clientHostname=hostname
|
||||
(
|
||||
ip,
|
||||
userService,
|
||||
userServiceInstance,
|
||||
transport,
|
||||
transportInstance,
|
||||
) = userServiceManager().getService(
|
||||
self._request.user,
|
||||
self._request.os,
|
||||
self._request.ip,
|
||||
data['service'],
|
||||
data['transport'],
|
||||
clientHostname=hostname,
|
||||
)
|
||||
logger.debug(
|
||||
'Res: %s %s %s %s %s',
|
||||
ip,
|
||||
userService,
|
||||
userServiceInstance,
|
||||
transport,
|
||||
transportInstance,
|
||||
)
|
||||
logger.debug('Res: %s %s %s %s %s', ip, userService, userServiceInstance, transport, transportInstance)
|
||||
password = cryptoManager().symDecrpyt(data['password'], scrambler)
|
||||
|
||||
# userService.setConnectionSource(srcIp, hostname) # Store where we are accessing from so we can notify Service
|
||||
if not ip:
|
||||
raise ServiceNotReadyError
|
||||
raise ServiceNotReadyError()
|
||||
|
||||
# Set "accesedByClient"
|
||||
userService.setProperty('accessedByClient', '1')
|
||||
# This should never happen, but it's here just in case
|
||||
if not transportInstance:
|
||||
raise Exception('No transport instance!!!')
|
||||
|
||||
transportScript, signature, params = transportInstance.getEncodedTransportScript(userService, transport, ip, self._request.os, self._request.user, password, self._request)
|
||||
(
|
||||
transportScript,
|
||||
signature,
|
||||
params,
|
||||
) = transportInstance.getEncodedTransportScript(
|
||||
userService,
|
||||
transport,
|
||||
ip,
|
||||
self._request.os,
|
||||
self._request.user,
|
||||
password,
|
||||
self._request,
|
||||
)
|
||||
|
||||
logger.debug('Signature: %s', signature)
|
||||
logger.debug('Data:#######\n%s\n###########', params)
|
||||
|
||||
return Client.result(result={
|
||||
'script': transportScript,
|
||||
'signature': signature, # It is already on base64
|
||||
'params': codecs.encode(codecs.encode(json.dumps(params).encode(), 'bz2'), 'base64').decode(),
|
||||
})
|
||||
return Client.result(
|
||||
result={
|
||||
'script': transportScript,
|
||||
'signature': signature, # It is already on base64
|
||||
'params': codecs.encode(
|
||||
codecs.encode(json.dumps(params).encode(), 'bz2'), 'base64'
|
||||
).decode(),
|
||||
}
|
||||
)
|
||||
except ServiceNotReadyError as e:
|
||||
# Refresh ticket and make this retrayable
|
||||
TicketStore.revalidate(ticket, 20) # Retry will be in at most 5 seconds, so 20 is fine :)
|
||||
return Client.result(error=errors.SERVICE_IN_PREPARATION, errorCode=e.code, retryable=True)
|
||||
TicketStore.revalidate(
|
||||
ticket, 20
|
||||
) # Retry will be in at most 5 seconds, so 20 is fine :)
|
||||
return Client.result(
|
||||
error=errors.SERVICE_IN_PREPARATION, errorCode=e.code, retryable=True
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("Exception")
|
||||
return Client.result(error=str(e))
|
||||
|
||||
finally:
|
||||
if userService:
|
||||
userService.setProperty('accessedByClient', '1')
|
||||
|
@@ -39,55 +39,26 @@ from uds.REST import Handler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Pair of section/value removed from current UDS version
|
||||
REMOVED = {
|
||||
'UDS': (
|
||||
'allowPreferencesAccess', 'customHtmlLogin', 'UDS Theme',
|
||||
'UDS Theme Enhaced', 'css', 'allowPreferencesAccess',
|
||||
'loginUrl', 'maxLoginTries', 'loginBlockTime'
|
||||
),
|
||||
'Cluster': ('Destination CPU Load', 'Migration CPU Load', 'Migration Free Memory'),
|
||||
'IPAUTH': ('autoLogin',),
|
||||
'VMWare': ('minUsableDatastoreGB', 'maxRetriesOnError'),
|
||||
'HyperV': ('minUsableDatastoreGB',),
|
||||
'Security': ('adminIdleTime', 'userSessionLength'),
|
||||
}
|
||||
|
||||
|
||||
# Enclosed methods under /config path
|
||||
class Config(Handler):
|
||||
needs_admin = True # By default, staff is lower level needed
|
||||
|
||||
def get(self):
|
||||
def get(self) -> typing.Any:
|
||||
cfg: CfgConfig.Value
|
||||
|
||||
res: typing.Dict[str, typing.Dict[str, typing.Any]] = {}
|
||||
addCrypt = self.is_admin()
|
||||
|
||||
for cfg in CfgConfig.enumerate():
|
||||
# Skip removed configuration values, even if they are in database
|
||||
logger.debug('Key: %s, val: %s', cfg.section(), cfg.key())
|
||||
if cfg.key() in REMOVED.get(cfg.section(), ()):
|
||||
continue
|
||||
|
||||
if cfg.isCrypted() is True and addCrypt is False:
|
||||
continue
|
||||
|
||||
# add section if it do not exists
|
||||
if cfg.section() not in res:
|
||||
res[cfg.section()] = {}
|
||||
res[cfg.section()][cfg.key()] = {
|
||||
'value': cfg.get(),
|
||||
'crypt': cfg.isCrypted(),
|
||||
'longText': cfg.isLongText(),
|
||||
'type': cfg.getType(),
|
||||
'params': cfg.getParams()
|
||||
configs = CfgConfig.getConfigValues(self.is_admin())
|
||||
# Remove values from cryptes keys
|
||||
return {
|
||||
section: {
|
||||
key: vals if not vals['crypt'] else {**vals, 'value': '********'}
|
||||
for key, vals in secDict.items()
|
||||
}
|
||||
logger.debug('Configuration: %s', res)
|
||||
return res
|
||||
for section, secDict in configs.items()
|
||||
}
|
||||
|
||||
def put(self):
|
||||
for section, secDict in self._params.items():
|
||||
for key, vals in secDict.items():
|
||||
logger.info('Updating config value %s.%s to %s by %s', section, key, vals['value'], self._user.name)
|
||||
CfgConfig.update(section, key, vals['value'])
|
||||
return 'done'
|
||||
|
@@ -88,7 +88,11 @@ class Connection(Handler):
|
||||
# Ensure user is present on request, used by web views methods
|
||||
self._request.user = self._user
|
||||
|
||||
return Connection.result(result=getServicesData(typing.cast(ExtendedHttpRequestWithUser, self._request)))
|
||||
return Connection.result(
|
||||
result=services.getServicesData(
|
||||
typing.cast(ExtendedHttpRequestWithUser, self._request)
|
||||
)
|
||||
)
|
||||
|
||||
def connection(self, doNotCheck: bool = False):
|
||||
idService = self._args[0]
|
||||
@@ -152,7 +156,7 @@ class Connection(Handler):
|
||||
self._request.ip, hostname
|
||||
) # Store where we are accessing from so we can notify Service
|
||||
|
||||
if not ip:
|
||||
if not ip or not transportInstance:
|
||||
raise ServiceNotReadyError()
|
||||
|
||||
transportScript = transportInstance.getEncodedTransportScript(
|
||||
@@ -183,7 +187,9 @@ class Connection(Handler):
|
||||
self._request.user = self._user # type: ignore
|
||||
self._request._cryptedpass = self._session['REST']['password'] # type: ignore
|
||||
self._request._scrambler = self._request.META['HTTP_SCRAMBLER'] # type: ignore
|
||||
linkInfo = services.enableService(self._request, idService=self._args[0], idTransport=self._args[1])
|
||||
linkInfo = services.enableService(
|
||||
self._request, idService=self._args[0], idTransport=self._args[1]
|
||||
)
|
||||
if linkInfo['error']:
|
||||
return Connection.result(error=linkInfo['error'])
|
||||
return Connection.result(result=linkInfo['url'])
|
||||
|
@@ -49,19 +49,27 @@ class Images(ModelHandler):
|
||||
"""
|
||||
Handles the gallery REST interface
|
||||
"""
|
||||
|
||||
path = 'gallery'
|
||||
model = Image
|
||||
save_fields = ['name', 'data']
|
||||
|
||||
table_title = _('Image Gallery')
|
||||
table_fields = [
|
||||
{'thumb': {'title': _('Image'), 'visible': True, 'type': 'image', 'width': '96px'}},
|
||||
{
|
||||
'thumb': {
|
||||
'title': _('Image'),
|
||||
'visible': True,
|
||||
'type': 'image',
|
||||
'width': '96px',
|
||||
}
|
||||
},
|
||||
{'name': {'title': _('Name')}},
|
||||
{'size': {'title': _('Size')}},
|
||||
]
|
||||
|
||||
def beforeSave(self, fields: typing.Dict[str, typing.Any]) -> None:
|
||||
fields['data'] = Image.prepareForDb(Image.decode64(fields['data'].encode('utf8')))
|
||||
fields['data'] = Image.prepareForDb(Image.decode64(fields['data']))
|
||||
|
||||
def afterSave(self, item: Image) -> None:
|
||||
# Updates the thumbnail and re-saves it
|
||||
@@ -69,17 +77,17 @@ class Images(ModelHandler):
|
||||
item.updateThumbnail()
|
||||
item.save()
|
||||
|
||||
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
return self.addField(
|
||||
self.addDefaultFields([], ['name']), {
|
||||
self.addDefaultFields([], ['name']),
|
||||
{
|
||||
'name': 'data',
|
||||
'value': '',
|
||||
'label': ugettext('Image'),
|
||||
'tooltip': ugettext('Image object'),
|
||||
'type': gui.InputField.IMAGECHOICE_TYPE,
|
||||
'order': 100, # At end
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
def item_as_dict(self, item: Image) -> typing.Dict[str, typing.Any]:
|
||||
@@ -92,7 +100,9 @@ class Images(ModelHandler):
|
||||
def item_as_dict_overview(self, item: Image) -> typing.Dict[str, typing.Any]:
|
||||
return {
|
||||
'id': item.uuid,
|
||||
'size': '{}x{}, {} bytes (thumb {} bytes)'.format(item.width, item.height, len(item.data), len(item.thumb)),
|
||||
'size': '{}x{}, {} bytes (thumb {} bytes)'.format(
|
||||
item.width, item.height, len(item.data), len(item.thumb)
|
||||
),
|
||||
'name': item.name,
|
||||
'thumb': item.thumb64,
|
||||
}
|
||||
|
@@ -53,15 +53,22 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Enclosed methods under /auth path
|
||||
|
||||
|
||||
class Login(Handler):
|
||||
"""
|
||||
Responsible of user authentication
|
||||
"""
|
||||
|
||||
path = 'auth'
|
||||
authenticated = False # Public method
|
||||
|
||||
@staticmethod
|
||||
def result(result: str = 'error', token: str = None, scrambler: str = None, error: str = None) -> typing.MutableMapping[str, typing.Any]:
|
||||
def result(
|
||||
result: str = 'error',
|
||||
token: typing.Optional[str] = None,
|
||||
scrambler: typing.Optional[str] = None,
|
||||
error: typing.Optional[str] = None,
|
||||
) -> typing.MutableMapping[str, typing.Any]:
|
||||
res = {
|
||||
'result': result,
|
||||
'token': token,
|
||||
@@ -109,15 +116,31 @@ class Login(Handler):
|
||||
cache = Cache('RESTapi')
|
||||
fails = cache.get(self._request.ip) or 0
|
||||
if fails > ALLOWED_FAILS:
|
||||
logger.info('Access to REST API %s is blocked for %s seconds since last fail', self._request.ip, GlobalConfig.LOGIN_BLOCK.getInt())
|
||||
|
||||
logger.info(
|
||||
'Access to REST API %s is blocked for %s seconds since last fail',
|
||||
self._request.ip,
|
||||
GlobalConfig.LOGIN_BLOCK.getInt(),
|
||||
)
|
||||
|
||||
try:
|
||||
if 'auth_id' not in self._params and 'authId' not in self._params and 'authSmallName' not in self._params and 'auth' not in self._params:
|
||||
if (
|
||||
'auth_id' not in self._params
|
||||
and 'authId' not in self._params
|
||||
and 'authSmallName' not in self._params
|
||||
and 'auth' not in self._params
|
||||
):
|
||||
raise RequestError('Invalid parameters (no auth)')
|
||||
|
||||
scrambler: str = ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(32)) # @UndefinedVariable
|
||||
authId: typing.Optional[str] = self._params.get('authId', self._params.get('auth_id', None))
|
||||
authSmallName: typing.Optional[str] = self._params.get('authSmallName', None)
|
||||
scrambler: str = ''.join(
|
||||
random.SystemRandom().choice(string.ascii_letters + string.digits)
|
||||
for _ in range(32)
|
||||
) # @UndefinedVariable
|
||||
authId: typing.Optional[str] = self._params.get(
|
||||
'authId', self._params.get('auth_id', None)
|
||||
)
|
||||
authSmallName: typing.Optional[str] = self._params.get(
|
||||
'authSmallName', None
|
||||
)
|
||||
authName: typing.Optional[str] = self._params.get('auth', None)
|
||||
platform: str = self._params.get('platform', self._request.os)
|
||||
|
||||
@@ -126,9 +149,18 @@ class Login(Handler):
|
||||
|
||||
username, password = self._params['username'], self._params['password']
|
||||
locale: str = self._params.get('locale', 'en')
|
||||
if authName == 'admin' or authSmallName == 'admin' or authId == '00000000-0000-0000-0000-000000000000':
|
||||
if GlobalConfig.SUPER_USER_LOGIN.get(True) == username and GlobalConfig.SUPER_USER_PASS.get(True) == password:
|
||||
self.genAuthToken(-1, username, password, locale, platform, True, True, scrambler)
|
||||
if (
|
||||
authName == 'admin'
|
||||
or authSmallName == 'admin'
|
||||
or authId == '00000000-0000-0000-0000-000000000000'
|
||||
):
|
||||
if (
|
||||
GlobalConfig.SUPER_USER_LOGIN.get(True) == username
|
||||
and GlobalConfig.SUPER_USER_PASS.get(True) == password
|
||||
):
|
||||
self.genAuthToken(
|
||||
-1, username, password, locale, platform, True, True, scrambler
|
||||
)
|
||||
return Login.result(result='ok', token=self.getAuthToken())
|
||||
return Login.result(error='Invalid credentials')
|
||||
|
||||
@@ -149,13 +181,24 @@ class Login(Handler):
|
||||
# Sleep a while here to "prottect"
|
||||
time.sleep(3) # Wait 3 seconds if credentials fails for "protection"
|
||||
# And store in cache for blocking for a while if fails
|
||||
cache.put(self._request.ip, fails+1, GlobalConfig.LOGIN_BLOCK.getInt())
|
||||
|
||||
cache.put(
|
||||
self._request.ip, fails + 1, GlobalConfig.LOGIN_BLOCK.getInt()
|
||||
)
|
||||
|
||||
return Login.result(error='Invalid credentials')
|
||||
return Login.result(
|
||||
result='ok',
|
||||
token=self.genAuthToken(auth.id, user.name, password, locale, platform, user.is_admin, user.staff_member, scrambler),
|
||||
scrambler=scrambler
|
||||
token=self.genAuthToken(
|
||||
auth.id,
|
||||
user.name,
|
||||
password,
|
||||
locale,
|
||||
platform,
|
||||
user.is_admin,
|
||||
user.staff_member,
|
||||
scrambler,
|
||||
),
|
||||
scrambler=scrambler,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
@@ -169,6 +212,7 @@ class Logout(Handler):
|
||||
"""
|
||||
Responsible of user de-authentication
|
||||
"""
|
||||
|
||||
path = 'auth'
|
||||
authenticated = True # By default, all handlers needs authentication
|
||||
|
||||
@@ -185,19 +229,21 @@ class Auths(Handler):
|
||||
path = 'auth'
|
||||
authenticated = False # By default, all handlers needs authentication
|
||||
|
||||
def auths(self):
|
||||
def auths(self) -> typing.Iterator[typing.Dict[str, typing.Any]]:
|
||||
paramAll: bool = self._params.get('all', 'false') == 'true'
|
||||
auth: Authenticator
|
||||
for auth in Authenticator.objects.all():
|
||||
theType = auth.getType()
|
||||
if paramAll or (theType.isCustom() is False and theType.typeType not in ('IP',)):
|
||||
if paramAll or (
|
||||
theType.isCustom() is False and theType.typeType not in ('IP',)
|
||||
):
|
||||
yield {
|
||||
'authId': auth.uuid,
|
||||
'authSmallName': str(auth.small_name),
|
||||
'auth': auth.name,
|
||||
'type': theType.typeType,
|
||||
'priority': auth.priority,
|
||||
'isCustom': theType.isCustom()
|
||||
'isCustom': theType.isCustom(),
|
||||
}
|
||||
|
||||
def get(self):
|
||||
|
@@ -54,6 +54,7 @@ class MetaPools(ModelHandler):
|
||||
"""
|
||||
Handles Services Pools REST requests
|
||||
"""
|
||||
|
||||
model = MetaPool
|
||||
detail = {
|
||||
'pools': MetaServicesPool,
|
||||
@@ -62,8 +63,18 @@ class MetaPools(ModelHandler):
|
||||
'access': AccessCalendars,
|
||||
}
|
||||
|
||||
save_fields = ['name', 'short_name', 'comments', 'tags',
|
||||
'image_id', 'servicesPoolGroup_id', 'visible', 'policy', 'calendar_message', 'transport_grouping']
|
||||
save_fields = [
|
||||
'name',
|
||||
'short_name',
|
||||
'comments',
|
||||
'tags',
|
||||
'image_id',
|
||||
'servicesPoolGroup_id',
|
||||
'visible',
|
||||
'policy',
|
||||
'calendar_message',
|
||||
'transport_grouping',
|
||||
]
|
||||
|
||||
table_title = _('Meta Pools')
|
||||
table_fields = [
|
||||
@@ -93,8 +104,16 @@ class MetaPools(ModelHandler):
|
||||
poolGroupThumb = item.servicesPoolGroup.image.thumb64
|
||||
|
||||
allPools = item.members.all()
|
||||
userServicesCount = sum((i.pool.userServices.exclude(state__in=State.INFO_STATES).count() for i in allPools))
|
||||
userServicesInPreparation = sum((i.pool.userServices.filter(state=State.PREPARING).count()) for i in allPools)
|
||||
userServicesCount = sum(
|
||||
(
|
||||
i.pool.userServices.exclude(state__in=State.INFO_STATES).count()
|
||||
for i in allPools
|
||||
)
|
||||
)
|
||||
userServicesInPreparation = sum(
|
||||
(i.pool.userServices.filter(state=State.PREPARING).count())
|
||||
for i in allPools
|
||||
)
|
||||
|
||||
val = {
|
||||
'id': item.uuid,
|
||||
@@ -102,7 +121,9 @@ class MetaPools(ModelHandler):
|
||||
'short_name': item.short_name,
|
||||
'tags': [tag.tag for tag in item.tags.all()],
|
||||
'comments': item.comments,
|
||||
'thumb': item.image.thumb64 if item.image is not None else DEFAULT_THUMB_BASE64,
|
||||
'thumb': item.image.thumb64
|
||||
if item.image is not None
|
||||
else DEFAULT_THUMB_BASE64,
|
||||
'image_id': item.image.uuid if item.image is not None else None,
|
||||
'servicesPoolGroup_id': poolGroupId,
|
||||
'pool_group_name': poolGroupName,
|
||||
@@ -114,7 +135,7 @@ class MetaPools(ModelHandler):
|
||||
'fallbackAccess': item.fallbackAccess,
|
||||
'permission': permissions.getEffectivePermission(self._user, item),
|
||||
'calendar_message': item.calendar_message,
|
||||
'transport_grouping': item.transport_grouping
|
||||
'transport_grouping': item.transport_grouping,
|
||||
}
|
||||
|
||||
return val
|
||||
@@ -123,30 +144,50 @@ class MetaPools(ModelHandler):
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
localGUI = self.addDefaultFields([], ['name', 'short_name', 'comments', 'tags'])
|
||||
|
||||
for field in [{
|
||||
for field in [
|
||||
{
|
||||
'name': 'policy',
|
||||
'values': [gui.choiceItem(k, str(v)) for k, v in MetaPool.TYPES.items()],
|
||||
'values': [
|
||||
gui.choiceItem(k, str(v)) for k, v in MetaPool.TYPES.items()
|
||||
],
|
||||
'label': ugettext('Policy'),
|
||||
'tooltip': ugettext('Service pool policy'),
|
||||
'type': gui.InputField.CHOICE_TYPE,
|
||||
'order': 100,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
'name': 'image_id',
|
||||
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)] + gui.sortedChoices([gui.choiceImage(v.uuid, v.name, v.thumb64) for v in Image.objects.all()]),
|
||||
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
|
||||
+ gui.sortedChoices(
|
||||
[
|
||||
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
|
||||
for v in Image.objects.all()
|
||||
]
|
||||
),
|
||||
'label': ugettext('Associated Image'),
|
||||
'tooltip': ugettext('Image assocciated with this service'),
|
||||
'type': gui.InputField.IMAGECHOICE_TYPE,
|
||||
'order': 120,
|
||||
'tab': gui.DISPLAY_TAB,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
'name': 'servicesPoolGroup_id',
|
||||
'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)] + gui.sortedChoices([gui.choiceImage(v.uuid, v.name, v.thumb64) for v in ServicePoolGroup.objects.all()]),
|
||||
'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)]
|
||||
+ gui.sortedChoices(
|
||||
[
|
||||
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
|
||||
for v in ServicePoolGroup.objects.all()
|
||||
]
|
||||
),
|
||||
'label': ugettext('Pool group'),
|
||||
'tooltip': ugettext('Pool group for this pool (for pool classify on display)'),
|
||||
'tooltip': ugettext(
|
||||
'Pool group for this pool (for pool classify on display)'
|
||||
),
|
||||
'type': gui.InputField.IMAGECHOICE_TYPE,
|
||||
'order': 121,
|
||||
'tab': gui.DISPLAY_TAB,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
'name': 'visible',
|
||||
'value': True,
|
||||
'label': ugettext('Visible'),
|
||||
@@ -154,23 +195,31 @@ class MetaPools(ModelHandler):
|
||||
'type': gui.InputField.CHECKBOX_TYPE,
|
||||
'order': 123,
|
||||
'tab': gui.DISPLAY_TAB,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
'name': 'calendar_message',
|
||||
'value': '',
|
||||
'label': ugettext('Calendar access denied text'),
|
||||
'tooltip': ugettext('Custom message to be shown to users if access is limited by calendar rules.'),
|
||||
'tooltip': ugettext(
|
||||
'Custom message to be shown to users if access is limited by calendar rules.'
|
||||
),
|
||||
'type': gui.InputField.TEXT_TYPE,
|
||||
'order': 124,
|
||||
'tab': gui.DISPLAY_TAB,
|
||||
}, {
|
||||
},
|
||||
{
|
||||
'name': 'transport_grouping',
|
||||
'values': [gui.choiceItem(k, str(v)) for k, v in MetaPool.TRANSPORT_SELECT.items()],
|
||||
'values': [
|
||||
gui.choiceItem(k, str(v))
|
||||
for k, v in MetaPool.TRANSPORT_SELECT.items()
|
||||
],
|
||||
'label': ugettext('Transport Selection'),
|
||||
'tooltip': ugettext('Transport selection policy'),
|
||||
'type': gui.InputField.CHOICE_TYPE,
|
||||
'order': 125,
|
||||
'tab': gui.DISPLAY_TAB
|
||||
}]:
|
||||
'tab': gui.DISPLAY_TAB,
|
||||
},
|
||||
]:
|
||||
self.addField(localGUI, field)
|
||||
|
||||
return localGUI
|
||||
|
@@ -154,7 +154,7 @@ class MetaAssignedService(DetailHandler):
|
||||
return UserService.objects.filter(
|
||||
uuid=processUuid(userServiceId),
|
||||
cache_level=0,
|
||||
deployed_service__meta=metaPool,
|
||||
deployed_service__in=[i.pool for i in metaPool.members.all()],
|
||||
)[0]
|
||||
except Exception:
|
||||
pass
|
||||
@@ -256,7 +256,7 @@ class MetaAssignedService(DetailHandler):
|
||||
user: User = User.objects.get(uuid=processUuid(fields['user_id']))
|
||||
|
||||
logStr = 'Changing ownership of service from {} to {} by {}'.format(
|
||||
service.user.pretty_name, user.pretty_name, self._user.pretty_name
|
||||
service.user.pretty_name, user.pretty_name, self._user.pretty_name # type: ignore
|
||||
)
|
||||
|
||||
# If there is another service that has this same owner, raise an exception
|
||||
|
118
server/src/uds/REST/methods/mfas.py
Normal file
118
server/src/uds/REST/methods/mfas.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2014-2021 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
#
|
||||
# * Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
# * Neither the name of Virtual Cable S.L.U. nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this software
|
||||
# without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
'''
|
||||
@itemor: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
'''
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from django.utils.translation import gettext_lazy as _, gettext
|
||||
from uds import models
|
||||
from uds.core import mfas
|
||||
from uds.core.ui import gui
|
||||
from uds.core.util import permissions
|
||||
|
||||
from uds.REST.model import ModelHandler
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Enclosed methods under /item path
|
||||
|
||||
|
||||
class MFA(ModelHandler):
|
||||
model = models.MFA
|
||||
save_fields = ['name', 'comments', 'tags', 'remember_device', 'validity']
|
||||
|
||||
table_title = _('Multi Factor Authentication')
|
||||
table_fields = [
|
||||
{'name': {'title': _('Name'), 'visible': True, 'type': 'iconType'}},
|
||||
{'type_name': {'title': _('Type')}},
|
||||
{'comments': {'title': _('Comments')}},
|
||||
{'tags': {'title': _('tags'), 'visible': False}},
|
||||
]
|
||||
|
||||
def enum_types(self) -> typing.Iterable[typing.Type[mfas.MFA]]:
|
||||
return mfas.factory().providers().values()
|
||||
|
||||
def getGui(self, type_: str) -> typing.List[typing.Any]:
|
||||
mfa = mfas.factory().lookup(type_)
|
||||
|
||||
if not mfa:
|
||||
raise self.invalidItemException()
|
||||
|
||||
localGui = self.addDefaultFields(
|
||||
mfa.guiDescription(), ['name', 'comments', 'tags']
|
||||
)
|
||||
self.addField(
|
||||
localGui,
|
||||
{
|
||||
'name': 'remember_device',
|
||||
'value': '0',
|
||||
'minValue': '0',
|
||||
'label': gettext('Device Caching'),
|
||||
'tooltip': gettext(
|
||||
'Time in hours to cache device so MFA is not required again. User based.'
|
||||
),
|
||||
'type': gui.InputField.NUMERIC_TYPE,
|
||||
'order': 111,
|
||||
},
|
||||
)
|
||||
self.addField(
|
||||
localGui,
|
||||
{
|
||||
'name': 'validity',
|
||||
'value': '5',
|
||||
'minValue': '0',
|
||||
'label': gettext('MFA code validity'),
|
||||
'tooltip': gettext(
|
||||
'Time in minutes to allow MFA code to be used.'
|
||||
),
|
||||
'type': gui.InputField.NUMERIC_TYPE,
|
||||
'order': 112,
|
||||
},
|
||||
|
||||
)
|
||||
|
||||
return localGui
|
||||
|
||||
def item_as_dict(self, item: models.MFA) -> typing.Dict[str, typing.Any]:
|
||||
type_ = item.getType()
|
||||
return {
|
||||
'id': item.uuid,
|
||||
'name': item.name,
|
||||
'remember_device': item.remember_device,
|
||||
'validity': item.validity,
|
||||
'tags': [tag.tag for tag in item.tags.all()],
|
||||
'comments': item.comments,
|
||||
'type': type_.type(),
|
||||
'type_name': type_.name(),
|
||||
'permission': permissions.getEffectivePermission(self._user, item),
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user