436 lines
18 KiB
Python
436 lines
18 KiB
Python
#!/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')
|