#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # Copyright (c) 2014 Virtual Cable S.L. # Copyright (c) 2024 Qindel Formación y Servicios 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: Ramón M. Gómez, ramongomez at us dot es @author: Natalia Serrano, nserrano at qindel dot com """ import base64 import os import random import shutil import string import threading import time import subprocess import urllib.error import urllib.parse import urllib.request from configparser import NoOptionError from opengnsys import REST, operations, VERSION from opengnsys.log import logger from opengnsys.scriptThread import ScriptExecutorThread from opengnsys.workers import ServerWorker # Check authorization header decorator def check_secret(fnc): """ Decorator to check for received secret key and raise exception if it isn't valid. """ def wrapper(*args, **kwargs): return fnc(*args, **kwargs) #try: # this, path, get_params, post_params, server = args # # Accept "status" operation with no arguments or any function with Authorization header # if fnc.__name__ == 'process_status' and not get_params: # return fnc(*args, **kwargs) # elif this.random == server.headers['Authorization']: # return fnc(*args, **kwargs) # else: # raise Exception('Unauthorized operation') #except Exception as e: # logger.error(str(e)) # raise Exception(e) return wrapper def interfaceAdmin (exe, parametros=[]): devel_bash_prefix = ''' PATH=/opt/opengnsys/scripts/:$PATH; for I in /opt/opengnsys/lib/engine/bin/*.lib; do source $I; done; for i in $(declare -F |cut -f3 -d" "); do export -f $i; done; ''' if 'nati' == os.environ['USER'] or 'nati' == os.environ['SUDO_USER']: ## DO NOT COMMIT devel_bash_prefix = ''' PATH=/home/nati/Downloads/work/opengnsys/opengnsys/client/shared/scripts:$PATH; for I in /home/nati/Downloads/work/opengnsys/opengnsys/client/engine/*.lib; do source $I; done; for i in $(declare -F |cut -f3 -d" "); do export -f $i; done; ''' if parametros: proc = ['bash', '-c', '{} {} {}'.format (devel_bash_prefix, exe, ' '.join (parametros))] else: proc = ['bash', '-c', '{} {}'.format (devel_bash_prefix, exe)] logger.debug ('subprocess.run ("{}", capture_output=True)'.format (proc)) return subprocess.run (proc, capture_output=True).stdout.strip().decode ('utf-8') class ogAdmClientWorker(ServerWorker): name = 'ogAdmClient' # Module name interface = None # Bound interface for OpenGnsys REST = None # REST object random = None # Random string for secure connections length = 32 # Random string length pathinterface = None IPlocal = None idordenador = None nombreordenador = None cache = None idproautoexec = None idcentro = None idaula = None def onActivation(self): """ Sends OGAgent activation notification to OpenGnsys server """ t = 0 # Count of time # Generate random secret to send on activation self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length)) # Ensure cfg has required configuration variables or an exception will be thrown try: url = self.service.config.get('ogAdmClient', 'remote') loglevel = self.service.config.get('ogAdmClient', 'log') #servidorAdm = self.service.config.get('ogAdmClient', 'servidorAdm') #puerto = self.service.config.get('ogAdmClient', 'puerto') self.pathinterface = self.service.config.get('ogAdmClient', 'pathinterface') urlMenu = self.service.config.get('ogAdmClient', 'urlMenu') urlMsg = self.service.config.get('ogAdmClient', 'urlMsg') logger.setLevel(loglevel) except NoOptionError as e: logger.error("Configuration error: {}".format(e)) raise e self.REST = REST(url) # Get network interfaces until they are active or timeout (5 minutes) for t in range(0, 300): try: # Get the first network interface self.interface = list(operations.getNetworkInfo())[0] except Exception as e: # Wait 1 sec. and retry logger.warn (e) time.sleep(1) finally: # Exit loop if interface is active if self.interface: if t > 0: logger.debug("Fetch connection data after {} tries".format(t)) break # Raise error after timeout if not self.interface: raise Exception ('not self.interface') # Loop to send initialization message init_retries = 100 for t in range(0, init_retries): try: self.REST.sendMessage('started', {'mac': self.interface.mac, 'ip': self.interface.ip, 'secret': self.random, 'ostype': operations.os_type, 'osversion': operations.os_version, 'agent_version': VERSION}) break except Exception as e: logger.warn (str (e)) time.sleep(3) # Raise error after timeout if t < init_retries-1: logger.debug('Successful connection after {} tries'.format(t)) elif t == init_retries-1: raise Exception('Initialization error: Cannot connect to remote server') if (not self.tomaIPlocal()): raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo') if (not self.inclusionCliente()): raise Exception ('Se han generado errores. No se puede continuar la ejecución de este módulo') def onDeactivation(self): """ Sends OGAgent stopping notification to OpenGnsys server """ logger.debug('onDeactivation') self.REST.sendMessage('stopped', {'mac': self.interface.mac, 'ip': self.interface.ip, 'ostype': operations.os_type, 'osversion': operations.os_version}) def processClientMessage(self, message, data): logger.debug('Got OpenGnsys message from client: {}, data {}'.format(message, data)) def onLogin(self, data): logger.warn('in onLogin, should not happen') def onLogout(self, user): logger.warn('in onLogout, should not happen') def process_ogclient(self, path, get_params, post_params, server): """ This method can be overridden to provide your own message processor, or better you can implement a method that is called exactly as "process_" + path[0] (module name has been removed from path array) and this default processMessage will invoke it * Example: Imagine this invocation url (no matter if GET or POST): http://example.com:9999/Sample/mazinger/Z The HTTP Server will remove "Sample" from path, parse arguments and invoke this method as this: module.processMessage(["mazinger","Z"], get_params, post_params) This method will process "mazinger", and look for a "self" method that is called "process_mazinger", and invoke it this way: return self.process_mazinger(["Z"], get_params, post_params) In the case path is empty (that is, the path is composed only by the module name, like in "http://example.com/Sample", the "process" method will be invoked directly The methods must return data that can be serialized to json (i.e. Objects are not serializable to json, basic type are) """ if not path: return "ok" try: operation = getattr(self, 'ogclient_' + path[0]) except Exception: raise Exception('Message processor for "{}" not found'.format(path[0])) return operation(path[1:], get_params, post_params) @check_secret def process_status(self, path, get_params, post_params, server): return {'ogAdmClient': 'in process_status'} @check_secret def process_reboot(self, path, get_params, post_params, server): """ Launches a system reboot operation :param path: :param get_params: :param post_params: :param server: authorization header :return: JSON object {"op": "launched"} """ logger.debug('Received reboot operation') # Rebooting thread def rebt(): operations.reboot() threading.Thread(target=rebt).start() return {'op': 'launched'} @check_secret def process_poweroff(self, path, get_params, post_params, server): """ Launches a system power off operation :param path: :param get_params: :param post_params: :param server: authorization header :return: JSON object {"op": "launched"} """ logger.debug('Received poweroff operation') # Powering off thread def pwoff(): time.sleep(2) operations.poweroff() threading.Thread(target=pwoff).start() return {'op': 'launched'} @check_secret def process_script(self, path, get_params, post_params, server): """ Processes an script execution (script should be encoded in base64) :param path: :param get_params: :param post_params: JSON object {"script": "commands"} :param server: authorization header :return: JSON object {"op": "launched"} """ logger.debug('Processing script request') # Decoding script script = urllib.parse.unquote(base64.b64decode(post_params.get('script')).decode('utf-8')) logger.debug('received script {}'.format(script)) if operations.os_type == 'Windows': ## for windows, we turn the script into utf16le, then to b64 again, and feed the blob to powershell u16 = script.encode ('utf-16le') ## utf16 b64 = base64.b64encode (u16).decode ('utf-8') ## b64 (which returns bytes, so we need an additional decode(utf8)) script = """ import os import tempfile import subprocess cp = subprocess.run ("powershell -WindowStyle Hidden -EncodedCommand {}", capture_output=True) subprocs_log = os.path.join (tempfile.gettempdir(), 'opengnsys-subprocs.log') with open (subprocs_log, 'ab') as fd: ## TODO improve this logging fd.write (cp.stdout) fd.write (cp.stderr) """.format (b64) else: script = 'import subprocess; subprocess.check_output("""{0}""",shell=True)'.format(script) # Executing script. if post_params.get('client', 'false') == 'false': thr = ScriptExecutorThread(script) thr.start() else: self.sendClientMessage('script', {'code': script}) return {'op': 'launched'} @check_secret def process_logoff(self, path, get_params, post_params, server): logger.warn('in process_logoff, should not happen') @check_secret def process_popup(self, path, get_params, post_params, server): logger.warn('in process_popup, should not happen') #def process_client_popup(self, params): # logger.warn('in process_client_popup') ## process_* are invoked from opengnsys/httpserver.py:99 "data = module.processServerMessage(path, get_params, post_params, self)" (via opengnsys/workers/server_worker.py) ## process_client_* are invoked from opengnsys/service.py:123 "v.processClientMessage(message, json.loads(data))" (via opengnsys/workers/server_worker.py) def tomaIPlocal(self): logger.debug (__name__) interface = '{}/getIpAddress'.format (self.pathinterface) try: self.IPlocal = interfaceAdmin (interface); logger.info (self.IPlocal) except Exception as e: logger.error (e) logger.error ('No se ha podido recuperar la dirección IP del cliente') return False return True def LeeConfiguracion(self): parametroscfg = interfaceAdmin ('{}/getConfiguration'.format (self.pathinterface)) ## Configuración de los Sistemas Operativos del cliente logger.debug ('parametroscfg ({})'.format (parametroscfg)) return (parametroscfg) def enviaMensajeServidor(self, path, obj): obj['iph'] = self.IPlocal ## Ip del ordenador obj['ido'] = self.idordenador ## Identificador del ordenador obj['npc'] = self.nombreordenador ## Nombre del ordenador obj['idc'] = self.idcentro ## Identificador del centro obj['ida'] = self.idaula ## Identificador del aula res = self.REST.sendMessage (path, obj) if (not res): ## TODO #logger.error ('No se ha podido establecer conexión con el Servidor de Administración') ## Error de conexión con el servidor logger.error ('Error al enviar trama ***send() fallo') return False return res def inclusionCliente(self): cfg = self.LeeConfiguracion() res = self.enviaMensajeServidor ('InclusionCliente', { 'cfg': cfg }) logger.debug ('res ({})'.format (res)) if (not res or 0 == res['res']) : logger.error ('Ha ocurrido algún problema en el proceso de inclusión del cliente') return False if (not res['ido'] or not res['npc']): logger.error ('Se han recibido parámetros con valores no válidos') return False self.idordenador = res['ido'] ## Identificador del ordenador self.nombreordenador = res['npc'] ## Nombre del ordenador self.cache = res['che'] ## Tamaño de la caché reservada al cliente self.idproautoexec = res['exe'] ## Procedimento de inicio (Autoexec) self.idcentro = res['idc'] ## Identificador de la Unidad Organizativa self.idaula = res['ida'] ## Identificador del aula return True def process_RESPUESTA_AutoexecCliente(self, path, get_params, post_params, server): logger.warn('in process_RESPUESTA_AutoexecCliente') def process_RESPUESTA_InclusionCliente(self, path, get_params, post_params, server): logger.warn('in process_RESPUESTA_InclusionCliente') def process_NoComandosPtes(self, path, get_params, post_params, server): logger.warn('in process_NoComandosPtes') def process_Actualizar(self, path, get_params, post_params, server): logger.warn('in process_Actualizar') def process_Purgar(self, path, get_params, post_params, server): logger.warn('in process_Purgar') def process_ConsolaRemota(self, path, get_params, post_params, server): logger.warn('in process_ConsolaRemota') def process_Sondeo(self, path, get_params, post_params, server): logger.warn('in process_Sondeo') def process_Arrancar(self, path, get_params, post_params, server): logger.warn('in process_Arrancar') def process_Apagar(self, path, get_params, post_params, server): logger.warn('in process_Apagar') def process_Reiniciar(self, path, get_params, post_params, server): logger.warn('in process_Reiniciar') def process_IniciarSesion(self, path, get_params, post_params, server): logger.warn('in process_IniciarSesion') def process_CrearImagen(self, path, get_params, post_params, server): logger.warn('in process_CrearImagen') def process_CrearImagenBasica(self, path, get_params, post_params, server): logger.warn('in process_CrearImagenBasica') def process_CrearSoftIncremental(self, path, get_params, post_params, server): logger.warn('in process_CrearSoftIncremental') def process_RestaurarImagen(self, path, get_params, post_params, server): logger.warn('in process_RestaurarImagen') def process_RestaurarImagenBasica(self, path, get_params, post_params, server): logger.warn('in process_RestaurarImagenBasica') def process_RestaurarSoftIncremental(self, path, get_params, post_params, server): logger.warn('in process_RestaurarSoftIncremental') def process_Configurar(self, path, get_params, post_params, server): logger.warn('in process_Configurar') def process_EjecutarScript(self, path, get_params, post_params, server): logger.warn('in process_EjecutarScript') def process_InventarioHardware(self, path, get_params, post_params, server): logger.warn('in process_InventarioHardware') def process_InventarioSoftware(self, path, get_params, post_params, server): logger.warn('in process_InventarioSoftware') def process_EjecutaComandosPendientes(self, path, get_params, post_params, server): logger.warn('in process_EjecutaComandosPendientes')