From 2509f41c85225dac43bb165eaa49ce3c3dff916c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20G=C3=B3mez=20Garc=C3=ADa?= Date: Wed, 11 Dec 2019 13:05:10 +0100 Subject: [PATCH] advancing on actor --- actor/src/udsactor/client.py | 21 ++- actor/src/udsactor/http/client.py | 10 +- actor/src/udsactor/http/server.py | 1 + actor/src/udsactor/linux/daemon.py | 9 +- actor/src/udsactor/log.py | 2 +- actor/src/udsactor/rest.py | 2 +- actor/src/udsactor/service.py | 22 ++- server/src/uds/core/managers/user_service.py | 3 - .../uds/core/managers/userservice/comms.py | 171 ++++++++---------- 9 files changed, 128 insertions(+), 113 deletions(-) diff --git a/actor/src/udsactor/client.py b/actor/src/udsactor/client.py index 1a331062..78606f99 100644 --- a/actor/src/udsactor/client.py +++ b/actor/src/udsactor/client.py @@ -80,13 +80,17 @@ class UDSActorClient(threading.Thread): self._running = True time.sleep(0.4) # Wait a bit before sending login - # Notify loging and mark it - self.api.login(platform.operations.getCurrentUser()) - while self._running: - time.sleep(1.1) # Sleeps between loop iterations + try: + # Notify loging and mark it + self.api.login(platform.operations.getCurrentUser()) - self.api.logout(platform.operations.getCurrentUser()) + while self._running: + time.sleep(1.1) # Sleeps between loop iterations + + self.api.logout(platform.operations.getCurrentUser()) + except Exception as e: + logger.error('Error on client loop: %s', e) self._listener.stop() # async listener for service @@ -117,8 +121,11 @@ class UDSActorClient(threading.Thread): ba = QByteArray() buffer = QBuffer(ba) buffer.open(QIODevice.WriteOnly) - pixmap.save(buffer) - return bytes(ba.toBase64()).decode() # 'result' of JSON will contain base64 of screen + pixmap.save(buffer, 'PNG') + buffer.close() + scrBase64 = bytes(ba.toBase64()).decode() + logger.debug('Screenshot length: %s', len(scrBase64)) + return scrBase64 # 'result' of JSON will contain base64 of screen def script(self, script: str) -> typing.Any: tools.ScriptExecutorThread(script).start() diff --git a/actor/src/udsactor/http/client.py b/actor/src/udsactor/http/client.py index cdebef26..d3aed687 100644 --- a/actor/src/udsactor/http/client.py +++ b/actor/src/udsactor/http/client.py @@ -139,7 +139,10 @@ class HTTPServerThread(threading.Thread): def stop(self) -> None: if self._server: logger.debug('Stopping Http-client Service') - self._app.api.unregister(self.url) + try: + self._app.api.unregister(self.url) + except Exception as e: + logger.error('Error unregistering on actor service: %s', e) self._server.shutdown() self._server = None @@ -153,6 +156,9 @@ class HTTPServerThread(threading.Thread): # Register using app api logger.debug('Registered %s', self.url) - self._app.api.register(self.url) + try: + self._app.api.register(self.url) + except Exception as e: + logger.error('Error registering on actor service: %s', e) self._server.serve_forever() diff --git a/actor/src/udsactor/http/server.py b/actor/src/udsactor/http/server.py index 6efbd5ab..03b27c38 100644 --- a/actor/src/udsactor/http/server.py +++ b/actor/src/udsactor/http/server.py @@ -148,6 +148,7 @@ class HTTPServerThread(threading.Thread): os.unlink(self._certFile) except Exception as e: logger.error('Error removing certificate file: %s', e) + logger.debug('Http-server stopped') def run(self): HTTPServerHandler._service = self._service # pylint: disable=protected-access diff --git a/actor/src/udsactor/linux/daemon.py b/actor/src/udsactor/linux/daemon.py index baad7d95..7de3cc5e 100644 --- a/actor/src/udsactor/linux/daemon.py +++ b/actor/src/udsactor/linux/daemon.py @@ -32,7 +32,7 @@ import sys import os import time import atexit -from signal import SIGTERM +from signal import SIGTERM, SIGKILL from udsactor.log import logger @@ -143,9 +143,14 @@ class Daemon: # Try killing the daemon process try: - while True: + cnt = 10 + while cnt: + cnt -= 1 os.kill(pid, SIGTERM) time.sleep(1) + + if not cnt: + os.kill(pid, SIGKILL) except OSError as err: if err.errno == 3: # No such process if os.path.exists(self.pidfile): diff --git a/actor/src/udsactor/log.py b/actor/src/udsactor/log.py index 6cb58043..2770561e 100644 --- a/actor/src/udsactor/log.py +++ b/actor/src/udsactor/log.py @@ -83,7 +83,7 @@ class Logger: if self.remoteLogger: self.remoteLogger.log(self.own_token, level, msg) except Exception as e: - self.localLogger.log(FATAL, 'Error notifying log to broker: {}'.format(e)) + self.localLogger.log(DEBUG, 'Log to broker: {}'.format(e)) self.localLogger.log(level, msg) diff --git a/actor/src/udsactor/rest.py b/actor/src/udsactor/rest.py index 553b3cff..dcbe89a9 100644 --- a/actor/src/udsactor/rest.py +++ b/actor/src/udsactor/rest.py @@ -180,7 +180,7 @@ class UDSServerApi(UDSApi): if result.ok: return result.json()['result'] except requests.ConnectionError as e: - raise RESTConnectionError(str(e)) + raise RESTConnectionError(e) except RESTError: raise except Exception as e: diff --git a/actor/src/udsactor/service.py b/actor/src/udsactor/service.py index 89bbba1c..70b68214 100644 --- a/actor/src/udsactor/service.py +++ b/actor/src/udsactor/service.py @@ -124,6 +124,9 @@ class CommonService: # pylint: disable=too-many-instance-attributes self._rebootRequested = True def setReady(self) -> None: + if not self._isAlive: + return + # First, if postconfig is available, execute it and disable it if self._cfg.post_command: self.execute(self._cfg.post_command, 'postConfig') @@ -140,7 +143,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes try: self._certificate = self._api.ready(self._cfg.own_token, self._secret, srvInterface.ip, rest.LISTEN_PORT) except rest.RESTConnectionError as e: - logger.info('Error connecting with UDS Broker: %s', e) + logger.info('Error connecting with UDS Broker') self.doWait(5000) continue except Exception as e: @@ -151,18 +154,24 @@ class CommonService: # pylint: disable=too-many-instance-attributes platform.operations.reboot() # On too many errors, simply reboot # Success or any error that is not recoverable (retunerd by UDS). if Error, service will be cleaned in a while. break - else: logger.error('Could not locate IP address!!!. (Not registered with UDS)') + # Do not continue if not alive... + if not self._isAlive: + return # Cleans sensible data if self._cfg.config: self._cfg = self._cfg._replace(config=self._cfg.config._replace(os=None), data=None) platform.store.writeConfig(self._cfg) + logger.debug('Done setReady') self._startHttpServer() def configureMachine(self) -> bool: + if not self._isAlive: + return False + # First, if runonce is present, honor it and remove it from config # Return values is "True" for keep service (or daemon) running, False if Stop it. if self._cfg.runonce_command: @@ -205,7 +214,7 @@ class CommonService: # pylint: disable=too-many-instance-attributes return True def initialize(self) -> bool: - if not self._cfg.host: # Not configured + if not self._cfg.host or not self._isAlive: # Not configured or not running return False # Force time sync, just in case... @@ -345,7 +354,12 @@ class CommonService: # pylint: disable=too-many-instance-attributes Invoked to wait a bit CAN be OVERRIDEN ''' - time.sleep(float(miliseconds) / 1000) + seconds = miliseconds / 1000.0 + # So it can be broken by "stop" + while self._isAlive and seconds > 1: + time.sleep(1) + seconds -= 1 + time.sleep(seconds) def notifyStop(self) -> None: ''' diff --git a/server/src/uds/core/managers/user_service.py b/server/src/uds/core/managers/user_service.py index 75812dd5..07217930 100644 --- a/server/src/uds/core/managers/user_service.py +++ b/server/src/uds/core/managers/user_service.py @@ -30,13 +30,10 @@ """ @author: Adolfo Gómez, dkmaster at dkmon dot com """ -import json import logging import random import typing -import requests - from django.utils.translation import ugettext as _ from django.db.models import Q from django.db import transaction diff --git a/server/src/uds/core/managers/userservice/comms.py b/server/src/uds/core/managers/userservice/comms.py index db2d0e24..26acbe24 100644 --- a/server/src/uds/core/managers/userservice/comms.py +++ b/server/src/uds/core/managers/userservice/comms.py @@ -10,116 +10,101 @@ if typing.TYPE_CHECKING: logger = logging.getLogger(__name__) +TIMEOUT = 2 -def notifyPreconnect(userService: 'UserService', userName: str, protocol: str) -> None: - ''' - Notifies a preconnect to an user service - ''' - proxy = userService.deployed_service.proxy +class NoActorComms(Exception): + pass + +class OldActorVersion(NoActorComms): + pass + +def _requestActor( + userService: 'UserService', + method: str, + data: typing.Optional[typing.MutableMapping[str, typing.Any]] = None, + minVersion: typing.Optional[str] = None + ) -> typing.Any: + """ + Makes a request to actor using "method" + if data is None, request is done using GET, else POST + if no communications url is provided or no min version, raises a "NoActorComms" exception (or OldActorVersion, derived from NoActorComms) + Returns request response value interpreted as json + """ url = userService.getCommsUrl() - ip, hostname = userService.getConnectionSource() - if not url: - logger.debug('No notification is made because agent does not supports notifications') - return + logger.warning('No notification is made because agent does not supports notifications: %s', userService.friendly_name) + raise NoActorComms('No notification urls for {}'.format(userService.friendly_name)) - url += '/preConnect' + minVersion = minVersion or '2.0.0' + version = userService.getProperty('actor_version') or '0.0.0' + if '-' in version or version < minVersion: + logger.warning('Pool %s has old actors (%s)', userService.deployed_service.name, version) + raise OldActorVersion('Old actor version {} for {}'.format(version, userService.friendly_name)) + url += '/' + method + + proxy = userService.deployed_service.proxy try: - data = {'user': userName, 'protocol': protocol, 'ip': ip, 'hostname': hostname} if proxy is not None: - r = proxy.doProxyRequest(url=url, data=data, timeout=2) + r = proxy.doProxyRequest(url=url, data=data, timeout=TIMEOUT) else: - r = requests.post( - url, - data=json.dumps(data), - headers={'content-type': 'application/json'}, - verify=False, - timeout=2 - ) - r = json.loads(r.content) - logger.debug('Sent pre-connection to client using %s: %s', url, r) - # In fact we ignore result right now - except Exception as e: - logger.info('preConnection failed: %s. Check connection on destination machine: %s', e, url) - -def checkUuid(userService: 'UserService') -> bool: - ''' - Checks if the uuid of the service is the same of our known uuid on DB - ''' - proxy = userService.deployed_service.proxy - - url = userService.getCommsUrl() - - if not url: - logger.debug('No uuid to retrieve because agent does not supports notifications') - return True # UUid is valid because it is not supported checking it - - version = userService.getProperty('actor_version') or '' - # Just for 2.0 or newer, previous actors will not support this method. - # Also externally supported agents will not support this method (as OpenGnsys) - if '-' in version or version < '2.0.0': - return True - - url += '/uuid' - - try: - if proxy: - r = proxy.doProxyRequest(url=url, timeout=5) - else: - r = requests.get(url, verify=False, timeout=5) - - if version >= '3.0.0': # New type of response: {'result': uuid} - uuid = r.json()['result'] - else: - uuid = r.json() - - if uuid != userService.uuid: - logger.info('The requested machine has uuid %s and the expected was %s', uuid, userService.uuid) - return False - - logger.debug('Got uuid from machine: %s %s %s', url, uuid, userService.uuid) - # In fact we ignore result right now - except Exception as e: - logger.error('Get uuid failed: %s. Check connection on destination machine: %s', e, url) - - return True - -def requestScreenshot(userService: 'UserService') -> bytes: - """ - Returns an screenshot in PNG format (bytes) or empty png if not supported - """ - png = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==' - proxy = userService.deployed_service.proxy - url = userService.getCommsUrl() - - version = userService.getProperty('actor_version') or '' - - # Just for 3.0 or newer, previous actors will not support this method. - # Also externally supported agents will not support this method (as OpenGnsys) - if '-' in version or version < '3.0.0': - url = '' - - if url: - try: - data: typing.Dict[str, str] = {} - if proxy is not None: - r = proxy.doProxyRequest(url=url, data=data, timeout=2) + if data is None: + r = requests.get(url, verify=False, timeout=TIMEOUT) else: r = requests.post( url, data=json.dumps(data), headers={'content-type': 'application/json'}, verify=False, - timeout=2 + timeout=TIMEOUT ) - png = json.loads(r.content)['result'] + js = r.json() - # In fact we ignore result right now - except Exception as e: - logger.error('Get uuid failed: %s. Check connection on destination machine: %s', e, url) + if version >= '3.0.0': + js = js['result'] + logger.debug('Requested %s to actor. Url=%s, Result=%s', method, url, js) + # In fact we ignore result right now + except Exception as e: + logger.warning('Request %s failed: %s. Check connection on destination machine: %s', method, e, url) + js = None - return base64.b64decode(png) + return js + +def notifyPreconnect(userService: 'UserService', userName: str, protocol: str) -> None: + ''' + Notifies a preconnect to an user service + ''' + ip, hostname = userService.getConnectionSource() + try: + _requestActor(userService, 'preConnect', {'user': userName, 'protocol': protocol, 'ip': ip, 'hostname': hostname}) + except NoActorComms: + pass # If no preconnect, warning will appear on UDS log + +def checkUuid(userService: 'UserService') -> bool: + ''' + Checks if the uuid of the service is the same of our known uuid on DB + ''' + try: + uuid = _requestActor(userService, 'uuid') + if uuid != userService.uuid: + logger.info('Machine %s do not have expected uuid %s, instead has %s', userService.friendly_name, userService.uuid, uuid) + return False + except NoActorComms: + pass + + return True # Actor does not supports checking + +def requestScreenshot(userService: 'UserService') -> bytes: + """ + Returns an screenshot in PNG format (bytes) or empty png if not supported + """ + emptyPng = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==' + try: + png = _requestActor(userService, 'screenshot', minVersion='3.0.0') # First valid version with screenshot is 3.0 + except NoActorComms: + png = None + + return base64.b64decode(png or emptyPng) def sendScript(userService: 'UserService', script: str, forUser: bool = False) -> None: