1
0
mirror of https://github.com/dkmstr/openuds.git synced 2025-10-06 11:33:43 +03:00

9 Commits
v3.6 ... v3.5

Author SHA1 Message Date
Adolfo Gómez García
e7089d9d86 Reverted typo on macos script and updated signatures 2023-04-13 13:23:34 +02:00
Adolfo Gómez García
7a367d9011 Fixed Spice tunneled connections 2023-04-13 13:20:52 +02:00
Adolfo Gómez García
089b8c82dc Updated tunnel scripts for spice and signatures 2023-04-13 12:48:02 +02:00
Adolfo Gómez García
df82c3854c minor fixes for 3.5 2023-03-30 01:20:50 +02:00
Adolfo Gómez García
f158235f16 fixes to allow rgeneration of clients 2023-03-08 13:28:40 +01:00
Adolfo Gómez García
fe704c0ba6 fixed Version to 3.5.1 to force download of UDS client 2023-03-08 11:09:01 +01:00
Adolfo Gómez García
8d635b781b regenerated 2023-03-08 10:57:47 +01:00
Adolfo Gómez García
00c48dff69 security update for UDS client 2023-03-08 10:09:40 +01:00
Adolfo Gómez García
01269e6d3e Ip Detection fix 2023-02-07 17:02:18 +01:00
311 changed files with 24855 additions and 45407 deletions

View File

@@ -12,4 +12,4 @@ This is an Open Source Source project, initiated by Spanish Company Virtualca
Any help provided will be welcome.
**Note: This is a previous release, being superseded by v4.0. Try to use actual Stable.**
**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.**

View File

@@ -1 +1 @@
3.6.0
3.5.0

4
actor/deps.txt Normal file
View File

@@ -0,0 +1,4 @@
Linux:
python3-prctl (recommended, but not required in fact)
python3-pyqt5

View File

@@ -11,9 +11,6 @@ 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,
@@ -25,7 +22,7 @@ cat udsactor-unmanaged-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

View File

@@ -1,9 +1,3 @@
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

View File

@@ -1,3 +1,3 @@
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
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

View File

@@ -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 xset
Requires: python3-six python3-requests python3-qt5 libXScrnSaver
Vendor: Virtual Cable S.L.U.
URL: http://www.udsenterprise.com
Provides: udsactor

View File

@@ -1,70 +0,0 @@
%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

View File

@@ -214,10 +214,10 @@
<item row="2" column="1">
<widget class="QLineEdit" name="serviceToken">
<property name="toolTip">
<string>UDS Service Token</string>
<string>UDS user with administration rights (Will not be stored on template)</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Token of the service on UDS platform&lt;/p&gt;&lt;p&gt;This token can be obtainend from the service configuration on UDS.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Administrator user on UDS Server.&lt;/p&gt;&lt;p&gt;Note: This credential will not be stored on client. Will be used to obtain an unique token for this image.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
@@ -268,10 +268,10 @@
<item row="3" column="1">
<widget class="QLineEdit" name="restrictNet">
<property name="toolTip">
<string>Restrict valid detection of network interfaces to this network.</string>
<string>UDS user with administration rights (Will not be stored on template)</string>
</property>
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Restrics valid detection of network interfaces.&lt;/p&gt;&lt;p&gt;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..&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Administrator user on UDS Server.&lt;/p&gt;&lt;p&gt;Note: This credential will not be stored on client. Will be used to obtain an unique token for this image.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>

View File

@@ -224,9 +224,6 @@
<property name="whatsThis">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Select the security for communications with UDS Broker.&lt;/p&gt;&lt;p&gt;The recommended method of communication is &lt;span style=&quot; font-weight:600;&quot;&gt;Use SSL&lt;/span&gt;, but selection needs to be acording to your broker configuration.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="currentIndex">
<number>1</number>
</property>
<item>
<property name="text">
<string>Ignore certificate</string>

View File

@@ -35,4 +35,4 @@ from . import platform
__title__ = 'udsactor'
__author__ = 'Adolfo Gómez <dkmaster@dkmon.com>'
__license__ = "BSD 3-clause"
__copyright__ = "Copyright 2014-2022 VirtualCable S.L.U."
__copyright__ = "Copyright 2014-2020 VirtualCable S.L.U."

View File

@@ -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.OpenModeFlag.WriteOnly)
buffer.open(QIODevice.WriteOnly)
pixmap.save(buffer, 'PNG')
buffer.close()
scrBase64 = bytes(ba.toBase64()).decode() # type: ignore # there are problems with Pylance and connects on PyQt5... :)

View File

@@ -1,10 +1,7 @@
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',
ciphers=''
password='Pw7qbatz5u-y-Z5ora2D2ZuBCm95AHnKRcpze53k8tw'
)

View File

@@ -37,9 +37,9 @@ import requests
from ..log import logger
# For avoid proxy on localhost connections
NO_PROXY: typing.Dict[str, str] = {
'http': '',
'https': '',
NO_PROXY = {
'http': None,
'https': None,
}
class UDSActorClientPool:

View File

@@ -42,18 +42,11 @@ 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'
@@ -61,12 +54,7 @@ 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')
@@ -83,13 +71,11 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
# Very simple path & params splitter
path = self.path.split('?')[0][1:].split('/')
logger.debug('Path: %s, ip: %s, params: %s', path, self.client_address, params)
logger.debug('Path: %s, params: %s', path, 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':
@@ -102,18 +88,12 @@ 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
@@ -121,10 +101,7 @@ 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 = {}
@@ -136,9 +113,7 @@ 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
@@ -150,7 +125,6 @@ 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'
@@ -179,22 +153,13 @@ 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)
# 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.options = ssl.CERT_NONE
context.load_cert_chain(certfile=self._certFile, password=password)
self._server.socket = context.wrap_socket(self._server.socket, server_side=True)

View File

@@ -41,11 +41,10 @@ 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
@@ -107,7 +106,6 @@ 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:
@@ -139,19 +137,14 @@ def reboot(flags: int = 0):
'''
Simple reboot using os command
'''
try:
subprocess.call(['/sbin/shutdown', 'now', '-r'])
except Exception as e:
logger.error('Error rebooting: %s', e)
subprocess.call(['/sbin/shutdown', 'now', '-r'])
def loggoff() -> None:
'''
Right now restarts the machine...
'''
try:
subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']])
except Exception as e:
logger.error('Error killing user processes: %s', e)
subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']])
# subprocess.call(['/sbin/shutdown', 'now', '-r'])
# subprocess.call(['/usr/bin/systemctl', 'reboot', '-i'])

View File

@@ -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=True),
validateCertificate=uds.getboolean('validate', fallback=False),
master_token=uds.get('master_token', None),
own_token=uds.get('own_token', None),
restrict_net=uds.get('restrict_net', None),

View File

@@ -33,10 +33,6 @@ import ctypes
import ctypes.util
import subprocess
from udsactor.log import logger
xlib = None
xss = None
display = None
@@ -111,12 +107,9 @@ def _ensureInitialized():
def initIdleDuration(atLeastSeconds: int) -> None:
_ensureInitialized()
if atLeastSeconds:
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)
subprocess.call(['/usr/bin/xset', 's', '{}'.format(atLeastSeconds + 30)])
# And now reset it
subprocess.call(['/usr/bin/xset', 's', 'reset'])
def getIdleDuration() -> float:

View File

@@ -31,12 +31,10 @@
# 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
@@ -44,18 +42,6 @@ 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
@@ -96,7 +82,6 @@ NO_PROXY = {
UDS_BASE_URL = 'https://{}/uds/rest/'
#
# Basic UDS Api
#
@@ -108,7 +93,6 @@ 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
@@ -122,30 +106,6 @@ 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 {
@@ -165,13 +125,15 @@ class UDSApi: # pylint: disable=too-few-public-methods
) -> typing.Any:
headers = headers or self._headers
try:
result = self._session.post(
result = requests.post(
self._apiURL(method),
data=json.dumps(payLoad),
headers=headers,
# verify=self._validateCert, Not needed, already in session
verify=self._validateCert,
timeout=TIMEOUT,
proxies=NO_PROXY if disableProxy else None, # type: ignore # if not proxies wanted, enforce it
proxies=NO_PROXY # type: ignore
if disableProxy
else None, # if not proxies wanted, enforce it
)
if result.ok:
@@ -200,10 +162,10 @@ class UDSServerApi(UDSApi):
def enumerateAuthenticators(self) -> typing.Iterable[types.AuthenticatorType]:
try:
result = self._session.get(
result = requests.get(
self._url + 'auth/auths',
headers=self._headers,
# verify=self._validateCert,
verify=self._validateCert,
timeout=4,
)
if result.ok:
@@ -216,7 +178,7 @@ class UDSServerApi(UDSApi):
priority=v['priority'],
isCustom=v['isCustom'],
)
except Exception as e:
except Exception:
pass
def register( # pylint: disable=too-many-arguments, too-many-locals
@@ -251,22 +213,22 @@ class UDSServerApi(UDSApi):
# First, try to login
authInfo = {'auth': auth, 'username': username, 'password': password}
headers = self._headers
result = self._session.post(
result = requests.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 = self._session.post(
result = requests.post(
self._apiURL('register'),
data=json.dumps(data),
headers=headers,
# verify=self._validateCert,
verify=self._validateCert,
)
if result.ok:
return result.json()['result']
@@ -310,7 +272,9 @@ 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)
@@ -318,10 +282,11 @@ 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)
@@ -329,7 +294,6 @@ class UDSServerApi(UDSApi):
private_key=result['private_key'],
server_certificate=result['server_certificate'],
password=result['password'],
ciphers=result.get('ciphers', ''),
)
def notifyUnmanagedCallback(
@@ -351,7 +315,6 @@ class UDSServerApi(UDSApi):
private_key=result['private_key'],
server_certificate=result['server_certificate'],
password=result['password'],
ciphers=result.get('ciphers', ''),
)
def login(
@@ -364,7 +327,9 @@ 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],
@@ -440,7 +405,9 @@ 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,
@@ -453,8 +420,11 @@ class UDSClientApi(UDSApi):
max_idle=result['max_idle'],
)
def logout(self, username: str, sessionType: typing.Optional[str] = None) -> None:
payLoad = {'username': username, 'session_type': sessionType or UNKNOWN}
def logout(self, username: str, sessionType: typing.Optional[str]) -> None:
payLoad = {
'username': username,
'session_type': sessionType or UNKNOWN
}
self.post('logout', payLoad)
def ping(self) -> bool:

View File

@@ -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, and prior to first login
) # For being used on "unmanaged" hosts only
self._http = None
# Initialzies loglevel and serviceLogger

View File

@@ -66,4 +66,3 @@ class CertificateInfoType(typing.NamedTuple):
private_key: str
server_certificate: str
password: str
ciphers: str

View File

@@ -1 +1 @@
VERSION = '3.6.0'
VERSION = '3.5.0'

View File

@@ -192,7 +192,6 @@ 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)

View File

@@ -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 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.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.label_loglevel.setText(_translate("UdsActorSetupDialog", "Log Level"))
self.label_restrictNet.setText(_translate("UdsActorSetupDialog", "Restrict Net"))
self.restrictNet.setToolTip(_translate("UdsActorSetupDialog", "Restrict valid detection of network interfaces to this network."))
self.restrictNet.setWhatsThis(_translate("UdsActorSetupDialog", "<html><head/><body><p>Restrics valid detection of network interfaces.</p><p>Note: Use this field only in case of several network interfaces, so UDS knows which one is the interface where the user will be connected..</p></body></html>"))
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>"))
from ui import uds_rc

View File

@@ -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\x81\xce\x8a\xac\xf2\
\x00\x00\x01\x6e\x86\x31\xef\xa3\
"
qt_version = [int(v) for v in QtCore.qVersion().split('.')]

View File

@@ -6,4 +6,3 @@
/UDSClient*.AppImage
/appimage*
/UDSClient.desktop
*.zsync

View File

@@ -90,7 +90,6 @@ 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
@@ -103,7 +102,6 @@ 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)

View File

@@ -12,6 +12,9 @@ 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 |

View File

@@ -1,9 +1,3 @@
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

View File

@@ -1 +1 @@
10
9

View File

@@ -1,10 +1,10 @@
Source: udsclient3
Section: admin
Priority: optional
Maintainer: Adolfo Gómez García <agomez@virtualcable.net>
Maintainer: Adolfo Gómez García <agomez@virtualcable.es>
Build-Depends: debhelper (>= 7), po-debconf
Standards-Version: 3.9.2
Homepage: http://www.udsenterprise.com
Homepage: http://www.virtualcable.es
Package: udsclient3
Section: admin

View File

@@ -5,9 +5,9 @@ Source: http://github.com/dkmstr/openuds/client-py3
Files: *
Copyright: (c) 2014-2022, Virtual Cable S.L.U.
License: BSD-3-clause
License: 3-BSD
License: BSD-3-clause
License: 3-BSD
All rights reserved.
.
Redistribution and use in source and binary forms, with or without
@@ -35,4 +35,4 @@ License: BSD-3-clause
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.

View File

@@ -1,2 +1,2 @@
udsclient3_3.6.0_all.deb admin optional
udsclient3_3.6.0_amd64.buildinfo admin optional
udsclient3_3.5.0_all.deb admin optional
udsclient3_3.5.0_amd64.buildinfo admin optional

View File

@@ -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.inf</ivalue>
<ivalue classname="custom_partition.source%.url" variableExpression="" variableSubstitutionActive="false">https://[UMS_SERVER]:8443/ums_filetransfer/UDSClient-igel.inf</ivalue>
<ivalue classname="custom_partition.source%.username" variableExpression="" variableSubstitutionActive="false">[UMS_USERNAME]</ivalue>
</instance>
<instance classprefix="sessions.chromium%" serialnumber="-6b5264e9:17ca6f65505:-8000127.0.1.1">

View File

@@ -1,4 +1,2 @@
#!/bin/sh
cp /UDSClient/UDSClient.desktop /usr/share/applications.mime
chmod 755 /UDSClient/UDSClient

View File

@@ -9,7 +9,6 @@ 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

View File

@@ -1,5 +1,3 @@
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

View File

@@ -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: 'https://ftp-master.debian.org/keys/archive-key-11.asc'
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x648ACFD622F3D138'
include:
- python3

View File

@@ -41,11 +41,11 @@ import typing
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import QSettings
from uds.rest import RestApi, RetryException, InvalidVersion
from uds.rest import RestApi, RetryException, InvalidVersion, UDSException
# Just to ensure there are available on runtime
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.forward import forward as ssh_forward # type: ignore
from uds.tunnel import forward as tunnel_forwards # type: ignore
from uds.log import logger
from uds import tools
@@ -55,6 +55,7 @@ from UDSWindow import Ui_MainWindow
class UDSClient(QtWidgets.QMainWindow):
ticket: str = ''
scrambler: str = ''
withError = False
@@ -148,7 +149,7 @@ class UDSClient(QtWidgets.QMainWindow):
webbrowser.open(e.downloadUrl)
self.closeWindow()
return
except Exception as e: # pylint: disable=broad-exception-caught
except Exception as e:
self.showError(e)
self.closeWindow()
return
@@ -167,9 +168,7 @@ class UDSClient(QtWidgets.QMainWindow):
# self.hide()
self.closeWindow()
exec(
script, globals(), {'parent': self, 'sp': params}
) # pylint: disable=exec-used
exec(script, globals(), {'parent': self, 'sp': params})
# Execute the waiting tasks...
threading.Thread(target=endScript).start()
@@ -178,8 +177,7 @@ 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: # pylint: disable=broad-exception-caught
logger.exception('Error getting transport data')
except Exception as e:
self.showError(e)
def start(self):
@@ -196,27 +194,27 @@ def endScript():
try:
# Remove early stage files...
tools.unlinkFiles(early=True)
except Exception as e: # pylint: disable=broad-exception-caught
except Exception as e:
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: # pylint: disable=broad-exception-caught
except Exception as e:
logger.debug('Watiting for tasks to finish: %s', e)
try:
logger.debug('Unlinking files')
tools.unlinkFiles(early=False)
except Exception as e: # pylint: disable=broad-exception-caught
except Exception as e:
logger.debug('Unlinking files on later stage: %s', e)
# Removing
try:
logger.debug('Executing threads before exit')
tools.execBeforeExit()
except Exception as e: # pylint: disable=broad-exception-caught
except Exception as e:
logger.debug('execBeforeExit: %s', e)
logger.debug('endScript done')
@@ -307,7 +305,7 @@ def minimal(api: RestApi, ticket: str, scrambler: str):
+ '\n\nPlease, retry again in a while.',
QtWidgets.QMessageBox.Ok,
)
except Exception as e: # pylint: disable=broad-exception-caught
except Exception as e:
# logger.exception('Got exception on getTransportData')
QtWidgets.QMessageBox.critical(
None, # type: ignore
@@ -354,38 +352,31 @@ def main(args: typing.List[str]):
sys.exit(0)
logger.debug('URI: %s', uri)
# 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
if uri[:6] != 'uds://' and uri[:7] != 'udss://':
raise Exception()
ssl = uri[3] == 's'
host, ticket, scrambler = uri.split('//')[1].split('/') # type: ignore
logger.debug(
'host:%s, ticket:%s, scrambler:%s',
'ssl:%s, host:%s, ticket:%s, scrambler:%s',
ssl,
host,
ticket,
scrambler,
)
except Exception: # pylint: disable=broad-except
except Exception:
logger.debug('Detected execution without valid URI, exiting')
QtWidgets.QMessageBox.critical(
None, # type: ignore
'Notice',
f'UDS Client Version {VERSION}',
'UDS Client Version {}'.format(VERSION),
QtWidgets.QMessageBox.Ok,
)
sys.exit(1)
# Setup REST api endpoint
api = RestApi(
f'https://{host}/uds/rest/client', sslError
'{}://{}/uds/rest/client'.format(['http', 'https'][ssl], host), sslError
)
try:
@@ -403,7 +394,7 @@ def main(args: typing.List[str]):
exitVal = app.exec()
logger.debug('Execution finished correctly')
except Exception as e: # pylint: disable=broad-exception-caught
except Exception as e:
logger.exception('Got an exception executing client:')
exitVal = 128
QtWidgets.QMessageBox.critical(
@@ -413,6 +404,5 @@ def main(args: typing.List[str]):
logger.debug('Exiting')
sys.exit(exitVal)
if __name__ == "__main__":
main(sys.argv)

View File

@@ -11,7 +11,6 @@ from PyQt5 import QtCore, QtWidgets, QtGui
SCRIPT_NAME = 'UDSClientLauncher'
class UdsApplication(QtWidgets.QApplication):
path: str
tunnels: typing.List[subprocess.Popen]
@@ -23,10 +22,6 @@ 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:
@@ -35,13 +30,13 @@ class UdsApplication(QtWidgets.QApplication):
logger.debug('Got error polling subprocess: %s', e)
return False
# Remove references to finished tunnels, they will be garbage collected
self.tunnels = [tunnel for tunnel in self.tunnels if isRunning(tunnel)]
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)
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())
@@ -50,7 +45,7 @@ class UdsApplication(QtWidgets.QApplication):
tunnel.kill()
def event(self, evnt: QtCore.QEvent) -> bool:
if evnt.type() == QtCore.QEvent.Type.FileOpen:
if evnt.type() == QtCore.QEvent.FileOpen:
fe = typing.cast(QtGui.QFileOpenEvent, evnt)
logger.debug('Got url: %s', fe.url().url())
fe.accept()
@@ -75,6 +70,6 @@ def main(args: typing.List[str]):
sys.exit(app.exec())
if __name__ == "__main__":
main(args=sys.argv)

View File

@@ -2,158 +2,13 @@
# Resource object code
#
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.8)
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2)
#
# 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\
@@ -2437,6 +2292,151 @@ 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\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\
\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\
"
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\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\
\x00\x00\x01\x70\xc4\x82\x24\xd0\
"
qt_version = [int(v) for v in QtCore.qVersion().split('.')]

View File

@@ -29,11 +29,13 @@
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
VERSION = '3.6.0'
from __future__ import unicode_literals
VERSION = '3.5.1'
__title__ = 'udclient'
__version__ = VERSION
__build__ = 0x010712
__author__ = 'Adolfo Gómez <dkmaster@dkmon.com>'
__build__ = 0x010760
__author__ = 'Adolfo Gómez'
__license__ = "BSD 3-clause"
__copyright__ = "Copyright 2014-2022 VirtualCable S.L.U."
__copyright__ = "Copyright 2014-2017 VirtualCable S.L.U."

View File

@@ -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() # type: ignore
ssh_transport = self.client.get_transport()
event = self.stopEvent
thread = self

View File

@@ -32,7 +32,6 @@
import logging
import os
import os.path
import platform
import sys
import tempfile
@@ -62,38 +61,3 @@ 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)

View File

@@ -51,18 +51,6 @@ 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
@@ -84,6 +72,7 @@ class InvalidVersion(UDSException):
super().__init__(downloadUrl)
self.downloadUrl = downloadUrl
class RestApi:
_restApiUrl: str # base Rest API URL
@@ -163,7 +152,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:
@@ -195,10 +184,6 @@ 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())

View File

@@ -44,17 +44,11 @@ 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]] = []
@@ -82,7 +76,9 @@ 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)
@@ -112,7 +108,9 @@ 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)
@@ -141,7 +139,9 @@ 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,7 +195,9 @@ 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()
@@ -222,7 +224,14 @@ def verifySignature(script: bytes, signature: bytes) -> bool:
param: signature String signature to be verified
return: Boolean. True if the signature is valid; False otherwise.
'''
public_key = serialization.load_pem_public_key(data=PUBLIC_KEY, backend=default_backend())
# 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()
)
try:
public_key.verify( # type: ignore
@@ -252,17 +261,9 @@ 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

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2022 Virtual Cable S.L.U.
# Copyright (c) 2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@@ -32,6 +32,8 @@ import socket
import socketserver
import ssl
import threading
import time
import random
import threading
import select
import typing
@@ -41,16 +43,14 @@ from . import tools
HANDSHAKE_V1 = b'\x5AMGB\xA5\x01\x00'
BUFFER_SIZE = 1024 * 16 # Max buffer length
LISTEN_ADDRESS = '127.0.0.1'
DEBUG = True
LISTEN_ADDRESS = '0.0.0.0' if DEBUG else '127.0.0.1'
# ForwarServer states
TUNNEL_LISTENING, TUNNEL_OPENING, TUNNEL_PROCESSING, TUNNEL_ERROR = 0, 1, 2, 3
logger = logging.getLogger(__name__)
PayLoadType = typing.Optional[typing.Tuple[typing.Optional[bytes], typing.Optional[bytes]]]
class ForwardServer(socketserver.ThreadingTCPServer):
daemon_threads = True
@@ -60,9 +60,9 @@ class ForwardServer(socketserver.ThreadingTCPServer):
ticket: str
stop_flag: threading.Event
can_stop: bool
timeout: int
timer: typing.Optional[threading.Timer]
check_certificate: bool
keep_listening: bool
current_connections: int
status: int
@@ -73,29 +73,30 @@ 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"
# 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.timeout = int(time.time()) + timeout if timeout > 0 else 0
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 = timeout or 60
self.timer = threading.Timer(abs(timeout), ForwardServer.__checkStarted, args=(self,))
timeout = abs(timeout) or 60
self.timer = threading.Timer(
abs(timeout), ForwardServer.__checkStarted, args=(self,)
)
self.timer.start()
def stop(self) -> None:
@@ -119,13 +120,10 @@ 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:
@@ -150,20 +148,18 @@ 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
return self.can_stop or (self.timeout != 0 and int(time.time()) > self.timeout)
@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
@@ -179,8 +175,8 @@ class Handler(socketserver.BaseRequestHandler):
def handle(self) -> None:
self.server.status = TUNNEL_OPENING
# If server processing is over time, and don't allow more connections
if self.server.stoppable and not self.server.keep_listening:
# If server processing is over time
if self.server.stoppable:
self.server.status = TUNNEL_ERROR
logger.info('Rejected timedout connection')
self.request.close() # End connection without processing it
@@ -198,10 +194,11 @@ 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}')
@@ -238,9 +235,10 @@ class Handler(socketserver.BaseRequestHandler):
def _run(server: ForwardServer) -> None:
logger.debug(
'Starting forwarder: %s -> %s',
'Starting forwarder: %s -> %s, timeout: %d',
server.server_address,
server.remote,
server.timeout,
)
server.serve_forever()
logger.debug('Stoped forwarder %s -> %s', server.server_address, server.remote)
@@ -252,17 +250,40 @@ 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,
)

View File

@@ -7,9 +7,9 @@
<groupId>org.openuds.server</groupId>
<artifactId>guacamole-auth-uds</artifactId>
<packaging>jar</packaging>
<version>4.0.0</version>
<version>2.5.0</version>
<name>UDS Integration Extension for Apache Guacamole</name>
<url>https://github.com/VirtualCable/openuds</url>
<url>https://github.com/dkmstr/openuds</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -18,11 +18,11 @@
<build>
<plugins>
<!-- Compile using Java 1.8, as guacamole-client -->
<!-- Compile using Java 1.8 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<version>3.3</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>3.1.2</version>
<version>2.10</version>
<executions>
<execution>
<id>unpack-dependencies</id>
@@ -70,15 +70,15 @@
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>jsr311-api</artifactId>
<scope>provided</scope>
<version>1.1.1</version>
<scope>provided</scope>
</dependency>
<!-- Guacamole extension API -->
<dependency>
<groupId>org.apache.guacamole</groupId>
<artifactId>guacamole-ext</artifactId>
<version>1.5.2</version>
<version>1.2.0</version>
<scope>provided</scope>
</dependency>
@@ -86,7 +86,7 @@
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>5.1.0</version>
<version>3.0</version>
</dependency>
</dependencies>

View File

@@ -55,7 +55,7 @@ public class UDSModule extends AbstractModule {
* If the guacamole.properties file cannot be read.
*/
public UDSModule() throws GuacamoleException {
this.environment = LocalEnvironment.getInstance();
this.environment = new LocalEnvironment();
}
@Override

View File

@@ -30,8 +30,13 @@ 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;

View File

@@ -174,10 +174,6 @@ 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

View File

@@ -1,4 +1,4 @@
Django==3.2.22
Django
bitarray
html5lib
six
@@ -19,4 +19,3 @@ webencodings
xml-marshaller
pycrypto>=2.6.1
cryptography
Pillow==9.5.0

199
server/src/requirements.txt Normal file
View File

@@ -0,0 +1,199 @@
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

View File

@@ -146,38 +146,9 @@ 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*'
# **** 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 ****
# This is a very long string, an RSA KEY (this can be changed, but if u loose it, all encription will be lost)
RSA_KEY = '-----BEGIN RSA PRIVATE KEY-----\nMIICXgIBAAKBgQC0qe1GlriQbHFYdKYRPBFDSS8Ne/TEKI2mtPKJf36XZTy6rIyH\nvUpT1gMScVjHjOISLNJQqktyv0G+ZGzLDmfkCUBev6JBlFwNeX3Dv/97Q0BsEzJX\noYHiDANUkuB30ukmGvG0sg1v4ccl+xs2Su6pFSc5bGINBcQ5tO0ZI6Q1nQIDAQAB\nAoGBAKA7Octqb+T/mQOX6ZXNjY38wXOXJb44LXHWeGnEnvUNf/Aci0L0epCidfUM\nfG33oKX4BMwwTVxHDrsa/HaXn0FZtbQeBVywZqMqWpkfL/Ho8XJ8Rsq8OfElrwek\nOCPXgxMzQYxoNHw8V97k5qhfupQ+h878BseN367xSyQ8plahAkEAuPgAi6aobwZ5\nFZhx/+6rmQ8sM8FOuzzm6bclrvfuRAUFa9+kMM2K48NAneAtLPphofqI8wDPCYgQ\nTl7O96GXVQJBAPoKtWIMuBHJXKCdUNOISmeEvEzJMPKduvyqnUYv17tM0JTV0uzO\nuDpJoNIwVPq5c3LJaORKeCZnt3dBrdH1FSkCQQC3DK+1hIvhvB0uUvxWlIL7aTmM\nSny47Y9zsc04N6JzbCiuVdeueGs/9eXHl6f9gBgI7eCD48QAocfJVygphqA1AkEA\nrvzZjcIK+9+pJHqUO0XxlFrPkQloaRK77uHUaW9IEjui6dZu4+2T/q7SjubmQgWR\nZy7Pap03UuFZA2wCoqJbaQJAUG0FVrnyUORUnMQvdDjAWps2sXoPvA8sbQY1W8dh\nR2k4TCFl2wD7LutvsdgdkiH0gWdh5tc1c4dRmSX1eQ27nA==\n-----END RSA PRIVATE KEY-----'
# Trusted cyphers
SECURE_CIPHERS = (
'AES-256-GCM-SHA384'
':CHACHA20-POLY1305-SHA256'
':AES-128-GCM-SHA256'
':ECDHE-RSA-AES256-GCM-SHA384'
':ECDHE-RSA-AES128-GCM-SHA256'
':ECDHE-RSA-CHACHA20-POLY1305'
':ECDHE-ECDSA-AES128-GCM-SHA256'
':ECDHE-ECDSA-AES256-GCM-SHA384'
':ECDHE-ECDSA-AES128-SHA256'
':ECDHE-ECDSA-CHACHA20-POLY1305'
)
# Min TLS version
SECURE_MIN_TLS_VERSION = '1.2'
# LDAP CIFHER SUITE can be enforced here. Use GNU TLS cipher suite names in this case
# i.e.:
# * NORMAL
# * NORMAL:-VERS-TLS-ALL:+VERS-TLS1.2:+VERS-TLS1.3
# * PFS
# * SECURE256
# If omitted, defaults to PFS:-VERS-TLS-ALL:+VERS-TLS1.2:+VERS-TLS1.3:-AES-128-CBC:-AES-256-CBC:-DHE-RSA
# Example:
LDAP_CIPHER_SUITE = 'PFS:-VERS-TLS-ALL:+VERS-TLS1.2:+VERS-TLS1.3:-AES-128-CBC:-AES-256-CBC:-DHE-RSA'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',

View File

@@ -43,8 +43,6 @@ 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,
@@ -63,6 +61,8 @@ if typing.TYPE_CHECKING:
logger = logging.getLogger(__name__)
__all__ = ['Handler', 'Dispatcher']
AUTH_TOKEN_HEADER = 'X-Auth-Token'
@@ -78,11 +78,13 @@ class Dispatcher(View):
# pylint: disable=too-many-locals, too-many-return-statements, too-many-branches, too-many-statements
@method_decorator(csrf_exempt)
# 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
def dispatch(self, request: 'ExtendedHttpRequestWithUser', *args, **kwargs):
"""
Processes the REST request and routes it wherever it needs to be routed
"""
# Remove session from request, so response middleware do nothing with this
del request.session
# Now we extract method and possible variables from path
path: typing.List[str] = kwargs['arguments'].split('/')
del kwargs['arguments']
@@ -147,8 +149,6 @@ 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,8 +158,6 @@ 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"
)
@@ -170,8 +168,6 @@ 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"
)
@@ -186,29 +182,20 @@ 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:
log.log_operation(handler, 501, log.ERROR)
return http.HttpResponseBadRequest(str(e), content_type="text/plain", status=501)
return http.HttpResponseBadRequest(str(e), content_type="text/plain")
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")
@@ -239,9 +226,7 @@ class Dispatcher(View):
service_node[name][''] = cls
else:
from .model import DetailHandler
if cls is not DetailHandler:
Dispatcher.registerSubclasses(cls.__subclasses__())
Dispatcher.registerSubclasses(cls.__subclasses__())
# Initializes the dispatchers
@staticmethod
@@ -255,9 +240,7 @@ class Dispatcher(View):
# Dinamycally import children of this package.
package = 'methods'
pkgpath = os.path.join(
os.path.dirname(typing.cast(str, sys.modules[__name__].__file__)), package
)
pkgpath = os.path.join(os.path.dirname(sys.modules[__name__].__file__), package)
for _, name, _ in pkgutil.iter_modules([pkgpath]):
# __import__(__name__ + '.' + package + '.' + name, globals(), locals(), [], 0)
importlib.import_module(

View File

@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2021 Virtual Cable S.L.U.
# All rights reserved.
@@ -27,7 +28,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
@@ -36,15 +37,11 @@ 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
@@ -130,8 +127,8 @@ class Handler:
self,
request: 'ExtendedHttpRequestWithUser',
path: str,
method: str,
params: typing.MutableMapping[str, typing.Any],
operation: str,
params: typing.Any,
*args: str,
**kwargs
):
@@ -150,7 +147,7 @@ class Handler:
self._request = request
self._path = path
self._operation = method
self._operation = operation
self._params = params
self._args = args
self._kwargs = kwargs
@@ -167,7 +164,7 @@ class Handler:
except Exception: # Couldn't authenticate
self._authToken = None
self._session = None
if self._authToken is None:
raise AccessDenied()
@@ -180,11 +177,6 @@ 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]:
"""

View File

@@ -1,107 +0,0 @@
# -*- 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
],
)

View File

@@ -141,7 +141,7 @@ class Actor(Handler):
except Exception:
return Actor.result({})
def get(self) -> typing.Any: # pylint: disable=too-many-return-statements
def get(self): # 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) -> typing.Any: # pylint: disable=too-many-branches
def post(self): # pylint: disable=too-many-branches
"""
Processes post requests
"""

View File

@@ -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, security
from uds.core.util import log, certs
from uds.core.util.state import State
from uds.core.util.cache import Cache
from uds.core.util.config import GlobalConfig
@@ -54,7 +54,6 @@ 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__)
@@ -70,24 +69,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(request: 'ExtendedHttpRequest') -> None:
def checkBlockedIp(ip: str) -> None:
if GlobalConfig.BLOCK_ACTOR_FAILURES.getBool() is False:
return
cache = Cache('actorv3')
fails = cache.get(request.ip) or 0
fails = cache.get(ip) or 0
if fails > ALLOWED_FAILS:
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)
logger.info(
'Access to actor from %s is blocked for %s seconds since last fail',
ip,
GlobalConfig.LOGIN_BLOCK.getInt(),
)
raise BlockAccess()
def incFailedIp(request: 'ExtendedHttpRequest') -> None:
def incFailedIp(ip: str) -> None:
cache = Cache('actorv3')
fails = cache.get(request.ip, 0) + 1
cache.put(request.ip, fails, GlobalConfig.LOGIN_BLOCK.getInt())
fails = (cache.get(ip) or 0) + 1
cache.put(ip, fails, GlobalConfig.LOGIN_BLOCK.getInt())
class ActorV3Action(Handler):
@@ -115,7 +114,6 @@ 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]:
@@ -123,13 +121,13 @@ class ActorV3Action(Handler):
def post(self) -> typing.MutableMapping[str, typing.Any]:
try:
checkBlockedIp(self._request)
checkBlockedIp(self._request.ip) # pylint: disable=protected-access
result = self.action()
logger.debug('Action result: %s', result)
return result
except (BlockAccess, KeyError):
# For blocking attacks
incFailedIp(self._request)
incFailedIp(self._request.ip) # pylint: disable=protected-access
except Exception as e:
logger.exception('Posting %s: %s', self.__class__, e)
@@ -184,7 +182,6 @@ 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,
@@ -350,7 +347,7 @@ class BaseReadyChange(ActorV3Action):
userServiceManager().notifyReadyFromOsManager(userService, '')
# Generates a certificate and send it to client.
privateKey, cert, password = security.selfSignedCert(self._params['ip'])
privateKey, cert, password = certs.selfSignedCert(self._params['ip'])
# Store certificate with userService
userService.setProperty('cert', cert)
userService.setProperty('priv', privateKey)
@@ -457,10 +454,8 @@ class LoginLogout(ActorV3Action):
else:
service.processLogout(validId, remote_login=is_remote)
# All right, service notified..
except Exception as e :
# Log error and continue
logger.error('Error notifying service: %s (%s)', e, self._params)
# All right, service notified...
except Exception:
raise BlockAccess()
@@ -688,7 +683,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 = security.selfSignedCert(ip)
privateKey, certificate, password = certs.selfSignedCert(ip)
cert: typing.Dict[str, str] = {
'private_key': privateKey,
'server_certificate': certificate,
@@ -734,8 +729,8 @@ class Notify(ActorV3Action):
try:
# Check block manually
checkBlockedIp(self._request) # pylint: disable=protected-access
if self._params['action'] == 'login':
checkBlockedIp(self._request.ip) # pylint: disable=protected-access
if 'action' == 'login':
Login.action(typing.cast(Login, self))
else:
Logout.action(typing.cast(Logout, self))
@@ -743,6 +738,6 @@ class Notify(ActorV3Action):
return ActorV3Action.actorResult('ok')
except UserService.DoesNotExist:
# For blocking attacks
incFailedIp(self._request) # pylint: disable=protected-access
incFailedIp(self._request.ip) # pylint: disable=protected-access
raise AccessDenied('Access denied')

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2022 Virtual Cable S.L.U.
# Copyright (c) 2014-2019 Virtual Cable S.L.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
@@ -30,18 +30,16 @@
"""
@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, MFA
from uds.models import Authenticator
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
@@ -60,7 +58,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', 'mfa_id:'] # mfa_id is optional, and defaults to '' (no mfa)
save_fields = ['name', 'comments', 'tags', 'priority', 'small_name', 'visible']
table_title = _('Authenticators')
table_fields = [
@@ -72,7 +70,6 @@ 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}},
]
@@ -90,17 +87,16 @@ 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:
authType = auths.factory().lookup(type_)
if authType:
tgui = auths.factory().lookup(type_)
if tgui:
g = self.addDefaultFields(
authType.guiDescription(),
tgui.guiDescription(),
['name', 'comments', 'tags', 'priority', 'small_name'],
)
self.addField(
@@ -110,39 +106,16 @@ class Authenticators(ModelHandler):
'value': True,
'label': ugettext('Visible'),
'tooltip': ugettext(
'If active, authenticator will be visible for users'
'If active, transport will be visible for users'
),
'type': gui.InputField.CHECKBOX_TYPE,
'order': 107,
'tab': gui.DISPLAY_TAB,
'tab': ugettext('Display'),
},
)
# 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 as e:
logger.info('Type not found: %s', e)
except Exception:
raise NotFound('type not found')
def item_as_dict(self, item: Authenticator) -> typing.Dict[str, typing.Any]:
@@ -154,8 +127,6 @@ 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(),
@@ -211,29 +182,6 @@ 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)

View File

@@ -70,7 +70,7 @@ class CalendarRules(DetailHandler): # pylint: disable=too-many-public-methods
'name': item.name,
'comments': item.comments,
'start': item.start,
'end': datetime.datetime.combine(item.end, datetime.time.max) if item.end else None,
'end': item.end,
'frequency': item.frequency,
'interval': item.interval,
'duration': item.duration,

View File

@@ -68,9 +68,6 @@ 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}},
]
@@ -81,10 +78,6 @@ 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),
}

View File

@@ -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) -> typing.Any: # pylint: disable=too-many-locals
def get(self): # pylint: disable=too-many-locals
"""
Processes get requests
"""
@@ -116,9 +116,7 @@ class Client(Handler):
{
'availableVersion': CLIENT_VERSION,
'requiredVersion': REQUIRED_CLIENT_VERSION,
'downloadUrl': self._request.build_absolute_uri(
reverse('page.client-download')
),
'downloadUrl': 'A new version of UDS Client is required.\nPlease, download it from Client Download section.',
}
)

View File

@@ -39,42 +39,61 @@ 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) -> typing.Any:
def get(self):
cfg: CfgConfig.Value
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()
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(),
}
for section, secDict in configs.items()
}
logger.debug('Configuration: %s', res)
return res
def put(self):
for section, secDict in self._params.items():
for key, vals in secDict.items():
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,
)
CfgConfig.update(section, key, vals['value'])
return 'done'

View File

@@ -65,9 +65,9 @@ class Login(Handler):
@staticmethod
def result(
result: str = 'error',
token: typing.Optional[str] = None,
scrambler: typing.Optional[str] = None,
error: typing.Optional[str] = None,
token: str = None,
scrambler: str = None,
error: 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) -> typing.Iterator[typing.Dict[str, typing.Any]]:
def auths(self):
paramAll: bool = self._params.get('all', 'false') == 'true'
auth: Authenticator
for auth in Authenticator.objects.all():

View File

@@ -160,7 +160,7 @@ class MetaPools(ModelHandler):
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
gui.choiceImage(v.uuid, v.name, v.thumb64)
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) # type: ignore
gui.choiceImage(v.uuid, v.name, v.thumb64)
for v in ServicePoolGroup.objects.all()
]
),

View File

@@ -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 # type: ignore
service.user.pretty_name, user.pretty_name, self._user.pretty_name
)
# If there is another service that has this same owner, raise an exception

View File

@@ -1,118 +0,0 @@
# -*- 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),
}

View File

@@ -68,7 +68,6 @@ class Permissions(Handler):
'calendars': models.Calendar,
'metapools': models.MetaPool,
'accounts': models.Account,
'mfa': models.MFA,
}.get(arg, None)
if cls is None:
@@ -93,10 +92,10 @@ class Permissions(Handler):
{
'id': perm.uuid,
'type': kind,
'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
'auth': entity.manager.uuid,
'auth_name': entity.manager.name,
'entity_id': entity.uuid,
'entity_name': entity.name,
'perm': perm.permission,
'perm_name': perm.permission_as_string,
}
@@ -104,7 +103,7 @@ class Permissions(Handler):
return sorted(res, key=lambda v: v['auth_name'] + v['entity_name'])
def get(self) -> typing.Any:
def get(self):
"""
Processes get requests
"""

View File

@@ -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) # type: ignore
gui.choiceItem(v.uuid, v.name)
for v in models.Proxy.objects.all()
]
),

View File

@@ -93,7 +93,7 @@ class ServicesPoolGroups(ModelHandler):
'values': [gui.choiceImage(-1, '--------', DEFAULT_THUMB_BASE64)]
+ gui.sortedChoices(
[
gui.choiceImage(v.uuid, v.name, v.thumb64) # type: ignore
gui.choiceImage(v.uuid, v.name, v.thumb64)
for v in Image.objects.all()
]
),

View File

@@ -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, # type: ignore
'parent_type': item.service.data_type, # type: ignore
'parent': item.service.name,
'parent_type': item.service.data_type,
'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, # type: ignore
'provider_id': item.service.provider.uuid, # type: ignore
'service_id': item.service.uuid,
'provider_id': item.service.provider.uuid,
'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) # type: ignore
val['info'] = Services.serviceInfo(item.service)
val['pool_group_id'] = poolGroupId
val['pool_group_name'] = poolGroupName
val['pool_group_thumb'] = poolGroupThumb
val['usage'] = str(item.usage(usage_count)[0]) + '%'
val['usage'] = str(item.usage(usage_count)) + '%'
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) # type: ignore
gui.choiceItem(v.uuid, v.provider.name + '\\' + v.name)
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()] # type: ignore
[gui.choiceItem(v.uuid, v.name) for v in OSManager.objects.all()]
),
'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) # type: ignore
gui.choiceImage(v.uuid, v.name, v.thumb64)
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) # type: ignore
gui.choiceImage(v.uuid, v.name, v.thumb64)
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()] # type: ignore
[gui.choiceItem(v.uuid, v.name) for v in Account.objects.all()]
),
'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() # type: ignore
itemInfo = item.service.getType()
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() # type: ignore
service = item.service.getInstance()
return [gui.choiceItem(i[0], i[1]) for i in service.listAssignables()]
def createFromAssignable(self, item: ServicePool) -> typing.Any:

View File

@@ -77,8 +77,8 @@ class ServicesUsage(DetailHandler):
'friendly_name': item.friendly_name,
'owner': owner,
'owner_info': owner_info,
'service': item.deployed_service.service.name, # type: ignore
'service_id': item.deployed_service.service.uuid, # type: ignore
'service': item.deployed_service.service.name,
'service_id': item.deployed_service.service.uuid,
'pool': item.deployed_service.name,
'pool_id': item.deployed_service.uuid,
'ip': props.get('ip', _('unknown')),

View File

@@ -53,7 +53,7 @@ if typing.TYPE_CHECKING:
cache = Cache('StatsDispatcher')
# Enclosed methods under /stats path
POINTS = 70
POINTS = 150
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) -> typing.Any:
def get(self):
logger.debug('args: %s', self._args)
# Only allow admin user for global stats
if len(self._args) == 1:

View File

@@ -58,11 +58,10 @@ VALID_PARAMS = (
'transport', # Admited to be backwards compatible, but not used. Will be removed on a future release.
'force',
'userIp',
'time',
)
# Enclosed methods under /tickets path
# Enclosed methods under /actor path
class Tickets(Handler):
"""
Processes tickets access requests.
@@ -128,7 +127,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,
) -> typing.Any:
): # pylint: disable=too-many-locals,too-many-branches,too-many-statements
"""
Processes put requests, currently only under "create"
"""
@@ -171,7 +170,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 or '')
groupIds.append(auth.groups.get(name=groupName).uuid)
except Exception:
logger.info(
'Group %s from ticket does not exists on auth %s, forced creation: %s',
@@ -185,7 +184,6 @@ class Tickets(Handler):
name=groupName,
comments='Autocreated form ticket by using force paratemeter',
).uuid
or ''
)
if not groupIds: # No valid group in groups names
@@ -226,7 +224,7 @@ class Tickets(Handler):
# For metapool, transport is ignored..
servicePoolId = 'M' + pool.uuid # type: ignore
servicePoolId = 'M' + pool.uuid
transportId = 'meta'
except models.MetaPool.DoesNotExist:
@@ -242,7 +240,7 @@ class Tickets(Handler):
):
pool.assignedGroups.add(auth.groups.get(uuid=addGrp))
servicePoolId = 'F' + pool.uuid # type: ignore
servicePoolId = 'F' + pool.uuid
except models.Authenticator.DoesNotExist:
return Tickets.result(error='Authenticator does not exists')

View File

@@ -30,7 +30,6 @@
'''
@itemor: Adolfo Gómez, dkmaster at dkmon dot com
'''
import re
import logging
import typing
@@ -110,7 +109,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(), # type: ignore
key=lambda x: x['text'].lower(),
),
'label': ugettext('Networks'),
'tooltip': ugettext(
@@ -148,7 +147,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 # type: ignore
if transport.protocol in x.service.getType().allowedProtocols
],
'label': ugettext('Service Pools'),
'tooltip': ugettext('Currently assigned services pools'),
@@ -200,13 +199,6 @@ 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:

View File

@@ -43,8 +43,9 @@ 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):
@@ -67,14 +68,11 @@ class TunnelTicket(Handler):
self._request.ip,
)
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',
)
if (
not isTrustedSource(self._request.ip)
or len(self._args) != 3
or len(self._args[0]) != 48
):
# Invalid requests
raise AccessDenied()
@@ -89,7 +87,9 @@ 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':

View File

@@ -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 # type: ignore
userService.user.pretty_name, user.pretty_name, self._user.pretty_name
)
# If there is another service that has this same owner, raise an exception

View File

@@ -78,23 +78,23 @@ def getPoolsForGroups(groups):
class Users(DetailHandler):
custom_methods = ['servicesPools', 'userServices', 'cleanRelated']
custom_methods = ['servicesPools', 'userServices']
@staticmethod
def uuid_to_id(iterator):
for v in iterator:
v['id'] = v['uuid']
del v['uuid']
yield v
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(
uuid_to_id(
(i for i in parent.users.all().values(
Users.uuid_to_id(
parent.users.all().values(
'uuid',
'name',
'real_name',
@@ -104,8 +104,7 @@ class Users(DetailHandler):
'is_admin',
'last_access',
'parent',
'mfa_data',
))
)
)
)
for res in values:
@@ -128,7 +127,6 @@ class Users(DetailHandler):
'is_admin',
'last_access',
'parent',
'mfa_data',
),
)
res['id'] = u.uuid
@@ -155,7 +153,7 @@ class Users(DetailHandler):
except Exception:
return _('Current users')
def getFields(self, parent: Authenticator):
def getFields(self, parent):
return [
{
'name': {
@@ -200,16 +198,12 @@ class Users(DetailHandler):
'staff_member',
'is_admin',
]
if self._params.get('name', '').strip() == '':
if self._params.get('name', '') == '':
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:
@@ -230,8 +224,9 @@ class Users(DetailHandler):
user.__dict__.update(fields)
logger.debug('User parent: %s', user.parent)
# If internal auth, threat it "special"
if auth.isExternalSource is False and not user.parent:
if auth.isExternalSource is False and (
user.parent is None or user.parent == ''
):
groups = self.readFieldsFromParams(['groups'])['groups']
logger.debug('Groups: %s', groups)
logger.debug('Got Groups %s', parent.groups.filter(uuid__in=groups))
@@ -282,7 +277,7 @@ class Users(DetailHandler):
return 'deleted'
def servicesPools(self, parent: Authenticator, item: str) -> typing.List[typing.Dict]:
def servicesPools(self, parent: Authenticator, item):
uuid = processUuid(item)
user = parent.users.get(uuid=processUuid(uuid))
res = []
@@ -304,7 +299,7 @@ class Users(DetailHandler):
return res
def userServices(self, parent: Authenticator, item: str) -> typing.List[typing.Dict]:
def userServices(self, parent: Authenticator, item):
uuid = processUuid(item)
user = parent.users.get(uuid=processUuid(uuid))
res = []
@@ -316,12 +311,6 @@ 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):
@@ -425,7 +414,7 @@ class Groups(DetailHandler):
except Exception:
raise self.invalidRequestException()
def saveItem(self, parent: Authenticator, item: typing.Optional[str]) -> None:
def saveItem(self, parent: Authenticator, item) -> None:
group = None # Avoid warning on reference before assignment
try:
is_meta = self._params['type'] == 'meta'
@@ -440,7 +429,7 @@ class Groups(DetailHandler):
fields = self.readFieldsFromParams(valid_fields)
is_pattern = fields.get('name', '').find('pat:') == 0
auth = parent.getInstance()
if not item: # Create new
if item is None: # Create new
if not is_meta and not is_pattern:
auth.createGroup(
fields
@@ -493,9 +482,7 @@ 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]] = []
@@ -516,9 +503,7 @@ 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))

View File

@@ -114,8 +114,8 @@ class BaseModelHandler(Handler):
'values': field.get('values', []),
},
}
if field.get('tab', None):
v['gui']['tab'] = _(field['tab'])
if 'tab' in field:
v['gui']['tab'] = field['tab']
gui.append(v)
return gui
@@ -268,11 +268,7 @@ class BaseModelHandler(Handler):
args: typing.Dict[str, str] = {}
try:
for key in fldList:
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]
args[key] = self._params[key]
# del self._params[key]
except KeyError as e:
raise RequestError('needed parameter not found in data {0}'.format(e))
@@ -679,9 +675,7 @@ class ModelHandler(BaseModelHandler):
detail: typing.ClassVar[
typing.Optional[typing.Dict[str, typing.Type[DetailHandler]]]
] = None # Dictionary containing detail routing
# 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
# Put needed fields
save_fields: typing.ClassVar[typing.List[str]] = []
# Put removable fields before updating
remove_fields: typing.ClassVar[typing.List[str]] = []
@@ -822,8 +816,7 @@ class ModelHandler(BaseModelHandler):
'Processing detail %s for with params %s', self._path, self._params
)
try:
item: models.Model = self.model.objects.get(uuid=self._args[0])
item: models.Model = self.model.objects.filter(uuid=self._args[0])[0]
# If we do not have access to parent to, at least, read...
if self._operation in ('put', 'post', 'delete'):
@@ -856,8 +849,6 @@ class ModelHandler(BaseModelHandler):
method = getattr(detail, self._operation)
return method()
except self.model.DoesNotExist:
raise self.invalidItemException()
except KeyError:
raise self.invalidMethodException()
except AttributeError:

View File

@@ -106,11 +106,8 @@ class ContentProcessor:
if isinstance(obj, (list, tuple, types.GeneratorType)):
return [ContentProcessor.procesForRender(v) for v in obj]
if isinstance(obj, (datetime.datetime,)):
if isinstance(obj, (datetime.datetime, datetime.date)):
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')

View File

@@ -71,7 +71,6 @@ 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
@@ -95,4 +94,5 @@ 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)

View File

@@ -56,7 +56,9 @@ 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
@@ -96,19 +98,14 @@ class InternalDBAuth(auths.Authenticator):
) # pylint: disable=maybe-no-member
if self.reverseDns.isTrue():
try:
return str(dns.resolver.query(dns.reversename.from_address(ip).to_text(), 'PTR')[0])
return str(
dns.resolver.query(dns.reversename.from_address(ip), '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.
@@ -116,37 +113,24 @@ 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:
username = username.lower()
def authenticate(
self, username: str, credentials: str, groupsManager: 'auths.GroupsManager'
) -> bool:
logger.debug('Username: %s, Password: %s', username, credentials)
dbAuth = self.dbAuthenticator()
try:
@@ -168,16 +152,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.lower(), state=State.ACTIVE)
user: 'models.User' = dbAuth.users.get(name=username, state=State.ACTIVE)
except Exception:
return
grps = [g.name for g in user.groups.all()]
groupsManager.validate(grps)
groupsManager.validate([g.name for g in user.groups.all()])
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.lower(), state=State.ACTIVE)
user = self.dbAuthenticator().users.get(name=username, state=State.ACTIVE)
return user.real_name or username
except Exception:
return super().getRealName(username)

View File

@@ -112,15 +112,6 @@ 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
@@ -135,27 +126,12 @@ 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, 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,
)
groups = connection.authenticate(username=username, password=credentials)
except Exception:
authLogLogin(getRequest(), self.dbAuthenticator(), username, 'Access denied by Raiuds')
return False
@@ -202,7 +178,7 @@ class RadiusAuth(auths.Authenticator):
try:
connection = self.radiusClient()
# Reply is not important...
connection.authenticate(cryptoManager().randomString(10), cryptoManager().randomString(10), mfaField=self.mfaAttr.value.strip())
connection.authenticate(cryptoManager().randomString(10), cryptoManager().randomString(10))
except client.RadiusAuthenticationError as e:
pass
except Exception:

View File

@@ -1,13 +1,16 @@
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
@@ -48,37 +51,6 @@ 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
@@ -96,23 +68,12 @@ 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 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:
def authenticate(self, username: str, password: str) -> typing.List[str]:
req: pyrad.packet.AuthPacket = self.radiusServer.CreateAuthPacket(
code=pyrad.packet.AccessRequest,
User_Name=username,
@@ -121,19 +82,9 @@ class RadiusClient:
req["User-Password"] = req.PwCrypt(password)
# Fill in extra fields
for k, v in kwargs.items():
req[k] = v
reply = typing.cast(pyrad.packet.AuthPacket, self.radiusServer.SendPacket(req))
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):
if reply.code != pyrad.packet.AccessAccept:
raise RadiusAuthenticationError('Access denied')
# User accepted, extract groups...
@@ -141,111 +92,10 @@ 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 ([], '', b'')
return []
# ...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])
return groups
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)

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2012-2022 Virtual Cable S.L.U.
# Copyright (c) 2012-2019 Virtual Cable S.L.
# 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.U. nor the names of its contributors
# * 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.
#
@@ -63,6 +63,7 @@ LDAP_RESULT_LIMIT = 100
class RegexLdap(auths.Authenticator):
host = gui.TextField(
length=64,
label=_('Host'),
@@ -81,7 +82,9 @@ 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,
@@ -92,7 +95,7 @@ class RegexLdap(auths.Authenticator):
tab=gui.CREDENTIALS_TAB,
)
password = gui.PasswordField(
length=32,
lenth=32,
label=_('Password'),
order=5,
tooltip=_('Password of the ldap user'),
@@ -103,32 +106,16 @@ class RegexLdap(auths.Authenticator):
length=3,
label=_('Timeout'),
defvalue='10',
order=10,
order=6,
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=20,
order=7,
tooltip=_('Common search base (used for "users" and "groups")'),
required=True,
tab=_('Ldap info'),
@@ -137,7 +124,7 @@ class RegexLdap(auths.Authenticator):
length=64,
label=_('User class'),
defvalue='posixAccount',
order=21,
order=8,
tooltip=_('Class for LDAP users (normally posixAccount)'),
required=True,
tab=_('Ldap info'),
@@ -146,7 +133,7 @@ class RegexLdap(auths.Authenticator):
length=64,
label=_('User Id Attr'),
defvalue='uid',
order=22,
order=9,
tooltip=_('Attribute that contains the user id'),
required=True,
tab=_('Ldap info'),
@@ -156,7 +143,7 @@ class RegexLdap(auths.Authenticator):
label=_('User Name Attr'),
multiline=2,
defvalue='uid',
order=23,
order=10,
tooltip=_(
'Attributes that contains the user name attributes or attribute patterns (one for each line)'
),
@@ -168,7 +155,7 @@ class RegexLdap(auths.Authenticator):
label=_('Group Name Attr'),
multiline=2,
defvalue='cn',
order=24,
order=11,
tooltip=_(
'Attribute that contains the group name attributes or attribute patterns (one for each line)'
),
@@ -181,22 +168,14 @@ class RegexLdap(auths.Authenticator):
length=64,
label=_('Alt. class'),
defvalue='',
order=25,
tooltip=_('Class for LDAP objects that will be also checked for groups retrieval (normally empty)'),
order=20,
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')
@@ -226,20 +205,19 @@ class RegexLdap(auths.Authenticator):
_groupNameAttr: str = ''
_userNameAttr: str = ''
_altClass: str = ''
_mfaAttr: str = ''
_verifySsl: bool = True
_certificate: str = ''
def initialize(self, values: typing.Optional[typing.Dict[str, str]]) -> None:
def __init__(
self,
dbAuth: 'models.Authenticator',
environment: 'Environment',
values: typing.Optional[typing.Dict[str, str]],
):
super().__init__(dbAuth, environment, values)
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'])
@@ -253,9 +231,6 @@ 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:
"""
@@ -281,46 +256,14 @@ class RegexLdap(auths.Authenticator):
attr = line[:equalPos]
else:
attr = line
# 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)
res.append(attr)
return res
def __processField(
self, field: str, attributes: typing.MutableMapping[str, typing.Any]
) -> typing.List[str]:
res: typing.List[str] = []
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)
logger.debug('Attributes: %s', attributes)
for line in field.splitlines():
equalPos = line.find('=')
if (
@@ -332,29 +275,27 @@ 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 = getAttr(attr)
val = attributes.get(attr, [])
if not isinstance(val, list): # May we have a single value
val = [val]
logger.debug('Pattern: %s', pattern)
for v in val:
try:
logger.debug('Pattern: %s on value %s', pattern, v)
searchResult = re.search(pattern, v, re.IGNORECASE) # @UndefinedVariable
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: # nosec: If not a valid regex, just ignore it
except Exception:
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,
@@ -369,15 +310,12 @@ 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(
[
'v5',
'v3',
self._host,
self._port,
gui.boolToStr(self._ssl),
@@ -390,67 +328,63 @@ 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[11:])
_regex, self._userNameAttr = vals[11:]
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)
self._groupNameAttr = self._groupNameAttr + '=' + _regex
self._userNameAttr = '\n'.join(self._userNameAttr.split(','))
elif vals[0] == 'v2':
logger.debug("Data v2: %s", vals[1:])
self._userNameAttr = vals[11]
(
self._host,
self._port,
ssl,
self._username,
self._password,
self._timeout,
self._ldapBase,
self._userClass,
self._userIdAttr,
self._groupNameAttr,
self._userNameAttr,
) = vals[1:]
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[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)
) = vals[1:]
self._ssl = gui.strToBool(ssl)
def __connection(self) -> typing.Any:
"""
@@ -458,7 +392,7 @@ class RegexLdap(auths.Authenticator):
@return: Connection established
@raise exception: If connection could not be established
"""
if self._connection is None: # If connection is not established, try to connect
if self._connection is None: # We want this method also to check credentials
self._connection = ldaputil.connection(
self._username,
self._password,
@@ -494,9 +428,6 @@ 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,
@@ -553,7 +484,9 @@ 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:
@@ -568,22 +501,21 @@ 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')
return False
# store the user mfa attribute if it is set
if self._mfaAttr:
self.storage.putPickle(
self.mfaStorageKey(username),
usr[self._mfaAttr][0],
authLogLogin(
getRequest(), self.dbAuthenticator(), username, 'Invalid password'
)
return False
groupsManager.validate(self.__getGroups(usr))
@@ -661,7 +593,9 @@ 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):
@@ -669,7 +603,9 @@ 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):
@@ -698,7 +634,9 @@ 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
@@ -711,7 +649,8 @@ 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,
)
)
@@ -720,7 +659,9 @@ 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
@@ -747,7 +688,9 @@ 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 (

View File

@@ -86,7 +86,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
tab=gui.CREDENTIALS_TAB,
)
password = gui.PasswordField(
length=32,
lenth=32,
label=_('Password'),
order=5,
tooltip=_('Password of the ldap user'),
@@ -97,35 +97,15 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=3,
label=_('Timeout'),
defvalue='10',
order=10,
order=6,
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=30,
order=7,
tooltip=_('Common search base (used for "users" and "groups")'),
required=True,
tab=_('Ldap info'),
@@ -134,7 +114,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=64,
label=_('User class'),
defvalue='posixAccount',
order=31,
order=8,
tooltip=_('Class for LDAP users (normally posixAccount)'),
required=True,
tab=_('Ldap info'),
@@ -143,7 +123,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=64,
label=_('User Id Attr'),
defvalue='uid',
order=32,
order=9,
tooltip=_('Attribute that contains the user id'),
required=True,
tab=_('Ldap info'),
@@ -152,7 +132,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=64,
label=_('User Name Attr'),
defvalue='uid',
order=33,
order=10,
tooltip=_(
'Attributes that contains the user name (list of comma separated values)'
),
@@ -163,7 +143,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=64,
label=_('Group class'),
defvalue='posixGroup',
order=34,
order=11,
tooltip=_('Class for LDAP groups (normally poxisGroup)'),
required=True,
tab=_('Ldap info'),
@@ -172,7 +152,7 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=64,
label=_('Group Id Attr'),
defvalue='cn',
order=35,
order=12,
tooltip=_('Attribute that contains the group id'),
required=True,
tab=_('Ldap info'),
@@ -181,25 +161,15 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
length=64,
label=_('Group membership attr'),
defvalue='memberUid',
order=36,
order=13,
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')
typeName = _('SimpleLDAP (DEPRECATED)')
typeType = 'SimpleLdapAuthenticator'
typeDescription = _('Simple LDAP authenticator')
typeDescription = _('Simple LDAP authenticator (DEPRECATED)')
iconFile = 'auth.png'
# If it has and external source where to get "new" users (groups must be declared inside UDS)
@@ -227,10 +197,6 @@ 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:
@@ -249,9 +215,6 @@ 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 {
@@ -268,15 +231,12 @@ 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(
[
'v2',
'v1',
self._host,
self._port,
gui.boolToStr(self._ssl),
@@ -290,59 +250,41 @@ 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')
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':
if vals[0] == 'v1':
logger.debug("Data: %s", vals[1:])
(
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 ''
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)
def __connection(
self
self,
username: typing.Optional[str] = None,
password: typing.Optional[str] = None,
):
"""
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 are not connected
if self._connection is None: # We want this method also to check credentials
self._connection = ldaputil.connection(
self._username,
self._password,
@@ -351,8 +293,6 @@ class SimpleLDAPAuthenticator(auths.Authenticator):
ssl=self._ssl,
timeout=int(self._timeout),
debug=False,
verify_ssl=self._verifySsl,
certificate=self._certificate,
)
return self._connection
@@ -366,8 +306,6 @@ 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]:
@@ -377,17 +315,13 @@ 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=attributes,
attributes=[i for i in self._userNameAttr.split(',') + [self._userIdAttr]],
sizeLimit=LDAP_RESULT_LIMIT,
)
@@ -483,13 +417,6 @@ 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

View File

@@ -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__) # type: ignore
pkgpath = os.path.dirname(sys.modules[__name__].__file__)
for _, name, _ in pkgutil.iter_modules([pkgpath]):
# __import__(name, globals(), locals(), [], 1)
importlib.import_module('.' + name, __name__) # import module

View File

@@ -50,7 +50,6 @@ 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
@@ -70,7 +69,6 @@ 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
@@ -88,9 +86,7 @@ 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', httponly=GlobalConfig.ENHANCED_SECURITY.getBool()
)
response.set_cookie('uds', cookie, samesite='Lax')
request.COOKIES['uds'] = cookie
else:
cookie = request.COOKIES['uds'][:UDS_COOKIE_LENGTH]
@@ -126,37 +122,32 @@ def getRootUser() -> User:
# Decorator to make easier protect pages that needs to be logged in
def webLoginRequired(
admin: typing.Union[bool, typing.Literal['admin']] = False
) -> typing.Callable[[typing.Callable[..., HttpResponse]], typing.Callable[..., HttpResponse]]:
"""Decorator to set protection to access page
admin: typing.Union[bool, str] = False
) -> typing.Callable[[typing.Callable[..., RT]], typing.Callable[..., RT]]:
"""
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[..., HttpResponse]) -> typing.Callable[..., HttpResponse]:
@wraps(view_func)
def _wrapped_view(request: 'ExtendedHttpRequest', *args, **kwargs) -> HttpResponse:
def decorator(view_func: typing.Callable[..., RT]) -> typing.Callable[..., RT]:
def _wrapped_view(request: 'ExtendedHttpRequest', *args, **kwargs) -> RT:
"""
Wrapped function for decorator
"""
# If no user or user authorization is not completed...
if not request.user or not request.authorized:
return HttpResponseRedirect(reverse('page.login'))
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 admin in (True, 'admin'):
if request.user.isStaff() is False or (admin == 'admin' and not request.user.is_admin):
return HttpResponseForbidden(_('Forbidden'))
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
return view_func(request, *args, **kwargs)
@@ -171,9 +162,12 @@ 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)
@@ -195,12 +189,12 @@ def trustedSourceRequired(view_func: typing.Callable[..., RT]) -> typing.Callabl
# decorator to deny non authenticated requests
# 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]:
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 or not request.authorized:
if not request.user:
return HttpResponseForbidden() # type: ignore
return view_func(request, *args, **kwargs)
@@ -229,7 +223,9 @@ 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,
@@ -257,7 +253,9 @@ 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 (
@@ -279,7 +277,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)
@@ -294,7 +292,9 @@ 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,8 +315,6 @@ def authenticateViaCallback(authenticator: Authenticator, params: typing.Any) ->
"""
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:
@@ -346,7 +344,7 @@ def authInfoUrl(authenticator: typing.Union[str, bytes, Authenticator]) -> str:
elif isinstance(authenticator, bytes):
name = authenticator.decode('utf8')
else:
name = typing.cast('Authenticator', authenticator).name
name = authenticator.name
return reverse('page.auth.info', kwargs={'authName': name})
@@ -363,7 +361,9 @@ 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,13 +372,10 @@ 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
@@ -402,27 +399,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, '_cryptedpass') and hasattr(request, '_scrambler'):
if hasattr(request, 'session'):
return cryptoManager().symDecrpyt(
getattr(request, '_cryptedpass'),
getattr(request, '_scrambler'),
)
return cryptoManager().symDecrpyt(
request.session.get(PASS_KEY, ''), getUDSCookie(request)
) # recover as original unicode string
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
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.
"""
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
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://')
try:
if request.user:
authenticator = request.user.manager.getInstance()
@@ -437,15 +434,14 @@ def webLogout(request: 'ExtendedHttpRequest', exit_url: typing.Optional[str] = N
srcip=request.ip,
)
else: # No user, redirect to /
return HttpResponseRedirect(exit_page)
return HttpResponseRedirect(reverse('page.login'))
except Exception:
raise
finally:
# Try to delete session
request.session.flush()
request.authorized = False
response = HttpResponseRedirect(exit_url)
response = HttpResponseRedirect(request.build_absolute_uri(exit_url))
if authenticator:
authenticator.webLogoutHook(username, request, response)
return response
@@ -475,11 +471,13 @@ def authLogLogin(
]
)
)
level = log.INFO if logStr in ('Logged in', 'Federated login') else log.ERROR
level = log.INFO if logStr == 'Logged in' 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,
)
@@ -503,4 +501,6 @@ 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
)

View File

@@ -290,25 +290,6 @@ 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:

View File

@@ -37,45 +37,27 @@ 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(AuthenticatorException):
class Redirect(Exception):
"""
This exception indicates that a redirect is required.
Used in authUrlCallback to indicate that redirect is needed
"""
pass
class Logout(AuthenticatorException):
class Logout(Exception):
"""
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

View File

@@ -30,7 +30,6 @@
"""
@author: Adolfo Gómez, dkmaster at dkmon dot com
"""
import typing
import logging
from uds.core.environment import Environmentable, Environment
@@ -44,11 +43,11 @@ class DelayedTask(Environmentable):
This is an object that represents an execution to be done "later"
"""
def __init__(self, environment: typing.Optional[Environment] = None) -> None:
def __init__(self):
"""
Remember to invoke parent init in derived clases using super(myClass,self).__init__() to let this initialize its own variables
"""
super().__init__(environment or Environment.getEnvForType(self.__class__))
super().__init__(Environment('DelayedTask'))
def execute(self) -> None:
"""

View File

@@ -139,7 +139,6 @@ class DelayedTaskRunner:
if taskInstance:
logger.debug('Executing delayedTask:>%s<', task)
# Re-create environment data
taskInstance.env = Environment.getEnvForType(taskInstance.__class__)
DelayedTaskThread(taskInstance).start()
@@ -147,13 +146,7 @@ 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(

View File

@@ -33,14 +33,11 @@ import hashlib
import array
import uuid
import codecs
import datetime
import struct
import re
import random
import string
import logging
import typing
import secrets
import time
from cryptography import x509
@@ -135,7 +132,9 @@ class CryptoManager(metaclass=singleton.Singleton):
modes.CBC(b'udsinitvectoruds'),
backend=default_backend(),
)
rndStr = secrets.token_bytes(16) # Same as block size of CBC (that is 16 here)
rndStr = self.randomString(
16
).encode() # 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]
@@ -144,7 +143,7 @@ class CryptoManager(metaclass=singleton.Singleton):
encoded = encryptor.update(toEncode) + encryptor.finalize()
if base64:
encoded = codecs.encode(encoded, 'base64').strip() # Return as bytes
return codecs.encode(encoded, 'base64') # Return as binary
return encoded
@@ -162,9 +161,7 @@ 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
@@ -174,13 +171,9 @@ 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]
@@ -234,30 +227,20 @@ class CryptoManager(metaclass=singleton.Singleton):
raise Exception('Invalid certificate')
def certificateString(self, certificate: str) -> str:
# 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)
return (
certificate.replace('-----BEGIN CERTIFICATE-----', '')
.replace('-----END CERTIFICATE-----', '')
.replace('\n', '')
)
def hash(self, value: typing.Union[str, bytes]) -> str:
if isinstance(value, str):
value = value.encode()
salt = self.salt(8) # 8 bytes = 16 chars
value = salt.encode() + value
if not value:
return ''
return '{SHA256SALT}' + salt + str(hashlib.sha3_256(value).hexdigest())
return '{SHA256}' + str(hashlib.sha3_256(value).hexdigest())
def checkHash(self, value: typing.Union[str, bytes], hash: str) -> bool:
if isinstance(value, str):
@@ -267,14 +250,9 @@ class CryptoManager(metaclass=singleton.Singleton):
return not hash
if hash[:8] == '{SHA256}':
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:])
return str(hashlib.sha3_256(value).hexdigest()) == hash[8:]
else: # Old sha1
return secrets.compare_digest(hash, str(hashlib.sha1(value).hexdigest())) # nosec: Old compatibility SHA1, not used anymore but need to be supported
return hash == str(hashlib.sha1(value).hexdigest())
def uuid(self, obj: typing.Any = None) -> str:
"""
@@ -295,12 +273,4 @@ 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(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()
return ''.join(random.SystemRandom().choices(base, k=length))

View File

@@ -60,25 +60,26 @@ class PublicationOldMachinesCleaner(DelayedTask):
This delayed task is for removing a pending "removable" publication
"""
def __init__(self, publicationId: int) -> None:
def __init__(self, publicationId: int):
super().__init__()
self._id = publicationId
def run(self) -> None:
def run(self):
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()
activePub: typing.Optional[
ServicePoolPublication
] = servicePoolPub.deployed_service.activePublication()
servicePoolPub.deployed_service.userServices.filter(in_use=True).update(
in_use=False, state_date=now
)
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)
servicePoolPub.deployed_service.markOldUserServicesAsRemovables(activePub)
except Exception:
pass
# Removed publication, no problem at all, just continue
@@ -99,7 +100,9 @@ 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
@@ -112,13 +115,16 @@ 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")
@@ -158,12 +164,18 @@ 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)
@@ -172,9 +184,13 @@ 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):
@@ -199,7 +215,9 @@ 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
@@ -214,17 +232,23 @@ 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',
@@ -246,7 +270,9 @@ 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
@@ -258,11 +284,15 @@ 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:
@@ -290,13 +320,17 @@ 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')
@@ -321,24 +355,35 @@ 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))

View File

@@ -36,7 +36,7 @@ import typing
from uds.core.util.config import GlobalConfig
from uds.core.util import singleton
from uds.models import StatsCounters, StatsCountersAccum
from uds.models import StatsCounters
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: typing.Type[typing.Union['StatsCounters', 'StatsEvents']]) -> None:
def __doCleanup(self, model):
minTime = time.mktime(
(
getSqlDatetime()
@@ -284,7 +284,3 @@ 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)

View File

@@ -39,7 +39,6 @@ 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 (
@@ -67,9 +66,6 @@ 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')
@@ -84,12 +80,14 @@ class UserServiceManager(metaclass=singleton.Singleton):
UserServiceManager()
) # Singleton pattern will return always the same instance
def getCacheStateFilter(self, servicePool: ServicePool, level: int) -> Q:
return Q(cache_level=level) & self.getStateFilter(servicePool.service)
@staticmethod
def getCacheStateFilter(servicePool: ServicePool, level: int) -> Q:
return Q(cache_level=level) & UserServiceManager.getStateFilter(servicePool)
def getStateFilter(self, service: 'models.Service') -> Q:
@staticmethod
def getStateFilter(servicePool: ServicePool) -> Q:
if (
service.getInstance().maxDeployed == services.Service.UNLIMITED
servicePool.service.getInstance().maxDeployed == services.Service.UNLIMITED
and GlobalConfig.MAX_SERVICES_COUNT_NEW.getBool() is False
):
states = [State.PREPARING, State.USABLE]
@@ -97,38 +95,23 @@ 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
"""
if self.maximumUserServicesDeployed(servicePool.service):
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:
raise MaxServicesReachedError(
_('Maximum number of user services reached for this {}').format(
servicePool
)
'Max number of allowed deployments for service reached'
)
def __createCacheAtDb(
@@ -200,7 +183,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
"""
Creates a new cache for the deployed service publication at level indicated
"""
logger.info(
logger.debug(
'Creating a new cache element at level %s for publication %s',
cacheLevel,
publication,
@@ -226,7 +209,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
if servicePool.service.getType().publicationType is not None:
publication = servicePool.activePublication()
logger.info(
logger.debug(
'Creating a new assigned element for user %s por publication %s',
user,
publication,
@@ -240,7 +223,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
)
)
else:
logger.info('Creating a new assigned element for user %s', user)
logger.debug('Creating a new assigned element for user %s', user)
assigned = self.__createAssignedAtDbForNoPublication(servicePool, user)
assignedInstance = assigned.getInstance()
@@ -324,7 +307,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
@return: the Uservice canceling
"""
userService.refresh_from_db()
logger.info('Canceling userService %s creation', userService)
logger.debug('Canceling userService %s creation', userService)
if userService.isPreparing() is False:
logger.info(
@@ -357,7 +340,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
"""
with transaction.atomic():
userService = UserService.objects.select_for_update().get(id=userService.id)
logger.info('Removing userService %a', userService)
logger.debug('Removing userService %a', userService)
if (
userService.isUsable() is False
and State.isRemovable(userService.state) is False
@@ -545,7 +528,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
if serviceType.usesCache:
inAssigned = (
servicePool.assignedUserServices()
.filter(self.getStateFilter(servicePool.service))
.filter(UserServiceManager.getStateFilter(servicePool))
.count()
)
if (
@@ -563,31 +546,26 @@ class UserServiceManager(metaclass=singleton.Singleton):
events.addEvent(servicePool, events.ET_CACHE_MISS, fld1=0)
return self.createAssignedFor(servicePool, user)
def getUserServicesInStatesForProvider(
self, provider: 'models.Provider', states: typing.List[str]
) -> int:
def getServicesInStateForProvider(self, provider_id: int, state: str) -> int:
"""
Returns the number of services of a service provider in the state indicated
"""
return UserService.objects.filter(
deployed_service__service__provider=provider, state__in=states
deployed_service__service__provider__id=provider_id, state=state
).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.getUserServicesInStatesForProvider(
servicePool.service.provider, [State.REMOVING]
removing = self.getServicesInStateForProvider(
servicePool.service.provider.id, State.REMOVING
)
serviceInstance = servicePool.service.getInstance()
if (
(removing >= serviceInstance.parent().getMaxRemovingServices()
and serviceInstance.parent().getIgnoreLimits() is False)
or servicePool.service.provider.isInMaintenance()
or servicePool.isRestrained()
removing >= serviceInstance.parent().getMaxRemovingServices()
and serviceInstance.parent().getIgnoreLimits() is False
):
return False
return True
@@ -596,12 +574,12 @@ class UserServiceManager(metaclass=singleton.Singleton):
"""
Checks if we can start a new service
"""
preparingForProvider = self.getUserServicesInStatesForProvider(
servicePool.service.provider, [State.PREPARING]
preparing = self.getServicesInStateForProvider(
servicePool.service.provider.id, State.PREPARING
)
serviceInstance = servicePool.service.getInstance()
if self.maximumUserServicesDeployed(servicePool.service) or (
preparingForProvider >= serviceInstance.parent().getMaxPreparingServices()
if (
preparing >= serviceInstance.parent().getMaxPreparingServices()
and serviceInstance.parent().getIgnoreLimits() is False
):
return False
@@ -782,7 +760,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 'meta')
return self.getMeta(user, srcIp, os, idService[1:], idTransport or '')
userService = self.locateUserService(user, idService, create=True)
@@ -940,7 +918,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
return metaId[0] == 'M'
def locateMetaService(
self, user: User, idService: str
self, user: User, idService: str, create: bool = False
) -> typing.Optional[UserService]:
kind, uuidMetapool = idService[0], idService[1:]
if kind != 'M':
@@ -990,7 +968,7 @@ class UserServiceManager(metaclass=singleton.Singleton):
# Get pool members. Just pools "visible" and "usable"
poolMembers = [
p for p in meta.members.filter(enabled=True) if p.pool.isVisible() and p.pool.isUsable()
p for p in meta.members.all() if p.pool.isVisible() and p.pool.isUsable()
]
# Sort pools based on meta selection
if meta.policy == MetaPool.PRIORITY_POOL:
@@ -1007,10 +985,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()[0] < 100 and p[1].isUsable()
p[1] for p in sortedPools if p[1].usage() < 100 and p[1].isUsable()
]
poolsFull: typing.List[ServicePool] = [
p[1] for p in sortedPools if p[1].usage()[0] == 100 and p[1].isUsable()
p[1] for p in sortedPools if p[1].usage() == 100 and p[1].isUsable()
]
logger.debug('Pools: %s/%s', pools, poolsFull)

View File

@@ -35,7 +35,7 @@ import tempfile
import logging
import typing
from uds.core.util.security import secureRequestsSession
import requests
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') or ''
# cert = '' # Uncomment to test without cert
cert = userService.getProperty('cert')
# cert = '' # Untils more tests, keep as previous.... TODO: Fix this when fully tested
if cert:
# Generate temp file, and delete it after
verify = tempfile.mktemp('udscrt')
@@ -99,11 +99,10 @@ def _requestActor(
f.write(cert.encode()) # Save cert
else:
verify = False
session = secureRequestsSession(verify=cert)
if data is None:
r = session.get(url, verify=verify, timeout=TIMEOUT)
r = requests.get(url, verify=verify, timeout=TIMEOUT)
else:
r = session.post(
r = requests.post(
url,
data=json.dumps(data),
headers={'content-type': 'application/json'},

View File

@@ -164,8 +164,7 @@ class UpdateFromPreparing(StateUpdater):
state = (
State.REMOVABLE
) # By default, if not valid publication, service will be marked for removal on preparation finished
osManager = self.userServiceInstance.osmanager()
if self.userService.isValidPublication() or (osManager and osManager.isPersistent()):
if self.userService.isValidPublication():
logger.debug('Publication is valid for %s', self.userService.friendly_name)
state = self.checkOsManagerRelated()

View File

@@ -1,44 +0,0 @@
# -*- 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