Implementing new IPC mechanics

This commit is contained in:
Adolfo Gómez García 2019-12-02 14:38:51 +01:00
parent 20bceadda9
commit a6cb350454
11 changed files with 181 additions and 45 deletions

View File

View File

@ -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 import typing
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
@ -12,5 +42,3 @@ class Handler:
self._service = service self._service = service
self._method = method self._method = method
self._params = params self._params = params

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014-2019 Virtual Cable S.L. # Copyright (c) 2019 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -36,11 +36,18 @@ if typing.TYPE_CHECKING:
from ..service import CommonService from ..service import CommonService
class LocalProvider(handler.Handler): class LocalProvider(handler.Handler):
def post_login(self) -> typing.Any: def post_login(self) -> typing.Any:
return 'ok' result = self._service.login(self._params['username'])
return result._asdict()
def post_logout(self) -> typing.Any: def post_logout(self) -> typing.Any:
self._service.logout(self._params['username'])
return 'ok' return 'ok'
def post_ping(self) -> typing.Any: 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' return 'ok'

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014-2019 Virtual Cable S.L. # Copyright (c) 2019 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,

View File

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

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (c) 2014-2019 Virtual Cable S.L. # Copyright (c) 2019 Virtual Cable S.L.
# All rights reserved. # All rights reserved.
# #
# Redistribution and use in source and binary forms, with or without modification, # Redistribution and use in source and binary forms, with or without modification,
@ -81,6 +81,7 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
handlerType = PublicProvider handlerType = PublicProvider
elif len(path) == 2 and path[0] == 'ui': elif len(path) == 2 and path[0] == 'ui':
# private method, only from localhost # private method, only from localhost
if self.client_address[0][:3] == '127':
handlerType = LocalProvider handlerType = LocalProvider
if not handlerType: if not handlerType:
@ -88,7 +89,7 @@ class HTTPServerHandler(http.server.BaseHTTPRequestHandler):
return return
try: 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: except AttributeError:
self.sendJsonResponse(error='Method not found', code=404) self.sendJsonResponse(error='Method not found', code=404)
return return

View File

@ -69,7 +69,7 @@ class Logger:
self.remoteLogger = remoteLogger self.remoteLogger = remoteLogger
self.own_token = own_token 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) level = int(level)
if level < self.logLevel: # Skip not wanted messages if level < self.logLevel: # Skip not wanted messages
return return
@ -77,26 +77,26 @@ class Logger:
# If remote logger is available, notify message to it # If remote logger is available, notify message to it
try: try:
if self.remoteLogger: if self.remoteLogger:
self.remoteLogger.log(self.own_token, level, message) self.remoteLogger.log(self.own_token, level, message % args)
except Exception as e: except Exception as e:
self.localLogger.log(FATAL, 'Error notifying log to broker: {}'.format(e)) self.localLogger.log(FATAL, 'Error notifying log to broker: {}'.format(e))
self.localLogger.log(level, message) self.localLogger.log(level, message)
def debug(self, message: str) -> None: def debug(self, message: str, *args) -> None:
self.log(DEBUG, message) self.log(DEBUG, message, *args)
def warn(self, message: str) -> None: def warn(self, message: str, *args) -> None:
self.log(WARN, message) self.log(WARN, message, *args)
def info(self, message: str) -> None: def info(self, message: str, *args) -> None:
self.log(INFO, message) self.log(INFO, message, *args)
def error(self, message: str) -> None: def error(self, message: str, *args) -> None:
self.log(ERROR, message) self.log(ERROR, message, *args)
def fatal(self, message: str) -> None: def fatal(self, message: str, *args) -> None:
self.log(FATAL, message) self.log(FATAL, message, *args)
def exception(self) -> None: def exception(self) -> None:
try: try:

View File

@ -93,23 +93,6 @@ class REST:
raise RESTError(result.content) 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]: def enumerateAuthenticators(self) -> typing.Iterable[types.AuthenticatorType]:
try: try:
result = requests.get(self.url + 'auth/auths', headers=self._headers, verify=self.validateCert, timeout=4) result = requests.get(self.url + 'auth/auths', headers=self._headers, verify=self.validateCert, timeout=4)
@ -153,8 +136,17 @@ class REST:
'log_level': logLevel 'log_level': logLevel
} }
# First, try to login to REST api
try: 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) result = requests.post(self.url + 'actor/v2/register', data=json.dumps(data), headers=headers, verify=self.validateCert)
if result.ok: if result.ok:
return result.json()['result'] return result.json()['result']
@ -162,8 +154,8 @@ class REST:
raise RESTConnectionError(str(e)) raise RESTConnectionError(str(e))
except RESTError: except RESTError:
raise raise
except Exception: except Exception as e:
pass raise RESTError('Invalid credentials')
raise RESTError(result.content) raise RESTError(result.content)
@ -207,6 +199,23 @@ class REST:
} }
self._actorPost('ipchange', payload) # Ignores result... 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: def log(self, own_token: str, level: int, message: str) -> None:
payLoad = { payLoad = {
'token': own_token, 'token': own_token,

View File

@ -41,7 +41,7 @@ from . import rest
from . import types from . import types
# from .script_thread import ScriptExecutorThread # from .script_thread import ScriptExecutorThread
from .log import logger from .log import logger
from .http import registry
# def setup() -> None: # def setup() -> None:
# cfg = platform.store.readConfig() # cfg = platform.store.readConfig()
@ -55,7 +55,6 @@ from .log import logger
# else: # else:
# logger.setLevel(20000) # logger.setLevel(20000)
class CommonService: class CommonService:
_isAlive: bool = True _isAlive: bool = True
_rebootRequested: bool = False _rebootRequested: bool = False
@ -65,6 +64,7 @@ class CommonService:
_api: rest.REST _api: rest.REST
_interfaces: typing.List[types.InterfaceInfoType] _interfaces: typing.List[types.InterfaceInfoType]
_secret: str _secret: str
_registry: registry.UDSActorClientRegistry
@staticmethod @staticmethod
def execute(cmdLine: str, section: str) -> bool: def execute(cmdLine: str, section: str) -> bool:
@ -81,6 +81,7 @@ class CommonService:
self._interfaces = [] self._interfaces = []
self._api = rest.REST(self._cfg.host, self._cfg.validateCertificate) self._api = rest.REST(self._cfg.host, self._cfg.validateCertificate)
self._secret = secrets.token_urlsafe(33) self._secret = secrets.token_urlsafe(33)
self._registry = registry.UDSActorClientRegistry()
# Initialzies loglevel # Initialzies loglevel
logger.setLevel(self._cfg.log_level * 10000) logger.setLevel(self._cfg.log_level * 10000)
@ -179,7 +180,7 @@ class CommonService:
while self._isAlive: while self._isAlive:
if not self._interfaces: if not self._interfaces:
self._interfaces = list(platform.operations.getNetworkInfo()) 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) self.doWait(5000)
continue continue
@ -281,6 +282,16 @@ class CommonService:
''' '''
logger.info('Base join invoked: {} on {}, {}'.format(name, domain, ou)) 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 # Methods that CAN BE overriden by actors
# **************************************** # ****************************************

View File

@ -48,3 +48,8 @@ class InitializationResultType(typing.NamedTuple):
unique_id: typing.Optional[str] = None unique_id: typing.Optional[str] = None
max_idle: typing.Optional[int] = None max_idle: typing.Optional[int] = None
os: typing.Optional[ActorOsConfigurationType] = None os: typing.Optional[ActorOsConfigurationType] = None
class LoginResultInfoType(typing.NamedTuple):
ip: str
hostname: str
dead_line: typing.Optional[int]

View File

@ -192,7 +192,7 @@ class UDSActorSvc(win32serviceutil.ServiceFramework, CommonService):
return 'done' return 'done'
def onLogout(self, userName) -> None: def onLogout(self, userName) -> None:
logger.debug('Windows onLogout invoked: {}, {}'.format(user, self._user)) logger.debug('Windows onLogout invoked: {}, {}'.format(userName, self._user))
try: try:
p = win32security.GetBinarySid(REMOTE_USERS_SID) p = win32security.GetBinarySid(REMOTE_USERS_SID)
groupName = win32security.LookupAccountSid(None, p)[0] groupName = win32security.LookupAccountSid(None, p)[0]