Compare commits

..

6 Commits
master ... v3.0

Author SHA1 Message Date
Adolfo Gómez García
e7216e8a24 updated user interface 2022-05-17 16:42:37 +02:00
Adolfo Gómez García
48557f96e4 Fixed assignement of new services if pool is at 100% usage 2021-11-30 12:18:15 +01:00
Adolfo Gómez García
263071750c Fixed logs removal 2021-09-02 13:23:04 +02:00
Adolfo Gómez García
4fed22d39d Merge branch 'v3.0' of github.com:dkmstr/openuds into v3.0 2021-08-25 12:49:08 +02:00
Adolfo Gómez García
24687fda2e Fixed configjs so disabled custom auths works in all cases 2021-08-25 12:48:54 +02:00
Adolfo Gómez García
51b0cec536 Upgraded git signatures outdated for RDP (thanks Dani por the report ;-) ) 2021-08-19 12:55:30 +02:00
1240 changed files with 109736 additions and 87922 deletions

7
.gitignore vendored
View File

@ -32,6 +32,9 @@
/client/administration/installer/UDSAdminInstaller/MSChart.exe /client/administration/installer/UDSAdminInstaller/MSChart.exe
/client/administration/installer/UDSAdminInstaller/UDSAdminSetup.exe /client/administration/installer/UDSAdminInstaller/UDSAdminSetup.exe
# /guacamole-tunnel/
/guacamole-tunnel/target
# /linuxActor/ # /linuxActor/
/linuxActor/udsactor_* /linuxActor/udsactor_*
@ -64,6 +67,8 @@
# /server/ # /server/
*_enterprise *_enterprise
/server/openuds.sublime-project
/server/openuds.sublime-workspace
# /server/src/ # /server/src/
/server/src/taskmanager.pid /server/src/taskmanager.pid
@ -86,6 +91,7 @@
# /server/src/uds/ # /server/src/uds/
/server/src/uds/*_enterprise.py /server/src/uds/*_enterprise.py
/server/src/uds/fixtures /server/src/uds/fixtures
/server/src/uds/tests
# /server/src/uds/auths/ # /server/src/uds/auths/
/server/src/uds/auths/*-enterprise /server/src/uds/auths/*-enterprise
@ -162,4 +168,3 @@
.vscode .vscode
.mypy_cache .mypy_cache
.pytest_cache

29
LICENSE
View File

@ -1,29 +0,0 @@
BSD 3-Clause License
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:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. 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.
3. Neither the name of the copyright holder 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.

View File

@ -10,6 +10,7 @@ OpenUDS (Universal Desktop Services) is a multiplatform connection broker for:
This is an Open Source Source project, initiated by Spanish Company Virtualcable and released Open Source with the help of several Spanish Universities. This is an Open Source Source project, initiated by Spanish Company Virtualcable and released Open Source with the help of several Spanish Universities.
Please fell free to contribute to this project. Any help provided will be welcome.
**Note: Master version is always under heavy development and it is not recommended for use, it will probably have unfixed bugs. Please use the latest stable branch.** **Note: Master version is always under heavy development and it is not recommended for use, it will probably have unfixed bugs.
For use, please use the latest stable branch.**

View File

@ -1 +1 @@
4.0.0 3.0.0

View File

@ -1,2 +0,0 @@
PYTHONPATH=./src:${PYTHONPATH}

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 | cat udsactor-template.spec |
sed -e s/"version 0.0.0"/"version ${VERSION}"/g | sed -e s/"version 0.0.0"/"version ${VERSION}"/g |
sed -e s/"release 1"/"release ${RELEASE}"/g > udsactor-$VERSION.spec 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 # Now fix dependencies for opensuse
# Note that, although on opensuse the library is "libXss1" on newer, # 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 # 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 udsactor-opensuse-$VERSION.spec; do
for pkg in udsactor-*$VERSION.spec; do for pkg in udsactor-$VERSION.spec; do
rm -rf rpm rm -rf rpm
for folder in SOURCES BUILD RPMS SPECS SRPMS; do for folder in SOURCES BUILD RPMS SPECS SRPMS; do

View File

@ -1,21 +1,3 @@
udsactor (4.0.0) stable; urgency=medium
* Upgraded to 4.0.0 release
-- Adolfo Gómez García <agomez@virtualcable.es> Fri, 1 Jul 2022 15:00:00 +0200
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
-- Adolfo Gómez García <agomez@virtualcable.es> Fri, 23 Oct 2020 8:00:00 +0200
udsactor (3.0.0) stable; urgency=medium udsactor (3.0.0) stable; urgency=medium
* Upgraded to 3.0.0 release * Upgraded to 3.0.0 release

View File

@ -10,7 +10,7 @@ Package: udsactor
Section: admin Section: admin
Priority: optional Priority: optional
Architecture: all Architecture: all
Depends: policykit-1(>=0.100), python3-requests (>=0.8.2), python3-pyqt5 (>=4.9), python3-six(>=1.1), python3 (>=3.6), libxss1, xscreensaver, ${misc:Depends} Depends: policykit-1(>=0.100), python3-requests (>=0.8.2), python3-pyqt5 (>=4.9), python3-six(>=1.1), python3 (>=3.4), libxss1, xscreensaver, ${misc:Depends}
Recommends: python3-prctl(>=1.1.1) Recommends: python3-prctl(>=1.1.1)
Description: Actor for Universal Desktop Services (UDS) Broker Description: Actor for Universal Desktop Services (UDS) Broker
This package provides the required components to allow managed machines to work on an environment managed by UDS Broker. This package provides the required components to allow managed machines to work on an environment managed by UDS Broker.
@ -19,7 +19,7 @@ Package: udsactor-unmanaged
Section: admin Section: admin
Priority: optional Priority: optional
Architecture: all Architecture: all
Depends: policykit-1(>=0.100), python3-requests (>=0.8.2), python3-pyqt5 (>=4.9), python3-six(>=1.1), python3 (>=3.6), libxss1, xscreensaver, ${misc:Depends} Depends: policykit-1(>=0.100), python3-requests (>=0.8.2), python3-pyqt5 (>=4.9), python3-six(>=1.1), python3 (>=3.4), libxss1, xscreensaver, ${misc:Depends}
Recommends: python3-prctl(>=1.1.1) Recommends: python3-prctl(>=1.1.1)
Description: Actor for Universal Desktop Services (UDS) Broker Static Unmanaged machines Description: Actor for Universal Desktop Services (UDS) Broker Static Unmanaged machines
This package provides the required components to allow unmanaged machines (static, independent machines) to work on an environment managed by UDS Broker. This package provides the required components to allow unmanaged machines (static, independent machines) to work on an environment managed by UDS Broker.

View File

@ -1,3 +1,3 @@
udsactor-unmanaged_3.6.0_all.deb admin optional udsactor-unmanaged_3.0.0_all.deb admin optional
udsactor_3.6.0_all.deb admin optional udsactor_3.0.0_all.deb admin optional
udsactor_3.6.0_amd64.buildinfo admin optional udsactor_3.0.0_amd64.buildinfo admin optional

View File

@ -3,4 +3,4 @@
FOLDER=/usr/share/UDSActor FOLDER=/usr/share/UDSActor
cd $FOLDER cd $FOLDER
exec python3 actor_config.py -platform xcb $@ exec python3 actor_config.py $@

View File

@ -3,4 +3,4 @@
FOLDER=/usr/share/UDSActor FOLDER=/usr/share/UDSActor
cd $FOLDER cd $FOLDER
exec python3 actor_config_unmanaged.py -platform xcb $@ exec python3 actor_config_unmanaged.py $@

View File

@ -3,4 +3,4 @@
FOLDER=/usr/share/UDSActor FOLDER=/usr/share/UDSActor
cd $FOLDER cd $FOLDER
exec python3 -s actor_client.py -platform xcb $@ exec python3 actor_client.py $@

View File

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>net.virtualcable.udsactor.server</string>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>ProgramArguments</key>
<array>
<string>/Applications/UDSActor.app/Contents/MacOS/udsactor</string>
<string>start</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardErrorPath</key>
<string>/var/log/udsactor.log</string>
<key>StandardOutPath</key>
<string>/var/log/nxserver.log</string>
<key>WorkingDirectory</key>
<string>/Applications/UDSActor.app/Contents/Resources/</string>
</dict>
</plist>

View File

@ -1 +0,0 @@
service file (net.virtualcable.udsactor.server.plist) goes in /Library/LaunchDaemons

View File

@ -29,32 +29,33 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
# pylint: disable=invalid-name
import sys import sys
import os import os
import PyQt5 # noqa import PyQt5 # pylint: disable=unused-import
from PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QMainWindow from PyQt5.QtWidgets import QMainWindow
from udsactor.log import logger, INFO from udsactor.log import logger, INFO
from udsactor.client import UDSClientQApp from udsactor.client import UDSClientQApp
from udsactor import platform from udsactor.platform import operations
if __name__ == "__main__": if __name__ == "__main__":
logger.setLevel(INFO) logger.setLevel(INFO)
# Ensure idle operations is initialized on start # Ensure idle operations is initialized on start
platform.operations.initIdleDuration(0) operations.initIdleDuration(0)
if platform.is_linux: if 'linux' in sys.platform:
os.environ['QT_X11_NO_MITSHM'] = '1' os.environ['QT_X11_NO_MITSHM'] = '1'
UDSClientQApp.setQuitOnLastWindowClosed(False) UDSClientQApp.setQuitOnLastWindowClosed(False)
qApp = UDSClientQApp(sys.argv) qApp = UDSClientQApp(sys.argv)
if platform.is_windows or platform.is_mac: if 'linux' not in sys.platform:
# The "hidden window" is not needed on linux # The "hidden window" is only needed to process events on Windows
# Not needed on Linux # Not needed on Linux
mw = QMainWindow() mw = QMainWindow()
mw.showMinimized() # Start minimized, will be hidden (not destroyed) as soon as qApp.init is invoked mw.showMinimized() # Start minimized, will be hidden (not destroyed) as soon as qApp.init is invoked
@ -66,9 +67,9 @@ if __name__ == "__main__":
# Note: Signals are only checked on python code execution, so we create a timer to force call back to python # Note: Signals are only checked on python code execution, so we create a timer to force call back to python
timer = QTimer(qApp) timer = QTimer(qApp)
timer.start(1000) timer.start(1000)
timer.timeout.connect(lambda *a: None) # type: ignore # timeout can be connected to a callable timer.timeout.connect(lambda *a: None)
qApp.exec() qApp.exec_()
# On windows, if no window is created, this point will never be reached. # On windows, if no window is created, this point will never be reached.
qApp.end() qApp.end()

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2020-2022 Virtual Cable S.L.U. # Copyright (c) 2020 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -35,7 +35,7 @@ import os
import logging import logging
import typing import typing
import PyQt5 # Ensures PyQt is included in the package import PyQt5 # pylint: disable=unused-import
from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog, QMessageBox from PyQt5.QtWidgets import QApplication, QDialog, QFileDialog, QMessageBox
import udsactor import udsactor
@ -187,9 +187,9 @@ if __name__ == "__main__":
app = QApplication(sys.argv) app = QApplication(sys.argv)
if udsactor.platform.operations.checkPermissions() is False: if udsactor.platform.operations.checkPermissions() is False:
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok) # type: ignore QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok)
sys.exit(1) sys.exit(1)
myapp = UDSConfigDialog() myapp = UDSConfigDialog()
myapp.show() myapp.show()
sys.exit(app.exec()) sys.exit(app.exec_())

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2020-2022 Virtual Cable S.L.U. # Copyright (c) 2020 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # 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, # * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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 # may be used to endorse or promote products derived from this software
# without specific prior written permission. # without specific prior written permission.
# #
@ -32,7 +32,7 @@
# pylint: disable=invalid-name # pylint: disable=invalid-name
import sys import sys
import os import os
import pickle # nosec: B403 import pickle
import logging import logging
import typing import typing
@ -40,7 +40,6 @@ import PyQt5 # pylint: disable=unused-import
from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox from PyQt5.QtWidgets import QApplication, QDialog, QMessageBox
import udsactor import udsactor
import udsactor.tools
from ui.setup_dialog_unmanaged_ui import Ui_UdsActorSetupDialog from ui.setup_dialog_unmanaged_ui import Ui_UdsActorSetupDialog
@ -50,7 +49,6 @@ if typing.TYPE_CHECKING:
logger = logging.getLogger('actor') logger = logging.getLogger('actor')
class UDSConfigDialog(QDialog): class UDSConfigDialog(QDialog):
_host: str = '' _host: str = ''
_config: udsactor.types.ActorConfigurationType _config: udsactor.types.ActorConfigurationType
@ -62,130 +60,91 @@ class UDSConfigDialog(QDialog):
self.ui = Ui_UdsActorSetupDialog() self.ui = Ui_UdsActorSetupDialog()
self.ui.setupUi(self) self.ui.setupUi(self)
self.ui.host.setText(self._config.host) self.ui.host.setText(self._config.host)
self.ui.validateCertificate.setCurrentIndex( self.ui.validateCertificate.setCurrentIndex(1 if self._config.validateCertificate else 0)
1 if self._config.validateCertificate else 0
)
self.ui.logLevelComboBox.setCurrentIndex(self._config.log_level) self.ui.logLevelComboBox.setCurrentIndex(self._config.log_level)
self.ui.serviceToken.setText(self._config.master_token or '') self.ui.serviceToken.setText(self._config.master_token)
self.ui.restrictNet.setText(self._config.restrict_net or '')
self.ui.testButton.setEnabled( self.ui.testButton.setEnabled(bool(self._config.master_token and self._config.host))
bool(self._config.master_token and self._config.host)
)
@property @property
def api(self) -> udsactor.rest.UDSServerApi: def api(self) -> udsactor.rest.UDSServerApi:
return udsactor.rest.UDSServerApi( return udsactor.rest.UDSServerApi(self.ui.host.text(), self.ui.validateCertificate.currentIndex() == 1)
self.ui.host.text(), self.ui.validateCertificate.currentIndex() == 1
)
def finish(self) -> None: def finish(self) -> None:
self.close() self.close()
def configChanged(self, text: str) -> None: def configChanged(self, text: str) -> None:
self.ui.testButton.setEnabled( self.ui.testButton.setEnabled(self.ui.host.text() == self._config.host and self.ui.serviceToken.text() == self._config.master_token)
self.ui.host.text() == self._config.host
and self.ui.serviceToken.text() == self._config.master_token
and self.ui.restrictNet.text() == self._config.restrict_net
)
def testUDSServer(self) -> None: def testUDSServer(self) -> None:
if not self._config.master_token or not self._config.host: if not self._config.master_token or not self._config.host:
self.ui.testButton.setEnabled(False) self.ui.testButton.setEnabled(False)
return return
try: try:
api = udsactor.rest.UDSServerApi( api = udsactor.rest.UDSServerApi(self._config.host, self._config.validateCertificate)
self._config.host, self._config.validateCertificate
)
if not api.test(self._config.master_token, udsactor.types.UNMANAGED): if not api.test(self._config.master_token, udsactor.types.UNMANAGED):
QMessageBox.information( QMessageBox.information(
self, self,
'UDS Test', 'UDS Test',
'Service token seems to be invalid . Please, check token validity.', 'Service token seems to be invalid . Please, check token validity.',
QMessageBox.Ok, # type: ignore QMessageBox.Ok
) )
else: else:
QMessageBox.information( QMessageBox.information(
self, self,
'UDS Test', 'UDS Test',
'Configuration for {} seems to be correct.'.format( 'Configuration for {} seems to be correct.'.format(self._config.host),
self._config.host QMessageBox.Ok
),
QMessageBox.Ok, # type: ignore
) )
except Exception: except Exception:
QMessageBox.information( QMessageBox.information(
self, self,
'UDS Test', 'UDS Test',
'Configured host {} seems to be inaccesible.'.format(self._config.host), 'Configured host {} seems to be inaccesible.'.format(self._config.host),
QMessageBox.Ok, # type: ignore QMessageBox.Ok
) )
def saveConfig(self) -> None: def saveConfig(self) -> None:
# Ensure restrict_net is empty or a valid subnet
restrictNet = self.ui.restrictNet.text().strip()
if restrictNet:
try:
subnet = udsactor.tools.strToNoIPV4Network(restrictNet)
if not subnet:
raise Exception('Invalid subnet')
except Exception:
QMessageBox.information(
self,
'Invalid subnet',
'Invalid subnet {}. Please, check it.'.format(restrictNet),
QMessageBox.Ok, # type: ignore
)
return
# Store parameters on register for later use, notify user of registration # Store parameters on register for later use, notify user of registration
self._config = udsactor.types.ActorConfigurationType( self._config = udsactor.types.ActorConfigurationType(
actorType=udsactor.types.UNMANAGED, actorType=udsactor.types.UNMANAGED,
host=self.ui.host.text(), host=self.ui.host.text(),
validateCertificate=self.ui.validateCertificate.currentIndex() == 1, validateCertificate=self.ui.validateCertificate.currentIndex() == 1,
master_token=self.ui.serviceToken.text().strip(), master_token=self.ui.serviceToken.text(),
restrict_net=restrictNet, log_level=self.ui.logLevelComboBox.currentIndex()
log_level=self.ui.logLevelComboBox.currentIndex(),
) )
udsactor.platform.store.writeConfig(self._config) udsactor.platform.store.writeConfig(self._config)
# Enables test button # Enables test button
self.ui.testButton.setEnabled(True) self.ui.testButton.setEnabled(True)
# Informs the user # Informs the user
QMessageBox.information( QMessageBox.information(self, 'UDS Configuration', 'Configuration saved.', QMessageBox.Ok)
self,
'UDS Configuration',
'Configuration saved.',
QMessageBox.Ok, # type: ignore
)
if __name__ == "__main__": if __name__ == "__main__":
# If run as "sudo" on linux, we will need this to avoid problems # If to be run as "sudo" on linux, we will need this to avoid problems
if 'linux' in sys.platform: if 'linux' in sys.platform:
os.environ['QT_X11_NO_MITSHM'] = '1' os.environ['QT_X11_NO_MITSHM'] = '1'
app = QApplication(sys.argv) app = QApplication(sys.argv)
if udsactor.platform.operations.checkPermissions() is False: if udsactor.platform.operations.checkPermissions() is False:
QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok) # type: ignore QMessageBox.critical(None, 'UDS Actor', 'This Program must be executed as administrator', QMessageBox.Ok)
sys.exit(1) sys.exit(1)
if len(sys.argv) > 2: if len(sys.argv) > 2:
if sys.argv[1] == 'export': if sys.argv[1] == 'export':
try: try:
with open(sys.argv[2], 'wb') as export_: with open(sys.argv[2], 'wb') as f:
pickle.dump( pickle.dump(udsactor.platform.store.readConfig(), f, protocol=3)
udsactor.platform.store.readConfig(), export_, protocol=3
)
except Exception as e: except Exception as e:
print('Error exporting configuration file: {}'.format(e)) print('Error exporting configuration file: {}'.format(e))
sys.exit(1) sys.exit(1)
sys.exit(0) sys.exit(0)
elif sys.argv[1] == 'import': if sys.argv[1] == 'import':
try: try:
with open(sys.argv[2], 'rb') as import_: with open(sys.argv[2], 'rb') as f:
config = pickle.load(import_) # nosec: B301: the file is provided by user, so it's not a security issue config = pickle.load(f)
udsactor.platform.store.writeConfig(config) udsactor.platform.store.writeConfig(config)
except Exception as e: except Exception as e:
print('Error importing configuration file: {}'.format(e)) print('Error importing configuration file: {}'.format(e))
@ -194,4 +153,4 @@ if __name__ == "__main__":
myapp = UDSConfigDialog() myapp = UDSConfigDialog()
myapp.show() myapp.show()
sys.exit(app.exec()) sys.exit(app.exec_())

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2020-2022 Virtual Cable S.L.U. # Copyright (c) 2020 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # 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, # * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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 # may be used to endorse or promote products derived from this software
# without specific prior written permission. # without specific prior written permission.
# #
@ -29,8 +29,12 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
from udsactor import platform import sys
if sys.platform == 'win32':
from udsactor.windows import runner
else:
from udsactor.linux import runner
if __name__ == "__main__": if __name__ == "__main__":
platform.runner.run() runner.run()

View File

@ -10,8 +10,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>601</width> <width>595</width>
<height>243</height> <height>220</height>
</rect> </rect>
</property> </property>
<property name="sizePolicy"> <property name="sizePolicy">
@ -55,7 +55,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>10</x> <x>10</x>
<y>210</y> <y>180</y>
<width>181</width> <width>181</width>
<height>23</height> <height>23</height>
</rect> </rect>
@ -83,7 +83,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>410</x> <x>410</x>
<y>210</y> <y>180</y>
<width>171</width> <width>171</width>
<height>23</height> <height>23</height>
</rect> </rect>
@ -117,7 +117,7 @@
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>210</x> <x>210</x>
<y>210</y> <y>180</y>
<width>181</width> <width>181</width>
<height>23</height> <height>23</height>
</rect> </rect>
@ -144,7 +144,7 @@
<x>10</x> <x>10</x>
<y>10</y> <y>10</y>
<width>571</width> <width>571</width>
<height>191</height> <height>161</height>
</rect> </rect>
</property> </property>
<layout class="QFormLayout" name="formLayout"> <layout class="QFormLayout" name="formLayout">
@ -214,21 +214,21 @@
<item row="2" column="1"> <item row="2" column="1">
<widget class="QLineEdit" name="serviceToken"> <widget class="QLineEdit" name="serviceToken">
<property name="toolTip"> <property name="toolTip">
<string>UDS Service Token</string> <string>UDS user with administration rights (Will not be stored on template)</string>
</property> </property>
<property name="whatsThis"> <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> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_loglevel"> <widget class="QLabel" name="label_loglevel">
<property name="text"> <property name="text">
<string>Log Level</string> <string>Log Level</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="3" column="1">
<widget class="QComboBox" name="logLevelComboBox"> <widget class="QComboBox" name="logLevelComboBox">
<property name="currentIndex"> <property name="currentIndex">
<number>1</number> <number>1</number>
@ -258,23 +258,6 @@
</item> </item>
</widget> </widget>
</item> </item>
<item row="3" column="0">
<widget class="QLabel" name="label_restrictNet">
<property name="text">
<string>Restrict Net</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="restrictNet">
<property name="toolTip">
<string>Restrict valid detection of network interfaces to this network.</string>
</property>
<property name="whatsThis">
<string>&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>
</property>
</widget>
</item>
</layout> </layout>
<zorder>label_host</zorder> <zorder>label_host</zorder>
<zorder>host</zorder> <zorder>host</zorder>
@ -284,8 +267,6 @@
<zorder>label_security</zorder> <zorder>label_security</zorder>
<zorder>label_loglevel</zorder> <zorder>label_loglevel</zorder>
<zorder>logLevelComboBox</zorder> <zorder>logLevelComboBox</zorder>
<zorder>label_restrictNet</zorder>
<zorder>restrictNet</zorder>
</widget> </widget>
</widget> </widget>
<resources> <resources>
@ -372,22 +353,6 @@
</hint> </hint>
</hints> </hints>
</connection> </connection>
<connection>
<sender>restrictNet</sender>
<signal>textChanged(QString)</signal>
<receiver>UdsActorSetupDialog</receiver>
<slot>configChanged()</slot>
<hints>
<hint type="sourcelabel">
<x>341</x>
<y>139</y>
</hint>
<hint type="destinationlabel">
<x>295</x>
<y>121</y>
</hint>
</hints>
</connection>
</connections> </connections>
<slots> <slots>
<slot>finish()</slot> <slot>finish()</slot>

View File

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

View File

@ -65,9 +65,9 @@ class UDSClientQApp(QApplication):
self._initialized = False self._initialized = False
# This will be invoked on session close # This will be invoked on session close
self.commitDataRequest.connect(self.end) # type: ignore # Will be invoked on session close, to gracely close app self.commitDataRequest.connect(self.end) # Will be invoked on session close, to gracely close app
# self.aboutToQuit.connect(self.end) # self.aboutToQuit.connect(self.end)
self.message.connect(self.showMessage) # type: ignore # there are problems with Pylance and connects on PyQt5... :) self.message.connect(self.showMessage)
# Execute backgroup thread for actions # Execute backgroup thread for actions
self._app = UDSActorClient(self) self._app = UDSActorClient(self)
@ -94,7 +94,7 @@ class UDSClientQApp(QApplication):
self._app.join() self._app.join()
def showMessage(self, message: str) -> None: def showMessage(self, message: str) -> None:
QMessageBox.information(None, 'Message', message) # type: ignore QMessageBox.information(None, 'Message', message)
def setMainWindow(self, mw: 'QMainWindow'): def setMainWindow(self, mw: 'QMainWindow'):
self._mainWindow = mw self._mainWindow = mw
@ -108,7 +108,6 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
_listener: client.HTTPServerThread _listener: client.HTTPServerThread
_loginInfo: typing.Optional['types.LoginResultInfoType'] _loginInfo: typing.Optional['types.LoginResultInfoType']
_notified: bool _notified: bool
_notifiedDeadline: bool
_sessionStartTime: datetime.datetime _sessionStartTime: datetime.datetime
api: rest.UDSClientApi api: rest.UDSClientApi
@ -116,14 +115,13 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
super().__init__() super().__init__()
self.api = rest.UDSClientApi() # Self initialized self.api = rest.UDSClientApi() # Self initialized
self._qApp = typing.cast(UDSClientQApp, qApp) self._qApp = qApp
self._running = False self._running = False
self._forceLogoff = False self._forceLogoff = False
self._extraLogoff = '' self._extraLogoff = ''
self._listener = client.HTTPServerThread(self) self._listener = client.HTTPServerThread(self)
self._loginInfo = None self._loginInfo = None
self._notified = False self._notified = False
self._notifiedDeadline = False
# Capture stop signals.. # Capture stop signals..
logger.debug('Setting signals...') logger.debug('Setting signals...')
@ -141,8 +139,8 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
remainingTime = self._loginInfo.dead_line - (datetime.datetime.now() - self._sessionStartTime).total_seconds() remainingTime = self._loginInfo.dead_line - (datetime.datetime.now() - self._sessionStartTime).total_seconds()
logger.debug('Remaining time: {}'.format(remainingTime)) logger.debug('Remaining time: {}'.format(remainingTime))
if not self._notifiedDeadline and remainingTime < 300: # With five minutes, show a warning message if not self._notified and remainingTime < 300: # With five minutes, show a warning message
self._notifiedDeadline = True self._notified = True
self._showMessage('Your session will expire in less that 5 minutes. Please, save your work and disconnect.') self._showMessage('Your session will expire in less that 5 minutes. Please, save your work and disconnect.')
return return
@ -185,8 +183,7 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
try: try:
# Notify loging and mark it # Notify loging and mark it
user, sessionType = platform.operations.getCurrentUser(), platform.operations.getSessionType() self._loginInfo = self.api.login(platform.operations.getCurrentUser(), platform.operations.getSessionType())
self._loginInfo = self.api.login(user, sessionType)
if self._loginInfo.max_idle: if self._loginInfo.max_idle:
platform.operations.initIdleDuration(self._loginInfo.max_idle) platform.operations.initIdleDuration(self._loginInfo.max_idle)
@ -196,13 +193,10 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
self.checkIdle() self.checkIdle()
self.checkDeadLine() self.checkDeadLine()
time.sleep(1.22) # Sleeps between loop iterations time.sleep(1.3) # Sleeps between loop iterations
self.api.logout(user + self._extraLogoff, sessionType)
logger.info('Notified logout for %s (%s)', user, sessionType) # Log logout
# Clean up login info
self._loginInfo = None self._loginInfo = None
self.api.logout(platform.operations.getCurrentUser() + self._extraLogoff)
except Exception as e: except Exception as e:
logger.error('Error on client loop: %s', e) logger.error('Error on client loop: %s', e)
@ -216,7 +210,7 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
platform.operations.loggoff() platform.operations.loggoff()
def _showMessage(self, message: str) -> None: def _showMessage(self, message: str) -> None:
self._qApp.message.emit(message) # type: ignore # there are problems with Pylance and connects on PyQt5... :) self._qApp.message.emit(message)
def stop(self) -> None: def stop(self) -> None:
logger.debug('Stopping client Service') logger.debug('Stopping client Service')
@ -236,13 +230,13 @@ class UDSActorClient(threading.Thread): # pylint: disable=too-many-instance-att
On windows, an RDP session with minimized screen will render "black screen" On windows, an RDP session with minimized screen will render "black screen"
So only when user is using RDP connection will return an "actual" screenshot So only when user is using RDP connection will return an "actual" screenshot
''' '''
pixmap: 'QPixmap' = self._qApp.primaryScreen().grabWindow(0) # type: ignore pixmap: 'QPixmap' = self._qApp.primaryScreen().grabWindow(0)
ba = QByteArray() ba = QByteArray()
buffer = QBuffer(ba) buffer = QBuffer(ba)
buffer.open(QIODevice.WriteOnly) # type: ignore buffer.open(QIODevice.WriteOnly)
pixmap.save(buffer, 'PNG') pixmap.save(buffer, 'PNG')
buffer.close() buffer.close()
scrBase64 = bytes(ba.toBase64()).decode() # type: ignore # there are problems with Pylance and connects on PyQt5... :) scrBase64 = bytes(ba.toBase64()).decode()
logger.debug('Screenshot length: %s', len(scrBase64)) logger.debug('Screenshot length: %s', len(scrBase64))
return scrBase64 # 'result' of JSON will contain base64 of screen return scrBase64 # 'result' of JSON will contain base64 of screen

View File

@ -132,7 +132,7 @@ class HTTPServerThread(threading.Thread):
self._app = app self._app = app
self.port = -1 self.port = -1
self.id = secrets.token_urlsafe(24) self.id = secrets.token_urlsafe(16)
@property @property
def url(self) -> str: def url(self) -> str:

View File

@ -33,8 +33,8 @@ import json
import typing import typing
import requests import requests
from udsactor import tools, types
from udsactor.log import logger from ..log import logger
# For avoid proxy on localhost connections # For avoid proxy on localhost connections
NO_PROXY = { NO_PROXY = {
@ -42,108 +42,55 @@ NO_PROXY = {
'https': None, 'https': None,
} }
class UDSActorClientPool:
class UDSActorClientPool(metaclass=tools.Singleton): _clientUrl: typing.List[str]
_clients: typing.List[types.ClientInfo]
def __init__(self) -> None: def __init__(self) -> None:
self._clients = [] self._clientUrl = []
def _post( def _post(self, method: str, data: typing.MutableMapping[str, str], timeout=2) -> typing.List[requests.Response]:
self, removables: typing.List[str] = []
session_id: typing.Optional[str], result: typing.List[typing.Any] = []
method: str, for clientUrl in self._clientUrl:
data: typing.MutableMapping[str, str],
timeout: int = 2,
) -> typing.List[
typing.Tuple[types.ClientInfo, typing.Optional[requests.Response]]
]:
result: typing.List[
typing.Tuple[types.ClientInfo, typing.Optional[requests.Response]]
] = []
for client in self._clients:
# Skip if session id is provided but does not match
if session_id and client.session_id != session_id:
continue
clientUrl = client.url
try: try:
result.append( result.append(requests.post(clientUrl + '/' + method, data=json.dumps(data), verify=False, timeout=timeout, proxies=NO_PROXY))
(
client,
requests.post(
clientUrl + '/' + method,
data=json.dumps(data),
verify=False,
timeout=timeout,
proxies=NO_PROXY, # type: ignore
),
)
)
except Exception as e: except Exception as e:
logger.info( # If cannot request to a clientUrl, remove it from list
'Could not connect with client %s: %s. ', logger.info('Could not connect with client %s: %s. Removed from registry.', e, clientUrl)
e, removables.append(clientUrl)
clientUrl,
) # Remove failed connections
result.append((client, None)) for clientUrl in removables:
self.unregister(clientUrl)
return result return result
@property def register(self, clientUrl: str) -> None:
def clients(self) -> typing.List[types.ClientInfo]:
return self._clients
def register(self, client_url: str) -> None:
# Remove first if exists, to avoid duplicates # Remove first if exists, to avoid duplicates
self.unregister(client_url) self.unregister(clientUrl)
# And add it again # And add it again
self._clients.append(types.ClientInfo(client_url, '')) self._clientUrl.append(clientUrl)
def set_session_id(self, client_url: str, session_id: typing.Optional[str]) -> None: def unregister(self, clientUrl: str) -> None:
"""Set the session id for a client self._clientUrl = list((i for i in self._clientUrl if i != clientUrl))
Args: def executeScript(self, script: str) -> None:
clientUrl (str): _description_ self._post('script', {'script': script}, timeout=30)
session_id (str): _description_
"""
for client in self._clients:
if client.url == client_url:
# remove existing client from list, create a new one and insert it
self._clients.remove(client)
self._clients.append(types.ClientInfo(client_url, session_id or ''))
break
def unregister(self, client_url: str) -> None: def logout(self) -> None:
# remove client url from array if found self._post('logout', {})
for i, client in enumerate(self._clients):
if client.url == client_url:
self._clients.pop(i)
return
def executeScript(self, session_id: typing.Optional[str], script: str) -> None: def message(self, message: str) -> None:
self._post(session_id, 'script', {'script': script}, timeout=30) self._post('message', {'message': message})
def logout(self, session_id: typing.Optional[str]) -> None: def ping(self) -> bool:
self._post(session_id, 'logout', {}) if not self._clientUrl:
return True # No clients, ping ok
self._post('ping', {}, timeout=1)
return bool(self._clientUrl) # There was clients, but they are now lost!!!
def message(self, session_id: typing.Optional[str], message: str) -> None: def screenshot(self) -> typing.Optional[str]: # Screenshot are returned as base64
self._post(session_id, 'message', {'message': message}) for r in self._post('screenshot', {}, timeout=3):
def lost_clients(
self,
session_id: typing.Optional[str] = None,
) -> typing.Iterable[types.ClientInfo]: # returns the list of "lost" clients
# Port ping to every client
for i in self._post(session_id, 'ping', {}, timeout=1):
if i[1] is None:
yield i[0]
def screenshot(
self, session_id: typing.Optional[str]
) -> typing.Optional[str]: # Screenshot are returned as base64
for client, r in self._post(session_id, 'screenshot', {}, timeout=3):
if not r:
continue # Missing client, so we ignore it
try: try:
return r.json()['result'] return r.json()['result']
except Exception: except Exception:

View File

@ -30,23 +30,19 @@
''' '''
import typing import typing
from udsactor.http import handler, clients_pool from . import handler
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from udsactor.service import CommonService from ..service import CommonService
class LocalProvider(handler.Handler): class LocalProvider(handler.Handler):
def post_login(self) -> typing.Any: def post_login(self) -> typing.Any:
result = self._service.login(self._params['username'], self._params['session_type']) result = self._service.login(self._params['username'], self._params['session_type'])
# if callback_url is provided, record it in the clients pool
if 'callback_url' in self._params and result.session_id:
# If no session id is returned, then no login is acounted for
clients_pool.UDSActorClientPool().set_session_id(self._params['callback_url'], result.session_id)
return result._asdict() return result._asdict()
def post_logout(self) -> typing.Any: def post_logout(self) -> typing.Any:
self._service.logout(self._params['username'], self._params['session_type'], self._params['session_id']) self._service.logout(self._params['username'])
return 'ok' return 'ok'
def post_ping(self) -> typing.Any: def post_ping(self) -> typing.Any:

View File

@ -38,7 +38,6 @@ from ..log import logger
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from ..service import CommonService from ..service import CommonService
class PublicProvider(handler.Handler): class PublicProvider(handler.Handler):
def post_logout(self) -> typing.Any: def post_logout(self) -> typing.Any:
logger.debug('Sending LOGOFF to clients') logger.debug('Sending LOGOFF to clients')
@ -52,9 +51,7 @@ class PublicProvider(handler.Handler):
logger.debug('Sending MESSAGE to clients') logger.debug('Sending MESSAGE to clients')
if 'message' not in self._params: if 'message' not in self._params:
raise Exception('Invalid message parameters') raise Exception('Invalid message parameters')
self._service._clientsPool.message( self._service._clientsPool.message(self._params['message']) # pylint: disable=protected-access
self._params['message']
) # pylint: disable=protected-access
return 'ok' return 'ok'
def post_script(self) -> typing.Any: def post_script(self) -> typing.Any:
@ -63,9 +60,7 @@ class PublicProvider(handler.Handler):
raise Exception('Invalid script parameters') raise Exception('Invalid script parameters')
if self._params.get('user', False): if self._params.get('user', False):
logger.debug('Sending SCRIPT to client') logger.debug('Sending SCRIPT to client')
self._service._clientsPool.executeScript( self._service._clientsPool.executeScript(self._params['script']) # pylint: disable=protected-access
self._params['script']
) # pylint: disable=protected-access
else: else:
# Execute script at server space, that is, here # Execute script at server space, that is, here
# as a parallel thread # as a parallel thread
@ -77,22 +72,14 @@ class PublicProvider(handler.Handler):
logger.debug('Received Pre connection') logger.debug('Received Pre connection')
if 'user' not in self._params or 'protocol' not in self._params: if 'user' not in self._params or 'protocol' not in self._params:
raise Exception('Invalid preConnect parameters') raise Exception('Invalid preConnect parameters')
return self._service.preConnect( return self._service.preConnect(self._params['user'], self._params['protocol'], self._params.get('ip', 'unknown'), self._params.get('hostname', 'unknown'))
self._params['user'],
self._params['protocol'],
self._params.get('ip', 'unknown'),
self._params.get('hostname', 'unknown'),
self._params.get('udsuser', 'unknown'),
)
def get_information(self) -> typing.Any: def get_information(self) -> typing.Any:
# Return something useful? :) # Return something useful? :)
return 'UDS Actor Secure Server' return 'UDS Actor Secure Server'
def get_screenshot(self) -> typing.Any: def get_screenshot(self) -> typing.Any:
return ( return self._service._clientsPool.screenshot() # pylint: disable=protected-access
self._service._clientsPool.screenshot()
) # pylint: disable=protected-access
def get_uuid(self) -> typing.Any: def get_uuid(self) -> typing.Any:
if self._service.isManaged(): if self._service.isManaged():

View File

@ -71,7 +71,7 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
# Very simple path & params splitter # Very simple path & params splitter
path = self.path.split('?')[0][1:].split('/') 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 handlerType: typing.Optional[typing.Type['Handler']] = None
@ -159,7 +159,7 @@ class HTTPServerThread(threading.Thread):
# self._server.socket = ssl.wrap_socket(self._server.socket, certfile=self.certFile, server_side=True) # self._server.socket = ssl.wrap_socket(self._server.socket, certfile=self.certFile, server_side=True)
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
# context.options = ssl.CERT_NONE context.options = ssl.CERT_NONE
context.load_cert_chain(certfile=self._certFile, password=password) context.load_cert_chain(certfile=self._certFile, password=password)
self._server.socket = context.wrap_socket(self._server.socket, server_side=True) self._server.socket = context.wrap_socket(self._server.socket, server_side=True)

View File

@ -0,0 +1 @@
VERSION = '3.0.0'

View File

@ -101,7 +101,7 @@ class Daemon:
def removePidFile(self) -> None: def removePidFile(self) -> None:
try: try:
os.remove(self.pidfile) os.remove(self.pidfile)
except Exception: # nosec: Not interested in exception except Exception:
# Not found/not permissions or whatever, ignore it # Not found/not permissions or whatever, ignore it
pass pass

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014-2022 Virtual Cable S.L.U. # Copyright (c) 2014-2019 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -11,7 +11,7 @@
# * Redistributions in binary form must reproduce the above copyright notice, # * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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 # may be used to endorse or promote products derived from this software
# without specific prior written permission. # without specific prior written permission.
# #
@ -35,9 +35,8 @@ import logging
import typing import typing
class LocalLogger: # pylint: disable=too-few-public-methods class LocalLogger: # pylint: disable=too-few-public-methods
linux = True linux = False
windows = False windows = True
serviceLogger = False
logger: typing.Optional[logging.Logger] logger: typing.Optional[logging.Logger]
@ -59,8 +58,7 @@ class LocalLogger: # pylint: disable=too-few-public-methods
self.logger = logging.getLogger('udsactor') self.logger = logging.getLogger('udsactor')
os.chmod(fname, 0o0600) os.chmod(fname, 0o0600)
return return
except Exception: # nosec: B110: we don't care about exceptions here except Exception:
# Ignore and try next
pass pass
# Logger can't be set # Logger can't be set

View File

@ -34,7 +34,7 @@ import platform
import socket import socket
import fcntl # Only available on Linux. Expect complains if edited from windows import fcntl # Only available on Linux. Expect complains if edited from windows
import os import os
import subprocess # nosec import subprocess
import struct import struct
import array import array
import typing import typing
@ -53,9 +53,7 @@ def _getMacAddr(ifname: str) -> typing.Optional[str]:
ifnameBytes = ifname.encode('utf-8') ifnameBytes = ifname.encode('utf-8')
try: try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
info = bytearray( info = bytearray(fcntl.ioctl(s.fileno(), 0x8927, struct.pack(str('256s'), ifnameBytes[:15])))
fcntl.ioctl(s.fileno(), 0x8927, struct.pack(str('256s'), ifnameBytes[:15]))
)
return str(''.join(['%02x:' % char for char in info[18:24]])[:-1]).upper() return str(''.join(['%02x:' % char for char in info[18:24]])[:-1]).upper()
except Exception: except Exception:
return None return None
@ -69,15 +67,11 @@ def _getIpAddr(ifname: str) -> typing.Optional[str]:
ifnameBytes = ifname.encode('utf-8') ifnameBytes = ifname.encode('utf-8')
try: try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
return str( return str(socket.inet_ntoa(fcntl.ioctl(
socket.inet_ntoa( s.fileno(),
fcntl.ioctl( 0x8915, # SIOCGIFADDR
s.fileno(), struct.pack(str('256s'), ifnameBytes[:15])
0x8915, # SIOCGIFADDR )[20:24]))
struct.pack(str('256s'), ifnameBytes[:15]),
)[20:24]
)
)
except Exception: except Exception:
return None return None
@ -97,32 +91,22 @@ def _getInterfaces() -> typing.List[str]:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
names = array.array(str('B'), b'\0' * space) names = array.array(str('B'), b'\0' * space)
outbytes = struct.unpack( outbytes = struct.unpack(str('iL'), fcntl.ioctl(
'iL', s.fileno(),
fcntl.ioctl( 0x8912, # SIOCGIFCONF
s.fileno(), struct.pack(str('iL'), space, names.buffer_info()[0])
0x8912, # SIOCGIFCONF ))[0]
struct.pack('iL', space, names.buffer_info()[0]),
),
)[0]
namestr = names.tobytes() namestr = names.tobytes()
# return namestr, outbytes # return namestr, outbytes
return [ return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)]
namestr[i : i + offset].split(b'\0', 1)[0].decode('utf-8')
for i in range(0, outbytes, length)
]
def _getIpAndMac( def _getIpAndMac(ifname: str) -> typing.Tuple[typing.Optional[str], typing.Optional[str]]:
ifname: str,
) -> typing.Tuple[typing.Optional[str], typing.Optional[str]]:
ip, mac = _getIpAddr(ifname), _getMacAddr(ifname) ip, mac = _getIpAddr(ifname), _getMacAddr(ifname)
return (ip, mac) return (ip, mac)
def checkPermissions() -> bool: def checkPermissions() -> bool:
return os.getuid() == 0 return os.getuid() == 0 # getuid only available on linux. Expect "complaioins" if edited from Windows
def getComputerName() -> str: def getComputerName() -> str:
''' '''
@ -130,23 +114,15 @@ def getComputerName() -> str:
''' '''
return socket.gethostname().split('.')[0] return socket.gethostname().split('.')[0]
def getNetworkInfo() -> typing.Iterator[types.InterfaceInfoType]: def getNetworkInfo() -> typing.Iterator[types.InterfaceInfoType]:
for ifname in _getInterfaces(): for ifname in _getInterfaces():
ip, mac = _getIpAndMac(ifname) ip, mac = _getIpAndMac(ifname)
if ( if mac != '00:00:00:00:00:00' and mac and ip and ip.startswith('169.254') is False: # Skips local interfaces & interfaces with no dhcp IPs
mac != '00:00:00:00:00:00'
and mac
and ip
and ip.startswith('169.254') is False
): # Skips local interfaces & interfaces with no dhcp IPs
yield types.InterfaceInfoType(name=ifname, mac=mac, ip=ip) yield types.InterfaceInfoType(name=ifname, mac=mac, ip=ip)
def getDomainName() -> str: def getDomainName() -> str:
return '' return ''
def getLinuxOs() -> str: def getLinuxOs() -> str:
try: try:
with open('/etc/os-release', 'r') as f: with open('/etc/os-release', 'r') as f:
@ -157,22 +133,18 @@ def getLinuxOs() -> str:
except Exception: except Exception:
return 'unknown' return 'unknown'
def getVersion() -> str:
return 'Linux ' + getLinuxOs()
def reboot(flags: int = 0): def reboot(flags: int = 0):
''' '''
Simple reboot using os command Simple reboot using os command
''' '''
subprocess.call(['/sbin/shutdown', 'now', '-r']) # nosec: Fine, all under control subprocess.call(['/sbin/shutdown', 'now', '-r'])
def loggoff() -> None: def loggoff() -> None:
''' '''
Right now restarts the machine... Right now restarts the machine...
''' '''
subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']]) # nosec: Fine, all under control subprocess.call(['/usr/bin/pkill', '-u', os.environ['USER']])
# subprocess.call(['/sbin/shutdown', 'now', '-r']) # subprocess.call(['/sbin/shutdown', 'now', '-r'])
# subprocess.call(['/usr/bin/systemctl', 'reboot', '-i']) # subprocess.call(['/usr/bin/systemctl', 'reboot', '-i'])
@ -183,12 +155,10 @@ def renameComputer(newName: str) -> bool:
Returns True if reboot needed Returns True if reboot needed
''' '''
rename(newName) rename(newName)
return True # Always reboot right now. Not much slower but much more convenient return True # Always reboot right now. Not much slower but much more better
def joinDomain( def joinDomain(domain: str, ou: str, account: str, password: str, executeInOneStep: bool = False):
domain: str, ou: str, account: str, password: str, executeInOneStep: bool = False
):
pass pass
@ -196,11 +166,7 @@ def changeUserPassword(user: str, oldPassword: str, newPassword: str) -> None:
''' '''
Simple password change for user using command line Simple password change for user using command line
''' '''
os.system('echo "{1}\n{1}" | /usr/bin/passwd {0} 2> /dev/null'.format(user, newPassword))
subprocess.run( # nosec: Fine, all under control
'echo "{1}\n{1}" | /usr/bin/passwd {0} 2> /dev/null'.format(user, newPassword),
shell=True,
)
def initIdleDuration(atLeastSeconds: int) -> None: def initIdleDuration(atLeastSeconds: int) -> None:
@ -215,22 +181,16 @@ def getCurrentUser() -> str:
''' '''
Returns current logged in user Returns current logged in user
''' '''
return os.getlogin() return os.environ['USER']
def getSessionType() -> str: def getSessionType() -> str:
''' '''
Known values: Known values:
* Unknown -> No XDG_SESSION_TYPE environment variable * Unknown -> No SESSIONNAME environment variable
* xrdp --> xrdp session * Console -> Local session
* other types * RDP-Tcp#[0-9]+ -> RDP Session
''' '''
return ( return 'xrdp' if 'XRDP_SESSION' in os.environ else os.environ.get('XDG_SESSION_TYPE', 'unknown')
'xrdp'
if 'XRDP_SESSION' in os.environ
else os.environ.get('XDG_SESSION_TYPE', 'unknown')
)
def forceTimeSync() -> None: def forceTimeSync() -> None:
return return

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014-2022 Virtual Cable S.L.U. # Copyright (c) 2014-2019 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -11,7 +11,7 @@
# * Redistributions in binary form must reproduce the above copyright notice, # * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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 # may be used to endorse or promote products derived from this software
# without specific prior written permission. # without specific prior written permission.
# #
@ -28,7 +28,7 @@
''' '''
@author: Alexey Shabalin, shaba at altlinux dot org @author: Alexey Shabalin, shaba at altlinux dot org
''' '''
import subprocess # nosec import os
from .common import renamers from .common import renamers
from ...log import logger from ...log import logger
@ -46,8 +46,8 @@ def rename(newName: str) -> bool:
hostname.write(newName) hostname.write(newName)
# Force system new name # Force system new name
subprocess.run(['hostnamectl', 'set-hostname', newName]) # nosec: subprocess os.system('/bin/hostname {}'.format(newName))
subprocess.run(['/bin/hostname', newName]) # nosec: subprocess os.system('/usr/bin/hostnamectl set-hostname {}'.format(newName))
# add name to "hosts" # add name to "hosts"
with open('/etc/hosts', 'r') as hosts: with open('/etc/hosts', 'r') as hosts:

View File

@ -29,6 +29,9 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
import os
import sys
import pkgutil
import typing import typing
from .. import operations from .. import operations

View File

@ -29,7 +29,7 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
import subprocess # nosec import os
from .common import renamers from .common import renamers
from ...log import logger from ...log import logger
@ -45,8 +45,8 @@ def rename(newName: str) -> bool:
hostname.write(newName) hostname.write(newName)
# Force system new name # Force system new name
subprocess.run(['hostnamectl', 'set-hostname', newName]) # nosec: ok, we are root os.system('/bin/hostname {}'.format(newName))
subprocess.run(['/bin/hostname', newName]) # nosec: ok, we are root os.system('/usr/bin/hostnamectl set-hostname {}'.format(newName))
# add name to "hosts" # add name to "hosts"
with open('/etc/hosts', 'r') as hosts: with open('/etc/hosts', 'r') as hosts:

View File

@ -28,7 +28,7 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
import subprocess # nosec import os
from .common import renamers from .common import renamers
from ...log import logger from ...log import logger
@ -46,8 +46,8 @@ def rename(newName: str) -> bool:
hostname.write(newName) hostname.write(newName)
# Force system new name # Force system new name
subprocess.run(['hostnamectl', 'set-hostname', newName]) # nosec: ok, we are root os.system('/bin/hostname {}'.format(newName))
subprocess.run(['/bin/hostname', newName]) # nosec: ok, we are root os.system('/usr/bin/hostnamectl set-hostname {}'.format(newName))
# add name to "hosts" # add name to "hosts"
with open('/etc/hosts', 'r') as hosts: with open('/etc/hosts', 'r') as hosts:

View File

@ -28,7 +28,7 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
import subprocess # nosec import os
from .common import renamers from .common import renamers
from ...log import logger from ...log import logger
@ -46,8 +46,8 @@ def rename(newName: str) -> bool:
hostname.write(newName) hostname.write(newName)
# Force system new name # Force system new name
subprocess.run(['hostnamectl', 'set-hostname', newName]) # nosec: ok, we are root os.system('/bin/hostname {}'.format(newName))
subprocess.run(['/bin/hostname', newName]) # nosec: ok, we are root os.system('/usr/bin/hostnamectl set-hostname {}'.format(newName))
# add name to "hosts" # add name to "hosts"
with open('/etc/hosts', 'r') as hosts: with open('/etc/hosts', 'r') as hosts:

View File

@ -50,7 +50,7 @@ def run() -> None:
r = client.login(sys.argv[2], platform.operations.getSessionType()) r = client.login(sys.argv[2], platform.operations.getSessionType())
print('{},{},{},{}\n'.format(r.ip, r.hostname, r.max_idle, r.dead_line or '')) print('{},{},{},{}\n'.format(r.ip, r.hostname, r.max_idle, r.dead_line or ''))
elif sys.argv[1] == 'logout': elif sys.argv[1] == 'logout':
client.logout(sys.argv[2], platform.operations.getSessionType()) client.logout(sys.argv[2])
except Exception as e: except Exception as e:
logger.exception() logger.exception()
logger.error('Got exception while processing command: %s', e) logger.error('Got exception while processing command: %s', e)

View File

@ -37,7 +37,7 @@ from ..log import logger
from ..service import CommonService from ..service import CommonService
try: try:
from prctl import set_proctitle # type: ignore from prctl import set_proctitle # @UnresolvedImport
except ImportError: # Platform may not include prctl, so in case it's not available, we let the "name" as is except ImportError: # Platform may not include prctl, so in case it's not available, we let the "name" as is
def set_proctitle(_): def set_proctitle(_):
pass pass

View File

@ -32,13 +32,12 @@
import os import os
import configparser import configparser
import base64 import base64
import pickle # nosec import pickle
from .. import types from .. import types
CONFIGFILE = '/etc/udsactor/udsactor.cfg' CONFIGFILE = '/etc/udsactor/udsactor.cfg'
def readConfig() -> types.ActorConfigurationType: def readConfig() -> types.ActorConfigurationType:
try: try:
cfg = configparser.ConfigParser() cfg = configparser.ConfigParser()
@ -46,22 +45,10 @@ def readConfig() -> types.ActorConfigurationType:
uds: configparser.SectionProxy = cfg['uds'] uds: configparser.SectionProxy = cfg['uds']
# Extract data: # Extract data:
base64Config = uds.get('config', None) base64Config = uds.get('config', None)
config = ( config = pickle.loads(base64.b64decode(base64Config.encode())) if base64Config else None
pickle.loads( # nosec: file is restricted
base64.b64decode(base64Config.encode())
)
if base64Config
else None
)
base64Data = uds.get('data', None) base64Data = uds.get('data', None)
data = ( data = pickle.loads(base64.b64decode(base64Data.encode())) if base64Data else None
pickle.loads( # nosec: file is restricted
base64.b64decode(base64Data.encode())
)
if base64Data
else None
)
return types.ActorConfigurationType( return types.ActorConfigurationType(
actorType=uds.get('type', types.MANAGED), actorType=uds.get('type', types.MANAGED),
@ -69,33 +56,28 @@ def readConfig() -> types.ActorConfigurationType:
validateCertificate=uds.getboolean('validate', fallback=False), validateCertificate=uds.getboolean('validate', fallback=False),
master_token=uds.get('master_token', None), master_token=uds.get('master_token', None),
own_token=uds.get('own_token', None), own_token=uds.get('own_token', None),
restrict_net=uds.get('restrict_net', None),
pre_command=uds.get('pre_command', None), pre_command=uds.get('pre_command', None),
runonce_command=uds.get('runonce_command', None), runonce_command=uds.get('runonce_command', None),
post_command=uds.get('post_command', None), post_command=uds.get('post_command', None),
log_level=int(uds.get('log_level', '2')), log_level=int(uds.get('log_level', '2')),
config=config, config=config,
data=data, data=data
) )
except Exception: except Exception:
return types.ActorConfigurationType('', False) return types.ActorConfigurationType('', False)
def writeConfig(config: types.ActorConfigurationType) -> None: def writeConfig(config: types.ActorConfigurationType) -> None:
cfg = configparser.ConfigParser() cfg = configparser.ConfigParser()
cfg.add_section('uds') cfg.add_section('uds')
uds: configparser.SectionProxy = cfg['uds'] uds: configparser.SectionProxy = cfg['uds']
uds['host'] = config.host uds['host'] = config.host
uds['validate'] = 'yes' if config.validateCertificate else 'no' uds['validate'] = 'yes' if config.validateCertificate else 'no'
def writeIfValue(val, name): def writeIfValue(val, name):
if val: if val:
uds[name] = val uds[name] = val
writeIfValue(config.actorType, 'type') writeIfValue(config.actorType, 'type')
writeIfValue(config.master_token, 'master_token') writeIfValue(config.master_token, 'master_token')
writeIfValue(config.own_token, 'own_token') writeIfValue(config.own_token, 'own_token')
writeIfValue(config.restrict_net, 'restrict_net')
writeIfValue(config.pre_command, 'pre_command') writeIfValue(config.pre_command, 'pre_command')
writeIfValue(config.post_command, 'post_command') writeIfValue(config.post_command, 'post_command')
writeIfValue(config.runonce_command, 'runonce_command') writeIfValue(config.runonce_command, 'runonce_command')
@ -109,19 +91,12 @@ def writeConfig(config: types.ActorConfigurationType) -> None:
# Ensures exists destination folder # Ensures exists destination folder
dirname = os.path.dirname(CONFIGFILE) dirname = os.path.dirname(CONFIGFILE)
if not os.path.exists(dirname): if not os.path.exists(dirname):
os.mkdir( os.mkdir(dirname, mode=0o700) # Will create only if route to path already exists, for example, /etc (that must... :-))
dirname, mode=0o700
) # Will create only if route to path already exists, for example, /etc (that must... :-))
with open(CONFIGFILE, 'w') as f: with open(CONFIGFILE, 'w') as f:
cfg.write(f) cfg.write(f)
os.chmod(CONFIGFILE, 0o0600) # Ensure only readable by root os.chmod(CONFIGFILE, 0o0600) # Ensure only readable by root
def useOldJoinSystem() -> bool: def useOldJoinSystem() -> bool:
return False return False
def invokeScriptOnLogin() -> str:
return ''

View File

@ -31,7 +31,7 @@
# pylint: disable=invalid-name # pylint: disable=invalid-name
import ctypes import ctypes
import ctypes.util import ctypes.util
import subprocess # nosec import subprocess
xlib = None xlib = None
xss = None xss = None
@ -39,22 +39,17 @@ display = None
xssInfo = None xssInfo = None
initialized = False initialized = False
class XScreenSaverInfo(ctypes.Structure): # pylint: disable=too-few-public-methods class XScreenSaverInfo(ctypes.Structure): # pylint: disable=too-few-public-methods
_fields_ = [ _fields_ = [('window', ctypes.c_long),
('window', ctypes.c_long), ('state', ctypes.c_int),
('state', ctypes.c_int), ('kind', ctypes.c_int),
('kind', ctypes.c_int), ('til_or_since', ctypes.c_ulong),
('til_or_since', ctypes.c_ulong), ('idle', ctypes.c_ulong),
('idle', ctypes.c_ulong), ('eventMask', ctypes.c_ulong)]
('eventMask', ctypes.c_ulong),
]
class c_ptr(ctypes.c_void_p): class c_ptr(ctypes.c_void_p):
pass pass
def _ensureInitialized(): def _ensureInitialized():
global xlib, xss, xssInfo, display, initialized # pylint: disable=global-statement global xlib, xss, xssInfo, display, initialized # pylint: disable=global-statement
@ -78,15 +73,13 @@ def _ensureInitialized():
xss.XScreenSaverQueryExtension.argtypes = [ xss.XScreenSaverQueryExtension.argtypes = [
ctypes.c_void_p, ctypes.c_void_p,
ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int),
ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int)
] ]
xss.XScreenSaverAllocInfo.restype = ctypes.POINTER( xss.XScreenSaverAllocInfo.restype = ctypes.POINTER(XScreenSaverInfo) # Result in a XScreenSaverInfo structure
XScreenSaverInfo
) # Result in a XScreenSaverInfo structure
xss.XScreenSaverQueryInfo.argtypes = [ xss.XScreenSaverQueryInfo.argtypes = [
ctypes.c_void_p, ctypes.c_void_p,
ctypes.c_void_p, ctypes.c_void_p,
ctypes.POINTER(XScreenSaverInfo), ctypes.POINTER(XScreenSaverInfo)
] ]
xlib.XOpenDisplay.argtypes = [ctypes.c_char_p] xlib.XOpenDisplay.argtypes = [ctypes.c_char_p]
xlib.XOpenDisplay.restype = c_ptr xlib.XOpenDisplay.restype = c_ptr
@ -102,9 +95,7 @@ def _ensureInitialized():
event_base = ctypes.c_int() event_base = ctypes.c_int()
error_base = ctypes.c_int() error_base = ctypes.c_int()
available = xss.XScreenSaverQueryExtension( available = xss.XScreenSaverQueryExtension(display, ctypes.byref(event_base), ctypes.byref(error_base))
display, ctypes.byref(event_base), ctypes.byref(error_base)
)
if available != 1: if available != 1:
raise Exception('ScreenSaver not available') raise Exception('ScreenSaver not available')
@ -116,11 +107,9 @@ def _ensureInitialized():
def initIdleDuration(atLeastSeconds: int) -> None: def initIdleDuration(atLeastSeconds: int) -> None:
_ensureInitialized() _ensureInitialized()
if atLeastSeconds: if atLeastSeconds:
subprocess.call( # nosec, controlled params subprocess.call(['/usr/bin/xset', 's', '{}'.format(atLeastSeconds + 30)])
['/usr/bin/xset', 's', '{}'.format(atLeastSeconds + 30)]
)
# And now reset it # And now reset it
subprocess.call(['/usr/bin/xset', 's', 'reset']) # nosec: fixed command subprocess.call(['/usr/bin/xset', 's', 'reset'])
def getIdleDuration() -> float: def getIdleDuration() -> float:
@ -133,11 +122,7 @@ def getIdleDuration() -> float:
xss.XScreenSaverQueryInfo(display, xlib.XDefaultRootWindow(display), xssInfo) xss.XScreenSaverQueryInfo(display, xlib.XDefaultRootWindow(display), xssInfo)
# States: 0 = off, 1 = On, 2 = Cycle, 3 = Disabled, ...? # States: 0 = off, 1 = On, 2 = Cycle, 3 = Disabled, ...?
if ( if xssInfo.contents.state == 1: # state = 1 means "active", so idle is not a valid state
xssInfo.contents.state == 1 return 3600 * 100 * 1000 # If screen saver is active, return a high enough value
): # state = 1 means "active", so idle is not a valid state
return (
3600 * 100 * 1000
) # If screen saver is active, return a high enough value
return xssInfo.contents.idle / 1000.0 return xssInfo.contents.idle / 1000.0

View File

@ -35,8 +35,6 @@ import typing
if sys.platform == 'win32': if sys.platform == 'win32':
from .windows.log import LocalLogger from .windows.log import LocalLogger
elif sys.platform == 'darwin':
from .macos.log import LocalLogger
else: else:
from .linux.log import LocalLogger from .linux.log import LocalLogger
@ -57,7 +55,7 @@ class Logger:
self.logLevel = INFO self.logLevel = INFO
self.localLogger = LocalLogger() self.localLogger = LocalLogger()
self.remoteLogger = None self.remoteLogger = None
self.own_token = '' # nosec: This is no password at all self.own_token = ''
def setLevel(self, level: typing.Union[str, int]) -> None: def setLevel(self, level: typing.Union[str, int]) -> None:
''' '''

View File

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

View File

@ -1,185 +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
'''
# Note. most methods are not implemented, as they are not needed for this platform (macos)
# that only supports unmanaged machines
import socket
import os
import re
import subprocess # nosec
import typing
import psutil
from udsactor import types, tools
MACVER_RE = re.compile(
r"<key>ProductVersion</key>\s*<string>(.*)</string>", re.MULTILINE
)
MACVER_FILE = '/System/Library/CoreServices/SystemVersion.plist'
def checkPermissions() -> bool:
return os.getuid() == 0
def getComputerName() -> str:
'''
Returns computer name, with no domain
'''
return socket.gethostname().split('.')[0]
def getNetworkInfo() -> typing.Iterator[types.InterfaceInfoType]:
ifdata: typing.List['psutil._common.snicaddr']
for ifname, ifdata in psutil.net_if_addrs().items():
name, ip, mac = '', '', ''
# Get IP address, interface name and MAC address whenever possible
for row in ifdata:
if row.family == socket.AF_INET:
ip = row.address
name = ifname
elif row.family == socket.AF_LINK:
mac = row.address
# if all data is available, stop iterating
if ip and name and mac:
if (
mac != '00:00:00:00:00:00'
and mac
and ip
and ip.startswith('169.254') is False
): # Skips local interfaces & interfaces with no dhcp IPs
yield types.InterfaceInfoType(name=name, ip=ip, mac=mac)
break
def getDomainName() -> str:
return ''
def getMacOs() -> str:
try:
with open(MACVER_FILE, 'r') as f:
data = f.read()
m = MACVER_RE.search(data)
if m:
return m.group(1)
except Exception: # nosec: B110: ignore exception because we are not interested in it
pass
return 'unknown'
def getVersion() -> str:
return 'MacOS ' + getMacOs()
def reboot(flags: int = 0) -> None:
'''
Simple reboot using os command
'''
subprocess.call(['/sbin/shutdown', '-r', 'now']) # nosec: Command line is fixed
def loggoff() -> None:
'''
Right now restarts the machine...
'''
subprocess.run(
"/bin/launchctl bootout gui/$(id -u $USER)", shell=True
) # nosec: Command line is fixed
# Ignores output, as it may fail if user is not logged in
def renameComputer(newName: str) -> bool:
'''
Changes the computer name
Returns True if reboot needed
Note: For macOS, no configuration is supported, only "unmanaged" actor
'''
return False
def joinDomain(
domain: str, ou: str, account: str, password: str, executeInOneStep: bool = False
):
pass
def changeUserPassword(user: str, oldPassword: str, newPassword: str) -> None:
pass
def initIdleDuration(atLeastSeconds: int) -> None:
pass
# se we cache for 20 seconds the result, that is enough for our needs
# and we avoid calling a system command every time we need it
@tools.cache(20)
def getIdleDuration() -> float:
# Execute:
try:
return (
int(
next(
filter(
lambda x: b"HIDIdleTime" in x,
subprocess.check_output(
["/usr/sbin/ioreg", "-c", "IOHIDSystem"]
).split(b"\n"),
)
).split(b"=")[1]
)
/ 1000000000
) # nosec: Command line is fixed
except Exception: # nosec: B110: ignore exception because we are not interested in it
return 0
def getCurrentUser() -> str:
'''
Returns current logged in user
'''
return os.getlogin()
def getSessionType() -> str:
'''
Returns the session type. Currently, only "macos" (console) is supported
'''
return 'macos'
def forceTimeSync() -> None:
return

View File

@ -1,71 +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.
'''
Author: Adolfo Gómez, dkmaster at dkmon dot com
'''
import sys
import typing
from .. import rest
from .. import platform
from ..log import logger
from .service import UDSActorSvc
def usage() -> typing.NoReturn:
sys.stderr.write('usage: udsactor start|login "username"|logout "username"\n')
sys.exit(2)
def run() -> None:
logger.setLevel(20000)
if len(sys.argv) == 3 and sys.argv[1] in ('login', 'logout'):
logger.debug('Running client udsactor')
try:
client: rest.UDSClientApi = rest.UDSClientApi()
if sys.argv[1] == 'login':
r = client.login(sys.argv[2], platform.operations.getSessionType())
print('{},{},{},{}\n'.format(r.ip, r.hostname, r.max_idle, r.dead_line or ''))
elif sys.argv[1] == 'logout':
client.logout(sys.argv[2], platform.operations.getSessionType())
except Exception as e:
logger.exception()
logger.error('Got exception while processing command: %s', e)
sys.exit(0)
elif len(sys.argv) != 2:
usage()
daemonSvr = UDSActorSvc()
if len(sys.argv) == 2:
# Daemon mode...
if sys.argv[1] in ('start', 'start-foreground'):
daemonSvr.run() # execute in foreground
else:
usage()
sys.exit(0)
else:
usage()

View File

@ -1,108 +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
import signal
from ..log import logger
from ..service import CommonService
class UDSActorSvc(CommonService):
def __init__(self) -> None:
CommonService.__init__(self)
# Captures signals so we can stop gracefully
signal.signal(signal.SIGINT, self.markForExit)
signal.signal(signal.SIGTERM, self.markForExit)
def markForExit(self, signum, frame) -> None: # pylint: disable=unused-argument
self._isAlive = False
def joinDomain( # pylint: disable=unused-argument, too-many-arguments
self,
name: str,
domain: str,
ou: str,
account: str,
password: str
) -> None:
pass # Not implemented for unmanaged machines
def rename(
self,
name: str,
userName: typing.Optional[str] = None,
oldPassword: typing.Optional[str] = None,
newPassword: typing.Optional[str] = None,
) -> None:
pass # Not implemented for unmanaged machines
def run(self) -> None:
logger.debug('Running Daemon: {}'.format(self._isAlive))
# Linux daemon will continue running unless something is requested to
# Unmanaged services does not initializes "on start", but rather when user logs in (because userservice does not exists "as such" before that)
if self.isManaged(): # Currently, managed is not implemented for UDS on M
logger.error('Managed machines not supported on MacOS')
# Wait a bit, this is mac os and will be run by launchd
# If the daemon shuts down too quickly, launchd may think it is a crash.
self.doWait(10000)
self.finish()
return # Stop daemon if initializes told to do so
if not self.initializeUnmanaged():
# Wait a bit, this is mac os and will be run by launchd
# If the daemon shuts down too quickly, launchd may think it is a crash.
self.doWait(10000)
self.finish()
return
# Start listening for petitions
self.startHttpServer()
# *********************
# * Main Service loop *
# *********************
# Counter used to check ip changes only once every 10 seconds, for
# example
counter = 0
while self._isAlive:
counter += 1
try:
if counter % 5 == 0:
self.loop()
except Exception as e:
logger.error('Got exception on main loop: %s', e)
# In milliseconds, will break
self.doWait(1000)
self.finish()

View File

@ -1,106 +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 os
import configparser
import base64
import pickle # nosec
from .. import types
CONFIGFILE = '/etc/udsactor/udsactor.cfg'
def readConfig() -> types.ActorConfigurationType:
try:
cfg = configparser.ConfigParser()
cfg.read(CONFIGFILE)
uds: configparser.SectionProxy = cfg['uds']
# Extract data:
base64Config = uds.get('config', None)
config = pickle.loads(base64.b64decode(base64Config.encode())) if base64Config else None # nosec: Read from root controled file, secure
base64Data = uds.get('data', None)
data = pickle.loads(base64.b64decode(base64Data.encode())) if base64Data else None # nosec: Read from root controled file, secure
return types.ActorConfigurationType(
actorType=uds.get('type', types.MANAGED),
host=uds.get('host', ''),
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),
pre_command=uds.get('pre_command', None),
runonce_command=uds.get('runonce_command', None),
post_command=uds.get('post_command', None),
log_level=int(uds.get('log_level', '2')),
config=config,
data=data
)
except Exception:
return types.ActorConfigurationType('', False)
def writeConfig(config: types.ActorConfigurationType) -> None:
cfg = configparser.ConfigParser()
cfg.add_section('uds')
uds: configparser.SectionProxy = cfg['uds']
uds['host'] = config.host
uds['validate'] = 'yes' if config.validateCertificate else 'no'
def writeIfValue(val, name):
if val:
uds[name] = val
writeIfValue(config.actorType, 'type')
writeIfValue(config.master_token, 'master_token')
writeIfValue(config.own_token, 'own_token')
writeIfValue(config.restrict_net, 'restrict_net')
writeIfValue(config.pre_command, 'pre_command')
writeIfValue(config.post_command, 'post_command')
writeIfValue(config.runonce_command, 'runonce_command')
uds['log_level'] = str(config.log_level)
if config.config: # Special case, encoded & dumped
uds['config'] = base64.b64encode(pickle.dumps(config.config)).decode()
if config.data: # Special case, encoded & dumped
uds['data'] = base64.b64encode(pickle.dumps(config.data)).decode()
# Ensures exists destination folder
dirname = os.path.dirname(CONFIGFILE)
if not os.path.exists(dirname):
os.mkdir(dirname, mode=0o700) # Will create only if route to path already exists, for example, /etc (that must... :-))
with open(CONFIGFILE, 'w') as f:
cfg.write(f)
os.chmod(CONFIGFILE, 0o0600) # Ensure only readable by root
def useOldJoinSystem() -> bool:
return False
def invokeScriptOnLogin() -> str:
return ''

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014-2022 Virtual Cable S.L.U. # Copyright (c) 2014 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -11,7 +11,7 @@
# * Redistributions in binary form must reproduce the above copyright notice, # * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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 # may be used to endorse or promote products derived from this software
# without specific prior written permission. # without specific prior written permission.
# #
@ -31,15 +31,7 @@
import sys import sys
name = sys.platform name = sys.platform
is_windows = is_linux = is_mac = False
if sys.platform == 'win32': if sys.platform == 'win32':
from .windows import operations, store, runner from .windows import operations, store # pylint: disable=unused-import
is_windows = True
elif sys.platform == 'darwin':
from .macos import operations, store, runner
is_mac = True
elif sys.platform == 'linux':
from .linux import operations, store, runner
is_linux = True
else: else:
raise Exception('Unsupported platform: {0}'.format(sys.platform)) from .linux import operations, store # pylint: disable=unused-import

View File

@ -36,52 +36,42 @@ import typing
import requests import requests
from udsactor import types, tools from . import types
from udsactor.version import VERSION, BUILD from .info import VERSION
# Default public listen port # Default public listen port
LISTEN_PORT = 43910 LISTEN_PORT = 43910
# Default timeout # Default timeout
TIMEOUT = 5 # 5 seconds is more than enought TIMEOUT = 5 # 5 seconds is more than enought
# Constants # Constants
UNKNOWN = 'unknown' UNKNOWN = 'unknown'
class RESTError(Exception): class RESTError(Exception):
ERRCODE = 0 ERRCODE = 0
class RESTConnectionError(RESTError): class RESTConnectionError(RESTError):
ERRCODE = -1 ERRCODE = -1
# Errors ""raised"" from broker # Errors ""raised"" from broker
class RESTInvalidKeyError(RESTError): class RESTInvalidKeyError(RESTError):
ERRCODE = 1 ERRCODE = 1
class RESTUnmanagedHostError(RESTError): class RESTUnmanagedHostError(RESTError):
ERRCODE = 2 ERRCODE = 2
class RESTUserServiceNotFoundError(RESTError): class RESTUserServiceNotFoundError(RESTError):
ERRCODE = 3 ERRCODE = 3
class RESTOsManagerError(RESTError): class RESTOsManagerError(RESTError):
ERRCODE = 4 ERRCODE = 4
# For avoid proxy on localhost connections # For avoid proxy on localhost connections
NO_PROXY = { NO_PROXY = {
'http': None, 'http': None,
'https': None, 'https': None,
} }
UDS_BASE_URL = 'https://{}/uds/rest/'
# #
# Basic UDS Api # Basic UDS Api
# #
@ -89,51 +79,48 @@ class UDSApi: # pylint: disable=too-few-public-methods
""" """
Base for remote api accesses Base for remote api accesses
""" """
_host: str
_host: str = '' _validateCert: bool
_validateCert: bool = True _url: str
_url: str = ''
def __init__(self, host: str, validateCert: bool) -> None: def __init__(self, host: str, validateCert: bool) -> None:
self._host = host self._host = host
self._validateCert = validateCert self._validateCert = validateCert
self._url = UDS_BASE_URL.format(self._host) self._url = "https://{}/uds/rest/".format(self._host)
# Disable logging requests messages except for errors, ... # Disable logging requests messages except for errors, ...
logging.getLogger('request').setLevel(logging.CRITICAL) logging.getLogger("requests").setLevel(logging.CRITICAL)
logging.getLogger('urllib3').setLevel(logging.ERROR) logging.getLogger("urllib3").setLevel(logging.ERROR)
try: try:
warnings.simplefilter('ignore') # Disables all warnings warnings.simplefilter("ignore") # Disables all warnings
except Exception: # nosec: not interested in exceptions except Exception:
pass pass
@property @property
def _headers(self) -> typing.MutableMapping[str, str]: def _headers(self) -> typing.MutableMapping[str, str]:
return { return {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'User-Agent': 'UDS Actor v{}/{}'.format(VERSION, BUILD), 'User-Agent': 'UDS Actor v{}'.format(VERSION)
} }
def _api_url(self, method: str) -> str: def _apiURL(self, method: str) -> str:
raise NotImplementedError raise NotImplementedError
def _doPost( def _doPost(
self, self,
method: str, # i.e. 'initialize', 'ready', .... method: str, # i.e. 'initialize', 'ready', ....
payLoad: typing.MutableMapping[str, typing.Any], payLoad: typing.MutableMapping[str, typing.Any],
headers: typing.Optional[typing.MutableMapping[str, str]] = None, headers: typing.Optional[typing.MutableMapping[str, str]] = None,
disableProxy: bool = False, disableProxy: bool = False
) -> typing.Any: ) -> typing.Any:
headers = headers or self._headers headers = headers or self._headers
try: try:
result = requests.post( result = requests.post(
self._api_url(method), self._apiURL(method),
data=json.dumps(payLoad), data=json.dumps(payLoad),
headers=headers, headers=headers,
verify=self._validateCert, verify=self._validateCert,
timeout=TIMEOUT, timeout=TIMEOUT,
proxies=NO_PROXY # type: ignore proxies=NO_PROXY if disableProxy else None # if not proxies wanted, enforce it
if disableProxy
else None, # if not proxies wanted, enforce it
) )
if result.ok: if result.ok:
@ -152,22 +139,16 @@ class UDSApi: # pylint: disable=too-few-public-methods
raise RESTError(data) raise RESTError(data)
# #
# UDS Broker API access # UDS Broker API access
# #
class UDSServerApi(UDSApi): class UDSServerApi(UDSApi):
def _api_url(self, method: str) -> str: def _apiURL(self, method: str) -> str:
return self._url + 'actor/v3/' + method return self._url + 'actor/v3/' + method
def enumerateAuthenticators(self) -> typing.Iterable[types.AuthenticatorType]: def enumerateAuthenticators(self) -> typing.Iterable[types.AuthenticatorType]:
try: try:
result = requests.get( result = requests.get(self._url + 'auth/auths', headers=self._headers, verify=self._validateCert, timeout=4)
self._url + 'auth/auths',
headers=self._headers,
verify=self._validateCert,
timeout=4,
)
if result.ok: if result.ok:
for v in sorted(result.json(), key=lambda x: x['priority']): for v in sorted(result.json(), key=lambda x: x['priority']):
yield types.AuthenticatorType( yield types.AuthenticatorType(
@ -176,24 +157,24 @@ class UDSServerApi(UDSApi):
auth=v['auth'], auth=v['auth'],
type=v['type'], type=v['type'],
priority=v['priority'], priority=v['priority'],
isCustom=v['isCustom'], isCustom=v['isCustom']
) )
except Exception: # nosec: not interested in exceptions except Exception:
pass pass
def register( def register( #pylint: disable=too-many-arguments, too-many-locals
self, self,
auth: str, auth: str,
username: str, username: str,
password: str, password: str,
hostname: str, hostname: str,
ip: str, ip: str,
mac: str, mac: str,
preCommand: str, preCommand: str,
runOnceCommand: str, runOnceCommand: str,
postCommand: str, postCommand: str,
logLevel: int, logLevel: int
) -> str: ) -> str:
""" """
Raises an exception if could not register, or registers and returns the "authorization token" Raises an exception if could not register, or registers and returns the "authorization token"
""" """
@ -205,7 +186,7 @@ class UDSServerApi(UDSApi):
'pre_command': preCommand, 'pre_command': preCommand,
'run_once_command': runOnceCommand, 'run_once_command': runOnceCommand,
'post_command': postCommand, 'post_command': postCommand,
'log_level': logLevel, 'log_level': logLevel
} }
# First, try to login to REST api # First, try to login to REST api
@ -213,23 +194,13 @@ class UDSServerApi(UDSApi):
# First, try to login # First, try to login
authInfo = {'auth': auth, 'username': username, 'password': password} authInfo = {'auth': auth, 'username': username, 'password': password}
headers = self._headers headers = self._headers
result = requests.post( result = requests.post(self._url + 'auth/login', data=json.dumps(authInfo), headers=headers, verify=self._validateCert)
self._url + 'auth/login',
data=json.dumps(authInfo),
headers=headers,
verify=self._validateCert,
)
if not result.ok or result.json()['result'] == 'error': if not result.ok or result.json()['result'] == 'error':
raise Exception() # Invalid credentials raise Exception() # Invalid credentials
headers['X-Auth-Token'] = result.json()['token'] headers['X-Auth-Token'] = result.json()['token']
result = requests.post( result = requests.post(self._apiURL('register'), data=json.dumps(data), headers=headers, verify=self._validateCert)
self._api_url('register'),
data=json.dumps(data),
headers=headers,
verify=self._validateCert,
)
if result.ok: if result.ok:
return result.json()['result'] return result.json()['result']
except requests.ConnectionError as e: except requests.ConnectionError as e:
@ -241,19 +212,13 @@ class UDSServerApi(UDSApi):
raise RESTError(result.content.decode()) raise RESTError(result.content.decode())
def initialize( def initialize(self, token: str, interfaces: typing.Iterable[types.InterfaceInfoType], actorType: typing.Optional[str]) -> types.InitializationResultType:
self,
token: str,
interfaces: typing.Iterable[types.InterfaceInfoType],
actor_type: typing.Optional[str],
) -> types.InitializationResultType:
# Generate id list from netork cards # Generate id list from netork cards
payload = { payload = {
'type': actor_type or types.MANAGED, 'type': actorType or types.MANAGED,
'token': token, 'token': token,
'version': VERSION, 'version': VERSION,
'build': BUILD, 'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces]
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
} }
r = self._doPost('initialize', payload) r = self._doPost('initialize', payload)
os = r['os'] os = r['os']
@ -267,115 +232,95 @@ class UDSServerApi(UDSApi):
password=os.get('password'), password=os.get('password'),
new_password=os.get('new_password'), new_password=os.get('new_password'),
ad=os.get('ad'), ad=os.get('ad'),
ou=os.get('ou'), ou=os.get('ou')
) ) if r['os'] else None
if r['os']
else None,
alias_token=r.get('alias_token'), # Possible alias for unmanaged
) )
def ready( def ready(self, own_token: str, secret: str, ip: str, port: int) -> types.CertificateInfoType:
self, own_token: str, secret: str, ip: str, port: int payload = {
) -> types.CertificateInfoType: 'token': own_token,
payload = {'token': own_token, 'secret': secret, 'ip': ip, 'port': port} 'secret': secret,
'ip': ip,
'port': port
}
result = self._doPost('ready', payload) result = self._doPost('ready', payload)
return types.CertificateInfoType( return types.CertificateInfoType(
private_key=result['private_key'], private_key=result['private_key'],
server_certificate=result['server_certificate'], server_certificate=result['server_certificate'],
password=result['password'], password=result['password']
) )
def notifyIpChange( def notifyIpChange(self, own_token: str, secret: str, ip: str, port: int) -> types.CertificateInfoType:
self, own_token: str, secret: str, ip: str, port: int payload = {
) -> types.CertificateInfoType: 'token': own_token,
payload = {'token': own_token, 'secret': secret, 'ip': ip, 'port': port} 'secret': secret,
'ip': ip,
'port': port
}
result = self._doPost('ipchange', payload) result = self._doPost('ipchange', payload)
return types.CertificateInfoType( return types.CertificateInfoType(
private_key=result['private_key'], private_key=result['private_key'],
server_certificate=result['server_certificate'], server_certificate=result['server_certificate'],
password=result['password'], password=result['password']
) )
def notifyUnmanagedCallback( def notifyUnmanagedCallback(self, master_token: str, secret: str, interfaces: typing.Iterable[types.InterfaceInfoType], port: int) -> types.CertificateInfoType:
self,
master_token: str,
secret: str,
interfaces: typing.Iterable[types.InterfaceInfoType],
port: int,
) -> types.CertificateInfoType:
payload = { payload = {
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces], 'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
'token': master_token, 'token': master_token,
'secret': secret, 'secret': secret,
'port': port, 'port': port
} }
result = self._doPost('unmanaged', payload) result = self._doPost('unmanaged', payload)
return types.CertificateInfoType( return types.CertificateInfoType(
private_key=result['private_key'], private_key=result['private_key'],
server_certificate=result['server_certificate'], server_certificate=result['server_certificate'],
password=result['password'], password=result['password']
) )
def login(
self, def login(self, own_token: str, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType:
actor_type: typing.Optional[str], if not own_token:
token: str,
username: str,
session_type: str,
interfaces: typing.Iterable[types.InterfaceInfoType],
secret: typing.Optional[str],
) -> types.LoginResultInfoType:
if not token:
return types.LoginResultInfoType( return types.LoginResultInfoType(
ip='0.0.0.0', hostname=UNKNOWN, dead_line=None, max_idle=None, session_id=None ip='0.0.0.0',
hostname=UNKNOWN,
dead_line=None,
max_idle=None
) )
payload = { payload = {
'type': actor_type or types.MANAGED, 'token': own_token,
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces],
'token': token,
'username': username, 'username': username,
'session_type': session_type, 'session_type': sessionType or UNKNOWN
'secret': secret or '',
} }
result = self._doPost('login', payload) result = self._doPost('login', payload)
return types.LoginResultInfoType( return types.LoginResultInfoType(
ip=result['ip'], ip=result['ip'],
hostname=result['hostname'], hostname=result['hostname'],
dead_line=result['dead_line'], dead_line=result['dead_line'],
max_idle=result['max_idle'], max_idle=result['max_idle']
session_id=result.get('session_id', ''),
) )
def logout( def logout(self, own_token: str, username: str) -> None:
self, if not own_token:
actor_type: typing.Optional[str], return
token: str,
username: str,
session_id: str,
session_type: str,
interfaces: typing.Iterable[types.InterfaceInfoType],
secret: typing.Optional[str],
) -> typing.Optional[str]:
if not token:
return None
payload = { payload = {
'type': actor_type or types.MANAGED, 'token': own_token,
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces], 'username': username
'token': token,
'username': username,
'session_type': session_type,
'session_id': session_id,
'secret': secret or '',
} }
return self._doPost('logout', payload) # Can be 'ok' or 'notified' self._doPost('logout', payload)
def log(self, own_token: str, level: int, message: str) -> None: def log(self, own_token: str, level: int, message: str) -> None:
if not own_token: if not own_token:
return return
payLoad = {'token': own_token, 'level': level, 'message': message} payLoad = {
'token': own_token,
'level': level,
'message': message
}
self._doPost('log', payLoad) # Ignores result... self._doPost('log', payLoad) # Ignores result...
def test(self, master_token: str, actorType: typing.Optional[str]) -> bool: def test(self, master_token: str, actorType: typing.Optional[str]) -> bool:
@ -386,62 +331,50 @@ class UDSServerApi(UDSApi):
return self._doPost('test', payLoad) == 'ok' return self._doPost('test', payLoad) == 'ok'
class UDSClientApi(UDSApi, metaclass=tools.Singleton): class UDSClientApi(UDSApi):
_session_id: str = ''
_callback_url: str = ''
def __init__(self) -> None: def __init__(self) -> None:
super().__init__('127.0.0.1:{}'.format(LISTEN_PORT), False) super().__init__('127.0.0.1:{}'.format(LISTEN_PORT), False)
# Override base url
# Replace base url
self._url = "https://{}/ui/".format(self._host) self._url = "https://{}/ui/".format(self._host)
def _api_url(self, method: str) -> str: def _apiURL(self, method: str) -> str:
return self._url + method return self._url + method
def post( def post(
self, self,
method: str, # i.e. 'initialize', 'ready', .... method: str, # i.e. 'initialize', 'ready', ....
payLoad: typing.MutableMapping[str, typing.Any], payLoad: typing.MutableMapping[str, typing.Any]
) -> typing.Any: ) -> typing.Any:
return self._doPost(method=method, payLoad=payLoad, disableProxy=True) return self._doPost(method=method, payLoad=payLoad, disableProxy=True)
def register(self, callback_url: str) -> None: def register(self, callbackUrl: str) -> None:
self._callback_url = callback_url payLoad = {
payLoad = {'callback_url': callback_url} 'callback_url': callbackUrl
}
self.post('register', payLoad) self.post('register', payLoad)
def unregister(self, callback_url: str) -> None: def unregister(self, callbackUrl: str) -> None:
payLoad = {'callback_url': callback_url} payLoad = {
'callback_url': callbackUrl
}
self.post('unregister', payLoad) self.post('unregister', payLoad)
self._callback_url = ''
def login( def login(self, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType:
self, username: str, sessionType: typing.Optional[str] = None
) -> types.LoginResultInfoType:
payLoad = { payLoad = {
'username': username, 'username': username,
'session_type': sessionType or UNKNOWN, 'session_type': sessionType or UNKNOWN,
'callback_url': self._callback_url, # So we identify ourselves
} }
result = self.post('login', payLoad) result = self.post('login', payLoad)
res = types.LoginResultInfoType( return types.LoginResultInfoType(
ip=result['ip'], ip=result['ip'],
hostname=result['hostname'], hostname=result['hostname'],
dead_line=result['dead_line'], dead_line=result['dead_line'],
max_idle=result['max_idle'], max_idle=result['max_idle']
session_id=result['session_id'],
) )
# Store session id for future use
self._session_id = res.session_id or ''
return res
def logout(self, username: str, sessionType: typing.Optional[str]) -> None: def logout(self, username: str) -> None:
payLoad = { payLoad = {
'username': username, 'username': username
'session_type': sessionType or UNKNOWN,
'callback_url': self._callback_url, # So we identify ourselves
'session_id': self._session_id, # We now know the session id, provided on login
} }
self.post('logout', payLoad) self.post('logout', payLoad)

View File

@ -36,13 +36,12 @@ import secrets
import subprocess import subprocess
import typing import typing
from udsactor import platform from . import platform
from udsactor import rest from . import rest
from udsactor import types from . import types
from udsactor import tools
from udsactor.log import logger, DEBUG, INFO, ERROR, FATAL from .log import logger, DEBUG, INFO, ERROR, FATAL
from udsactor.http import clients_pool, server, cert from .http import clients_pool, server, cert
# def setup() -> None: # def setup() -> None:
# cfg = platform.store.readConfig() # cfg = platform.store.readConfig()
@ -56,16 +55,18 @@ from udsactor.http import clients_pool, server, cert
# else: # else:
# logger.setLevel(20000) # logger.setLevel(20000)
class CommonService: # pylint: disable=too-many-instance-attributes class CommonService: # pylint: disable=too-many-instance-attributes
_isAlive: bool = True _isAlive: bool = True
_rebootRequested: bool = False _rebootRequested: bool = False
_loggedIn: bool = False
_initialized: bool = False _initialized: bool = False
_cfg: types.ActorConfigurationType _cfg: types.ActorConfigurationType
_api: rest.UDSServerApi _api: rest.UDSServerApi
_interfaces: typing.List[types.InterfaceInfoType] _interfaces: typing.List[types.InterfaceInfoType]
_secret: str _secret: str
_certificate: types.CertificateInfoType _certificate: types.CertificateInfoType
_clientsPool: clients_pool.UDSActorClientPool
_http: typing.Optional[server.HTTPServerThread] _http: typing.Optional[server.HTTPServerThread]
@staticmethod @staticmethod
@ -74,9 +75,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
logger.debug('Executing command on {}: {}'.format(section, cmdLine)) logger.debug('Executing command on {}: {}'.format(section, cmdLine))
res = subprocess.check_call(cmdLine, shell=True) res = subprocess.check_call(cmdLine, shell=True)
except Exception as e: except Exception as e:
logger.error( logger.error('Got exception executing: {} - {} - {}'.format(section, cmdLine, e))
'Got exception executing: {} - {} - {}'.format(section, cmdLine, e)
)
return False return False
logger.debug('Result of executing cmd for {} was {}'.format(section, res)) logger.debug('Result of executing cmd for {} was {}'.format(section, res))
return True return True
@ -87,9 +86,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self._api = rest.UDSServerApi(self._cfg.host, self._cfg.validateCertificate) self._api = rest.UDSServerApi(self._cfg.host, self._cfg.validateCertificate)
self._secret = secrets.token_urlsafe(33) self._secret = secrets.token_urlsafe(33)
self._clientsPool = clients_pool.UDSActorClientPool() self._clientsPool = clients_pool.UDSActorClientPool()
self._certificate = ( self._certificate = cert.defaultCertificate # For being used on "unmanaged" hosts only
cert.defaultCertificate
) # For being used on "unmanaged" hosts only
self._http = None self._http = None
# Initialzies loglevel and serviceLogger # Initialzies loglevel and serviceLogger
@ -98,7 +95,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
# 0 = OTHER, 10000 = DEBUG, 20000 = WARN, 30000 = INFO, 40000 = ERROR, 50000 = FATAL # 0 = OTHER, 10000 = DEBUG, 20000 = WARN, 30000 = INFO, 40000 = ERROR, 50000 = FATAL
# So this comes: # So this comes:
logger.setLevel([DEBUG, INFO, ERROR, FATAL][self._cfg.log_level]) logger.setLevel([DEBUG, INFO, ERROR, FATAL][self._cfg.log_level])
# If windows, enable service logger FOR SERVICE only # If windows, enable service logger
logger.enableServiceLogger() logger.enableServiceLogger()
socket.setdefaulttimeout(20) socket.setdefaulttimeout(20)
@ -115,24 +112,16 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self._http.start() self._http.start()
def isManaged(self) -> bool: def isManaged(self) -> bool:
return ( return self._cfg.actorType != types.UNMANAGED # Only "unmanaged" hosts are unmanaged, the rest are "managed"
self._cfg.actorType != types.UNMANAGED
) # Only "unmanaged" hosts are unmanaged, the rest are "managed"
def serviceInterfaceInfo( def serviceInterfaceInfo(self, interfaces: typing.Optional[typing.List[types.InterfaceInfoType]] = None) -> typing.Optional[types.InterfaceInfoType]:
self, interfaces: typing.Optional[typing.List[types.InterfaceInfoType]] = None
) -> typing.Optional[types.InterfaceInfoType]:
""" """
returns the inteface with unique_id mac or first interface or None if no interfaces... returns the inteface with unique_id mac or first interface or None if no interfaces...
""" """
interfaces = ( interfaces = interfaces or self._interfaces # Emty interfaces is like "no ip change" because cannot be notified
interfaces or self._interfaces
) # Emty interfaces is like "no ip change" because cannot be notified
if self._cfg.config and interfaces: if self._cfg.config and interfaces:
try: try:
return next( return next(x for x in interfaces if x.mac.lower() == self._cfg.config.unique_id)
x for x in interfaces if x.mac.lower() == self._cfg.config.unique_id
)
except StopIteration: except StopIteration:
return interfaces[0] return interfaces[0]
@ -163,12 +152,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
while self._isAlive: while self._isAlive:
counter -= 1 counter -= 1
try: try:
self._certificate = self._api.ready( self._certificate = self._api.ready(self._cfg.own_token, self._secret, srvInterface.ip, rest.LISTEN_PORT)
self._cfg.own_token,
self._secret,
srvInterface.ip,
rest.LISTEN_PORT,
)
except rest.RESTConnectionError as e: except rest.RESTConnectionError as e:
if not logged: # Only log connection problems ONCE if not logged: # Only log connection problems ONCE
logged = True logged = True
@ -184,9 +168,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
# Success or any error that is not recoverable (retunerd by UDS). if Error, service will be cleaned in a while. # Success or any error that is not recoverable (retunerd by UDS). if Error, service will be cleaned in a while.
break break
else: else:
logger.error( logger.error('Could not locate IP address!!!. (Not registered with UDS)')
'Could not locate IP address!!!. (Not registered with UDS)'
)
# Do not continue if not alive... # Do not continue if not alive...
if not self._isAlive: if not self._isAlive:
@ -194,9 +176,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
# Cleans sensible data # Cleans sensible data
if self._cfg.config: if self._cfg.config:
self._cfg = self._cfg._replace( self._cfg = self._cfg._replace(config=self._cfg.config._replace(os=None), data=None)
config=self._cfg.config._replace(os=None), data=None
)
platform.store.writeConfig(self._cfg) platform.store.writeConfig(self._cfg)
logger.info('Service ready') logger.info('Service ready')
@ -215,10 +195,10 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self._cfg = self._cfg._replace(runonce_command=None) self._cfg = self._cfg._replace(runonce_command=None)
platform.store.writeConfig(self._cfg) platform.store.writeConfig(self._cfg)
if self.execute(runOnce, "runOnce"): if self.execute(runOnce, "runOnce"):
# If runonce is present, will not do anythin more # If runonce is present, will not do anythin more
# So we have to ensure that, when runonce command is finished, reboots the machine. # So we have to ensure that, when runonce command is finished, reboots the machine.
# That is, the COMMAND itself has to restart the machine! # That is, the COMMAND itself has to restart the machine!
return False # If the command fails, continue with the rest of the operations... return False # If the command fails, continue with the rest of the operations...
# Retry configuration while not stop service, config in case of error 10 times, reboot vm # Retry configuration while not stop service, config in case of error 10 times, reboot vm
counter = 10 counter = 10
@ -228,20 +208,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes
if self._cfg.config and self._cfg.config.os: if self._cfg.config and self._cfg.config.os:
osData = self._cfg.config.os osData = self._cfg.config.os
if osData.action == 'rename': if osData.action == 'rename':
self.rename( self.rename(osData.name, osData.username, osData.password, osData.new_password)
osData.name,
osData.username,
osData.password,
osData.new_password,
)
elif osData.action == 'rename_ad': elif osData.action == 'rename_ad':
self.joinDomain( self.joinDomain(osData.name, osData.ad or '', osData.ou or '', osData.username or '', osData.password or '')
osData.name,
osData.ad or '',
osData.ou or '',
osData.username or '',
osData.password or '',
)
if self._rebootRequested: if self._rebootRequested:
try: try:
@ -265,12 +234,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self.getInterfaces() # Ensure we have interfaces self.getInterfaces() # Ensure we have interfaces
if self._cfg.master_token: if self._cfg.master_token:
try: try:
self._certificate = self._api.notifyUnmanagedCallback( self._certificate = self._api.notifyUnmanagedCallback(self._cfg.master_token, self._secret, self._interfaces, rest.LISTEN_PORT)
self._cfg.master_token,
self._secret,
self._interfaces,
rest.LISTEN_PORT,
)
except Exception as e: except Exception as e:
logger.error('Couuld not notify unmanaged callback: %s', e) logger.error('Couuld not notify unmanaged callback: %s', e)
@ -281,17 +245,13 @@ class CommonService: # pylint: disable=too-many-instance-attributes
return return
while self._isAlive: while self._isAlive:
self._interfaces = tools.validNetworkCards( self._interfaces = list(platform.operations.getNetworkInfo())
self._cfg.restrict_net, platform.operations.getNetworkInfo()
)
if self._interfaces: if self._interfaces:
break break
self.doWait(5000) self.doWait(5000)
def initialize(self) -> bool: def initialize(self) -> bool:
if ( if self._initialized or not self._cfg.host or not self._isAlive: # Not configured or not running
self._initialized or not self._cfg.host or not self._isAlive
): # Not configured or not running
return False return False
self._initialized = True self._initialized = True
@ -308,37 +268,25 @@ class CommonService: # pylint: disable=too-many-instance-attributes
try: try:
# If master token is present, initialize and get configuration data # If master token is present, initialize and get configuration data
if self._cfg.master_token: if self._cfg.master_token:
initResult: types.InitializationResultType = self._api.initialize( initResult: types.InitializationResultType = self._api.initialize(self._cfg.master_token, self._interfaces, self._cfg.actorType)
self._cfg.master_token, self._interfaces, self._cfg.actorType
)
if not initResult.own_token: # Not managed if not initResult.own_token: # Not managed
logger.debug( logger.debug('This host is not managed by UDS Broker (ids: {})'.format(self._interfaces))
'This host is not managed by UDS Broker (ids: {})'.format(
self._interfaces
)
)
return False return False
# Only removes master token for managed machines (will need it on next client execution) # Only removes token for managed machines
# For unmanaged, if alias is present, replace master token with it master_token = None if self.isManaged() else self._cfg.master_token
master_token = (
None
if self.isManaged()
else (initResult.alias_token or self._cfg.master_token)
)
# Replace master token with alias token if present
self._cfg = self._cfg._replace( self._cfg = self._cfg._replace(
master_token=master_token, master_token=master_token,
own_token=initResult.own_token, own_token=initResult.own_token,
config=types.ActorDataConfigurationType( config=types.ActorDataConfigurationType(
unique_id=initResult.unique_id, os=initResult.os unique_id=initResult.unique_id,
), os=initResult.os
)
) )
# On first successfull initialization request, master token will dissapear for managed hosts # On first successfull initialization request, master token will dissapear for managed hosts so it will be no more available (not needed anyway)
# so it will be no more available (not needed anyway). For unmanaged, the master token will if self.isManaged():
# be replaced with an alias token. platform.store.writeConfig(self._cfg)
platform.store.writeConfig(self._cfg)
# Setup logger now # Setup logger now
if self._cfg.own_token: if self._cfg.own_token:
@ -346,51 +294,29 @@ class CommonService: # pylint: disable=too-many-instance-attributes
break # Initial configuration done.. break # Initial configuration done..
except rest.RESTConnectionError as e: except rest.RESTConnectionError as e:
logger.info( logger.info('Trying to inititialize connection with broker (last error: {})'.format(e))
'Trying to inititialize connection with broker (last error: {})'.format(
e
)
)
self.doWait(5000) # Wait a bit and retry self.doWait(5000) # Wait a bit and retry
except rest.RESTError as e: # Invalid key? except rest.RESTError as e: # Invalid key?
logger.error( logger.error('Error validating with broker. (Invalid token?): {}'.format(e))
'Error validating with broker. (Invalid token?): {}'.format(e)
)
return False return False
except Exception:
logger.exception()
self.doWait(5000) # Wait a bit and retry...
return self.configureMachine() return self.configureMachine()
def uninitialize(self): def uninitialize(self):
self._initialized = False self._initialized = False
self._cfg = self._cfg._replace( self._cfg = self._cfg._replace(own_token=None) # Ensures assigned token is cleared
own_token=None
) # Ensures assigned token is cleared
def finish(self) -> None: def finish(self) -> None:
if self._http: if self._http:
self._http.stop() self._http.stop()
# If logged in, notify UDS of logout (daemon stoped = no control = logout) # If logged in, notify UDS of logout (daemon stoped = no control = logout)
# For every connected client... if self._loggedIn and self._cfg.own_token:
if self._cfg.own_token: self._loggedIn = False
for client in clients_pool.UDSActorClientPool().clients: try:
if client.session_id: self._api.logout(self._cfg.own_token, '')
try: except Exception as e:
self._api.logout( logger.error('Error notifying final logout to UDS: %s', e)
self._cfg.actorType,
self._cfg.own_token,
'',
client.session_id
or 'stop', # If no session id, pass "stop"
'',
self._interfaces,
self._secret,
)
except Exception as e:
logger.error('Error notifying final logout to UDS: %s', e)
self.notifyStop() self.notifyStop()
@ -399,33 +325,19 @@ class CommonService: # pylint: disable=too-many-instance-attributes
return # Unamanaged hosts does not changes ips. (The full initialize-login-logout process is done in a row, so at login the IP is correct) return # Unamanaged hosts does not changes ips. (The full initialize-login-logout process is done in a row, so at login the IP is correct)
try: try:
if ( if not self._cfg.own_token or not self._cfg.config or not self._cfg.config.unique_id:
not self._cfg.own_token
or not self._cfg.config
or not self._cfg.config.unique_id
):
# Not enouth data do check # Not enouth data do check
return return
currentInterfaces = tools.validNetworkCards( currentInterfaces = list(platform.operations.getNetworkInfo())
self._cfg.restrict_net, platform.operations.getNetworkInfo()
)
old = self.serviceInterfaceInfo() old = self.serviceInterfaceInfo()
new = self.serviceInterfaceInfo(currentInterfaces) new = self.serviceInterfaceInfo(currentInterfaces)
if not new or not old: if not new or not old:
raise Exception( raise Exception('No ip currently available for {}'.format(self._cfg.config.unique_id))
'No ip currently available for {}'.format(
self._cfg.config.unique_id
)
)
if old.ip != new.ip: if old.ip != new.ip:
self._certificate = self._api.notifyIpChange( self._certificate = self._api.notifyIpChange(self._cfg.own_token, self._secret, new.ip, rest.LISTEN_PORT)
self._cfg.own_token, self._secret, new.ip, rest.LISTEN_PORT
)
# Now store new addresses & interfaces... # Now store new addresses & interfaces...
self._interfaces = currentInterfaces self._interfaces = currentInterfaces
logger.info( logger.info('Ip changed from {} to {}. Notified to UDS'.format(old.ip, new.ip))
'Ip changed from {} to {}. Notified to UDS'.format(old.ip, new.ip)
)
# Stop the running HTTP Thread and start a new one, with new generated cert # Stop the running HTTP Thread and start a new one, with new generated cert
self.startHttpServer() self.startHttpServer()
except Exception as e: except Exception as e:
@ -433,34 +345,29 @@ class CommonService: # pylint: disable=too-many-instance-attributes
logger.warn('Checking ips failed: {}'.format(e)) logger.warn('Checking ips failed: {}'.format(e))
def rename( def rename(
self, self,
name: str, name: str,
userName: typing.Optional[str] = None, userName: typing.Optional[str] = None,
oldPassword: typing.Optional[str] = None, oldPassword: typing.Optional[str] = None,
newPassword: typing.Optional[str] = None, newPassword: typing.Optional[str] = None
) -> None: ) -> None:
''' '''
Invoked when broker requests a rename action Invoked when broker requests a rename action
default does nothing default does nothing
''' '''
hostName = platform.operations.getComputerName() hostName = platform.operations.getComputerName()
if hostName.lower() == name.lower():
logger.info('Computer name is already {}'.format(hostName))
return
# Check for password change request for an user # Check for password change request for an user
if userName and newPassword: if userName and newPassword:
logger.info('Setting password for configured user') logger.info('Setting password for configured user')
try: try:
platform.operations.changeUserPassword( platform.operations.changeUserPassword(userName, oldPassword or '', newPassword)
userName, oldPassword or '', newPassword
)
except Exception as e: except Exception as e:
# Logs error, but continue renaming computer raise Exception('Could not change password for user {} (maybe invalid current password is configured at broker): {} '.format(userName, str(e)))
logger.error(
'Could not change password for user {}: {}'.format(userName, e)
)
if hostName.lower() == name.lower():
logger.info('Computer name is already {}'.format(hostName))
return
if platform.operations.renameComputer(name): if platform.operations.renameComputer(name):
self.reboot() self.reboot()
@ -472,9 +379,8 @@ class CommonService: # pylint: disable=too-many-instance-attributes
self.checkIpsChanged() self.checkIpsChanged()
# Now check if every registered client is already there (if logged in OFC) # Now check if every registered client is already there (if logged in OFC)
for lost_client in clients_pool.UDSActorClientPool().lost_clients(): if self._loggedIn and not self._clientsPool.ping():
logger.info('Lost client: {}'.format(lost_client)) self.logout('client_unavailable')
self.logout('client_unavailable', '', lost_client.session_id or '') # '' means "all clients"
except Exception as e: except Exception as e:
logger.error('Exception on main service loop: %s', e) logger.error('Exception on main service loop: %s', e)
@ -482,8 +388,13 @@ class CommonService: # pylint: disable=too-many-instance-attributes
# Methods that can be overriden by linux & windows Actor # Methods that can be overriden by linux & windows Actor
# ****************************************************** # ******************************************************
def joinDomain( # pylint: disable=unused-argument, too-many-arguments def joinDomain( # pylint: disable=unused-argument, too-many-arguments
self, name: str, domain: str, ou: str, account: str, password: str self,
) -> None: name: str,
domain: str,
ou: str,
account: str,
password: str
) -> None:
''' '''
Invoked when broker requests a "domain" action Invoked when broker requests a "domain" action
default does nothing default does nothing
@ -491,86 +402,30 @@ class CommonService: # pylint: disable=too-many-instance-attributes
logger.debug('Base join invoked: {} on {}, {}'.format(name, domain, ou)) logger.debug('Base join invoked: {} on {}, {}'.format(name, domain, ou))
# Client notifications # Client notifications
def login( def login(self, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType:
self, username: str, sessionType: typing.Optional[str] = None result = types.LoginResultInfoType(ip='', hostname='', dead_line=None, max_idle=None)
) -> types.LoginResultInfoType: self._loggedIn = True
result = types.LoginResultInfoType(
ip='', hostname='', dead_line=None, max_idle=None, session_id=None
)
master_token = None
secret = None
# If unmanaged, do initialization now, because we don't know before this
# Also, even if not initialized, get a "login" notification token
if not self.isManaged(): if not self.isManaged():
self._initialized = ( self.initialize()
self.initialize()
) # Maybe it's a local login by an unmanaged host.... On real login, will execute initilize again
# Unamanaged, need the master token
master_token = self._cfg.master_token
secret = self._secret
# Own token will not be set if UDS did not assigned the initialized VM to an user if self._cfg.own_token:
# In that case, take master token (if machine is Unamanaged version) result = self._api.login(self._cfg.own_token, username, sessionType)
token = self._cfg.own_token or master_token
if token:
result = self._api.login(
self._cfg.actorType,
token,
username,
sessionType or '',
self._interfaces,
secret,
)
if (
result.session_id
): # If logged in, process it. client_pool will take account of login response to client and session
script = platform.store.invokeScriptOnLogin()
if script:
logger.info('Executing script on login: {}'.format(script))
script += f'{username} {sessionType or "unknown"} {self._cfg.actorType}'
self.execute(script, 'Logon')
return result return result
def logout( def logout(self, username: str) -> None:
self, self._loggedIn = False
username: str, if self._cfg.own_token:
session_type: typing.Optional[str], self._api.logout(self._cfg.own_token, username)
session_id: typing.Optional[str],
) -> None:
master_token = self._cfg.master_token
# Own token will not be set if UDS did not assigned the initialized VM to an user self.onLogout(username)
# In that case, take master token (if machine is Unamanaged version)
token = self._cfg.own_token or master_token
if token:
# If logout is not processed (that is, not ok result), the logout has not been processed
if (
self._api.logout(
self._cfg.actorType,
token,
username,
session_id or '',
session_type or '',
self._interfaces,
self._secret,
)
!= 'ok' # Can return also "notified", that means the logout has not been processed by UDS
):
logger.info(
'Logout from %s ignored as required by uds broker', username
)
return
self.onLogout(username, session_id or '')
if not self.isManaged(): if not self.isManaged():
self.uninitialize() self.uninitialize()
# ****************************************************** # ****************************************
# Methods that CAN BE overriden by specific OS Actor # Methods that CAN BE overriden by actors
# ****************************************************** # ****************************************
def doWait(self, miliseconds: int) -> None: def doWait(self, miliseconds: int) -> None:
''' '''
Invoked to wait a bit Invoked to wait a bit
@ -589,27 +444,15 @@ class CommonService: # pylint: disable=too-many-instance-attributes
''' '''
logger.info('Service stopped') logger.info('Service stopped')
def preConnect( def preConnect(self, userName: str, protocol: str, ip: str, hostname: str) -> str: # pylint: disable=unused-argument
self, userName: str, protocol: str, ip: str, hostname: str, udsUserName: str
) -> str:
''' '''
Invoked when received a PRE Connection request via REST Invoked when received a PRE Connection request via REST
Base preconnect executes the preconnect command Base preconnect executes the preconnect command
''' '''
if self._cfg.pre_command: if self._cfg.pre_command:
self.execute( self.execute(self._cfg.pre_command + ' {} {} {} {}'.format(userName.replace('"', '%22'), protocol, ip, hostname), 'preConnect')
self._cfg.pre_command
+ ' {} {} {} {} {}'.format(
userName.replace('"', '%22'),
protocol,
ip,
hostname,
udsUserName.replace('"', '%22'),
),
'preConnect',
)
return 'ok' return 'ok'
def onLogout(self, userName: str, session_id: str) -> None: def onLogout(self, userName: str) -> None:
logger.debug('On logout invoked for {}'.format(userName)) logger.debug('On logout invoked for {}'.format(userName))

View File

@ -28,113 +28,20 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
# pylint: disable=invalid-name
import threading import threading
import ipaddress
import time
import typing
import functools
if typing.TYPE_CHECKING: from udsactor.log import logger
from udsactor.types import InterfaceInfoType
# Simple cache for n seconds (default = 30) decorator
def cache(seconds: int = 30) -> typing.Callable:
'''
Simple cache for n seconds (default = 30) decorator
'''
def decorator(func) -> typing.Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> typing.Any:
if not hasattr(wrapper, 'cache'):
wrapper.cache = {} # type: ignore
cache = wrapper.cache # type: ignore
# Compose a key for the cache
key = '{}:{}'.format(args, kwargs)
if key in cache:
if time.time() - cache[key][0] < seconds:
return cache[key][1]
# Call the function
result = func(*args, **kwargs)
cache[key] = (time.time(), result)
return result
return wrapper
return decorator
# Simple sub-script exectution thread
class ScriptExecutorThread(threading.Thread): class ScriptExecutorThread(threading.Thread):
def __init__(self, script: str) -> None: def __init__(self, script: str) -> None:
super(ScriptExecutorThread, self).__init__() super(ScriptExecutorThread, self).__init__()
self.script = script self.script = script
def run(self) -> None: def run(self) -> None:
from udsactor.log import logger
try: try:
logger.debug('Executing script: {}'.format(self.script)) logger.debug('Executing script: {}'.format(self.script))
exec( exec(self.script, globals(), None) # pylint: disable=exec-used
self.script, globals(), None
) # nosec: exec is fine, it's a "trusted" script
except Exception as e: except Exception as e:
logger.error('Error executing script: {}'.format(e)) logger.error('Error executing script: {}'.format(e))
logger.exception() logger.exception()
class Singleton(type):
'''
Metaclass for singleton pattern
Usage:
class MyClass(metaclass=Singleton):
...
'''
_instance: typing.Optional[typing.Any]
# We use __init__ so we customise the created class from this metaclass
def __init__(self, *args, **kwargs) -> None:
self._instance = None
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs) -> typing.Any:
if self._instance is None:
self._instance = super().__call__(*args, **kwargs)
return self._instance
# Convert "X.X.X.X/X" to ipaddress.IPv4Network
def strToNoIPV4Network(
net: typing.Optional[str],
) -> typing.Optional[ipaddress.IPv4Network]:
if not net: # Empty or None
return None
try:
return ipaddress.IPv4Interface(net).network
except Exception:
return None
def validNetworkCards(
net: typing.Optional[str], cards: typing.Iterable['InterfaceInfoType']
) -> typing.List['InterfaceInfoType']:
try:
subnet = strToNoIPV4Network(net)
except Exception as e:
subnet = None
if subnet is None:
return list(cards)
def isValid(ip: str, subnet: ipaddress.IPv4Network) -> bool:
if not ip:
return False
try:
return ipaddress.IPv4Address(ip) in subnet
except Exception:
return False
return [c for c in cards if isValid(c.ip, subnet)]

View File

@ -35,7 +35,6 @@ class ActorConfigurationType(typing.NamedTuple):
actorType: typing.Optional[str] = None actorType: typing.Optional[str] = None
master_token: typing.Optional[str] = None master_token: typing.Optional[str] = None
own_token: typing.Optional[str] = None own_token: typing.Optional[str] = None
restrict_net: typing.Optional[str] = None
pre_command: typing.Optional[str] = None pre_command: typing.Optional[str] = None
runonce_command: typing.Optional[str] = None runonce_command: typing.Optional[str] = None
@ -51,22 +50,12 @@ class InitializationResultType(typing.NamedTuple):
own_token: typing.Optional[str] = None own_token: typing.Optional[str] = None
unique_id: typing.Optional[str] = None unique_id: typing.Optional[str] = None
os: typing.Optional[ActorOsConfigurationType] = None os: typing.Optional[ActorOsConfigurationType] = None
alias_token: typing.Optional[str] = None
class LoginResultInfoType(typing.NamedTuple): class LoginResultInfoType(typing.NamedTuple):
ip: str ip: str
hostname: str hostname: str
dead_line: typing.Optional[int] dead_line: typing.Optional[int]
max_idle: typing.Optional[int] max_idle: typing.Optional[int] # Not provided by broker
session_id: typing.Optional[str]
@property
def logged_in(self) -> bool:
return bool(self.session_id)
class ClientInfo(typing.NamedTuple):
url: str
session_id: str
class CertificateInfoType(typing.NamedTuple): class CertificateInfoType(typing.NamedTuple):
private_key: str private_key: str

View File

@ -1,2 +0,0 @@
VERSION = '4.0.0'
BUILD = '20220901'

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014-2022 Virtual Cable S.L.U. # Copyright (c) 2014-2019 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -11,7 +11,7 @@
# * Redistributions in binary form must reproduce the above copyright notice, # * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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 # may be used to endorse or promote products derived from this software
# without specific prior written permission. # without specific prior written permission.
# #

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014-2022 Virtual Cable S.L.U. # Copyright (c) 2014 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -11,7 +11,7 @@
# * Redistributions in binary form must reproduce the above copyright notice, # * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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 # may be used to endorse or promote products derived from this software
# without specific prior written permission. # without specific prior written permission.
# #
@ -34,7 +34,7 @@ import os
import tempfile import tempfile
import typing import typing
import servicemanager import servicemanager # pylint: disable=import-error
# Valid logging levels, from UDS Broker (uds.core.utils.log). # Valid logging levels, from UDS Broker (uds.core.utils.log).
from .. import loglevel from .. import loglevel
@ -42,7 +42,6 @@ from .. import loglevel
class LocalLogger: # pylint: disable=too-few-public-methods class LocalLogger: # pylint: disable=too-few-public-methods
linux = False linux = False
windows = True windows = True
serviceLogger = False
logger: typing.Optional[logging.Logger] logger: typing.Optional[logging.Logger]

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014-2022 Virtual Cable S.L.U. # Copyright (c) 2014-2019 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -11,7 +11,7 @@
# * Redistributions in binary form must reproduce the above copyright notice, # * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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 # may be used to endorse or promote products derived from this software
# without specific prior written permission. # without specific prior written permission.
# #
@ -45,46 +45,32 @@ import win32con
from .. import types from .. import types
from ..log import logger from ..log import logger
def checkPermissions() -> bool: def checkPermissions() -> bool:
return shell.IsUserAnAdmin() return shell.IsUserAnAdmin()
def getErrorMessage(resultCode: int = 0) -> str: def getErrorMessage(resultCode: int = 0) -> str:
# sys_fs_enc = sys.getfilesystemencoding() or 'mbcs' # sys_fs_enc = sys.getfilesystemencoding() or 'mbcs'
msg = win32api.FormatMessage(resultCode) msg = win32api.FormatMessage(resultCode)
return msg return msg
def getComputerName() -> str: def getComputerName() -> str:
return win32api.GetComputerNameEx(win32con.ComputerNamePhysicalDnsHostname) return win32api.GetComputerNameEx(win32con.ComputerNamePhysicalDnsHostname)
def getNetworkInfo() -> typing.Iterator[types.InterfaceInfoType]: def getNetworkInfo() -> typing.Iterator[types.InterfaceInfoType]:
obj = win32com.client.Dispatch("WbemScripting.SWbemLocator") obj = win32com.client.Dispatch("WbemScripting.SWbemLocator")
wmobj = obj.ConnectServer("localhost", "root\\cimv2") wmobj = obj.ConnectServer("localhost", "root\\cimv2")
adapters = wmobj.ExecQuery( adapters = wmobj.ExecQuery("Select * from Win32_NetworkAdapterConfiguration where IpEnabled=True")
"Select * from Win32_NetworkAdapterConfiguration where IpEnabled=True"
)
try: try:
for obj in adapters: for obj in adapters:
for ip in obj.IPAddress: for ip in obj.IPAddress:
if ':' in ip: # Is IPV6, skip this if ':' in ip: # Is IPV6, skip this
continue continue
if ( if ip is None or ip == '' or ip.startswith('169.254') or ip.startswith('0.'): # If single link ip, or no ip
ip is None
or ip == ''
or ip.startswith('169.254')
or ip.startswith('0.')
): # If single link ip, or no ip
continue continue
yield types.InterfaceInfoType( yield types.InterfaceInfoType(name=obj.Caption, mac=obj.MACAddress, ip=ip)
name=obj.Caption, mac=obj.MACAddress, ip=ip
)
except Exception: except Exception:
return return
def getDomainName() -> str: def getDomainName() -> str:
''' '''
Will return the domain name if we belong a domain, else None Will return the domain name if we belong a domain, else None
@ -101,19 +87,9 @@ def getDomainName() -> str:
return domain return domain
def getWindowsVersion() -> typing.Tuple[int, int, int, int, str]: def getWindowsVersion() -> typing.Tuple[int, int, int, int, str]:
return win32api.GetVersionEx() return win32api.GetVersionEx()
def getVersion() -> str:
verinfo = getWindowsVersion()
# Remove platform id i
return 'Windows-{}.{} Build {} ({})'.format(
verinfo[0], verinfo[1], verinfo[2], verinfo[4]
)
EWX_LOGOFF = 0x00000000 EWX_LOGOFF = 0x00000000
EWX_SHUTDOWN = 0x00000001 EWX_SHUTDOWN = 0x00000001
EWX_REBOOT = 0x00000002 EWX_REBOOT = 0x00000002
@ -121,53 +97,31 @@ EWX_FORCE = 0x00000004
EWX_POWEROFF = 0x00000008 EWX_POWEROFF = 0x00000008
EWX_FORCEIFHUNG = 0x00000010 EWX_FORCEIFHUNG = 0x00000010
def reboot(flags: int = EWX_FORCEIFHUNG | EWX_REBOOT) -> None: def reboot(flags: int = EWX_FORCEIFHUNG | EWX_REBOOT) -> None:
hproc = win32api.GetCurrentProcess() hproc = win32api.GetCurrentProcess()
htok = win32security.OpenProcessToken( htok = win32security.OpenProcessToken(hproc, win32security.TOKEN_ADJUST_PRIVILEGES | win32security.TOKEN_QUERY)
hproc, win32security.TOKEN_ADJUST_PRIVILEGES | win32security.TOKEN_QUERY privs = ((win32security.LookupPrivilegeValue(None, win32security.SE_SHUTDOWN_NAME), win32security.SE_PRIVILEGE_ENABLED),)
)
privs = (
(
win32security.LookupPrivilegeValue(None, win32security.SE_SHUTDOWN_NAME),
win32security.SE_PRIVILEGE_ENABLED,
),
)
win32security.AdjustTokenPrivileges(htok, 0, privs) win32security.AdjustTokenPrivileges(htok, 0, privs)
win32api.ExitWindowsEx(flags, 0) win32api.ExitWindowsEx(flags, 0)
def loggoff() -> None: def loggoff() -> None:
win32api.ExitWindowsEx(EWX_LOGOFF) win32api.ExitWindowsEx(EWX_LOGOFF)
def renameComputer(newName: str) -> bool: def renameComputer(newName: str) -> bool:
''' '''
Changes the computer name Changes the computer name
Returns True if reboot needed Returns True if reboot needed
''' '''
# Needs admin privileges to work # Needs admin privileges to work
if ( if ctypes.windll.kernel32.SetComputerNameExW(DWORD(win32con.ComputerNamePhysicalDnsHostname), LPCWSTR(newName)) == 0: # @UndefinedVariable
ctypes.windll.kernel32.SetComputerNameExW(
DWORD(win32con.ComputerNamePhysicalDnsHostname), LPCWSTR(newName)
)
== 0
): # @UndefinedVariable
# win32api.FormatMessage -> returns error string # win32api.FormatMessage -> returns error string
# win32api.GetLastError -> returns error code # win32api.GetLastError -> returns error code
# (just put this comment here to remember to log this when logger is available) # (just put this comment here to remember to log this when logger is available)
error = getErrorMessage() error = getErrorMessage()
computerName = win32api.GetComputerNameEx( computerName = win32api.GetComputerNameEx(win32con.ComputerNamePhysicalDnsHostname)
win32con.ComputerNamePhysicalDnsHostname raise Exception('Error renaming computer from {} to {}: {}'.format(computerName, newName, error))
)
raise Exception(
'Error renaming computer from {} to {}: {}'.format(
computerName, newName, error
)
)
return True return True
NETSETUP_JOIN_DOMAIN = 0x00000001 NETSETUP_JOIN_DOMAIN = 0x00000001
NETSETUP_ACCT_CREATE = 0x00000002 NETSETUP_ACCT_CREATE = 0x00000002
NETSETUP_ACCT_DELETE = 0x00000004 NETSETUP_ACCT_DELETE = 0x00000004
@ -178,10 +132,7 @@ NETSETUP_MACHINE_PWD_PASSED = 0x00000080
NETSETUP_JOIN_WITH_NEW_NAME = 0x00000400 NETSETUP_JOIN_WITH_NEW_NAME = 0x00000400
NETSETUP_DEFER_SPN_SET = 0x1000000 NETSETUP_DEFER_SPN_SET = 0x1000000
def joinDomain(domain: str, ou: str, account: str, password: str, executeInOneStep: bool = False) -> None:
def joinDomain(
domain: str, ou: str, account: str, password: str, executeInOneStep: bool = False
) -> None:
''' '''
Joins machine to a windows domain Joins machine to a windows domain
:param domain: Domain to join to :param domain: Domain to join to
@ -198,9 +149,7 @@ def joinDomain(
account = domain + '\\' + account account = domain + '\\' + account
# Do log # Do log
flags: typing.Any = ( flags: typing.Any = NETSETUP_ACCT_CREATE | NETSETUP_DOMAIN_JOIN_IF_JOINED | NETSETUP_JOIN_DOMAIN
NETSETUP_ACCT_CREATE | NETSETUP_DOMAIN_JOIN_IF_JOINED | NETSETUP_JOIN_DOMAIN
)
if executeInOneStep: if executeInOneStep:
flags |= NETSETUP_JOIN_WITH_NEW_NAME flags |= NETSETUP_JOIN_WITH_NEW_NAME
@ -214,31 +163,18 @@ def joinDomain(
lpAccount = LPCWSTR(account) lpAccount = LPCWSTR(account)
lpPassword = LPCWSTR(password) lpPassword = LPCWSTR(password)
res = ctypes.windll.netapi32.NetJoinDomain( res = ctypes.windll.netapi32.NetJoinDomain(None, lpDomain, lpOu, lpAccount, lpPassword, flags)
None, lpDomain, lpOu, lpAccount, lpPassword, flags
)
# Machine found in another ou, use it and warn this on log # Machine found in another ou, use it and warn this on log
if res == 2224: if res == 2224:
flags = DWORD(NETSETUP_DOMAIN_JOIN_IF_JOINED | NETSETUP_JOIN_DOMAIN) flags = DWORD(NETSETUP_DOMAIN_JOIN_IF_JOINED | NETSETUP_JOIN_DOMAIN)
res = ctypes.windll.netapi32.NetJoinDomain( res = ctypes.windll.netapi32.NetJoinDomain(None, lpDomain, None, lpAccount, lpPassword, flags)
None, lpDomain, None, lpAccount, lpPassword, flags
)
if res: if res:
# Log the error # Log the error
error = getErrorMessage(res) error = getErrorMessage(res)
if res == 1355: if res == 1355:
error = "DC Is not reachable" error = "DC Is not reachable"
logger.error('Error joining domain: {}, {}'.format(error, res)) logger.error('Error joining domain: {}, {}'.format(error, res))
raise Exception( raise Exception('Error joining domain {}, with credentials {}/*****{}: {}, {}'.format(domain, account, ', under OU {}'.format(ou) if ou is not None else '', res, error))
'Error joining domain {}, with credentials {}/*****{}: {}, {}'.format(
domain,
account,
', under OU {}'.format(ou) if ou is not None else '',
res,
error,
)
)
def changeUserPassword(user: str, oldPassword: str, newPassword: str) -> None: def changeUserPassword(user: str, oldPassword: str, newPassword: str) -> None:
# lpUser = LPCWSTR(user) # lpUser = LPCWSTR(user)
@ -252,10 +188,7 @@ def changeUserPassword(user: str, oldPassword: str, newPassword: str) -> None:
if res: if res:
# Log the error, and raise exception to parent # Log the error, and raise exception to parent
error = getErrorMessage(res) error = getErrorMessage(res)
raise Exception( raise Exception('Error changing password for user {}: {} {}'.format(user, res, error))
'Error changing password for user {}: {} {}'.format(user, res, error)
)
class LASTINPUTINFO(ctypes.Structure): # pylint: disable=too-few-public-methods class LASTINPUTINFO(ctypes.Structure): # pylint: disable=too-few-public-methods
_fields_ = [ _fields_ = [
@ -263,20 +196,16 @@ class LASTINPUTINFO(ctypes.Structure): # pylint: disable=too-few-public-methods
('dwTime', ctypes.c_uint), ('dwTime', ctypes.c_uint),
] ]
def initIdleDuration(atLeastSeconds: int): # pylint: disable=unused-argument def initIdleDuration(atLeastSeconds: int): # pylint: disable=unused-argument
''' '''
In windows, there is no need to set screensaver In windows, there is no need to set screensaver
''' '''
return return
def getIdleDuration() -> float: def getIdleDuration() -> float:
try: try:
lastInputInfo = LASTINPUTINFO() lastInputInfo = LASTINPUTINFO()
lastInputInfo.cbSize = ctypes.sizeof( lastInputInfo.cbSize = ctypes.sizeof(lastInputInfo) # pylint: disable=attribute-defined-outside-init
lastInputInfo
) # pylint: disable=attribute-defined-outside-init
if ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lastInputInfo)) == 0: if ctypes.windll.user32.GetLastInputInfo(ctypes.byref(lastInputInfo)) == 0:
return 0 return 0
current = ctypes.c_uint(ctypes.windll.kernel32.GetTickCount()).value current = ctypes.c_uint(ctypes.windll.kernel32.GetTickCount()).value
@ -288,27 +217,22 @@ def getIdleDuration() -> float:
logger.error('Getting idle duration: {}'.format(e)) logger.error('Getting idle duration: {}'.format(e))
return 0 return 0
def getCurrentUser() -> str: def getCurrentUser() -> str:
''' '''
Returns current logged in username Returns current logged in username
''' '''
return os.environ['USERNAME'] return os.environ['USERNAME']
def getSessionType() -> str: def getSessionType() -> str:
''' '''
Known values: Known values:
* Unknown -> No SESSIONNAME environment variable * Unknown -> No SESSIONNAME environment variable
* Console -> Local session * Console -> Local session
* RDP-Tcp#[0-9]+ -> RDP Session * RDP-Tcp#[0-9]+ -> RDP Session
''' '''
return os.environ.get('SESSIONNAME', 'unknown') return os.environ.get('SESSIONNAME', 'unknown')
def writeToPipe(pipeName: str, bytesPayload: bytes, waitForResponse: bool) -> typing.Optional[bytes]:
def writeToPipe(
pipeName: str, bytesPayload: bytes, waitForResponse: bool
) -> typing.Optional[bytes]:
# (str, bytes, bool) -> Optional[bytes] # (str, bytes, bool) -> Optional[bytes]
try: try:
with open(pipeName, 'r+b', 0) as f: with open(pipeName, 'r+b', 0) as f:
@ -320,11 +244,8 @@ def writeToPipe(
except Exception: except Exception:
return None return None
def forceTimeSync() -> None: def forceTimeSync() -> None:
try: try:
subprocess.call( subprocess.call([r'c:\WINDOWS\System32\w32tm.exe', ' /resync']) # , '/rediscover'])
[r'c:\WINDOWS\System32\w32tm.exe', ' /resync']
) # , '/rediscover'])
except Exception as e: except Exception as e:
logger.error('Error invoking time sync command: %s', e) logger.error('Error invoking time sync command: %s', e)

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2019-2022 Virtual Cable S.L.U. # Copyright (c) 2019 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -11,7 +11,7 @@
# * Redistributions in binary form must reproduce the above copyright notice, # * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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 # may be used to endorse or promote products derived from this software
# without specific prior written permission. # without specific prior written permission.
# #
@ -41,8 +41,6 @@ from .service import UDSActorSvc
def setupRecoverService(): def setupRecoverService():
svc_name = UDSActorSvc._svc_name_ # pylint: disable=protected-access svc_name = UDSActorSvc._svc_name_ # pylint: disable=protected-access
hs = None
hscm = None
try: try:
hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS) hscm = win32service.OpenSCManager(None, None, win32service.SC_MANAGER_ALL_ACCESS)
@ -59,11 +57,9 @@ def setupRecoverService():
} }
win32service.ChangeServiceConfig2(hs, win32service.SERVICE_CONFIG_FAILURE_ACTIONS, service_failure_actions) win32service.ChangeServiceConfig2(hs, win32service.SERVICE_CONFIG_FAILURE_ACTIONS, service_failure_actions)
finally: finally:
if hs: win32service.CloseServiceHandle(hs)
win32service.CloseServiceHandle(hs)
finally: finally:
if hscm: win32service.CloseServiceHandle(hscm)
win32service.CloseServiceHandle(hscm)
def run() -> None: def run() -> None:

View File

@ -39,7 +39,6 @@ import win32net
import win32event import win32event
import pythoncom import pythoncom
import servicemanager import servicemanager
import winreg as wreg
from . import operations from . import operations
from . import store from . import store
@ -139,7 +138,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
logger.info('Using multiple step join because configuration requests to do so') logger.info('Using multiple step join because configuration requests to do so')
self.multiStepJoin(name, domain, ou, account, password) self.multiStepJoin(name, domain, ou, account, password)
def preConnect(self, userName: str, protocol: str, ip: str, hostname: str, udsUserName: str) -> str: def preConnect(self, userName: str, protocol: str, ip: str, hostname: str) -> str:
logger.debug('Pre connect invoked') logger.debug('Pre connect invoked')
if protocol == 'rdp': # If connection is not using rdp, skip adding user if protocol == 'rdp': # If connection is not using rdp, skip adding user
@ -168,7 +167,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
self._user = None self._user = None
logger.debug('User {} already in group'.format(userName)) logger.debug('User {} already in group'.format(userName))
return super().preConnect(userName, protocol, ip, hostname, udsUserName) return super().preConnect(userName, protocol, ip, hostname)
def ovLogon(self, username: str, password: str) -> str: def ovLogon(self, username: str, password: str) -> str:
""" """
@ -183,7 +182,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
operations.writeToPipe("\\\\.\\pipe\\VDSMDPipe", packet, True) operations.writeToPipe("\\\\.\\pipe\\VDSMDPipe", packet, True)
return 'done' return 'done'
def onLogout(self, userName: str, session_id: str) -> None: def onLogout(self, userName) -> None:
logger.debug('Windows onLogout invoked: {}, {}'.format(userName, self._user)) logger.debug('Windows onLogout invoked: {}, {}'.format(userName, self._user))
try: try:
p = win32security.GetBinarySid(REMOTE_USERS_SID) p = win32security.GetBinarySid(REMOTE_USERS_SID)
@ -198,18 +197,6 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
except Exception as e: except Exception as e:
logger.error('Exception removing user from Remote Desktop Users: {}'.format(e)) logger.error('Exception removing user from Remote Desktop Users: {}'.format(e))
def isInstallationRunning(self):
'''
Detect if windows is installing anything, so we can delay the execution of Service
'''
try:
key = wreg.OpenKey(wreg.HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\State')
data, _ = wreg.QueryValueEx(key, 'ImageState')
logger.debug('State: %s', data)
return data != 'IMAGE_STATE_COMPLETE' # If ImageState is different of ImageStateComplete, there is something running on installation
except Exception: # If not found, means that no installation is running
return False
def SvcDoRun(self) -> None: # pylint: disable=too-many-statements, too-many-branches def SvcDoRun(self) -> None: # pylint: disable=too-many-statements, too-many-branches
''' '''
Main service loop Main service loop
@ -222,17 +209,6 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
pythoncom.CoInitialize() # pylint: disable=no-member pythoncom.CoInitialize() # pylint: disable=no-member
# Check if some install is running on windows before proceeding
while self._isAlive:
if self.isInstallationRunning():
win32event.WaitForSingleObject(self._hWaitStop, 1000) # Wait a bit, and check again
continue
break
if not self._isAlive: # Has been stopped while waiting windows installations
self.finish()
return
# Unmanaged services does not initializes "on start", but rather when user logs in (because userservice does not exists "as such" before that) # Unmanaged services does not initializes "on start", but rather when user logs in (because userservice does not exists "as such" before that)
if self.isManaged(): if self.isManaged():
if not self.initialize(): if not self.initialize():

View File

@ -76,9 +76,9 @@ def writeConfig(config: types.ActorConfigurationType) -> None:
except Exception: except Exception:
key = wreg.CreateKeyEx(BASEKEY, PATH, 0, wreg.KEY_ALL_ACCESS) key = wreg.CreateKeyEx(BASEKEY, PATH, 0, wreg.KEY_ALL_ACCESS)
fixRegistryPermissions(key.handle) # type: ignore fixRegistryPermissions(key.handle)
wreg.SetValueEx(key, "", 0, wreg.REG_BINARY, pickle.dumps(config)) # type: ignore wreg.SetValueEx(key, "", 0, wreg.REG_BINARY, pickle.dumps(config))
wreg.CloseKey(key) wreg.CloseKey(key)
@ -94,16 +94,3 @@ def useOldJoinSystem() -> bool:
data = '' data = ''
return data == 'old' return data == 'old'
def invokeScriptOnLogin() -> str:
try:
key = wreg.OpenKey(BASEKEY, PATH, 0, wreg.KEY_QUERY_VALUE)
try:
data, _ = wreg.QueryValueEx(key, 'logonScript')
except Exception:
data = ''
wreg.CloseKey(key)
except Exception:
data = ''
return data

View File

@ -2,10 +2,9 @@
# Form implementation generated from reading ui file 'setup-dialog.ui' # Form implementation generated from reading ui file 'setup-dialog.ui'
# #
# Created by: PyQt5 UI code generator 5.15.2 # Created by: PyQt5 UI code generator 5.13.2
# #
# WARNING: Any manual changes made to this file will be lost when pyuic5 is # WARNING! All changes made in this file will be lost!
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets

View File

@ -2,10 +2,9 @@
# Form implementation generated from reading ui file 'setup-dialog-unmanaged.ui' # Form implementation generated from reading ui file 'setup-dialog-unmanaged.ui'
# #
# Created by: PyQt5 UI code generator 5.15.2 # Created by: PyQt5 UI code generator 5.13.2
# #
# WARNING: Any manual changes made to this file will be lost when pyuic5 is # WARNING! All changes made in this file will be lost!
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
@ -15,7 +14,7 @@ class Ui_UdsActorSetupDialog(object):
def setupUi(self, UdsActorSetupDialog): def setupUi(self, UdsActorSetupDialog):
UdsActorSetupDialog.setObjectName("UdsActorSetupDialog") UdsActorSetupDialog.setObjectName("UdsActorSetupDialog")
UdsActorSetupDialog.setWindowModality(QtCore.Qt.WindowModal) UdsActorSetupDialog.setWindowModality(QtCore.Qt.WindowModal)
UdsActorSetupDialog.resize(601, 243) UdsActorSetupDialog.resize(595, 220)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
@ -35,12 +34,12 @@ class Ui_UdsActorSetupDialog(object):
UdsActorSetupDialog.setModal(True) UdsActorSetupDialog.setModal(True)
self.saveButton = QtWidgets.QPushButton(UdsActorSetupDialog) self.saveButton = QtWidgets.QPushButton(UdsActorSetupDialog)
self.saveButton.setEnabled(True) self.saveButton.setEnabled(True)
self.saveButton.setGeometry(QtCore.QRect(10, 210, 181, 23)) self.saveButton.setGeometry(QtCore.QRect(10, 180, 181, 23))
self.saveButton.setMinimumSize(QtCore.QSize(181, 0)) self.saveButton.setMinimumSize(QtCore.QSize(181, 0))
self.saveButton.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu) self.saveButton.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu)
self.saveButton.setObjectName("saveButton") self.saveButton.setObjectName("saveButton")
self.closeButton = QtWidgets.QPushButton(UdsActorSetupDialog) self.closeButton = QtWidgets.QPushButton(UdsActorSetupDialog)
self.closeButton.setGeometry(QtCore.QRect(410, 210, 171, 23)) self.closeButton.setGeometry(QtCore.QRect(410, 180, 171, 23))
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0) sizePolicy.setVerticalStretch(0)
@ -50,11 +49,11 @@ class Ui_UdsActorSetupDialog(object):
self.closeButton.setObjectName("closeButton") self.closeButton.setObjectName("closeButton")
self.testButton = QtWidgets.QPushButton(UdsActorSetupDialog) self.testButton = QtWidgets.QPushButton(UdsActorSetupDialog)
self.testButton.setEnabled(False) self.testButton.setEnabled(False)
self.testButton.setGeometry(QtCore.QRect(210, 210, 181, 23)) self.testButton.setGeometry(QtCore.QRect(210, 180, 181, 23))
self.testButton.setMinimumSize(QtCore.QSize(181, 0)) self.testButton.setMinimumSize(QtCore.QSize(181, 0))
self.testButton.setObjectName("testButton") self.testButton.setObjectName("testButton")
self.layoutWidget = QtWidgets.QWidget(UdsActorSetupDialog) self.layoutWidget = QtWidgets.QWidget(UdsActorSetupDialog)
self.layoutWidget.setGeometry(QtCore.QRect(10, 10, 571, 191)) self.layoutWidget.setGeometry(QtCore.QRect(10, 10, 571, 161))
self.layoutWidget.setObjectName("layoutWidget") self.layoutWidget.setObjectName("layoutWidget")
self.formLayout = QtWidgets.QFormLayout(self.layoutWidget) self.formLayout = QtWidgets.QFormLayout(self.layoutWidget)
self.formLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) self.formLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
@ -85,7 +84,7 @@ class Ui_UdsActorSetupDialog(object):
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.serviceToken) self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.serviceToken)
self.label_loglevel = QtWidgets.QLabel(self.layoutWidget) self.label_loglevel = QtWidgets.QLabel(self.layoutWidget)
self.label_loglevel.setObjectName("label_loglevel") self.label_loglevel.setObjectName("label_loglevel")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.label_loglevel) self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_loglevel)
self.logLevelComboBox = QtWidgets.QComboBox(self.layoutWidget) self.logLevelComboBox = QtWidgets.QComboBox(self.layoutWidget)
self.logLevelComboBox.setFrame(True) self.logLevelComboBox.setFrame(True)
self.logLevelComboBox.setObjectName("logLevelComboBox") self.logLevelComboBox.setObjectName("logLevelComboBox")
@ -97,13 +96,7 @@ class Ui_UdsActorSetupDialog(object):
self.logLevelComboBox.setItemText(2, "ERROR") self.logLevelComboBox.setItemText(2, "ERROR")
self.logLevelComboBox.addItem("") self.logLevelComboBox.addItem("")
self.logLevelComboBox.setItemText(3, "FATAL") self.logLevelComboBox.setItemText(3, "FATAL")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.logLevelComboBox) self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.logLevelComboBox)
self.label_restrictNet = QtWidgets.QLabel(self.layoutWidget)
self.label_restrictNet.setObjectName("label_restrictNet")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_restrictNet)
self.restrictNet = QtWidgets.QLineEdit(self.layoutWidget)
self.restrictNet.setObjectName("restrictNet")
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.restrictNet)
self.label_host.raise_() self.label_host.raise_()
self.host.raise_() self.host.raise_()
self.label_serviceToken.raise_() self.label_serviceToken.raise_()
@ -112,8 +105,6 @@ class Ui_UdsActorSetupDialog(object):
self.label_security.raise_() self.label_security.raise_()
self.label_loglevel.raise_() self.label_loglevel.raise_()
self.logLevelComboBox.raise_() self.logLevelComboBox.raise_()
self.label_restrictNet.raise_()
self.restrictNet.raise_()
self.retranslateUi(UdsActorSetupDialog) self.retranslateUi(UdsActorSetupDialog)
self.logLevelComboBox.setCurrentIndex(1) self.logLevelComboBox.setCurrentIndex(1)
@ -122,7 +113,6 @@ class Ui_UdsActorSetupDialog(object):
self.saveButton.clicked.connect(UdsActorSetupDialog.saveConfig) self.saveButton.clicked.connect(UdsActorSetupDialog.saveConfig)
self.host.textChanged['QString'].connect(UdsActorSetupDialog.configChanged) self.host.textChanged['QString'].connect(UdsActorSetupDialog.configChanged)
self.serviceToken.textChanged['QString'].connect(UdsActorSetupDialog.configChanged) self.serviceToken.textChanged['QString'].connect(UdsActorSetupDialog.configChanged)
self.restrictNet.textChanged['QString'].connect(UdsActorSetupDialog.configChanged)
QtCore.QMetaObject.connectSlotsByName(UdsActorSetupDialog) QtCore.QMetaObject.connectSlotsByName(UdsActorSetupDialog)
def retranslateUi(self, UdsActorSetupDialog): def retranslateUi(self, UdsActorSetupDialog):
@ -146,10 +136,7 @@ class Ui_UdsActorSetupDialog(object):
self.host.setToolTip(_translate("UdsActorSetupDialog", "Uds Broker Server Addres. Use IP or FQDN")) 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.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.label_serviceToken.setText(_translate("UdsActorSetupDialog", "Service Token"))
self.serviceToken.setToolTip(_translate("UdsActorSetupDialog", "UDS Service Token")) self.serviceToken.setToolTip(_translate("UdsActorSetupDialog", "UDS user with administration rights (Will not be stored on template)"))
self.serviceToken.setWhatsThis(_translate("UdsActorSetupDialog", "<html><head/><body><p>Administrator user on UDS Server.</p><p>Note: This credential will not be stored on client. Will be used to obtain an unique token for this image.</p></body></html>")) self.serviceToken.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_loglevel.setText(_translate("UdsActorSetupDialog", "Log Level"))
self.label_restrictNet.setText(_translate("UdsActorSetupDialog", "Restrict Net"))
self.restrictNet.setToolTip(_translate("UdsActorSetupDialog", "UDS user with administration rights (Will not be stored on template)"))
self.restrictNet.setWhatsThis(_translate("UdsActorSetupDialog", "<html><head/><body><p>Administrator user on UDS Server.</p><p>Note: This credential will not be stored on client. Will be used to obtain an unique token for this image.</p></body></html>"))
from ui import uds_rc from ui import uds_rc

View File

@ -2,7 +2,7 @@
# Resource object code # Resource object code
# #
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) # Created by: The Resource Compiler for PyQt5 (Qt v5.13.2)
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!

View File

@ -1,2 +0,0 @@
PYTHONPATH=./src:${PYTHONPATH}

View File

@ -2,7 +2,3 @@
/udsclient-[0-9]*.spec /udsclient-[0-9]*.spec
/debian/udsclient /debian/udsclient
/targz /targz
/UDSClientDir
/UDSClient*.AppImage
/appimage*
/UDSClient.desktop

View File

@ -14,8 +14,6 @@ APPSDIR := $(DESTDIR)/usr/share/applications
PYC := $(shell find $(SOURCEDIR) -name '*.py[co]') PYC := $(shell find $(SOURCEDIR) -name '*.py[co]')
CACHES := $(shell find $(SOURCEDIR) -name '__pycache__') CACHES := $(shell find $(SOURCEDIR) -name '__pycache__')
clean: clean:
rm -rf $(PYC) $(CACHES) $(DESTDIR) rm -rf $(PYC) $(CACHES) $(DESTDIR)
install: install:
@ -48,60 +46,8 @@ endif
ifeq ($(DISTRO),rh) ifeq ($(DISTRO),rh)
endif endif
# chmod 0755 $(BINDIR)/udsclient
uninstall: uninstall:
rm -rf $(LIBDIR) rm -rf $(LIBDIR)
# rm -f $(BINDIR)/udsclient # rm -f $(BINDIR)/udsclient
# rm -rf $(CFGDIR) # rm -rf $(CFGDIR)
build-appimage:
ifeq ($(DISTRO),x86_64)
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g > appimage.recipe
endif
ifeq ($(DISTRO),armhf)
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g | sed -e s/amd64/armhf/g | sed -e s/x86_64/armhf/g > appimage.recipe
endif
ifeq ($(DISTRO),i686)
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g | sed -e s/amd64/i386/g | sed -e s/x86_64/i686/g > appimage.recipe
endif
# Ensure all working folders are "clean"
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir
appimage-builder --recipe appimage.recipe
# Now create dist and move appimage
rm -rf $(DESTDIR)
mkdir -p $(DESTDIR)
cp UDSClient-$(VERSION)-$(DISTRO).AppImage $(DESTDIR)
# Generate the .desktop fixed for new path
cat desktop/UDSClient.desktop | sed -e s/".usr.lib.UDSClient.UDSClient.py"/"\/usr\/bin\/UDSClient-$(VERSION)-$(DISTRO).AppImage"/g > $(DESTDIR)/UDSClient.desktop
# And also, generater installer
cat installer-appimage-template.sh | sed -e s/"0.0.0"/"$(VERSION)"/g | sed -e s/x86_64/$(DISTRO)/g > $(DESTDIR)/installer.sh
chmod 755 $(DESTDIR)/installer.sh
tar czvf ../udsclient3-$(DISTRO)-$(VERSION).tar.gz -C $(DESTDIR) .
# cleanup
-rm -rf appimage appimage-builder-cache /tmp/UDSClientDir
build-igel:
rm -rf $(DESTDIR)
mkdir -p $(DESTDIR)
# Calculate the size of the custom partition (15 megas more than the appimage size)
@$(eval APPIMAGE_SIZE=$(shell du -sm UDSClient-$(VERSION)-x86_64.AppImage | cut -f1))
@$(eval APPIMAGE_SIZE=$(shell expr $(APPIMAGE_SIZE) + 15))
cat igel/UDSClient-Profile-template.xml | sed -e s/"_SIZE_"/"$(APPIMAGE_SIZE)M"/g > $(DESTDIR)/UDSClient-Profile.xml
cat igel/UDSClient-template.inf | sed -e s/"_SIZE_"/"$(APPIMAGE_SIZE)M"/g > $(DESTDIR)/UDSClient.inf
cp UDSClient-$(VERSION)-x86_64.AppImage $(DESTDIR)/UDSClient
cp igel/UDSClient.desktop $(DESTDIR)/UDSClient.desktop
cp igel/init.sh $(DESTDIR)/init.sh
tar cjvf $(DESTDIR)/UDSClient.tar.bz2 -C $(DESTDIR) UDSClient UDSClient.desktop init.sh
zip -j ../udsclient3-$(VERSION)-igel.zip $(DESTDIR)/UDSClient-Profile.xml $(DESTDIR)/UDSClient.inf $(DESTDIR)/UDSClient.tar.bz2
cd ..
rm -rf $(DESTDIR)
build-thinpro:
rm -rf $(DESTDIR)
mkdir -p $(DESTDIR)
cp -r thinpro/* $(DESTDIR)
cp UDSClient-$(VERSION)-x86_64.AppImage $(DESTDIR)/UDSClient
tar czvf ../udsclient3-$(VERSION)-thinpro.tar.gz -C $(DESTDIR) .
rm -rf $(DESTDIR)

View File

@ -12,9 +12,6 @@ cat udsclient-template.spec |
sed -e s/"version 0.0.0"/"version ${VERSION}"/g | sed -e s/"version 0.0.0"/"version ${VERSION}"/g |
sed -e s/"release 1"/"release ${RELEASE}"/g > udsclient-$VERSION.spec 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 # Now fix dependencies for opensuse
# Note: Right now, opensuse & rh seems to have same dependencies, only 1 package needed # Note: Right now, opensuse & rh seems to have same dependencies, only 1 package needed
# cat udsclient-template.spec | # cat udsclient-template.spec |
@ -35,19 +32,6 @@ done
#rm udsclient-$VERSION #rm udsclient-$VERSION
# Make .tar.gz with source
make DESTDIR=targz DISTRO=targz VERSION=${VERSION} install make DESTDIR=targz DISTRO=targz VERSION=${VERSION} install
# And make FULL CLIENT .tar.gz for x86 and raspberry
make DESTDIR=appimage DISTRO=x86_64 VERSION=${VERSION} build-appimage
make DESTDIR=appimage DISTRO=armhf VERSION=${VERSION} build-appimage
make DESTDIR=appimage DISTRO=i686 VERSION=${VERSION} build-appimage
# Now create igel version
# we need first to create the Appimage for x86_64
make DESTDIR=igelimage DISTRO=x86_64 VERSION=${VERSION} build-igel
# Create the thinpro version
make DESTDIR=thinproimage DISTRO=x86_64 VERSION=${VERSION} build-thinpro
rpm --addsign ../*rpm rpm --addsign ../*rpm

View File

@ -1,21 +1,3 @@
udsclient3 (4.0.0) stable; urgency=medium
* Upgraded to 4.0.0 release
-- Adolfo Gómez García <agomez@virtualcable.es> Fri, 1 Jul 2022 15:12:10 +0200
udsclient3 (4.0.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
-- Adolfo Gómez García <agomez@virtualcable.es> Fri, 23 Oct 2020 08:12:10 +0200
udsclient3 (3.0.0) stable; urgency=medium udsclient3 (3.0.0) stable; urgency=medium
* Upgraded to 3.0.0 release * Upgraded to 3.0.0 release

View File

@ -1 +1 @@
10 9

View File

@ -10,6 +10,6 @@ Package: udsclient3
Section: admin Section: admin
Priority: optional Priority: optional
Architecture: all Architecture: all
Depends: python3-paramiko (>=2.0.0), python3-certifi, python3-cryptography, python3-psutil, python3-pyqt5 (>=5.0), python3 (>=3.6), freerdp2-x11 | freerdp-x11 | freerdp-nightly, desktop-file-utils, ${misc:Depends} Depends: python3-paramiko (>=2.0.0), python3-crypto, python3-pyqt5 (>=5.0), python3-six(>=1.1), python3 (>=3.6), freerdp2-x11 | freerdp-x11, desktop-file-utils, ${misc:Depends}
Description: Client connector for Universal Desktop Services (UDS) Broker Description: Client connector for Universal Desktop Services (UDS) Broker
This package provides the required components to allow this machine to connect to services provided by UDS Broker. This package provides the required components to allow this machine to connect to services provided by UDS Broker.

View File

@ -1,38 +1,26 @@
Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135 Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135
Name: udsclient3 Name: udsclient3
Maintainer: Adolfo Gómez García Maintainer: Adolfo Gómez García
Source: http://github.com/dkmstr/openuds/client-py3 Source: http://www.udsenterprise.com/
Files: * Copyright: 2014 Virtual Cable S.L.U.
Copyright: (c) 2014-2022, Virtual Cable S.L.U. License: BSD-3-clause
License: 3-BSD
License: 3-BSD License: GPL-2+
All rights reserved. This program is free software; you can redistribute it and/or modify
. it under the terms of the GNU General Public License as published by
Redistribution and use in source and binary forms, with or without the Free Software Foundation; either version 2 of the License, or
modification, are permitted provided that the following conditions are met: (at your option) any later version.
. .
* Redistributions of source code must retain the above copyright notice, this This program is distributed in the hope that it will be useful,
list of conditions and the following disclaimer. but WITHOUT ANY WARRANTY; without even the implied warranty of
. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Redistributions in binary form must reproduce the above copyright notice, GNU General Public License for more details.
this list of conditions and the following disclaimer in the documentation .
and/or other materials provided with the distribution. You should have received a copy of the GNU General Public License along
. with this program; if not, write to the Free Software Foundation, Inc.,
* Neither the name of pg_query nor the names of its contributors may be used 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
to endorse or promote products derived from this software without specific .
prior written permission. On Debian systems, the full text of the GNU General Public
. License version 2 can be found in the file
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" `/usr/share/common-licenses/GPL-2'.
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.

View File

@ -1,2 +1,2 @@
udsclient3_4.0.0_all.deb admin optional udsclient3_3.0.0_all.deb admin optional
udsclient3_4.0.0_amd64.buildinfo admin optional udsclient3_3.0.0_amd64.buildinfo admin optional

View File

@ -2,7 +2,7 @@
Name=UDSClient Name=UDSClient
Comment=UDS Helper Comment=UDS Helper
Keywords=uds;client;vdi; Keywords=uds;client;vdi;
Exec=/usr/lib/UDSClient/UDSClient.py %u -platform xcb Exec=/usr/lib/UDSClient/UDSClient.py %u
Icon=help-browser Icon=help-browser
StartupNotify=true StartupNotify=true
Terminal=false Terminal=false

View File

@ -1,69 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<profile>
<profile_id>1126</profile_id>
<profilename>UDSClient</profilename>
<firmware>
<model>IGEL OS 11</model>
<version>11.05.120.01</version>
</firmware>
<description></description>
<overwritesessions>false</overwritesessions>
<is_master_profile>false</is_master_profile>
<is_igel_os>true</is_igel_os>
<settings>
<pclass name="custom_partition.enabled">
<pvalue instancenr="-1" variableExpression="" variableSubstitutionActive="false">true</pvalue>
<variableSubstitutionActive>false</variableSubstitutionActive>
</pclass>
<pclass name="system.security.apparmor">
<pvalue instancenr="-1" variableExpression="" variableSubstitutionActive="false">false</pvalue>
<variableSubstitutionActive>false</variableSubstitutionActive>
</pclass>
<pclass name="custom_partition.mountpoint">
<pvalue instancenr="-1" variableExpression="" variableSubstitutionActive="false">/UDSClient</pvalue>
<variableSubstitutionActive>false</variableSubstitutionActive>
</pclass>
<pclass name="custom_partition.size">
<pvalue instancenr="-1" variableExpression="" variableSubstitutionActive="false">_SIZE_</pvalue>
<variableSubstitutionActive>false</variableSubstitutionActive>
</pclass>
</settings>
<instancesettings>
<instance classprefix="custom_partition.source%" serialnumber="-719cadfe:17ca470644a:-7fff127.0.1.1">
<ivalue classname="custom_partition.source%.autoupdate" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="custom_partition.source%.crypt_password" variableExpression="" variableSubstitutionActive="false">000d4317311f2c0031133c4d3e4c3d</ivalue>
<ivalue classname="custom_partition.source%.final_action" variableExpression="" variableSubstitutionActive="false"></ivalue>
<ivalue classname="custom_partition.source%.init_action" variableExpression="" variableSubstitutionActive="false">/UDSClient/init.sh</ivalue>
<ivalue classname="custom_partition.source%.password" variableExpression="" variableSubstitutionActive="false"></ivalue>
<ivalue classname="custom_partition.source%.url" variableExpression="" variableSubstitutionActive="false">https://[UMS_SERVER]:8443/ums_filetransfer/UDSClient-igel.inf</ivalue>
<ivalue classname="custom_partition.source%.username" variableExpression="" variableSubstitutionActive="false">[UMS_USERNAME]</ivalue>
</instance>
<instance classprefix="sessions.chromium%" serialnumber="-6b5264e9:17ca6f65505:-8000127.0.1.1">
<ivalue classname="sessions.chromium%.app.browser_startup_page" variableExpression="" variableSubstitutionActive="false">1</ivalue>
<ivalue classname="sessions.chromium%.app.homepage" variableExpression="" variableSubstitutionActive="false">https://demo.udsenterprise.com</ivalue>
<ivalue classname="sessions.chromium%.applaunch" variableExpression="" variableSubstitutionActive="false">true</ivalue>
<ivalue classname="sessions.chromium%.applaunch_path" variableExpression="" variableSubstitutionActive="false"></ivalue>
<ivalue classname="sessions.chromium%.applaunch_system" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.autostart" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.autostartnotify" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.desktop" variableExpression="" variableSubstitutionActive="false">true</ivalue>
<ivalue classname="sessions.chromium%.desktop_path" variableExpression="" variableSubstitutionActive="false"></ivalue>
<ivalue classname="sessions.chromium%.hotkey" variableExpression="" variableSubstitutionActive="false"></ivalue>
<ivalue classname="sessions.chromium%.hotkeymodifier" variableExpression="" variableSubstitutionActive="false">None</ivalue>
<ivalue classname="sessions.chromium%.icon" variableExpression="" variableSubstitutionActive="false">chromium</ivalue>
<ivalue classname="sessions.chromium%.menu_path" variableExpression="" variableSubstitutionActive="false"></ivalue>
<ivalue classname="sessions.chromium%.name" variableExpression="UDS" variableSubstitutionActive="true">###LOC_DEFAULT###</ivalue>
<ivalue classname="sessions.chromium%.position" variableExpression="" variableSubstitutionActive="false">0</ivalue>
<ivalue classname="sessions.chromium%.pulldown" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.pwprotected" variableExpression="" variableSubstitutionActive="false">none</ivalue>
<ivalue classname="sessions.chromium%.quick_start" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.scardautostart" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.snotify" variableExpression="" variableSubstitutionActive="false">true</ivalue>
<ivalue classname="sessions.chromium%.startmenu" variableExpression="" variableSubstitutionActive="false">true</ivalue>
<ivalue classname="sessions.chromium%.startmenu_system" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.usehotkey" variableExpression="" variableSubstitutionActive="false">false</ivalue>
<ivalue classname="sessions.chromium%.waittime2autostart" variableExpression="" variableSubstitutionActive="false">0</ivalue>
<ivalue classname="sessions.chromium%.waittime2restart" variableExpression="" variableSubstitutionActive="false">0</ivalue>
</instance>
</instancesettings>
</profile>

View File

@ -1,7 +0,0 @@
[INFO]
[PART]
file="UDSClient.tar.bz2"
version="1.1_igel1"
size="_SIZE_"
name="UDSClient"
minfw="11.05.120"

View File

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

View File

@ -1,15 +0,0 @@
#!/bin/sh
# Check for root
if ! [ $(id -u) = 0 ]; then
echo "This script must be run as root"
exit 1
fi
echo "Installing UDSClient Portable..."
cp UDSClient-0.0.0-x86_64.AppImage /usr/bin
cp UDSClient.desktop /usr/share/applications
update-desktop-database
echo "Installation process done."

View File

@ -8,8 +8,6 @@ echo "Installation process done."
echo "Remember that the following packages must be installed on system:" echo "Remember that the following packages must be installed on system:"
echo "* Python3 paramiko" echo "* Python3 paramiko"
echo "* Python3 PyQt5" echo "* Python3 PyQt5"
echo "* Python3 six"
echo "* Python3 requests" echo "* Python3 requests"
echo "* Python3 cryptography"
echo "Theese packages (as their names), are dependent on your platform, so you must locate and install them" echo "Theese packages (as their names), are dependent on your platform, so you must locate and install them"
echo "Also, ensure that a /media folder exists on your machine, that will be redirected on RDP connections" echo "Also, ensure that a /media folder exists on your machine, that will be redirected on RDP connections"

View File

@ -1,4 +0,0 @@
# UDS handlers.json
cp "/lib/UDSClient/firefox/handlers.json" "$FIREFOX_PROFILE_HANDLERS"
ffset "network.protocol-handler.external.uds" "true"
ffset "network.protocol-handler.external.udss" "true"

View File

@ -1,98 +0,0 @@
{
"defaultHandlersVersion": {
"en-US": 4
},
"mimeTypes": {
"application/pdf": {
"action": 3,
"extensions": [
"pdf"
]
},
"application/x-ica": {
"action": 2,
"extensions": [
"ica"
],
"handlers": [
{
"name": "wfica",
"path": "/usr/share/hptc-firefox-mgr/handlers/citrix"
}
]
},
"application/x-rdp": {
"action": 2,
"extensions": [
"rdp"
],
"handlers": [
{
"name": "HP xfreerdp",
"path": "/usr/share/hptc-firefox-mgr/handlers/rdp"
}
]
},
"text/lic": {
"action": 2,
"extensions": [
"lic"
],
"handlers": [
{
"name": "Copy license to ThinPro",
"path": "/usr/share/hptc-firefox-mgr/handlers/copy_lic"
}
]
},
"text/xml": {
"action": 3,
"extensions": [
"xml"
]
},
"image/svg+xml": {
"action": 3,
"extensions": [
"svg"
]
},
"image/webp": {
"action": 3,
"extensions": [
"webp"
]
}
},
"schemes": {
"vmware-view": {
"action": 2,
"handlers": [
{
"name": "VMWare Horizon View",
"path": "/usr/share/hptc-firefox-mgr/handlers/vmware"
}
]
},
"uds": {
"action": 2,
"handlers": [
{
"name": "UDS Client for ThinPro (SSL)",
"path": "/usr/share/hptc-firefox-mgr/handlers/uds"
}
]
},
"udss": {
"action": 2,
"handlers": [
{
"name": "UDS Client for ThinPro",
"path": "/usr/share/hptc-firefox-mgr/handlers/uds"
}
]
}
}
}

View File

@ -1,5 +0,0 @@
#!/bin/sh
export LD_PRELOAD=""
/bin/udsclient $*
exit 0

View File

@ -1,2 +0,0 @@
# UDS handlers.json
restore "/lib/UDSClient/firefox/handlers.json" "$FIREFOX_PROFILE_HANDLERS"

View File

@ -1,50 +0,0 @@
{
"defaultHandlersVersion":{
"en-US":4
},
"mimeTypes":{
"application/pdf":{
"action":3,
"extensions":["pdf"]
},
"application/x-ica":{
"action":2,
"handlers":[{
"name":"wfica",
"path":"/usr/bin/hptc-firefox-run-wfica.sh"
}],
"extensions":["ica"]
},
"application/x-rdp":{
"action":2,
"handlers":[{
"name":"HP xfreerdp",
"path":"/usr/bin/hptc-run-rdp-file-freerdp.sh"
}],
"extensions":["rdp"]
}
},
"schemes":{
"vmware-view":{
"action":2,
"handlers":[{
"name":"VMWare Horizon View",
"path":"/usr/bin/vmware-view"
}]
},
"udss":{
"action":2,
"handlers":[{
"name":"UDS Client",
"path":"/bin/udsclient"
}]
},
"uds":{
"action":2,
"handlers":[{
"name":"UDS Client",
"path":"/bin/udsclient"
}]
}
}
}

View File

@ -1,37 +0,0 @@
// This file can be used to configure global preferences for Firefox
// Example: Homepage
//pref("browser.startup.homepage", "http://www.weebls-stuff.com/wab/");
pref("plugin.default.state", 2);
pref("xpinstall.signatures.required", false, locked);
pref("extensions.autoDisableScopes", 0, locked);
pref("extensions.pocket.enabled", false, locked);
pref("extensions.screenshots.disabled", true, locked);
pref("datareporting.policy.dataSubmissionEnabled", false, locked);
pref("datareporting.policy.dataSubmissionEnabled.v2", false, locked);
pref("app.update.auto", false, locked);
pref("app.update.enabled", false, locked);
pref("browser.download.manager.closeWhenDone", true, locked);
pref("browser.helperApps.neverAsk.openFile", "application/x-rdp, application/x-java-jnlp-file", locked);
pref("browser.EULA.3.accepted", true, locked);
pref("browser.rights.3.shown", true, locked);
pref("browser.safebrowsing.enabled", false, locked);
pref("browser.search.update", false, locked);
pref("browser.sessionstore.enabled", false, locked);
pref("browser.sessionhistory.cache_subframes", false, locked);
pref("datareporting.healthreport.service.enabled", false, locked);
pref("datareporting.healthreport.uploadEnabled", false, locked);
pref("devtools.toolbox.host", "none", locked);
pref("extensions.autoDisableScopes", 14, locked);
pref("extensions.blocklist.enabled", false, locked);
pref("extensions.update.enabled", false, locked);
pref("intl.charsetmenu.browser.cache", "UTF-8", locked);
pref("network.protocol-handler.external.mailto", false, locked);
pref("network.protocol-handler.external.news", false, locked);
pref("network.protocol-handler.external.snews", false, locked);
pref("network.protocol-handler.external.nntp", false, locked);
pref("network.protocol-handler.external-default", false, locked);
pref("network.protocol-handler.external.vmware-view", true, locked);
pref("network.protocol-handler.external.uds", true, locked);
pref("network.protocol-handler.external.udss", true, locked);

View File

@ -1,38 +0,0 @@
#!/bin/sh
# Common part
# unlocks so we can write on TC
fsunlock
cp UDSClient /bin/udsclient
chmod 755 /bin/udsclient
# RDP Script for UDSClient. Launchs udsclient using the "Template_UDS" profile
cp udsrdp /usr/bin
INSTALLED=0
# Installation for 7.1.x version
grep -q "7.1" /etc/issue
if [ $? -eq 0 ]; then
echo "Installing for thinpro version 7.1"
# Allow UDS apps without asking
cp firefox7.1/syspref.js /etc/firefox
# Copy handlers.json for firefox
mkdir -p /lib/UDSClient/firefox/ > /dev/null 2>&1
cp firefox7.1/handlers.json /lib/UDSClient/firefox/
# and runner
cp firefox7.1/45-uds /etc/hptc-firefox-mgr/prestart
else
echo "Installing for thinpro version 7.2 or later"
# Copy handlers for firefox
mkdir -p /lib/UDSClient/firefox/ > /dev/null 2>&1
# Copy handlers.json for firefox
cp firefox/handlers.json /lib/UDSClient/firefox/
cp firefox/45-uds /etc/hptc-firefox-mgr/prestart
# copy uds handler for firefox
cp firefox/uds /usr/share/hptc-firefox-mgr/handlers/uds
chmod 755 /usr/share/hptc-firefox-mgr/handlers/uds
fi
# Common part
fslock

View File

@ -1,390 +0,0 @@
#!/bin/bash
function clearParams {
mclient set $REGKEY/address ""
mclient set $REGKEY/username ""
mclient set $REGKEY/password ""
mclient set $REGKEY/domain ""
mclient set $REGKEY/authorizations/user/execution 0
mclient commit
}
function getRegKey {
# Get Template_UDS
for key in `mclient get root/ConnectionType/freerdp/connections | sed "s/dir //g"`; do
val=`mclient get $key/label | sed "s/value //g"`
if [ "$val" == "Template_UDS" ]; then
REGKEY=$key
fi
done
}
function createUDSConnectionTemplate {
TMPFILE=$(mktemp /tmp/udsexport.XXXXXX)
cat > $TMPFILE << EOF
<Profile>
<ProfileSettings>
<Name>UDS Template Profile</Name>
<RegistryRoot>root/ConnectionType/freerdp/connections/{ff064bd9-047a-45ec-b70f-04ab218186ff}</RegistryRoot>
<Target>
<Hardware>t420</Hardware>
<ImageId>T7X62022</ImageId>
<Version>6.2.0</Version>
<Config>standard</Config>
</Target>
</ProfileSettings>
<ProfileRegistry>
<NodeDir name="{ff064bd9-047a-45ec-b70f-04ab218186ff}">
<NodeDir name="rdWebFeed">
<NodeKey name="keepResourcesWindowOpened">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="autoStartSingleResource">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="autoDisconnectTimeout">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
</NodeDir>
<NodeDir name="loginfields">
<NodeKey name="username">
<NodeParam name="value">3</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="rememberme">
<NodeParam name="value">2</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="password">
<NodeParam name="value">3</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="domain">
<NodeParam name="value">3</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
</NodeDir>
<NodeDir name="authorizations">
<NodeDir name="user">
<NodeKey name="execution">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="edit">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
</NodeDir>
</NodeDir>
<NodeKey name="address">
<NodeParam name="value"/>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="username">
<NodeParam name="value"/>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="password">
<NodeParam name="value">NLCR.1</NodeParam>
<NodeParam name="type">rc4</NodeParam>
</NodeKey>
<NodeKey name="domain">
<NodeParam name="value"/>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="label">
<NodeParam name="value">Template_UDS</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="credentialsType">
<NodeParam name="value">password</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="gatewayEnabled">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="gatewayPort">
<NodeParam name="value">443</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="gatewayUsesSameCredentials">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="gatewayCredentialsType">
<NodeParam name="value">password</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="remoteDesktopService">
<NodeParam name="value">Remote Computer</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="windowMode">
<NodeParam name="value">Remote Application</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="seamlessWindow">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="windowType">
<NodeParam name="value">full</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="windowSizePercentage">
<NodeParam name="value">70</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="windowSizeWidth">
<NodeParam name="value">1024</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="windowSizeHeight">
<NodeParam name="value">768</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="mouseMotionEvents">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="compression">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="rdpEncryption">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="offScreenBitmaps">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="attachToConsole">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="clipboardExtension">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="rdp6Buffering">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="rdpProgressiveCodec">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="securityLevel">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="tlsVersion">
<NodeParam name="value">auto</NodeParam>
<NodeParam name="type">string</NodeParam>
</NodeKey>
<NodeKey name="sound">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="printerMapping">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="portMapping">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="usbStorageRedirection">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="localPartitionRedirection">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="scRedirection">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="usbMiscRedirection">
<NodeParam name="value">2</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="perfFlagNoWallpaper">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="perfFlagFontSmoothing">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="perfFlagDesktopComposition">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="perfFlagNoWindowDrag">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="perfFlagNoMenuAnimations">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="perfFlagNoTheming">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="timeoutsEnabled">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="timeoutWarning">
<NodeParam name="value">6000</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="timeoutWarningDialog">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="timeoutRecovery">
<NodeParam name="value">30000</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="timeoutError">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="showRDPDashboard">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="showConnectionGraph">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="x11Synchronous">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="x11Logging">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="x11LogAutoflush">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="x11Capture">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="SingleSignOn">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="autostart">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">number</NodeParam>
</NodeKey>
<NodeKey name="waitForNetwork">
<NodeParam name="value">1</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="hasDesktopIcon">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
<NodeKey name="autoReconnect">
<NodeParam name="value">0</NodeParam>
<NodeParam name="type">bool</NodeParam>
</NodeKey>
</NodeDir>
</ProfileRegistry>
<ProfileFiles/>
</Profile>
EOF
mclient import $TMPFILE
rm $TMPFILE
}
ADDRESS=
USERNAME=
PASSWORD=
DOMAIN=
REGKEY=
CLEAR=0
# Try to locate registry key for UDS Template
getRegKey
if [ "$REGKEY" == "" ]; then
# Not found, create on based on our template
createUDSConnectionTemplate
getRegKey
fi
for param in $@; do
if [ "/u:" == "${param:0:3}" ]; then
USERNAME=${param:3}
CLEAR=1
fi
if [ "/p:" == "${param:0:3}" ]; then
PASSWORD=${param:3}
CLEAR=1
fi
if [ "/d:" == "${param:0:3}" ]; then
DOMAIN=${param:3}
CLEAR=1
fi
if [ "/v:" == "${param:0:3}" ]; then
ADDRESS=${param:3}
CLEAR=1
fi
done
if [ "$CLEAR" -eq 1 ]; then
clearParams
fi
ID=`basename $REGKEY`
RESPAWN=0
if [ "" != "$ADDRESS" ]; then
mclient set $REGKEY/address "${ADDRESS}"
RESPAWN=1
fi
if [ "" != "$USERNAME" ]; then
mclient set $REGKEY/username "${USERNAME}"
RESPAWN=1
fi
if [ "" != "$PASSWORD" ]; then
mclient set $REGKEY/password "${PASSWORD}"
RESPAWN=1
fi
if [ "" != "$DOMAIN" ]; then
mclient set $REGKEY/domain "${DOMAIN}"
RESPAWN=1
fi
if [ "$RESPAWN" -eq 1 ]; then
mclient set $REGKEY/authorizations/user/execution 1
mclient commit
exec $0 # Restart without command line
fi
process-connection $ID
clearParams

View File

@ -1,62 +0,0 @@
version: 1
script:
# Remove any previous build
- rm -rf /tmp/UDSClientDir | true
# Make usr and icons dirs
- mkdir -p /tmp/UDSClientDir/usr/src
# Copy the python application code into the UDSClientDir
- cp ../src/UDS*.py /tmp/UDSClientDir/usr/src
- cp -r ../src/uds /tmp/UDSClientDir/usr/src
# Remove __pycache__ and .mypy if exists
- rm /tmp/UDSClientDir/usr/src/.mypy_cache -rf 2>&1 > /dev/null
- rm /tmp/UDSClientDir/usr/src/uds/.mypy_cache -rf 2>&1 > /dev/null
- rm /tmp/UDSClientDir/usr/src/__pycache__ -rf 2>&1 > /dev/null
- rm /tmp/UDSClientDir/usr/src/uds/__pycache__ -rf 2>&1 > /dev/null
AppDir:
# On /tmp, that is an ext4 filesystem. On btrfs squashfs complains with "Unrecognised xattr prefix btrfs.compression"
path: /tmp/UDSClientDir
app_info:
id: com.udsenterprise.UDSClient3
name: UDSClient
icon: utilities-terminal
version: 0.0.0
# Set the python executable as entry point
exec: usr/bin/python3
# Set the application main script path as argument. Use '$@' to forward CLI parameters
exec_args: "$APPDIR/usr/src/UDSClient.py $@"
apt:
arch: amd64
sources:
- sourceline: 'deb [arch=amd64] http://ftp.de.debian.org/debian/ bullseye main contrib non-free'
key_url: 'http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x648ACFD622F3D138'
include:
- python3
- python3-pkg-resources
- python3-pyqt5
- python3-paramiko
- python3-cryptography
- python3-certifi
- python3-psutil
- freerdp2-x11
- freerdp2-wayland
- x2goclient
- openssh-sftp-server
exclude: []
runtime:
env:
# Set python home
# See https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHOME
PYTHONHOME: '${APPDIR}/usr'
# Path to the site-packages dir or other modules dirs
# See https://docs.python.org/3/using/cmdline.html#envvar-PYTHONPATH
PYTHONPATH: '${APPDIR}/usr/lib/python3.9/site-packages'
AppImage:
update-information: None
sign-key: None
arch: x86_64

View File

@ -11,7 +11,7 @@ Release: %{release}
Summary: Client for Universal Desktop Services (UDS) Broker Summary: Client for Universal Desktop Services (UDS) Broker
License: BSD3 License: BSD3
Group: Applications/Productivity Group: Applications/Productivity
Requires: python3-paramiko python3-qt5 python3-cryptography python3-certifi python3-psutil Requires: python3-six python3-requests python3-paramiko python3-qt5 (python3-crypto or python3-pycrypto)
Vendor: Virtual Cable S.L.U. Vendor: Virtual Cable S.L.U.
URL: http://www.udsenterprise.com URL: http://www.udsenterprise.com
Provides: udsclient Provides: udsclient

View File

@ -1,6 +1,4 @@
/build /build
/dist /dist
/UDSClient*.pkg UDSClient.dmg
/UDSClient*.dist UDSClient.pkg
/UDSClient*.build
/.eggs

View File

@ -1,7 +1,7 @@
#!/usr/bin/env -S python3 -s #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014-2021 Virtual Cable S.L.U. # Copyright (c) 2014-2017 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # 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, # * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation # this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution. # 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 # may be used to endorse or promote products derived from this software
# without specific prior written permission. # without specific prior written permission.
# #
@ -31,45 +31,41 @@
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
import sys import sys
import os
import platform
import time
import webbrowser import webbrowser
import threading import json
import typing import base64, bz2
from PyQt5 import QtCore, QtWidgets, QtGui from PyQt5 import QtCore, QtGui, QtWidgets # @UnresolvedImport
from PyQt5.QtCore import QSettings import six
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
from uds.tunnel import forward as tunnel_forwards # type: ignore
from uds.rest import RestRequest
from uds.forward import forward # pylint: disable=unused-import
from uds.log import logger from uds.log import logger
from uds import tools from uds import tools
from uds import VERSION from uds import VERSION
from UDSWindow import Ui_MainWindow from UDSWindow import Ui_MainWindow
# Server before this version uses "unsigned" scripts
OLD_METHOD_VERSION = '2.4.0'
class RetryException(Exception):
pass
class UDSClient(QtWidgets.QMainWindow): class UDSClient(QtWidgets.QMainWindow):
ticket: str = '' ticket = None
scrambler: str = '' scrambler = None
withError = False withError = False
animTimer: typing.Optional[QtCore.QTimer] = None animTimer = None
anim: int = 0 anim = 0
animInverted: bool = False animInverted = False
api: RestApi serverVersion = 'X.Y.Z' # Will be overwriten on getVersion
req = None
def __init__(self, api: RestApi, ticket: str, scrambler: str): def __init__(self):
QtWidgets.QMainWindow.__init__(self) QtWidgets.QMainWindow.__init__(self)
self.api = api self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
self.ticket = ticket
self.scrambler = scrambler
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint) # type: ignore
self.ui = Ui_MainWindow() self.ui = Ui_MainWindow()
self.ui.setupUi(self) self.ui.setupUi(self)
@ -79,36 +75,41 @@ class UDSClient(QtWidgets.QMainWindow):
self.ui.info.setText('Initializing...') self.ui.info.setText('Initializing...')
screen_geometry = QtGui.QGuiApplication.primaryScreen().geometry() screen = QtWidgets.QDesktopWidget().screenGeometry()
mysize = self.geometry() mysize = self.geometry()
hpos = (screen_geometry.width() - mysize.width()) // 2 hpos = (screen.width() - mysize.width()) // 2
vpos = (screen_geometry.height() - mysize.height() - mysize.height()) // 2 vpos = (screen.height() - mysize.height() - mysize.height()) // 2
self.move(hpos, vpos) self.move(hpos, vpos)
self.animTimer = QtCore.QTimer() self.animTimer = QtCore.QTimer()
self.animTimer.timeout.connect(self.updateAnim) # type: ignore self.animTimer.timeout.connect(self.updateAnim)
# QtCore.QObject.connect(self.animTimer, QtCore.SIGNAL('timeout()'), self.updateAnim) # QtCore.QObject.connect(self.animTimer, QtCore.SIGNAL('timeout()'), self.updateAnim)
self.activateWindow() self.activateWindow()
self.startAnim() self.startAnim()
def closeWindow(self): def closeWindow(self):
self.close() self.close()
def processError(self, data):
if 'error' in data:
# QtWidgets.QMessageBox.critical(self, 'Request error {}'.format(data.get('retryable', '0')), data['error'], QtWidgets.QMessageBox.Ok)
if data.get('retryable', '0') == '1':
raise RetryException(data['error'])
raise Exception(data['error'])
# QtWidgets.QMessageBox.critical(self, 'Request error', rest.data['error'], QtWidgets.QMessageBox.Ok)
# self.closeWindow()
# return
def showError(self, error): def showError(self, error):
logger.error('got error: %s', error) logger.error('got error: %s', error)
self.stopAnim() self.stopAnim()
self.ui.info.setText( self.ui.info.setText('UDS Plugin Error') # In fact, main window is hidden, so this is not visible... :)
'UDS Plugin Error'
) # In fact, main window is hidden, so this is not visible... :)
self.closeWindow() self.closeWindow()
QtWidgets.QMessageBox.critical( QtWidgets.QMessageBox.critical(None, 'UDS Plugin Error', '{}'.format(error), QtWidgets.QMessageBox.Ok)
None, # type: ignore
'UDS Plugin Error',
'{}'.format(error),
QtWidgets.QMessageBox.Ok,
)
self.withError = True self.withError = True
def cancelPushed(self): def cancelPushed(self):
@ -124,227 +125,165 @@ class UDSClient(QtWidgets.QMainWindow):
self.ui.progressBar.setValue(self.anim) self.ui.progressBar.setValue(self.anim)
def startAnim(self): def startAnim(self):
self.ui.progressBar.invertedAppearance = False # type: ignore self.ui.progressBar.invertedAppearance = False
self.anim = 0 self.anim = 0
self.animInverted = False self.animInverted = False
self.ui.progressBar.setInvertedAppearance(self.animInverted) self.ui.progressBar.setInvertedAppearance(self.animInverted)
if self.animTimer: self.animTimer.start(40)
self.animTimer.start(40)
def stopAnim(self): def stopAnim(self):
self.ui.progressBar.invertedAppearance = False # type: ignore self.ui.progressBar.invertedAppearance = False
if self.animTimer: self.animTimer.stop()
self.animTimer.stop()
def getVersion(self): def getVersion(self):
self.req = RestRequest('', self, self.version)
self.req.get()
def version(self, data):
try: try:
self.api.getVersion() self.processError(data)
except InvalidVersion as e: self.ui.info.setText('Processing...')
QtWidgets.QMessageBox.critical(
self, if data['result']['requiredVersion'] > VERSION:
'Upgrade required', QtWidgets.QMessageBox.critical(self, 'Upgrade required', 'A newer connector version is required.\nA browser will be opened to download it.', QtWidgets.QMessageBox.Ok)
'A newer connector version is required.\nA browser will be opened to download it.', webbrowser.open(data['result']['downloadUrl'])
QtWidgets.QMessageBox.Ok, self.closeWindow()
) return
webbrowser.open(e.downloadUrl)
self.closeWindow() self.serverVersion = data['result']['requiredVersion']
return self.getTransportData()
except RetryException as e:
self.ui.info.setText(str(e))
QtCore.QTimer.singleShot(1000, self.getVersion)
except Exception as e: except Exception as e:
self.showError(e) self.showError(e)
self.getTransportData()
def getTransportData(self): def getTransportData(self):
try: try:
script, params = self.api.getScriptAndParams(self.ticket, self.scrambler) self.req = RestRequest('/{}/{}'.format(self.ticket, self.scrambler), self, self.transportDataReceived, params={'hostname': tools.getHostName(), 'version': VERSION})
self.req.get()
except Exception as e:
logger.exception('Got exception on getTransportData')
raise e
def transportDataReceived(self, data):
logger.debug('Transport data received')
try:
self.processError(data)
params = None
if self.serverVersion <= OLD_METHOD_VERSION:
script = bz2.decompress(base64.b64decode(data['result']))
# This fixes uds 2.2 "write" string on binary streams on some transport
script = script.replace(b'stdin.write("', b'stdin.write(b"')
script = script.replace(b'version)', b'version.decode("utf-8"))')
else:
res = data['result']
# We have three elements on result:
# * Script
# * Signature
# * Script data
# We test that the Script has correct signature, and them execute it with the parameters
#script, signature, params = res['script'].decode('base64').decode('bz2'), res['signature'], json.loads(res['params'].decode('base64').decode('bz2'))
script, signature, params = bz2.decompress(base64.b64decode(res['script'])), res['signature'], json.loads(bz2.decompress(base64.b64decode(res['params'])))
if tools.verifySignature(script, signature) is False:
logger.error('Signature is invalid')
raise Exception('Invalid UDS code signature. Please, report to administrator')
self.stopAnim() self.stopAnim()
if 'darwin' in sys.platform: if 'darwin' in sys.platform:
self.showMinimized() self.showMinimized()
# QtCore.QTimer.singleShot(3000, self.endScript) QtCore.QTimer.singleShot(3000, self.endScript)
# self.hide() self.hide()
self.closeWindow()
exec(script, globals(), {'parent': self, 'sp': params}) six.exec_(script.decode("utf-8"), globals(), {'parent': self, 'sp': params})
# Execute the waiting tasks...
threading.Thread(target=endScript).start()
except RetryException as e: except RetryException as e:
self.ui.info.setText(str(e) + ', retrying access...') self.ui.info.setText(six.text_type(e) + ', retrying access...')
# Retry operation in ten seconds # Retry operation in ten seconds
QtCore.QTimer.singleShot(10000, self.getTransportData) QtCore.QTimer.singleShot(10000, self.getTransportData)
except Exception as e: except Exception as e:
#logger.exception('Got exception executing script:')
self.showError(e) self.showError(e)
def endScript(self):
# After running script, wait for stuff
try:
tools.waitForTasks()
except Exception:
pass
try:
tools.unlinkFiles()
except Exception:
pass
try:
tools.execBeforeExit()
except Exception:
pass
self.closeWindow()
def start(self): def start(self):
""" '''
Starts proccess by requesting version info Starts proccess by requesting version info
""" '''
self.ui.info.setText('Initializing...') self.ui.info.setText('Initializing...')
QtCore.QTimer.singleShot(100, self.getVersion) QtCore.QTimer.singleShot(100, self.getVersion)
def endScript(): def done(data):
# Wait a bit before start processing ending sequence QtWidgets.QMessageBox.critical(None, 'Notice', six.text_type(data.data), QtWidgets.QMessageBox.Ok)
time.sleep(3) sys.exit(0)
try:
# Remove early stage files...
tools.unlinkFiles(early=True)
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:
logger.debug('Watiting for tasks to finish: %s', e)
try:
logger.debug('Unlinking files')
tools.unlinkFiles(early=False)
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:
logger.debug('execBeforeExit: %s', e)
logger.debug('endScript done')
# Ask user to approve endpoint # Ask user to approve endpoint
def approveHost(hostName: str): def approveHost(hostName, parentWindow=None):
settings = QtCore.QSettings() settings = QtCore.QSettings()
settings.beginGroup('endpoints') settings.beginGroup('endpoints')
# approved = settings.value(hostName, False).toBool() #approved = settings.value(hostName, False).toBool()
approved = bool(settings.value(hostName, False)) approved = bool(settings.value(hostName, False))
errorString = '<p>The server <b>{}</b> must be approved:</p>'.format(hostName) errorString = '<p>The server <b>{}</b> must be approved:</p>'.format(hostName)
errorString += ( errorString += '<p>Only approve UDS servers that you trust to avoid security issues.</p>'
'<p>Only approve UDS servers that you trust to avoid security issues.</p>'
)
if not approved: if approved or QtWidgets.QMessageBox.warning(parentWindow, 'ACCESS Warning', errorString, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) == QtWidgets.QMessageBox.Yes:
if ( settings.setValue(hostName, True)
QtWidgets.QMessageBox.warning(
None, # type: ignore
'ACCESS Warning',
errorString,
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, # type: ignore
)
== QtWidgets.QMessageBox.Yes
):
settings.setValue(hostName, True)
approved = True
settings.endGroup()
return approved
def sslError(hostname: str, serial):
settings = QSettings()
settings.beginGroup('ssl')
approved = settings.value(serial, False)
if (
approved
or QtWidgets.QMessageBox.warning(
None, # type: ignore
'SSL Warning',
f'Could not check SSL certificate for {hostname}.\nDo you trust this host?',
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, # type: ignore
)
== QtWidgets.QMessageBox.Yes
):
approved = True approved = True
settings.setValue(serial, True)
settings.endGroup() settings.endGroup()
return approved return approved
if __name__ == "__main__":
# Used only if command line says so logger.debug('Initializing connector')
def minimal(api: RestApi, ticket: str, scrambler: str):
try: # Initialize app
logger.info('Minimal Execution')
logger.debug('Getting version')
try:
api.getVersion()
except InvalidVersion as e:
QtWidgets.QMessageBox.critical(
None, # type: ignore
'Upgrade required',
'A newer connector version is required.\nA browser will be opened to download it.',
QtWidgets.QMessageBox.Ok,
)
webbrowser.open(e.downloadUrl)
return 0
logger.debug('Transport data')
script, params = api.getScriptAndParams(ticket, scrambler)
# Execute UDS transport script
exec(script, globals(), {'parent': None, 'sp': params})
# Execute the waiting task...
threading.Thread(target=endScript).start()
except RetryException as e:
QtWidgets.QMessageBox.warning(
None, # type: ignore
'Service not ready',
'{}'.format('.\n'.join(str(e).split('.')))
+ '\n\nPlease, retry again in a while.',
QtWidgets.QMessageBox.Ok,
)
except Exception as e:
# logger.exception('Got exception on getTransportData')
QtWidgets.QMessageBox.critical(
None, # type: ignore
'Error',
'{}'.format(str(e)) + '\n\nPlease, retry again in a while.',
QtWidgets.QMessageBox.Ok,
)
return 0
def main(args: typing.List[str]):
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
logger.debug('Initializing connector for %s(%s)', sys.platform, platform.machine())
logger.debug('Arguments: %s', args)
# Set several info for settings # Set several info for settings
QtCore.QCoreApplication.setOrganizationName('Virtual Cable S.L.U.') QtCore.QCoreApplication.setOrganizationName('Virtual Cable S.L.U.')
QtCore.QCoreApplication.setApplicationName('UDS Connector') QtCore.QCoreApplication.setApplicationName('UDS Connector')
if 'darwin' not in sys.platform: if 'darwin' not in sys.platform:
logger.debug('Mac OS *NOT* Detected') logger.debug('Mac OS *NOT* Detected')
app.setStyle('plastique') # type: ignore app.setStyle('plastique')
else:
logger.debug('Platform is Mac OS, adding homebrew possible paths') if six.PY3 is False:
os.environ['PATH'] += ''.join( logger.debug('Fixing threaded execution of commands')
os.pathsep + i import threading
for i in ( threading._DummyThread._Thread__stop = lambda x: 42 # type: ignore # pylint: disable=protected-access
'/usr/local/bin',
'/opt/homebrew/bin',
)
)
logger.debug('Now path is %s', os.environ['PATH'])
# First parameter must be url # First parameter must be url
useMinimal = False
try: try:
uri = args[1] uri = sys.argv[1]
if uri == '--minimal':
useMinimal = True
uri = args[2] # And get URI
if uri == '--test': if uri == '--test':
sys.exit(0) sys.exit(0)
@ -354,28 +293,17 @@ def main(args: typing.List[str]):
raise Exception() raise Exception()
ssl = uri[3] == 's' ssl = uri[3] == 's'
host, ticket, scrambler = uri.split('//')[1].split('/') # type: ignore host, UDSClient.ticket, UDSClient.scrambler = uri.split('//')[1].split('/') # type: ignore
logger.debug( logger.debug('ssl:%s, host:%s, ticket:%s, scrambler:%s', ssl, host, UDSClient.ticket, UDSClient.scrambler)
'ssl:%s, host:%s, ticket:%s, scrambler:%s',
ssl,
host,
ticket,
scrambler,
)
except Exception: except Exception:
logger.debug('Detected execution without valid URI, exiting') logger.debug('Detected execution without valid URI, exiting')
QtWidgets.QMessageBox.critical( QtWidgets.QMessageBox.critical(None, 'Notice', 'UDS Client Version {}'.format(VERSION), QtWidgets.QMessageBox.Ok)
None, # type: ignore
'Notice',
'UDS Client Version {}'.format(VERSION),
QtWidgets.QMessageBox.Ok,
)
sys.exit(1) sys.exit(1)
# Setup REST api endpoint # Setup REST api endpoint
api = RestApi( RestRequest.restApiUrl = '{}://{}/rest/client'.format(['http', 'https'][ssl], host)
'{}://{}/uds/rest/client'.format(['http', 'https'][ssl], host), sslError logger.debug('Setting request URL to %s', RestRequest.restApiUrl)
) # RestRequest.restApiUrl = 'https://172.27.0.1/rest/client'
try: try:
logger.debug('Starting execution') logger.debug('Starting execution')
@ -384,23 +312,18 @@ def main(args: typing.List[str]):
if approveHost(host) is False: if approveHost(host) is False:
raise Exception('Host {} was not approved'.format(host)) raise Exception('Host {} was not approved'.format(host))
win = UDSClient(api, ticket, scrambler) win = UDSClient()
win.show() win.show()
win.start() win.start()
exitVal = app.exec() exitVal = app.exec_()
logger.debug('Execution finished correctly') logger.debug('Execution finished correctly')
except Exception as e: except Exception as e:
logger.exception('Got an exception executing client:') logger.exception('Got an exception executing client:')
exitVal = 128 exitVal = 128
QtWidgets.QMessageBox.critical( QtWidgets.QMessageBox.critical(None, 'Error', six.text_type(e), QtWidgets.QMessageBox.Ok)
None, 'Error', str(e), QtWidgets.QMessageBox.Ok # type: ignore
)
logger.debug('Exiting') logger.debug('Exiting')
sys.exit(exitVal) sys.exit(exitVal)
if __name__ == "__main__":
main(sys.argv)

View File

@ -1,75 +0,0 @@
import sys
import os.path
import subprocess
import typing
from uds.log import logger
import UDSClient
from UDSLauncherMac import Ui_MacLauncher
from PyQt5 import QtCore, QtWidgets, QtGui
SCRIPT_NAME = 'UDSClientLauncher'
class UdsApplication(QtWidgets.QApplication):
path: str
tunnels: typing.List[subprocess.Popen]
def __init__(self, argv: typing.List[str]) -> None:
super().__init__(argv)
self.path = os.path.join(os.path.dirname(sys.argv[0]).replace('Resources', 'MacOS'), SCRIPT_NAME)
self.tunnels = []
self.lastWindowClosed.connect(self.closeTunnels) # type: ignore
def cleanTunnels(self) -> None:
def isRunning(p: subprocess.Popen):
try:
if p.poll() is None:
return True
except Exception as e:
logger.debug('Got error polling subprocess: %s', e)
return False
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:
logger.debug('Closing remaining tunnels')
for tunnel in self.tunnels:
logger.debug('Checking %s - "%s"', tunnel, tunnel.poll())
if tunnel.poll() is None: # Running
logger.info('Found running tunnel %s, closing it', tunnel.pid)
tunnel.kill()
def event(self, evnt: QtCore.QEvent) -> bool:
if evnt.type() == QtCore.QEvent.FileOpen: # type: ignore
fe = typing.cast(QtGui.QFileOpenEvent, evnt)
logger.debug('Got url: %s', fe.url().url())
fe.accept()
logger.debug('Spawning %s', self.path)
# First, remove all finished tunnel processed from check queue
self.cleanTunnels()
# And now add a new one
self.tunnels.append(subprocess.Popen([self.path, fe.url().url()]))
return super().event(evnt)
def main(args: typing.List[str]):
if len(args) > 1:
UDSClient.main(args)
else:
app = UdsApplication(sys.argv)
window = QtWidgets.QMainWindow()
Ui_MacLauncher().setupUi(window)
window.showMinimized()
sys.exit(app.exec())
if __name__ == "__main__":
main(args=sys.argv)

View File

@ -1,75 +0,0 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'UDSLauncherMac.ui'
#
# Created by: PyQt5 UI code generator 5.15.2
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MacLauncher(object):
def setupUi(self, MacLauncher):
MacLauncher.setObjectName("MacLauncher")
MacLauncher.setWindowModality(QtCore.Qt.NonModal)
MacLauncher.resize(235, 120)
MacLauncher.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(":/images/logo-uds-small"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
MacLauncher.setWindowIcon(icon)
MacLauncher.setWindowOpacity(1.0)
self.centralwidget = QtWidgets.QWidget(MacLauncher)
self.centralwidget.setAutoFillBackground(True)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout_2.setContentsMargins(4, 4, 4, 4)
self.verticalLayout_2.setSpacing(4)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.frame = QtWidgets.QFrame(self.centralwidget)
self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
self.frame.setObjectName("frame")
self.verticalLayout = QtWidgets.QVBoxLayout(self.frame)
self.verticalLayout.setObjectName("verticalLayout")
self.topLabel = QtWidgets.QLabel(self.frame)
self.topLabel.setTextFormat(QtCore.Qt.RichText)
self.topLabel.setObjectName("topLabel")
self.verticalLayout.addWidget(self.topLabel)
self.image = QtWidgets.QLabel(self.frame)
self.image.setMinimumSize(QtCore.QSize(0, 32))
self.image.setAutoFillBackground(True)
self.image.setText("")
self.image.setPixmap(QtGui.QPixmap(":/images/logo-uds-small"))
self.image.setScaledContents(False)
self.image.setAlignment(QtCore.Qt.AlignCenter)
self.image.setObjectName("image")
self.verticalLayout.addWidget(self.image)
self.label_2 = QtWidgets.QLabel(self.frame)
self.label_2.setTextFormat(QtCore.Qt.RichText)
self.label_2.setObjectName("label_2")
self.verticalLayout.addWidget(self.label_2)
self.verticalLayout_2.addWidget(self.frame)
MacLauncher.setCentralWidget(self.centralwidget)
self.retranslateUi(MacLauncher)
QtCore.QMetaObject.connectSlotsByName(MacLauncher)
def retranslateUi(self, MacLauncher):
_translate = QtCore.QCoreApplication.translate
MacLauncher.setWindowTitle(_translate("MacLauncher", "UDS Launcher"))
self.topLabel.setText(_translate("MacLauncher", "<html><head/><body><p align=\"center\"><span style=\" font-size:12pt; font-weight:600;\">UDS Launcher</span></p></body></html>"))
self.label_2.setText(_translate("MacLauncher", "<html><head/><body><p align=\"center\"><span style=\" font-size:6pt;\">Closing this window will end all UDS tunnels</span></p></body></html>"))
import UDSResources_rc
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
MacLauncher = QtWidgets.QMainWindow()
ui = Ui_MacLauncher()
ui.setupUi(MacLauncher)
MacLauncher.show()
sys.exit(app.exec())

View File

@ -1,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MacLauncher</class>
<widget class="QMainWindow" name="MacLauncher">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>235</width>
<height>120</height>
</rect>
</property>
<property name="cursor">
<cursorShape>ArrowCursor</cursorShape>
</property>
<property name="windowTitle">
<string>UDS Launcher</string>
</property>
<property name="windowIcon">
<iconset resource="UDSResources.qrc">
<normaloff>:/images/logo-uds-small</normaloff>:/images/logo-uds-small</iconset>
</property>
<property name="windowOpacity">
<double>1.000000000000000</double>
</property>
<widget class="QWidget" name="centralwidget">
<property name="autoFillBackground">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>4</number>
</property>
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Raised</enum>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="topLabel">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;center&quot;&gt;&lt;span style=&quot; font-size:12pt; font-weight:600;&quot;&gt;UDS Launcher&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="image">
<property name="minimumSize">
<size>
<width>0</width>
<height>32</height>
</size>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="text">
<string notr="true"/>
</property>
<property name="pixmap">
<pixmap resource="UDSResources.qrc">:/images/logo-uds-small</pixmap>
</property>
<property name="scaledContents">
<bool>false</bool>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;center&quot;&gt;&lt;span style=&quot; font-size:6pt;&quot;&gt;Closing this window will end all UDS tunnels&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
<resources>
<include location="UDSResources.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -2,7 +2,7 @@
# Resource object code # Resource object code
# #
# Created by: The Resource Compiler for PyQt5 (Qt v5.15.2) # Created by: The Resource Compiler for PyQt5 (Qt v5.13.2)
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!

View File

@ -2,10 +2,9 @@
# Form implementation generated from reading ui file 'UDSWindow.ui' # Form implementation generated from reading ui file 'UDSWindow.ui'
# #
# Created by: PyQt5 UI code generator 5.15.2 # Created by: PyQt5 UI code generator 5.13.2
# #
# WARNING: Any manual changes made to this file will be lost when pyuic5 is # WARNING! All changes made in this file will be lost!
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import QtCore, QtGui, QtWidgets
@ -90,4 +89,4 @@ if __name__ == "__main__":
ui = Ui_MainWindow() ui = Ui_MainWindow()
ui.setupUi(MainWindow) ui.setupUi(MainWindow)
MainWindow.show() MainWindow.show()
sys.exit(app.exec()) sys.exit(app.exec_())

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014-2021 Virtual Cable S.L. # Copyright (c) 2014-2017 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -29,11 +29,13 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
VERSION = '3.6.0' from __future__ import unicode_literals
VERSION = '3.0.0'
__title__ = 'udclient' __title__ = 'udclient'
__version__ = VERSION __version__ = VERSION
__build__ = 0x010712 __build__ = 0x010760
__author__ = 'Adolfo Gómez <dkmaster@dkmon.com>' __author__ = 'Adolfo Gómez'
__license__ = "BSD 3-clause" __license__ = "BSD 3-clause"
__copyright__ = "Copyright 2014-2022 VirtualCable S.L.U." __copyright__ = "Copyright 2014-2017 VirtualCable S.L.U."

View File

@ -9,7 +9,6 @@ import random
import time import time
import select import select
import socketserver import socketserver
import typing
import paramiko import paramiko
@ -25,11 +24,7 @@ class CheckfingerPrints(paramiko.MissingHostKeyPolicy):
if self.fingerPrints: if self.fingerPrints:
remotefingerPrints = hexlify(key.get_fingerprint()).decode().lower() remotefingerPrints = hexlify(key.get_fingerprint()).decode().lower()
if remotefingerPrints not in self.fingerPrints.split(','): if remotefingerPrints not in self.fingerPrints.split(','):
logger.error( logger.error("Server {!r} has invalid fingerPrints. ({} vs {})".format(hostname, remotefingerPrints, self.fingerPrints))
"Server {!r} has invalid fingerPrints. ({} vs {})".format(
hostname, remotefingerPrints, self.fingerPrints
)
)
raise paramiko.SSHException( raise paramiko.SSHException(
"Server {!r} has invalid fingerPrints".format(hostname) "Server {!r} has invalid fingerPrints".format(hostname)
) )
@ -41,49 +36,26 @@ class ForwardServer(socketserver.ThreadingTCPServer):
class Handler(socketserver.BaseRequestHandler): class Handler(socketserver.BaseRequestHandler):
event: threading.Event
thread: 'ForwardThread'
ssh_transport: paramiko.Transport
chain_host: str
chain_port: int
def handle(self): def handle(self):
self.thread.currentConnections += 1 self.thread.currentConnections += 1
try: try:
chan = self.ssh_transport.open_channel( chan = self.ssh_transport.open_channel('direct-tcpip',
'direct-tcpip', (self.chain_host, self.chain_port),
(self.chain_host, self.chain_port), self.request.getpeername())
self.request.getpeername(),
)
except Exception as e: except Exception as e:
logger.exception( logger.exception('Incoming request to %s:%d failed: %s', self.chain_host, self.chain_port, repr(e))
'Incoming request to %s:%d failed: %s',
self.chain_host,
self.chain_port,
repr(e),
)
return return
if chan is None: if chan is None:
logger.error( logger.error('Incoming request to %s:%d was rejected by the SSH server.', self.chain_host, self.chain_port)
'Incoming request to %s:%d was rejected by the SSH server.',
self.chain_host,
self.chain_port,
)
return return
logger.debug( logger.debug('Connected! Tunnel open %r -> %r -> %r', self.request.getpeername(), chan.getpeername(), (self.chain_host, self.chain_port))
'Connected! Tunnel open %r -> %r -> %r',
self.request.getpeername(),
chan.getpeername(),
(self.chain_host, self.chain_port),
)
# self.ssh_transport.set_keepalive(10) # Keep alive every 10 seconds... # self.ssh_transport.set_keepalive(10) # Keep alive every 10 seconds...
try: try:
while self.event.is_set() is False: while self.event.is_set() is False:
r, _w, _x = select.select( r, _w, _x = select.select([self.request, chan], [], [], 1) # pylint: disable=unused-variable
[self.request, chan], [], [], 1
) # pylint: disable=unused-variable
if self.request in r: if self.request in r:
data = self.request.recv(1024) data = self.request.recv(1024)
@ -102,10 +74,7 @@ class Handler(socketserver.BaseRequestHandler):
peername = self.request.getpeername() peername = self.request.getpeername()
chan.close() chan.close()
self.request.close() self.request.close()
logger.debug( logger.debug('Tunnel closed from %r', peername,)
'Tunnel closed from %r',
peername,
)
except Exception: except Exception:
pass pass
@ -117,21 +86,8 @@ class Handler(socketserver.BaseRequestHandler):
class ForwardThread(threading.Thread): class ForwardThread(threading.Thread):
status = 0 # Connecting status = 0 # Connecting
client: typing.Optional[paramiko.SSHClient]
fs: typing.Optional[ForwardServer]
def __init__( def __init__(self, server, port, username, password, localPort, redirectHost, redirectPort, waitTime, fingerPrints):
self,
server,
port,
username,
password,
localPort,
redirectHost,
redirectPort,
waitTime,
fingerPrints,
):
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.client = None self.client = None
self.fs = None self.fs = None
@ -146,7 +102,7 @@ class ForwardThread(threading.Thread):
self.redirectPort = redirectPort self.redirectPort = redirectPort
self.waitTime = waitTime self.waitTime = waitTime
self.fingerPrints = fingerPrints self.fingerPrints = fingerPrints
self.stopEvent = threading.Event() self.stopEvent = threading.Event()
@ -160,19 +116,9 @@ class ForwardThread(threading.Thread):
if localPort is None: if localPort is None:
localPort = random.randrange(33000, 53000) localPort = random.randrange(33000, 53000)
ft = ForwardThread( ft = ForwardThread(self.server, self.port, self.username, self.password, localPort, redirectHost, redirectPort, self.waitTime, self.fingerPrints)
self.server,
self.port,
self.username,
self.password,
localPort,
redirectHost,
redirectPort,
self.waitTime,
self.fingerPrints,
)
ft.client = self.client ft.client = self.client
self.client.useCount += 1 # type: ignore self.client.useCount += 1 # One more using this client
ft.start() ft.start()
while ft.status == 0: while ft.status == 0:
@ -180,6 +126,7 @@ class ForwardThread(threading.Thread):
return (ft, localPort) return (ft, localPort)
def _timerFnc(self): def _timerFnc(self):
self.timer = None self.timer = None
logger.debug('Timer fnc: %s', self.currentConnections) logger.debug('Timer fnc: %s', self.currentConnections)
@ -191,23 +138,14 @@ class ForwardThread(threading.Thread):
if self.client is None: if self.client is None:
try: try:
self.client = paramiko.SSHClient() self.client = paramiko.SSHClient()
self.client.useCount = 1 # type: ignore self.client.useCount = 1 # Custom added variable, to keep track on when to close tunnel
self.client.load_system_host_keys() self.client.load_system_host_keys()
self.client.set_missing_host_key_policy( self.client.set_missing_host_key_policy(CheckfingerPrints(self.fingerPrints))
CheckfingerPrints(self.fingerPrints)
)
logger.debug('Connecting to ssh host %s:%d ...', self.server, self.port) logger.debug('Connecting to ssh host %s:%d ...', self.server, self.port)
# To disable ssh-ageng asking for passwords: allow_agent=False # To disable ssh-ageng asking for passwords: allow_agent=False
self.client.connect( self.client.connect(self.server, self.port, username=self.username, password=self.password, timeout=5, allow_agent=False)
self.server,
self.port,
username=self.username,
password=self.password,
timeout=5,
allow_agent=False,
)
except Exception: except Exception:
logger.exception('Exception connecting: ') logger.exception('Exception connecting: ')
self.status = 2 # Error self.status = 2 # Error
@ -235,30 +173,18 @@ class ForwardThread(threading.Thread):
self.timer.cancel() self.timer.cancel()
self.stopEvent.set() self.stopEvent.set()
self.fs.shutdown()
if self.fs:
self.fs.shutdown()
if self.client is not None: if self.client is not None:
self.client.useCount -= 1 # type: ignore self.client.useCount -= 1
if self.client.useCount == 0: # type: ignore if self.client.useCount == 0:
self.client.close() self.client.close()
self.client = None # Clean up self.client = None # Clean up
except Exception: except Exception:
logger.exception('Exception stopping') logger.exception('Exception stopping')
def forward( def forward(server, port, username, password, redirectHost, redirectPort, localPort=None, waitTime=10, fingerPrints=None):
server,
port,
username,
password,
redirectHost,
redirectPort,
localPort=None,
waitTime=10,
fingerPrints=None,
):
''' '''
Instantiates an ssh connection to server:port Instantiates an ssh connection to server:port
Returns the Thread created and the local redirected port as a list: (thread, port) Returns the Thread created and the local redirected port as a list: (thread, port)
@ -268,28 +194,10 @@ def forward(
if localPort is None: if localPort is None:
localPort = random.randrange(40000, 50000) localPort = random.randrange(40000, 50000)
logger.debug( logger.debug('Connecting to %s:%s using %s/%s redirecting to %s:%s, listening on 127.0.0.1:%s',
'Connecting to %s:%s using %s/%s redirecting to %s:%s, listening on 127.0.0.1:%s', server, port, username, password, redirectHost, redirectPort, localPort)
server,
port,
username,
password,
redirectHost,
redirectPort,
localPort,
)
ft = ForwardThread( ft = ForwardThread(server, port, username, password, localPort, redirectHost, redirectPort, waitTime, fingerPrints)
server,
port,
username,
password,
localPort,
redirectHost,
redirectPort,
waitTime,
fingerPrints,
)
ft.start() ft.start()

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014-2021 Virtual Cable S.L.U. # Copyright (c) 2014 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -29,35 +29,27 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
from __future__ import unicode_literals
import logging import logging
import os import os
import os.path
import sys import sys
import tempfile import tempfile
LOGLEVEL = logging.INFO if sys.platform.startswith('linux'):
DEBUG = False from os.path import expanduser # pylint: disable=ungrouped-imports
logFile = expanduser('~/udsclient.log')
# Update debug level if uds-debug-on exists
if 'linux' in sys.platform or 'darwin' in sys.platform:
logFile = os.path.expanduser('~/udsclient.log')
if os.path.isfile(os.path.expanduser('~/uds-debug-on')):
LOGLEVEL = logging.DEBUG
DEBUG = True
else: else:
logFile = os.path.join(tempfile.gettempdir(), 'udsclient.log') logFile = os.path.join(tempfile.gettempdir(), b'udsclient.log')
if os.path.isfile(os.path.join(tempfile.gettempdir(), 'uds-debug-on')):
LOGLEVEL = logging.DEBUG
DEBUG = True
try: try:
logging.basicConfig( logging.basicConfig(
filename=logFile, filename=logFile,
filemode='a', filemode='a',
format='%(levelname)s %(asctime)s %(message)s', format='%(levelname)s %(asctime)s %(message)s',
level=LOGLEVEL, level=logging.INFO
) )
except Exception: except Exception:
logging.basicConfig(format='%(levelname)s %(asctime)s %(message)s', level=LOGLEVEL) logging.basicConfig(format='%(levelname)s %(asctime)s %(message)s', level=logging.INFO)
logger = logging.getLogger('udsclient') logger = logging.getLogger('udsclient')

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2017-2021 Virtual Cable S.L.U. # Copyright (c) 2017 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -30,13 +30,14 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
from __future__ import unicode_literals
import sys import sys
LINUX = 'Linux' LINUX = 'Linux'
WINDOWS = 'Windows' WINDOWS = 'Windows'
MAC_OS_X = 'Mac os x' MAC_OS_X = 'Mac os x'
def getOs(): def getOs():
if sys.platform.startswith('linux'): if sys.platform.startswith('linux'):
return LINUX return LINUX

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2017-2021 Virtual Cable S.L.U. # Copyright (c) 2017 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -29,224 +30,95 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
# pylint: disable=c-extension-no-member,no-name-in-module
import json import json
import bz2
import base64
import urllib import urllib
import urllib.parse import urllib.parse
import urllib.request
import urllib.error
import ssl
import socket
import typing
from cryptography import x509 from PyQt5.QtCore import pyqtSignal, pyqtSlot
from cryptography.hazmat.backends import default_backend from PyQt5.QtCore import QObject, QUrl, QSettings
from PyQt5.QtCore import Qt
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QSslCertificate
from PyQt5.QtWidgets import QMessageBox
from . import osDetector
from . import os_detector
from . import tools
from . import VERSION from . import VERSION
from .log import logger
# Server before this version uses "unsigned" scripts
OLD_METHOD_VERSION = '2.4.0'
# Callback for error on cert
# parameters are hostname, serial
# If returns True, ignores error
CertCallbackType = typing.Callable[[str, str], bool]
# Exceptions
class UDSException(Exception):
pass
class RetryException(UDSException):
pass
class RestRequest(QObject):
class InvalidVersion(UDSException): restApiUrl = '' #
downloadUrl: str
def __init__(self, downloadUrl: str) -> None: done = pyqtSignal(dict, name='done')
super().__init__(downloadUrl)
self.downloadUrl = downloadUrl
class RestApi:
_restApiUrl: str # base Rest API URL
_callbackInvalidCert: typing.Optional[CertCallbackType]
_serverVersion: str
def __init__(
self,
restApiUrl,
callbackInvalidCert: typing.Optional[CertCallbackType] = None,
) -> None: # parent not used
logger.debug('Setting request URL to %s', restApiUrl)
self._restApiUrl = restApiUrl
self._callbackInvalidCert = callbackInvalidCert
self._serverVersion = ''
def get(
self, url: str, params: typing.Optional[typing.Mapping[str, str]] = None
) -> typing.Any:
if params:
url += '?' + '&'.join(
'{}={}'.format(k, urllib.parse.quote(str(v).encode('utf8')))
for k, v in params.items()
)
return json.loads(
RestApi.getUrl(self._restApiUrl + url, self._callbackInvalidCert)
)
def processError(self, data: typing.Any) -> None:
if 'error' in data:
if data.get('retryable', '0') == '1':
raise RetryException(data['error'])
raise UDSException(data['error'])
def getVersion(self) -> str:
'''Gets and stores the serverVersion.
Also checks that the version is valid for us. If not,
will raise an "InvalidVersion' exception'''
downloadUrl = ''
if not self._serverVersion:
data = self.get('')
self.processError(data)
self._serverVersion = data['result']['requiredVersion']
downloadUrl = data['result']['downloadUrl']
def __init__(self, url, parentWindow, done, params=None): # parent not used
super(RestRequest, self).__init__()
# private
self._manager = QNetworkAccessManager()
try: try:
if self._serverVersion > VERSION: if os.path.exists('/etc/ssl/certs/ca-certificates.crt'):
raise InvalidVersion(downloadUrl) pass
# os.environ['REQUESTS_CA_BUNDLE'] = '/etc/ssl/certs/ca-certificates.crt'
except Exception:
pass
return self._serverVersion
if params is not None:
url += '?' + '&'.join('{}={}'.format(k, urllib.parse.quote(str(v).encode('utf8'))) for k, v in params.items())
self.url = QUrl(RestRequest.restApiUrl + url)
# connect asynchronous result, when a request finishes
self._manager.finished.connect(self._finished)
self._manager.sslErrors.connect(self._sslError)
self._parentWindow = parentWindow
self.done.connect(done, Qt.QueuedConnection)
def _finished(self, reply):
'''
Handle signal 'finished'. A network request has finished.
'''
try:
if reply.error() != QNetworkReply.NoError:
raise Exception(reply.errorString())
data = bytes(reply.readAll())
data = json.loads(data)
except Exception as e: except Exception as e:
raise UDSException(e) data = {
'result': None,
'error': str(e)
}
def getScriptAndParams( self.done.emit(data)
self, ticket: str, scrambler: str
) -> typing.Tuple[str, typing.Any]:
'''Gets the transport script, validates it if necesary
and returns it'''
try:
data = self.get(
'/{}/{}'.format(ticket, scrambler),
params={'hostname': tools.getHostName(), 'version': VERSION},
)
except Exception as e:
logger.exception('Got exception on getTransportData')
raise e
logger.debug('Transport data received') reply.deleteLater() # schedule for delete from main event loop
self.processError(data)
params = None def _sslError(self, reply, errors):
settings = QSettings()
settings.beginGroup('ssl')
cert = errors[0].certificate()
digest = str(cert.digest().toHex())
if self._serverVersion <= OLD_METHOD_VERSION: approved = settings.value(digest, False)
script = bz2.decompress(base64.b64decode(data['result']))
# This fixes uds 2.2 "write" string on binary streams on some transport
script = script.replace(b'stdin.write("', b'stdin.write(b"')
script = script.replace(b'version)', b'version.decode("utf-8"))')
else:
res = data['result']
# We have three elements on result:
# * Script
# * Signature
# * Script data
# We test that the Script has correct signature, and them execute it with the parameters
# script, signature, params = res['script'].decode('base64').decode('bz2'), res['signature'], json.loads(res['params'].decode('base64').decode('bz2'))
script, signature, params = (
bz2.decompress(base64.b64decode(res['script'])),
res['signature'],
json.loads(bz2.decompress(base64.b64decode(res['params']))),
)
if tools.verifySignature(script, signature) is False:
logger.error('Signature is invalid')
raise Exception( errorString = '<p>The certificate for <b>{}</b> has the following errors:</p><ul>'.format(cert.subjectInfo(QSslCertificate.CommonName))
'Invalid UDS code signature. Please, report to administrator'
)
return script.decode(), params for err in errors:
errorString += '<li>' + err.errorString() + '</li>'
# exec(script.decode("utf-8"), globals(), {'parent': self, 'sp': params}) errorString += '</ul>'
@staticmethod if approved or QMessageBox.warning(self._parentWindow, 'SSL Warning', errorString, QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
def _open( settings.setValue(digest, True)
url: str, certErrorCallback: typing.Optional[CertCallbackType] = None reply.ignoreSslErrors()
) -> typing.Any:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
# If we have the certificates file, we use it
if tools.getCaCertsFile() is not None:
ctx.load_verify_locations(tools.getCaCertsFile())
hostname = urllib.parse.urlparse(url)[1]
serial = ''
port = '' settings.endGroup()
if ':' in hostname:
hostname, port = hostname.split(':')
if url.startswith('https'): def get(self):
port = port or '443' request = QNetworkRequest(self.url)
with ctx.wrap_socket( request.setRawHeader(b'User-Agent', osDetector.getOs().encode('utf-8') + b" - UDS Connector " + VERSION.encode('utf-8'))
socket.socket(socket.AF_INET, socket.SOCK_STREAM), self._manager.get(request)
server_hostname=hostname,
) as s:
s.connect((hostname, int(port)))
# Get binary certificate
binCert = s.getpeercert(True)
if binCert:
cert = x509.load_der_x509_certificate(binCert, default_backend())
else:
raise Exception('Certificate not found!')
serial = hex(cert.serial_number)[2:]
response = None
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.check_hostname = True
def urlopen(url: str):
# Generate the request with the headers
req = urllib.request.Request(
url,
headers={
'User-Agent': os_detector.getOs() + " - UDS Connector " + VERSION
},
)
return urllib.request.urlopen(req, context=ctx)
try:
response = urlopen(url)
except urllib.error.URLError as e:
if isinstance(e.reason, ssl.SSLCertVerificationError):
# Ask about invalid certificate
if certErrorCallback:
if certErrorCallback(hostname, serial):
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
response = urlopen(url)
else:
raise
else:
raise
return response
@staticmethod
def getUrl(
url: str, certErrorCallback: typing.Optional[CertCallbackType] = None
) -> bytes:
with RestApi._open(url, certErrorCallback) as response:
resp = response.read()
return resp

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2015-2021 Virtual Cable S.L.U. # Copyright (c) 2015 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -29,36 +30,31 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
from __future__ import unicode_literals
from base64 import b64decode
import tempfile import tempfile
import string import string
import random import random
import os import os
import os.path
import sys
import socket import socket
import stat import stat
import sys import sys
import time import time
import base64
import typing
import certifi import six
try:
import psutil
except ImportError:
psutil = None
from .log import logger from .log import logger
_unlinkFiles: typing.List[typing.Tuple[str, bool]] = [] _unlinkFiles = []
_tasksToWait: typing.List[typing.Tuple[typing.Any, bool]] = [] _tasksToWait = []
_execBeforeExit: typing.List[typing.Callable[[], None]] = [] _execBeforeExit = []
sys_fs_enc = sys.getfilesystemencoding() or 'mbcs' sys_fs_enc = sys.getfilesystemencoding() or 'mbcs'
# Public key for scripts # Public key for scripts
PUBLIC_KEY = b'''-----BEGIN PUBLIC KEY----- PUBLIC_KEY = '''-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuNURlGjBpqbglkTTg2lh MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuNURlGjBpqbglkTTg2lh
dU5qPbg9Q+RofoDDucGfrbY0pjB9ULgWXUetUWDZhFG241tNeKw+aYFTEorK5P+g dU5qPbg9Q+RofoDDucGfrbY0pjB9ULgWXUetUWDZhFG241tNeKw+aYFTEorK5P+g
ud7h9KfyJ6huhzln9eyDu3k+kjKUIB1PLtA3lZLZnBx7nmrHRody1u5lRaLVplsb ud7h9KfyJ6huhzln9eyDu3k+kjKUIB1PLtA3lZLZnBx7nmrHRody1u5lRaLVplsb
@ -74,13 +70,15 @@ nVgtClKcDDlSaBsO875WDR0CAwEAAQ==
-----END PUBLIC KEY-----''' -----END PUBLIC KEY-----'''
def saveTempFile(content: str, filename: typing.Optional[str] = None) -> str: def saveTempFile(content, filename=None):
if filename is None: if filename is None:
filename = ''.join( filename = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
random.choice(string.ascii_lowercase + string.digits) for _ in range(16)
)
filename = filename + '.uds' filename = filename + '.uds'
if 'win32' in sys.platform:
logger.info('Fixing for win32')
filename = filename.encode(sys_fs_enc)
filename = os.path.join(tempfile.gettempdir(), filename) filename = os.path.join(tempfile.gettempdir(), filename)
with open(filename, 'w') as f: with open(filename, 'w') as f:
@ -90,7 +88,10 @@ def saveTempFile(content: str, filename: typing.Optional[str] = None) -> str:
return filename return filename
def readTempFile(filename: str) -> typing.Optional[str]: def readTempFile(filename):
if 'win32' in sys.platform:
filename = filename.encode('utf-8')
filename = os.path.join(tempfile.gettempdir(), filename) filename = os.path.join(tempfile.gettempdir(), filename)
try: try:
with open(filename, 'r') as f: with open(filename, 'r') as f:
@ -99,7 +100,7 @@ def readTempFile(filename: str) -> typing.Optional[str]:
return None return None
def testServer(host: str, port: typing.Union[str, int], timeOut: int = 4) -> bool: def testServer(host, port, timeOut=4):
try: try:
sock = socket.create_connection((host, int(port)), timeOut) sock = socket.create_connection((host, int(port)), timeOut)
sock.close() sock.close()
@ -108,11 +109,11 @@ def testServer(host: str, port: typing.Union[str, int], timeOut: int = 4) -> boo
return True return True
def findApp( def findApp(appName, extraPath=None):
appName: str, extraPath: typing.Optional[str] = None if 'win32' in sys.platform and isinstance(appName, six.text_type):
) -> typing.Optional[str]: appName = appName.encode(sys_fs_enc)
searchPath = os.environ['PATH'].split(os.pathsep) searchPath = os.environ['PATH'].split(os.pathsep)
if extraPath: if extraPath is not None:
searchPath += list(extraPath) searchPath += list(extraPath)
for path in searchPath: for path in searchPath:
@ -122,101 +123,68 @@ def findApp(
return None return None
def getHostName() -> str: def getHostName():
''' '''
Returns current host name Returns current host name
In fact, it's a wrapper for socket.gethostname() In fact, it's a wrapper for socket.gethostname()
''' '''
hostname = socket.gethostname() hostname = socket.gethostname()
if 'win32' in sys.platform:
hostname = hostname.decode(sys_fs_enc)
hostname = six.text_type(hostname)
logger.info('Hostname: %s', hostname) logger.info('Hostname: %s', hostname)
return hostname return hostname
# Queing operations (to be executed before exit) # Queing operations (to be executed before exit)
def addFileToUnlink(filename: str, early: bool = False) -> None: def addFileToUnlink(filename):
''' '''
Adds a file to the wait-and-unlink list Adds a file to the wait-and-unlink list
''' '''
logger.debug( _unlinkFiles.append(filename)
'Added file %s to unlink on %s stage', filename, 'early' if early else 'later'
)
_unlinkFiles.append((filename, early))
def unlinkFiles(early: bool = False) -> None: def unlinkFiles():
''' '''
Removes all wait-and-unlink files Removes all wait-and-unlink files
''' '''
logger.debug('Unlinking files on %s stage', 'early' if early else 'later') if _unlinkFiles:
filesToUnlink = list(filter(lambda x: x[1] == early, _unlinkFiles)) time.sleep(5) # Wait 5 seconds before deleting anything
if filesToUnlink:
logger.debug('Files to unlink: %s', filesToUnlink)
# Wait 2 seconds before deleting anything on early and 5 on later stages
time.sleep(1 + 2 * (1 + int(early)))
for f in filesToUnlink: for f in _unlinkFiles:
try: try:
os.unlink(f[0]) os.unlink(f)
except Exception as e: except Exception:
logger.debug('File %s not deleted: %s', f[0], e) pass
def addTaskToWait(task: typing.Any, includeSubprocess: bool = False) -> None: def addTaskToWait(taks):
logger.debug( _tasksToWait.append(taks)
'Added task %s to wait %s',
task,
'with subprocesses' if includeSubprocess else '',
)
_tasksToWait.append((task, includeSubprocess))
def waitForTasks() -> None: def waitForTasks():
logger.debug('Started to wait %s', _tasksToWait) for t in _tasksToWait:
for task, waitForSubp in _tasksToWait:
logger.debug('Waiting for task %s, subprocess wait: %s', task, waitForSubp)
try: try:
if hasattr(task, 'join'): if hasattr(t, 'join'):
task.join() t.join()
elif hasattr(task, 'wait'): elif hasattr(t, 'wait'):
task.wait() t.wait()
# If wait for spanwed process (look for process with task pid) and we can look for them... except Exception:
logger.debug( pass
'Psutil: %s, waitForSubp: %s, hasattr: %s',
psutil,
waitForSubp,
hasattr(task, 'pid'),
)
if psutil and waitForSubp and hasattr(task, 'pid'):
subProcesses = list(
filter(
lambda x: x.ppid() == task.pid, # type: ignore
psutil.process_iter(attrs=('ppid',)),
)
)
logger.debug(
'Waiting for subprocesses... %s, %s', task.pid, subProcesses
)
for i in subProcesses:
logger.debug('Found %s', i)
i.wait()
except Exception as e:
logger.error('Waiting for tasks to finish error: %s', e)
def addExecBeforeExit(fnc: typing.Callable[[], None]) -> None: def addExecBeforeExit(fnc):
logger.debug('Added exec before exit: %s', fnc)
_execBeforeExit.append(fnc) _execBeforeExit.append(fnc)
def execBeforeExit() -> None: def execBeforeExit():
logger.debug('Esecuting exec before exit: %s', _execBeforeExit)
for fnc in _execBeforeExit: for fnc in _execBeforeExit:
fnc() fnc.__call__()
def verifySignature(script: bytes, signature: bytes) -> bool: def verifySignature(script, signature):
''' '''
Verifies with a public key from whom the data came that it was indeed Verifies with a public key from whom the data came that it was indeed
signed by their private key signed by their private key
@ -225,45 +193,13 @@ def verifySignature(script: bytes, signature: bytes) -> bool:
return: Boolean. True if the signature is valid; False otherwise. return: Boolean. True if the signature is valid; False otherwise.
''' '''
# For signature checking # For signature checking
from cryptography.hazmat.backends import default_backend from Crypto.PublicKey import RSA
from cryptography.hazmat.primitives import serialization, hashes from Crypto.Signature import PKCS1_v1_5
from cryptography.hazmat.primitives.asymmetric import utils, padding from Crypto.Hash import SHA256
public_key = serialization.load_pem_public_key( rsakey = RSA.importKey(PUBLIC_KEY)
data=PUBLIC_KEY, backend=default_backend() signer = PKCS1_v1_5.new(rsakey)
) digest = SHA256.new(script) # Script is "binary string" here
if signer.verify(digest, b64decode(signature)):
try: return True
public_key.verify( # type: ignore return False
base64.b64decode(signature), script, padding.PKCS1v15(), hashes.SHA256() # type: ignore
)
except Exception: # InvalidSignature
return False
# If no exception, the script was fine...
return True
def getCaCertsFile() -> typing.Optional[str]:
# First, try certifi...
# If environment contains CERTIFICATE_BUNDLE_PATH, use it
if 'CERTIFICATE_BUNDLE_PATH' in os.environ:
return os.environ['CERTIFICATE_BUNDLE_PATH']
try:
if os.path.exists(certifi.where()):
return certifi.where()
except Exception:
pass
logger.info('Certifi file does not exists: %s', certifi.where())
# Check if "standard" paths are valid for linux systems
if 'linux' in sys.platform:
for path in ('/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/certs/ca-certificates.crt', '/etc/ssl/ca-bundle.pem'):
if os.path.exists(path):
logger.info('Found certifi path: %s', path)
return path
return None

View File

@ -1,289 +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.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
import socket
import socketserver
import ssl
import threading
import time
import random
import threading
import select
import typing
import logging
from . import tools
HANDSHAKE_V1 = b'\x5AMGB\xA5\x01\x00'
BUFFER_SIZE = 1024 * 16 # Max buffer length
DEBUG = True
LISTEN_ADDRESS = '0.0.0.0' if DEBUG else '127.0.0.1'
# ForwarServer states
TUNNEL_LISTENING, TUNNEL_OPENING, TUNNEL_PROCESSING, TUNNEL_ERROR = 0, 1, 2, 3
logger = logging.getLogger(__name__)
class ForwardServer(socketserver.ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
remote: typing.Tuple[str, int]
ticket: str
stop_flag: threading.Event
can_stop: bool
timeout: int
timer: typing.Optional[threading.Timer]
check_certificate: bool
current_connections: int
status: int
def __init__(
self,
remote: typing.Tuple[str, int],
ticket: str,
timeout: int = 0,
local_port: int = 0,
check_certificate: bool = True,
) -> None:
local_port = local_port or random.randrange(33000, 53000)
super().__init__(
server_address=(LISTEN_ADDRESS, local_port), RequestHandlerClass=Handler
)
self.remote = remote
self.ticket = ticket
# Negative values for timeout, means "accept always connections"
# "but if no connection is stablished on timeout (positive)"
# "stop the listener"
self.timeout = int(time.time()) + timeout if timeout > 0 else 0
self.check_certificate = check_certificate
self.stop_flag = threading.Event() # False initial
self.current_connections = 0
self.status = TUNNEL_LISTENING
self.can_stop = False
timeout = abs(timeout) or 60
self.timer = threading.Timer(
abs(timeout), ForwardServer.__checkStarted, args=(self,)
)
self.timer.start()
def stop(self) -> None:
if not self.stop_flag.is_set():
logger.debug('Stopping servers')
self.stop_flag.set()
if self.timer:
self.timer.cancel()
self.timer = None
self.shutdown()
def connect(self) -> ssl.SSLSocket:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as rsocket:
logger.info('CONNECT to %s', self.remote)
rsocket.connect(self.remote)
rsocket.sendall(HANDSHAKE_V1) # No response expected, just the handshake
context = ssl.create_default_context()
# Do not "recompress" data, use only "base protocol" compression
context.options |= ssl.OP_NO_COMPRESSION
if tools.getCaCertsFile() is not None:
context.load_verify_locations(
tools.getCaCertsFile()
) # Load certifi certificates
# If ignore remote certificate
if self.check_certificate is False:
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
logger.warning('Certificate checking is disabled!')
return context.wrap_socket(rsocket, server_hostname=self.remote[0])
def check(self) -> bool:
if self.status == TUNNEL_ERROR:
return False
logger.debug('Checking tunnel availability')
try:
with self.connect() as ssl_socket:
ssl_socket.sendall(b'TEST')
resp = ssl_socket.recv(2)
if resp != b'OK':
raise Exception({'Invalid tunnelresponse: {resp}'})
logger.debug('Tunnel is available!')
return True
except Exception as e:
logger.error(
'Error connecting to tunnel server %s: %s', self.server_address, e
)
return False
@property
def stoppable(self) -> bool:
logger.debug('Is stoppable: %s', self.can_stop)
return self.can_stop or (self.timeout != 0 and int(time.time()) > self.timeout)
@staticmethod
def __checkStarted(fs: 'ForwardServer') -> None:
logger.debug('New connection limit reached')
fs.timer = None
fs.can_stop = True
if fs.current_connections <= 0:
fs.stop()
class Handler(socketserver.BaseRequestHandler):
# Override Base type
server: ForwardServer
# server: ForwardServer
def handle(self) -> None:
self.server.status = TUNNEL_OPENING
# 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
return
self.server.current_connections += 1
# Open remote connection
try:
logger.debug('Ticket %s', self.server.ticket)
with self.server.connect() as ssl_socket:
# Send handhshake + command + ticket
ssl_socket.sendall(b'OPEN' + self.server.ticket.encode())
# Check response is OK
data = ssl_socket.recv(2)
if data != b'OK':
data += ssl_socket.recv(128)
raise Exception(
f'Error received: {data.decode(errors="ignore")}'
) # Notify error
# 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}')
self.server.status = TUNNEL_ERROR
self.server.stop()
finally:
self.server.current_connections -= 1
if self.server.current_connections <= 0 and self.server.stoppable:
self.server.stop()
# Processes data forwarding
def process(self, remote: ssl.SSLSocket):
self.server.status = TUNNEL_PROCESSING
logger.debug('Processing tunnel with ticket %s', self.server.ticket)
# Process data until stop requested or connection closed
try:
while not self.server.stop_flag.is_set():
r, _w, _x = select.select([self.request, remote], [], [], 1.0)
if self.request in r:
data = self.request.recv(BUFFER_SIZE)
if not data:
break
remote.sendall(data)
if remote in r:
data = remote.recv(BUFFER_SIZE)
if not data:
break
self.request.sendall(data)
logger.debug('Finished tunnel with ticket %s', self.server.ticket)
except Exception as e:
pass
def _run(server: ForwardServer) -> None:
logger.debug(
'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)
def forward(
remote: typing.Tuple[str, int],
ticket: str,
timeout: int = 0,
local_port: int = 0,
check_certificate=True,
) -> ForwardServer:
fs = ForwardServer(
remote=remote,
ticket=ticket,
timeout=timeout,
local_port=local_port,
check_certificate=check_certificate,
)
# 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,
)

Some files were not shown because too many files have changed in this diff Show More