Compare commits
6 Commits
nonMultiCl
...
v3.0t
Author | SHA1 | Date | |
---|---|---|---|
|
48557f96e4 | ||
|
263071750c | ||
|
4fed22d39d | ||
|
24687fda2e | ||
|
51b0cec536 | ||
|
d38347c534 |
3
.gitignore
vendored
@@ -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_*
|
||||||
|
|
||||||
|
@@ -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.**
|
||||||
|
@@ -1,2 +0,0 @@
|
|||||||
PYTHONPATH=./src:${PYTHONPATH}
|
|
||||||
|
|
@@ -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
|
||||||
|
@@ -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.
|
||||||
|
@@ -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
|
||||||
|
@@ -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 $@
|
||||||
|
@@ -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 $@
|
||||||
|
@@ -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 $@
|
||||||
|
@@ -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_()
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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')
|
||||||
|
|
||||||
|
@@ -100,6 +100,3 @@ def writeConfig(config: types.ActorConfigurationType) -> None:
|
|||||||
|
|
||||||
def useOldJoinSystem() -> bool:
|
def useOldJoinSystem() -> bool:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def invokeScriptOnLogin() -> str:
|
|
||||||
return ''
|
|
||||||
|
@@ -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', ....
|
||||||
|
@@ -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)
|
||||||
|
|
||||||
|
@@ -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():
|
||||||
|
@@ -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
|
|
||||||
|
@@ -1,2 +0,0 @@
|
|||||||
PYTHONPATH=./src:${PYTHONPATH}
|
|
||||||
|
|
4
client-py3/full/linux/.gitignore
vendored
@@ -2,7 +2,3 @@
|
|||||||
/udsclient-[0-9]*.spec
|
/udsclient-[0-9]*.spec
|
||||||
/debian/udsclient
|
/debian/udsclient
|
||||||
/targz
|
/targz
|
||||||
/UDSClientDir
|
|
||||||
/UDSClient*.AppImage
|
|
||||||
/appimage*
|
|
||||||
/UDSClient.desktop
|
|
||||||
|
@@ -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
|
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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.
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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."
|
|
@@ -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
|
|
@@ -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
|
||||||
|
6
client-py3/full/src/.gitignore
vendored
@@ -1,6 +1,4 @@
|
|||||||
/build
|
/build
|
||||||
/dist
|
/dist
|
||||||
/UDSClient*.pkg
|
UDSClient.dmg
|
||||||
/UDSClient*.dist
|
UDSClient.pkg
|
||||||
/UDSClient*.build
|
|
||||||
/.eggs
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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:
|
||||||
|
@@ -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')
|
||||||
|
@@ -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,
|
@@ -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
|
|
||||||
|
@@ -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
|
|
||||||
|
@@ -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
@@ -0,0 +1,4 @@
|
|||||||
|
/bin
|
||||||
|
/udsclient_*
|
||||||
|
/udsclient-*.tar.gz
|
||||||
|
/*.rpm
|
17
client/full/.project
Normal 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
@@ -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
@@ -0,0 +1,4 @@
|
|||||||
|
/udsclient-opensuse-[0-9]*.spec
|
||||||
|
/udsclient-[0-9]*.spec
|
||||||
|
/debian/udsclient
|
||||||
|
/targz
|
51
client/full/linux/Makefile
Normal 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)
|
36
client/full/linux/build-packages.sh
Executable 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
@@ -0,0 +1,3 @@
|
|||||||
|
/udsactor/
|
||||||
|
/udsactor-xrdp/
|
||||||
|
/udsactor-nx/
|
47
client/full/linux/debian/changelog
Normal 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
|
1
client/full/linux/debian/compat
Normal file
@@ -0,0 +1 @@
|
|||||||
|
9
|
15
client/full/linux/debian/control
Normal 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.
|
26
client/full/linux/debian/copyright
Normal 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'.
|
1
client/full/linux/debian/docs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
readme.txt
|
2
client/full/linux/debian/files
Normal 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
@@ -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
|
17
client/full/linux/debian/udsclient.log
Normal 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
|
21
client/full/linux/debian/udsclient.postinst
Executable 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
|
6
client/full/linux/debian/udsclient.postrm
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh -e
|
||||||
|
|
||||||
|
. /usr/share/debconf/confmodule
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
1
client/full/linux/debian/udsclient.prerm
Executable file
@@ -0,0 +1 @@
|
|||||||
|
#! /bin/bash -e
|
20
client/full/linux/debian/udsclient.templates
Normal 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.
|
11
client/full/linux/desktop/UDSClient.desktop
Normal 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;
|
14
client/full/linux/installer.sh
Normal 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"
|
||||||
|
|
3
client/full/linux/readme.txt
Normal 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
|
52
client/full/linux/udsclient-template.spec
Normal 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
@@ -0,0 +1,4 @@
|
|||||||
|
/build
|
||||||
|
/dist
|
||||||
|
UDSClient.dmg
|
||||||
|
UDSClient.pkg
|
338
client/full/src/UDSClient.py
Executable 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)
|
6
client/full/src/UDSResources.qrc
Normal 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>
|
2471
client/full/src/UDSResources_rc.py
Normal file
105
client/full/src/UDSWindow.py
Normal 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_())
|
||||||
|
|
160
client/full/src/UDSWindow.ui
Normal 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>
|
BIN
client/full/src/images/logo-512.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
client/full/src/images/logo-uds-small.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
client/full/src/images/logo-uds.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
client/full/src/images/uds.ico
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
client/full/src/macosx/uds.icns
Normal file
BIN
client/full/src/macosx/uds.iconset/icon_128x128.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
client/full/src/macosx/uds.iconset/icon_128x128@2x.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
client/full/src/macosx/uds.iconset/icon_16x16.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
client/full/src/macosx/uds.iconset/icon_16x16@2x.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
client/full/src/macosx/uds.iconset/icon_256x256.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
client/full/src/macosx/uds.iconset/icon_256x256@2x.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
client/full/src/macosx/uds.iconset/icon_32x32.png
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
client/full/src/macosx/uds.iconset/icon_32x32@2x.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
client/full/src/macosx/uds.iconset/icon_512x512.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
client/full/src/macosx/uds.iconset/icon_512x512@2x.png
Normal file
After Width: | Height: | Size: 137 KiB |
@@ -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."
|
211
client/full/src/uds/forward.py
Normal 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)
|
55
client/full/src/uds/log.py
Normal 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')
|
@@ -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
@@ -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)
|
205
client/full/src/uds/tools.py
Normal 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
@@ -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
@@ -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
@@ -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>
|
174
client/thin/src/UDSClient.py
Normal 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
|
1
client/thin/src/uds/__init__.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../full/src/uds/__init__.py
|
1
client/thin/src/uds/forward.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../full/src/uds/forward.py
|
1
client/thin/src/uds/log.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../full/src/uds/log.py
|
1
client/thin/src/uds/osDetector.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../full/src/uds/osDetector.py
|
84
client/thin/src/uds/rest.py
Normal 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))
|
1
client/thin/src/uds/tools.py
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../../../full/src/uds/tools.py
|
@@ -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)
|
||||||
|
|
58
client/thin/src/uds/ui/browser.py
Normal 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)
|