400 lines
16 KiB
Python
400 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Copyright (c) 2014 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: Ramón M. Gómez, ramongomez at us dot es
|
|
"""
|
|
|
|
|
|
import base64
|
|
import os
|
|
import random
|
|
import shutil
|
|
import string
|
|
import threading
|
|
import time
|
|
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.jobmgr import JobMgr
|
|
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):
|
|
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.debug (str(e))
|
|
raise Exception(e)
|
|
|
|
return wrapper
|
|
|
|
|
|
# Check if operation is permitted
|
|
def execution_level(level):
|
|
def check_permitted(fnc):
|
|
def wrapper(*args, **kwargs):
|
|
levels = ['status', 'halt', 'full']
|
|
this = args[0]
|
|
try:
|
|
if levels.index(level) <= levels.index(this.exec_level):
|
|
return fnc(*args, **kwargs)
|
|
else:
|
|
raise Exception('Unauthorized operation')
|
|
except Exception as e:
|
|
logger.debug (str(e))
|
|
raise Exception(e)
|
|
|
|
return wrapper
|
|
|
|
return check_permitted
|
|
|
|
|
|
class OpenGnSysWorker(ServerWorker):
|
|
name = 'opengnsys' # Module name
|
|
interface = None # Bound interface for OpenGnsys
|
|
REST = None # REST object
|
|
user = [] # User sessions
|
|
session_type = '' # User session type
|
|
random = None # Random string for secure connections
|
|
length = 32 # Random string length
|
|
exec_level = None # Execution level (permitted operations)
|
|
jobmgr = JobMgr()
|
|
|
|
def onActivation(self):
|
|
"""
|
|
Sends OGAgent activation notification to OpenGnsys server
|
|
"""
|
|
if os.path.exists ('/scripts/oginit'):
|
|
## estamos en oglive, este modulo no debe cargarse
|
|
## esta lógica la saco de src/opengnsys/linux/operations.py, donde hay un if similar
|
|
raise Exception ('Refusing to load within an ogLive image')
|
|
|
|
e = None # Error info
|
|
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(self.name, 'remote')
|
|
except NoOptionError as e:
|
|
logger.error("Configuration error: {}".format(e))
|
|
raise e
|
|
self.REST = REST(url)
|
|
# Execution level ('full' by default)
|
|
try:
|
|
self.exec_level = self.service.config.get(self.name, 'level')
|
|
except NoOptionError:
|
|
self.exec_level = 'full'
|
|
# 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:
|
|
## UnboundLocalError: cannot access local variable 'e' where it is not associated with a value
|
|
raise e
|
|
|
|
# Loop to send initialization message
|
|
init_retries = 100
|
|
for t in range(0, init_retries):
|
|
try:
|
|
try:
|
|
self.REST.sendMessage('ogagent/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))
|
|
# Trying to initialize on alternative server, if defined
|
|
# (used in "exam mode" from the University of Seville)
|
|
self.REST = REST(self.service.config.get(self.name, 'altremote'))
|
|
self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
|
|
'secret': self.random, 'ostype': operations.os_type,
|
|
'osversion': operations.os_version, 'alt_url': True,
|
|
'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')
|
|
|
|
# Delete marking files
|
|
for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']:
|
|
try:
|
|
os.remove(os.sep + f)
|
|
except OSError:
|
|
pass
|
|
# Copy file "HostsFile.FirstOctetOfIPAddress" to "HostsFile", if it exists
|
|
# (used in "exam mode" from the University of Seville)
|
|
hosts_file = os.path.join(operations.get_etc_path(), 'hosts')
|
|
new_hosts_file = hosts_file + '.' + self.interface.ip.split('.')[0]
|
|
if os.path.isfile(new_hosts_file):
|
|
shutil.copyfile(new_hosts_file, hosts_file)
|
|
|
|
logger.debug ('onActivation ok')
|
|
|
|
def onDeactivation(self):
|
|
"""
|
|
Sends OGAgent stopping notification to OpenGnsys server
|
|
"""
|
|
logger.debug('onDeactivation')
|
|
self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip,
|
|
'ostype': operations.os_type, 'osversion': operations.os_version})
|
|
|
|
def onLogin(self, data):
|
|
"""
|
|
Sends session login notification to OpenGnsys server
|
|
"""
|
|
user, language, self.session_type = tuple(data.split(','))
|
|
logger.debug('Received login for {0} using {2} with language {1}'.format(user, language, self.session_type))
|
|
self.user.append(user)
|
|
self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language,
|
|
'session': self.session_type,
|
|
'ostype': operations.os_type, 'osversion': operations.os_version})
|
|
|
|
def onLogout(self, user):
|
|
"""
|
|
Sends session logout notification to OpenGnsys server
|
|
"""
|
|
logger.debug('Received logout for {}'.format(user))
|
|
try:
|
|
self.user.pop()
|
|
except IndexError:
|
|
pass
|
|
self.REST.sendMessage('ogagent/loggedout', {'ip': self.interface.ip, 'user': user})
|
|
|
|
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)
|
|
|
|
# Warning: the order of the decorators matters
|
|
@execution_level('status')
|
|
@check_secret
|
|
def process_status(self, path, get_params, post_params, server):
|
|
"""
|
|
Returns client status (OS type or execution status) and login status
|
|
:param path:
|
|
:param get_params: optional parameter "detail" to show extended status
|
|
:param post_params:
|
|
:param server:
|
|
:return: JSON object {"status": "status_code", "loggedin": boolean, ...}
|
|
"""
|
|
st = {'linux': 'LNX', 'macos': 'OSX', 'windows': 'WIN'}
|
|
try:
|
|
# Standard status
|
|
res = {'status': st[operations.os_type.lower()], 'loggedin': len(self.user) > 0,
|
|
'session': self.session_type}
|
|
# Detailed status
|
|
if get_params.get('detail', 'false') == 'true':
|
|
res.update({'agent_version': VERSION, 'os_version': operations.os_version, 'sys_load': os.getloadavg()})
|
|
if res['loggedin']:
|
|
res.update({'sessions': len(self.user), 'current_user': self.user[-1]})
|
|
except KeyError:
|
|
# Unknown operating system
|
|
res = {'status': 'UNK'}
|
|
return res
|
|
|
|
@execution_level('halt')
|
|
@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'}
|
|
|
|
@execution_level('halt')
|
|
@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'}
|
|
|
|
@execution_level('full')
|
|
@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 post_params.get('client', 'false') == 'false':
|
|
jobid = self.jobmgr.launch_job (script, False)
|
|
return {'op': 'launched', 'jobid': jobid}
|
|
|
|
else: ## post_params.get('client') is not 'false'
|
|
## send script as-is
|
|
self.sendClientMessage('script', {'code': script})
|
|
#return {'op': 'launched', 'jobid': jobid} ## TODO obtain jobid generated at the client (can it be done?)
|
|
return {'op': 'launched'}
|
|
|
|
@execution_level('full')
|
|
@check_secret
|
|
def process_terminatescript(self, path, get_params, post_params, server):
|
|
jobid = post_params.get('jobid', None)
|
|
logger.debug('Processing terminate_script request, jobid "{}"'.format (jobid))
|
|
if jobid is None:
|
|
return {}
|
|
self.sendClientMessage('terminatescript', {'jobid': jobid})
|
|
self.jobmgr.terminate_job (jobid)
|
|
return {}
|
|
|
|
@execution_level('full')
|
|
@check_secret
|
|
def process_preparescripts(self, path, get_params, post_params, server):
|
|
logger.debug('Processing preparescripts request')
|
|
self.st = self.jobmgr.prepare_jobs()
|
|
logger.debug('Sending preparescripts to client')
|
|
self.sendClientMessage('preparescripts', None)
|
|
return {}
|
|
|
|
def process_client_preparescripts(self, params):
|
|
logger.debug('Processing preparescripts message from client')
|
|
for p in params:
|
|
#logger.debug ('p "{}"'.format(p))
|
|
self.st.append (p)
|
|
|
|
@execution_level('full')
|
|
@check_secret
|
|
def process_getscripts(self, path, get_params, post_params, server):
|
|
logger.debug('Processing getscripts request')
|
|
return self.st
|
|
|
|
@execution_level('full')
|
|
@check_secret
|
|
def process_logoff(self, path, get_params, post_params, server):
|
|
"""
|
|
Closes user session
|
|
"""
|
|
logger.debug('Received logoff operation')
|
|
# Sending log off message to OGAgent client
|
|
self.sendClientMessage('logoff', {})
|
|
return {'op': 'sent to client'}
|
|
|
|
@execution_level('full')
|
|
@check_secret
|
|
def process_popup(self, path, get_params, post_params, server):
|
|
"""
|
|
Shows a message popup on the user's session
|
|
"""
|
|
logger.debug('Received message operation')
|
|
# Sending popup message to OGAgent client
|
|
self.sendClientMessage('popup', post_params)
|
|
return {'op': 'launched'}
|
|
|
|
def process_client_popup(self, params):
|
|
self.REST.sendMessage('popup_done', params)
|