diff --git a/actor/src/udsactor/http/client.py b/actor/src/udsactor/http/client.py new file mode 100644 index 00000000..e69de29b diff --git a/actor/src/udsactor/http/handler.py b/actor/src/udsactor/http/handler.py index 52d7bea8..92939809 100644 --- a/actor/src/udsactor/http/handler.py +++ b/actor/src/udsactor/http/handler.py @@ -1,3 +1,33 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019 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 +''' import typing if typing.TYPE_CHECKING: @@ -12,5 +42,3 @@ class Handler: self._service = service self._method = method self._params = params - - diff --git a/actor/src/udsactor/http/local.py b/actor/src/udsactor/http/local.py index e58bfcd4..7c9a490b 100644 --- a/actor/src/udsactor/http/local.py +++ b/actor/src/udsactor/http/local.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2014-2019 Virtual Cable S.L. +# Copyright (c) 2019 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -36,11 +36,18 @@ if typing.TYPE_CHECKING: from ..service import CommonService class LocalProvider(handler.Handler): + def post_login(self) -> typing.Any: - return 'ok' + result = self._service.login(self._params['username']) + return result._asdict() def post_logout(self) -> typing.Any: + self._service.logout(self._params['username']) return 'ok' def post_ping(self) -> typing.Any: + return 'pong' + + def post_register(self) -> typing.Any: + self._service._registry.register(self._params['url']) # pylint: disable=protected-access return 'ok' diff --git a/actor/src/udsactor/http/public.py b/actor/src/udsactor/http/public.py index 836a64c2..797bcb79 100644 --- a/actor/src/udsactor/http/public.py +++ b/actor/src/udsactor/http/public.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2014-2019 Virtual Cable S.L. +# Copyright (c) 2019 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, diff --git a/actor/src/udsactor/http/registry.py b/actor/src/udsactor/http/registry.py new file mode 100644 index 00000000..550d94cd --- /dev/null +++ b/actor/src/udsactor/http/registry.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2019 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 +''' +import json +import typing + +import requests + +from ..log import logger + +class UDSActorClientRegistry: + _clientUrl: typing.List[str] + + def __init__(self) -> None: + self._clientUrl = [] + + def _post(self, method: str, data: typing.Any = None) -> None: + removables: typing.List[str] = [] + for clientUrl in self._clientUrl: + try: + requests.post(clientUrl + '/' + method, data=json.dumps(data), verify=False) + except Exception as e: + # If cannot request to a clientUrl, remove it from list + logger.info('Could not coneect with client %s: %s. Removed from registry.', e, clientUrl) + removables.append(clientUrl) + + # Remove failed connections + for clientUrl in removables: + self.unregister(clientUrl) + + def register(self, clientUrl: str) -> None: + # Remove first if exists, to avoid duplicates + self.unregister(clientUrl) + # And add it again + self._clientUrl.append(clientUrl) + + def unregister(self, clientUrl: str) -> None: + self._clientUrl = list((i for i in self._clientUrl if i != clientUrl)) + + def executeScript(self, script: str) -> None: + self._post('script', script) + + def logout(self) -> None: + self._post('logout', None) + + def ping(self) -> bool: + self._post('ping', None) + return bool(self._clientUrl) # if no clients available diff --git a/actor/src/udsactor/http/server.py b/actor/src/udsactor/http/server.py index 1447531b..2b8a04ae 100644 --- a/actor/src/udsactor/http/server.py +++ b/actor/src/udsactor/http/server.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (c) 2014-2019 Virtual Cable S.L. +# Copyright (c) 2019 Virtual Cable S.L. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, @@ -81,14 +81,15 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler): handlerType = PublicProvider elif len(path) == 2 and path[0] == 'ui': # private method, only from localhost - handlerType = LocalProvider + if self.client_address[0][:3] == '127': + handlerType = LocalProvider if not handlerType: self.sendJsonResponse(error='Forbidden', code=403) return try: - result = getattr(handlerType(self._service, method, params), method + '_' + path[-1])() + result = getattr(handlerType(self._service, method, params), method + '_' + path[-1])() # last part of path is method except AttributeError: self.sendJsonResponse(error='Method not found', code=404) return diff --git a/actor/src/udsactor/log.py b/actor/src/udsactor/log.py index 0bd25f08..0114393d 100644 --- a/actor/src/udsactor/log.py +++ b/actor/src/udsactor/log.py @@ -69,7 +69,7 @@ class Logger: self.remoteLogger = remoteLogger self.own_token = own_token - def log(self, level: typing.Union[str, int], message: str) -> None: + def log(self, level: typing.Union[str, int], message: str, *args) -> None: level = int(level) if level < self.logLevel: # Skip not wanted messages return @@ -77,26 +77,26 @@ class Logger: # If remote logger is available, notify message to it try: if self.remoteLogger: - self.remoteLogger.log(self.own_token, level, message) + self.remoteLogger.log(self.own_token, level, message % args) except Exception as e: self.localLogger.log(FATAL, 'Error notifying log to broker: {}'.format(e)) self.localLogger.log(level, message) - def debug(self, message: str) -> None: - self.log(DEBUG, message) + def debug(self, message: str, *args) -> None: + self.log(DEBUG, message, *args) - def warn(self, message: str) -> None: - self.log(WARN, message) + def warn(self, message: str, *args) -> None: + self.log(WARN, message, *args) - def info(self, message: str) -> None: - self.log(INFO, message) + def info(self, message: str, *args) -> None: + self.log(INFO, message, *args) - def error(self, message: str) -> None: - self.log(ERROR, message) + def error(self, message: str, *args) -> None: + self.log(ERROR, message, *args) - def fatal(self, message: str) -> None: - self.log(FATAL, message) + def fatal(self, message: str, *args) -> None: + self.log(FATAL, message, *args) def exception(self) -> None: try: diff --git a/actor/src/udsactor/rest.py b/actor/src/udsactor/rest.py index cde90cf2..05108ece 100644 --- a/actor/src/udsactor/rest.py +++ b/actor/src/udsactor/rest.py @@ -93,23 +93,6 @@ class REST: raise RESTError(result.content) - def _login(self, auth: str, username: str, password: str) -> typing.MutableMapping[str, str]: - try: - # First, try to login - authInfo = {'auth': auth, 'username': username, 'password': password} - headers = self._headers - result = requests.post(self.url + 'auth/login', data=json.dumps(authInfo), headers=headers, verify=self.validateCert) - if not result.ok or result.json()['result'] == 'error': - raise Exception() # Invalid credentials - except requests.ConnectionError as e: - raise RESTConnectionError(str(e)) - except Exception as e: - raise RESTError('Invalid credentials') - - headers['X-Auth-Token'] = result.json()['token'] - - return headers - def enumerateAuthenticators(self) -> typing.Iterable[types.AuthenticatorType]: try: result = requests.get(self.url + 'auth/auths', headers=self._headers, verify=self.validateCert, timeout=4) @@ -153,8 +136,17 @@ class REST: 'log_level': logLevel } + # First, try to login to REST api try: - headers = self._login(auth, username, password) + # First, try to login + authInfo = {'auth': auth, 'username': username, 'password': password} + headers = self._headers + result = requests.post(self.url + 'auth/login', data=json.dumps(authInfo), headers=headers, verify=self.validateCert) + if not result.ok or result.json()['result'] == 'error': + raise Exception() # Invalid credentials + + headers['X-Auth-Token'] = result.json()['token'] + result = requests.post(self.url + 'actor/v2/register', data=json.dumps(data), headers=headers, verify=self.validateCert) if result.ok: return result.json()['result'] @@ -162,8 +154,8 @@ class REST: raise RESTConnectionError(str(e)) except RESTError: raise - except Exception: - pass + except Exception as e: + raise RESTError('Invalid credentials') raise RESTError(result.content) @@ -207,6 +199,23 @@ class REST: } self._actorPost('ipchange', payload) # Ignores result... + def login(self, own_token: str, username: str) -> types.LoginResultInfoType: + payload = { + 'token': own_token, + 'username': username + } + result = self._actorPost('login', payload) + return types.LoginResultInfoType(ip=result['ip'], hostname=result['hostname'], dead_line=result['dead_line']) + + + def logout(self, own_token: str, username: str) -> None: + payload = { + 'token': own_token, + 'username': username + } + self._actorPost('logout', payload) + + def log(self, own_token: str, level: int, message: str) -> None: payLoad = { 'token': own_token, diff --git a/actor/src/udsactor/service.py b/actor/src/udsactor/service.py index c3c63cf8..fd93914e 100644 --- a/actor/src/udsactor/service.py +++ b/actor/src/udsactor/service.py @@ -41,7 +41,7 @@ from . import rest from . import types # from .script_thread import ScriptExecutorThread from .log import logger - +from .http import registry # def setup() -> None: # cfg = platform.store.readConfig() @@ -55,7 +55,6 @@ from .log import logger # else: # logger.setLevel(20000) - class CommonService: _isAlive: bool = True _rebootRequested: bool = False @@ -65,6 +64,7 @@ class CommonService: _api: rest.REST _interfaces: typing.List[types.InterfaceInfoType] _secret: str + _registry: registry.UDSActorClientRegistry @staticmethod def execute(cmdLine: str, section: str) -> bool: @@ -81,6 +81,7 @@ class CommonService: self._interfaces = [] self._api = rest.REST(self._cfg.host, self._cfg.validateCertificate) self._secret = secrets.token_urlsafe(33) + self._registry = registry.UDSActorClientRegistry() # Initialzies loglevel logger.setLevel(self._cfg.log_level * 10000) @@ -179,7 +180,7 @@ class CommonService: while self._isAlive: if not self._interfaces: self._interfaces = list(platform.operations.getNetworkInfo()) - if not self._interfaces: # Wait a bit for interfaces to get initialized... + if not self._interfaces: # Wait a bit for interfaces to get initialized... (has valid IPs) self.doWait(5000) continue @@ -281,6 +282,16 @@ class CommonService: ''' logger.info('Base join invoked: {} on {}, {}'.format(name, domain, ou)) + # Client notifications + def login(self, username: str) -> types.LoginResultInfoType: + if self._cfg.own_token: + return self._api.login(self._cfg.own_token, username) + return types.LoginResultInfoType(ip='', hostname='', dead_line=None) + + def logout(self, username: str) -> None: + if self._cfg.own_token: + self._api.logout(self._cfg.own_token, username) + # **************************************** # Methods that CAN BE overriden by actors # **************************************** diff --git a/actor/src/udsactor/types.py b/actor/src/udsactor/types.py index b8e864d1..4beb0c3f 100644 --- a/actor/src/udsactor/types.py +++ b/actor/src/udsactor/types.py @@ -48,3 +48,8 @@ class InitializationResultType(typing.NamedTuple): unique_id: typing.Optional[str] = None max_idle: typing.Optional[int] = None os: typing.Optional[ActorOsConfigurationType] = None + +class LoginResultInfoType(typing.NamedTuple): + ip: str + hostname: str + dead_line: typing.Optional[int] diff --git a/actor/src/udsactor/windows/service.py b/actor/src/udsactor/windows/service.py index 39aeb435..fa929a55 100644 --- a/actor/src/udsactor/windows/service.py +++ b/actor/src/udsactor/windows/service.py @@ -192,7 +192,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService): return 'done' def onLogout(self, userName) -> None: - logger.debug('Windows onLogout invoked: {}, {}'.format(user, self._user)) + logger.debug('Windows onLogout invoked: {}, {}'.format(userName, self._user)) try: p = win32security.GetBinarySid(REMOTE_USERS_SID) groupName = win32security.LookupAccountSid(None, p)[0] @@ -216,7 +216,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService): # call the CoInitialize to allow the registration to run in an other # thread logger.debug('Initializing com...') - + pythoncom.CoInitialize() if not self.initialize():