source: ogAgent-Git/src/opengnsys/modules/server/OpenGnSys/__init__.py

main 5.1.1
Last change on this file was 16d24f3, checked in by Natalia Serrano <natalia.serrano@…>, 11 days ago

refs #1943 have REST object support TLS

  • Property mode set to 100644
File size: 17.1 KB
Line 
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3#
4# Copyright (c) 2014 Virtual Cable S.L.
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without modification,
8# are permitted provided that the following conditions are met:
9#
10#    * Redistributions of source code must retain the above copyright notice,
11#      this list of conditions and the following disclaimer.
12#    * Redistributions in binary form must reproduce the above copyright notice,
13#      this list of conditions and the following disclaimer in the documentation
14#      and/or other materials provided with the distribution.
15#    * Neither the name of Virtual Cable S.L. nor the names of its contributors
16#      may be used to endorse or promote products derived from this software
17#      without specific prior written permission.
18#
19# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29"""
30@author: Ramón M. Gómez, ramongomez at us dot es
31"""
32
33import base64
34import os
35import random
36import shutil
37import string
38import threading
39import time
40import urllib.error
41import urllib.parse
42import urllib.request
43
44from configparser import NoOptionError
45from opengnsys import REST, operations, VERSION
46from opengnsys.log import logger
47from opengnsys.jobmgr import JobMgr
48from opengnsys.workers import ServerWorker
49
50
51# Check authorization header decorator
52def check_secret(fnc):
53    """
54    Decorator to check for received secret key and raise exception if it isn't valid.
55    """
56    def wrapper(*args, **kwargs):
57        try:
58            this, path, get_params, post_params, server = args
59            # Accept "status" operation with no arguments or any function with Authorization header
60            if fnc.__name__ == 'process_status' and not get_params:
61                return fnc(*args, **kwargs)
62            elif this.random == server.headers['Authorization']:
63                return fnc(*args, **kwargs)
64            else:
65                raise Exception('Unauthorized operation')
66        except Exception as e:
67            logger.debug (str(e))
68            raise Exception(e)
69
70    return wrapper
71
72
73# Check if operation is permitted
74def execution_level(level):
75    def check_permitted(fnc):
76        def wrapper(*args, **kwargs):
77            levels = ['status', 'halt', 'full']
78            this = args[0]
79            try:
80                if levels.index(level) <= levels.index(this.exec_level):
81                    return fnc(*args, **kwargs)
82                else:
83                    raise Exception('Unauthorized operation')
84            except Exception as e:
85                logger.debug (str(e))
86                raise Exception(e)
87
88        return wrapper
89
90    return check_permitted
91
92
93class OpenGnSysWorker(ServerWorker):
94    name = 'opengnsys'  # Module name
95    interface = None  # Bound interface for OpenGnsys
96    REST = None  # REST object
97    user = []  # User sessions
98    session_type = ''  # User session type
99    random = None  # Random string for secure connections
100    length = 32  # Random string length
101    exec_level = None  # Execution level (permitted operations)
102    jobmgr = JobMgr()
103
104    ## pings ogcore
105    def mon (self):
106        n = 0
107        while True:
108            time.sleep (1)
109            n += 1
110            if not n % 10:
111                body = {
112                    "iph": self.interface.ip,
113                    "timestamp": int (time.time()),
114                }
115                logger.debug (f'about to send ping ({body})')
116                self.REST.sendMessage ('clients/status/webhook', body)
117
118    def onActivation(self):
119        """
120        Sends OGAgent activation notification to OpenGnsys server
121        """
122        if os.path.exists ('/scripts/oginit'):
123            ## estamos en oglive, este modulo no debe cargarse
124            ## esta lógica la saco de src/opengnsys/linux/operations.py, donde hay un if similar
125            raise Exception ('Refusing to load within an ogLive image')
126
127        e = None  # Error info
128        t = 0  # Count of time
129        # Generate random secret to send on activation
130        self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length))
131        # Ensure cfg has required configuration variables or an exception will be thrown
132        try:
133            url      = self.service.config.get(self.name, 'remote')
134            ca_file  = self.service.config.get(self.name, 'ca')
135            crt_file = self.service.config.get(self.name, 'crt')
136            key_file = self.service.config.get(self.name, 'key')
137        except NoOptionError as e:
138            logger.error("Configuration error: {}".format(e))
139            raise e
140        self.REST = REST (url, ca_file=ca_file, crt_file=crt_file, key_file=key_file)
141        # Execution level ('full' by default)
142        try:
143            self.exec_level = self.service.config.get(self.name, 'level')
144        except NoOptionError:
145            self.exec_level = 'full'
146        # Get network interfaces until they are active or timeout (5 minutes)
147        for t in range(0, 300):
148            try:
149                # Get the first network interface
150                self.interface = list(operations.getNetworkInfo())[0]
151            except Exception as e:
152                # Wait 1 sec. and retry
153                logger.warn (e)
154                time.sleep(1)
155            finally:
156                # Exit loop if interface is active
157                if self.interface:
158                    if t > 0:
159                        logger.debug("Fetch connection data after {} tries".format(t))
160                    break
161        # Raise error after timeout
162        if not self.interface:
163            ## UnboundLocalError: cannot access local variable 'e' where it is not associated with a value
164            raise e
165
166        # Loop to send initialization message
167        init_retries = 100
168        for t in range(0, init_retries):
169            try:
170                try:
171                    self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
172                                                              'secret': self.random, 'ostype': operations.os_type,
173                                                              'osversion': operations.os_version,
174                                                              'agent_version': VERSION})
175                    break
176                except Exception as e:
177                    logger.warn (str (e))
178                    # Trying to initialize on alternative server, if defined
179                    # (used in "exam mode" from the University of Seville)
180                    self.REST = REST(self.service.config.get(self.name, 'altremote'), ca_file=ca_file, crt_file=crt_file, key_file=key_file)
181                    self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
182                                                              'secret': self.random, 'ostype': operations.os_type,
183                                                              'osversion': operations.os_version, 'alt_url': True,
184                                                              'agent_version': VERSION})
185                    break
186            except Exception as e:
187                logger.warn (str (e))
188                time.sleep(3)
189        # Raise error after timeout
190        if t < init_retries-1:
191            logger.debug('Successful connection after {} tries'.format(t))
192        elif t == init_retries-1:
193            raise Exception('Initialization error: Cannot connect to remote server')
194
195        # Delete marking files
196        for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']:
197            try:
198                os.remove(os.sep + f)
199            except OSError:
200                pass
201        # Copy file "HostsFile.FirstOctetOfIPAddress" to "HostsFile", if it exists
202        # (used in "exam mode" from the University of Seville)
203        hosts_file = os.path.join(operations.get_etc_path(), 'hosts')
204        new_hosts_file = hosts_file + '.' + self.interface.ip.split('.')[0]
205        if os.path.isfile(new_hosts_file):
206            shutil.copyfile(new_hosts_file, hosts_file)
207
208        threading.Thread (name='monitoring_thread', target=self.mon, daemon=True).start()
209
210        logger.debug ('onActivation ok')
211
212    def onDeactivation(self):
213        """
214        Sends OGAgent stopping notification to OpenGnsys server
215        """
216        logger.debug('onDeactivation')
217        self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip,
218                                                  'ostype': operations.os_type, 'osversion': operations.os_version})
219
220    def onLogin(self, data):
221        """
222        Sends session login notification to OpenGnsys server
223        """
224        user, language, self.session_type = tuple(data.split(','))
225        logger.debug('Received login for {0} using {2} with language {1}'.format(user, language, self.session_type))
226        self.user.append(user)
227        self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language,
228                                                   'session': self.session_type,
229                                                   'ostype': operations.os_type, 'osversion': operations.os_version})
230
231    def onLogout(self, user):
232        """
233        Sends session logout notification to OpenGnsys server
234        """
235        logger.debug('Received logout for {}'.format(user))
236        try:
237            self.user.pop()
238        except IndexError:
239            pass
240        self.REST.sendMessage('ogagent/loggedout', {'ip': self.interface.ip, 'user': user})
241
242    def process_ogclient(self, path, get_params, post_params, server):
243        """
244        This method can be overridden to provide your own message processor, or better you can
245        implement a method that is called exactly as "process_" + path[0] (module name has been removed from path
246        array) and this default processMessage will invoke it
247        * Example:
248            Imagine this invocation url (no matter if GET or POST): http://example.com:9999/Sample/mazinger/Z
249            The HTTP Server will remove "Sample" from path, parse arguments and invoke this method as this:
250            module.processMessage(["mazinger","Z"], get_params, post_params)
251
252            This method will process "mazinger", and look for a "self" method that is called "process_mazinger",
253            and invoke it this way:
254               return self.process_mazinger(["Z"], get_params, post_params)
255
256            In the case path is empty (that is, the path is composed only by the module name, like in
257            "http://example.com/Sample", the "process" method will be invoked directly
258
259            The methods must return data that can be serialized to json (i.e. Objects are not serializable to json,
260            basic type are)
261        """
262        if not path:
263            return "ok"
264        try:
265            operation = getattr(self, 'ogclient_' + path[0])
266        except Exception:
267            raise Exception('Message processor for "{}" not found'.format(path[0]))
268        return operation(path[1:], get_params, post_params)
269
270    # Warning: the order of the decorators matters
271    @execution_level('status')
272    @check_secret
273    def process_status(self, path, get_params, post_params, server):
274        """
275        Returns client status (OS type or execution status) and login status
276        :param path:
277        :param get_params: optional parameter "detail" to show extended status
278        :param post_params:
279        :param server:
280        :return: JSON object {"status": "status_code", "loggedin": boolean, ...}
281        """
282        st = {'linux': 'LNX', 'macos': 'OSX', 'windows': 'WIN'}
283        try:
284            # Standard status
285            res = {'status': st[operations.os_type.lower()], 'loggedin': len(self.user) > 0,
286                   'session': self.session_type}
287            # Detailed status
288            if get_params.get('detail', 'false') == 'true':
289                res.update({'agent_version': VERSION, 'os_version': operations.os_version, 'sys_load': os.getloadavg()})
290                if res['loggedin']:
291                    res.update({'sessions': len(self.user), 'current_user': self.user[-1]})
292        except KeyError:
293            # Unknown operating system
294            res = {'status': 'UNK'}
295        return res
296
297    @execution_level('halt')
298    @check_secret
299    def process_reboot(self, path, get_params, post_params, server):
300        """
301        Launches a system reboot operation
302        :param path:
303        :param get_params:
304        :param post_params:
305        :param server: authorization header
306        :return: JSON object {"op": "launched"}
307        """
308        logger.debug('Received reboot operation')
309
310        # Rebooting thread
311        def rebt():
312            operations.reboot()
313        threading.Thread(target=rebt).start()
314        return {'op': 'launched'}
315
316    @execution_level('halt')
317    @check_secret
318    def process_poweroff(self, path, get_params, post_params, server):
319        """
320        Launches a system power off operation
321        :param path:
322        :param get_params:
323        :param post_params:
324        :param server: authorization header
325        :return: JSON object {"op": "launched"}
326        """
327        logger.debug('Received poweroff operation')
328
329        # Powering off thread
330        def pwoff():
331            time.sleep(2)
332            operations.poweroff()
333        threading.Thread(target=pwoff).start()
334        return {'op': 'launched'}
335
336    @execution_level('full')
337    @check_secret
338    def process_script(self, path, get_params, post_params, server):
339        """
340        Processes an script execution (script should be encoded in base64)
341        :param path:
342        :param get_params:
343        :param post_params: JSON object {"script": "commands"}
344        :param server: authorization header
345        :return: JSON object {"op": "launched"}
346        """
347        logger.debug('Processing script request')
348        # Decoding script
349        script = urllib.parse.unquote(base64.b64decode(post_params.get('script')).decode('utf-8'))
350        logger.debug('received script "{}"'.format(script))
351
352        if post_params.get('client', 'false') == 'false':
353            jobid = self.jobmgr.launch_job (script, False)
354            return {'op': 'launched', 'jobid': jobid}
355
356        else:   ## post_params.get('client') is not 'false'
357            ## send script as-is
358            self.sendClientMessage('script', {'code': script})
359            #return {'op': 'launched', 'jobid': jobid}       ## TODO obtain jobid generated at the client (can it be done?)
360            return {'op': 'launched'}
361
362    @execution_level('full')
363    @check_secret
364    def process_terminatescript(self, path, get_params, post_params, server):
365        jobid = post_params.get('jobid', None)
366        logger.debug('Processing terminate_script request, jobid "{}"'.format (jobid))
367        if jobid is None:
368            return {}
369        self.sendClientMessage('terminatescript', {'jobid': jobid})
370        self.jobmgr.terminate_job (jobid)
371        return {}
372
373    @execution_level('full')
374    @check_secret
375    def process_preparescripts(self, path, get_params, post_params, server):
376        logger.debug('Processing preparescripts request')
377        self.st = self.jobmgr.prepare_jobs()
378        logger.debug('Sending preparescripts to client')
379        self.sendClientMessage('preparescripts', None)
380        return {}
381
382    def process_client_preparescripts(self, params):
383        logger.debug('Processing preparescripts message from client')
384        for p in params:
385            #logger.debug ('p "{}"'.format(p))
386            self.st.append (p)
387
388    @execution_level('full')
389    @check_secret
390    def process_getscripts(self, path, get_params, post_params, server):
391        logger.debug('Processing getscripts request')
392        return self.st
393
394    @execution_level('full')
395    @check_secret
396    def process_logoff(self, path, get_params, post_params, server):
397        """
398        Closes user session
399        """
400        logger.debug('Received logoff operation')
401        # Sending log off message to OGAgent client
402        self.sendClientMessage('logoff', {})
403        return {'op': 'sent to client'}
404
405    @execution_level('full')
406    @check_secret
407    def process_popup(self, path, get_params, post_params, server):
408        """
409        Shows a message popup on the user's session
410        """
411        logger.debug('Received message operation')
412        # Sending popup message to OGAgent client
413        self.sendClientMessage('popup', post_params)
414        return {'op': 'launched'}
415
416    def process_client_popup(self, params):
417        self.REST.sendMessage('popup_done', params)
Note: See TracBrowser for help on using the repository browser.