Source code for univention.management.console.protocol.modserver

#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention Management Console
#  module server process implementation
#
# Copyright 2006-2022 Univention GmbH
#
# https://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <https://www.gnu.org/licenses/>.

"""This module provides a class for an UMC module server. it is based on
the UMC server class
:class:`~univention.management.console.protocol.server.Server`.
"""

import errno
import sys
import traceback
import socket

import notifier
import six

from .server import Server
from .message import Response, Message, IncompleteMessageError, ParseError
from .definitions import MODULE_ERR_INIT_FAILED, SUCCESS, RECV_BUFFER_SIZE

from univention.management.console.acl import ACLs
from univention.management.console.module import Module
from univention.management.console.log import MODULE, PROTOCOL

from univention.lib.i18n import Translation

try:
	from typing import Any, NoReturn, Optional  # noqa: F401
except ImportError:
	pass

_ = Translation('univention.management.console').translate


[docs]class ModuleServer(Server): """Implements an UMC module server :param str socket: UNIX socket filename :param str module: name of the UMC module to serve :param int timeout: If there are no incoming requests for *timeout* seconds the module server shuts down :param bool check_acls: if False the module server does not check the permissions (**dangerous!**) """ def __init__(self, socket, module, timeout=300, check_acls=True): # type: (str, str, int, bool) -> None self.__name = module self.__module = module self.__commands = Module() self.__comm = None self.__client = None self.__buffer = b'' self.__acls = None self.__timeout = timeout self.__time_remaining = timeout self.__active_requests = 0 self._timer() self.__check_acls = check_acls self.__queue = b'' self.__username = None self.__user_dn = None self.__password = None self.__init_etype = None self.__init_exc = None self.__init_etraceback = None self.__handler = None self._load_module() Server.__init__(self, port=None, ssl=False, unix=socket, magic=False, load_ressources=False) MODULE.process('Module socket initialized.') def __enter__(self): x = super(ModuleServer, self).__enter__() self.signal_connect('session_new', self._client) return x def _load_module(self): # type: () -> None MODULE.process('Loading python module.') modname = self.__module from ..error import UMC_Error try: try: file_ = 'univention.management.console.modules.%s' % (modname,) self.__module = __import__(file_, {}, {}, modname) MODULE.process('Imported python module.') self.__handler = self.__module.Instance() MODULE.process('Module instance created.') except Exception as exc: error = _('Failed to load module %(module)s: %(error)s\n%(traceback)s') % {'module': modname, 'error': exc, 'traceback': traceback.format_exc()} MODULE.error(error) if isinstance(exc, ImportError) and str(exc).startswith('No module named %s' % (modname,)): error = '\n'.join(( _('The requested module %r does not exist.') % (modname,), _('The module may have been removed recently.'), _('Please relogin to the Univention Management Console to see if the error persists.'), _('Further information can be found in the logfile %s.') % ('/var/log/univention/management-console-module-%s.log' % (modname,),), )) raise UMC_Error(error, status=MODULE_ERR_INIT_FAILED) except UMC_Error: try: exc_info = sys.exc_info() self.__init_etype, self.__init_exc, self.__init_etraceback = exc_info # FIXME: do not keep a reference to traceback finally: exc_info = None else: self.__handler.signal_connect('success', notifier.Callback(self._reply, True)) def _reply(self, msg, final): if final: self.__active_requests -= 1 self.response(msg) def _timer(self): # type: () -> None """In order to avoid problems when the system time is changed (e.g., via rdate), we register a timer event that counts down the session timeout second-wise.""" # count down the remaining time if not self.__active_requests: self.__time_remaining -= 1 if self.__time_remaining <= 0: # module has timed out self._timed_out() else: # count down the timer second-wise (in order to avoid problems when # changing the system time, e.g. via rdate) notifier.timer_add(1000, self._timer) def _timed_out(self): # type: () -> NoReturn MODULE.info('Committing suicide') if self.__handler: self.__handler.destroy() self.exit() sys.exit(0) def _client(self, client, socket): self.__comm = socket self.__client = client notifier.socket_add(self.__comm, self._recv) def _recv(self, sock): # type: (socket.socket) -> bool try: data = sock.recv(RECV_BUFFER_SIZE) except socket.error as exc: MODULE.error('Failed connection: %s' % (errno.errorcode.get(exc.errno, exc.errno),)) data = None # connection closed? if not data: sock.close() if sock == self.__comm: MODULE.info('UMC server connection closed. This module is no longer in use.') # the connection to UMC server connection has been closed/died/... # so from now on this module is unused. Thus it is committing suicide right now. self._timed_out() else: MODULE.info('Connection %r closed' % (sock,)) # remove socket from notifier return False self.__buffer += data msg = None while self.__buffer: try: msg = Message() self.__buffer = msg.parse(self.__buffer) MODULE.info("Received request %s" % msg.id) self.handle(msg) except IncompleteMessageError: MODULE.info('Failed to parse incomplete message') return True except ParseError as exc: MODULE.error('Failed to parse message: %s' % (exc,)) if not msg.id: msg.id = -1 status, message = exc.args from ..error import UMC_Error raise UMC_Error(message, status=status) except Exception: self.error_handling(msg, 'init', *sys.exc_info()) return True
[docs] def error_handling(self, request, method, etype, exc, etraceback): if self.__handler: self.__handler._Base__requests[request.id] = (request, method) self.__handler._Base__error_handling(request, method, etype, exc, etraceback) return trace = ''.join(traceback.format_exception(etype, exc, etraceback)) MODULE.error('The init function of the module failed\n%s: %s' % (exc, trace,)) from ..error import UMC_Error if not isinstance(exc, UMC_Error): error = _('The initialization of the module failed: %s') % (trace,) exc = UMC_Error(error, status=MODULE_ERR_INIT_FAILED) etype = UMC_Error resp = Response(request) resp.status = exc.status resp.message = str(exc) resp.result = exc.result resp.headers = exc.headers self.response(resp)
[docs] def handle(self, msg): # type: (Request) -> None """Handles incoming UMCP requests. This function is called only when it is a valid UMCP request. :param Request msg: the received UMCP request The following commands are handled directly and are not passed to the custom module code: * SET (acls|username|credentials) * EXIT """ from ..error import UMC_Error, NotAcceptable self.__time_remaining = self.__timeout PROTOCOL.info('Received UMCP %s REQUEST %s' % (msg.command, msg.id)) resp = Response(msg) resp.status = SUCCESS if msg.command == 'EXIT': shutdown_timeout = 100 MODULE.info("EXIT: module shutdown in %dms" % shutdown_timeout) # shutdown module after one second resp.message = 'module %s will shutdown in %dms' % (msg.arguments[0], shutdown_timeout) self.response(resp) notifier.timer_add(shutdown_timeout, self._timed_out) return if self.__init_etype: notifier.timer_add(10000, self._timed_out) six.reraise(self.__init_etype, self.__init_exc, self.__init_etraceback) if msg.command == 'SET': for key, value in msg.options.items(): if key == 'acls': self.__acls = ACLs(acls=value) self.__handler.acls = self.__acls elif key == 'commands': self.__commands.fromJSON(value['commands']) elif key == 'username': self.__username = value self.__handler.username = self.__username elif key == 'password': self.__password = value self.__handler.password = self.__password elif key == 'auth_type': self.__auth_type = value self.__handler.auth_type = self.__auth_type elif key == 'credentials': self.__username = value['username'] self.__user_dn = value['user_dn'] self.__password = value['password'] self.__auth_type = value.get('auth_type') self.__handler.username = self.__username self.__handler.user_dn = self.__user_dn self.__handler.password = self.__password self.__handler.auth_type = self.__auth_type elif key == 'locale' and value is not None: try: self.__handler.update_language([value]) except NotAcceptable: pass # ignore if the locale doesn't exists, it continues with locale C else: raise UMC_Error(status=422) # if SET command contains 'acls', commands' and # 'credentials' it is the initialization of the module # process if 'acls' in msg.options and 'commands' in msg.options and 'credentials' in msg.options: MODULE.process('Initializing module.') try: self.__handler.init() except Exception: try: exc_info = sys.exc_info() self.__init_etype, self.__init_exc, self.__init_etraceback = exc_info # FIXME: do not keep a reference to traceback self.error_handling(msg, 'init', *exc_info) finally: exc_info = None return self.response(resp) return if msg.arguments: cmd = msg.arguments[0] cmd_obj = self.command_get(cmd) if cmd_obj and (not self.__check_acls or self.__acls.is_command_allowed(cmd, options=msg.options, flavor=msg.flavor)): self.__active_requests += 1 self.__handler.execute(cmd_obj.method, msg) return raise UMC_Error('Not initialized.', status=403)
[docs] def command_get(self, command_name): # type: (str) -> Optional[Any] """Returns the command object that matches the given command name""" for cmd in self.__commands.commands: if cmd.name == command_name: return cmd return None
[docs] def command_is_known(self, command_name): # type: (str) -> bool """Checks if a command with the given command name is known :rtype: bool """ for cmd in self.__commands.commands: if cmd.name == command_name: return True return False
def _do_send(self, sock): if len(self.__queue) > 0: length = len(self.__queue) try: ret = self.__comm.send(self.__queue) except socket.error as exc: if exc.errno == errno.EWOULDBLOCK: return True if exc.errno == errno.EPIPE: return False raise if ret < length: self.__queue = self.__queue[ret:] return True else: self.__queue = b'' return False else: return False
[docs] def response(self, msg): """Sends an UMCP response to the client""" PROTOCOL.info('Sending UMCP RESPONSE %s' % msg.id) self.__queue += bytes(msg) if self._do_send(self.__comm): notifier.socket_add(self.__comm, self._do_send, notifier.IO_WRITE)