mirror of
https://github.com/dkmstr/openuds.git
synced 2025-10-05 07:33:41 +03:00
Compare commits
423 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
f8628ea002 | ||
|
6f0a993342 | ||
|
7fa3d494e2 | ||
|
3505285663 | ||
|
7da008ba53 | ||
|
c96193f755 | ||
|
05200f7e71 | ||
|
375c1cc452 | ||
|
cd9a11a445 | ||
|
6203ffca2c | ||
|
103978955f | ||
|
9a5a906410 | ||
|
1061858460 | ||
|
728775a95a | ||
|
891b9a8652 | ||
|
3b7f060398 | ||
|
db406439a3 | ||
|
3d128be218 | ||
|
0e98225659 | ||
|
488e2748c0 | ||
|
d0d0fef70e | ||
|
4e4218bb47 | ||
|
2c66bdf5c0 | ||
|
f8b399080a | ||
|
6e367d3252 | ||
|
df4067296b | ||
|
f160b7443e | ||
|
7fe34a2118 | ||
|
86f0d1f4ea | ||
|
d01ff17321 | ||
|
da7851a2e1 | ||
|
51bfa32ecb | ||
|
6559be32e6 | ||
|
f2893d43a6 | ||
|
1e29c18abf | ||
|
1319d85328 | ||
|
63d63e0158 | ||
|
74dd61526b | ||
|
5f9c9079d5 | ||
|
8e8027c115 | ||
|
f2405b0544 | ||
|
36c827c985 | ||
|
38f766ce3e | ||
|
7966bdae26 | ||
|
2442ac0c7f | ||
|
78a70368b7 | ||
|
2f6132ec56 | ||
|
8046add26e | ||
|
f89c929b48 | ||
|
054a644144 | ||
|
d3dda58c76 | ||
|
60740831c9 | ||
|
e88cd19ea9 | ||
|
cd8d19c93e | ||
|
2b8ccc9108 | ||
|
b21acb75f8 | ||
|
67a901525d | ||
|
f1cf36b047 | ||
|
67bf7bf7ad | ||
|
292d746329 | ||
|
de1f59e5d7 | ||
|
586c24dcfb | ||
|
7c40fbe63f | ||
|
fbde003611 | ||
|
083b6d468f | ||
|
bbeff4f0c7 | ||
|
98f286a1e2 | ||
|
98eb4fee95 | ||
|
062e21a144 | ||
|
9f36472607 | ||
|
68c341e5c8 | ||
|
f9e0ecb193 | ||
|
dc5f620333 | ||
|
27d694739a | ||
|
378e643cf2 | ||
|
c4b0150509 | ||
|
b911d251d8 | ||
|
1ecc5dae33 | ||
|
f0f158bb18 | ||
|
833e3b22c1 | ||
|
c626b79ee9 | ||
|
822968bf4b | ||
|
46b3a2020b | ||
|
0739559512 | ||
|
ad05b4b252 | ||
|
e642b2ac34 | ||
|
2edd9b4b9f | ||
|
b98004b0a0 | ||
|
3bd45f7a80 | ||
|
9aa833d7fb | ||
|
5979620eb1 | ||
|
3cee557e3a | ||
|
84e3779188 | ||
|
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 | ||
|
e15ba650f9 | ||
|
48cd8240e9 | ||
|
75005399aa | ||
|
da02ad0c7c | ||
|
0a15f7bdce | ||
|
2189267358 | ||
|
1a9517675d | ||
|
aa97309db9 | ||
|
3e947e1d82 | ||
|
dcedb268dd | ||
|
645f61c8e6 | ||
|
d462ecbb32 | ||
|
c1e4c5b81a | ||
|
d707771fe5 | ||
|
544fb21a01 | ||
|
159aa3c6ec | ||
|
8cc17464c3 | ||
|
adeb6b2a46 | ||
|
b7962a24f1 | ||
|
94cab0c204 | ||
|
fa3e6cb5a2 | ||
|
2a9900d2eb | ||
|
ccf75e7104 | ||
|
c208814dd0 | ||
|
6081122311 | ||
|
d22f606f49 | ||
|
859f09aa0b | ||
|
dba2526ffb | ||
|
d17dae8bdd | ||
|
c6b9817749 | ||
|
b34f09e58e | ||
|
053fcfd3e3 | ||
|
36e3c7eea9 | ||
|
6772455111 | ||
|
b69efb5426 | ||
|
5c4141f9a2 | ||
|
d8e8ddd1bb | ||
|
c8770e5f4f | ||
|
f2e3a2468a | ||
|
106c28cb34 | ||
|
2165798408 | ||
|
271351b3d6 | ||
|
d0d6892858 | ||
|
80e53615c5 | ||
|
899156843e | ||
|
2b917839c7 | ||
|
d96a639a3d | ||
|
721d32c972 | ||
|
1cf2e2cd76 | ||
|
40364cdcce | ||
|
39fd5dbf3f | ||
|
396e0f0c38 | ||
|
fc6224dada | ||
|
1d8d1fe2c9 | ||
|
0923a3fbca | ||
|
114247e407 | ||
|
dd08257fb9 | ||
|
7bd0d571e6 | ||
|
ad269b3c28 | ||
|
f3dd5753a3 | ||
|
13336b966e | ||
|
a76989d885 | ||
|
5f0e5a5dfe | ||
|
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 | ||
|
6692e5ce6d | ||
|
38b3318704 | ||
|
ccec281e0d | ||
|
230187d9ee | ||
|
092bb83001 | ||
|
ac62aed420 | ||
|
e16be78ad5 | ||
|
28319b216f | ||
|
739b0c7f81 | ||
|
e5e8ad5fbd | ||
|
86ebd7766e | ||
|
4f0ea76666 | ||
|
18e9cab9ef | ||
|
6053e34d1d | ||
|
11041ff44f | ||
|
98826504d6 | ||
|
3a990e19a6 | ||
|
8a150439ae | ||
|
e79753748e | ||
|
a8a9b24596 | ||
|
f24c77f20a | ||
|
d2fa5e38d0 | ||
|
ada5374db5 | ||
|
93ba05f6cb | ||
|
94cf5582e2 | ||
|
afcfffbd29 | ||
|
d1329849f3 | ||
|
f5d2776478 | ||
|
0496117fc1 | ||
|
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 | ||
|
ec02f63cac | ||
|
0de655d14f | ||
|
68e327847b | ||
|
81ea07f0a0 | ||
|
d7540c3305 |
@@ -12,4 +12,4 @@ This is an Open Source Source project, initiated by Spanish Company Virtualca
|
||||
|
||||
Any help provided will be welcome.
|
||||
|
||||
**Note: Master version is always under heavy development and it is not recommended for use, it will probably have unfixed bugs. Please use the latest stable branch.**
|
||||
**Note: This is a previous release, being superseded by v4.0. Try to use actual Stable.**
|
||||
|
@@ -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
|
||||
|
@@ -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
|
@@ -214,10 +214,10 @@
|
||||
<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>
|
||||
@@ -268,10 +268,10 @@
|
||||
<item row="3" column="1">
|
||||
<widget class="QLineEdit" name="restrictNet">
|
||||
<property name="toolTip">
|
||||
<string>UDS user with administration rights (Will not be stored on template)</string>
|
||||
<string>Restrict valid detection of network interfaces to this network.</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>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>
|
||||
|
@@ -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."
|
||||
|
@@ -239,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,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)
|
||||
|
||||
|
@@ -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
|
||||
@@ -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'])
|
||||
|
||||
|
@@ -53,7 +53,7 @@ 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),
|
||||
|
@@ -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,10 +31,12 @@
|
||||
# pylint: disable=invalid-name
|
||||
import warnings
|
||||
import json
|
||||
import ssl
|
||||
import logging
|
||||
import typing
|
||||
|
||||
import requests
|
||||
import requests.adapters
|
||||
|
||||
from . import types
|
||||
from .version import VERSION
|
||||
@@ -42,6 +44,18 @@ from .version import VERSION
|
||||
# Default public listen port
|
||||
LISTEN_PORT = 43910
|
||||
|
||||
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-CHACHA20-POLY1305'
|
||||
)
|
||||
|
||||
# Default timeout
|
||||
TIMEOUT = 5 # 5 seconds is more than enought
|
||||
|
||||
@@ -82,6 +96,7 @@ NO_PROXY = {
|
||||
|
||||
UDS_BASE_URL = 'https://{}/uds/rest/'
|
||||
|
||||
|
||||
#
|
||||
# Basic UDS Api
|
||||
#
|
||||
@@ -93,6 +108,7 @@ class UDSApi: # pylint: disable=too-few-public-methods
|
||||
_host: str
|
||||
_validateCert: bool
|
||||
_url: str
|
||||
_session: 'requests.Session'
|
||||
|
||||
def __init__(self, host: str, validateCert: bool) -> None:
|
||||
self._host = host
|
||||
@@ -106,6 +122,30 @@ class UDSApi: # pylint: disable=too-few-public-methods
|
||||
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_2
|
||||
context.set_ciphers(SECURE_CIPHERS)
|
||||
|
||||
# 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 {
|
||||
@@ -125,15 +165,13 @@ class UDSApi: # pylint: disable=too-few-public-methods
|
||||
) -> 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 # type: ignore
|
||||
if disableProxy
|
||||
else None, # if not proxies wanted, enforce it
|
||||
proxies=NO_PROXY if disableProxy else None, # type: ignore # if not proxies wanted, enforce it
|
||||
)
|
||||
|
||||
if result.ok:
|
||||
@@ -162,10 +200,10 @@ class UDSServerApi(UDSApi):
|
||||
|
||||
def enumerateAuthenticators(self) -> typing.Iterable[types.AuthenticatorType]:
|
||||
try:
|
||||
result = requests.get(
|
||||
result = self._session.get(
|
||||
self._url + 'auth/auths',
|
||||
headers=self._headers,
|
||||
verify=self._validateCert,
|
||||
# verify=self._validateCert,
|
||||
timeout=4,
|
||||
)
|
||||
if result.ok:
|
||||
@@ -178,7 +216,7 @@ class UDSServerApi(UDSApi):
|
||||
priority=v['priority'],
|
||||
isCustom=v['isCustom'],
|
||||
)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
def register( # pylint: disable=too-many-arguments, too-many-locals
|
||||
@@ -213,22 +251,22 @@ class UDSServerApi(UDSApi):
|
||||
# First, try to login
|
||||
authInfo = {'auth': auth, 'username': username, 'password': password}
|
||||
headers = self._headers
|
||||
result = requests.post(
|
||||
result = self._session.post(
|
||||
self._url + 'auth/login',
|
||||
data=json.dumps(authInfo),
|
||||
headers=headers,
|
||||
verify=self._validateCert,
|
||||
# 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(
|
||||
result = self._session.post(
|
||||
self._apiURL('register'),
|
||||
data=json.dumps(data),
|
||||
headers=headers,
|
||||
verify=self._validateCert,
|
||||
# verify=self._validateCert,
|
||||
)
|
||||
if result.ok:
|
||||
return result.json()['result']
|
||||
@@ -272,9 +310,7 @@ class UDSServerApi(UDSApi):
|
||||
else None,
|
||||
)
|
||||
|
||||
def ready(
|
||||
self, own_token: str, secret: str, ip: str, port: int
|
||||
) -> types.CertificateInfoType:
|
||||
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)
|
||||
|
||||
@@ -282,11 +318,10 @@ class UDSServerApi(UDSApi):
|
||||
private_key=result['private_key'],
|
||||
server_certificate=result['server_certificate'],
|
||||
password=result['password'],
|
||||
ciphers=result.get('ciphers', ''),
|
||||
)
|
||||
|
||||
def notifyIpChange(
|
||||
self, own_token: str, secret: str, ip: str, port: int
|
||||
) -> types.CertificateInfoType:
|
||||
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)
|
||||
|
||||
@@ -294,6 +329,7 @@ class UDSServerApi(UDSApi):
|
||||
private_key=result['private_key'],
|
||||
server_certificate=result['server_certificate'],
|
||||
password=result['password'],
|
||||
ciphers=result.get('ciphers', ''),
|
||||
)
|
||||
|
||||
def notifyUnmanagedCallback(
|
||||
@@ -315,6 +351,7 @@ class UDSServerApi(UDSApi):
|
||||
private_key=result['private_key'],
|
||||
server_certificate=result['server_certificate'],
|
||||
password=result['password'],
|
||||
ciphers=result.get('ciphers', ''),
|
||||
)
|
||||
|
||||
def login(
|
||||
@@ -327,9 +364,7 @@ class UDSServerApi(UDSApi):
|
||||
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
|
||||
)
|
||||
return types.LoginResultInfoType(ip='0.0.0.0', hostname=UNKNOWN, dead_line=None, max_idle=None)
|
||||
payload = {
|
||||
'type': actor_type or types.MANAGED,
|
||||
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
|
||||
@@ -405,9 +440,7 @@ class UDSClientApi(UDSApi):
|
||||
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,
|
||||
@@ -420,11 +453,8 @@ class UDSClientApi(UDSApi):
|
||||
max_idle=result['max_idle'],
|
||||
)
|
||||
|
||||
def logout(self, username: str, sessionType: typing.Optional[str]) -> None:
|
||||
payLoad = {
|
||||
'username': username,
|
||||
'session_type': sessionType or UNKNOWN
|
||||
}
|
||||
def logout(self, username: str, sessionType: typing.Optional[str] = None) -> None:
|
||||
payLoad = {'username': username, 'session_type': sessionType or UNKNOWN}
|
||||
self.post('logout', payLoad)
|
||||
|
||||
def ping(self) -> bool:
|
||||
|
@@ -92,7 +92,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
|
||||
self._clientsPool = clients_pool.UDSActorClientPool()
|
||||
self._certificate = (
|
||||
cert.defaultCertificate
|
||||
) # For being used on "unmanaged" hosts only
|
||||
) # For being used on "unmanaged" hosts only, and prior to first login
|
||||
self._http = None
|
||||
|
||||
# Initialzies loglevel and serviceLogger
|
||||
|
@@ -66,3 +66,4 @@ class CertificateInfoType(typing.NamedTuple):
|
||||
private_key: str
|
||||
server_certificate: str
|
||||
password: str
|
||||
ciphers: str
|
||||
|
@@ -1 +1 @@
|
||||
VERSION = '3.5.0'
|
||||
VERSION = '3.6.0'
|
||||
|
@@ -192,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)
|
||||
|
@@ -146,10 +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", "UDS user with administration rights (Will not be stored on template)"))
|
||||
self.restrictNet.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.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
|
||||
|
@@ -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
|
@@ -90,6 +90,7 @@ build-igel:
|
||||
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
|
||||
chmod 755 $(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
|
||||
@@ -102,6 +103,7 @@ build-thinpro:
|
||||
mkdir -p $(DESTDIR)
|
||||
cp -r thinpro/* $(DESTDIR)
|
||||
cp UDSClient-$(VERSION)-x86_64.AppImage $(DESTDIR)/UDSClient
|
||||
chmod 755 $(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 |
|
||||
|
@@ -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
|
||||
|
@@ -5,9 +5,9 @@ Source: http://github.com/dkmstr/openuds/client-py3
|
||||
|
||||
Files: *
|
||||
Copyright: (c) 2014-2022, Virtual Cable S.L.U.
|
||||
License: 3-BSD
|
||||
License: BSD-3-clause
|
||||
|
||||
License: 3-BSD
|
||||
License: BSD-3-clause
|
||||
All rights reserved.
|
||||
.
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
@@ -35,4 +35,4 @@ License: 3-BSD
|
||||
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
|
||||
|
@@ -35,7 +35,7 @@
|
||||
<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%.url" variableExpression="" variableSubstitutionActive="false">https://[UMS_SERVER]:8443/ums_filetransfer/UDSClient.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">
|
||||
|
@@ -1,2 +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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -30,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=0x648ACFD622F3D138'
|
||||
key_url: 'https://ftp-master.debian.org/keys/archive-key-11.asc'
|
||||
|
||||
include:
|
||||
- python3
|
||||
|
@@ -41,11 +41,11 @@ 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 as ssh_forward # type: ignore
|
||||
from uds.tunnel import forward as tunnel_forwards # 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
|
||||
from uds import tools
|
||||
@@ -55,7 +55,6 @@ from UDSWindow import Ui_MainWindow
|
||||
|
||||
|
||||
class UDSClient(QtWidgets.QMainWindow):
|
||||
|
||||
ticket: str = ''
|
||||
scrambler: str = ''
|
||||
withError = False
|
||||
@@ -149,7 +148,7 @@ 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
|
||||
@@ -168,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()
|
||||
@@ -177,7 +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:
|
||||
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
|
||||
@@ -352,31 +354,38 @@ def main(args: typing.List[str]):
|
||||
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,
|
||||
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:
|
||||
@@ -394,7 +403,7 @@ def main(args: typing.List[str]):
|
||||
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,5 +413,6 @@ def main(args: typing.List[str]):
|
||||
logger.debug('Exiting')
|
||||
sys.exit(exitVal)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv)
|
||||
|
@@ -11,6 +11,7 @@ from PyQt5 import QtCore, QtWidgets, QtGui
|
||||
|
||||
SCRIPT_NAME = 'UDSClientLauncher'
|
||||
|
||||
|
||||
class UdsApplication(QtWidgets.QApplication):
|
||||
path: str
|
||||
tunnels: typing.List[subprocess.Popen]
|
||||
@@ -22,6 +23,10 @@ class UdsApplication(QtWidgets.QApplication):
|
||||
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:
|
||||
@@ -30,13 +35,13 @@ class UdsApplication(QtWidgets.QApplication):
|
||||
logger.debug('Got error polling subprocess: %s', e)
|
||||
return False
|
||||
|
||||
for k in [i for i, tunnel in enumerate(self.tunnels) if not isRunning(tunnel)]:
|
||||
try:
|
||||
del self.tunnels[k]
|
||||
except Exception as e:
|
||||
logger.debug('Error closing tunnel: %s', e)
|
||||
# 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())
|
||||
@@ -45,7 +50,7 @@ class UdsApplication(QtWidgets.QApplication):
|
||||
tunnel.kill()
|
||||
|
||||
def event(self, evnt: QtCore.QEvent) -> bool:
|
||||
if evnt.type() == QtCore.QEvent.FileOpen:
|
||||
if evnt.type() == QtCore.QEvent.Type.FileOpen:
|
||||
fe = typing.cast(QtGui.QFileOpenEvent, evnt)
|
||||
logger.debug('Got url: %s', fe.url().url())
|
||||
fe.accept()
|
||||
@@ -70,6 +75,6 @@ def main(args: typing.List[str]):
|
||||
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(args=sys.argv)
|
||||
|
||||
|
@@ -2,13 +2,158 @@
|
||||
|
||||
# Resource object code
|
||||
#
|
||||
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)
|
||||
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.8)
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
from PyQt5 import QtCore
|
||||
|
||||
qt_resource_data = b"\
|
||||
\x00\x00\x08\xed\
|
||||
\x89\
|
||||
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
|
||||
\x00\x00\x25\x00\x00\x00\x30\x08\x06\x00\x00\x00\x96\x85\xb3\x2b\
|
||||
\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\
|
||||
\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\
|
||||
\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\
|
||||
\xdf\x03\x1b\x0e\x10\x3b\x1e\x53\x78\x6b\x00\x00\x08\x7a\x49\x44\
|
||||
\x41\x54\x58\xc3\xed\x98\x7b\x70\x54\xf5\x15\xc7\x3f\xbf\xfb\xd8\
|
||||
\x7b\xb3\x79\x42\x70\x09\x08\xa8\xa5\x20\x54\x4a\xaa\xa5\x6b\xad\
|
||||
\x81\xfa\x18\xf1\x3a\xad\x53\x67\xaa\x16\xad\xd1\x5a\x75\xc4\x19\
|
||||
\xa7\x63\xeb\xab\x1a\x94\x91\x91\xaa\xb5\x76\x3a\xd3\x97\xa9\x75\
|
||||
\x98\x96\xd2\x86\x19\x5b\x47\x85\x61\xd5\x1a\x35\xc1\x16\x57\xa8\
|
||||
\xe1\xfd\x0e\x04\x03\x92\x9b\x40\x4c\x76\x37\xd9\xec\x7d\xfc\xfa\
|
||||
\xc7\xde\xd0\x4d\xd8\x4d\x42\xa1\xd3\x3f\xf4\xcc\xec\x24\xfb\xdb\
|
||||
\x73\xcf\xef\xfb\x3b\xe7\x7b\xce\x3d\xbf\x03\x9f\xc9\x67\xf2\x7f\
|
||||
\x94\x58\x3c\x72\x5a\xcf\x5f\xb1\xc8\xe4\x8a\x45\xe6\xa8\x7a\x62\
|
||||
\x2c\x40\xac\xa8\x9d\xfb\xdd\x00\xa6\x07\x9f\xb3\x81\xb3\x1c\x47\
|
||||
\x96\xef\xde\xe3\x86\xf7\xb7\xba\x66\x67\x97\x27\x92\x29\xc9\xc0\
|
||||
\x80\x74\x84\xa0\x17\xf8\x18\x68\x03\xb6\x34\x36\xa4\xf7\xe4\x02\
|
||||
\x6c\x6c\x48\x8f\x1d\xd4\x20\x90\xc0\x33\xa5\xc0\xc5\xc0\x0d\xc0\
|
||||
\x95\x01\x98\xec\xc3\x02\x76\xed\x76\x59\xf7\x16\x74\xa7\x2a\xf1\
|
||||
\xb4\x08\x52\x14\x81\x00\x55\xf1\xd0\x45\x0f\xaa\xdf\x83\xf0\xd3\
|
||||
\x08\xd9\x8f\xe2\xf7\xf5\x80\xf7\x5b\x50\x7f\xd7\xd8\x90\x3e\x50\
|
||||
\x08\xd8\x88\x9e\x7a\x73\xd3\xc4\x99\x9e\x27\x7f\x13\x80\x39\x49\
|
||||
\x14\x05\x7e\xf9\x82\x4a\x4b\xea\x01\x12\xe6\xe5\xf8\x98\x20\x3d\
|
||||
\x94\xa2\x32\xb4\x8a\x89\x08\xc3\x40\x75\x93\x18\x99\x7d\x14\xa5\
|
||||
\x37\x52\x92\x7a\x9b\xe2\x54\x13\x9a\xdb\x71\x50\x0a\xf5\xa6\xc6\
|
||||
\x86\xf4\x86\x7c\xc0\x46\x02\x25\x5e\x6e\x9a\xb0\x24\x5c\xa4\x2c\
|
||||
\x93\xb2\x80\x82\x80\xf8\xf6\x49\xfc\xb4\x79\x15\xaa\xec\x47\xe0\
|
||||
\xa2\x16\x8f\x43\x1b\x37\x29\x8b\x58\x4a\x40\x20\x85\x0a\x68\x80\
|
||||
\x47\x79\xe2\x35\xaa\x3a\x1e\x46\x73\x0f\xef\x01\x65\x4e\x63\x43\
|
||||
\xda\x19\x6e\x57\x2d\x84\x68\x42\x0d\xea\x79\x53\x94\xab\xcb\x4a\
|
||||
\xd4\x1a\xd3\x28\x8c\x7d\x62\xe5\x00\x42\xfa\x6c\x6d\xaf\x46\x33\
|
||||
\x34\xb4\xca\x29\x08\x45\x19\x76\x72\x1f\x81\x8b\x40\x92\x36\x2e\
|
||||
\x20\xe4\xb4\x53\x94\x6e\xa9\x04\xd9\x74\x60\x9b\xdb\x7a\x52\x04\
|
||||
\x0a\x6d\xd6\xb5\x1e\x6f\xdb\x9e\xf4\xe6\xdd\xad\x03\x8e\xeb\x15\
|
||||
\x76\xa7\xa6\x38\x7c\xb3\xfa\xaf\x5c\x32\xbd\x09\x69\x54\x9c\x04\
|
||||
\x68\xa8\x64\x3d\x97\x36\x2f\xc0\x57\x0c\x80\x0b\xf3\xd2\x62\x24\
|
||||
\x4e\xe9\x21\x5e\xde\xba\xab\x7f\xed\xc1\xf6\xcc\x88\x19\x5a\x56\
|
||||
\xd4\x43\xed\x25\x2f\x32\x6d\xdc\xfe\xec\xbe\xa3\xa5\xbc\xf4\x72\
|
||||
\x51\x8e\x1d\x54\x6d\x9d\xce\xaa\xe5\xee\x80\xa6\xb1\xe4\x9f\x9b\
|
||||
\x52\x6d\x76\x97\x4b\x21\x27\x78\xbe\xca\xb4\xca\x83\xdc\xf1\x95\
|
||||
\x67\xa8\x30\xbb\xf0\x65\xe1\xb3\x4a\xa1\xa0\xbb\xed\x08\x99\x01\
|
||||
\xd8\x7d\x4a\xa0\x56\x2e\x77\xb2\xc0\x9e\x72\xb7\xfb\x52\xd6\x35\
|
||||
\x6f\x4c\xd2\xdd\xe3\xa1\xaa\xf9\xf9\x35\xe0\x9a\x54\x4f\xfe\x07\
|
||||
\xb7\xcf\x5d\x42\x99\xd1\x8d\xe3\x87\x0a\x96\x46\xdd\x69\x47\x48\
|
||||
\x07\x60\xc3\x29\x87\x6f\x50\xfe\xfc\xb4\xbb\xaa\xab\x3b\xb3\x74\
|
||||
\xfd\xc6\x24\x7d\x7d\x3e\x4a\x01\xde\xf7\xbb\xc5\xcc\xab\x5a\xc7\
|
||||
\x3d\x17\xde\xc7\xb9\xe5\x3b\xba\xd3\x5e\x78\xf7\x70\x40\x8a\x9f\
|
||||
\x44\x75\x6d\x84\xf4\xed\xc6\x86\x74\x67\xbe\x0a\x5f\x10\xd4\x8f\
|
||||
\x9e\x1b\xcf\xca\xe5\xd9\x6c\x3d\xd2\x7d\x83\x59\x77\xcf\x42\x31\
|
||||
\xbe\xac\x94\x0f\xb6\x26\xf0\xfc\xc2\x07\x70\xfc\x10\x73\x26\x34\
|
||||
\xf1\xf0\x57\xbf\x3b\xf0\xc3\x79\x77\x3d\x74\xf4\x58\xe9\xd9\xc0\
|
||||
\x75\xc0\x8b\x20\xfa\x34\xaf\x0b\xd5\xeb\x06\x78\xaf\x90\x8d\x82\
|
||||
\xa0\x7e\x7e\xff\x71\x00\xe5\xef\x1b\x6b\x96\x1c\xef\x49\x1d\x0b\
|
||||
\x1b\xda\xd2\x19\xb3\x6e\x61\xdf\xe1\x89\xec\x6b\xeb\x1f\xd1\xb3\
|
||||
\x12\x41\x59\xe8\x58\xd5\xc5\x93\xd7\x2e\x8d\xdd\x3a\xd1\x3c\xfa\
|
||||
\x44\xd9\x2b\xc0\x5d\x8e\x56\x3c\xc3\xc8\xec\x7b\x5b\xf5\x93\x80\
|
||||
\x7c\x17\x38\xf5\x8a\x7e\xfd\x83\x28\xba\xc2\x9a\xd9\xb3\x6a\xae\
|
||||
\x39\xcc\x6a\x8e\xf4\x4c\x26\xec\xac\xa2\x38\x75\x27\x57\x5e\x12\
|
||||
\xa6\x72\x9c\x36\x96\xe8\xaf\x05\x6e\xb5\xa2\xf6\x71\x80\x4b\x6f\
|
||||
\x9b\x7b\xa3\xee\x7c\xbc\x5a\xf1\x7b\x2e\x6c\x6c\xc8\xb4\x9c\x72\
|
||||
\xf6\xbd\xf4\x2c\xbe\xa6\xd2\xba\xa3\xd5\xc7\x4e\x4e\x26\xa4\x81\
|
||||
\x6b\x5e\x4f\x52\xb9\x8d\x0d\x2d\x7d\xa4\x07\x24\x42\x8c\xe2\x34\
|
||||
\xf8\x06\xf0\xc4\xe0\x82\x99\xde\x76\x4c\xf1\x7b\x5e\x02\xf5\x50\
|
||||
\xa1\x8e\x61\x74\xa2\x0b\xde\xd3\xfd\x96\xa4\x22\x82\xda\xa2\x18\
|
||||
\xf8\xe5\x4b\xb1\x13\xb3\x89\x6f\x4e\x8d\xb5\x0b\xb9\x37\x16\x8f\
|
||||
\xdc\x92\xad\x7d\xc6\x3b\xa0\xdc\x02\xb2\xfb\x94\xba\x84\xa1\x1e\
|
||||
\x0b\x85\x91\x99\x5d\x99\xca\x75\x53\x5d\xc3\x3a\xf1\x94\x9a\xd9\
|
||||
\x48\xa8\x73\x3e\x73\x66\xea\x44\xab\xc3\x64\x9c\x51\xab\xe6\xc7\
|
||||
\x52\x32\xe5\x9a\x8b\x6d\x7f\x34\xc5\x51\x3d\xb5\x72\x79\xa6\x0f\
|
||||
\xf8\x83\x9e\xa8\xe3\x44\x4d\x94\xe0\x85\xe6\xe1\x54\xfc\x9a\xbd\
|
||||
\xad\x09\x5a\x3f\xca\x14\xac\x5f\xb9\xaf\x53\x21\xb8\x6a\x2c\x24\
|
||||
\x1c\x53\x9d\x42\x29\x5d\x2a\x9c\x2d\x5d\x7a\x6a\xc5\x7f\x7c\x2b\
|
||||
\xc1\x2d\xfe\x3e\xfd\x7a\x2d\x5b\x76\x7e\x42\x22\xe9\x8d\xc6\x2f\
|
||||
\x01\x4c\x3a\x23\xa0\x6a\xeb\x74\x56\x3e\x99\xf0\x41\x7c\x4f\xef\
|
||||
\x7d\x0c\xc5\xd9\x3d\x04\x58\xa6\xfc\x57\x74\x25\xe7\xb2\x63\x6f\
|
||||
\x02\xcf\x1b\xd1\x94\x0b\x6c\x1a\x0b\x28\x75\x34\x85\x2d\xcd\x7e\
|
||||
\x16\xd8\x72\x67\x6f\xf5\xfc\xbe\x22\xc5\xdd\x5e\xe3\x19\x57\x83\
|
||||
\x28\x09\xce\x6f\x20\xf5\xd9\x24\x3a\xd7\x30\xa1\x22\x4d\x69\x49\
|
||||
\xc1\x32\xf1\x82\x15\xb5\x57\x9c\xb9\xf0\x9d\x48\x70\xef\x49\x25\
|
||||
\xf3\xce\x9f\x8c\x9e\x7b\x10\xb2\xf7\xc4\xb2\x6f\x7c\x9d\xfe\xd0\
|
||||
\xdd\xfc\x6b\x87\x87\xeb\xe5\x25\xfc\x0b\xc0\x03\x63\xbd\x7c\xa8\
|
||||
\x63\xc1\xb2\xa5\xd9\x0f\xfe\x92\xa9\x9e\x2f\x9b\x84\xb7\xf7\x7c\
|
||||
\x35\xf3\xde\x2c\xd7\xfc\x36\x08\x23\xeb\x30\x7d\x3a\xe9\xee\x35\
|
||||
\x14\x85\xba\x99\x58\xa9\xe5\xf6\x24\xcf\x00\x8f\x59\x51\x3b\x39\
|
||||
\xfc\x12\x72\xda\x9e\xaa\xad\xd3\xb3\xd9\xf8\x13\x8e\x01\x37\x2a\
|
||||
\x99\xf5\x7f\x2b\xea\x9c\x85\xe2\xbc\x0f\x02\x3c\x7d\x1a\x8a\x3e\
|
||||
\x89\x9d\x7b\xd3\x88\xec\x1b\x3b\x03\x3c\x02\xd4\x59\x51\xbb\x77\
|
||||
\xac\x80\xc6\x54\xa7\xf2\x12\x3f\x78\x51\xd7\xd6\xa9\x8f\x81\xff\
|
||||
\xa0\x1b\xbe\xa3\xd4\xd7\x2f\x45\x4b\x2c\xc3\x50\xdb\xb9\xf9\xda\
|
||||
\xf1\x47\x5d\x4f\x3e\x60\x45\xed\x55\xf9\xae\x69\x67\x1c\xd4\x10\
|
||||
\xaf\x2d\x77\xa8\xad\x53\xbe\x84\xf4\x1f\x15\x92\x8b\x7c\xa1\x98\
|
||||
\xe7\x4e\x29\xda\xb6\xb0\xa6\xf4\xfe\x05\x73\x8f\x6e\x1f\xe4\xcf\
|
||||
\xa9\x00\x2a\x78\xfb\x1d\x8d\x8c\xb1\x78\xe4\x24\x9d\x9b\x1f\xd1\
|
||||
\xab\x6f\x7f\x3c\xb4\x60\x73\x6b\x44\xcb\x67\xa3\x90\xcd\x11\xf7\
|
||||
\x3a\x5d\x50\x23\xd9\x1c\x66\x7b\x7c\x2c\x1e\xf9\x62\x2c\x1e\x39\
|
||||
\xa7\xd0\xfe\x62\xd8\x0f\xe7\x02\x1b\xac\xa8\x5d\x55\x60\x83\x1f\
|
||||
\x00\x33\xad\xa8\x7d\x6f\xb0\x76\x14\xd8\x09\xf4\x07\x49\x53\x01\
|
||||
\x84\x81\x57\x81\xa7\xac\xa8\x9d\xca\xb9\x6d\xcf\xc8\x36\x7a\xf4\
|
||||
\x02\xbb\x80\x09\x40\x0d\x50\x6f\x45\xed\x67\x73\xb9\xa7\xe5\xc9\
|
||||
\xc6\xca\x11\x0e\x6f\x02\xc5\x39\xdf\xcf\x02\x2e\xb5\xa2\xf6\xfe\
|
||||
\x1c\xf0\x4a\x00\xfe\xfd\x58\x3c\x72\xbd\x15\xb5\x77\x05\x3f\x6d\
|
||||
\x08\xfa\xaa\xb5\x39\xba\x26\xf0\x7a\x2c\x1e\x29\xb5\xa2\xf6\xe3\
|
||||
\x83\xdc\x53\x38\x7d\xd1\x86\xcd\x20\x7c\x2b\x6a\xff\x02\x58\x01\
|
||||
\xd4\x05\xeb\x93\x80\x72\x60\x48\x53\x67\x45\xed\x34\xb0\x18\xe8\
|
||||
\x0d\x0e\x33\xd4\xe0\x99\x90\x61\x59\xd6\x0c\x58\xb1\x78\x64\x8a\
|
||||
\x15\xb5\xdb\x63\xf1\xc8\xcf\x80\xa7\x63\xf1\x48\x0b\x90\x00\x8e\
|
||||
\x00\x6d\x56\xd4\xde\x0a\xec\x1c\xe4\x69\xbe\xf0\x89\x42\x23\xa0\
|
||||
\xff\xa2\x8c\x78\xc1\x4b\x58\x0f\x00\xff\x38\x16\x8f\x7c\x39\x98\
|
||||
\xda\x8c\x03\x66\x03\x97\xc5\xe2\x91\x69\xc0\x8b\x56\xd4\x7e\xbd\
|
||||
\x90\xa7\xbc\x02\xa7\x3e\x71\x69\x06\xfc\x31\xce\xb3\xce\x0a\x0e\
|
||||
\xd0\x11\x8b\x47\xae\x02\x34\x2b\x6a\xaf\x1b\xec\x14\x62\xf1\x48\
|
||||
\x51\x30\x66\x8a\x02\x8f\xc6\xe2\x91\xb6\x41\xfe\x29\xc3\xdc\x7f\
|
||||
\x10\xb0\x63\xf1\xc8\x75\x05\x42\x73\x25\xf0\xfe\x68\x21\x8c\xc5\
|
||||
\x23\xa5\xc0\x43\xc0\xab\x56\xd4\xee\x03\xce\x07\x9e\x1b\xa6\xd7\
|
||||
\x6f\x45\x6d\x1b\xd8\x1c\x80\x2f\x39\x29\x14\xaf\x35\xcd\xe1\xda\
|
||||
\x05\xdb\x08\x5c\xbc\x1a\x78\x12\x78\x27\x48\xf7\x69\x41\x46\xe9\
|
||||
\x56\xd4\x5e\x94\xe3\x95\x04\x70\x6f\x30\xa9\x53\x03\xc3\x17\x00\
|
||||
\xdf\x01\xfe\x68\x45\xed\xe7\x72\x74\xd7\x01\x4e\x00\x6e\x7f\x70\
|
||||
\xa9\xa8\x06\xee\x03\x5a\xac\xa8\xfd\x50\x5e\x7e\xac\x58\xf9\xb8\
|
||||
\x72\x7b\xed\x32\x7f\xcd\xfa\x99\xc5\xba\x71\xec\x56\x29\xb5\xb9\
|
||||
\x42\x78\x2a\xc8\x0e\x21\xbc\x37\x16\xce\xfb\xa4\x39\x57\x7f\x4d\
|
||||
\xf3\xac\x07\x75\xb3\xcb\x43\xf8\x02\x29\x84\x94\x6a\x1a\xe4\x81\
|
||||
\xe2\xf1\x9d\x6f\xce\xff\x3c\x27\xa6\x22\xcf\x3f\xff\xfb\xb2\xc5\
|
||||
\x8b\xef\xec\x7d\x63\x53\xf9\x65\xd2\xd7\xae\x90\x52\x9d\x0c\xd2\
|
||||
\x45\xc8\x43\x8a\x9a\x5e\xb7\xf0\xa2\xe4\x87\x00\x0d\xaf\xdc\xc4\
|
||||
\xa2\x6f\xfd\x65\x28\xa8\xfa\xfa\xfa\xf3\x84\x92\x69\xf3\xdd\x70\
|
||||
\xcd\x40\xdf\x94\x23\xbd\x1d\x0b\xba\x8c\xe2\x43\x45\xba\x69\x9b\
|
||||
\xba\xd9\xa9\xeb\x66\xa7\x16\x64\xce\x78\x84\xff\x51\x26\x35\xf5\
|
||||
\x6c\x29\x55\x0f\x29\xfc\xfe\xc4\xf4\xce\x70\xc5\xf6\x73\x14\x35\
|
||||
\x83\xa2\xa6\x3b\x15\xb5\xcf\x00\xe1\x03\xdd\xd9\x22\x29\x76\x3a\
|
||||
\xe9\x09\x5a\xdf\x27\x5f\x38\xae\x86\x7a\x8b\x9d\xfe\xaa\x94\xef\
|
||||
\x99\x99\xf2\xaa\xc6\xa9\x5a\xa8\xc7\x01\x79\xe8\xee\xbb\x17\xf7\
|
||||
\xe5\x23\xba\x2f\xfd\xd0\xd7\x84\xe2\xce\x30\x4b\x0e\x9a\x66\xc9\
|
||||
\xc1\xcf\x01\x21\x20\x02\x1c\x00\xa6\x06\xc5\xb5\x03\xa9\x74\x84\
|
||||
\xc2\x87\xcf\x0f\x6c\x54\x19\x25\x6d\xfb\x80\x99\xc0\xd1\x6c\x2f\
|
||||
\x3e\xd8\x9a\xb2\x39\x1b\x2a\x79\xb9\x6e\x76\x66\xca\xab\xde\x3d\
|
||||
\x14\x84\xd9\x05\x7a\x82\xaa\x7e\x18\x84\x07\xec\xcd\x07\xea\xa3\
|
||||
\x60\x9a\xfb\x41\x90\x65\x22\xa7\xd2\xbb\x01\x6f\xd4\x20\x4b\x1d\
|
||||
\x60\x4d\xa0\x37\xb8\xf6\x56\x4e\x06\x1b\xc1\xe7\x78\xc0\x21\x2d\
|
||||
\xd0\xcd\xb5\x2b\x81\x0f\x83\x35\xf7\x7f\x3a\x6f\xaf\xaf\xaf\xcf\
|
||||
\xfb\xff\x67\xf2\xa9\x90\x7f\x03\xbb\x9c\x9c\x9c\x32\xd8\x63\xca\
|
||||
\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
|
||||
\x00\x00\x8e\x8c\
|
||||
\x89\
|
||||
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
|
||||
@@ -2292,151 +2437,6 @@ qt_resource_data = b"\
|
||||
\x00\x80\x10\x0c\x00\x00\x00\x10\x82\x01\x00\x00\x00\x42\x30\x00\
|
||||
\x00\x00\xe0\x96\xff\x7f\x00\x27\x97\xdb\xb5\x4d\x29\xcb\x9d\x00\
|
||||
\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
|
||||
\x00\x00\x08\xed\
|
||||
\x89\
|
||||
\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\
|
||||
\x00\x00\x25\x00\x00\x00\x30\x08\x06\x00\x00\x00\x96\x85\xb3\x2b\
|
||||
\x00\x00\x00\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\
|
||||
\xa7\x93\x00\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\
|
||||
\x0b\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\
|
||||
\xdf\x03\x1b\x0e\x10\x3b\x1e\x53\x78\x6b\x00\x00\x08\x7a\x49\x44\
|
||||
\x41\x54\x58\xc3\xed\x98\x7b\x70\x54\xf5\x15\xc7\x3f\xbf\xfb\xd8\
|
||||
\x7b\xb3\x79\x42\x70\x09\x08\xa8\xa5\x20\x54\x4a\xaa\xa5\x6b\xad\
|
||||
\x81\xfa\x18\xf1\x3a\xad\x53\x67\xaa\x16\xad\xd1\x5a\x75\xc4\x19\
|
||||
\xa7\x63\xeb\xab\x1a\x94\x91\x91\xaa\xb5\x76\x3a\xd3\x97\xa9\x75\
|
||||
\x98\x96\xd2\x86\x19\x5b\x47\x85\x61\xd5\x1a\x35\xc1\x16\x57\xa8\
|
||||
\xe1\xfd\x0e\x04\x03\x92\x9b\x40\x4c\x76\x37\xd9\xec\x7d\xfc\xfa\
|
||||
\xc7\xde\xd0\x4d\xd8\x4d\x42\xa1\xd3\x3f\xf4\xcc\xec\x24\xfb\xdb\
|
||||
\x73\xcf\xef\xfb\x3b\xe7\x7b\xce\x3d\xbf\x03\x9f\xc9\x67\xf2\x7f\
|
||||
\x94\x58\x3c\x72\x5a\xcf\x5f\xb1\xc8\xe4\x8a\x45\xe6\xa8\x7a\x62\
|
||||
\x2c\x40\xac\xa8\x9d\xfb\xdd\x00\xa6\x07\x9f\xb3\x81\xb3\x1c\x47\
|
||||
\x96\xef\xde\xe3\x86\xf7\xb7\xba\x66\x67\x97\x27\x92\x29\xc9\xc0\
|
||||
\x80\x74\x84\xa0\x17\xf8\x18\x68\x03\xb6\x34\x36\xa4\xf7\xe4\x02\
|
||||
\x6c\x6c\x48\x8f\x1d\xd4\x20\x90\xc0\x33\xa5\xc0\xc5\xc0\x0d\xc0\
|
||||
\x95\x01\x98\xec\xc3\x02\x76\xed\x76\x59\xf7\x16\x74\xa7\x2a\xf1\
|
||||
\xb4\x08\x52\x14\x81\x00\x55\xf1\xd0\x45\x0f\xaa\xdf\x83\xf0\xd3\
|
||||
\x08\xd9\x8f\xe2\xf7\xf5\x80\xf7\x5b\x50\x7f\xd7\xd8\x90\x3e\x50\
|
||||
\x08\xd8\x88\x9e\x7a\x73\xd3\xc4\x99\x9e\x27\x7f\x13\x80\x39\x49\
|
||||
\x14\x05\x7e\xf9\x82\x4a\x4b\xea\x01\x12\xe6\xe5\xf8\x98\x20\x3d\
|
||||
\x94\xa2\x32\xb4\x8a\x89\x08\xc3\x40\x75\x93\x18\x99\x7d\x14\xa5\
|
||||
\x37\x52\x92\x7a\x9b\xe2\x54\x13\x9a\xdb\x71\x50\x0a\xf5\xa6\xc6\
|
||||
\x86\xf4\x86\x7c\xc0\x46\x02\x25\x5e\x6e\x9a\xb0\x24\x5c\xa4\x2c\
|
||||
\x93\xb2\x80\x82\x80\xf8\xf6\x49\xfc\xb4\x79\x15\xaa\xec\x47\xe0\
|
||||
\xa2\x16\x8f\x43\x1b\x37\x29\x8b\x58\x4a\x40\x20\x85\x0a\x68\x80\
|
||||
\x47\x79\xe2\x35\xaa\x3a\x1e\x46\x73\x0f\xef\x01\x65\x4e\x63\x43\
|
||||
\xda\x19\x6e\x57\x2d\x84\x68\x42\x0d\xea\x79\x53\x94\xab\xcb\x4a\
|
||||
\xd4\x1a\xd3\x28\x8c\x7d\x62\xe5\x00\x42\xfa\x6c\x6d\xaf\x46\x33\
|
||||
\x34\xb4\xca\x29\x08\x45\x19\x76\x72\x1f\x81\x8b\x40\x92\x36\x2e\
|
||||
\x20\xe4\xb4\x53\x94\x6e\xa9\x04\xd9\x74\x60\x9b\xdb\x7a\x52\x04\
|
||||
\x0a\x6d\xd6\xb5\x1e\x6f\xdb\x9e\xf4\xe6\xdd\xad\x03\x8e\xeb\x15\
|
||||
\x76\xa7\xa6\x38\x7c\xb3\xfa\xaf\x5c\x32\xbd\x09\x69\x54\x9c\x04\
|
||||
\x68\xa8\x64\x3d\x97\x36\x2f\xc0\x57\x0c\x80\x0b\xf3\xd2\x62\x24\
|
||||
\x4e\xe9\x21\x5e\xde\xba\xab\x7f\xed\xc1\xf6\xcc\x88\x19\x5a\x56\
|
||||
\xd4\x43\xed\x25\x2f\x32\x6d\xdc\xfe\xec\xbe\xa3\xa5\xbc\xf4\x72\
|
||||
\x51\x8e\x1d\x54\x6d\x9d\xce\xaa\xe5\xee\x80\xa6\xb1\xe4\x9f\x9b\
|
||||
\x52\x6d\x76\x97\x4b\x21\x27\x78\xbe\xca\xb4\xca\x83\xdc\xf1\x95\
|
||||
\x67\xa8\x30\xbb\xf0\x65\xe1\xb3\x4a\xa1\xa0\xbb\xed\x08\x99\x01\
|
||||
\xd8\x7d\x4a\xa0\x56\x2e\x77\xb2\xc0\x9e\x72\xb7\xfb\x52\xd6\x35\
|
||||
\x6f\x4c\xd2\xdd\xe3\xa1\xaa\xf9\xf9\x35\xe0\x9a\x54\x4f\xfe\x07\
|
||||
\xb7\xcf\x5d\x42\x99\xd1\x8d\xe3\x87\x0a\x96\x46\xdd\x69\x47\x48\
|
||||
\x07\x60\xc3\x29\x87\x6f\x50\xfe\xfc\xb4\xbb\xaa\xab\x3b\xb3\x74\
|
||||
\xfd\xc6\x24\x7d\x7d\x3e\x4a\x01\xde\xf7\xbb\xc5\xcc\xab\x5a\xc7\
|
||||
\x3d\x17\xde\xc7\xb9\xe5\x3b\xba\xd3\x5e\x78\xf7\x70\x40\x8a\x9f\
|
||||
\x44\x75\x6d\x84\xf4\xed\xc6\x86\x74\x67\xbe\x0a\x5f\x10\xd4\x8f\
|
||||
\x9e\x1b\xcf\xca\xe5\xd9\x6c\x3d\xd2\x7d\x83\x59\x77\xcf\x42\x31\
|
||||
\xbe\xac\x94\x0f\xb6\x26\xf0\xfc\xc2\x07\x70\xfc\x10\x73\x26\x34\
|
||||
\xf1\xf0\x57\xbf\x3b\xf0\xc3\x79\x77\x3d\x74\xf4\x58\xe9\xd9\xc0\
|
||||
\x75\xc0\x8b\x20\xfa\x34\xaf\x0b\xd5\xeb\x06\x78\xaf\x90\x8d\x82\
|
||||
\xa0\x7e\x7e\xff\x71\x00\xe5\xef\x1b\x6b\x96\x1c\xef\x49\x1d\x0b\
|
||||
\x1b\xda\xd2\x19\xb3\x6e\x61\xdf\xe1\x89\xec\x6b\xeb\x1f\xd1\xb3\
|
||||
\x12\x41\x59\xe8\x58\xd5\xc5\x93\xd7\x2e\x8d\xdd\x3a\xd1\x3c\xfa\
|
||||
\x44\xd9\x2b\xc0\x5d\x8e\x56\x3c\xc3\xc8\xec\x7b\x5b\xf5\x93\x80\
|
||||
\x7c\x17\x38\xf5\x8a\x7e\xfd\x83\x28\xba\xc2\x9a\xd9\xb3\x6a\xae\
|
||||
\x39\xcc\x6a\x8e\xf4\x4c\x26\xec\xac\xa2\x38\x75\x27\x57\x5e\x12\
|
||||
\xa6\x72\x9c\x36\x96\xe8\xaf\x05\x6e\xb5\xa2\xf6\x71\x80\x4b\x6f\
|
||||
\x9b\x7b\xa3\xee\x7c\xbc\x5a\xf1\x7b\x2e\x6c\x6c\xc8\xb4\x9c\x72\
|
||||
\xf6\xbd\xf4\x2c\xbe\xa6\xd2\xba\xa3\xd5\xc7\x4e\x4e\x26\xa4\x81\
|
||||
\x6b\x5e\x4f\x52\xb9\x8d\x0d\x2d\x7d\xa4\x07\x24\x42\x8c\xe2\x34\
|
||||
\xf8\x06\xf0\xc4\xe0\x82\x99\xde\x76\x4c\xf1\x7b\x5e\x02\xf5\x50\
|
||||
\xa1\x8e\x61\x74\xa2\x0b\xde\xd3\xfd\x96\xa4\x22\x82\xda\xa2\x18\
|
||||
\xf8\xe5\x4b\xb1\x13\xb3\x89\x6f\x4e\x8d\xb5\x0b\xb9\x37\x16\x8f\
|
||||
\xdc\x92\xad\x7d\xc6\x3b\xa0\xdc\x02\xb2\xfb\x94\xba\x84\xa1\x1e\
|
||||
\x0b\x85\x91\x99\x5d\x99\xca\x75\x53\x5d\xc3\x3a\xf1\x94\x9a\xd9\
|
||||
\x48\xa8\x73\x3e\x73\x66\xea\x44\xab\xc3\x64\x9c\x51\xab\xe6\xc7\
|
||||
\x52\x32\xe5\x9a\x8b\x6d\x7f\x34\xc5\x51\x3d\xb5\x72\x79\xa6\x0f\
|
||||
\xf8\x83\x9e\xa8\xe3\x44\x4d\x94\xe0\x85\xe6\xe1\x54\xfc\x9a\xbd\
|
||||
\xad\x09\x5a\x3f\xca\x14\xac\x5f\xb9\xaf\x53\x21\xb8\x6a\x2c\x24\
|
||||
\x1c\x53\x9d\x42\x29\x5d\x2a\x9c\x2d\x5d\x7a\x6a\xc5\x7f\x7c\x2b\
|
||||
\xc1\x2d\xfe\x3e\xfd\x7a\x2d\x5b\x76\x7e\x42\x22\xe9\x8d\xc6\x2f\
|
||||
\x01\x4c\x3a\x23\xa0\x6a\xeb\x74\x56\x3e\x99\xf0\x41\x7c\x4f\xef\
|
||||
\x7d\x0c\xc5\xd9\x3d\x04\x58\xa6\xfc\x57\x74\x25\xe7\xb2\x63\x6f\
|
||||
\x02\xcf\x1b\xd1\x94\x0b\x6c\x1a\x0b\x28\x75\x34\x85\x2d\xcd\x7e\
|
||||
\x16\xd8\x72\x67\x6f\xf5\xfc\xbe\x22\xc5\xdd\x5e\xe3\x19\x57\x83\
|
||||
\x28\x09\xce\x6f\x20\xf5\xd9\x24\x3a\xd7\x30\xa1\x22\x4d\x69\x49\
|
||||
\xc1\x32\xf1\x82\x15\xb5\x57\x9c\xb9\xf0\x9d\x48\x70\xef\x49\x25\
|
||||
\xf3\xce\x9f\x8c\x9e\x7b\x10\xb2\xf7\xc4\xb2\x6f\x7c\x9d\xfe\xd0\
|
||||
\xdd\xfc\x6b\x87\x87\xeb\xe5\x25\xfc\x0b\xc0\x03\x63\xbd\x7c\xa8\
|
||||
\x63\xc1\xb2\xa5\xd9\x0f\xfe\x92\xa9\x9e\x2f\x9b\x84\xb7\xf7\x7c\
|
||||
\x35\xf3\xde\x2c\xd7\xfc\x36\x08\x23\xeb\x30\x7d\x3a\xe9\xee\x35\
|
||||
\x14\x85\xba\x99\x58\xa9\xe5\xf6\x24\xcf\x00\x8f\x59\x51\x3b\x39\
|
||||
\xfc\x12\x72\xda\x9e\xaa\xad\xd3\xb3\xd9\xf8\x13\x8e\x01\x37\x2a\
|
||||
\x99\xf5\x7f\x2b\xea\x9c\x85\xe2\xbc\x0f\x02\x3c\x7d\x1a\x8a\x3e\
|
||||
\x89\x9d\x7b\xd3\x88\xec\x1b\x3b\x03\x3c\x02\xd4\x59\x51\xbb\x77\
|
||||
\xac\x80\xc6\x54\xa7\xf2\x12\x3f\x78\x51\xd7\xd6\xa9\x8f\x81\xff\
|
||||
\xa0\x1b\xbe\xa3\xd4\xd7\x2f\x45\x4b\x2c\xc3\x50\xdb\xb9\xf9\xda\
|
||||
\xf1\x47\x5d\x4f\x3e\x60\x45\xed\x55\xf9\xae\x69\x67\x1c\xd4\x10\
|
||||
\xaf\x2d\x77\xa8\xad\x53\xbe\x84\xf4\x1f\x15\x92\x8b\x7c\xa1\x98\
|
||||
\xe7\x4e\x29\xda\xb6\xb0\xa6\xf4\xfe\x05\x73\x8f\x6e\x1f\xe4\xcf\
|
||||
\xa9\x00\x2a\x78\xfb\x1d\x8d\x8c\xb1\x78\xe4\x24\x9d\x9b\x1f\xd1\
|
||||
\xab\x6f\x7f\x3c\xb4\x60\x73\x6b\x44\xcb\x67\xa3\x90\xcd\x11\xf7\
|
||||
\x3a\x5d\x50\x23\xd9\x1c\x66\x7b\x7c\x2c\x1e\xf9\x62\x2c\x1e\x39\
|
||||
\xa7\xd0\xfe\x62\xd8\x0f\xe7\x02\x1b\xac\xa8\x5d\x55\x60\x83\x1f\
|
||||
\x00\x33\xad\xa8\x7d\x6f\xb0\x76\x14\xd8\x09\xf4\x07\x49\x53\x01\
|
||||
\x84\x81\x57\x81\xa7\xac\xa8\x9d\xca\xb9\x6d\xcf\xc8\x36\x7a\xf4\
|
||||
\x02\xbb\x80\x09\x40\x0d\x50\x6f\x45\xed\x67\x73\xb9\xa7\xe5\xc9\
|
||||
\xc6\xca\x11\x0e\x6f\x02\xc5\x39\xdf\xcf\x02\x2e\xb5\xa2\xf6\xfe\
|
||||
\x1c\xf0\x4a\x00\xfe\xfd\x58\x3c\x72\xbd\x15\xb5\x77\x05\x3f\x6d\
|
||||
\x08\xfa\xaa\xb5\x39\xba\x26\xf0\x7a\x2c\x1e\x29\xb5\xa2\xf6\xe3\
|
||||
\x83\xdc\x53\x38\x7d\xd1\x86\xcd\x20\x7c\x2b\x6a\xff\x02\x58\x01\
|
||||
\xd4\x05\xeb\x93\x80\x72\x60\x48\x53\x67\x45\xed\x34\xb0\x18\xe8\
|
||||
\x0d\x0e\x33\xd4\xe0\x99\x90\x61\x59\xd6\x0c\x58\xb1\x78\x64\x8a\
|
||||
\x15\xb5\xdb\x63\xf1\xc8\xcf\x80\xa7\x63\xf1\x48\x0b\x90\x00\x8e\
|
||||
\x00\x6d\x56\xd4\xde\x0a\xec\x1c\xe4\x69\xbe\xf0\x89\x42\x23\xa0\
|
||||
\xff\xa2\x8c\x78\xc1\x4b\x58\x0f\x00\xff\x38\x16\x8f\x7c\x39\x98\
|
||||
\xda\x8c\x03\x66\x03\x97\xc5\xe2\x91\x69\xc0\x8b\x56\xd4\x7e\xbd\
|
||||
\x90\xa7\xbc\x02\xa7\x3e\x71\x69\x06\xfc\x31\xce\xb3\xce\x0a\x0e\
|
||||
\xd0\x11\x8b\x47\xae\x02\x34\x2b\x6a\xaf\x1b\xec\x14\x62\xf1\x48\
|
||||
\x51\x30\x66\x8a\x02\x8f\xc6\xe2\x91\xb6\x41\xfe\x29\xc3\xdc\x7f\
|
||||
\x10\xb0\x63\xf1\xc8\x75\x05\x42\x73\x25\xf0\xfe\x68\x21\x8c\xc5\
|
||||
\x23\xa5\xc0\x43\xc0\xab\x56\xd4\xee\x03\xce\x07\x9e\x1b\xa6\xd7\
|
||||
\x6f\x45\x6d\x1b\xd8\x1c\x80\x2f\x39\x29\x14\xaf\x35\xcd\xe1\xda\
|
||||
\x05\xdb\x08\x5c\xbc\x1a\x78\x12\x78\x27\x48\xf7\x69\x41\x46\xe9\
|
||||
\x56\xd4\x5e\x94\xe3\x95\x04\x70\x6f\x30\xa9\x53\x03\xc3\x17\x00\
|
||||
\xdf\x01\xfe\x68\x45\xed\xe7\x72\x74\xd7\x01\x4e\x00\x6e\x7f\x70\
|
||||
\xa9\xa8\x06\xee\x03\x5a\xac\xa8\xfd\x50\x5e\x7e\xac\x58\xf9\xb8\
|
||||
\x72\x7b\xed\x32\x7f\xcd\xfa\x99\xc5\xba\x71\xec\x56\x29\xb5\xb9\
|
||||
\x42\x78\x2a\xc8\x0e\x21\xbc\x37\x16\xce\xfb\xa4\x39\x57\x7f\x4d\
|
||||
\xf3\xac\x07\x75\xb3\xcb\x43\xf8\x02\x29\x84\x94\x6a\x1a\xe4\x81\
|
||||
\xe2\xf1\x9d\x6f\xce\xff\x3c\x27\xa6\x22\xcf\x3f\xff\xfb\xb2\xc5\
|
||||
\x8b\xef\xec\x7d\x63\x53\xf9\x65\xd2\xd7\xae\x90\x52\x9d\x0c\xd2\
|
||||
\x45\xc8\x43\x8a\x9a\x5e\xb7\xf0\xa2\xe4\x87\x00\x0d\xaf\xdc\xc4\
|
||||
\xa2\x6f\xfd\x65\x28\xa8\xfa\xfa\xfa\xf3\x84\x92\x69\xf3\xdd\x70\
|
||||
\xcd\x40\xdf\x94\x23\xbd\x1d\x0b\xba\x8c\xe2\x43\x45\xba\x69\x9b\
|
||||
\xba\xd9\xa9\xeb\x66\xa7\x16\x64\xce\x78\x84\xff\x51\x26\x35\xf5\
|
||||
\x6c\x29\x55\x0f\x29\xfc\xfe\xc4\xf4\xce\x70\xc5\xf6\x73\x14\x35\
|
||||
\x83\xa2\xa6\x3b\x15\xb5\xcf\x00\xe1\x03\xdd\xd9\x22\x29\x76\x3a\
|
||||
\xe9\x09\x5a\xdf\x27\x5f\x38\xae\x86\x7a\x8b\x9d\xfe\xaa\x94\xef\
|
||||
\x99\x99\xf2\xaa\xc6\xa9\x5a\xa8\xc7\x01\x79\xe8\xee\xbb\x17\xf7\
|
||||
\xe5\x23\xba\x2f\xfd\xd0\xd7\x84\xe2\xce\x30\x4b\x0e\x9a\x66\xc9\
|
||||
\xc1\xcf\x01\x21\x20\x02\x1c\x00\xa6\x06\xc5\xb5\x03\xa9\x74\x84\
|
||||
\xc2\x87\xcf\x0f\x6c\x54\x19\x25\x6d\xfb\x80\x99\xc0\xd1\x6c\x2f\
|
||||
\x3e\xd8\x9a\xb2\x39\x1b\x2a\x79\xb9\x6e\x76\x66\xca\xab\xde\x3d\
|
||||
\x14\x84\xd9\x05\x7a\x82\xaa\x7e\x18\x84\x07\xec\xcd\x07\xea\xa3\
|
||||
\x60\x9a\xfb\x41\x90\x65\x22\xa7\xd2\xbb\x01\x6f\xd4\x20\x4b\x1d\
|
||||
\x60\x4d\xa0\x37\xb8\xf6\x56\x4e\x06\x1b\xc1\xe7\x78\xc0\x21\x2d\
|
||||
\xd0\xcd\xb5\x2b\x81\x0f\x83\x35\xf7\x7f\x3a\x6f\xaf\xaf\xaf\xcf\
|
||||
\xfb\xff\x67\xf2\xa9\x90\x7f\x03\xbb\x9c\x9c\x9c\x32\xd8\x63\xca\
|
||||
\x00\x00\x00\x00\x49\x45\x4e\x44\xae\x42\x60\x82\
|
||||
"
|
||||
|
||||
qt_resource_name = b"\
|
||||
@@ -2444,21 +2444,21 @@ qt_resource_name = b"\
|
||||
\x07\x03\x7d\xc3\
|
||||
\x00\x69\
|
||||
\x00\x6d\x00\x61\x00\x67\x00\x65\x00\x73\
|
||||
\x00\x0c\
|
||||
\x05\xe1\xfc\x77\
|
||||
\x00\x6c\
|
||||
\x00\x6f\x00\x67\x00\x6f\x00\x2d\x00\x75\x00\x64\x00\x73\x00\x2d\x00\x62\x00\x69\x00\x67\
|
||||
\x00\x0e\
|
||||
\x01\x8e\xbf\xec\
|
||||
\x00\x6c\
|
||||
\x00\x6f\x00\x67\x00\x6f\x00\x2d\x00\x75\x00\x64\x00\x73\x00\x2d\x00\x73\x00\x6d\x00\x61\x00\x6c\x00\x6c\
|
||||
\x00\x0c\
|
||||
\x05\xe1\xfc\x77\
|
||||
\x00\x6c\
|
||||
\x00\x6f\x00\x67\x00\x6f\x00\x2d\x00\x75\x00\x64\x00\x73\x00\x2d\x00\x62\x00\x69\x00\x67\
|
||||
"
|
||||
|
||||
qt_resource_struct_v1 = b"\
|
||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\
|
||||
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x02\
|
||||
\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x8e\x90\
|
||||
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
|
||||
\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x08\xf1\
|
||||
"
|
||||
|
||||
qt_resource_struct_v2 = b"\
|
||||
@@ -2466,10 +2466,10 @@ qt_resource_struct_v2 = b"\
|
||||
\x00\x00\x00\x00\x00\x00\x00\x00\
|
||||
\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\x30\x00\x00\x00\x00\x00\x01\x00\x00\x8e\x90\
|
||||
\x00\x00\x01\x70\xc4\x82\x24\xd0\
|
||||
\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\x87\x81\xc5\x5e\x03\
|
||||
\x00\x00\x00\x34\x00\x00\x00\x00\x00\x01\x00\x00\x08\xf1\
|
||||
\x00\x00\x01\x87\x81\xc5\x5e\x03\
|
||||
"
|
||||
|
||||
qt_version = [int(v) for v in QtCore.qVersion().split('.')]
|
||||
|
@@ -29,13 +29,11 @@
|
||||
'''
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
'''
|
||||
from __future__ import unicode_literals
|
||||
|
||||
VERSION = '3.5.1'
|
||||
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."
|
||||
|
@@ -216,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
|
||||
|
||||
|
@@ -32,6 +32,7 @@
|
||||
import logging
|
||||
import os
|
||||
import os.path
|
||||
import platform
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
@@ -61,3 +62,38 @@ 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)
|
||||
|
@@ -51,6 +51,18 @@ 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-CHACHA20-POLY1305'
|
||||
)
|
||||
|
||||
# Callback for error on cert
|
||||
# parameters are hostname, serial
|
||||
# If returns True, ignores error
|
||||
@@ -72,7 +84,6 @@ class InvalidVersion(UDSException):
|
||||
super().__init__(downloadUrl)
|
||||
self.downloadUrl = downloadUrl
|
||||
|
||||
|
||||
class RestApi:
|
||||
|
||||
_restApiUrl: str # base Rest API URL
|
||||
@@ -152,7 +163,7 @@ class RestApi:
|
||||
params = None
|
||||
|
||||
if self._serverVersion <= OLD_METHOD_VERSION:
|
||||
raise Exception('Server version is too old. Please, update it.')
|
||||
raise Exception('Server version is too old. Please, update it')
|
||||
else:
|
||||
res = data['result']
|
||||
# We have three elements on result:
|
||||
@@ -184,6 +195,10 @@ class RestApi:
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
# 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())
|
||||
|
@@ -44,11 +44,17 @@ 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]] = []
|
||||
@@ -76,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)
|
||||
@@ -108,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)
|
||||
@@ -139,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))
|
||||
|
||||
|
||||
@@ -195,9 +195,7 @@ def waitForTasks() -> None:
|
||||
psutil.process_iter(attrs=('ppid',)),
|
||||
)
|
||||
)
|
||||
logger.debug(
|
||||
'Waiting for subprocesses... %s, %s', task.pid, subProcesses
|
||||
)
|
||||
logger.debug('Waiting for subprocesses... %s, %s', task.pid, subProcesses)
|
||||
for i in subProcesses:
|
||||
logger.debug('Found %s', i)
|
||||
i.wait()
|
||||
@@ -224,14 +222,7 @@ 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( # type: ignore
|
||||
@@ -261,9 +252,17 @@ def getCaCertsFile() -> typing.Optional[str]:
|
||||
|
||||
# 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'):
|
||||
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,8 +32,6 @@ import socket
|
||||
import socketserver
|
||||
import ssl
|
||||
import threading
|
||||
import time
|
||||
import random
|
||||
import threading
|
||||
import select
|
||||
import typing
|
||||
@@ -43,14 +41,16 @@ from . import tools
|
||||
|
||||
HANDSHAKE_V1 = b'\x5AMGB\xA5\x01\x00'
|
||||
BUFFER_SIZE = 1024 * 16 # Max buffer length
|
||||
DEBUG = True
|
||||
LISTEN_ADDRESS = '0.0.0.0' if DEBUG else '127.0.0.1'
|
||||
LISTEN_ADDRESS = '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:
|
||||
@@ -120,10 +119,13 @@ class ForwardServer(socketserver.ThreadingTCPServer):
|
||||
|
||||
# Do not "recompress" data, use only "base protocol" compression
|
||||
context.options |= ssl.OP_NO_COMPRESSION
|
||||
# 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
|
||||
context.load_verify_locations(tools.getCaCertsFile()) # Load certifi certificates
|
||||
|
||||
# If ignore remote certificate
|
||||
if self.check_certificate is False:
|
||||
@@ -148,18 +150,20 @@ class ForwardServer(socketserver.ThreadingTCPServer):
|
||||
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
|
||||
@@ -175,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
|
||||
@@ -194,11 +198,10 @@ class Handler(socketserver.BaseRequestHandler):
|
||||
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}')
|
||||
@@ -235,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)
|
||||
@@ -250,40 +252,17 @@ 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()
|
||||
|
||||
return fs
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
log = logging.getLogger()
|
||||
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
|
||||
handler.setFormatter(formatter)
|
||||
log.addHandler(handler)
|
||||
|
||||
ticket = 'mffqg7q4s61fvx0ck2pe0zke6k0c5ipb34clhbkbs4dasb4g'
|
||||
|
||||
fs = forward(
|
||||
('172.27.0.1', 7777),
|
||||
ticket,
|
||||
local_port=49999,
|
||||
timeout=-20,
|
||||
check_certificate=False,
|
||||
)
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -174,6 +174,10 @@ public class ConnectionService {
|
||||
public GuacamoleConfiguration getConnectionConfiguration(String data)
|
||||
throws GuacamoleException {
|
||||
|
||||
// Clean up data { and } characters
|
||||
// sample valid data: nsgxslnuqvsoyvr8hacmjlezgmyjcjxvbpxiiqgs.ERg5gP0uq10WrnrqpttJJgqWSAFXpR7F
|
||||
data = data.replace("{", "").replace("}", "");
|
||||
|
||||
logger.debug("Retrieving/validating connection configuration using data from \"{}\"...", data);
|
||||
|
||||
// Build URI of remote service from the base URI and given data
|
||||
|
@@ -1,4 +1,4 @@
|
||||
Django
|
||||
Django==3.2.22
|
||||
bitarray
|
||||
html5lib
|
||||
six
|
||||
@@ -19,3 +19,4 @@ webencodings
|
||||
xml-marshaller
|
||||
pycrypto>=2.6.1
|
||||
cryptography
|
||||
Pillow==9.5.0
|
@@ -1,199 +0,0 @@
|
||||
altgraph==0.17.2
|
||||
appdirs==1.4.4
|
||||
asgiref==3.5.0
|
||||
asttokens==2.0.5
|
||||
Babel==2.9.1
|
||||
backcall==0.2.0
|
||||
backports.entry-points-selectable==1.1.1
|
||||
bcrypt==3.2.0
|
||||
beautifulsoup4==4.10.0
|
||||
bitarray==2.6.0
|
||||
black==22.1.0
|
||||
boto3==1.21.27
|
||||
botocore==1.24.27
|
||||
Brotli==1.0.9
|
||||
CacheControl==0.12.11
|
||||
cachetools==5.0.0
|
||||
cairocffi==1.3.0
|
||||
CairoSVG==2.5.2
|
||||
certifi==2021.10.8
|
||||
cffi==1.15.0
|
||||
chardet==4.0.0
|
||||
charset-normalizer==2.0.12
|
||||
click==8.0.4
|
||||
commonmark==0.9.1
|
||||
contextlib2==21.6.0
|
||||
coverage==6.4.4
|
||||
cryptography==36.0.2
|
||||
cssselect2==0.5.0
|
||||
curio==1.5
|
||||
cycler==0.11.0
|
||||
cyclonedx-python-lib==3.1.0
|
||||
Cython==0.29.28
|
||||
decorator==5.1.1
|
||||
defusedxml==0.7.1
|
||||
Delorean==1.0.0
|
||||
distlib==0.3.4
|
||||
Django==3.2.16
|
||||
django-extensions==3.1.5
|
||||
dnspython==2.2.1
|
||||
docker==5.0.3
|
||||
emrichen==0.2.3
|
||||
et-xmlfile==1.1.0
|
||||
executing==0.8.3
|
||||
filelock==3.6.0
|
||||
fonttools==4.31.2
|
||||
gdown==4.4.0
|
||||
gitdb==4.0.9
|
||||
GitPython==3.1.27
|
||||
google-api-core==2.7.1
|
||||
google-auth==2.6.2
|
||||
google-cloud-core==2.2.3
|
||||
google-cloud-translate==3.7.2
|
||||
googleapis-common-protos==1.56.0
|
||||
grpcio==1.44.0
|
||||
grpcio-status==1.44.0
|
||||
gunicorn==20.1.0
|
||||
h11==0.13.0
|
||||
html5lib==1.1
|
||||
huggingface-hub==0.4.0
|
||||
humanize==4.0.0
|
||||
idna==3.3
|
||||
ipython==8.2.0
|
||||
ipython-genutils==0.2.0
|
||||
isodate==0.6.1
|
||||
jedi==0.18.1
|
||||
jmespath==1.0.0
|
||||
joblib==1.2.0
|
||||
jsonpath-rw==1.4.0
|
||||
kiwisolver==1.4.1
|
||||
ldap3==2.9.1
|
||||
libvirt-python==8.1.0
|
||||
lockfile==0.12.2
|
||||
lxml==4.6.5
|
||||
matplotlib==3.5.1
|
||||
matplotlib-inline==0.1.3
|
||||
mpmath==1.2.1
|
||||
msgpack==1.0.4
|
||||
mypy==0.942
|
||||
mypy-extensions==0.4.3
|
||||
mysqlclient==2.1.0
|
||||
netaddr==0.8.0
|
||||
ntlm-auth==1.5.0
|
||||
numpy==1.22.3
|
||||
openpyxl==3.0.9
|
||||
ovirt-engine-sdk-python==4.5.0
|
||||
packageurl-python==0.10.4
|
||||
packaging==21.3
|
||||
paramiko==2.10.3
|
||||
parso==0.8.3
|
||||
pathspec==0.9.0
|
||||
pexpect==4.8.0
|
||||
pickleshare==0.7.5
|
||||
Pillow==9.0.1
|
||||
pip-api==0.0.30
|
||||
pip-requirements-parser==31.2.0
|
||||
pip_audit==2.4.5
|
||||
pkg_resources==0.0.0
|
||||
platformdirs==2.5.1
|
||||
ply==3.11
|
||||
prompt-toolkit==3.0.28
|
||||
proto-plus==1.20.3
|
||||
protobuf==3.19.4
|
||||
psutil==5.9.0
|
||||
ptyprocess==0.7.0
|
||||
pure-eval==0.2.2
|
||||
pyaml==21.10.1
|
||||
pyasn1==0.4.8
|
||||
pyasn1-modules==0.2.8
|
||||
pycparser==2.21
|
||||
pycryptodome==3.14.1
|
||||
pycurl==7.45.1
|
||||
pydyf==0.1.2
|
||||
Pygments==2.11.2
|
||||
pyinstaller==4.10
|
||||
pyinstaller-hooks-contrib==2022.3
|
||||
PyJWT==2.4.0
|
||||
PyNaCl==1.5.0
|
||||
pyOpenSSL==22.0.0
|
||||
pyparsing==3.0.7
|
||||
pyphen==0.12.0
|
||||
PyQt5==5.15.6
|
||||
PyQt5-Qt5==5.15.2
|
||||
PyQt5-sip==12.9.1
|
||||
pyrad==2.4
|
||||
PySide2==5.15.2.1
|
||||
PySocks==1.7.1
|
||||
pyspnego==0.5.1
|
||||
python-dateutil==2.8.2
|
||||
python-geoip-geolite2-coex==2018.1113
|
||||
python-geoip-python3==1.3
|
||||
python-ldap==3.4.0
|
||||
python-memcached==1.59
|
||||
python3-saml==1.14.0
|
||||
pytz==2022.1
|
||||
pytz-deprecation-shim==0.1.0.post0
|
||||
pyvmomi==7.0.3
|
||||
pywinrm==0.4.2
|
||||
PyYAML==6.0
|
||||
questionary==1.10.0
|
||||
regex==2022.3.15
|
||||
requests==2.27.1
|
||||
requests-credssp==2.0.0
|
||||
requests-ntlm==1.1.0
|
||||
resolvelib==0.8.1
|
||||
rich==12.6.0
|
||||
roam==0.3.1
|
||||
rsa==4.8
|
||||
ruamel.yaml==0.17.21
|
||||
ruamel.yaml.clib==0.2.6
|
||||
s3transfer==0.5.2
|
||||
sacremoses==0.0.49
|
||||
schema==0.7.5
|
||||
Send2Trash==1.8.0
|
||||
sentencepiece==0.1.96
|
||||
setproctitle==1.2.2
|
||||
shiboken2==5.15.2.1
|
||||
six==1.16.0
|
||||
smmap==5.0.0
|
||||
sortedcontainers==2.4.0
|
||||
soupsieve==2.3.1
|
||||
sqlparse==0.4.2
|
||||
stack-data==0.2.0
|
||||
sympy==1.10.1
|
||||
tinycss2==1.1.1
|
||||
tokenizers==0.11.6
|
||||
toml==0.10.2
|
||||
tomli==2.0.1
|
||||
tqdm==4.63.1
|
||||
traitlets==5.1.1
|
||||
transformers==4.17.0
|
||||
translate-toolkit==3.6.0
|
||||
typed-ast==1.5.2
|
||||
types-cryptography==3.3.18
|
||||
types-enum34==1.1.8
|
||||
types-ipaddress==1.0.8
|
||||
types-paramiko==2.8.17
|
||||
types-pycurl==7.44.7
|
||||
types-python-dateutil==2.8.10
|
||||
types-pytz==2021.3.7
|
||||
types-pyvmomi==7.0.5
|
||||
types-PyYAML==6.0.7
|
||||
types-requests==2.27.15
|
||||
types-urllib3==1.26.11
|
||||
typing_extensions==4.1.1
|
||||
tzdata==2022.1
|
||||
tzlocal==4.1
|
||||
urllib3==1.26.9
|
||||
uvicorn==0.17.6
|
||||
virtualenv==20.14.0
|
||||
wcwidth==0.2.5
|
||||
weasyprint==54.2
|
||||
webencodings==0.5.1
|
||||
websocket-client==1.3.1
|
||||
whitenoise==6.0.0
|
||||
xml-marshaller==1.0.2
|
||||
xmlsec==1.3.12
|
||||
xmltodict==0.12.0
|
||||
xxhash==3.0.0
|
||||
zopfli==0.2.1
|
@@ -146,9 +146,38 @@ FILE_UPLOAD_MAX_MEMORY_SIZE = 512 * 1024 # 512 Kb
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
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)
|
||||
# **** NOTE!: The provided RSA Key is a Sample, a very old generated one and probably will not be accepted by your implementation of criptography ****
|
||||
# **** You MUST change this key to a new one, generated with the following command: ****
|
||||
# openssl genrsa -out private.pem 2048
|
||||
# **** And then, you must copy the contents of the file private.pem into the following variable ****
|
||||
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',
|
||||
|
@@ -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,
|
||||
@@ -61,8 +63,6 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ['Handler', 'Dispatcher']
|
||||
|
||||
AUTH_TOKEN_HEADER = 'X-Auth-Token'
|
||||
|
||||
|
||||
@@ -78,13 +78,11 @@ class Dispatcher(View):
|
||||
|
||||
# pylint: disable=too-many-locals, too-many-return-statements, too-many-branches, too-many-statements
|
||||
@method_decorator(csrf_exempt)
|
||||
def dispatch(self, request: 'ExtendedHttpRequestWithUser', *args, **kwargs):
|
||||
# We know for sure that request is an ExtendedHttpRequestWithUser because of an middleware that is applied to all requests
|
||||
def dispatch(self, request: 'ExtendedHttpRequestWithUser', *args: typing.Any, **kwargs: typing.Any) -> http.HttpResponse: # type: ignore
|
||||
"""
|
||||
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']
|
||||
@@ -149,6 +147,8 @@ class Dispatcher(View):
|
||||
except processors.ParametersException as e:
|
||||
logger.debug('Path: %s', full_path)
|
||||
logger.debug('Error: %s', e)
|
||||
log.log_operation(handler, 500, log.ERROR)
|
||||
|
||||
return http.HttpResponseServerError(
|
||||
'Invalid parameters invoking {0}: {1}'.format(full_path, e),
|
||||
content_type="text/plain",
|
||||
@@ -158,6 +158,8 @@ class Dispatcher(View):
|
||||
for n in ['get', 'post', 'put', 'delete']:
|
||||
if hasattr(handler, n):
|
||||
allowedMethods.append(n)
|
||||
log.log_operation(handler, 405, log.ERROR)
|
||||
|
||||
return http.HttpResponseNotAllowed(
|
||||
allowedMethods, content_type="text/plain"
|
||||
)
|
||||
@@ -168,6 +170,8 @@ class Dispatcher(View):
|
||||
except Exception:
|
||||
logger.exception('error accessing attribute')
|
||||
logger.debug('Getting attribute %s for %s', http_method, full_path)
|
||||
|
||||
log.log_operation(handler, 500, log.ERROR)
|
||||
return http.HttpResponseServerError(
|
||||
'Unexcepected error', content_type="text/plain"
|
||||
)
|
||||
@@ -182,20 +186,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")
|
||||
|
||||
@@ -226,7 +239,9 @@ class Dispatcher(View):
|
||||
|
||||
service_node[name][''] = cls
|
||||
else:
|
||||
Dispatcher.registerSubclasses(cls.__subclasses__())
|
||||
from .model import DetailHandler
|
||||
if cls is not DetailHandler:
|
||||
Dispatcher.registerSubclasses(cls.__subclasses__())
|
||||
|
||||
# Initializes the dispatchers
|
||||
@staticmethod
|
||||
@@ -240,7 +255,9 @@ 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(
|
||||
|
@@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
@@ -28,7 +27,7 @@
|
||||
# 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 typing
|
||||
import logging
|
||||
@@ -37,11 +36,15 @@ from django.contrib.sessions.backends.base import SessionBase
|
||||
from django.contrib.sessions.backends.db import SessionStore
|
||||
|
||||
from uds.core.util.config import GlobalConfig
|
||||
from uds.core.util.state import State
|
||||
from uds.core.auths.auth import getRootUser
|
||||
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 ExtendedHttpRequestWithUser
|
||||
@@ -127,8 +130,8 @@ class Handler:
|
||||
self,
|
||||
request: 'ExtendedHttpRequestWithUser',
|
||||
path: str,
|
||||
operation: str,
|
||||
params: typing.Any,
|
||||
method: str,
|
||||
params: typing.MutableMapping[str, typing.Any],
|
||||
*args: str,
|
||||
**kwargs
|
||||
):
|
||||
@@ -147,7 +150,7 @@ class Handler:
|
||||
|
||||
self._request = request
|
||||
self._path = path
|
||||
self._operation = operation
|
||||
self._operation = method
|
||||
self._params = params
|
||||
self._args = args
|
||||
self._kwargs = kwargs
|
||||
@@ -164,7 +167,7 @@ class Handler:
|
||||
except Exception: # Couldn't authenticate
|
||||
self._authToken = None
|
||||
self._session = None
|
||||
|
||||
|
||||
if self._authToken is None:
|
||||
raise AccessDenied()
|
||||
|
||||
@@ -177,6 +180,11 @@ class Handler:
|
||||
self._user = self.getUser()
|
||||
else:
|
||||
self._user = User() # Empty user for non authenticated handlers
|
||||
self._user.state = State.ACTIVE
|
||||
|
||||
if self._user.state != State.ACTIVE:
|
||||
raise AccessDenied()
|
||||
|
||||
|
||||
def headers(self) -> typing.Dict[str, str]:
|
||||
"""
|
||||
|
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
|
||||
],
|
||||
)
|
@@ -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
|
||||
"""
|
||||
|
@@ -44,7 +44,7 @@ from uds.models import (
|
||||
# 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__)
|
||||
|
||||
@@ -69,24 +70,24 @@ class BlockAccess(Exception):
|
||||
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):
|
||||
@@ -114,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]:
|
||||
@@ -121,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)
|
||||
|
||||
@@ -182,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,
|
||||
@@ -347,7 +350,7 @@ 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)
|
||||
@@ -454,8 +457,10 @@ class LoginLogout(ActorV3Action):
|
||||
else:
|
||||
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()
|
||||
|
||||
|
||||
@@ -683,7 +688,7 @@ class Unmanaged(ActorV3Action):
|
||||
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,
|
||||
@@ -729,8 +734,8 @@ class Notify(ActorV3Action):
|
||||
|
||||
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))
|
||||
@@ -738,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)
|
||||
|
||||
|
@@ -70,7 +70,7 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
|
||||
'name': item.name,
|
||||
'comments': item.comments,
|
||||
'start': item.start,
|
||||
'end': item.end,
|
||||
'end': datetime.datetime.combine(item.end, datetime.time.max) if item.end else None,
|
||||
'frequency': item.frequency,
|
||||
'interval': item.interval,
|
||||
'duration': item.duration,
|
||||
|
@@ -68,6 +68,9 @@ class Calendars(ModelHandler):
|
||||
},
|
||||
{'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}},
|
||||
]
|
||||
|
||||
@@ -78,6 +81,10 @@ class Calendars(ModelHandler):
|
||||
'tags': [tag.tag for tag in item.tags.all()],
|
||||
'comments': item.comments,
|
||||
'modified': item.modified,
|
||||
'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),
|
||||
}
|
||||
|
||||
|
@@ -51,9 +51,9 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#CLIENT_VERSION = UDS_VERSION
|
||||
CLIENT_VERSION = UDS_VERSION
|
||||
REQUIRED_CLIENT_VERSION = '3.6.0'
|
||||
CLIENT_VERSION = REQUIRED_CLIENT_VERSION
|
||||
|
||||
|
||||
|
||||
# Enclosed methods under /client path
|
||||
@@ -105,7 +105,7 @@ 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
|
||||
"""
|
||||
@@ -116,7 +116,9 @@ class Client(Handler):
|
||||
{
|
||||
'availableVersion': CLIENT_VERSION,
|
||||
'requiredVersion': REQUIRED_CLIENT_VERSION,
|
||||
'downloadUrl': 'A new version of UDS Client is required.\nPlease, download it from Client Download section.',
|
||||
'downloadUrl': self._request.build_absolute_uri(
|
||||
reverse('page.client-download')
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
@@ -39,61 +39,42 @@ 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():
|
||||
CfgConfig.update(section, key, vals['value'])
|
||||
config = CfgConfig.update(section, key, vals['value'])
|
||||
if config is not None:
|
||||
logger.info(
|
||||
'Updating config value %s.%s to %s by %s',
|
||||
section,
|
||||
key,
|
||||
'********' if config.isCrypted() else vals['value'],
|
||||
self._user.name,
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
'Non existing config value %s.%s to %s by %s',
|
||||
section,
|
||||
key,
|
||||
vals['value'],
|
||||
self._user.name,
|
||||
)
|
||||
return 'done'
|
||||
|
@@ -65,9 +65,9 @@ class Login(Handler):
|
||||
@staticmethod
|
||||
def result(
|
||||
result: str = 'error',
|
||||
token: str = None,
|
||||
scrambler: str = None,
|
||||
error: str = None,
|
||||
token: typing.Optional[str] = None,
|
||||
scrambler: typing.Optional[str] = None,
|
||||
error: typing.Optional[str] = None,
|
||||
) -> typing.MutableMapping[str, typing.Any]:
|
||||
res = {
|
||||
'result': result,
|
||||
@@ -229,7 +229,7 @@ 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():
|
||||
|
@@ -160,7 +160,7 @@ class MetaPools(ModelHandler):
|
||||
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
|
||||
+ gui.sortedChoices(
|
||||
[
|
||||
gui.choiceImage(v.uuid, v.name, v.thumb64)
|
||||
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
|
||||
for v in Image.objects.all()
|
||||
]
|
||||
),
|
||||
@@ -175,7 +175,7 @@ class MetaPools(ModelHandler):
|
||||
'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)]
|
||||
+ gui.sortedChoices(
|
||||
[
|
||||
gui.choiceImage(v.uuid, v.name, v.thumb64)
|
||||
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
|
||||
for v in ServicePoolGroup.objects.all()
|
||||
]
|
||||
),
|
||||
|
@@ -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),
|
||||
}
|
@@ -68,6 +68,7 @@ class Permissions(Handler):
|
||||
'calendars': models.Calendar,
|
||||
'metapools': models.MetaPool,
|
||||
'accounts': models.Account,
|
||||
'mfa': models.MFA,
|
||||
}.get(arg, None)
|
||||
|
||||
if cls is None:
|
||||
@@ -92,10 +93,10 @@ class Permissions(Handler):
|
||||
{
|
||||
'id': perm.uuid,
|
||||
'type': kind,
|
||||
'auth': entity.manager.uuid,
|
||||
'auth_name': entity.manager.name,
|
||||
'entity_id': entity.uuid,
|
||||
'entity_name': entity.name,
|
||||
'auth': entity.manager.uuid, # type: ignore
|
||||
'auth_name': entity.manager.name, # type: ignore
|
||||
'entity_id': entity.uuid, # type: ignore
|
||||
'entity_name': entity.name, # type: ignore
|
||||
'perm': perm.permission,
|
||||
'perm_name': perm.permission_as_string,
|
||||
}
|
||||
@@ -103,7 +104,7 @@ class Permissions(Handler):
|
||||
|
||||
return sorted(res, key=lambda v: v['auth_name'] + v['entity_name'])
|
||||
|
||||
def get(self):
|
||||
def get(self) -> typing.Any:
|
||||
"""
|
||||
Processes get requests
|
||||
"""
|
||||
|
@@ -308,7 +308,7 @@ class Services(DetailHandler): # pylint: disable=too-many-public-methods
|
||||
'values': [gui.choiceItem(-1, '')]
|
||||
+ gui.sortedChoices(
|
||||
[
|
||||
gui.choiceItem(v.uuid, v.name)
|
||||
gui.choiceItem(v.uuid, v.name) # type: ignore
|
||||
for v in models.Proxy.objects.all()
|
||||
]
|
||||
),
|
||||
|
@@ -93,7 +93,7 @@ class ServicesPoolGroups(ModelHandler):
|
||||
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
|
||||
+ gui.sortedChoices(
|
||||
[
|
||||
gui.choiceImage(v.uuid, v.name, v.thumb64)
|
||||
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
|
||||
for v in Image.objects.all()
|
||||
]
|
||||
),
|
||||
|
@@ -233,8 +233,8 @@ class ServicesPools(ModelHandler):
|
||||
'name': item.name,
|
||||
'short_name': item.short_name,
|
||||
'tags': [tag.tag for tag in item.tags.all()],
|
||||
'parent': item.service.name,
|
||||
'parent_type': item.service.data_type,
|
||||
'parent': item.service.name, # type: ignore
|
||||
'parent_type': item.service.data_type, # type: ignore
|
||||
'comments': item.comments,
|
||||
'state': state,
|
||||
'thumb': item.image.thumb64
|
||||
@@ -242,8 +242,8 @@ class ServicesPools(ModelHandler):
|
||||
else DEFAULT_THUMB_BASE64,
|
||||
'account': item.account.name if item.account is not None else '',
|
||||
'account_id': item.account.uuid if item.account is not None else None,
|
||||
'service_id': item.service.uuid,
|
||||
'provider_id': item.service.provider.uuid,
|
||||
'service_id': item.service.uuid, # type: ignore
|
||||
'provider_id': item.service.provider.uuid, # type: ignore
|
||||
'image_id': item.image.uuid if item.image is not None else None,
|
||||
'initial_srvs': item.initial_srvs,
|
||||
'cache_l1_srvs': item.cache_l1_srvs,
|
||||
@@ -297,11 +297,11 @@ class ServicesPools(ModelHandler):
|
||||
val['tags'] = [tag.tag for tag in item.tags.all()]
|
||||
val['restrained'] = restrained
|
||||
val['permission'] = permissions.getEffectivePermission(self._user, item)
|
||||
val['info'] = Services.serviceInfo(item.service)
|
||||
val['info'] = Services.serviceInfo(item.service) # type: ignore
|
||||
val['pool_group_id'] = poolGroupId
|
||||
val['pool_group_name'] = poolGroupName
|
||||
val['pool_group_thumb'] = poolGroupThumb
|
||||
val['usage'] = str(item.usage(usage_count)) + '%'
|
||||
val['usage'] = str(item.usage(usage_count)[0]) + '%'
|
||||
|
||||
if item.osmanager:
|
||||
val['osmanager_id'] = item.osmanager.uuid
|
||||
@@ -325,7 +325,7 @@ class ServicesPools(ModelHandler):
|
||||
'values': [gui.choiceItem('', '')]
|
||||
+ gui.sortedChoices(
|
||||
[
|
||||
gui.choiceItem(v.uuid, v.provider.name + '\\' + v.name)
|
||||
gui.choiceItem(v.uuid, v.provider.name + '\\' + v.name) # type: ignore
|
||||
for v in Service.objects.all()
|
||||
]
|
||||
),
|
||||
@@ -339,7 +339,7 @@ class ServicesPools(ModelHandler):
|
||||
'name': 'osmanager_id',
|
||||
'values': [gui.choiceItem(-1, '')]
|
||||
+ gui.sortedChoices(
|
||||
[gui.choiceItem(v.uuid, v.name) for v in OSManager.objects.all()]
|
||||
[gui.choiceItem(v.uuid, v.name) for v in OSManager.objects.all()] # type: ignore
|
||||
),
|
||||
'label': ugettext('OS Manager'),
|
||||
'tooltip': ugettext('OS Manager used as base of this service pool'),
|
||||
@@ -394,7 +394,7 @@ class ServicesPools(ModelHandler):
|
||||
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
|
||||
+ gui.sortedChoices(
|
||||
[
|
||||
gui.choiceImage(v.uuid, v.name, v.thumb64)
|
||||
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
|
||||
for v in Image.objects.all()
|
||||
]
|
||||
),
|
||||
@@ -409,7 +409,7 @@ class ServicesPools(ModelHandler):
|
||||
'values': [gui.choiceImage(-1, _('Default'), DEFAULT_THUMB_BASE64)]
|
||||
+ gui.sortedChoices(
|
||||
[
|
||||
gui.choiceImage(v.uuid, v.name, v.thumb64)
|
||||
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
|
||||
for v in ServicePoolGroup.objects.all()
|
||||
]
|
||||
),
|
||||
@@ -493,7 +493,7 @@ class ServicesPools(ModelHandler):
|
||||
'name': 'account_id',
|
||||
'values': [gui.choiceItem(-1, '')]
|
||||
+ gui.sortedChoices(
|
||||
[gui.choiceItem(v.uuid, v.name) for v in Account.objects.all()]
|
||||
[gui.choiceItem(v.uuid, v.name) for v in Account.objects.all()] # type: ignore
|
||||
),
|
||||
'label': ugettext('Accounting'),
|
||||
'tooltip': ugettext('Account associated to this service pool'),
|
||||
@@ -659,7 +659,7 @@ class ServicesPools(ModelHandler):
|
||||
# Returns the action list based on current element, for calendar
|
||||
def actionsList(self, item: ServicePool) -> typing.Any:
|
||||
validActions: typing.Tuple[typing.Dict, ...] = ()
|
||||
itemInfo = item.service.getType()
|
||||
itemInfo = item.service.getType() # type: ignore
|
||||
if itemInfo.usesCache is True:
|
||||
validActions += (
|
||||
CALENDAR_ACTION_INITIAL,
|
||||
@@ -691,7 +691,7 @@ class ServicesPools(ModelHandler):
|
||||
return validActions
|
||||
|
||||
def listAssignables(self, item: ServicePool) -> typing.Any:
|
||||
service = item.service.getInstance()
|
||||
service = item.service.getInstance() # type: ignore
|
||||
return [gui.choiceItem(i[0], i[1]) for i in service.listAssignables()]
|
||||
|
||||
def createFromAssignable(self, item: ServicePool) -> typing.Any:
|
||||
|
@@ -77,8 +77,8 @@ class ServicesUsage(DetailHandler):
|
||||
'friendly_name': item.friendly_name,
|
||||
'owner': owner,
|
||||
'owner_info': owner_info,
|
||||
'service': item.deployed_service.service.name,
|
||||
'service_id': item.deployed_service.service.uuid,
|
||||
'service': item.deployed_service.service.name, # type: ignore
|
||||
'service_id': item.deployed_service.service.uuid, # type: ignore
|
||||
'pool': item.deployed_service.name,
|
||||
'pool_id': item.deployed_service.uuid,
|
||||
'ip': props.get('ip', _('unknown')),
|
||||
|
@@ -53,7 +53,7 @@ if typing.TYPE_CHECKING:
|
||||
cache = Cache('StatsDispatcher')
|
||||
|
||||
# Enclosed methods under /stats path
|
||||
POINTS = 150
|
||||
POINTS = 70
|
||||
SINCE = 7 # Days, if higer values used, ensure mysql/mariadb has a bigger sort buffer
|
||||
USE_MAX = True
|
||||
CACHE_TIME = SINCE * 24 * 3600 // POINTS
|
||||
@@ -112,7 +112,7 @@ class System(Handler):
|
||||
needs_admin = False
|
||||
needs_staff = True
|
||||
|
||||
def get(self):
|
||||
def get(self) -> typing.Any:
|
||||
logger.debug('args: %s', self._args)
|
||||
# Only allow admin user for global stats
|
||||
if len(self._args) == 1:
|
||||
|
@@ -58,10 +58,11 @@ VALID_PARAMS = (
|
||||
'transport', # Admited to be backwards compatible, but not used. Will be removed on a future release.
|
||||
'force',
|
||||
'userIp',
|
||||
'time',
|
||||
)
|
||||
|
||||
|
||||
# Enclosed methods under /actor path
|
||||
# Enclosed methods under /tickets path
|
||||
class Tickets(Handler):
|
||||
"""
|
||||
Processes tickets access requests.
|
||||
@@ -127,7 +128,7 @@ class Tickets(Handler):
|
||||
# Must be invoked as '/rest/ticket/create, with "username", ("authId" or ("authSmallName" or "authTag"), "groups" (array) and optionally "time" (in seconds) as paramteres
|
||||
def put(
|
||||
self,
|
||||
): # pylint: disable=too-many-locals,too-many-branches,too-many-statements
|
||||
) -> typing.Any:
|
||||
"""
|
||||
Processes put requests, currently only under "create"
|
||||
"""
|
||||
@@ -170,7 +171,7 @@ class Tickets(Handler):
|
||||
groupIds: typing.List[str] = []
|
||||
for groupName in tools.asList(self._params['groups']):
|
||||
try:
|
||||
groupIds.append(auth.groups.get(name=groupName).uuid)
|
||||
groupIds.append(auth.groups.get(name=groupName).uuid or '')
|
||||
except Exception:
|
||||
logger.info(
|
||||
'Group %s from ticket does not exists on auth %s, forced creation: %s',
|
||||
@@ -184,6 +185,7 @@ class Tickets(Handler):
|
||||
name=groupName,
|
||||
comments='Autocreated form ticket by using force paratemeter',
|
||||
).uuid
|
||||
or ''
|
||||
)
|
||||
|
||||
if not groupIds: # No valid group in groups names
|
||||
@@ -224,7 +226,7 @@ class Tickets(Handler):
|
||||
|
||||
# For metapool, transport is ignored..
|
||||
|
||||
servicePoolId = 'M' + pool.uuid
|
||||
servicePoolId = 'M' + pool.uuid # type: ignore
|
||||
transportId = 'meta'
|
||||
|
||||
except models.MetaPool.DoesNotExist:
|
||||
@@ -240,7 +242,7 @@ class Tickets(Handler):
|
||||
):
|
||||
pool.assignedGroups.add(auth.groups.get(uuid=addGrp))
|
||||
|
||||
servicePoolId = 'F' + pool.uuid
|
||||
servicePoolId = 'F' + pool.uuid # type: ignore
|
||||
|
||||
except models.Authenticator.DoesNotExist:
|
||||
return Tickets.result(error='Authenticator does not exists')
|
||||
|
@@ -30,6 +30,7 @@
|
||||
'''
|
||||
@itemor: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
'''
|
||||
import re
|
||||
import logging
|
||||
import typing
|
||||
|
||||
@@ -109,7 +110,7 @@ class Transports(ModelHandler):
|
||||
'value': [],
|
||||
'values': sorted(
|
||||
[{'id': x.uuid, 'text': x.name} for x in Network.objects.all()],
|
||||
key=lambda x: x['text'].lower(),
|
||||
key=lambda x: x['text'].lower(), # type: ignore
|
||||
),
|
||||
'label': ugettext('Networks'),
|
||||
'tooltip': ugettext(
|
||||
@@ -147,7 +148,7 @@ class Transports(ModelHandler):
|
||||
'values': [
|
||||
{'id': x.uuid, 'text': x.name}
|
||||
for x in ServicePool.objects.all().order_by('name')
|
||||
if transport.protocol in x.service.getType().allowedProtocols
|
||||
if transport.protocol in x.service.getType().allowedProtocols # type: ignore
|
||||
],
|
||||
'label': ugettext('Service Pools'),
|
||||
'tooltip': ugettext('Currently assigned services pools'),
|
||||
@@ -199,6 +200,13 @@ class Transports(ModelHandler):
|
||||
|
||||
def beforeSave(self, fields: typing.Dict[str, typing.Any]) -> None:
|
||||
fields['allowed_oss'] = ','.join(fields['allowed_oss'])
|
||||
# If label has spaces, replace them with underscores
|
||||
fields['label'] = fields['label'].strip().replace(' ', '-')
|
||||
# And ensure small_name chars are valid [ a-zA-Z0-9:-]+
|
||||
if fields['label'] and not re.match(r'^[a-zA-Z0-9:-]+$', fields['label']):
|
||||
raise self.invalidRequestException(
|
||||
_('Label must contain only letters, numbers, ":" and "-"')
|
||||
)
|
||||
|
||||
def afterSave(self, item: Transport) -> None:
|
||||
try:
|
||||
|
@@ -43,9 +43,8 @@ from uds.core.util.stats import events
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MAX_SESSION_LENGTH = (
|
||||
60 * 60 * 24 * 7 * 2
|
||||
) # Two weeks is max session length for a tunneled connection
|
||||
MAX_SESSION_LENGTH = 60 * 60 * 24 * 7 * 2 # Two weeks is max session length for a tunneled connection
|
||||
|
||||
|
||||
# Enclosed methods under /tunnel path
|
||||
class TunnelTicket(Handler):
|
||||
@@ -68,11 +67,14 @@ class TunnelTicket(Handler):
|
||||
self._request.ip,
|
||||
)
|
||||
|
||||
if (
|
||||
not isTrustedSource(self._request.ip)
|
||||
or len(self._args) != 3
|
||||
or len(self._args[0]) != 48
|
||||
):
|
||||
if not isTrustedSource(self._request.ip) or len(self._args) != 3 or len(self._args[0]) != 48:
|
||||
logger.warning(
|
||||
'Invalid request from %s: (validArgs: %s, validLength: %s, trustedSource: %s)',
|
||||
self._request.ip,
|
||||
'Yes' if len(self._args) == 3 else 'No',
|
||||
'Yes' if len(self._args[0]) == 48 else 'No',
|
||||
'Yes' if isTrustedSource(self._request.ip) else 'No',
|
||||
)
|
||||
# Invalid requests
|
||||
raise AccessDenied()
|
||||
|
||||
@@ -87,9 +89,7 @@ class TunnelTicket(Handler):
|
||||
|
||||
# Try to get ticket from DB
|
||||
try:
|
||||
user, userService, host, port, extra = models.TicketStore.get_for_tunnel(
|
||||
self._args[0]
|
||||
)
|
||||
user, userService, host, port, extra = models.TicketStore.get_for_tunnel(self._args[0])
|
||||
host = host or ''
|
||||
data = {}
|
||||
if self._args[1][:4] == 'stop':
|
||||
|
@@ -208,7 +208,7 @@ class AssignedService(DetailHandler):
|
||||
user = models.User.objects.get(uuid=processUuid(fields['user_id']))
|
||||
|
||||
logStr = 'Changing ownership of service from {} to {} by {}'.format(
|
||||
userService.user.pretty_name, user.pretty_name, self._user.pretty_name
|
||||
userService.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
|
||||
|
@@ -78,23 +78,23 @@ def getPoolsForGroups(groups):
|
||||
|
||||
class Users(DetailHandler):
|
||||
|
||||
custom_methods = ['servicesPools', 'userServices']
|
||||
|
||||
@staticmethod
|
||||
def uuid_to_id(iterator):
|
||||
for v in iterator:
|
||||
v['id'] = v['uuid']
|
||||
del v['uuid']
|
||||
yield v
|
||||
custom_methods = ['servicesPools', 'userServices', 'cleanRelated']
|
||||
|
||||
def getItems(self, parent: Authenticator, item: typing.Optional[str]):
|
||||
# processes item to change uuid key for id
|
||||
def uuid_to_id(iterable: typing.Iterable[typing.MutableMapping[str, typing.Any]]):
|
||||
for v in iterable:
|
||||
v['id'] = v['uuid']
|
||||
del v['uuid']
|
||||
yield v
|
||||
|
||||
logger.debug(item)
|
||||
# Extract authenticator
|
||||
try:
|
||||
if item is None:
|
||||
values = list(
|
||||
Users.uuid_to_id(
|
||||
parent.users.all().values(
|
||||
uuid_to_id(
|
||||
(i for i in parent.users.all().values(
|
||||
'uuid',
|
||||
'name',
|
||||
'real_name',
|
||||
@@ -104,7 +104,8 @@ class Users(DetailHandler):
|
||||
'is_admin',
|
||||
'last_access',
|
||||
'parent',
|
||||
)
|
||||
'mfa_data',
|
||||
))
|
||||
)
|
||||
)
|
||||
for res in values:
|
||||
@@ -127,6 +128,7 @@ class Users(DetailHandler):
|
||||
'is_admin',
|
||||
'last_access',
|
||||
'parent',
|
||||
'mfa_data',
|
||||
),
|
||||
)
|
||||
res['id'] = u.uuid
|
||||
@@ -153,7 +155,7 @@ class Users(DetailHandler):
|
||||
except Exception:
|
||||
return _('Current users')
|
||||
|
||||
def getFields(self, parent):
|
||||
def getFields(self, parent: Authenticator):
|
||||
return [
|
||||
{
|
||||
'name': {
|
||||
@@ -198,12 +200,16 @@ class Users(DetailHandler):
|
||||
'staff_member',
|
||||
'is_admin',
|
||||
]
|
||||
if self._params.get('name', '') == '':
|
||||
if self._params.get('name', '').strip() == '':
|
||||
raise RequestError(_('Username cannot be empty'))
|
||||
|
||||
if 'password' in self._params:
|
||||
valid_fields.append('password')
|
||||
self._params['password'] = cryptoManager().hash(self._params['password'])
|
||||
|
||||
if 'mfa_data' in self._params:
|
||||
valid_fields.append('mfa_data')
|
||||
self._params['mfa_data'] = self._params['mfa_data'].strip()
|
||||
|
||||
fields = self.readFieldsFromParams(valid_fields)
|
||||
if not self._user.is_admin:
|
||||
@@ -224,9 +230,8 @@ class Users(DetailHandler):
|
||||
user.__dict__.update(fields)
|
||||
|
||||
logger.debug('User parent: %s', user.parent)
|
||||
if auth.isExternalSource is False and (
|
||||
user.parent is None or user.parent == ''
|
||||
):
|
||||
# If internal auth, threat it "special"
|
||||
if auth.isExternalSource is False and not user.parent:
|
||||
groups = self.readFieldsFromParams(['groups'])['groups']
|
||||
logger.debug('Groups: %s', groups)
|
||||
logger.debug('Got Groups %s', parent.groups.filter(uuid__in=groups))
|
||||
@@ -277,7 +282,7 @@ class Users(DetailHandler):
|
||||
|
||||
return 'deleted'
|
||||
|
||||
def servicesPools(self, parent: Authenticator, item):
|
||||
def servicesPools(self, parent: Authenticator, item: str) -> typing.List[typing.Dict]:
|
||||
uuid = processUuid(item)
|
||||
user = parent.users.get(uuid=processUuid(uuid))
|
||||
res = []
|
||||
@@ -299,7 +304,7 @@ class Users(DetailHandler):
|
||||
|
||||
return res
|
||||
|
||||
def userServices(self, parent: Authenticator, item):
|
||||
def userServices(self, parent: Authenticator, item: str) -> typing.List[typing.Dict]:
|
||||
uuid = processUuid(item)
|
||||
user = parent.users.get(uuid=processUuid(uuid))
|
||||
res = []
|
||||
@@ -311,6 +316,12 @@ class Users(DetailHandler):
|
||||
res.append(v)
|
||||
|
||||
return res
|
||||
|
||||
def cleanRelated(self, parent: Authenticator, item: str) -> typing.Dict:
|
||||
uuid = processUuid(item)
|
||||
user = parent.users.get(uuid=processUuid(uuid))
|
||||
user.cleanRelated()
|
||||
return {'status': 'ok'}
|
||||
|
||||
|
||||
class Groups(DetailHandler):
|
||||
@@ -414,7 +425,7 @@ class Groups(DetailHandler):
|
||||
except Exception:
|
||||
raise self.invalidRequestException()
|
||||
|
||||
def saveItem(self, parent: Authenticator, item) -> None:
|
||||
def saveItem(self, parent: Authenticator, item: typing.Optional[str]) -> None:
|
||||
group = None # Avoid warning on reference before assignment
|
||||
try:
|
||||
is_meta = self._params['type'] == 'meta'
|
||||
@@ -429,7 +440,7 @@ class Groups(DetailHandler):
|
||||
fields = self.readFieldsFromParams(valid_fields)
|
||||
is_pattern = fields.get('name', '').find('pat:') == 0
|
||||
auth = parent.getInstance()
|
||||
if item is None: # Create new
|
||||
if not item: # Create new
|
||||
if not is_meta and not is_pattern:
|
||||
auth.createGroup(
|
||||
fields
|
||||
@@ -482,7 +493,9 @@ class Groups(DetailHandler):
|
||||
except Exception:
|
||||
raise self.invalidItemException()
|
||||
|
||||
def servicesPools(self, parent: Authenticator, item: str) -> typing.List[typing.Mapping[str, typing.Any]]:
|
||||
def servicesPools(
|
||||
self, parent: Authenticator, item: str
|
||||
) -> typing.List[typing.Mapping[str, typing.Any]]:
|
||||
uuid = processUuid(item)
|
||||
group = parent.groups.get(uuid=processUuid(uuid))
|
||||
res: typing.List[typing.Mapping[str, typing.Any]] = []
|
||||
@@ -503,7 +516,9 @@ class Groups(DetailHandler):
|
||||
|
||||
return res
|
||||
|
||||
def users(self, parent: Authenticator, item: str) -> typing.List[typing.Mapping[str, typing.Any]]:
|
||||
def users(
|
||||
self, parent: Authenticator, item: str
|
||||
) -> typing.List[typing.Mapping[str, typing.Any]]:
|
||||
uuid = processUuid(item)
|
||||
group = parent.groups.get(uuid=processUuid(uuid))
|
||||
|
||||
|
@@ -114,8 +114,8 @@ class BaseModelHandler(Handler):
|
||||
'values': field.get('values', []),
|
||||
},
|
||||
}
|
||||
if 'tab' in field:
|
||||
v['gui']['tab'] = field['tab']
|
||||
if field.get('tab', None):
|
||||
v['gui']['tab'] = _(field['tab'])
|
||||
gui.append(v)
|
||||
return gui
|
||||
|
||||
@@ -268,7 +268,11 @@ class BaseModelHandler(Handler):
|
||||
args: typing.Dict[str, str] = {}
|
||||
try:
|
||||
for key in fldList:
|
||||
args[key] = self._params[key]
|
||||
if ':' in key: # optional field? get default if not present
|
||||
k, default = key.split(':')[:2]
|
||||
args[k] = self._params.get(k, default)
|
||||
else:
|
||||
args[key] = self._params[key]
|
||||
# del self._params[key]
|
||||
except KeyError as e:
|
||||
raise RequestError('needed parameter not found in data {0}'.format(e))
|
||||
@@ -675,7 +679,9 @@ class ModelHandler(BaseModelHandler):
|
||||
detail: typing.ClassVar[
|
||||
typing.Optional[typing.Dict[str, typing.Type[DetailHandler]]]
|
||||
] = None # Dictionary containing detail routing
|
||||
# Put needed fields
|
||||
# Fields that are going to be saved directly
|
||||
# If a field is in the form "field:default" and field is not present in the request, default will be used
|
||||
# Note that these fields has to be present in the model, and they can be "edited" in the beforeSave method
|
||||
save_fields: typing.ClassVar[typing.List[str]] = []
|
||||
# Put removable fields before updating
|
||||
remove_fields: typing.ClassVar[typing.List[str]] = []
|
||||
@@ -816,7 +822,8 @@ class ModelHandler(BaseModelHandler):
|
||||
'Processing detail %s for with params %s', self._path, self._params
|
||||
)
|
||||
try:
|
||||
item: models.Model = self.model.objects.filter(uuid=self._args[0])[0]
|
||||
item: models.Model = self.model.objects.get(uuid=self._args[0])
|
||||
|
||||
# If we do not have access to parent to, at least, read...
|
||||
|
||||
if self._operation in ('put', 'post', 'delete'):
|
||||
@@ -849,6 +856,8 @@ class ModelHandler(BaseModelHandler):
|
||||
method = getattr(detail, self._operation)
|
||||
|
||||
return method()
|
||||
except self.model.DoesNotExist:
|
||||
raise self.invalidItemException()
|
||||
except KeyError:
|
||||
raise self.invalidMethodException()
|
||||
except AttributeError:
|
||||
|
@@ -106,8 +106,11 @@ class ContentProcessor:
|
||||
if isinstance(obj, (list, tuple, types.GeneratorType)):
|
||||
return [ContentProcessor.procesForRender(v) for v in obj]
|
||||
|
||||
if isinstance(obj, (datetime.datetime, datetime.date)):
|
||||
if isinstance(obj, (datetime.datetime,)):
|
||||
return int(time.mktime(obj.timetuple()))
|
||||
|
||||
if isinstance(obj, (datetime.date,)):
|
||||
return '{}-{:02d}-{:02d}'.format(obj.year, obj.month, obj.day)
|
||||
|
||||
if isinstance(obj, bytes):
|
||||
return obj.decode('utf-8')
|
||||
|
@@ -71,6 +71,7 @@ class UDSAppConfig(AppConfig):
|
||||
# pylint: disable=unused-import
|
||||
from . import services # to make sure that the packages are initialized at this point
|
||||
from . import auths # To make sure that the packages are initialized at this point
|
||||
from . import mfas # To make sure mfas are loaded on memory
|
||||
from . import osmanagers # To make sure that packages are initialized at this point
|
||||
from . import transports # To make sure that packages are initialized at this point
|
||||
from . import dispatchers # Ensure all dischatchers all also available
|
||||
@@ -94,5 +95,4 @@ def extend_sqlite(connection=None, **kwargs):
|
||||
cursor.execute('PRAGMA journal_mode=WAL')
|
||||
connection.connection.create_function("MIN", 2, min)
|
||||
connection.connection.create_function("MAX", 2, max)
|
||||
connection.connection.create_function("CEIL", 1, math.ceil)
|
||||
|
||||
|
@@ -56,9 +56,7 @@ logger = logging.getLogger(__name__)
|
||||
class InternalDBAuth(auths.Authenticator):
|
||||
typeName = _('Internal Database')
|
||||
typeType = 'InternalDBAuth'
|
||||
typeDescription = _(
|
||||
'Internal dabasase authenticator. Doesn\'t use external sources'
|
||||
)
|
||||
typeDescription = _('Internal dabasase authenticator. Doesn\'t use external sources')
|
||||
iconFile = 'auth.png'
|
||||
|
||||
# If we need to enter the password for this user
|
||||
@@ -98,14 +96,19 @@ class InternalDBAuth(auths.Authenticator):
|
||||
) # pylint: disable=maybe-no-member
|
||||
if self.reverseDns.isTrue():
|
||||
try:
|
||||
return str(
|
||||
dns.resolver.query(dns.reversename.from_address(ip), 'PTR')[0]
|
||||
)
|
||||
return str(dns.resolver.query(dns.reversename.from_address(ip).to_text(), 'PTR')[0])
|
||||
except Exception:
|
||||
pass
|
||||
return ip
|
||||
|
||||
def mfaIdentifier(self, username: str) -> str:
|
||||
try:
|
||||
return self.dbAuthenticator().users.get(name=username.lower(), state=State.ACTIVE).mfa_data
|
||||
except Exception: # User not found
|
||||
return ''
|
||||
|
||||
def transformUsername(self, username: str) -> str:
|
||||
username = username.lower()
|
||||
if self.differentForEachHost.isTrue():
|
||||
newUsername = self.getIp() + '-' + username
|
||||
# Duplicate basic user into username.
|
||||
@@ -113,24 +116,37 @@ class InternalDBAuth(auths.Authenticator):
|
||||
# "Derived" users will belong to no group at all, because we will extract groups from "base" user
|
||||
# This way also, we protect from using forged "ip" + "username", because those will belong in fact to no group
|
||||
# and access will be denied
|
||||
grps: typing.List['models.Group'] = []
|
||||
try:
|
||||
usr = auth.users.get(name=username, state=State.ACTIVE)
|
||||
parent = usr.uuid
|
||||
grps = [g for g in usr.groups.all()]
|
||||
usr.id = usr.uuid = None # type: ignore # Empty id
|
||||
if usr.real_name.strip() == '':
|
||||
usr.real_name = usr.name
|
||||
usr.name = newUsername
|
||||
usr.parent = parent
|
||||
usr.save()
|
||||
# Now, coyp groups from base user
|
||||
|
||||
except Exception:
|
||||
pass # User already exists
|
||||
|
||||
# Update groups of user
|
||||
try:
|
||||
usr = auth.users.get(name=newUsername, state=State.ACTIVE)
|
||||
usr.groups.clear()
|
||||
for grp in grps:
|
||||
usr.groups.add(grp)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
username = newUsername
|
||||
|
||||
return username
|
||||
|
||||
def authenticate(
|
||||
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
|
||||
) -> bool:
|
||||
def authenticate(self, username: str, credentials: str, groupsManager: 'auths.GroupsManager') -> bool:
|
||||
username = username.lower()
|
||||
logger.debug('Username: %s, Password: %s', username, credentials)
|
||||
dbAuth = self.dbAuthenticator()
|
||||
try:
|
||||
@@ -152,16 +168,16 @@ class InternalDBAuth(auths.Authenticator):
|
||||
def getGroups(self, username: str, groupsManager: 'auths.GroupsManager'):
|
||||
dbAuth = self.dbAuthenticator()
|
||||
try:
|
||||
user: 'models.User' = dbAuth.users.get(name=username, state=State.ACTIVE)
|
||||
user: 'models.User' = dbAuth.users.get(name=username.lower(), state=State.ACTIVE)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
groupsManager.validate([g.name for g in user.groups.all()])
|
||||
grps = [g.name for g in user.groups.all()]
|
||||
groupsManager.validate(grps)
|
||||
|
||||
def getRealName(self, username: str) -> str:
|
||||
# Return the real name of the user, if it is set
|
||||
try:
|
||||
user = self.dbAuthenticator().users.get(name=username, state=State.ACTIVE)
|
||||
user = self.dbAuthenticator().users.get(name=username.lower(), state=State.ACTIVE)
|
||||
return user.real_name or username
|
||||
except Exception:
|
||||
return super().getRealName(username)
|
||||
|
@@ -112,6 +112,15 @@ class RadiusAuth(auths.Authenticator):
|
||||
tooltip=_('If set, this value will be added as group for all radius users'),
|
||||
tab=gui.ADVANCED_TAB,
|
||||
)
|
||||
mfaAttr = gui.TextField(
|
||||
length=2048,
|
||||
multiline=2,
|
||||
label=_('MFA attribute'),
|
||||
order=13,
|
||||
tooltip=_('Attribute from where to extract the MFA code'),
|
||||
required=False,
|
||||
tab=gui.MFA_TAB,
|
||||
)
|
||||
|
||||
def initialize(self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None:
|
||||
pass
|
||||
@@ -126,12 +135,27 @@ class RadiusAuth(auths.Authenticator):
|
||||
appClassPrefix=self.appClassPrefix.value,
|
||||
)
|
||||
|
||||
def mfaStorageKey(self, username: str) -> str:
|
||||
return 'mfa_' + str(self.dbAuthenticator().uuid) + username
|
||||
|
||||
def mfaIdentifier(self, username: str) -> str:
|
||||
return self.storage.getPickle(self.mfaStorageKey(username)) or ''
|
||||
|
||||
def authenticate(
|
||||
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
|
||||
) -> bool:
|
||||
try:
|
||||
connection = self.radiusClient()
|
||||
groups = connection.authenticate(username=username, password=credentials)
|
||||
groups, mfaCode, state = connection.authenticate(username=username, password=credentials, mfaField=self.mfaAttr.value.strip())
|
||||
if state:
|
||||
getRequest().session[client.STATE_VAR_NAME] = state.decode()
|
||||
# store the user mfa attribute if it is set
|
||||
if mfaCode:
|
||||
self.storage.putPickle(
|
||||
self.mfaStorageKey(username),
|
||||
mfaCode,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
authLogLogin(getRequest(), self.dbAuthenticator(), username, 'Access denied by Raiuds')
|
||||
return False
|
||||
@@ -178,7 +202,7 @@ class RadiusAuth(auths.Authenticator):
|
||||
try:
|
||||
connection = self.radiusClient()
|
||||
# Reply is not important...
|
||||
connection.authenticate(cryptoManager().randomString(10), cryptoManager().randomString(10))
|
||||
connection.authenticate(cryptoManager().randomString(10), cryptoManager().randomString(10), mfaField=self.mfaAttr.value.strip())
|
||||
except client.RadiusAuthenticationError as e:
|
||||
pass
|
||||
except Exception:
|
||||
|
@@ -1,16 +1,13 @@
|
||||
import io
|
||||
import logging
|
||||
import enum
|
||||
import typing
|
||||
import string
|
||||
|
||||
from pyrad.client import Client
|
||||
from pyrad.dictionary import Dictionary
|
||||
import pyrad.packet
|
||||
|
||||
__all__ = ['RadiusClient', 'RadiusAuthenticationError', 'RADDICT']
|
||||
|
||||
class RadiusAuthenticationError(Exception):
|
||||
pass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
RADDICT = """ATTRIBUTE User-Name 1 string
|
||||
@@ -51,6 +48,37 @@ ATTRIBUTE Framed-AppleTalk-Link 37 integer
|
||||
ATTRIBUTE Framed-AppleTalk-Network 38 integer
|
||||
ATTRIBUTE Framed-AppleTalk-Zone 39 string"""
|
||||
|
||||
# For AccessChallenge return values
|
||||
NOT_CHECKED, INCORRECT, CORRECT = -1, 0, 1 # for pwd and otp
|
||||
NOT_NEEDED, NEEDED = INCORRECT, CORRECT # for otp_needed
|
||||
|
||||
STATE_VAR_NAME = 'radius_state'
|
||||
|
||||
class RadiusAuthenticationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class RadiusStates(enum.IntEnum):
|
||||
NOT_CHECKED = -1
|
||||
INCORRECT = 0
|
||||
CORRECT = 1
|
||||
|
||||
# Aliases
|
||||
NOT_NEEDED = INCORRECT
|
||||
NEEDED = CORRECT
|
||||
|
||||
|
||||
class RadiusResult(typing.NamedTuple):
|
||||
"""
|
||||
Result of an AccessChallenge request.
|
||||
"""
|
||||
|
||||
pwd: RadiusStates = RadiusStates.INCORRECT
|
||||
replyMessage: typing.Optional[bytes] = None
|
||||
state: typing.Optional[bytes] = None
|
||||
otp: RadiusStates = RadiusStates.NOT_CHECKED
|
||||
otp_needed: RadiusStates = RadiusStates.NOT_CHECKED
|
||||
|
||||
|
||||
class RadiusClient:
|
||||
radiusServer: Client
|
||||
@@ -68,12 +96,23 @@ class RadiusClient:
|
||||
dictionary: str = RADDICT,
|
||||
) -> None:
|
||||
self.radiusServer = Client(
|
||||
server=server, authport=authPort, secret=secret, dict=Dictionary(io.StringIO(dictionary))
|
||||
server=server,
|
||||
authport=authPort,
|
||||
secret=secret,
|
||||
dict=Dictionary(io.StringIO(dictionary)),
|
||||
)
|
||||
self.nasIdentifier = nasIdentifier
|
||||
self.appClassPrefix = appClassPrefix
|
||||
|
||||
def authenticate(self, username: str, password: str) -> typing.List[str]:
|
||||
def extractAccessChallenge(self, reply: pyrad.packet.AuthPacket) -> RadiusResult:
|
||||
return RadiusResult(
|
||||
pwd=RadiusStates.CORRECT,
|
||||
replyMessage=typing.cast(typing.List[bytes], reply.get('Reply-Message') or [''])[0],
|
||||
state=typing.cast(typing.List[bytes], reply.get('State') or [b''])[0],
|
||||
otp_needed=RadiusStates.NEEDED,
|
||||
)
|
||||
|
||||
def sendAccessRequest(self, username: str, password: str, **kwargs) -> pyrad.packet.AuthPacket:
|
||||
req: pyrad.packet.AuthPacket = self.radiusServer.CreateAuthPacket(
|
||||
code=pyrad.packet.AccessRequest,
|
||||
User_Name=username,
|
||||
@@ -82,9 +121,19 @@ class RadiusClient:
|
||||
|
||||
req["User-Password"] = req.PwCrypt(password)
|
||||
|
||||
reply = typing.cast(pyrad.packet.AuthPacket, self.radiusServer.SendPacket(req))
|
||||
# Fill in extra fields
|
||||
for k, v in kwargs.items():
|
||||
req[k] = v
|
||||
|
||||
if reply.code != pyrad.packet.AccessAccept:
|
||||
return typing.cast(pyrad.packet.AuthPacket, self.radiusServer.SendPacket(req))
|
||||
|
||||
# Second element of return value is the mfa code from field
|
||||
def authenticate(
|
||||
self, username: str, password: str, mfaField: str = ''
|
||||
) -> typing.Tuple[typing.List[str], str, bytes]:
|
||||
reply = self.sendAccessRequest(username, password)
|
||||
|
||||
if reply.code not in (pyrad.packet.AccessAccept, pyrad.packet.AccessChallenge):
|
||||
raise RadiusAuthenticationError('Access denied')
|
||||
|
||||
# User accepted, extract groups...
|
||||
@@ -92,10 +141,111 @@ class RadiusClient:
|
||||
groupClassPrefix = (self.appClassPrefix + 'group=').encode()
|
||||
groupClassPrefixLen = len(groupClassPrefix)
|
||||
if 'Class' in reply:
|
||||
groups = [i[groupClassPrefixLen:].decode() for i in typing.cast(typing.Iterable[bytes], reply['Class']) if i.startswith(groupClassPrefix)]
|
||||
groups = [
|
||||
i[groupClassPrefixLen:].decode()
|
||||
for i in typing.cast(typing.Iterable[bytes], reply['Class'])
|
||||
if i.startswith(groupClassPrefix)
|
||||
]
|
||||
else:
|
||||
logger.info('No "Class (25)" attribute found')
|
||||
return []
|
||||
return ([], '', b'')
|
||||
|
||||
return groups
|
||||
# ...and mfa code
|
||||
mfaCode = ''
|
||||
if mfaField and mfaField in reply:
|
||||
mfaCode = ''.join(
|
||||
i[groupClassPrefixLen:].decode()
|
||||
for i in typing.cast(typing.Iterable[bytes], reply['Class'])
|
||||
if i.startswith(groupClassPrefix)
|
||||
)
|
||||
return (groups, mfaCode, typing.cast(typing.List[bytes], reply.get('State') or [b''])[0])
|
||||
|
||||
def authenticate_only(self, username: str, password: str) -> RadiusResult:
|
||||
reply = self.sendAccessRequest(username, password)
|
||||
|
||||
if reply.code == pyrad.packet.AccessChallenge:
|
||||
return self.extractAccessChallenge(reply)
|
||||
|
||||
# user/pwd accepted: this user does not have challenge data
|
||||
if reply.code == pyrad.packet.AccessAccept:
|
||||
return RadiusResult(
|
||||
pwd=RadiusStates.CORRECT,
|
||||
otp_needed=RadiusStates.NOT_CHECKED,
|
||||
)
|
||||
|
||||
# user/pwd rejected
|
||||
return RadiusResult(
|
||||
pwd=RadiusStates.INCORRECT,
|
||||
state=typing.cast(typing.List[bytes], reply.get('State') or [b''])[0],
|
||||
)
|
||||
|
||||
def challenge_only(self, username: str, otp: str, state: bytes = b'0000000000000000') -> RadiusResult:
|
||||
# clean otp code
|
||||
otp = ''.join([x for x in otp if x in string.digits])
|
||||
|
||||
logger.debug('Sending AccessChallenge request wit otp [%s]', otp)
|
||||
|
||||
reply = self.sendAccessRequest(username, otp, State=state)
|
||||
|
||||
logger.debug('Received AccessChallenge reply: %s', reply)
|
||||
|
||||
# correct OTP challenge
|
||||
if reply.code == pyrad.packet.AccessAccept:
|
||||
return RadiusResult(
|
||||
otp=RadiusStates.CORRECT,
|
||||
)
|
||||
|
||||
# incorrect OTP challenge
|
||||
return RadiusResult(
|
||||
otp=RadiusStates.INCORRECT,
|
||||
state=typing.cast(typing.List[bytes], reply.get('State') or [b''])[0],
|
||||
)
|
||||
|
||||
def authenticate_and_challenge(self, username: str, password: str, otp: str) -> RadiusResult:
|
||||
reply = self.sendAccessRequest(username, password)
|
||||
|
||||
if reply.code == pyrad.packet.AccessChallenge:
|
||||
state = typing.cast(typing.List[bytes], reply.get('State') or [b''])[0]
|
||||
# replyMessage = typing.cast(typing.List[bytes], reply.get('Reply-Message') or [''])[0]
|
||||
return self.challenge_only(username, otp, state=state)
|
||||
|
||||
# user/pwd accepted: but this user does not have challenge data
|
||||
# we should not be here...
|
||||
if reply.code == pyrad.packet.AccessAccept:
|
||||
logger.warning("Radius OTP error: cheking for OTP for not needed user [%s]", username)
|
||||
return RadiusResult(
|
||||
pwd=RadiusStates.CORRECT,
|
||||
otp_needed=RadiusStates.NOT_NEEDED,
|
||||
state=typing.cast(typing.List[bytes], reply.get('State') or [b''])[0],
|
||||
)
|
||||
|
||||
# TODO: accept more AccessChallenge authentications (as RFC says)
|
||||
|
||||
# incorrect user/pwd
|
||||
return RadiusResult()
|
||||
|
||||
def authenticate_challenge(
|
||||
self, username: str, password: str = '', otp: str = '', state: typing.Optional[bytes] = None
|
||||
) -> RadiusResult:
|
||||
'''
|
||||
wrapper for above 3 functions: authenticate_only, challenge_only, authenticate_and_challenge
|
||||
calls wrapped functions based on passed input values: (pwd/otp/state)
|
||||
'''
|
||||
# clean input data
|
||||
# Keep only numbers in otp
|
||||
state = state or b'0000000000000000'
|
||||
otp = ''.join([x for x in otp if x in string.digits])
|
||||
username = username.strip()
|
||||
password = password.strip()
|
||||
state = state.strip()
|
||||
|
||||
if not username or (not password and not otp):
|
||||
return RadiusResult() # no user/pwd provided
|
||||
|
||||
if not otp:
|
||||
return self.authenticate_only(username, password)
|
||||
if otp and not password:
|
||||
# check only otp with static/invented state. allow this ?
|
||||
return self.challenge_only(username, otp, state=state)
|
||||
# otp and password
|
||||
return self.authenticate_and_challenge(username, password, otp)
|
||||
|
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
#
|
||||
# Copyright (c) 2012-2019 Virtual Cable S.L.
|
||||
# Copyright (c) 2012-2022 Virtual Cable S.L.U.
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
@@ -12,7 +12,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.
|
||||
#
|
||||
@@ -63,7 +63,6 @@ LDAP_RESULT_LIMIT = 100
|
||||
|
||||
|
||||
class RegexLdap(auths.Authenticator):
|
||||
|
||||
host = gui.TextField(
|
||||
length=64,
|
||||
label=_('Host'),
|
||||
@@ -82,9 +81,7 @@ class RegexLdap(auths.Authenticator):
|
||||
ssl = gui.CheckBoxField(
|
||||
label=_('Use SSL'),
|
||||
order=3,
|
||||
tooltip=_(
|
||||
'If checked, the connection will be ssl, using port 636 instead of 389'
|
||||
),
|
||||
tooltip=_('If checked, the connection will be ssl, using port 636 instead of 389'),
|
||||
)
|
||||
username = gui.TextField(
|
||||
length=64,
|
||||
@@ -95,7 +92,7 @@ class RegexLdap(auths.Authenticator):
|
||||
tab=gui.CREDENTIALS_TAB,
|
||||
)
|
||||
password = gui.PasswordField(
|
||||
lenth=32,
|
||||
length=32,
|
||||
label=_('Password'),
|
||||
order=5,
|
||||
tooltip=_('Password of the ldap user'),
|
||||
@@ -106,16 +103,32 @@ class RegexLdap(auths.Authenticator):
|
||||
length=3,
|
||||
label=_('Timeout'),
|
||||
defvalue='10',
|
||||
order=6,
|
||||
order=10,
|
||||
tooltip=_('Timeout in seconds of connection to LDAP'),
|
||||
required=True,
|
||||
minValue=1,
|
||||
tab=gui.ADVANCED_TAB,
|
||||
)
|
||||
verifySsl = gui.CheckBoxField(
|
||||
label=_('Verify SSL'),
|
||||
defvalue=True,
|
||||
order=11,
|
||||
tooltip=_('If checked, SSL verification will be enforced. If not, SSL verification will be disabled'),
|
||||
tab=gui.ADVANCED_TAB,
|
||||
)
|
||||
certificate = gui.TextField(
|
||||
length=8192,
|
||||
multiline=4,
|
||||
label=_('Certificate'),
|
||||
order=12,
|
||||
tooltip=_('Certificate to use for SSL verification'),
|
||||
required=False,
|
||||
tab=gui.ADVANCED_TAB,
|
||||
)
|
||||
|
||||
ldapBase = gui.TextField(
|
||||
length=64,
|
||||
label=_('Base'),
|
||||
order=7,
|
||||
order=20,
|
||||
tooltip=_('Common search base (used for "users" and "groups")'),
|
||||
required=True,
|
||||
tab=_('Ldap info'),
|
||||
@@ -124,7 +137,7 @@ class RegexLdap(auths.Authenticator):
|
||||
length=64,
|
||||
label=_('User class'),
|
||||
defvalue='posixAccount',
|
||||
order=8,
|
||||
order=21,
|
||||
tooltip=_('Class for LDAP users (normally posixAccount)'),
|
||||
required=True,
|
||||
tab=_('Ldap info'),
|
||||
@@ -133,7 +146,7 @@ class RegexLdap(auths.Authenticator):
|
||||
length=64,
|
||||
label=_('User Id Attr'),
|
||||
defvalue='uid',
|
||||
order=9,
|
||||
order=22,
|
||||
tooltip=_('Attribute that contains the user id'),
|
||||
required=True,
|
||||
tab=_('Ldap info'),
|
||||
@@ -143,7 +156,7 @@ class RegexLdap(auths.Authenticator):
|
||||
label=_('User Name Attr'),
|
||||
multiline=2,
|
||||
defvalue='uid',
|
||||
order=10,
|
||||
order=23,
|
||||
tooltip=_(
|
||||
'Attributes that contains the user name attributes or attribute patterns (one for each line)'
|
||||
),
|
||||
@@ -155,7 +168,7 @@ class RegexLdap(auths.Authenticator):
|
||||
label=_('Group Name Attr'),
|
||||
multiline=2,
|
||||
defvalue='cn',
|
||||
order=11,
|
||||
order=24,
|
||||
tooltip=_(
|
||||
'Attribute that contains the group name attributes or attribute patterns (one for each line)'
|
||||
),
|
||||
@@ -168,14 +181,22 @@ class RegexLdap(auths.Authenticator):
|
||||
length=64,
|
||||
label=_('Alt. class'),
|
||||
defvalue='',
|
||||
order=20,
|
||||
tooltip=_(
|
||||
'Class for LDAP objects that will be also checked for groups retrieval (normally empty)'
|
||||
),
|
||||
order=25,
|
||||
tooltip=_('Class for LDAP objects that will be also checked for groups retrieval (normally empty)'),
|
||||
required=False,
|
||||
tab=_('Advanced'),
|
||||
)
|
||||
|
||||
mfaAttr = gui.TextField(
|
||||
length=2048,
|
||||
multiline=2,
|
||||
label=_('MFA attribute'),
|
||||
order=30,
|
||||
tooltip=_('Attribute from where to extract the MFA code'),
|
||||
required=False,
|
||||
tab=gui.MFA_TAB,
|
||||
)
|
||||
|
||||
typeName = _('Regex LDAP Authenticator')
|
||||
typeType = 'RegexLdapAuthenticator'
|
||||
typeDescription = _('Regular Expressions LDAP authenticator')
|
||||
@@ -205,19 +226,20 @@ class RegexLdap(auths.Authenticator):
|
||||
_groupNameAttr: str = ''
|
||||
_userNameAttr: str = ''
|
||||
_altClass: str = ''
|
||||
_mfaAttr: str = ''
|
||||
_verifySsl: bool = True
|
||||
_certificate: str = ''
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dbAuth: 'models.Authenticator',
|
||||
environment: 'Environment',
|
||||
values: typing.Optional[typing.Dict[str, str]],
|
||||
):
|
||||
super().__init__(dbAuth, environment, values)
|
||||
def initialize(self, values: typing.Optional[typing.Dict[str, str]]) -> None:
|
||||
if values:
|
||||
self.__validateField(values['userNameAttr'], str(self.userNameAttr.label))
|
||||
self.__validateField(values['userIdAttr'], str(self.userIdAttr.label))
|
||||
self.__validateField(values['groupNameAttr'], str(self.groupNameAttr.label))
|
||||
|
||||
for i in ('userNameAttr', 'userIdAttr', 'groupNameAttr'):
|
||||
if ':' in values[i]:
|
||||
raise auths.Authenticator.ValidationException(f'Invalid character ":" in {i}: {values[i]}')
|
||||
|
||||
self._host = values['host']
|
||||
self._port = values['port']
|
||||
self._ssl = gui.strToBool(values['ssl'])
|
||||
@@ -231,6 +253,9 @@ class RegexLdap(auths.Authenticator):
|
||||
# self._regex = values['regex']
|
||||
self._userNameAttr = values['userNameAttr']
|
||||
self._altClass = values['altClass']
|
||||
self._mfaAttr = values['mfaAttr']
|
||||
self._verifySsl = gui.strToBool(values['verifySsl'])
|
||||
self._certificate = values['certificate']
|
||||
|
||||
def __validateField(self, field: str, fieldLabel: str) -> None:
|
||||
"""
|
||||
@@ -256,14 +281,46 @@ class RegexLdap(auths.Authenticator):
|
||||
attr = line[:equalPos]
|
||||
else:
|
||||
attr = line
|
||||
res.append(attr)
|
||||
# If + is present, we must split it
|
||||
if '+' in attr:
|
||||
for a in attr.split('+'):
|
||||
if a not in res:
|
||||
res.append(a)
|
||||
elif ':' in attr:
|
||||
res.append(attr.split(':')[0])
|
||||
else:
|
||||
if attr not in res:
|
||||
res.append(attr)
|
||||
return res
|
||||
|
||||
def __processField(
|
||||
self, field: str, attributes: typing.MutableMapping[str, typing.Any]
|
||||
) -> typing.List[str]:
|
||||
res: typing.List[str] = []
|
||||
logger.debug('Attributes: %s', attributes)
|
||||
|
||||
def getAttr(attrName: str) -> typing.List[str]:
|
||||
def asList(val: typing.Any) -> typing.List[str]:
|
||||
if isinstance(val, list):
|
||||
return val
|
||||
return [val]
|
||||
|
||||
if '+' in attrName:
|
||||
attrList = attrName.split('+')
|
||||
# Check all attributes are present, and has only one value
|
||||
if not all([len(attributes.get(a, [])) <= 1 for a in attrList]):
|
||||
logger.warning('Attribute %s do not has exactly one value, skipping %s', attrName, line)
|
||||
return []
|
||||
|
||||
val = [''.join([asList(attributes.get(a, ['']))[0] for a in attrList])]
|
||||
elif '**' in attrName:
|
||||
# Prepend the value after : to value before :
|
||||
attr, prependable = attrName.split('**', 1)
|
||||
val = [prependable + a for a in asList(attributes.get(attr, []))]
|
||||
else:
|
||||
val = asList(attributes.get(attrName, []))
|
||||
return val
|
||||
|
||||
logger.debug('******** Attributes: %s', attributes)
|
||||
for line in field.splitlines():
|
||||
equalPos = line.find('=')
|
||||
if (
|
||||
@@ -275,27 +332,29 @@ class RegexLdap(auths.Authenticator):
|
||||
# if pattern do not have groups, define one with complete pattern (i.e. id=.* --> id=(.*))
|
||||
if pattern.find('(') == -1:
|
||||
pattern = '(' + pattern + ')'
|
||||
val = attributes.get(attr, [])
|
||||
|
||||
if not isinstance(val, list): # May we have a single value
|
||||
val = [val]
|
||||
val = getAttr(attr)
|
||||
|
||||
logger.debug('Pattern: %s', pattern)
|
||||
|
||||
for v in val:
|
||||
try:
|
||||
searchResult = re.search(
|
||||
pattern, v, re.IGNORECASE
|
||||
) # @UndefinedVariable
|
||||
logger.debug('Pattern: %s on value %s', pattern, v)
|
||||
searchResult = re.search(pattern, v, re.IGNORECASE) # @UndefinedVariable
|
||||
if searchResult is None:
|
||||
continue
|
||||
logger.debug("Found against %s: %s ", v, searchResult.groups())
|
||||
res.append(''.join(searchResult.groups()))
|
||||
except Exception:
|
||||
except Exception: # nosec: If not a valid regex, just ignore it
|
||||
pass # Ignore exceptions here
|
||||
logger.debug('Res: %s', res)
|
||||
return res
|
||||
|
||||
def mfaStorageKey(self, username: str) -> str:
|
||||
return 'mfa_' + self.dbAuthenticator().uuid + username # type: ignore
|
||||
|
||||
def mfaIdentifier(self, username: str) -> str:
|
||||
return self.storage.getPickle(self.mfaStorageKey(username)) or ''
|
||||
|
||||
def valuesDict(self) -> gui.ValuesDictType:
|
||||
return {
|
||||
'host': self._host,
|
||||
@@ -310,12 +369,15 @@ class RegexLdap(auths.Authenticator):
|
||||
'groupNameAttr': self._groupNameAttr,
|
||||
'userNameAttr': self._userNameAttr,
|
||||
'altClass': self._altClass,
|
||||
'mfaAttr': self._mfaAttr,
|
||||
'verifySsl': gui.boolToStr(self._verifySsl),
|
||||
'certificate': self._certificate,
|
||||
}
|
||||
|
||||
def marshal(self) -> bytes:
|
||||
return '\t'.join(
|
||||
[
|
||||
'v3',
|
||||
'v5',
|
||||
self._host,
|
||||
self._port,
|
||||
gui.boolToStr(self._ssl),
|
||||
@@ -328,63 +390,67 @@ class RegexLdap(auths.Authenticator):
|
||||
self._groupNameAttr,
|
||||
self._userNameAttr,
|
||||
self._altClass,
|
||||
self._mfaAttr,
|
||||
gui.boolToStr(self._verifySsl),
|
||||
self._certificate.strip(),
|
||||
]
|
||||
).encode('utf8')
|
||||
|
||||
def unmarshal(self, data: bytes) -> None:
|
||||
vals = data.decode('utf8').split('\t')
|
||||
|
||||
self._verifySsl = False # Backward compatibility
|
||||
self._mfaAttr = '' # Backward compatibility
|
||||
self._certificate = '' # Backward compatibility
|
||||
|
||||
# Common
|
||||
logger.debug('Common: %s', vals[1:11])
|
||||
(
|
||||
self._host,
|
||||
self._port,
|
||||
ssl,
|
||||
self._username,
|
||||
self._password,
|
||||
self._timeout,
|
||||
self._ldapBase,
|
||||
self._userClass,
|
||||
self._userIdAttr,
|
||||
self._groupNameAttr,
|
||||
) = vals[1:11]
|
||||
self._ssl = gui.strToBool(ssl)
|
||||
|
||||
if vals[0] == 'v1':
|
||||
logger.debug("Data: %s", vals[1:])
|
||||
(
|
||||
self._host,
|
||||
self._port,
|
||||
ssl,
|
||||
self._username,
|
||||
self._password,
|
||||
self._timeout,
|
||||
self._ldapBase,
|
||||
self._userClass,
|
||||
self._userIdAttr,
|
||||
self._groupNameAttr,
|
||||
_regex,
|
||||
self._userNameAttr,
|
||||
) = vals[1:]
|
||||
self._ssl = gui.strToBool(ssl)
|
||||
logger.debug("Data: %s", vals[11:])
|
||||
_regex, self._userNameAttr = vals[11:]
|
||||
self._groupNameAttr = self._groupNameAttr + '=' + _regex
|
||||
self._userNameAttr = '\n'.join(self._userNameAttr.split(','))
|
||||
elif vals[0] == 'v2':
|
||||
logger.debug("Data v2: %s", vals[1:])
|
||||
(
|
||||
self._host,
|
||||
self._port,
|
||||
ssl,
|
||||
self._username,
|
||||
self._password,
|
||||
self._timeout,
|
||||
self._ldapBase,
|
||||
self._userClass,
|
||||
self._userIdAttr,
|
||||
self._groupNameAttr,
|
||||
self._userNameAttr,
|
||||
) = vals[1:]
|
||||
self._userNameAttr = vals[11]
|
||||
self._ssl = gui.strToBool(ssl)
|
||||
elif vals[0] == 'v3':
|
||||
logger.debug("Data v3: %s", vals[1:])
|
||||
(
|
||||
self._host,
|
||||
self._port,
|
||||
ssl,
|
||||
self._username,
|
||||
self._password,
|
||||
self._timeout,
|
||||
self._ldapBase,
|
||||
self._userClass,
|
||||
self._userIdAttr,
|
||||
self._groupNameAttr,
|
||||
self._userNameAttr,
|
||||
self._altClass,
|
||||
) = vals[1:]
|
||||
self._ssl = gui.strToBool(ssl)
|
||||
) = vals[11:]
|
||||
elif vals[0] == 'v4':
|
||||
logger.debug("Data v4: %s", vals[1:])
|
||||
(
|
||||
self._userNameAttr,
|
||||
self._altClass,
|
||||
self._mfaAttr,
|
||||
) = vals[11:]
|
||||
elif vals[0] == 'v5':
|
||||
logger.debug("Data v5: %s", vals[1:])
|
||||
(
|
||||
self._userNameAttr,
|
||||
self._altClass,
|
||||
self._mfaAttr,
|
||||
verifySsl,
|
||||
self._certificate,
|
||||
) = vals[11:]
|
||||
self._verifySsl = gui.strToBool(verifySsl)
|
||||
|
||||
def __connection(self) -> typing.Any:
|
||||
"""
|
||||
@@ -392,7 +458,7 @@ class RegexLdap(auths.Authenticator):
|
||||
@return: Connection established
|
||||
@raise exception: If connection could not be established
|
||||
"""
|
||||
if self._connection is None: # We want this method also to check credentials
|
||||
if self._connection is None: # If connection is not established, try to connect
|
||||
self._connection = ldaputil.connection(
|
||||
self._username,
|
||||
self._password,
|
||||
@@ -428,6 +494,9 @@ class RegexLdap(auths.Authenticator):
|
||||
+ self.__getAttrsFromField(self._userNameAttr)
|
||||
+ self.__getAttrsFromField(self._groupNameAttr)
|
||||
)
|
||||
if self._mfaAttr:
|
||||
attributes = attributes + self.__getAttrsFromField(self._mfaAttr)
|
||||
|
||||
user = ldaputil.getFirst(
|
||||
con=self.__connection(),
|
||||
base=self._ldapBase,
|
||||
@@ -484,9 +553,7 @@ class RegexLdap(auths.Authenticator):
|
||||
def __getUserRealName(self, user: ldaputil.LDAPResultType):
|
||||
return ' '.join(self.__processField(self._userNameAttr, user))
|
||||
|
||||
def authenticate(
|
||||
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
|
||||
) -> bool:
|
||||
def authenticate(self, username: str, credentials: str, groupsManager: 'auths.GroupsManager') -> bool:
|
||||
"""
|
||||
Must authenticate the user.
|
||||
We can have to different situations here:
|
||||
@@ -501,22 +568,23 @@ class RegexLdap(auths.Authenticator):
|
||||
usr = self.__getUser(username)
|
||||
|
||||
if usr is None:
|
||||
authLogLogin(
|
||||
getRequest(), self.dbAuthenticator(), username, 'Invalid user'
|
||||
)
|
||||
authLogLogin(getRequest(), self.dbAuthenticator(), username, 'Invalid user')
|
||||
return False
|
||||
|
||||
try:
|
||||
# Let's see first if it credentials are fine
|
||||
self.__connectAs(
|
||||
usr['dn'], credentials
|
||||
) # Will raise an exception if it can't connect
|
||||
self.__connectAs(usr['dn'], credentials) # Will raise an exception if it can't connect
|
||||
except:
|
||||
authLogLogin(
|
||||
getRequest(), self.dbAuthenticator(), username, 'Invalid password'
|
||||
)
|
||||
authLogLogin(getRequest(), self.dbAuthenticator(), username, 'Invalid password')
|
||||
return False
|
||||
|
||||
# store the user mfa attribute if it is set
|
||||
if self._mfaAttr:
|
||||
self.storage.putPickle(
|
||||
self.mfaStorageKey(username),
|
||||
usr[self._mfaAttr][0],
|
||||
)
|
||||
|
||||
groupsManager.validate(self.__getGroups(usr))
|
||||
|
||||
return True
|
||||
@@ -593,9 +661,7 @@ class RegexLdap(auths.Authenticator):
|
||||
return res
|
||||
except Exception:
|
||||
logger.exception("Exception: ")
|
||||
raise auths.exceptions.AuthenticatorException(
|
||||
_('Too many results, be more specific')
|
||||
)
|
||||
raise auths.exceptions.AuthenticatorException(_('Too many results, be more specific'))
|
||||
|
||||
@staticmethod
|
||||
def test(env, data):
|
||||
@@ -603,9 +669,7 @@ class RegexLdap(auths.Authenticator):
|
||||
auth = RegexLdap(None, env, data) # type: ignore # Regexldap does not use "dbAuth", so it's safe...
|
||||
return auth.testConnection()
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
'Exception found testing Simple LDAP auth %s: %s', e.__class__, e
|
||||
)
|
||||
logger.error('Exception found testing Simple LDAP auth %s: %s', e.__class__, e)
|
||||
return [False, "Error testing connection"]
|
||||
|
||||
def testConnection(self):
|
||||
@@ -634,9 +698,7 @@ class RegexLdap(auths.Authenticator):
|
||||
raise Exception()
|
||||
return [
|
||||
False,
|
||||
_(
|
||||
'Ldap user class seems to be incorrect (no user found by that class)'
|
||||
),
|
||||
_('Ldap user class seems to be incorrect (no user found by that class)'),
|
||||
]
|
||||
except Exception:
|
||||
# If found 1 or more, all right
|
||||
@@ -649,8 +711,7 @@ class RegexLdap(auths.Authenticator):
|
||||
con.search_ext_s(
|
||||
base=self._ldapBase,
|
||||
scope=ldap.SCOPE_SUBTREE, # type: ignore # ldap.SCOPE_* not resolved due to dynamic creation?
|
||||
filterstr='(&(objectClass=%s)(%s=*))'
|
||||
% (self._userClass, self._userIdAttr),
|
||||
filterstr='(&(objectClass=%s)(%s=*))' % (self._userClass, self._userIdAttr),
|
||||
sizelimit=1,
|
||||
)
|
||||
)
|
||||
@@ -659,9 +720,7 @@ class RegexLdap(auths.Authenticator):
|
||||
raise Exception()
|
||||
return [
|
||||
False,
|
||||
_(
|
||||
'Ldap user id attr is probably wrong (can\'t find any user with both conditions)'
|
||||
),
|
||||
_('Ldap user id attr is probably wrong (can\'t find any user with both conditions)'),
|
||||
]
|
||||
except Exception:
|
||||
# If found 1 or more, all right
|
||||
@@ -688,9 +747,7 @@ class RegexLdap(auths.Authenticator):
|
||||
continue
|
||||
return [
|
||||
False,
|
||||
_(
|
||||
'Ldap group id attribute seems to be incorrect (no group found by that attribute)'
|
||||
),
|
||||
_('Ldap group id attribute seems to be incorrect (no group found by that attribute)'),
|
||||
]
|
||||
|
||||
# Now try to test regular expression to see if it matches anything (
|
||||
|
@@ -86,7 +86,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
tab=gui.CREDENTIALS_TAB,
|
||||
)
|
||||
password = gui.PasswordField(
|
||||
lenth=32,
|
||||
length=32,
|
||||
label=_('Password'),
|
||||
order=5,
|
||||
tooltip=_('Password of the ldap user'),
|
||||
@@ -97,15 +97,35 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
length=3,
|
||||
label=_('Timeout'),
|
||||
defvalue='10',
|
||||
order=6,
|
||||
order=10,
|
||||
tooltip=_('Timeout in seconds of connection to LDAP'),
|
||||
required=True,
|
||||
minValue=1,
|
||||
tab=gui.ADVANCED_TAB,
|
||||
)
|
||||
verifySsl = gui.CheckBoxField(
|
||||
label=_('Verify SSL'),
|
||||
defvalue=True,
|
||||
order=11,
|
||||
tooltip=_(
|
||||
'If checked, SSL verification will be enforced. If not, SSL verification will be disabled'
|
||||
),
|
||||
tab=gui.ADVANCED_TAB,
|
||||
)
|
||||
certificate = gui.TextField(
|
||||
length=8192,
|
||||
multiline=4,
|
||||
label=_('Certificate'),
|
||||
order=12,
|
||||
tooltip=_('Certificate to use for SSL verification'),
|
||||
required=False,
|
||||
tab=gui.ADVANCED_TAB,
|
||||
)
|
||||
|
||||
ldapBase = gui.TextField(
|
||||
length=64,
|
||||
label=_('Base'),
|
||||
order=7,
|
||||
order=30,
|
||||
tooltip=_('Common search base (used for "users" and "groups")'),
|
||||
required=True,
|
||||
tab=_('Ldap info'),
|
||||
@@ -114,7 +134,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
length=64,
|
||||
label=_('User class'),
|
||||
defvalue='posixAccount',
|
||||
order=8,
|
||||
order=31,
|
||||
tooltip=_('Class for LDAP users (normally posixAccount)'),
|
||||
required=True,
|
||||
tab=_('Ldap info'),
|
||||
@@ -123,7 +143,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
length=64,
|
||||
label=_('User Id Attr'),
|
||||
defvalue='uid',
|
||||
order=9,
|
||||
order=32,
|
||||
tooltip=_('Attribute that contains the user id'),
|
||||
required=True,
|
||||
tab=_('Ldap info'),
|
||||
@@ -132,7 +152,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
length=64,
|
||||
label=_('User Name Attr'),
|
||||
defvalue='uid',
|
||||
order=10,
|
||||
order=33,
|
||||
tooltip=_(
|
||||
'Attributes that contains the user name (list of comma separated values)'
|
||||
),
|
||||
@@ -143,7 +163,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
length=64,
|
||||
label=_('Group class'),
|
||||
defvalue='posixGroup',
|
||||
order=11,
|
||||
order=34,
|
||||
tooltip=_('Class for LDAP groups (normally poxisGroup)'),
|
||||
required=True,
|
||||
tab=_('Ldap info'),
|
||||
@@ -152,7 +172,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
length=64,
|
||||
label=_('Group Id Attr'),
|
||||
defvalue='cn',
|
||||
order=12,
|
||||
order=35,
|
||||
tooltip=_('Attribute that contains the group id'),
|
||||
required=True,
|
||||
tab=_('Ldap info'),
|
||||
@@ -161,15 +181,25 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
length=64,
|
||||
label=_('Group membership attr'),
|
||||
defvalue='memberUid',
|
||||
order=13,
|
||||
order=36,
|
||||
tooltip=_('Attribute of the group that contains the users belonging to it'),
|
||||
required=True,
|
||||
tab=_('Ldap info'),
|
||||
)
|
||||
mfaAttr = gui.TextField(
|
||||
length=2048,
|
||||
multiline=2,
|
||||
label=_('MFA attribute'),
|
||||
order=13,
|
||||
tooltip=_('Attribute from where to extract the MFA code'),
|
||||
required=False,
|
||||
tab=gui.MFA_TAB,
|
||||
)
|
||||
|
||||
typeName = _('SimpleLDAP (DEPRECATED)')
|
||||
|
||||
typeName = _('SimpleLDAP')
|
||||
typeType = 'SimpleLdapAuthenticator'
|
||||
typeDescription = _('Simple LDAP authenticator (DEPRECATED)')
|
||||
typeDescription = _('Simple LDAP authenticator')
|
||||
iconFile = 'auth.png'
|
||||
|
||||
# If it has and external source where to get "new" users (groups must be declared inside UDS)
|
||||
@@ -197,6 +227,10 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
_groupIdAttr: str = ''
|
||||
_memberAttr: str = ''
|
||||
_userNameAttr: str = ''
|
||||
_mfaAttr: str = ''
|
||||
_verifySsl: bool = True
|
||||
_certificate: str = ''
|
||||
|
||||
|
||||
def initialize(self, values: typing.Optional[typing.Dict[str, typing.Any]]) -> None:
|
||||
if values:
|
||||
@@ -215,6 +249,9 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
self._userNameAttr = values['userNameAttr'].replace(
|
||||
' ', ''
|
||||
) # Removes white spaces
|
||||
self._mfaAttr = values['mfaAttr']
|
||||
self._verifySsl = gui.strToBool(values['verifySsl'])
|
||||
self._certificate = values['certificate']
|
||||
|
||||
def valuesDict(self) -> gui.ValuesDictType:
|
||||
return {
|
||||
@@ -231,12 +268,15 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
'groupIdAttr': self._groupIdAttr,
|
||||
'memberAttr': self._memberAttr,
|
||||
'userNameAttr': self._userNameAttr,
|
||||
'mfaAttr': self._mfaAttr,
|
||||
'verifySsl': gui.boolToStr(self._verifySsl),
|
||||
'certificate': self._certificate,
|
||||
}
|
||||
|
||||
def marshal(self) -> bytes:
|
||||
return '\t'.join(
|
||||
[
|
||||
'v1',
|
||||
'v2',
|
||||
self._host,
|
||||
self._port,
|
||||
gui.boolToStr(self._ssl),
|
||||
@@ -250,41 +290,59 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
self._groupIdAttr,
|
||||
self._memberAttr,
|
||||
self._userNameAttr,
|
||||
self._mfaAttr,
|
||||
gui.boolToStr(self._verifySsl),
|
||||
self._certificate.strip(),
|
||||
]
|
||||
).encode('utf8')
|
||||
|
||||
def unmarshal(self, data: bytes):
|
||||
vals = data.decode('utf8').split('\t')
|
||||
if vals[0] == 'v1':
|
||||
logger.debug("Data: %s", vals[1:])
|
||||
self._verifySsl = False # Backward compatibility
|
||||
self._mfaAttr = '' # Backward compatibility
|
||||
self._certificate = '' # Backward compatibility
|
||||
|
||||
logger.debug("Data: %s", vals[1:])
|
||||
(
|
||||
self._host,
|
||||
self._port,
|
||||
ssl,
|
||||
self._username,
|
||||
self._password,
|
||||
self._timeout,
|
||||
self._ldapBase,
|
||||
self._userClass,
|
||||
self._groupClass,
|
||||
self._userIdAttr,
|
||||
self._groupIdAttr,
|
||||
self._memberAttr,
|
||||
self._userNameAttr,
|
||||
) = vals[1:14]
|
||||
self._ssl = gui.strToBool(ssl)
|
||||
|
||||
if vals[0] == 'v2':
|
||||
(
|
||||
self._host,
|
||||
self._port,
|
||||
ssl,
|
||||
self._username,
|
||||
self._password,
|
||||
self._timeout,
|
||||
self._ldapBase,
|
||||
self._userClass,
|
||||
self._groupClass,
|
||||
self._userIdAttr,
|
||||
self._groupIdAttr,
|
||||
self._memberAttr,
|
||||
self._userNameAttr,
|
||||
) = vals[1:]
|
||||
self._ssl = gui.strToBool(ssl)
|
||||
self._mfaAttr,
|
||||
verifySsl,
|
||||
self._certificate
|
||||
) = vals[14:17]
|
||||
self._verifySsl = gui.strToBool(verifySsl)
|
||||
|
||||
def mfaStorageKey(self, username: str) -> str:
|
||||
return 'mfa_' + str(self.dbAuthenticator().uuid) + username
|
||||
|
||||
def mfaIdentifier(self, username: str) -> str:
|
||||
return self.storage.getPickle(self.mfaStorageKey(username)) or ''
|
||||
|
||||
def __connection(
|
||||
self,
|
||||
username: typing.Optional[str] = None,
|
||||
password: typing.Optional[str] = None,
|
||||
self
|
||||
):
|
||||
"""
|
||||
Tries to connect to ldap. If username is None, it tries to connect using user provided credentials.
|
||||
@return: Connection established
|
||||
@raise exception: If connection could not be established
|
||||
"""
|
||||
if self._connection is None: # We want this method also to check credentials
|
||||
if self._connection is None: # We are not connected
|
||||
self._connection = ldaputil.connection(
|
||||
self._username,
|
||||
self._password,
|
||||
@@ -293,6 +351,8 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
ssl=self._ssl,
|
||||
timeout=int(self._timeout),
|
||||
debug=False,
|
||||
verify_ssl=self._verifySsl,
|
||||
certificate=self._certificate,
|
||||
)
|
||||
|
||||
return self._connection
|
||||
@@ -306,6 +366,8 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
ssl=self._ssl,
|
||||
timeout=int(self._timeout),
|
||||
debug=False,
|
||||
verify_ssl=self._verifySsl,
|
||||
certificate=self._certificate,
|
||||
)
|
||||
|
||||
def __getUser(self, username: str) -> typing.Optional[ldaputil.LDAPResultType]:
|
||||
@@ -315,13 +377,17 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
@return: None if username is not found, an dictionary of LDAP entry attributes if found.
|
||||
@note: Active directory users contains the groups it belongs to in "memberOf" attribute
|
||||
"""
|
||||
attributes = [i for i in self._userNameAttr.split(',') + [self._userIdAttr]]
|
||||
if self._mfaAttr:
|
||||
attributes = attributes + [self._mfaAttr]
|
||||
|
||||
return ldaputil.getFirst(
|
||||
con=self.__connection(),
|
||||
base=self._ldapBase,
|
||||
objectClass=self._userClass,
|
||||
field=self._userIdAttr,
|
||||
value=username,
|
||||
attributes=[i for i in self._userNameAttr.split(',') + [self._userIdAttr]],
|
||||
attributes=attributes,
|
||||
sizeLimit=LDAP_RESULT_LIMIT,
|
||||
)
|
||||
|
||||
@@ -417,6 +483,13 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
|
||||
)
|
||||
return False
|
||||
|
||||
# store the user mfa attribute if it is set
|
||||
if self._mfaAttr:
|
||||
self.storage.putPickle(
|
||||
self.mfaStorageKey(username),
|
||||
user[self._mfaAttr][0],
|
||||
)
|
||||
|
||||
groupsManager.validate(self.__getGroups(user))
|
||||
|
||||
return True
|
||||
|
@@ -53,7 +53,7 @@ def __init__():
|
||||
from uds.core import auths
|
||||
|
||||
# Dinamycally import children of this package. The __init__.py files must declare authenticators as subclasses of auths.Authenticator
|
||||
pkgpath = os.path.dirname(sys.modules[__name__].__file__)
|
||||
pkgpath = os.path.dirname(sys.modules[__name__].__file__) # type: ignore
|
||||
for _, name, _ in pkgutil.iter_modules([pkgpath]):
|
||||
# __import__(name, globals(), locals(), [], 1)
|
||||
importlib.import_module('.' + name, __name__) # import module
|
||||
|
@@ -50,6 +50,7 @@ from django.utils.translation import ugettext as _
|
||||
from uds.core import auths
|
||||
from uds.core.util import log
|
||||
from uds.core.util import net
|
||||
from uds.core.util import config
|
||||
from uds.core.util.config import GlobalConfig
|
||||
from uds.core.util.stats import events
|
||||
from uds.core.util.state import State
|
||||
@@ -69,6 +70,7 @@ authLogger = logging.getLogger('authLog')
|
||||
USER_KEY = 'uk'
|
||||
PASS_KEY = 'pk'
|
||||
EXPIRY_KEY = 'ek'
|
||||
AUTHORIZED_KEY = 'ak'
|
||||
ROOT_ID = -20091204 # Any negative number will do the trick
|
||||
UDS_COOKIE_LENGTH = 48
|
||||
|
||||
@@ -86,7 +88,9 @@ def getUDSCookie(
|
||||
if 'uds' not in request.COOKIES:
|
||||
cookie = cryptoManager().randomString(UDS_COOKIE_LENGTH)
|
||||
if response is not None:
|
||||
response.set_cookie('uds', cookie, samesite='Lax')
|
||||
response.set_cookie(
|
||||
'uds', cookie, samesite='Lax', httponly=GlobalConfig.ENHANCED_SECURITY.getBool()
|
||||
)
|
||||
request.COOKIES['uds'] = cookie
|
||||
else:
|
||||
cookie = request.COOKIES['uds'][:UDS_COOKIE_LENGTH]
|
||||
@@ -122,32 +126,37 @@ def getRootUser() -> User:
|
||||
|
||||
# Decorator to make easier protect pages that needs to be logged in
|
||||
def webLoginRequired(
|
||||
admin: typing.Union[bool, str] = False
|
||||
) -> typing.Callable[[typing.Callable[..., RT]], typing.Callable[..., RT]]:
|
||||
"""
|
||||
Decorator to set protection to access page
|
||||
admin: typing.Union[bool, typing.Literal['admin']] = False
|
||||
) -> typing.Callable[[typing.Callable[..., HttpResponse]], typing.Callable[..., HttpResponse]]:
|
||||
"""Decorator to set protection to access page
|
||||
Look for samples at uds.core.web.views
|
||||
if admin == True, needs admin or staff
|
||||
if admin == 'admin', needs admin
|
||||
|
||||
Args:
|
||||
admin (bool, optional): If True, needs admin or staff. Is it's "admin" literal, needs admin . Defaults to False (any user).
|
||||
|
||||
Returns:
|
||||
typing.Callable[[typing.Callable[..., HttpResponse]], typing.Callable[..., HttpResponse]]: Decorator
|
||||
|
||||
Note:
|
||||
This decorator is used to protect pages that needs to be logged in.
|
||||
To protect against ajax calls, use `denyNonAuthenticated` instead
|
||||
"""
|
||||
|
||||
def decorator(view_func: typing.Callable[..., RT]) -> typing.Callable[..., RT]:
|
||||
def _wrapped_view(request: 'ExtendedHttpRequest', *args, **kwargs) -> RT:
|
||||
def decorator(view_func: typing.Callable[..., HttpResponse]) -> typing.Callable[..., HttpResponse]:
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request: 'ExtendedHttpRequest', *args, **kwargs) -> HttpResponse:
|
||||
"""
|
||||
Wrapped function for decorator
|
||||
"""
|
||||
if not request.user:
|
||||
# url = request.build_absolute_uri(GlobalConfig.LOGIN_URL.get())
|
||||
# if GlobalConfig.REDIRECT_TO_HTTPS.getBool() is True:
|
||||
# url = url.replace('http://', 'https://')
|
||||
# logger.debug('No user found, redirecting to %s', url)
|
||||
return HttpResponseRedirect(reverse('page.login')) # type: ignore
|
||||
# If no user or user authorization is not completed...
|
||||
if not request.user or not request.authorized:
|
||||
return HttpResponseRedirect(reverse('page.login'))
|
||||
|
||||
if admin is True or admin == 'admin': # bool or string "admin"
|
||||
if request.user.isStaff() is False or (
|
||||
admin == 'admin' and not request.user.is_admin
|
||||
):
|
||||
return HttpResponseForbidden(_('Forbidden')) # type: ignore
|
||||
if admin in (True, 'admin'):
|
||||
if request.user.isStaff() is False or (admin == 'admin' and not request.user.is_admin):
|
||||
return HttpResponseForbidden(_('Forbidden'))
|
||||
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
@@ -162,12 +171,9 @@ def isTrustedSource(ip: str) -> bool:
|
||||
|
||||
|
||||
# Decorator to protect pages that needs to be accessed from "trusted sites"
|
||||
def trustedSourceRequired(
|
||||
view_func: typing.Callable[..., RT]
|
||||
) -> typing.Callable[..., RT]:
|
||||
def trustedSourceRequired(view_func: typing.Callable[..., RT]) -> typing.Callable[..., RT]:
|
||||
"""
|
||||
Decorator to set protection to access page
|
||||
look for sample at uds.dispatchers.pam
|
||||
"""
|
||||
|
||||
@wraps(view_func)
|
||||
@@ -189,12 +195,12 @@ def trustedSourceRequired(
|
||||
|
||||
|
||||
# decorator to deny non authenticated requests
|
||||
def denyNonAuthenticated(
|
||||
view_func: typing.Callable[..., RT]
|
||||
) -> typing.Callable[..., RT]:
|
||||
# The difference with webLoginRequired is that this one does not redirect to login page
|
||||
# it's designed to be used in ajax calls mainly
|
||||
def denyNonAuthenticated(view_func: typing.Callable[..., RT]) -> typing.Callable[..., RT]:
|
||||
@wraps(view_func)
|
||||
def _wrapped_view(request: 'ExtendedHttpRequest', *args, **kwargs) -> RT:
|
||||
if not request.user:
|
||||
if not request.user or not request.authorized:
|
||||
return HttpResponseForbidden() # type: ignore
|
||||
return view_func(request, *args, **kwargs)
|
||||
|
||||
@@ -223,9 +229,7 @@ def __registerUser(
|
||||
# Now we update database groups for this user
|
||||
usr.getManager().recreateGroups(usr)
|
||||
# And add an login event
|
||||
events.addEvent(
|
||||
authenticator, events.ET_LOGIN, username=username, srcip=request.ip
|
||||
)
|
||||
events.addEvent(authenticator, events.ET_LOGIN, username=username, srcip=request.ip)
|
||||
events.addEvent(
|
||||
authenticator,
|
||||
events.ET_PLATFORM,
|
||||
@@ -253,9 +257,7 @@ def authenticate(
|
||||
This is so because in some situations we may want to use a "trusted" method (internalAuthenticate is never invoked directly from web)
|
||||
@return: None if authentication fails, User object (database object) if authentication is o.k.
|
||||
"""
|
||||
logger.debug(
|
||||
'Authenticating user %s with authenticator %s', username, authenticator
|
||||
)
|
||||
logger.debug('Authenticating user %s with authenticator %s', username, authenticator)
|
||||
|
||||
# If global root auth is enabled && user/password is correct,
|
||||
if (
|
||||
@@ -277,7 +279,7 @@ def authenticate(
|
||||
return None
|
||||
|
||||
if isinstance(res, str):
|
||||
return res # type: ignore # note: temporal fix on 3.5 for possible redirect on failed login
|
||||
return res # type: ignore # note: temporal fix on >= 3.5 for possible redirect on failed login
|
||||
|
||||
logger.debug('Groups manager: %s', gm)
|
||||
|
||||
@@ -292,9 +294,7 @@ def authenticate(
|
||||
return __registerUser(authenticator, authInstance, username)
|
||||
|
||||
|
||||
def authenticateViaCallback(
|
||||
authenticator: Authenticator, params: typing.Any
|
||||
) -> typing.Optional[User]:
|
||||
def authenticateViaCallback(authenticator: Authenticator, params: typing.Any) -> typing.Optional[User]:
|
||||
"""
|
||||
Given an username, this method will get invoked whenever the url for a callback
|
||||
for an authenticator is requested.
|
||||
@@ -315,6 +315,8 @@ def authenticateViaCallback(
|
||||
"""
|
||||
gm = auths.GroupsManager(authenticator)
|
||||
authInstance = authenticator.getInstance()
|
||||
|
||||
logger.debug('Authenticating user with authenticator %s and params %s', authenticator, params)
|
||||
|
||||
# If there is no callback for this authenticator...
|
||||
if authInstance.authCallback is auths.Authenticator.authCallback:
|
||||
@@ -344,7 +346,7 @@ def authInfoUrl(authenticator: typing.Union[str, bytes, Authenticator]) -> str:
|
||||
elif isinstance(authenticator, bytes):
|
||||
name = authenticator.decode('utf8')
|
||||
else:
|
||||
name = authenticator.name
|
||||
name = typing.cast('Authenticator', authenticator).name
|
||||
|
||||
return reverse('page.auth.info', kwargs={'authName': name})
|
||||
|
||||
@@ -361,9 +363,7 @@ def webLogin(
|
||||
"""
|
||||
from uds import REST
|
||||
|
||||
if (
|
||||
user.id != ROOT_ID
|
||||
): # If not ROOT user (this user is not inside any authenticator)
|
||||
if user.id != ROOT_ID: # If not ROOT user (this user is not inside any authenticator)
|
||||
manager_id = user.manager.id
|
||||
else:
|
||||
manager_id = -1
|
||||
@@ -372,10 +372,13 @@ def webLogin(
|
||||
cookie = getUDSCookie(request, response)
|
||||
|
||||
user.updateLastAccess()
|
||||
request.authorized = False # For now, we don't know if the user is authorized until MFA is checked
|
||||
# If Enabled zero trust, do not cache credentials
|
||||
if GlobalConfig.ENFORCE_ZERO_TRUST.getBool(False):
|
||||
password = ''
|
||||
|
||||
request.session[USER_KEY] = user.id
|
||||
request.session[PASS_KEY] = cryptoManager().symCrypt(
|
||||
password, cookie
|
||||
) # Stores "bytes"
|
||||
request.session[PASS_KEY] = cryptoManager().symCrypt(password, cookie) # Stores "bytes"
|
||||
|
||||
# Ensures that this user will have access through REST api if logged in through web interface
|
||||
# Note that REST api will set the session expiry to selected value if user is an administrator
|
||||
@@ -399,27 +402,27 @@ def webPassword(request: HttpRequest) -> str:
|
||||
session (db) and client browser cookies. This method uses this two values to recompose the user password
|
||||
so we can provide it to remote sessions.
|
||||
"""
|
||||
if hasattr(request, 'session'):
|
||||
if hasattr(request, '_cryptedpass') and hasattr(request, '_scrambler'):
|
||||
return cryptoManager().symDecrpyt(
|
||||
request.session.get(PASS_KEY, ''), getUDSCookie(request)
|
||||
) # recover as original unicode string
|
||||
else: # No session, get from _session instead, this is an "client" REST request
|
||||
return cryptoManager().symDecrpyt(request._cryptedpass, request._scrambler) # type: ignore
|
||||
getattr(request, '_cryptedpass'),
|
||||
getattr(request, '_scrambler'),
|
||||
)
|
||||
return cryptoManager().symDecrpyt(
|
||||
request.session.get(PASS_KEY, ''), getUDSCookie(request)
|
||||
) # recover as original unicode string
|
||||
|
||||
|
||||
def webLogout(
|
||||
request: 'ExtendedHttpRequest', exit_url: typing.Optional[str] = None
|
||||
) -> HttpResponse:
|
||||
def webLogout(request: 'ExtendedHttpRequest', exit_url: typing.Optional[str] = None) -> HttpResponse:
|
||||
"""
|
||||
Helper function to clear user related data from session. If this method is not used, the session we be cleaned anyway
|
||||
by django in regular basis.
|
||||
"""
|
||||
if exit_url is None:
|
||||
exit_url = request.build_absolute_uri(reverse('page.login'))
|
||||
# exit_url = GlobalConfig.LOGIN_URL.get()
|
||||
# if GlobalConfig.REDIRECT_TO_HTTPS.getBool() is True:
|
||||
# exit_url = exit_url.replace('http://', 'https://')
|
||||
|
||||
tag = request.session.get('tag', None)
|
||||
if tag and config.GlobalConfig.REDIRECT_TO_TAG_ON_LOGOUT.getBool(False):
|
||||
exit_page = reverse('page.login.tag', kwargs={'tag': tag})
|
||||
else:
|
||||
exit_page = reverse('page.login')
|
||||
exit_url = exit_url or exit_page
|
||||
try:
|
||||
if request.user:
|
||||
authenticator = request.user.manager.getInstance()
|
||||
@@ -434,14 +437,15 @@ def webLogout(
|
||||
srcip=request.ip,
|
||||
)
|
||||
else: # No user, redirect to /
|
||||
return HttpResponseRedirect(reverse('page.login'))
|
||||
return HttpResponseRedirect(exit_page)
|
||||
except Exception:
|
||||
raise
|
||||
finally:
|
||||
# Try to delete session
|
||||
request.session.flush()
|
||||
request.authorized = False
|
||||
|
||||
response = HttpResponseRedirect(request.build_absolute_uri(exit_url))
|
||||
response = HttpResponseRedirect(exit_url)
|
||||
if authenticator:
|
||||
authenticator.webLogoutHook(username, request, response)
|
||||
return response
|
||||
@@ -471,13 +475,11 @@ def authLogLogin(
|
||||
]
|
||||
)
|
||||
)
|
||||
level = log.INFO if logStr == 'Logged in' else log.ERROR
|
||||
level = log.INFO if logStr in ('Logged in', 'Federated login') else log.ERROR
|
||||
log.doLog(
|
||||
authenticator,
|
||||
level,
|
||||
'user {} has {} from {} where os is {}'.format(
|
||||
userName, logStr, request.ip, request.os['OS'].value[0]
|
||||
),
|
||||
'user {} has {} from {} where os is {}'.format(userName, logStr, request.ip, request.os['OS'].value[0]),
|
||||
log.WEB,
|
||||
)
|
||||
|
||||
@@ -501,6 +503,4 @@ def authLogLogout(request: 'ExtendedHttpRequest') -> None:
|
||||
'user {} has logged out from {}'.format(request.user.name, request.ip),
|
||||
log.WEB,
|
||||
)
|
||||
log.doLog(
|
||||
request.user, log.INFO, 'has logged out from {}'.format(request.ip), log.WEB
|
||||
)
|
||||
log.doLog(request.user, log.INFO, 'has logged out from {}'.format(request.ip), log.WEB)
|
||||
|
@@ -290,6 +290,25 @@ class Authenticator(Module): # pylint: disable=too-many-public-methods
|
||||
"""
|
||||
return []
|
||||
|
||||
def mfaIdentifier(self, username: str) -> str:
|
||||
"""
|
||||
If this method is provided by an authenticator, the user will be allowed to enter a MFA code
|
||||
You must return the value used by a MFA provider to identify the user (i.e. email, phone number, etc)
|
||||
If not provided, or the return value is '', the user will be allowed to access UDS without MFA
|
||||
|
||||
Note: Field capture will be responsible of provider. Put it on MFA tab of user form.
|
||||
Take into consideration that mfaIdentifier will never be invoked if the user has not been
|
||||
previously authenticated. (that is, authenticate method has already been called)
|
||||
"""
|
||||
return ''
|
||||
|
||||
@classmethod
|
||||
def providesMfa(cls) -> bool:
|
||||
"""
|
||||
Returns if this authenticator provides a MFA identifier
|
||||
"""
|
||||
return cls.mfaIdentifier is not Authenticator.mfaIdentifier
|
||||
|
||||
def authenticate(
|
||||
self, username: str, credentials: str, groupsManager: 'GroupsManager'
|
||||
) -> bool:
|
||||
|
@@ -37,27 +37,45 @@ class AuthenticatorException(Exception):
|
||||
Generic authentication exception
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidUserException(AuthenticatorException):
|
||||
"""
|
||||
Invalid user specified. The user cant access the requested service
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAuthenticatorException(AuthenticatorException):
|
||||
"""
|
||||
Invalida authenticator has been specified
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class Redirect(Exception):
|
||||
|
||||
class Redirect(AuthenticatorException):
|
||||
"""
|
||||
This exception indicates that a redirect is required.
|
||||
Used in authUrlCallback to indicate that redirect is needed
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class Logout(Exception):
|
||||
|
||||
class Logout(AuthenticatorException):
|
||||
"""
|
||||
This exceptions redirects logouts an user and redirects to an url
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MFAError(AuthenticatorException):
|
||||
"""
|
||||
This exceptions indicates than an MFA error has ocurred
|
||||
"""
|
||||
|
||||
pass
|
||||
|
@@ -30,6 +30,7 @@
|
||||
"""
|
||||
@author: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
import typing
|
||||
import logging
|
||||
|
||||
from uds.core.environment import Environmentable, Environment
|
||||
@@ -43,11 +44,11 @@ class DelayedTask(Environmentable):
|
||||
This is an object that represents an execution to be done "later"
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, environment: typing.Optional[Environment] = None) -> None:
|
||||
"""
|
||||
Remember to invoke parent init in derived clases using super(myClass,self).__init__() to let this initialize its own variables
|
||||
"""
|
||||
super().__init__(Environment('DelayedTask'))
|
||||
super().__init__(environment or Environment.getEnvForType(self.__class__))
|
||||
|
||||
def execute(self) -> None:
|
||||
"""
|
||||
|
@@ -139,6 +139,7 @@ class DelayedTaskRunner:
|
||||
|
||||
if taskInstance:
|
||||
logger.debug('Executing delayedTask:>%s<', task)
|
||||
# Re-create environment data
|
||||
taskInstance.env = Environment.getEnvForType(taskInstance.__class__)
|
||||
DelayedTaskThread(taskInstance).start()
|
||||
|
||||
@@ -146,7 +147,13 @@ class DelayedTaskRunner:
|
||||
now = getSqlDatetime()
|
||||
exec_time = now + timedelta(seconds=delay)
|
||||
cls = instance.__class__
|
||||
|
||||
# Save "env" from delayed task, set it to None and restore it after save
|
||||
env = instance.env
|
||||
instance.env = None # type: ignore # clean env before saving pickle, save space (the env will be created again when executing)
|
||||
instanceDump = codecs.encode(pickle.dumps(instance), 'base64').decode()
|
||||
instance.env = env
|
||||
|
||||
typeName = str(cls.__module__ + '.' + cls.__name__)
|
||||
|
||||
logger.debug(
|
||||
|
@@ -33,11 +33,14 @@ import hashlib
|
||||
import array
|
||||
import uuid
|
||||
import codecs
|
||||
import datetime
|
||||
import struct
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import logging
|
||||
import typing
|
||||
import secrets
|
||||
import time
|
||||
|
||||
|
||||
from cryptography import x509
|
||||
@@ -132,9 +135,7 @@ class CryptoManager(metaclass=singleton.Singleton):
|
||||
modes.CBC(b'udsinitvectoruds'),
|
||||
backend=default_backend(),
|
||||
)
|
||||
rndStr = self.randomString(
|
||||
16
|
||||
).encode() # Same as block size of CBC (that is 16 here)
|
||||
rndStr = secrets.token_bytes(16) # Same as block size of CBC (that is 16 here)
|
||||
paddedLength = ((len(text) + 4 + 15) // 16) * 16
|
||||
toEncode = (
|
||||
struct.pack('>i', len(text)) + text + rndStr[: paddedLength - len(text) - 4]
|
||||
@@ -143,7 +144,7 @@ class CryptoManager(metaclass=singleton.Singleton):
|
||||
encoded = encryptor.update(toEncode) + encryptor.finalize()
|
||||
|
||||
if base64:
|
||||
return codecs.encode(encoded, 'base64') # Return as binary
|
||||
encoded = codecs.encode(encoded, 'base64').strip() # Return as bytes
|
||||
|
||||
return encoded
|
||||
|
||||
@@ -161,7 +162,9 @@ class CryptoManager(metaclass=singleton.Singleton):
|
||||
toDecode = decryptor.update(text) + decryptor.finalize()
|
||||
return toDecode[4 : 4 + struct.unpack('>i', toDecode[:4])[0]]
|
||||
|
||||
def xor(self, value: typing.Union[str, bytes], key: typing.Union[str, bytes]) -> bytes:
|
||||
def xor(
|
||||
self, value: typing.Union[str, bytes], key: typing.Union[str, bytes]
|
||||
) -> bytes:
|
||||
if not key:
|
||||
return b'' # Protect against division by cero
|
||||
|
||||
@@ -171,9 +174,13 @@ class CryptoManager(metaclass=singleton.Singleton):
|
||||
key = key.encode('utf-8')
|
||||
mult = len(value) // len(key) + 1
|
||||
value_array = array.array('B', value)
|
||||
key_array = array.array('B', key * mult) # Ensure key array is at least as long as value_array
|
||||
key_array = array.array(
|
||||
'B', key * mult
|
||||
) # Ensure key array is at least as long as value_array
|
||||
# We must return binary in xor, because result is in fact binary
|
||||
return array.array('B', (value_array[i] ^ key_array[i] for i in range(len(value_array)))).tobytes()
|
||||
return array.array(
|
||||
'B', (value_array[i] ^ key_array[i] for i in range(len(value_array)))
|
||||
).tobytes()
|
||||
|
||||
def symCrypt(
|
||||
self, text: typing.Union[str, bytes], key: typing.Union[str, bytes]
|
||||
@@ -227,20 +234,30 @@ class CryptoManager(metaclass=singleton.Singleton):
|
||||
raise Exception('Invalid certificate')
|
||||
|
||||
def certificateString(self, certificate: str) -> str:
|
||||
return (
|
||||
certificate.replace('-----BEGIN CERTIFICATE-----', '')
|
||||
.replace('-----END CERTIFICATE-----', '')
|
||||
.replace('\n', '')
|
||||
)
|
||||
# Remove -----.*-----\n strings using regex
|
||||
return re.sub(r'(-----.*-----\n)', '', certificate)
|
||||
|
||||
def secret(self, length: int = 16) -> str:
|
||||
"""
|
||||
Get a random secret string from config.SECRET_KEY
|
||||
"""
|
||||
from django.conf import settings
|
||||
return settings.SECRET_KEY[:length]
|
||||
|
||||
def salt(self, length: int = 16) -> str:
|
||||
"""
|
||||
Get a random salt random string
|
||||
"""
|
||||
return secrets.token_hex(length)
|
||||
|
||||
def hash(self, value: typing.Union[str, bytes]) -> str:
|
||||
if isinstance(value, str):
|
||||
value = value.encode()
|
||||
|
||||
if not value:
|
||||
return ''
|
||||
salt = self.salt(8) # 8 bytes = 16 chars
|
||||
value = salt.encode() + value
|
||||
|
||||
return '{SHA256}' + str(hashlib.sha3_256(value).hexdigest())
|
||||
return '{SHA256SALT}' + salt + str(hashlib.sha3_256(value).hexdigest())
|
||||
|
||||
def checkHash(self, value: typing.Union[str, bytes], hash: str) -> bool:
|
||||
if isinstance(value, str):
|
||||
@@ -250,9 +267,14 @@ class CryptoManager(metaclass=singleton.Singleton):
|
||||
return not hash
|
||||
|
||||
if hash[:8] == '{SHA256}':
|
||||
return str(hashlib.sha3_256(value).hexdigest()) == hash[8:]
|
||||
return secrets.compare_digest(hashlib.sha3_256(value).hexdigest(), hash[8:])
|
||||
elif hash[:12] == '{SHA256SALT}':
|
||||
# Extract 16 chars salt and hash
|
||||
salt = hash[12:28].encode()
|
||||
value = salt + value
|
||||
return secrets.compare_digest(hashlib.sha3_256(value).hexdigest(), hash[28:])
|
||||
else: # Old sha1
|
||||
return hash == str(hashlib.sha1(value).hexdigest())
|
||||
return secrets.compare_digest(hash, str(hashlib.sha1(value).hexdigest())) # nosec: Old compatibility SHA1, not used anymore but need to be supported
|
||||
|
||||
def uuid(self, obj: typing.Any = None) -> str:
|
||||
"""
|
||||
@@ -273,4 +295,12 @@ class CryptoManager(metaclass=singleton.Singleton):
|
||||
|
||||
def randomString(self, length: int = 40, digits: bool = True) -> str:
|
||||
base = string.ascii_letters + (string.digits if digits else '')
|
||||
return ''.join(random.SystemRandom().choices(base, k=length))
|
||||
return ''.join(secrets.choice(base) for _ in range(length))
|
||||
|
||||
def unique(self) -> str:
|
||||
return hashlib.sha3_256(
|
||||
(
|
||||
self.randomString(24, True)
|
||||
+ datetime.datetime.now().strftime('%H%M%S%f')
|
||||
).encode()
|
||||
).hexdigest()
|
||||
|
@@ -60,26 +60,25 @@ class PublicationOldMachinesCleaner(DelayedTask):
|
||||
This delayed task is for removing a pending "removable" publication
|
||||
"""
|
||||
|
||||
def __init__(self, publicationId: int):
|
||||
def __init__(self, publicationId: int) -> None:
|
||||
super().__init__()
|
||||
self._id = publicationId
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
try:
|
||||
servicePoolPub: ServicePoolPublication = ServicePoolPublication.objects.get(
|
||||
pk=self._id
|
||||
)
|
||||
servicePoolPub: ServicePoolPublication = ServicePoolPublication.objects.get(pk=self._id)
|
||||
if servicePoolPub.state != State.REMOVABLE:
|
||||
logger.info('Already removed')
|
||||
|
||||
now = getSqlDatetime()
|
||||
activePub: typing.Optional[
|
||||
ServicePoolPublication
|
||||
] = servicePoolPub.deployed_service.activePublication()
|
||||
servicePoolPub.deployed_service.userServices.filter(in_use=True).update(
|
||||
in_use=False, state_date=now
|
||||
activePub: typing.Optional[ServicePoolPublication] = (
|
||||
servicePoolPub.deployed_service.activePublication()
|
||||
)
|
||||
servicePoolPub.deployed_service.markOldUserServicesAsRemovables(activePub)
|
||||
if activePub:
|
||||
servicePoolPub.deployed_service.userServices.filter(in_use=True).exclude(
|
||||
publication=activePub
|
||||
).update(in_use=False, state_date=now)
|
||||
servicePoolPub.deployed_service.markOldUserServicesAsRemovables(activePub)
|
||||
except Exception:
|
||||
pass
|
||||
# Removed publication, no problem at all, just continue
|
||||
@@ -100,9 +99,7 @@ class PublicationLauncher(DelayedTask):
|
||||
try:
|
||||
now = getSqlDatetime()
|
||||
with transaction.atomic():
|
||||
servicePoolPub = ServicePoolPublication.objects.select_for_update().get(
|
||||
pk=self._publicationId
|
||||
)
|
||||
servicePoolPub = ServicePoolPublication.objects.select_for_update().get(pk=self._publicationId)
|
||||
if (
|
||||
servicePoolPub.state != State.LAUNCHING
|
||||
): # If not preparing (may has been canceled by user) just return
|
||||
@@ -115,16 +112,13 @@ class PublicationLauncher(DelayedTask):
|
||||
servicePool.current_pub_revision += 1
|
||||
servicePool.storeValue(
|
||||
'toBeReplacedIn',
|
||||
pickle.dumps(
|
||||
now
|
||||
+ datetime.timedelta(
|
||||
hours=GlobalConfig.SESSION_EXPIRE_TIME.getInt(True)
|
||||
)
|
||||
),
|
||||
pickle.dumps(now + datetime.timedelta(hours=GlobalConfig.SESSION_EXPIRE_TIME.getInt(True))),
|
||||
)
|
||||
servicePool.save()
|
||||
PublicationFinishChecker.checkAndUpdateState(servicePoolPub, pi, state)
|
||||
except ServicePoolPublication.DoesNotExist: # Deployed service publication has been removed from database, this is ok, just ignore it
|
||||
except (
|
||||
ServicePoolPublication.DoesNotExist
|
||||
): # Deployed service publication has been removed from database, this is ok, just ignore it
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception("Exception launching publication")
|
||||
@@ -164,18 +158,12 @@ class PublicationFinishChecker(DelayedTask):
|
||||
# Now we mark, if it exists, the previous usable publication as "Removable"
|
||||
if State.isPreparing(prevState):
|
||||
old: ServicePoolPublication
|
||||
for old in publication.deployed_service.publications.filter(
|
||||
state=State.USABLE
|
||||
):
|
||||
for old in publication.deployed_service.publications.filter(state=State.USABLE):
|
||||
old.setState(State.REMOVABLE)
|
||||
|
||||
osm = publication.deployed_service.osmanager
|
||||
# If os manager says "machine is persistent", do not tray to delete "previous version" assigned machines
|
||||
doPublicationCleanup = (
|
||||
True
|
||||
if osm is None
|
||||
else not osm.getInstance().isPersistent()
|
||||
)
|
||||
doPublicationCleanup = True if osm is None else not osm.getInstance().isPersistent()
|
||||
|
||||
if doPublicationCleanup:
|
||||
pc = PublicationOldMachinesCleaner(old.id)
|
||||
@@ -184,13 +172,9 @@ class PublicationFinishChecker(DelayedTask):
|
||||
'pclean-' + str(old.id),
|
||||
True,
|
||||
)
|
||||
publication.deployed_service.markOldUserServicesAsRemovables(
|
||||
publication
|
||||
)
|
||||
publication.deployed_service.markOldUserServicesAsRemovables(publication)
|
||||
else: # Remove only cache services, not assigned
|
||||
publication.deployed_service.markOldUserServicesAsRemovables(
|
||||
publication, True
|
||||
)
|
||||
publication.deployed_service.markOldUserServicesAsRemovables(publication, True)
|
||||
|
||||
publication.setState(State.USABLE)
|
||||
elif State.isRemoving(prevState):
|
||||
@@ -215,9 +199,7 @@ class PublicationFinishChecker(DelayedTask):
|
||||
PublicationFinishChecker.checkLater(publication, publicationInstance)
|
||||
|
||||
@staticmethod
|
||||
def checkLater(
|
||||
publication: ServicePoolPublication, publicationInstance: 'services.Publication'
|
||||
):
|
||||
def checkLater(publication: ServicePoolPublication, publicationInstance: 'services.Publication'):
|
||||
"""
|
||||
Inserts a task in the delayedTaskRunner so we can check the state of this publication
|
||||
@param dps: Database object for ServicePoolPublication
|
||||
@@ -232,23 +214,17 @@ class PublicationFinishChecker(DelayedTask):
|
||||
def run(self):
|
||||
logger.debug('Checking publication finished %s', self._publishId)
|
||||
try:
|
||||
publication: ServicePoolPublication = ServicePoolPublication.objects.get(
|
||||
pk=self._publishId
|
||||
)
|
||||
publication: ServicePoolPublication = ServicePoolPublication.objects.get(pk=self._publishId)
|
||||
if publication.state != self._state:
|
||||
logger.debug('Task overrided by another task (state of item changed)')
|
||||
else:
|
||||
publicationInstance = publication.getInstance()
|
||||
logger.debug(
|
||||
"publication instance class: %s", publicationInstance.__class__
|
||||
)
|
||||
logger.debug("publication instance class: %s", publicationInstance.__class__)
|
||||
try:
|
||||
state = publicationInstance.checkState()
|
||||
except Exception:
|
||||
state = State.ERROR
|
||||
PublicationFinishChecker.checkAndUpdateState(
|
||||
publication, publicationInstance, state
|
||||
)
|
||||
PublicationFinishChecker.checkAndUpdateState(publication, publicationInstance, state)
|
||||
except Exception as e:
|
||||
logger.debug(
|
||||
'Deployed service not found (erased from database) %s : %s',
|
||||
@@ -270,9 +246,7 @@ class PublicationManager(metaclass=singleton.Singleton):
|
||||
"""
|
||||
Returns the singleton to this manager
|
||||
"""
|
||||
return (
|
||||
PublicationManager()
|
||||
) # Singleton pattern will return always the same instance
|
||||
return PublicationManager() # Singleton pattern will return always the same instance
|
||||
|
||||
def publish(
|
||||
self, servicePool: ServicePool, changeLog: typing.Optional[str] = None
|
||||
@@ -284,15 +258,11 @@ class PublicationManager(metaclass=singleton.Singleton):
|
||||
"""
|
||||
if servicePool.publications.filter(state__in=State.PUBLISH_STATES).count() > 0:
|
||||
raise PublishException(
|
||||
_(
|
||||
'Already publishing. Wait for previous publication to finish and try again'
|
||||
)
|
||||
_('Already publishing. Wait for previous publication to finish and try again')
|
||||
)
|
||||
|
||||
if servicePool.isInMaintenance():
|
||||
raise PublishException(
|
||||
_('Service is in maintenance mode and new publications are not allowed')
|
||||
)
|
||||
raise PublishException(_('Service is in maintenance mode and new publications are not allowed'))
|
||||
|
||||
publication: typing.Optional[ServicePoolPublication] = None
|
||||
try:
|
||||
@@ -320,17 +290,13 @@ class PublicationManager(metaclass=singleton.Singleton):
|
||||
logger.info('Could not delete %s', publication)
|
||||
raise PublishException(str(e))
|
||||
|
||||
def cancel(
|
||||
self, publication: ServicePoolPublication
|
||||
): # pylint: disable=no-self-use
|
||||
def cancel(self, publication: ServicePoolPublication): # pylint: disable=no-self-use
|
||||
"""
|
||||
Invoked to cancel a publication.
|
||||
Double invokation (i.e. invokation over a "cancelling" item) will lead to a "forced" cancellation (unclean)
|
||||
:param servicePoolPub: Service pool publication (db object for a publication)
|
||||
"""
|
||||
publication = ServicePoolPublication.objects.get(
|
||||
pk=publication.id
|
||||
) # Reloads publication from db
|
||||
publication = ServicePoolPublication.objects.get(pk=publication.id) # Reloads publication from db
|
||||
if publication.state not in State.PUBLISH_STATES:
|
||||
if publication.state == State.CANCELING: # Double cancel
|
||||
logger.info('Double cancel invoked for a publication')
|
||||
@@ -355,35 +321,24 @@ class PublicationManager(metaclass=singleton.Singleton):
|
||||
pubInstance = publication.getInstance()
|
||||
state = pubInstance.cancel()
|
||||
publication.setState(State.CANCELING)
|
||||
PublicationFinishChecker.checkAndUpdateState(
|
||||
publication, pubInstance, state
|
||||
)
|
||||
PublicationFinishChecker.checkAndUpdateState(publication, pubInstance, state)
|
||||
return publication
|
||||
except Exception as e:
|
||||
raise PublishException(str(e))
|
||||
|
||||
def unpublish(
|
||||
self, servicePoolPub: ServicePoolPublication
|
||||
): # pylint: disable=no-self-use
|
||||
def unpublish(self, servicePoolPub: ServicePoolPublication): # pylint: disable=no-self-use
|
||||
"""
|
||||
Unpublishes an active (usable) or removable publication
|
||||
:param servicePoolPub: Publication to unpublish
|
||||
"""
|
||||
if (
|
||||
State.isUsable(servicePoolPub.state) is False
|
||||
and State.isRemovable(servicePoolPub.state) is False
|
||||
):
|
||||
if State.isUsable(servicePoolPub.state) is False and State.isRemovable(servicePoolPub.state) is False:
|
||||
raise PublishException(_('Can\'t unpublish non usable publication'))
|
||||
if servicePoolPub.userServices.exclude(state__in=State.INFO_STATES).count() > 0:
|
||||
raise PublishException(
|
||||
_('Can\'t unpublish publications with services in process')
|
||||
)
|
||||
raise PublishException(_('Can\'t unpublish publications with services in process'))
|
||||
try:
|
||||
pubInstance = servicePoolPub.getInstance()
|
||||
state = pubInstance.destroy()
|
||||
servicePoolPub.setState(State.REMOVING)
|
||||
PublicationFinishChecker.checkAndUpdateState(
|
||||
servicePoolPub, pubInstance, state
|
||||
)
|
||||
PublicationFinishChecker.checkAndUpdateState(servicePoolPub, pubInstance, state)
|
||||
except Exception as e:
|
||||
raise PublishException(str(e))
|
||||
|
@@ -36,7 +36,7 @@ import typing
|
||||
|
||||
from uds.core.util.config import GlobalConfig
|
||||
from uds.core.util import singleton
|
||||
from uds.models import StatsCounters
|
||||
from uds.models import StatsCounters, StatsCountersAccum
|
||||
from uds.models import getSqlDatetime, getSqlDatetimeAsUnix
|
||||
from uds.models import StatsEvents
|
||||
|
||||
@@ -73,7 +73,7 @@ class StatsManager(metaclass=singleton.Singleton):
|
||||
def manager() -> 'StatsManager':
|
||||
return StatsManager() # Singleton pattern will return always the same instance
|
||||
|
||||
def __doCleanup(self, model):
|
||||
def __doCleanup(self, model: typing.Type[typing.Union['StatsCounters', 'StatsEvents']]) -> None:
|
||||
minTime = time.mktime(
|
||||
(
|
||||
getSqlDatetime()
|
||||
@@ -284,3 +284,7 @@ class StatsManager(metaclass=singleton.Singleton):
|
||||
"""
|
||||
|
||||
self.__doCleanup(StatsEvents)
|
||||
|
||||
def acummulate(self, max_days: int = 7):
|
||||
for interval in StatsCountersAccum.IntervalType:
|
||||
StatsCountersAccum.acummulate(interval, max_days)
|
||||
|
@@ -39,6 +39,7 @@ from django.db.models import Q
|
||||
from django.db import transaction
|
||||
from uds.core.services.exceptions import OperationException
|
||||
from uds.core.util.config import GlobalConfig
|
||||
from uds.core.util.decorators import allowCache
|
||||
from uds.core.util.state import State
|
||||
from uds.core.util import log
|
||||
from uds.core.services.exceptions import (
|
||||
@@ -66,6 +67,9 @@ from uds.web.util.errors import MAX_SERVICES_REACHED
|
||||
from .userservice import comms
|
||||
from .userservice.opchecker import UserServiceOpChecker
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from uds import models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
traceLogger = logging.getLogger('traceLog')
|
||||
|
||||
@@ -80,14 +84,12 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
UserServiceManager()
|
||||
) # Singleton pattern will return always the same instance
|
||||
|
||||
@staticmethod
|
||||
def getCacheStateFilter(servicePool: ServicePool, level: int) -> Q:
|
||||
return Q(cache_level=level) & UserServiceManager.getStateFilter(servicePool)
|
||||
def getCacheStateFilter(self, servicePool: ServicePool, level: int) -> Q:
|
||||
return Q(cache_level=level) & self.getStateFilter(servicePool.service)
|
||||
|
||||
@staticmethod
|
||||
def getStateFilter(servicePool: ServicePool) -> Q:
|
||||
def getStateFilter(self, service: 'models.Service') -> Q:
|
||||
if (
|
||||
servicePool.service.getInstance().maxDeployed == services.Service.UNLIMITED
|
||||
service.getInstance().maxDeployed == services.Service.UNLIMITED
|
||||
and GlobalConfig.MAX_SERVICES_COUNT_NEW.getBool() is False
|
||||
):
|
||||
states = [State.PREPARING, State.USABLE]
|
||||
@@ -95,23 +97,38 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
states = [State.PREPARING, State.USABLE, State.REMOVING, State.REMOVABLE]
|
||||
return Q(state__in=states)
|
||||
|
||||
def getExistingUserServices(self, service: 'models.Service') -> int:
|
||||
"""
|
||||
Returns the number of running user services for this service
|
||||
"""
|
||||
return UserService.objects.filter(
|
||||
self.getStateFilter(service) & Q(deployed_service__service=service)
|
||||
).count()
|
||||
|
||||
def maximumUserServicesDeployed(self, service: 'models.Service') -> bool:
|
||||
"""
|
||||
Checks if the maximum number of user services for this service has been reached
|
||||
"""
|
||||
serviceInstance = service.getInstance()
|
||||
# Early return, so no database count is needed
|
||||
if serviceInstance.maxDeployed == services.Service.UNLIMITED:
|
||||
return False
|
||||
|
||||
if self.getExistingUserServices(service) >= serviceInstance.maxDeployed:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def __checkMaxDeployedReached(self, servicePool: ServicePool) -> None:
|
||||
"""
|
||||
Checks if maxDeployed for the service has been reached, and, if so,
|
||||
raises an exception that no more services of this kind can be reached
|
||||
"""
|
||||
serviceInstance = servicePool.service.getInstance()
|
||||
# Early return, so no database count is needed
|
||||
if serviceInstance.maxDeployed == services.Service.UNLIMITED:
|
||||
return
|
||||
|
||||
numberOfServices = servicePool.userServices.filter(
|
||||
state__in=[State.PREPARING, State.USABLE]
|
||||
).count()
|
||||
|
||||
if serviceInstance.maxDeployed <= numberOfServices:
|
||||
if self.maximumUserServicesDeployed(servicePool.service):
|
||||
raise MaxServicesReachedError(
|
||||
'Max number of allowed deployments for service reached'
|
||||
_('Maximum number of user services reached for this {}').format(
|
||||
servicePool
|
||||
)
|
||||
)
|
||||
|
||||
def __createCacheAtDb(
|
||||
@@ -183,7 +200,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
"""
|
||||
Creates a new cache for the deployed service publication at level indicated
|
||||
"""
|
||||
logger.debug(
|
||||
logger.info(
|
||||
'Creating a new cache element at level %s for publication %s',
|
||||
cacheLevel,
|
||||
publication,
|
||||
@@ -209,7 +226,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
|
||||
if servicePool.service.getType().publicationType is not None:
|
||||
publication = servicePool.activePublication()
|
||||
logger.debug(
|
||||
logger.info(
|
||||
'Creating a new assigned element for user %s por publication %s',
|
||||
user,
|
||||
publication,
|
||||
@@ -223,7 +240,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
)
|
||||
)
|
||||
else:
|
||||
logger.debug('Creating a new assigned element for user %s', user)
|
||||
logger.info('Creating a new assigned element for user %s', user)
|
||||
assigned = self.__createAssignedAtDbForNoPublication(servicePool, user)
|
||||
|
||||
assignedInstance = assigned.getInstance()
|
||||
@@ -307,7 +324,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
@return: the Uservice canceling
|
||||
"""
|
||||
userService.refresh_from_db()
|
||||
logger.debug('Canceling userService %s creation', userService)
|
||||
logger.info('Canceling userService %s creation', userService)
|
||||
|
||||
if userService.isPreparing() is False:
|
||||
logger.info(
|
||||
@@ -340,7 +357,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
"""
|
||||
with transaction.atomic():
|
||||
userService = UserService.objects.select_for_update().get(id=userService.id)
|
||||
logger.debug('Removing userService %a', userService)
|
||||
logger.info('Removing userService %a', userService)
|
||||
if (
|
||||
userService.isUsable() is False
|
||||
and State.isRemovable(userService.state) is False
|
||||
@@ -528,7 +545,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
if serviceType.usesCache:
|
||||
inAssigned = (
|
||||
servicePool.assignedUserServices()
|
||||
.filter(UserServiceManager.getStateFilter(servicePool))
|
||||
.filter(self.getStateFilter(servicePool.service))
|
||||
.count()
|
||||
)
|
||||
if (
|
||||
@@ -546,26 +563,31 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
events.addEvent(servicePool, events.ET_CACHE_MISS, fld1=0)
|
||||
return self.createAssignedFor(servicePool, user)
|
||||
|
||||
def getServicesInStateForProvider(self, provider_id: int, state: str) -> int:
|
||||
def getUserServicesInStatesForProvider(
|
||||
self, provider: 'models.Provider', states: typing.List[str]
|
||||
) -> int:
|
||||
"""
|
||||
Returns the number of services of a service provider in the state indicated
|
||||
"""
|
||||
return UserService.objects.filter(
|
||||
deployed_service__service__provider__id=provider_id, state=state
|
||||
deployed_service__service__provider=provider, state__in=states
|
||||
).count()
|
||||
|
||||
@allowCache(cachePrefix='max_svrs', cacheTimeout=30, cachingArgs=(1,), cachingKWArgs=('servicePool'))
|
||||
def canRemoveServiceFromDeployedService(self, servicePool: ServicePool) -> bool:
|
||||
"""
|
||||
checks if we can do a "remove" from a deployed service
|
||||
serviceIsntance is just a helper, so if we already have unserialized deployedService
|
||||
"""
|
||||
removing = self.getServicesInStateForProvider(
|
||||
servicePool.service.provider.id, State.REMOVING
|
||||
removing = self.getUserServicesInStatesForProvider(
|
||||
servicePool.service.provider, [State.REMOVING]
|
||||
)
|
||||
serviceInstance = servicePool.service.getInstance()
|
||||
if (
|
||||
removing >= serviceInstance.parent().getMaxRemovingServices()
|
||||
and serviceInstance.parent().getIgnoreLimits() is False
|
||||
(removing >= serviceInstance.parent().getMaxRemovingServices()
|
||||
and serviceInstance.parent().getIgnoreLimits() is False)
|
||||
or servicePool.service.provider.isInMaintenance()
|
||||
or servicePool.isRestrained()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
@@ -574,12 +596,12 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
"""
|
||||
Checks if we can start a new service
|
||||
"""
|
||||
preparing = self.getServicesInStateForProvider(
|
||||
servicePool.service.provider.id, State.PREPARING
|
||||
preparingForProvider = self.getUserServicesInStatesForProvider(
|
||||
servicePool.service.provider, [State.PREPARING]
|
||||
)
|
||||
serviceInstance = servicePool.service.getInstance()
|
||||
if (
|
||||
preparing >= serviceInstance.parent().getMaxPreparingServices()
|
||||
if self.maximumUserServicesDeployed(servicePool.service) or (
|
||||
preparingForProvider >= serviceInstance.parent().getMaxPreparingServices()
|
||||
and serviceInstance.parent().getIgnoreLimits() is False
|
||||
):
|
||||
return False
|
||||
@@ -760,7 +782,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
Get service info from user service
|
||||
"""
|
||||
if idService[0] == 'M': # Meta pool
|
||||
return self.getMeta(user, srcIp, os, idService[1:], idTransport or '')
|
||||
return self.getMeta(user, srcIp, os, idService[1:], idTransport or 'meta')
|
||||
|
||||
userService = self.locateUserService(user, idService, create=True)
|
||||
|
||||
@@ -918,7 +940,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
return metaId[0] == 'M'
|
||||
|
||||
def locateMetaService(
|
||||
self, user: User, idService: str, create: bool = False
|
||||
self, user: User, idService: str
|
||||
) -> typing.Optional[UserService]:
|
||||
kind, uuidMetapool = idService[0], idService[1:]
|
||||
if kind != 'M':
|
||||
@@ -968,7 +990,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
|
||||
# Get pool members. Just pools "visible" and "usable"
|
||||
poolMembers = [
|
||||
p for p in meta.members.all() if p.pool.isVisible() and p.pool.isUsable()
|
||||
p for p in meta.members.filter(enabled=True) if p.pool.isVisible() and p.pool.isUsable()
|
||||
]
|
||||
# Sort pools based on meta selection
|
||||
if meta.policy == MetaPool.PRIORITY_POOL:
|
||||
@@ -985,10 +1007,10 @@ class UserServiceManager(metaclass=singleton.Singleton):
|
||||
# Remove "full" pools (100%) from result and pools in maintenance mode, not ready pools, etc...
|
||||
sortedPools = sorted(sortPools, key=lambda x: x[0])
|
||||
pools: typing.List[ServicePool] = [
|
||||
p[1] for p in sortedPools if p[1].usage() < 100 and p[1].isUsable()
|
||||
p[1] for p in sortedPools if p[1].usage()[0] < 100 and p[1].isUsable()
|
||||
]
|
||||
poolsFull: typing.List[ServicePool] = [
|
||||
p[1] for p in sortedPools if p[1].usage() == 100 and p[1].isUsable()
|
||||
p[1] for p in sortedPools if p[1].usage()[0] == 100 and p[1].isUsable()
|
||||
]
|
||||
|
||||
logger.debug('Pools: %s/%s', pools, poolsFull)
|
||||
|
@@ -35,7 +35,7 @@ import tempfile
|
||||
import logging
|
||||
import typing
|
||||
|
||||
import requests
|
||||
from uds.core.util.security import secureRequestsSession
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from uds.models import UserService
|
||||
@@ -90,8 +90,8 @@ def _requestActor(
|
||||
r = proxy.doProxyRequest(url=url, data=data, timeout=TIMEOUT)
|
||||
else:
|
||||
verify: typing.Union[bool, str]
|
||||
cert = userService.getProperty('cert')
|
||||
# cert = '' # Untils more tests, keep as previous.... TODO: Fix this when fully tested
|
||||
cert = userService.getProperty('cert') or ''
|
||||
# cert = '' # Uncomment to test without cert
|
||||
if cert:
|
||||
# Generate temp file, and delete it after
|
||||
verify = tempfile.mktemp('udscrt')
|
||||
@@ -99,10 +99,11 @@ def _requestActor(
|
||||
f.write(cert.encode()) # Save cert
|
||||
else:
|
||||
verify = False
|
||||
session = secureRequestsSession(verify=cert)
|
||||
if data is None:
|
||||
r = requests.get(url, verify=verify, timeout=TIMEOUT)
|
||||
r = session.get(url, verify=verify, timeout=TIMEOUT)
|
||||
else:
|
||||
r = requests.post(
|
||||
r = session.post(
|
||||
url,
|
||||
data=json.dumps(data),
|
||||
headers={'content-type': 'application/json'},
|
||||
|
@@ -164,7 +164,8 @@ class UpdateFromPreparing(StateUpdater):
|
||||
state = (
|
||||
State.REMOVABLE
|
||||
) # By default, if not valid publication, service will be marked for removal on preparation finished
|
||||
if self.userService.isValidPublication():
|
||||
osManager = self.userServiceInstance.osmanager()
|
||||
if self.userService.isValidPublication() or (osManager and osManager.isPersistent()):
|
||||
logger.debug('Publication is valid for %s', self.userService.friendly_name)
|
||||
state = self.checkOsManagerRelated()
|
||||
|
||||
|
44
server/src/uds/core/mfas/__init__.py
Normal file
44
server/src/uds/core/mfas/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- 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. 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.
|
||||
|
||||
"""
|
||||
UDS os managers related interfaces and classes
|
||||
|
||||
.. moduleauthor:: Adolfo Gómez, dkmaster at dkmon dot com
|
||||
"""
|
||||
from .mfa import MFA
|
||||
|
||||
|
||||
def factory():
|
||||
"""
|
||||
Returns factory for register/access to authenticators
|
||||
"""
|
||||
from .mfafactory import MFAsFactory
|
||||
|
||||
return MFAsFactory.factory()
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user