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

6 Commits

Author SHA1 Message Date
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
Adolfo Gómez García
d38347c534 Fixed ticket for metapools & fixed get interfaces list for python > 3.2 (as is the case) 2021-07-19 13:25:43 +02:00
634 changed files with 49727 additions and 39042 deletions

3
.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_*

View File

@@ -12,4 +12,5 @@ This is an Open Source Source project, initiated by Spanish Company Virtualca
Any help provided will be welcome. 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 @@
3.5.0 3.0.0

View File

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

View File

@@ -1,9 +1,3 @@
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.5.0_all.deb admin optional udsactor-unmanaged_3.0.0_all.deb admin optional
udsactor_3.5.0_all.deb admin optional udsactor_3.0.0_all.deb admin optional
udsactor_3.5.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 actor_client.py -platform xcb $@ exec python3 actor_client.py $@

View File

@@ -67,7 +67,7 @@ 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_()

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
@@ -212,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')
@@ -232,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) 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

@@ -96,7 +96,7 @@ def _getInterfaces() -> typing.List[str]:
0x8912, # SIOCGIFCONF 0x8912, # SIOCGIFCONF
struct.pack(str('iL'), space, names.buffer_info()[0]) struct.pack(str('iL'), space, names.buffer_info()[0])
))[0] ))[0]
namestr = names.tostring() namestr = names.tobytes()
# return namestr, outbytes # return namestr, outbytes
return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)] return [namestr[i:i + offset].split(b'\0', 1)[0].decode('utf-8') for i in range(0, outbytes, length)]
@@ -186,9 +186,9 @@ def getCurrentUser() -> str:
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 'xrdp' if 'XRDP_SESSION' in os.environ else os.environ.get('XDG_SESSION_TYPE', 'unknown') return 'xrdp' if 'XRDP_SESSION' in os.environ else os.environ.get('XDG_SESSION_TYPE', 'unknown')

View File

@@ -100,6 +100,3 @@ def writeConfig(config: types.ActorConfigurationType) -> None:
def useOldJoinSystem() -> bool: def useOldJoinSystem() -> bool:
return False return False
def invokeScriptOnLogin() -> str:
return ''

View File

@@ -162,19 +162,19 @@ class UDSServerApi(UDSApi):
except Exception: except Exception:
pass pass
def register( # pylint: disable=too-many-arguments, too-many-locals 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"
""" """
@@ -212,10 +212,10 @@ class UDSServerApi(UDSApi):
raise RESTError(result.content.decode()) raise RESTError(result.content.decode())
def initialize(self, token: str, interfaces: typing.Iterable[types.InterfaceInfoType], actor_type: typing.Optional[str]) -> types.InitializationResultType: def initialize(self, token: str, interfaces: typing.Iterable[types.InterfaceInfoType], actorType: 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,
'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces] 'id': [{'mac': i.mac, 'ip': i.ip} for i in interfaces]
@@ -281,16 +281,9 @@ class UDSServerApi(UDSApi):
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,
sessionType: 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', ip='0.0.0.0',
hostname=UNKNOWN, hostname=UNKNOWN,
@@ -298,12 +291,9 @@ class UDSServerApi(UDSApi):
max_idle=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': sessionType, 'session_type': sessionType or UNKNOWN
'secret': secret or '',
} }
result = self._doPost('login', payload) result = self._doPost('login', payload)
return types.LoginResultInfoType( return types.LoginResultInfoType(
@@ -313,22 +303,12 @@ class UDSServerApi(UDSApi):
max_idle=result['max_idle'] max_idle=result['max_idle']
) )
def logout( def logout(self, own_token: str, username: str) -> None:
self, if not own_token:
actor_type: typing.Optional[str],
token: str,
username: str,
interfaces: typing.Iterable[types.InterfaceInfoType],
secret: typing.Optional[str]
) -> None:
if not token:
return return
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,
'secret': secret or ''
} }
self._doPost('logout', payload) self._doPost('logout', payload)
@@ -359,6 +339,7 @@ class UDSClientApi(UDSApi):
def _apiURL(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', ....

View File

@@ -273,7 +273,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
logger.debug('This host is not managed by UDS Broker (ids: {})'.format(self._interfaces)) logger.debug('This host is not managed by UDS Broker (ids: {})'.format(self._interfaces))
return False return False
# Only removes master token for managed machines (will need it on next client execution) # Only removes token for managed machines
master_token = None if self.isManaged() else self._cfg.master_token master_token = None if self.isManaged() else self._cfg.master_token
self._cfg = self._cfg._replace( self._cfg = self._cfg._replace(
master_token=master_token, master_token=master_token,
@@ -299,9 +299,6 @@ class CommonService: # pylint: disable=too-many-instance-attributes
except rest.RESTError as e: # Invalid key? except rest.RESTError as e: # Invalid key?
logger.error('Error validating with broker. (Invalid token?): {}'.format(e)) logger.error('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()
@@ -317,13 +314,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes
if self._loggedIn and self._cfg.own_token: if self._loggedIn and self._cfg.own_token:
self._loggedIn = False self._loggedIn = False
try: try:
self._api.logout( self._api.logout(self._cfg.own_token, '')
self._cfg.actorType,
self._cfg.own_token,
'',
self._interfaces,
self._secret
)
except Exception as e: except Exception as e:
logger.error('Error notifying final logout to UDS: %s', e) logger.error('Error notifying final logout to UDS: %s', e)
@@ -414,52 +405,18 @@ class CommonService: # pylint: disable=too-many-instance-attributes
def login(self, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType: def login(self, username: str, sessionType: typing.Optional[str] = None) -> types.LoginResultInfoType:
result = types.LoginResultInfoType(ip='', hostname='', dead_line=None, max_idle=None) result = types.LoginResultInfoType(ip='', hostname='', dead_line=None, max_idle=None)
self._loggedIn = True self._loggedIn = True
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.initialize() self.initialize()
master_token = self._cfg.master_token
secret = self._secret
# Own token will not be set if UDS did not assigned the initialized VM to an user
# In that case, take master token (if machine is Unamanaged version)
token = self._cfg.own_token or master_token
if token:
result = self._api.login(
self._cfg.actorType,
token,
username,
sessionType or '',
self._interfaces,
secret
)
script = platform.store.invokeScriptOnLogin() if self._cfg.own_token:
if script: result = self._api.login(self._cfg.own_token, username, sessionType)
script += f'{username} {sessionType or "unknown"} {self._cfg.actorType}'
self.execute(script, 'Logon')
return result return result
def logout(self, username: str) -> None: def logout(self, username: str) -> None:
self._loggedIn = False self._loggedIn = False
if self._cfg.own_token:
master_token = self._cfg.master_token if self.isManaged() else None self._api.logout(self._cfg.own_token, username)
# Own token will not be set if UDS did not assigned the initialized VM to an user
# In that case, take master token (if machine is Unamanaged version)
token = self._cfg.own_token or master_token
if token:
self._api.logout(
self._cfg.actorType,
token,
username,
self._interfaces,
self._secret
)
self.onLogout(username) self.onLogout(username)

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
@@ -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

@@ -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

@@ -46,35 +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),armf)
cat udsclient-appimage-x86_64.recipe | sed -e s/"version: 0.0.0"/"version: $(VERSION)"/g | sed -e s/amd64\\\|x86_64/armhf/g > appimage.recipe
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

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,13 +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
rpm --addsign ../*rpm rpm --addsign ../*rpm

View File

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

@@ -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,2 +1,2 @@
udsclient3_3.5.0_all.deb admin optional udsclient3_3.0.0_all.deb admin optional
udsclient3_3.5.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,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

@@ -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=0x04EE7237B7D453EC'
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 python3 #!/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 from PyQt5 import QtCore, QtGui, QtWidgets # @UnresolvedImport
from PyQt5.QtCore import QSettings import six
from uds.rest import RestApi, RetryException, InvalidVersion, UDSException from uds.rest import RestRequest
from uds.forward import forward # pylint: disable=unused-import
# Just to ensure there are available on runtime from uds.log import logger
from uds.forward import forward # type: ignore
from uds.tunnel import forward as f2 # type: ignore
from uds.log import logger, DEBUG
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)
@@ -86,29 +82,34 @@ class UDSClient(QtWidgets.QMainWindow):
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,201 +125,146 @@ 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:
if DEBUG: #logger.exception('Got exception executing script:')
logger.exception('Got exception on getTransportData')
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
# Used only if command line says so
def minimal(api: RestApi, ticket: str, scrambler: str):
try:
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
if __name__ == "__main__": if __name__ == "__main__":
logger.debug('Initializing connector for %s(%s)', sys.platform, platform.machine()) logger.debug('Initializing connector')
# Initialize app # Initialize app
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
@@ -328,27 +274,17 @@ if __name__ == "__main__":
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 = sys.argv[1] uri = sys.argv[1]
if uri == '--minimal':
useMinimal = True
uri = sys.argv[2] # And get URI
if uri == '--test': if uri == '--test':
sys.exit(0) sys.exit(0)
@@ -357,28 +293,17 @@ if __name__ == "__main__":
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,
UDSClient.ticket,
UDSClient.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')
@@ -387,7 +312,7 @@ if __name__ == "__main__":
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()
@@ -398,9 +323,7 @@ if __name__ == "__main__":
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)

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,
@@ -31,7 +31,7 @@
''' '''
from __future__ import unicode_literals from __future__ import unicode_literals
VERSION = '3.5.0' VERSION = '3.0.0'
__title__ = 'udclient' __title__ = 'udclient'
__version__ = VERSION __version__ = VERSION

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
@@ -37,11 +36,6 @@ 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
@@ -92,8 +86,6 @@ 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__(self, server, port, username, password, localPort, redirectHost, redirectPort, waitTime, fingerPrints): def __init__(self, server, port, username, password, localPort, redirectHost, redirectPort, waitTime, fingerPrints):
threading.Thread.__init__(self) threading.Thread.__init__(self)
@@ -126,7 +118,7 @@ class ForwardThread(threading.Thread):
ft = ForwardThread(self.server, self.port, self.username, self.password, localPort, redirectHost, redirectPort, self.waitTime, self.fingerPrints) ft = ForwardThread(self.server, self.port, self.username, self.password, localPort, redirectHost, redirectPort, self.waitTime, self.fingerPrints)
ft.client = self.client 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:
@@ -146,7 +138,7 @@ 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(CheckfingerPrints(self.fingerPrints)) self.client.set_missing_host_key_policy(CheckfingerPrints(self.fingerPrints))
@@ -181,13 +173,11 @@ 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:

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,
@@ -33,33 +33,23 @@ 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,

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,
@@ -32,206 +33,92 @@
# pylint: disable=c-extension-no-member,no-name-in-module # 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
import certifi from PyQt5.QtCore import pyqtSignal, pyqtSlot
from cryptography import x509 from PyQt5.QtCore import QObject, QUrl, QSettings
from cryptography.hazmat.backends import default_backend 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 InvalidVersion(UDSException):
downloadUrl: str
def __init__(self, downloadUrl: str) -> None:
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 = '' class RestRequest(QObject):
if not self._serverVersion:
data = self.get('')
self.processError(data)
self._serverVersion = data['result']['requiredVersion']
downloadUrl = data['result']['downloadUrl']
restApiUrl = '' #
done = pyqtSignal(dict, name='done')
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'
return self._serverVersion except Exception:
pass
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, ticket: str, scrambler: str) -> typing.Tuple[str, typing.Any]: self.done.emit(data)
'''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>'
if approved or QMessageBox.warning(self._parentWindow, 'SSL Warning', errorString, QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
settings.setValue(digest, True)
reply.ignoreSslErrors()
@staticmethod settings.endGroup()
def _open(
url: str, certErrorCallback: typing.Optional[CertCallbackType] = None
) -> typing.Any:
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx.load_verify_locations(certifi.where())
hostname = urllib.parse.urlparse(url)[1]
serial = ''
if url.startswith('https'): def get(self):
with ctx.wrap_socket( request = QNetworkRequest(self.url)
socket.socket(socket.AF_INET, socket.SOCK_STREAM), server_hostname=hostname request.setRawHeader(b'User-Agent', osDetector.getOs().encode('utf-8') + b" - UDS Connector " + VERSION.encode('utf-8'))
) as s: self._manager.get(request)
s.connect((hostname, 443))
# 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.check_hostname = True
ctx.verify_mode = ssl.CERT_REQUIRED
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,6 +30,10 @@
''' '''
@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
@@ -37,24 +42,19 @@ import socket
import stat import stat
import sys import sys
import time import time
import base64
import typing
try: import six
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
@@ -70,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:
@@ -86,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:
@@ -95,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()
@@ -104,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:
@@ -118,89 +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('Psutil: %s, waitForSubp: %s, hasattr: %s', psutil, waitForSubp, hasattr(task, 'pid')) pass
if psutil and waitForSubp and hasattr(task, 'pid'):
subProcesses = list(filter(
lambda x: x.ppid() == task.pid, 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
@@ -209,20 +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( return False
base64.b64decode(signature), script, padding.PKCS1v15(), hashes.SHA256()
)
except Exception: # InvalidSignature
return False
# If no exception, the script was fine...
return True

View File

@@ -1,284 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2021 Virtual Cable S.L.U.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * Neither the name of Virtual Cable S.L. nor the names of its contributors
# may be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
'''
@author: Adolfo Gómez, dkmaster at dkmon dot com
'''
import socket
import socketserver
import ssl
import threading
import time
import random
import threading
import select
import typing
import logging
import certifi
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)
context = ssl.create_default_context()
# Do not "recompress" data, use only "base protocol" compression
context.options |= ssl.OP_NO_COMPRESSION
context.load_verify_locations(certifi.where()) # Load certifi certificates
# 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(HANDSHAKE_V1 + 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(HANDSHAKE_V1 + 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,
)

4
client/full/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/bin
/udsclient_*
/udsclient-*.tar.gz
/*.rpm

17
client/full/.project Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>UDSclient</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

14
client/full/.pydevproject Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/${PROJECT_DIR_NAME}/src</path>
</pydev_pathproperty>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">system-2.7</pydev_property>
</pydev_project>

4
client/full/linux/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/udsclient-opensuse-[0-9]*.spec
/udsclient-[0-9]*.spec
/debian/udsclient
/targz

View File

@@ -0,0 +1,51 @@
#!/usr/bin/make -f
# -*- makefile -*-
# Version
# VERSION := 1.7.5
# Directories
SOURCEDIR := ../src
LIBDIR := $(DESTDIR)/usr/lib/UDSClient
BINDIR := $(DESTDIR)/usr/bin
SBINDIR = $(DESTDIR)/usr/sbin
APPSDIR := $(DESTDIR)/usr/share/applications
PYC := $(shell find $(SOURCEDIR) -name '*.py[co]')
CACHES := $(shell find $(SOURCEDIR) -name '__pycache__')
clean:
rm -rf $(PYC) $(CACHES) $(DESTDIR)
install:
rm -rf $(DESTDIR)
mkdir -p $(LIBDIR)
#mkdir -p $(BINDIR)
#mkdir -p $(SBINDIR)
mkdir -p $(APPSDIR)
mkdir $(LIBDIR)/uds
# Cleans up .pyc and cache folders
rm -f $(PYC) $(CACHES)
cp $(SOURCEDIR)/uds/*.py $(LIBDIR)/uds
cp $(SOURCEDIR)/UDS*.py $(LIBDIR)
# URL Catchers elements for gnome/kde
cp desktop/UDSClient.desktop $(APPSDIR)
chmod 755 $(LIBDIR)/UDSClient.py
ifeq ($(DISTRO),targz)
cp installer.sh $(DESTDIR)/install.sh
tar czvf ../udsclient-$(VERSION).tar.gz -C $(DESTDIR) .
endif
# chmod 0755 $(BINDIR)/udsclient
uninstall:
rm -rf $(LIBDIR)
# rm -f $(BINDIR)/udsclient
# rm -rf $(CFGDIR)

View File

@@ -0,0 +1,36 @@
#!/bin/bash
VERSION=`cat ../../../VERSION`
RELEASE=1
# Debian based
dpkg-buildpackage -b
# Now rpm based
top=`pwd`
cat udsclient-template.spec |
sed -e s/"version 0.0.0"/"version ${VERSION}"/g |
sed -e s/"release 1"/"release ${RELEASE}"/g > udsclient-$VERSION.spec
# Now fix dependencies for opensuse
cat udsclient-template.spec |
sed -e s/"version 0.0.0"/"version ${VERSION}"/g |
sed -e s/"name udsclient"/"name udsclient-opensuse"/g |
sed -e s/"PyQt4"/"python-qt4"/g |
sed -e s/"libXScrnSaver"/"libXss1"/g > udsclient-opensuse-$VERSION.spec
# Right now, udsactor-xrdp-.spec is not needed
for pkg in udsclient-$VERSION.spec udsclient-opensuse-$VERSION.spec; do
rm -rf rpm
for folder in SOURCES BUILD RPMS SPECS SRPMS; do
mkdir -p rpm/$folder
done
rpmbuild -v -bb --clean --buildroot=$top/rpm/BUILD/$pkg-root --target noarch $pkg 2>&1
done
#rm udsclient-$VERSION
make DESTDIR=targz DISTRO=targz VERSION=${VERSION} install

3
client/full/linux/debian/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
/udsactor/
/udsactor-xrdp/
/udsactor-nx/

View File

@@ -0,0 +1,47 @@
udsclient (3.0.0) stable; urgency=medium
* Upgraded to 3.0.0 release
-- Adolfo Gómez García <agomez@virtualcable.es> Wed, 10 Jul 2019 9:24:10 +0200
udsclient (2.2.1) stable; urgency=medium
* Upgraded to 2.2.1 release
-- Adolfo Gómez García <agomez@virtualcable.es> Thu, 2 Oct 2018 12:44:12 +0200
udsclient (2.2.0) stable; urgency=medium
* Updated release
-- Adolfo Gómez García <agomez@virtualcable.es> Thu, 27 Aug 2017 14:18:18 +0200
udsclient (2.1.0) stable; urgency=medium
* Updated release
-- Adolfo Gómez García <agomez@virtualcable.es> Sun, 23 Oct 2016 21:12:23 +0200
udsclient (2.0.0) stable; urgency=medium
* Release upgrade
-- Adolfo Gómez García <agomez@virtualcable.es> Tue, 01 Mar 2016 09:33:18 +0100
udsclient (1.9.1) stable; urgency=medium
* Minor fixes & making version match UDS version
-- Adolfo Gómez García <agomez@virtualcable.es> Tue, 01 Mar 2016 03:02:37 +0100
udsclient (1.9.0) stable; urgency=medium
* Minor fixes & making version match UDS version
-- Adolfo Gómez García <agomez@virtualcable.es> Tue, 05 May 2015 07:03:47 +0200
udsclient (1.7.5) stable; urgency=medium
* Initial release.
-- Adolfo Gómez García <agomez@virtualcable.es> Fri, 10 Apr 2015 05:32:41 +0100

View File

@@ -0,0 +1 @@
9

View File

@@ -0,0 +1,15 @@
Source: udsclient
Section: admin
Priority: optional
Maintainer: Adolfo Gómez García <agomez@virtualcable.es>
Build-Depends: debhelper (>= 7), po-debconf
Standards-Version: 3.9.2
Homepage: http://www.virtualcable.es
Package: udsclient
Section: admin
Priority: optional
Architecture: all
Depends: python-paramiko (>=0.8.2), python-crypto, python-qt4 (>=4.9), python-six(>=1.1), python (>=2.7), freerdp2-x11 | freerdp-x11, desktop-file-utils, ${misc:Depends}
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.

View File

@@ -0,0 +1,26 @@
Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=135
Name: udsclient
Maintainer: Adolfo Gómez García
Source: http://www.udsenterprise.com/
Copyright: 2014 Virtual Cable S.L.U.
License: BSD-3-clause
License: GPL-2+
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
.
On Debian systems, the full text of the GNU General Public
License version 2 can be found in the file
`/usr/share/common-licenses/GPL-2'.

View File

@@ -0,0 +1 @@
readme.txt

View File

@@ -0,0 +1,2 @@
udsclient_3.0.0_all.deb admin optional
udsclient_3.0.0_amd64.buildinfo admin optional

44
client/full/linux/debian/rules Executable file
View File

@@ -0,0 +1,44 @@
#!/usr/bin/make -f
# -*- makefile -*-
configure: configure-stamp
configure-stamp:
dh_testdir
touch configure-stamp
build: build-arch build-indep
build-arch: build-stamp
build-indep: build-stamp
build-stamp: configure-stamp
dh_testdir
$(MAKE)
touch $@
clean:
dh_testdir
dh_testroot
rm -f build-stamp configure-stamp
dh_clean
install: build
dh_testdir
dh_testroot
dh_prep
dh_installdirs
$(MAKE) DESTDIR=$(CURDIR)/debian/udsclient install
binary-arch: build install
# emptyness
binary-indep: build install
dh_testdir
dh_testroot
dh_installchangelogs
dh_installdocs
dh_installdebconf
dh_installinit --no-start
dh_python2=python
dh_compress
dh_link
dh_fixperms
dh_installdeb
dh_shlibdeps
dh_gencontrol
dh_md5sums
dh_builddeb
binary: binary-indep
.PHONY: build clean binary-indep binary install configure

View File

@@ -0,0 +1,17 @@
dh_prep
dh_installdirs
dh_installchangelogs
dh_installdocs
dh_installdebconf
dh_installinit
dh_compress
dh_link
dh_fixperms
dh_installdeb
dh_shlibdeps
dh_gencontrol
dh_md5sums
dh_builddeb
dh_builddeb
dh_builddeb
dh_builddeb

View File

@@ -0,0 +1,21 @@
#!/bin/sh
. /usr/share/debconf/confmodule
set -e
case "$1" in
configure)
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0

View File

@@ -0,0 +1,6 @@
#!/bin/sh -e
. /usr/share/debconf/confmodule
set -e

View File

@@ -0,0 +1 @@
#! /bin/bash -e

View File

@@ -0,0 +1,20 @@
Template: udsactor/host
Type: string
Default:
Description: UDS Server address:
The actor needs the address of the server in order to communicate with it.
Provide here full address (or i) of the UDS server
Template: udsactor/secure
Type: boolean
Default: true
Description: Use secure (https) connection to communicate with UDS server?
If selected, the communication will be done using https.
If not selected, the communication will be done using http
Template: udsactor/masterKey
Type: string
Default:
Description: Master Key:
This key is available on UDS Administration interface.
Look for it under configuration, on Security tab.

View File

@@ -0,0 +1,11 @@
[Desktop Entry]
Name=UDSClient
Comment=UDS Helper
Keywords=uds;client;vdi;
Exec=/usr/lib/UDSClient/UDSClient.py %u
Icon=help-browser
StartupNotify=true
Terminal=false
Type=Application
Categories=Utility;
MimeType=x-scheme-handler/uds;x-scheme-handler/udss;

View File

@@ -0,0 +1,14 @@
#!/bin/sh
cp -r usr/lib/UDSClient /usr/lib/UDSClient
cp -r usr/share/applications /usr/lib/applications -R
update-desktop-database
echo "Installation process done."
echo "Remembar that the following packages must be installed on system:"
echo "* Python paramiko"
echo "* Python pyqt4"
echo "Theese packages (as their names), are dependent on your platform, so you must locate and install them"
echo "You can install them directly on any platform with pip, using this simple command: "
echo "pip install PyQt4 paramiko"

View File

@@ -0,0 +1,3 @@
UDSClient is the client connector needed to get acccess to services managed by UDS Broker.
Please, visit http://www.udsenterprise.com for more information

View File

@@ -0,0 +1,52 @@
%define _topdir %(echo $PWD)/rpm
%define name udsclient
%define version 0.0.0
%define release 1
%define buildroot %{_topdir}/%{name}-%{version}-%{release}-root
BuildRoot: %{buildroot}
Name: %{name}
Version: %{version}
Release: %{release}
Summary: Client for Universal Desktop Services (UDS) Broker
License: BSD3
Group: Applications/Productivity
Requires: python-six python-paramiko PyQt4
Vendor: Virtual Cable S.L.U.
URL: http://www.udsenterprise.com
Provides: udsclient
%define _rpmdir ../
%define _rpmfilename %%{NAME}-%%{VERSION}-%%{RELEASE}.%%{ARCH}.rpm
%install
curdir=`pwd`
cd ../..
make DESTDIR=$RPM_BUILD_ROOT DISTRO=rh install
cd $curdir
%post
/usr/bin/update-desktop-database
if [ ! -d /media ]; then mkdir /media; echo "/media created for compatibility"; fi
%clean
rm -rf $RPM_BUILD_ROOT
curdir=`pwd`
cd ../..
make DESTDIR=$RPM_BUILD_ROOT DISTRO=rh clean
cd $curdir
%postun
# And, posibly, the .pyc leaved behind on /usr/share/UDSActor
rm -rf /usr/share/UDClient > /dev/null 2>&1
/usr/bin/update-desktop-database
%description
This package provides the required components to allow connection to services offered by UDS Broker.
%files
%defattr(-,root,root)
/usr/lib/UDSClient/*
/usr/share/applications/UDSClient.desktop

4
client/full/src/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/build
/dist
UDSClient.dmg
UDSClient.pkg

338
client/full/src/UDSClient.py Executable file
View File

@@ -0,0 +1,338 @@
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2017 Virtual Cable S.L.
# 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
'''
# pylint: disable=c-extension-no-member
from __future__ import unicode_literals
import sys
import webbrowser
import json
from PyQt4 import QtCore, QtGui # @UnresolvedImport
import six
from uds.rest import RestRequest
from uds.forward import forward # pylint: disable=unused-import
from uds.log import logger
from uds import tools
from uds import VERSION
from UDSWindow import Ui_MainWindow
# Server before this version uses "unsigned" scripts
OLD_METHOD_VERSION = '2.4.0'
class RetryException(Exception):
pass
class UDSClient(QtGui.QMainWindow):
ticket = None
scrambler = None
withError = False
animTimer = None
anim = 0
animInverted = False
serverVersion = 'X.Y.Z' # Will be overwriten on getVersion
req = None
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.progressBar.setValue(0)
self.ui.cancelButton.clicked.connect(self.cancelPushed)
self.ui.info.setText('Initializing...')
screen = QtGui.QDesktopWidget().screenGeometry()
mysize = self.geometry()
hpos = (screen.width() - mysize.width()) / 2
vpos = (screen.height() - mysize.height() - mysize.height()) / 2
self.move(hpos, vpos)
self.animTimer = QtCore.QTimer()
QtCore.QObject.connect(self.animTimer, QtCore.SIGNAL('timeout()'), self.updateAnim)
self.activateWindow()
self.startAnim()
def closeWindow(self):
self.close()
def processError(self, data):
if 'error' in data:
# QtGui.QMessageBox.critical(self, 'Request error {}'.format(data.get('retryable', '0')), data['error'], QtGui.QMessageBox.Ok)
if data.get('retryable', '0') == '1':
raise RetryException(data['error'])
raise Exception(data['error'])
# QtGui.QMessageBox.critical(self, 'Request error', rest.data['error'], QtGui.QMessageBox.Ok)
# self.closeWindow()
# return
def showError(self, error):
logger.error('got error: %s', error)
self.stopAnim()
self.ui.info.setText('UDS Plugin Error') # In fact, main window is hidden, so this is not visible... :)
self.closeWindow()
QtGui.QMessageBox.critical(None, 'UDS Plugin Error', '{}'.format(error), QtGui.QMessageBox.Ok)
self.withError = True
def cancelPushed(self):
self.close()
@QtCore.pyqtSlot()
def updateAnim(self):
self.anim += 2
if self.anim > 99:
self.animInverted = not self.animInverted
self.ui.progressBar.setInvertedAppearance(self.animInverted)
self.anim = 0
self.ui.progressBar.setValue(self.anim)
def startAnim(self):
self.ui.progressBar.invertedAppearance = False
self.anim = 0
self.animInverted = False
self.ui.progressBar.setInvertedAppearance(self.animInverted)
self.animTimer.start(40)
def stopAnim(self):
self.ui.progressBar.invertedAppearance = False
self.animTimer.stop()
@QtCore.pyqtSlot()
def getVersion(self):
self.req = RestRequest('', self, self.version)
self.req.get()
@QtCore.pyqtSlot(dict)
def version(self, data):
try:
self.processError(data)
self.ui.info.setText('Processing...')
if data['result']['requiredVersion'] > VERSION:
QtGui.QMessageBox.critical(self, 'Upgrade required', 'A newer connector version is required.\nA browser will be opened to download it.', QtGui.QMessageBox.Ok)
webbrowser.open(data['result']['downloadUrl'])
self.closeWindow()
return
self.serverVersion = data['result']['requiredVersion']
self.getTransportData()
except RetryException as e:
self.ui.info.setText(six.text_type(e))
QtCore.QTimer.singleShot(1000, self.getVersion)
except Exception as e:
self.showError(e)
@QtCore.pyqtSlot()
def getTransportData(self):
try:
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
@QtCore.pyqtSlot(dict)
def transportDataReceived(self, data):
logger.debug('Transport data received')
try:
self.processError(data)
params = None
if self.serverVersion <= OLD_METHOD_VERSION:
script = data['result'].decode('base64').decode('bz2')
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'))
if tools.verifySignature(script, signature) is False:
logger.error('Signature is invalid')
raise Exception('Invalid UDS code signature. Please, report to administrator')
self.stopAnim()
if 'darwin' in sys.platform:
self.showMinimized()
QtCore.QTimer.singleShot(3000, self.endScript)
self.hide()
# if self.serverVersion <= OLD_METHOD_VERSION:
# errorString = '<p>The server <b>{}</b> runs an old version of UDS:</p>'.format(host)
# errorString += '<p>To avoid security issues, you must approve old UDS Version access.</p>'
#
# if QtGui.QMessageBox.warning(None, 'ACCESS Warning', errorString, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) == QtGui.QMessageBox.No:
# raise Exception('Server not approved. Access denied.')
six.exec_(script, globals(), {'parent': self, 'sp': params})
except RetryException as e:
self.ui.info.setText(six.text_type(e) + ', retrying access...')
# Retry operation in ten seconds
QtCore.QTimer.singleShot(10000, self.getTransportData)
except Exception as e:
#logger.exception('Got exception executing script:')
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):
'''
Starts proccess by requesting version info
'''
self.ui.info.setText('Initializing...')
QtCore.QTimer.singleShot(100, self.getVersion)
def done(data):
QtGui.QMessageBox.critical(None, 'Notice', six.text_type(data.data), QtGui.QMessageBox.Ok)
sys.exit(0)
# Ask user to approve endpoint
def approveHost(hostName, parentWindow=None):
settings = QtCore.QSettings()
settings.beginGroup('endpoints')
approved = settings.value(hostName, False).toBool()
errorString = '<p>The server <b>{}</b> must be approved:</p>'.format(hostName)
errorString += '<p>Only approve UDS servers that you trust to avoid security issues.</p>'
if approved or QtGui.QMessageBox.warning(parentWindow, 'ACCESS Warning', errorString, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) == QtGui.QMessageBox.Yes:
settings.setValue(hostName, True)
approved = True
settings.endGroup()
return approved
if __name__ == "__main__":
logger.debug('Initializing connector')
# Initialize app
app = QtGui.QApplication(sys.argv)
# Set several info for settings
QtCore.QCoreApplication.setOrganizationName('Virtual Cable S.L.U.')
QtCore.QCoreApplication.setApplicationName('UDS Connector')
if 'darwin' not in sys.platform:
logger.debug('Mac OS *NOT* Detected')
app.setStyle('plastique')
if six.PY3 is False:
logger.debug('Fixing threaded execution of commands')
import threading
threading._DummyThread._Thread__stop = lambda x: 42 # type: ignore, pylint: disable=protected-access
# First parameter must be url
try:
uri = sys.argv[1]
if uri == '--test':
sys.exit(0)
logger.debug('URI: %s', uri)
if uri[:6] != 'uds://' and uri[:7] != 'udss://':
raise Exception()
ssl = uri[3] == 's'
host, UDSClient.ticket, UDSClient.scrambler = uri.split('//')[1].split('/') # type: ignore
logger.debug('ssl:%s, host:%s, ticket:%s, scrambler:%s', ssl, host, UDSClient.ticket, UDSClient.scrambler)
except Exception:
logger.debug('Detected execution without valid URI, exiting')
QtGui.QMessageBox.critical(None, 'Notice', 'UDS Client Version {}'.format(VERSION), QtGui.QMessageBox.Ok)
sys.exit(1)
# Setup REST api endpoint
RestRequest.restApiUrl = '{}://{}/rest/client'.format(['http', 'https'][ssl], host)
logger.debug('Setting request URL to %s', RestRequest.restApiUrl)
# RestRequest.restApiUrl = 'https://172.27.0.1/rest/client'
try:
logger.debug('Starting execution')
# Approbe before going on
if approveHost(host) is False:
raise Exception('Host {} was not approved'.format(host))
win = UDSClient()
win.show()
win.start()
exitVal = app.exec_()
logger.debug('Execution finished correctly')
except Exception as e:
logger.exception('Got an exception executing client:')
exitVal = 128
QtGui.QMessageBox.critical(None, 'Error', six.text_type(e), QtGui.QMessageBox.Ok)
logger.debug('Exiting')
sys.exit(exitVal)

View File

@@ -0,0 +1,6 @@
<RCC>
<qresource prefix="images">
<file alias="logo-uds-small">images/logo-uds-small.png</file>
<file alias="logo-uds-big">images/logo-uds.png</file>
</qresource>
</RCC>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'UDSWindow.ui'
#
# Created: Mon Apr 27 21:41:43 2015
# by: PyQt4 UI code generator 4.11.2
#
# WARNING! All changes made in this file will be lost!
from PyQt4 import QtCore, QtGui
try:
_fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
def _fromUtf8(s):
return s
try:
_encoding = QtGui.QApplication.UnicodeUTF8
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig)
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName(_fromUtf8("MainWindow"))
MainWindow.setWindowModality(QtCore.Qt.NonModal)
MainWindow.resize(259, 185)
MainWindow.setCursor(QtGui.QCursor(QtCore.Qt.BusyCursor))
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(_fromUtf8(":/images/logo-uds-small")), QtGui.QIcon.Normal, QtGui.QIcon.Off)
MainWindow.setWindowIcon(icon)
MainWindow.setWindowOpacity(1.0)
self.centralwidget = QtGui.QWidget(MainWindow)
self.centralwidget.setAutoFillBackground(True)
self.centralwidget.setObjectName(_fromUtf8("centralwidget"))
self.verticalLayout_2 = QtGui.QVBoxLayout(self.centralwidget)
self.verticalLayout_2.setSpacing(4)
self.verticalLayout_2.setMargin(4)
self.verticalLayout_2.setObjectName(_fromUtf8("verticalLayout_2"))
self.frame = QtGui.QFrame(self.centralwidget)
self.frame.setFrameShape(QtGui.QFrame.StyledPanel)
self.frame.setFrameShadow(QtGui.QFrame.Raised)
self.frame.setObjectName(_fromUtf8("frame"))
self.verticalLayout_3 = QtGui.QVBoxLayout(self.frame)
self.verticalLayout_3.setSpacing(4)
self.verticalLayout_3.setMargin(4)
self.verticalLayout_3.setObjectName(_fromUtf8("verticalLayout_3"))
self.verticalLayout = QtGui.QVBoxLayout()
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
self.image = QtGui.QLabel(self.frame)
self.image.setMinimumSize(QtCore.QSize(0, 24))
self.image.setAutoFillBackground(True)
self.image.setText(_fromUtf8(""))
self.image.setPixmap(QtGui.QPixmap(_fromUtf8(":/images/logo-uds-small")))
self.image.setScaledContents(False)
self.image.setAlignment(QtCore.Qt.AlignCenter)
self.image.setObjectName(_fromUtf8("image"))
self.verticalLayout.addWidget(self.image)
self.info = QtGui.QLabel(self.frame)
self.info.setMaximumSize(QtCore.QSize(16777215, 16))
self.info.setObjectName(_fromUtf8("info"))
self.verticalLayout.addWidget(self.info)
self.progressBar = QtGui.QProgressBar(self.frame)
self.progressBar.setProperty("value", 24)
self.progressBar.setTextVisible(False)
self.progressBar.setObjectName(_fromUtf8("progressBar"))
self.verticalLayout.addWidget(self.progressBar)
self.horizontalLayout = QtGui.QHBoxLayout()
self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.cancelButton = QtGui.QPushButton(self.frame)
self.cancelButton.setDefault(True)
self.cancelButton.setFlat(False)
self.cancelButton.setObjectName(_fromUtf8("cancelButton"))
self.horizontalLayout.addWidget(self.cancelButton)
spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem1)
self.verticalLayout.addLayout(self.horizontalLayout)
self.verticalLayout_3.addLayout(self.verticalLayout)
self.verticalLayout_2.addWidget(self.frame)
MainWindow.setCentralWidget(self.centralwidget)
self.retranslateUi(MainWindow)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
MainWindow.setWindowTitle(_translate("MainWindow", "UDS Connection", None))
self.info.setText(_translate("MainWindow", "TextLabel", None))
self.cancelButton.setText(_translate("MainWindow", "Cancel", None))
import UDSResources_rc
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
MainWindow = QtGui.QMainWindow()
ui = Ui_MainWindow()
ui.setupUi(MainWindow)
MainWindow.show()
sys.exit(app.exec_())

View File

@@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="windowModality">
<enum>Qt::NonModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>259</width>
<height>185</height>
</rect>
</property>
<property name="cursor">
<cursorShape>BusyCursor</cursorShape>
</property>
<property name="windowTitle">
<string>UDS Connection</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="margin">
<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_3">
<property name="spacing">
<number>4</number>
</property>
<property name="margin">
<number>4</number>
</property>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="image">
<property name="minimumSize">
<size>
<width>0</width>
<height>24</height>
</size>
</property>
<property name="autoFillBackground">
<bool>true</bool>
</property>
<property name="text">
<string/>
</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="info">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16</height>
</size>
</property>
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
<property name="default">
<bool>true</bool>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</widget>
<resources>
<include location="UDSResources.qrc"/>
</resources>
<connections/>
</ui>

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 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,
@@ -29,10 +29,13 @@
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
from django.conf.urls import url from __future__ import unicode_literals
from . import views VERSION = '3.0.0'
urlpatterns = [ __title__ = 'udclient'
url(r'^uds/ognotify/(?P<msg>[a-z]+)/(?P<token>[a-zA-Z0-9-_]+)/(?P<uuid>[a-zA-Z0-9-_]+)$', views.opengnsys, name='dispatcher.opengnsys'), __version__ = VERSION
] __build__ = 0x010760
__author__ = 'Adolfo Gómez'
__license__ = "BSD 3-clause"
__copyright__ = "Copyright 2014-2017 VirtualCable S.L.U."

View File

@@ -0,0 +1,211 @@
# Based on forward.py from paramiko
# Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
# https://github.com/paramiko/paramiko/blob/master/demos/forward.py
from __future__ import unicode_literals
from binascii import hexlify
import threading
import random
import time
import select
import paramiko
import six
from .log import logger
if six.PY2:
import SocketServer as socketserver # pylint: disable=import-error
else:
import socketserver
class CheckfingerPrints(paramiko.MissingHostKeyPolicy):
def __init__(self, fingerPrints):
super(CheckfingerPrints, self).__init__()
self.fingerPrints = fingerPrints
def missing_host_key(self, client, hostname, key):
if self.fingerPrints:
remotefingerPrints = hexlify(key.get_fingerprint()).decode().lower()
logger.debug('Checking keys {} against {}'.format(remotefingerPrints, self.fingerPrints))
if remotefingerPrints not in self.fingerPrints.split(','):
logger.error("Server {!r} has invalid fingerPrints. ({} vs {})".format(hostname, remotefingerPrints, self.fingerPrints))
raise paramiko.SSHException(
"Server {!r} has invalid fingerPrints".format(hostname)
)
class ForwardServer(socketserver.ThreadingTCPServer):
daemon_threads = True
allow_reuse_address = True
class Handler(socketserver.BaseRequestHandler):
def handle(self):
self.thread.currentConnections += 1
try:
chan = self.ssh_transport.open_channel('direct-tcpip',
(self.chain_host, self.chain_port),
self.request.getpeername())
except Exception as e:
logger.exception('Incoming request to %s:%d failed: %s', self.chain_host, self.chain_port, repr(e))
return
if chan is None:
logger.error('Incoming request to %s:%d was rejected by the SSH server.', self.chain_host, self.chain_port)
return
logger.debug('Connected! Tunnel open %r -> %r -> %r', self.request.getpeername(), chan.getpeername(), (self.chain_host, self.chain_port))
try:
while self.event.is_set() is False:
r, _w, _x = select.select([self.request, chan], [], [], 1) # pylint: disable=unused-variable
if self.request in r:
data = self.request.recv(1024)
if not data:
break
chan.send(data)
if chan in r:
data = chan.recv(1024)
if not data:
break
self.request.send(data)
except Exception:
pass
try:
peername = self.request.getpeername()
chan.close()
self.request.close()
logger.debug('Tunnel closed from %r', peername,)
except Exception:
pass
self.thread.currentConnections -= 1
if self.thread.stoppable is True and self.thread.currentConnections == 0:
self.thread.stop()
class ForwardThread(threading.Thread):
status = 0 # Connecting
def __init__(self, server, port, username, password, localPort, redirectHost, redirectPort, waitTime, fingerPrints):
threading.Thread.__init__(self)
self.client = None
self.fs = None
self.server = server
self.port = int(port)
self.username = username
self.password = password
self.localPort = int(localPort)
self.redirectHost = redirectHost
self.redirectPort = redirectPort
self.waitTime = waitTime
self.fingerPrints = fingerPrints
self.stopEvent = threading.Event()
self.timer = None
self.currentConnections = 0
self.stoppable = False
self.client = None
def clone(self, redirectHost, redirectPort, localPort=None):
if localPort is None:
localPort = random.randrange(33000, 52000)
ft = ForwardThread(self.server, self.port, self.username, self.password, localPort, redirectHost, redirectPort, self.waitTime, self.fingerPrints)
ft.client = self.client
self.client.useCount += 1 # One more using this client
ft.start()
while ft.status == 0:
time.sleep(0.1)
return (ft, localPort)
def _timerFnc(self):
self.timer = None
logger.debug('Timer fnc: %s', self.currentConnections)
self.stoppable = True
if self.currentConnections <= 0:
self.stop()
def run(self):
if self.client is None:
try:
self.client = paramiko.SSHClient()
self.client.useCount = 1 # Custom added variable, to keep track on when to close tunnel
# self.client.load_system_host_keys()
self.client.set_missing_host_key_policy(CheckfingerPrints(self.fingerPrints))
logger.debug('Connecting to ssh host %s:%d ...', self.server, self.port)
self.client.connect(self.server, self.port, username=self.username, password=self.password, timeout=5)
except Exception:
logger.exception('Exception connecting: ')
self.status = 2 # Error
return
class SubHandler(Handler):
chain_host = self.redirectHost
chain_port = self.redirectPort
ssh_transport = self.client.get_transport()
event = self.stopEvent
thread = self
logger.debug('Wait Time: %s', self.waitTime)
self.timer = threading.Timer(self.waitTime, self._timerFnc)
self.timer.start()
self.status = 1 # Ok, listening
self.fs = ForwardServer(('', self.localPort), SubHandler)
self.fs.serve_forever()
def stop(self):
try:
if self.timer:
self.timer.cancel()
self.stopEvent.set()
self.fs.shutdown()
if self.client is not None:
self.client.useCount -= 1
if self.client.useCount == 0:
self.client.close()
self.client = None # Clean up
except Exception:
logger.exception('Exception stopping')
def forward(server, port, username, password, redirectHost, redirectPort, localPort=None, waitTime=10, fingerPrints=None):
'''
Instantiates an ssh connection to server:port
Returns the Thread created and the local redirected port as a list: (thread, port)
'''
port, redirectPort = int(port), int(redirectPort)
if localPort is None:
localPort = random.randrange(33000, 52000)
logger.debug('Connecting to %s:%s using %s/%s redirecting to %s:%s, listening on 127.0.0.1:%s',
server, port, username, password, redirectHost, redirectPort, localPort)
ft = ForwardThread(server, port, username, password, localPort, redirectHost, redirectPort, waitTime, fingerPrints)
ft.start()
while ft.status == 0:
time.sleep(0.1)
return (ft, localPort)

View File

@@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014 Virtual Cable S.L.
# 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
'''
from __future__ import unicode_literals
import logging
import os
import sys
import tempfile
if sys.platform.startswith('linux'):
from os.path import expanduser # pylint: disable=ungrouped-imports
logFile = expanduser('~/udsclient.log')
else:
logFile = os.path.join(tempfile.gettempdir(), b'udsclient.log')
try:
logging.basicConfig(
filename=logFile,
filemode='a',
format='%(levelname)s %(asctime)s %(message)s',
level=logging.INFO
)
except Exception:
logging.basicConfig(format='%(levelname)s %(asctime)s %(message)s', level=logging.INFO)
logger = logging.getLogger('udsclient')

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 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,
@@ -25,21 +26,23 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # 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 # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
import enum from __future__ import unicode_literals
import socket
import typing
class Command(enum.IntEnum): import sys
TUNNEL = 0
STATS = 1
class Message: LINUX = 'Linux'
command: Command WINDOWS = 'Windows'
connection: typing.Optional[typing.Tuple[socket.socket, typing.Any]] MAC_OS_X = 'Mac os x'
def __init__(self, command: Command, connection: typing.Optional[typing.Tuple[socket.socket, typing.Any]]): def getOs():
self.command = command if sys.platform.startswith('linux'):
self.connection = connection return LINUX
if sys.platform.startswith('win'):
return WINDOWS
if sys.platform.startswith('darwin'):
return MAC_OS_X
return 'other'

123
client/full/src/uds/rest.py Normal file
View File

@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017 Virtual Cable S.L.
# 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
'''
# pylint: disable=c-extension-no-member,no-name-in-module
from __future__ import unicode_literals
import json
import urllib
import six
from PyQt4.QtCore import pyqtSignal, pyqtSlot
from PyQt4.QtCore import QObject, QUrl, QSettings
from PyQt4.QtCore import Qt
from PyQt4.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply, QSslCertificate
from PyQt4.QtGui import QMessageBox
from . import osDetector
from . import VERSION
class RestRequest(QObject):
restApiUrl = '' #
done = pyqtSignal(dict, name='done')
def __init__(self, url, parentWindow, done, params=None): # parent not used
super(RestRequest, self).__init__()
# private
self._manager = QNetworkAccessManager()
if params is not None:
url += '?' + '&'.join('{}={}'.format(k, urllib.quote(six.text_type(v).encode('utf8'))) for k, v in params.iteritems())
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)
# private slot, no need to declare as slot
@pyqtSlot(QNetworkReply)
def _finished(self, reply):
'''
Handle signal 'finished'. A network request has finished.
'''
try:
if reply.error() != QNetworkReply.NoError:
raise Exception(reply.errorString())
data = six.text_type(reply.readAll())
data = json.loads(data)
except Exception as e:
data = {
'result': None,
'error': six.text_type(e)
}
self.done.emit(data)
reply.deleteLater() # schedule for delete from main event loop
@pyqtSlot(QNetworkReply, list)
def _sslError(self, reply, errors):
# reply.ignoreSslErrors()
settings = QSettings()
settings.beginGroup('ssl')
cert = errors[0].certificate()
digest = six.text_type(cert.digest().toHex())
approved = settings.value(digest, False).toBool()
errorString = '<p>Please, accept the certificate for <b>{}</b></p>'.format(cert.subjectInfo(QSslCertificate.CommonName))
# for err in errors:
# errorString += '<li>' + err.errorString() + '</li>'
# errorString += '</ul>'
if approved or QMessageBox.warning(self._parentWindow, 'SSL Warning', errorString, QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
settings.setValue(digest, True)
reply.ignoreSslErrors()
settings.endGroup()
def get(self):
request = QNetworkRequest(self.url)
request.setRawHeader('User-Agent', osDetector.getOs() + " - UDS Connector " + VERSION)
self._manager.get(request)

View File

@@ -0,0 +1,205 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2015 Virtual Cable S.L.
# 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
'''
from __future__ import unicode_literals
from base64 import b64decode
import tempfile
import string
import random
import os
import socket
import stat
import sys
import time
import six
from .log import logger
_unlinkFiles = []
_tasksToWait = []
_execBeforeExit = []
sys_fs_enc = sys.getfilesystemencoding() or 'mbcs'
# Public key for scripts
PUBLIC_KEY = '''-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuNURlGjBpqbglkTTg2lh
dU5qPbg9Q+RofoDDucGfrbY0pjB9ULgWXUetUWDZhFG241tNeKw+aYFTEorK5P+g
ud7h9KfyJ6huhzln9eyDu3k+kjKUIB1PLtA3lZLZnBx7nmrHRody1u5lRaLVplsb
FmcnptwYD+3jtJ2eK9ih935DYAkYS4vJFi2FO+npUQdYBZHPG/KwXLjP4oGOuZp0
pCTLiCXWGjqh2GWsTECby2upGS/ZNZ1r4Ymp4V2A6DZnN0C0xenHIY34FWYahbXF
ZGdr4DFBPdYde5Rb5aVKJQc/pWK0CV7LK6Krx0/PFc7OGg7ItdEuC7GSfPNV/ANt
5BEQNF5w2nUUsyN8ziOrNih+z6fWQujAAUZfpCCeV9ekbwXGhbRtdNkbAryE5vH6
eCE0iZ+cFsk72VScwLRiOhGNelMQ7mIMotNck3a0P15eaGJVE2JV0M/ag/Cnk0Lp
wI1uJQRAVqz9ZAwvF2SxM45vnrBn6TqqxbKnHCeiwstLDYG4fIhBwFxP3iMH9EqV
2+QXqdJW/wLenFjmXfxrjTRr+z9aYMIdtIkSpADIlbaJyTtuQpEdWnrlDS2b1IGd
Okbm65EebVzOxfje+8dRq9Uqwip8f/qmzFsIIsx3wPSvkKawFwb0G5h2HX5oJrk0
nVgtClKcDDlSaBsO875WDR0CAwEAAQ==
-----END PUBLIC KEY-----'''
def saveTempFile(content, filename=None):
if filename is None:
filename = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(16))
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)
with open(filename, 'w') as f:
f.write(content)
logger.info('Returning filename')
return filename
def readTempFile(filename):
if 'win32' in sys.platform:
filename = filename.encode('utf-8')
filename = os.path.join(tempfile.gettempdir(), filename)
try:
with open(filename, 'r') as f:
return f.read()
except Exception:
return None
def testServer(host, port, timeOut=4):
try:
sock = socket.create_connection((host, int(port)), timeOut)
sock.close()
except Exception:
return False
return True
def findApp(appName, extraPath=None):
if 'win32' in sys.platform and isinstance(appName, six.text_type):
appName = appName.encode(sys_fs_enc)
searchPath = os.environ['PATH'].split(os.pathsep)
if extraPath is not None:
searchPath += list(extraPath)
for path in searchPath:
fileName = os.path.join(path, appName)
if os.path.isfile(fileName) and (os.stat(fileName).st_mode & stat.S_IXUSR) != 0:
return fileName
return None
def getHostName():
'''
Returns current host name
In fact, it's a wrapper for 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)
return hostname
# Queing operations (to be executed before exit)
def addFileToUnlink(filename):
'''
Adds a file to the wait-and-unlink list
'''
_unlinkFiles.append(filename)
def unlinkFiles():
'''
Removes all wait-and-unlink files
'''
if _unlinkFiles:
time.sleep(5) # Wait 5 seconds before deleting anything
for f in _unlinkFiles:
try:
os.unlink(f)
except Exception:
pass
def addTaskToWait(taks):
_tasksToWait.append(taks)
def waitForTasks():
for t in _tasksToWait:
try:
if hasattr(t, 'join'):
t.join()
elif hasattr(t, 'wait'):
t.wait()
except Exception:
pass
def addExecBeforeExit(fnc):
_execBeforeExit.append(fnc)
def execBeforeExit():
for fnc in _execBeforeExit:
fnc.__call__()
def verifySignature(script, signature):
'''
Verifies with a public key from whom the data came that it was indeed
signed by their private key
param: public_key_loc Path to public key
param: signature String signature to be verified
return: Boolean. True if the signature is valid; False otherwise.
'''
# For signature checking
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
rsakey = RSA.importKey(PUBLIC_KEY)
signer = PKCS1_v1_5.new(rsakey)
digest = SHA256.new(script) # Script is "binary string" here
if signer.verify(digest, b64decode(signature)):
return True
return False

15
client/full/src/update.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash
function process {
for a in *.ui; do
pyuic4 $a -o `basename $a .ui`.py -x
done
}
pyrcc4 -py3 UDSResources.qrc -o UDSResources_rc.py
# process current directory ui's
process

17
client/thin/.project Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>UDSClient-Thin</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

14
client/thin/.pydevproject Normal file
View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/${PROJECT_DIR_NAME}/src</path>
</pydev_pathproperty>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">system-2.7</pydev_property>
</pydev_project>

View File

@@ -0,0 +1,174 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017 Virtual Cable S.L.
# 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
'''
from __future__ import unicode_literals
import sys
import json
import six
from uds import ui
from uds.rest import RestRequest, RetryException
from uds.forward import forward # pylint: disable=unused-import
from uds import VERSION
from uds.log import logger # @UnresolvedImport
from uds import tools
# Server before this version uses "unsigned" scripts
OLD_METHOD_VERSION = '2.4.0'
def approveHost(hostName):
from os.path import expanduser
hostsFile = expanduser('~/.udsclient.hosts')
try:
with open(hostsFile, 'r') as f:
approvedHosts = f.read().splitlines()
except Exception:
approvedHosts = []
hostName = hostName.lower()
if hostName in approvedHosts:
return True
errorString = 'The server {} must be approved:\n'.format(hostName)
errorString += 'Only approve UDS servers that you trust to avoid security issues.'
approved = ui.question("ACCESS Warning", errorString)
if approved:
approvedHosts.append(hostName)
logger.debug('Host was approved, saving to approvedHosts file')
try:
with open(hostsFile, 'w') as f:
f.write('\n'.join(approvedHosts))
except Exception:
logger.warning('Got exception writing to %s', hostsFile)
return approved
def getWithRetry(api, url, parameters=None):
while True:
try:
return api.get(url, parameters)
except RetryException as e:
if ui.question('Service not available', 'Error {}.\nPlease, wait a minute and press "OK" to retry, or "CANCEL" to abort'.format(e)) is True:
continue
raise Exception('Cancelled by user')
if __name__ == "__main__":
logger.debug('Initializing connector')
if six.PY3 is False:
logger.debug('Fixing threaded execution of commands')
import threading
threading._DummyThread._Thread__stop = lambda x: 42 # type: ignore, pylint:disable=protected-access
# First parameter must be url
try:
uri = sys.argv[1]
if uri == '--test':
sys.exit(0)
logger.debug('URI: %s', uri)
if uri[:6] != 'uds://' and uri[:7] != 'udss://':
raise Exception()
ssl = uri[3] == 's'
host, ticket, scrambler = uri.split('//')[1].split('/')
logger.debug('ssl: %s, host:%s, ticket:%s, scrambler:%s', ssl, host, ticket, scrambler)
except Exception:
logger.debug('Detected execution without valid URI, exiting')
ui.message('UDS Client', 'UDS Client Version {}'.format(VERSION))
sys.exit(1)
rest = RestRequest(host, ssl)
logger.debug('Setting request URL to %s', rest.restApiUrl)
# Main requests part
# First, get version
try:
res = getWithRetry(rest, '')
logger.debug('Got information %s', res)
requiredVersion = res['requiredVersion']
if requiredVersion > VERSION:
ui.message("New UDS Client available", "A new uds version is needed in order to access this version of UDS.\nPlease, download and install it")
sys.exit(1)
res = getWithRetry(rest, '/{}/{}'.format(ticket, scrambler), parameters={'hostname': tools.getHostName(), 'version': VERSION})
params = None
if requiredVersion <= OLD_METHOD_VERSION:
script = res.decode('base64').decode('bz2')
else:
# 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'))
if tools.verifySignature(script, signature) is False:
logger.error('Signature is invalid')
raise Exception('Invalid UDS code signature. Please, report to administrator')
logger.debug('Script: %s', script)
six.exec_(script, globals(), {'parent': None, 'sp': params})
except Exception as e:
error = 'ERROR: {}'.format(e)
logger.error(error)
ui.message('Error', error)
sys.exit(2)
# Finalize
try:
tools.waitForTasks()
except Exception:
pass
try:
tools.unlinkFiles()
except Exception:
pass
try:
tools.execBeforeExit()
except Exception:
pass

View File

@@ -0,0 +1 @@
../../../full/src/uds/__init__.py

View File

@@ -0,0 +1 @@
../../../full/src/uds/forward.py

1
client/thin/src/uds/log.py Symbolic link
View File

@@ -0,0 +1 @@
../../../full/src/uds/log.py

View File

@@ -0,0 +1 @@
../../../full/src/uds/osDetector.py

View File

@@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2015 Virtual Cable S.L.
# 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
'''
from __future__ import unicode_literals
import requests
from . import VERSION
import json
import six
import osDetector
from .log import logger
class RetryException(Exception):
pass
class RestRequest(object):
restApiUrl = ''
def __init__(self, host, ssl=True): # parent not used
super(RestRequest, self).__init__()
self.host = host
self.ssl = ssl
self.restApiUrl = '{}://{}/rest/client'.format(['http', 'https'][ssl], host)
def get(self, url, params=None):
url = self.restApiUrl + url
if params is not None:
url += '?' + '&'.join('{}={}'.format(k, six.moves.urllib.parse.quote(six.text_type(v).encode('utf8'))) for k, v in params.iteritems()) # @UndefinedVariable
logger.debug('Requesting %s', url)
try:
r = requests.get(url, headers={'Content-type': 'application/json', 'User-Agent': osDetector.getOs() + " - UDS Connector " + VERSION}, verify=False)
except requests.exceptions.ConnectionError:
raise Exception('Error connecting to UDS Server at {}'.format(self.restApiUrl[0:-11]))
if r.ok:
logger.debug('Request was OK. %s', r.text)
data = json.loads(r.text)
if not 'error' in data:
return data['result']
# Has error
if data.get('retryable', '0') == '1':
raise RetryException(data['error'])
raise Exception(data['error'])
logger.error('Error requesting %s: %s, %s', url, r.code, r.text)
raise Exception('Error {}: {}'.format(r.code, r.text))

View File

@@ -0,0 +1 @@
../../../full/src/uds/tools.py

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 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,
@@ -25,7 +26,19 @@
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # 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 # 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. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
''' '''
@author: Adolfo Gómez, dkmaster at dkmon dot com @author: Adolfo Gómez, dkmaster at dkmon dot com
''' '''
from __future__ import unicode_literals
try:
import gtkui as theUI
except Exception:
import consoleui as theUI # @Reimport
def message(title, message):
theUI.message(title, message)
def question(title, message):
return theUI.question(title, message)

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017 Virtual Cable S.L.
# 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
'''
from __future__ import unicode_literals
import random
import os
import tempfile
import string
import webbrowser
TEMPLATE = '''<html>
<head>
<title>{title}</title>
</head>
<body>
<h1>{title}</h1>
<p>{message}<P>
</body>
</html>
'''
def _htmlFilename():
return os.path.join(tempfile.gettempdir(), ''.join([random.choice(string.ascii_lowercase) for i in range(22)]) + '.html')
def message(title, message):
filename = _htmlFilename()
with open(filename, 'w') as f:
f.write(TEMPLATE.format(title=title, message=message))
webbrowser.open('file://' + filename, new=0, autoraise=False)

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