diff options
Diffstat (limited to 'admin/Sources/Clients/ogagent/src')
5 files changed, 161 insertions, 98 deletions
diff --git a/admin/Sources/Clients/ogagent/src/cfg/ogagent.cfg b/admin/Sources/Clients/ogagent/src/cfg/ogagent.cfg index 8888e88a..3fa38ab2 100644 --- a/admin/Sources/Clients/ogagent/src/cfg/ogagent.cfg +++ b/admin/Sources/Clients/ogagent/src/cfg/ogagent.cfg @@ -8,6 +8,8 @@ path=test_modules/server # Remote OpenGnsys Service remote=https://192.168.2.10/opengnsys/rest +# Alternate OpenGnsys Service (comment out to enable this option) +#altremote=https://10.0.2.2/opengnsys/rest # Log Level, if ommited, will be set to INFO log=DEBUG diff --git a/admin/Sources/Clients/ogagent/src/opengnsys/RESTApi.py b/admin/Sources/Clients/ogagent/src/opengnsys/RESTApi.py index 5caaf8c4..d785dfa7 100644 --- a/admin/Sources/Clients/ogagent/src/opengnsys/RESTApi.py +++ b/admin/Sources/Clients/ogagent/src/opengnsys/RESTApi.py @@ -26,9 +26,9 @@ # 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 -''' +""" # pylint: disable-msg=E1101,W0703 @@ -43,7 +43,8 @@ from .log import logger from .utils import exceptionToMessage -VERIFY_CERT = False +VERIFY_CERT = False # Do not check server certificate +TIMEOUT = 5 # Connection timout, in seconds class RESTError(Exception): @@ -66,30 +67,34 @@ try: except Exception: pass # In fact, isn't too important, but wil log warns to logging file + class REST(object): - ''' + """ Simple interface to remote REST apis. The constructor expects the "base url" as parameter, that is, the url that will be common on all REST requests Remember that this is a helper for "easy of use". You can provide your owns using requests lib for example. Examples: v = REST('https://example.com/rest/v1/') (Can omit trailing / if desired) v.sendMessage('hello?param1=1¶m2=2') - This will generate a GET message to https://example.com/rest/v1/hello?param1=1¶m2=2, and return the deserialized JSON result or an exception + This will generate a GET message to https://example.com/rest/v1/hello?param1=1¶m2=2, and return the + deserialized JSON result or an exception v.sendMessage('hello?param1=1¶m2=2', {'name': 'mario' }) - This will generate a POST message to https://example.com/rest/v1/hello?param1=1¶m2=2, with json encoded body {'name': 'mario' }, and also returns + This will generate a POST message to https://example.com/rest/v1/hello?param1=1¶m2=2, with json encoded + body {'name': 'mario' }, and also returns the deserialized JSON result or raises an exception in case of error - ''' + """ + def __init__(self, url): - ''' + """ Initializes the REST helper url is the full url of the REST API Base, as for example "https://example.com/rest/v1". @param url The url of the REST API Base. The trailing '/' can be included or omitted, as desired. - ''' + """ self.endpoint = url - + if self.endpoint[-1] != '/': self.endpoint += '/' - + # Some OSs ships very old python requests lib implementations, workaround them... try: self.newerRequestLib = requests.__version__.split('.')[0] >= '1' @@ -105,37 +110,39 @@ class REST(object): pass def _getUrl(self, method): - ''' + """ Internal method Composes the URL based on "method" @param method: Method to append to base url for composition - ''' + """ url = self.endpoint + method return url def _request(self, url, data=None): - ''' + """ Launches the request @param url: The url to obtain - @param data: if None, the request will be sent as a GET request. If != None, the request will be sent as a POST, with data serialized as JSON in the body. - ''' + @param data: if None, the request will be sent as a GET request. If != None, the request will be sent as a POST, + with data serialized as JSON in the body. + """ try: if data is None: logger.debug('Requesting using GET (no data provided) {}'.format(url)) - # Old requests version does not support verify, but they do not checks ssl certificate by default + # Old requests version does not support verify, but it do not checks ssl certificate by default if self.newerRequestLib: - r = requests.get(url, verify=VERIFY_CERT) + r = requests.get(url, verify=VERIFY_CERT, timeout=TIMEOUT) else: - r = requests.get(url) - else: # POST + r = requests.get(url) + else: # POST logger.debug('Requesting using POST {}, data: {}'.format(url, data)) if self.newerRequestLib: - r = requests.post(url, data=data, headers={'content-type': 'application/json'}, verify=VERIFY_CERT) + r = requests.post(url, data=data, headers={'content-type': 'application/json'}, + verify=VERIFY_CERT, timeout=TIMEOUT) else: r = requests.post(url, data=data, headers={'content-type': 'application/json'}) - r = json.loads(r.content) # Using instead of r.json() to make compatible with oooold rquests lib versions + r = json.loads(r.content) # Using instead of r.json() to make compatible with old requests lib versions except requests.exceptions.RequestException as e: raise ConnectionError(e) except Exception as e: @@ -144,17 +151,17 @@ class REST(object): return r def sendMessage(self, msg, data=None, processData=True): - ''' + """ Sends a message to remote REST server @param data: if None or omitted, message will be a GET, else it will send a POST @param processData: if True, data will be serialized to json before sending, else, data will be sent as "raw" - ''' + """ logger.debug('Invoking post message {} with data {}'.format(msg, data)) if processData and data is not None: data = json.dumps(data) - + url = self._getUrl(msg) logger.debug('Requesting {}'.format(url)) - + return self._request(url, data) diff --git a/admin/Sources/Clients/ogagent/src/opengnsys/config.py b/admin/Sources/Clients/ogagent/src/opengnsys/config.py index c86c6979..d1f3ede6 100644 --- a/admin/Sources/Clients/ogagent/src/opengnsys/config.py +++ b/admin/Sources/Clients/ogagent/src/opengnsys/config.py @@ -33,7 +33,6 @@ from __future__ import unicode_literals from ConfigParser import SafeConfigParser -from .log import logger config = None diff --git a/admin/Sources/Clients/ogagent/src/opengnsys/modules/server/OpenGnSys/__init__.py b/admin/Sources/Clients/ogagent/src/opengnsys/modules/server/OpenGnSys/__init__.py index 8ef866ad..24d69ee4 100644 --- a/admin/Sources/Clients/ogagent/src/opengnsys/modules/server/OpenGnSys/__init__.py +++ b/admin/Sources/Clients/ogagent/src/opengnsys/modules/server/OpenGnSys/__init__.py @@ -25,14 +25,12 @@ # 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 -''' +""" from __future__ import unicode_literals -import subprocess import threading -import thread import os import platform import time @@ -47,8 +45,28 @@ from opengnsys import operations from opengnsys.log import logger from opengnsys.scriptThread import ScriptExecutorThread + +# 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 # @UnusedVariable + if this.random == server.headers['Authorization']: + fnc(*args, **kwargs) + else: + raise Exception('Unauthorized operation') + except Exception as e: + logger.error(e) + raise Exception(e) + + return wrapper + + # Error handler decorator. -def catchBackgroundError(fnc): +def catch_background_error(fnc): def wrapper(*args, **kwargs): this = args[0] try: @@ -57,32 +75,26 @@ def catchBackgroundError(fnc): this.REST.sendMessage('error?id={}'.format(kwargs.get('requestId', 'error')), {'error': '{}'.format(e)}) return wrapper + class OpenGnSysWorker(ServerWorker): name = 'opengnsys' - interface = None # Binded interface for OpenGnsys - loggedin = False # User session flag + interface = None # Bound interface for OpenGnsys + REST = None # REST object + logged_in = False # User session flag locked = {} random = None # Random string for secure connections length = 32 # Random string length - def checkSecret(self, server): - """ - Checks for received secret key and raise exception if it isn't valid. - """ - try: - if self.random != server.headers['Authorization']: - raise Exception('Unauthorized operation') - except Exception as e: - logger.error(e) - raise Exception(e) - def onActivation(self): """ Sends OGAgent activation notification to OpenGnsys server """ - self.cmd = None + t = 0 + # 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 - self.REST = REST(self.service.config.get('opengnsys', 'remote')) + url = self.service.config.get('opengnsys', 'remote') + self.REST = REST(url) # Get network interfaces until they are active or timeout (5 minutes) for t in range(0, 300): try: @@ -99,6 +111,29 @@ class OpenGnSysWorker(ServerWorker): # Raise error after timeout if not self.interface: raise e + # Loop to send initialization message + for t in range(0, 100): + 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}) + break + except: + # Trying to initialize on alternative server, if defined + # (used in "exam mode" from the University of Seville) + self.REST = REST(self.service.config.get('opengnsys', '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}) + break + except: + time.sleep(3) + # Raise error after timeout + if 0 < t < 100: + logger.debug('Successful connection after {} tries'.format(t)) + elif t == 100: + raise Exception('Initialization error: Cannot connect to remote server') # Delete marking files for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']: try: @@ -106,22 +141,19 @@ class OpenGnSysWorker(ServerWorker): except OSError: pass # Copy file "HostsFile.FirstOctetOfIPAddress" to "HostsFile", if it exists - # (used in "exam mode" of the University of Seville) - hostsFile = os.path.join(operations.get_etc_path(), 'hosts') - newHostsFile = hostsFile + '.' + self.interface.ip.split('.')[0] - if os.path.isfile(newHostsFile): - shutil.copyfile(newHostsFile, hostsFile) - # Generate random secret to send on activation - self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length)) - # Send initialization message - self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip, 'secret': self.random, 'ostype': operations.osType, 'osversion': operations.osVersion}) + # (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) 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.osType, 'osversion': operations.osVersion}) + self.REST.sendMessage('ogagent/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)) @@ -132,18 +164,19 @@ class OpenGnSysWorker(ServerWorker): """ user, sep, language = data.partition(',') logger.debug('Received login for {} with language {}'.format(user, language)) - self.loggedin = True - self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language, 'ostype': operations.osType, 'osversion': operations.osVersion}) + self.logged_in = True + self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language, + '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)) - self.loggedin = False + self.logged_in = False self.REST.sendMessage('ogagent/loggedout', {'ip': self.interface.ip, 'user': user}) - def process_ogclient(self, path, getParams, postParams, server): + 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 @@ -151,11 +184,11 @@ class OpenGnSysWorker(ServerWorker): * 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"], getParams, postParams) + 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"], getParams, postParams) + 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 @@ -169,13 +202,18 @@ class OpenGnSysWorker(ServerWorker): operation = getattr(self, 'ogclient_' + path[0]) except Exception: raise Exception('Message processor for "{}" not found'.format(path[0])) - return operation(path[1:], getParams, postParams) + return operation(path[1:], get_params, post_params) - def process_status(self, path, getParams, postParams, server): + def process_status(self, path, get_params, post_params, server): """ - Returns client status. + Returns client status (OS type or execution status) and login status + :param path: + :param get_params: + :param post_params: + :param server: + :return: JSON object {"status": "status_code", "loggedin": boolean} """ - res = {'status': '', 'loggedin': self.loggedin} + res = {'status': '', 'loggedin': self.logged_in} if platform.system() == 'Linux': # GNU/Linux # Check if it's OpenGnsys Client. if os.path.exists('/scripts/oginit'): @@ -194,66 +232,83 @@ class OpenGnSysWorker(ServerWorker): res['status'] = 'OSX' return res - def process_reboot(self, path, getParams, postParams, server): + @check_secret + def process_reboot(self, path, get_params, post_params, server): """ - Launches a system reboot operation. + 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') - self.checkSecret(server) - # Rebooting thread. + + # Rebooting thread def rebt(): operations.reboot() threading.Thread(target=rebt).start() return {'op': 'launched'} - def process_poweroff(self, path, getParams, postParams, server): + @check_secret + def process_poweroff(self, path, get_params, post_params, server): """ - Launches a system power off operation. + 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') - self.checkSecret(server) - # Powering off thread. + + # Powering off thread def pwoff(): time.sleep(2) operations.poweroff() threading.Thread(target=pwoff).start() return {'op': 'launched'} - def process_script(self, path, getParams, postParams, server): + @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') - self.checkSecret(server) - # Decoding script. - script = urllib.unquote(postParams.get('script').decode('base64')).decode('utf8') + # Decoding script + script = urllib.unquote(post_params.get('script').decode('base64')).decode('utf8') script = 'import subprocess; subprocess.check_output("""{}""",shell=True)'.format(script) # Executing script. - if postParams.get('client', 'false') == 'false': + if post_params.get('client', 'false') == 'false': thr = ScriptExecutorThread(script) thr.start() else: self.sendClientMessage('script', {'code': script}) return {'op': 'launched'} - def process_logoff(self, path, getParams, postParams, server): + @check_secret + def process_logoff(self, path, get_params, post_params, server): """ - Closes user session. + Closes user session """ logger.debug('Received logoff operation') - self.checkSecret(server) - # Sending log off message to OGAgent client. + # Sending log off message to OGAgent client self.sendClientMessage('logoff', {}) return {'op': 'sent to client'} - def process_popup(self, path, getParams, postParams, server): + @check_secret + def process_popup(self, path, get_params, post_params, server): """ - Shows a message popup on the user's session. + Shows a message popup on the user's session """ logger.debug('Received message operation') - self.checkSecret(server) - # Sending popup message to OGAgent client. - self.sendClientMessage('popup', postParams) + # Sending popup message to OGAgent client + self.sendClientMessage('popup', post_params) return {'op': 'launched'} def process_client_popup(self, params): diff --git a/admin/Sources/Clients/ogagent/src/opengnsys/operations.py b/admin/Sources/Clients/ogagent/src/opengnsys/operations.py index dcfa40cb..1a274b20 100644 --- a/admin/Sources/Clients/ogagent/src/opengnsys/operations.py +++ b/admin/Sources/Clients/ogagent/src/opengnsys/operations.py @@ -26,25 +26,25 @@ # 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 -''' +""" # pylint: disable=unused-wildcard-import,wildcard-import from __future__ import unicode_literals - import sys + # Importing platform operations and getting operating system data. if sys.platform == 'win32': from .windows.operations import * # @UnusedWildImport - osType = 'Windows' - osVersion = getWindowsVersion() + os_type = 'Windows' + os_version = getWindowsVersion() else: if sys.platform == 'darwin': from .macos.operations import * # @UnusedWildImport - osType = 'MacOS' - osVersion = getMacosVersion().replace(',','') + os_type = 'MacOS' + os_version = getMacosVersion().replace(',', '') else: from .linux.operations import * # @UnusedWildImport - osType = 'Linux' - osVersion = getLinuxVersion().replace(',','') + os_type = 'Linux' + os_version = getLinuxVersion().replace(',', '') |