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

decorare-oglive-methodsexec-ogbrowserfix-urlfixes-winlgromero-filebeatlog-sess-lenmainmodulesnew-browserno-ptt-paramno-tlsogadmcliogadmclient-statusogagent-jobsogagent-macosogcore1oggitoglogoglog2override-moduleping1ping2ping3ping4py3-winpython3report-progresssched-tasktlsunification2unification3versionswindows-fixes
Last change on this file since 2b25718 was be263c6, checked in by Ramón M. Gómez <ramongomez@…>, 5 years ago

#992: Cherry-pick commit af35fd9.

ogAgent sends the session type when user logs in.

  • Property mode set to 100644
File size: 15.0 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
33
34import base64
35import os
36import random
37import shutil
38import string
39import threading
40import time
41import urllib.error
42import urllib.parse
43import urllib.request
44
45from configparser import NoOptionError
46from opengnsys import REST, operations, VERSION
47from opengnsys.log import logger
48from opengnsys.scriptThread import ScriptExecutorThread
49from opengnsys.workers import ServerWorker
50
51
52# Check authorization header decorator
53def check_secret(fnc):
54    """
55    Decorator to check for received secret key and raise exception if it isn't valid.
56    """
57    def wrapper(*args, **kwargs):
58        try:
59            this, path, get_params, post_params, server = args
60            # Accept "status" operation with no arguments or any function with Authorization header
61            if fnc.__name__ == 'process_status' and not get_params:
62                return fnc(*args, **kwargs)
63            elif this.random == server.headers['Authorization']:
64                return fnc(*args, **kwargs)
65            else:
66                raise Exception('Unauthorized operation')
67        except Exception as e:
68            logger.error(e)
69            raise Exception(e)
70
71    return wrapper
72
73
74# Check if operation is permitted
75def execution_level(level):
76    def check_permitted(fnc):
77        def wrapper(*args, **kwargs):
78            levels = ['status', 'halt', 'full']
79            this = args[0]
80            try:
81                if levels.index(level) <= levels.index(this.exec_level):
82                    return fnc(*args, **kwargs)
83                else:
84                    raise Exception('Unauthorized operation')
85            except Exception as e:
86                logger.error(e)
87                raise Exception(e)
88
89        return wrapper
90
91    return check_permitted
92
93
94# Error handler decorator.
95def catch_background_error(fnc):
96    def wrapper(*args, **kwargs):
97        this = args[0]
98        try:
99            fnc(*args, **kwargs)
100        except Exception as e:
101            this.REST.sendMessage('error?id={}'.format(kwargs.get('requestId', 'error')), {'error': '{}'.format(e)})
102    return wrapper
103
104
105class OpenGnSysWorker(ServerWorker):
106    name = 'opengnsys'  # Module name
107    interface = None  # Bound interface for OpenGnsys
108    REST = None  # REST object
109    user = []  # User sessions
110    session_type = ''  # User session type
111    random = None  # Random string for secure connections
112    length = 32  # Random string length
113    exec_level = None  # Execution level (permitted operations)
114
115    def onActivation(self):
116        """
117        Sends OGAgent activation notification to OpenGnsys server
118        """
119        e = None  # Error info
120        t = 0  # Count of time
121        # Generate random secret to send on activation
122        self.random = ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(self.length))
123        # Ensure cfg has required configuration variables or an exception will be thrown
124        try:
125            url = self.service.config.get('opengnsys', 'remote')
126        except NoOptionError as e:
127            logger.error("Configuration error: {}".format(e))
128            raise e
129        self.REST = REST(url)
130        # Execution level ('full' by default)
131        try:
132            self.exec_level = self.service.config.get('opengnsys', 'level')
133        except NoOptionError:
134            self.exec_level = 'full'
135        # Get network interfaces until they are active or timeout (5 minutes)
136        for t in range(0, 300):
137            try:
138                # Get the first network interface
139                self.interface = list(operations.getNetworkInfo())[0]
140            except Exception as e:
141                # Wait 1 sec. and retry
142                time.sleep(1)
143            finally:
144                # Exit loop if interface is active
145                if self.interface:
146                    if t > 0:
147                        logger.debug("Fetch connection data after {} tries".format(t))
148                    break
149        # Raise error after timeout
150        if not self.interface:
151            raise e
152        # Loop to send initialization message
153        for t in range(0, 100):
154            try:
155                try:
156                    self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
157                                                              'secret': self.random, 'ostype': operations.os_type,
158                                                              'osversion': operations.os_version,
159                                                              'agent_version': VERSION})
160                    break
161                except:
162                    # Trying to initialize on alternative server, if defined
163                    # (used in "exam mode" from the University of Seville)
164                    self.REST = REST(self.service.config.get('opengnsys', 'altremote'))
165                    self.REST.sendMessage('ogagent/started', {'mac': self.interface.mac, 'ip': self.interface.ip,
166                                                              'secret': self.random, 'ostype': operations.os_type,
167                                                              'osversion': operations.os_version, 'alt_url': True,
168                                                              'agent_version': VERSION})
169                    break
170            except:
171                time.sleep(3)
172        # Raise error after timeout
173        if 0 < t < 100:
174            logger.debug('Successful connection after {} tries'.format(t))
175        elif t == 100:
176            raise Exception('Initialization error: Cannot connect to remote server')
177        # Delete marking files
178        for f in ['ogboot.me', 'ogboot.firstboot', 'ogboot.secondboot']:
179            try:
180                os.remove(os.sep + f)
181            except OSError:
182                pass
183        # Copy file "HostsFile.FirstOctetOfIPAddress" to "HostsFile", if it exists
184        # (used in "exam mode" from the University of Seville)
185        hosts_file = os.path.join(operations.get_etc_path(), 'hosts')
186        new_hosts_file = hosts_file + '.' + self.interface.ip.split('.')[0]
187        if os.path.isfile(new_hosts_file):
188            shutil.copyfile(new_hosts_file, hosts_file)
189
190    def onDeactivation(self):
191        """
192        Sends OGAgent stopping notification to OpenGnsys server
193        """
194        logger.debug('onDeactivation')
195        self.REST.sendMessage('ogagent/stopped', {'mac': self.interface.mac, 'ip': self.interface.ip,
196                                                  'ostype': operations.os_type, 'osversion': operations.os_version})
197
198    def processClientMessage(self, message, data):
199        logger.debug('Got OpenGnsys message from client: {}, data {}'.format(message, data))
200
201    def onLogin(self, data):
202        """
203        Sends session login notification to OpenGnsys server
204        """
205        user, language, self.session_type = tuple(data.split(','))
206        logger.debug('Received login for {0} using {2} with language {1}'.format(user, language, self.session_type))
207        self.user.append(user)
208        self.REST.sendMessage('ogagent/loggedin', {'ip': self.interface.ip, 'user': user, 'language': language,
209                                                   'session': self.session_type,
210                                                   'ostype': operations.os_type, 'osversion': operations.os_version})
211
212    def onLogout(self, user):
213        """
214        Sends session logout notification to OpenGnsys server
215        """
216        logger.debug('Received logout for {}'.format(user))
217        try:
218            self.user.pop()
219        except IndexError:
220            pass
221        self.REST.sendMessage('ogagent/loggedout', {'ip': self.interface.ip, 'user': user})
222
223    def process_ogclient(self, path, get_params, post_params, server):
224        """
225        This method can be overridden to provide your own message processor, or better you can
226        implement a method that is called exactly as "process_" + path[0] (module name has been removed from path
227        array) and this default processMessage will invoke it
228        * Example:
229            Imagine this invocation url (no matter if GET or POST): http://example.com:9999/Sample/mazinger/Z
230            The HTTP Server will remove "Sample" from path, parse arguments and invoke this method as this:
231            module.processMessage(["mazinger","Z"], get_params, post_params)
232
233            This method will process "mazinger", and look for a "self" method that is called "process_mazinger",
234            and invoke it this way:
235               return self.process_mazinger(["Z"], get_params, post_params)
236
237            In the case path is empty (that is, the path is composed only by the module name, like in
238            "http://example.com/Sample", the "process" method will be invoked directly
239
240            The methods must return data that can be serialized to json (i.e. Objects are not serializable to json,
241            basic type are)
242        """
243        if not path:
244            return "ok"
245        try:
246            operation = getattr(self, 'ogclient_' + path[0])
247        except Exception:
248            raise Exception('Message processor for "{}" not found'.format(path[0]))
249        return operation(path[1:], get_params, post_params)
250
251    # Warning: the order of the decorators matters
252    @execution_level('status')
253    @check_secret
254    def process_status(self, path, get_params, post_params, server):
255        """
256        Returns client status (OS type or execution status) and login status
257        :param path:
258        :param get_params: optional parameter "detail" to show extended status
259        :param post_params:
260        :param server:
261        :return: JSON object {"status": "status_code", "loggedin": boolean, ...}
262        """
263        st = {'linux': 'LNX', 'macos': 'OSX', 'windows': 'WIN'}
264        try:
265            # Standard status
266            res = {'status': st[operations.os_type.lower()], 'loggedin': len(self.user) > 0,
267                   'session': self.session_type}
268            # Detailed status
269            if get_params.get('detail', 'false') == 'true':
270                res.update({'agent_version': VERSION, 'os_version': operations.os_version, 'sys_load': os.getloadavg()})
271                if res['loggedin']:
272                    res.update({'sessions': len(self.user), 'current_user': self.user[-1]})
273        except KeyError:
274            # Unknown operating system
275            res = {'status': 'UNK'}
276        return res
277
278    @execution_level('halt')
279    @check_secret
280    def process_reboot(self, path, get_params, post_params, server):
281        """
282        Launches a system reboot operation
283        :param path:
284        :param get_params:
285        :param post_params:
286        :param server: authorization header
287        :return: JSON object {"op": "launched"}
288        """
289        logger.debug('Received reboot operation')
290
291        # Rebooting thread
292        def rebt():
293            operations.reboot()
294        threading.Thread(target=rebt).start()
295        return {'op': 'launched'}
296
297    @execution_level('halt')
298    @check_secret
299    def process_poweroff(self, path, get_params, post_params, server):
300        """
301        Launches a system power off 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 poweroff operation')
309
310        # Powering off thread
311        def pwoff():
312            time.sleep(2)
313            operations.poweroff()
314        threading.Thread(target=pwoff).start()
315        return {'op': 'launched'}
316
317    @execution_level('full')
318    @check_secret
319    def process_script(self, path, get_params, post_params, server):
320        """
321        Processes an script execution (script should be encoded in base64)
322        :param path:
323        :param get_params:
324        :param post_params: JSON object {"script": "commands"}
325        :param server: authorization header
326        :return: JSON object {"op": "launched"}
327        """
328        logger.debug('Processing script request')
329        # Decoding script (Windows scripts need a subprocess call per line)
330        script = urllib.parse.unquote(base64.b64decode(post_params.get('script')).decode('utf-8'))
331        if operations.os_type == 'Windows':
332            script = 'import subprocess; {0}'.format(
333                ';'.join(['subprocess.check_output({0},shell=True)'.format(repr(c)) for c in script.split('\n')]))
334        else:
335            script = 'import subprocess; subprocess.check_output("""{0}""",shell=True)'.format(script)
336        # Executing script.
337        if post_params.get('client', 'false') == 'false':
338            thr = ScriptExecutorThread(script)
339            thr.start()
340        else:
341            self.sendClientMessage('script', {'code': script})
342        return {'op': 'launched'}
343
344    @execution_level('full')
345    @check_secret
346    def process_logoff(self, path, get_params, post_params, server):
347        """
348        Closes user session
349        """
350        logger.debug('Received logoff operation')
351        # Sending log off message to OGAgent client
352        self.sendClientMessage('logoff', {})
353        return {'op': 'sent to client'}
354
355    @execution_level('full')
356    @check_secret
357    def process_popup(self, path, get_params, post_params, server):
358        """
359        Shows a message popup on the user's session
360        """
361        logger.debug('Received message operation')
362        # Sending popup message to OGAgent client
363        self.sendClientMessage('popup', post_params)
364        return {'op': 'launched'}
365
366    def process_client_popup(self, params):
367        self.REST.sendMessage('popup_done', params)
Note: See TracBrowser for help on using the repository browser.