OpenSecurity/bin/opensecurity_client_restful_server.py
author BarthaM@N3SIM1218.D03.arc.local
Mon, 19 May 2014 17:19:47 +0100
changeset 163 e7fbdaabd0bc
parent 160 c014a9db4b55
child 164 b6b9dc0ed2ac
permissions -rwxr-xr-x
OpenSecurity/bin/opensecurity_client_restful_server.py added join to wait for netmount /umount thread finish
om@13
     1
#!/bin/env python
om@13
     2
# -*- coding: utf-8 -*-
om@13
     3
om@13
     4
# ------------------------------------------------------------
om@13
     5
# opensecurity_client_restful_server
om@13
     6
# 
om@13
     7
# the OpenSecurity client RESTful server
om@13
     8
#
om@13
     9
# Autor: Oliver Maurhart, <oliver.maurhart@ait.ac.at>
om@13
    10
#
om@13
    11
# Copyright (C) 2013 AIT Austrian Institute of Technology
om@13
    12
# AIT Austrian Institute of Technology GmbH
om@13
    13
# Donau-City-Strasse 1 | 1220 Vienna | Austria
om@13
    14
# http://www.ait.ac.at
om@13
    15
#
om@13
    16
# This program is free software; you can redistribute it and/or
om@13
    17
# modify it under the terms of the GNU General Public License
om@13
    18
# as published by the Free Software Foundation version 2.
om@13
    19
# 
om@13
    20
# This program is distributed in the hope that it will be useful,
om@13
    21
# but WITHOUT ANY WARRANTY; without even the implied warranty of
om@13
    22
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
om@13
    23
# GNU General Public License for more details.
om@13
    24
# 
om@13
    25
# You should have received a copy of the GNU General Public License
om@13
    26
# along with this program; if not, write to the Free Software
om@13
    27
# Foundation, Inc., 51 Franklin Street, Fifth Floor, 
om@13
    28
# Boston, MA  02110-1301, USA.
om@13
    29
# ------------------------------------------------------------
om@13
    30
om@13
    31
om@13
    32
# ------------------------------------------------------------
om@13
    33
# imports
om@13
    34
oliver@142
    35
import getpass
oliver@134
    36
import json
om@13
    37
import os
om@13
    38
import os.path
oliver@142
    39
import platform
oliver@136
    40
import socket
om@13
    41
import subprocess
om@13
    42
import sys
om@29
    43
import urllib
om@29
    44
import urllib2
om@13
    45
import web
oliver@156
    46
import threading
oliver@156
    47
import time
oliver@156
    48
import string
BarthaM@151
    49
BarthaM@151
    50
from opensecurity_util import logger, setupLogger, OpenSecurityException
BarthaM@151
    51
if sys.platform == 'win32' or sys.platform == 'cygwin':
BarthaM@151
    52
    from cygwin import Cygwin
om@13
    53
om@13
    54
# local
oliver@144
    55
import __init__ as opensecurity
om@13
    56
om@13
    57
om@13
    58
# ------------------------------------------------------------
om@13
    59
# const
om@13
    60
om@13
    61
om@13
    62
"""All the URLs we know mapping to class handler"""
om@13
    63
opensecurity_urls = (
om@13
    64
    '/credentials',             'os_credentials',
oliver@134
    65
    '/keyfile',                 'os_keyfile',
oliver@142
    66
    '/log',                     'os_log',
om@29
    67
    '/notification',            'os_notification',
om@13
    68
    '/password',                'os_password',
BarthaM@151
    69
    '/netmount',                'os_netmount',
BarthaM@151
    70
    '/netumount',               'os_netumount',
om@13
    71
    '/',                        'os_root'
om@13
    72
)
om@13
    73
om@13
    74
om@13
    75
# ------------------------------------------------------------
oliver@136
    76
# vars
oliver@136
    77
oliver@136
    78
oliver@136
    79
"""The REST server object"""
oliver@136
    80
server = None
oliver@136
    81
oliver@136
    82
oliver@136
    83
# ------------------------------------------------------------
om@13
    84
# code
om@13
    85
om@13
    86
oliver@134
    87
class os_credentials:
om@31
    88
om@13
    89
    """OpenSecurity '/credentials' handler.
om@13
    90
    
om@13
    91
    This is called on GET /credentials?text=TEXT.
om@13
    92
    Ideally this should pop up a user dialog to insert his
om@13
    93
    credentials based the given TEXT.
om@13
    94
    """
om@13
    95
    
om@13
    96
    def GET(self):
om@13
    97
        
om@13
    98
        # pick the arguments
om@13
    99
        args = web.input()
om@13
   100
        
om@29
   101
        # we _need_ a text
om@13
   102
        if not "text" in args:
om@29
   103
            raise web.badrequest('no text given')
om@13
   104
        
oliver@134
   105
        # remember remote ip
oliver@134
   106
        remote_ip = web.ctx.environ['REMOTE_ADDR']
oliver@134
   107
oliver@134
   108
        # create the process which queries the user
oliver@134
   109
        dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
om@29
   110
        process_command = [sys.executable, dlg_image, 'credentials', args.text]
oliver@134
   111
        process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)        
om@13
   112
        
oliver@134
   113
        # run process result handling in seprate thread (not to block main one)
oliver@136
   114
        bouncer = ProcessResultBouncer(process, remote_ip, '/credentials')
oliver@134
   115
        bouncer.start()
oliver@134
   116
         
oliver@134
   117
        return 'user queried for credentials'
oliver@134
   118
oliver@134
   119
oliver@134
   120
class os_keyfile:
oliver@134
   121
oliver@134
   122
    """OpenSecurity '/keyfile' handler.
oliver@134
   123
    
oliver@134
   124
    This is called on GET /keyfile?text=TEXT.
oliver@134
   125
    Ideally this should pop up a user dialog to insert his
oliver@134
   126
    password along with a keyfile.
oliver@134
   127
    """
oliver@134
   128
    
oliver@134
   129
    def GET(self):
oliver@134
   130
        
oliver@134
   131
        # pick the arguments
oliver@134
   132
        args = web.input()
oliver@134
   133
        
oliver@134
   134
        # we _need_ a text
oliver@134
   135
        if not "text" in args:
oliver@134
   136
            raise web.badrequest('no text given')
oliver@134
   137
            
oliver@134
   138
        # remember remote ip
oliver@134
   139
        remote_ip = web.ctx.environ['REMOTE_ADDR']
oliver@134
   140
        
oliver@134
   141
        # create the process which queries the user
oliver@134
   142
        dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
oliver@134
   143
        process_command = [sys.executable, dlg_image, 'keyfile', args.text]
oliver@134
   144
        process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)        
oliver@134
   145
        
oliver@134
   146
        # run process result handling in seprate thread (not to block main one)
oliver@136
   147
        bouncer = ProcessResultBouncer(process, remote_ip, '/keyfile')
oliver@134
   148
        bouncer.start()
oliver@134
   149
         
oliver@134
   150
        return 'user queried for password and keyfile'
om@13
   151
om@13
   152
oliver@142
   153
class os_log:
oliver@142
   154
oliver@142
   155
    """OpenSecurity '/log' handler.
oliver@142
   156
    
oliver@142
   157
    This is called on GET or POST on the log function /log
oliver@142
   158
    """
oliver@142
   159
    
oliver@142
   160
    def GET(self):
oliver@142
   161
        
oliver@142
   162
        # pick the arguments
oliver@142
   163
        self.POST()
oliver@142
   164
oliver@142
   165
oliver@142
   166
    def POST(self):
oliver@142
   167
        
oliver@142
   168
        # pick the arguments
oliver@142
   169
        args = web.input()
oliver@142
   170
        args['user'] = getpass.getuser()
oliver@142
   171
        args['system'] = platform.node() + " " + platform.system() + " " + platform.release()
oliver@142
   172
BarthaM@163
   173
        # bounce log data
BarthaM@163
   174
        url_addr = 'http://GIMME-SERVER-TO-LOG-TO/log'
oliver@142
   175
BarthaM@163
   176
        # by provided a 'data' we turn this into a POST statement
BarthaM@163
   177
        d = urllib.urlencode(args)
BarthaM@163
   178
        req = urllib2.Request(url_addr, d)
BarthaM@163
   179
        try:
BarthaM@163
   180
            res = urllib2.urlopen(req)
BarthaM@163
   181
        except:
BarthaM@163
   182
            print('failed to contact: ' + url_addr)
BarthaM@163
   183
            print('log data: ' + d)
BarthaM@163
   184
            return "Failed"
BarthaM@163
   185
         
oliver@142
   186
        return "Ok"
oliver@142
   187
oliver@142
   188
om@29
   189
class os_notification:
oliver@134
   190
om@29
   191
    """OpenSecurity '/notification' handler.
om@29
   192
    
om@29
   193
    This is called on GET /notification?msgtype=TYPE&text=TEXT.
om@29
   194
    This will pop up an OpenSecurity notifcation window
om@29
   195
    """
om@29
   196
    
om@29
   197
    def GET(self):
om@29
   198
        
om@29
   199
        # pick the arguments
om@29
   200
        args = web.input()
om@29
   201
        
om@29
   202
        # we _need_ a type
om@29
   203
        if not "msgtype" in args:
om@29
   204
            raise web.badrequest('no msgtype given')
om@29
   205
            
oliver@134
   206
        if not args.msgtype in ['information', 'warning', 'critical']:
om@29
   207
            raise web.badrequest('Unknown value for msgtype')
om@29
   208
            
om@29
   209
        # we _need_ a text
om@29
   210
        if not "text" in args:
om@29
   211
            raise web.badrequest('no text given')
om@29
   212
            
om@29
   213
        # invoke the user dialog as a subprocess
om@29
   214
        dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.py')
om@29
   215
        process_command = [sys.executable, dlg_image, 'notification-' + args.msgtype, args.text]
om@29
   216
        process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)
oliver@134
   217
om@29
   218
        return "Ok"
om@29
   219
om@29
   220
om@13
   221
class os_password:
oliver@134
   222
om@13
   223
    """OpenSecurity '/password' handler.
om@13
   224
    
om@13
   225
    This is called on GET /password?text=TEXT.
om@13
   226
    Ideally this should pop up a user dialog to insert his
om@13
   227
    password based device name.
om@13
   228
    """
om@13
   229
    
om@13
   230
    def GET(self):
om@13
   231
        
om@13
   232
        # pick the arguments
om@13
   233
        args = web.input()
om@13
   234
        
om@29
   235
        # we _need_ a text
om@13
   236
        if not "text" in args:
om@29
   237
            raise web.badrequest('no text given')
om@13
   238
            
om@29
   239
        # remember remote ip
om@29
   240
        remote_ip = web.ctx.environ['REMOTE_ADDR']
om@29
   241
        
oliver@134
   242
        # create the process which queries the user
oliver@134
   243
        dlg_image = os.path.join(sys.path[0], 'opensecurity_dialog.pyw')
oliver@134
   244
        process_command = [sys.executable, dlg_image, 'password', args.text]
oliver@134
   245
        process = subprocess.Popen(process_command, shell = False, stdout = subprocess.PIPE)        
om@29
   246
        
oliver@134
   247
        # run process result handling in seprate thread (not to block main one)
oliver@136
   248
        bouncer = ProcessResultBouncer(process, remote_ip, '/password')
oliver@134
   249
        bouncer.start()
oliver@134
   250
        
oliver@134
   251
        return 'user queried for password'
om@13
   252
BarthaM@151
   253
# handles netumount request                    
BarthaM@151
   254
class MountNetworkDriveHandler(threading.Thread): 
BarthaM@151
   255
    drive = None
BarthaM@151
   256
    resource = None
BarthaM@151
   257
    
BarthaM@151
   258
    def __init__(self, drv, net_path):
BarthaM@151
   259
        threading.Thread.__init__(self)
BarthaM@151
   260
        self.drive = drv
BarthaM@151
   261
        self.networkPath = net_path
BarthaM@151
   262
    
BarthaM@151
   263
    def run(self):
BarthaM@151
   264
        #Check for drive availability
BarthaM@151
   265
        if os.path.exists(self.drive):
BarthaM@151
   266
            logger.error("Drive letter is already in use: " + self.drive)
BarthaM@151
   267
            return 1
BarthaM@151
   268
        
BarthaM@151
   269
        #Check for network resource availability
BarthaM@151
   270
        retry = 5
BarthaM@151
   271
        while not os.path.exists(self.networkPath):
BarthaM@151
   272
            time.sleep(1)
BarthaM@151
   273
            if retry == 0:
BarthaM@151
   274
                return 1
BarthaM@151
   275
            logger.info("Path not accessible: " + self.networkPath + " retrying")
BarthaM@151
   276
            retry-=1
BarthaM@151
   277
    
BarthaM@151
   278
        command = 'USE ' + self.drive + ' ' + self.networkPath + ' /PERSISTENT:NO'
BarthaM@151
   279
    
BarthaM@151
   280
        result = Cygwin.checkResult(Cygwin.execute('C:\\Windows\\system32\\NET', command))
BarthaM@151
   281
        if string.find(result[1], 'successfully',) == -1:
BarthaM@151
   282
            logger.error("Failed: NET " + command)
BarthaM@151
   283
            return 1
BarthaM@151
   284
        return 0
BarthaM@151
   285
BarthaM@151
   286
class os_netmount:
BarthaM@151
   287
    
BarthaM@151
   288
    """OpenSecurity '/netmount' handler"""
BarthaM@151
   289
    
BarthaM@151
   290
    def GET(self):
BarthaM@151
   291
        # pick the arguments
BarthaM@151
   292
        args = web.input()
BarthaM@151
   293
        
BarthaM@151
   294
        # we _need_ a net_resource
BarthaM@151
   295
        if not "net_resource" in args:
BarthaM@151
   296
            raise web.badrequest('no net_resource given')
BarthaM@151
   297
        
BarthaM@151
   298
        # we _need_ a drive_letter
BarthaM@151
   299
        if not "drive_letter" in args:
BarthaM@151
   300
            raise web.badrequest('no drive_letter given')
BarthaM@151
   301
BarthaM@151
   302
        driveHandler = MountNetworkDriveHandler(args['drive_letter'], args['net_resource'])
BarthaM@151
   303
        driveHandler.start()
BarthaM@163
   304
        driveHandler.join(None)
BarthaM@151
   305
        return 'Ok'
BarthaM@151
   306
BarthaM@151
   307
         
BarthaM@151
   308
        
BarthaM@151
   309
# handles netumount request                    
BarthaM@151
   310
class UmountNetworkDriveHandler(threading.Thread): 
BarthaM@151
   311
    drive = None
BarthaM@151
   312
    running = True
BarthaM@151
   313
    
BarthaM@151
   314
    def __init__(self, drv):
BarthaM@151
   315
        threading.Thread.__init__(self)
BarthaM@151
   316
        self.drive = drv
BarthaM@151
   317
BarthaM@151
   318
    def run(self):
BarthaM@151
   319
        while self.running:
BarthaM@151
   320
            result = Cygwin.checkResult(Cygwin.execute('C:\\Windows\\system32\\net.exe', 'USE'))
BarthaM@151
   321
            mappedDrives = list()
BarthaM@151
   322
            for line in result[1].splitlines():
BarthaM@151
   323
                if 'USB' in line or 'Download' in line:
BarthaM@151
   324
                    parts = line.split()
BarthaM@151
   325
                    mappedDrives.append(parts[1])
BarthaM@151
   326
            
BarthaM@151
   327
            logger.info(mappedDrives)
BarthaM@151
   328
            logger.info(self.drive)
BarthaM@151
   329
            if self.drive not in mappedDrives:
BarthaM@151
   330
                self.running = False
BarthaM@151
   331
            else:
BarthaM@151
   332
                command = 'USE ' + self.drive + ' /DELETE /YES' 
BarthaM@151
   333
                result = Cygwin.checkResult(Cygwin.execute('C:\\Windows\\system32\\net.exe', command)) 
BarthaM@151
   334
                if string.find(str(result[1]), 'successfully',) == -1:
BarthaM@151
   335
                    logger.error(result[2])
BarthaM@151
   336
                    continue
BarthaM@151
   337
                        
BarthaM@151
   338
BarthaM@151
   339
class os_netumount:
BarthaM@151
   340
    
BarthaM@151
   341
    """OpenSecurity '/netumount' handler"""
BarthaM@151
   342
    
BarthaM@151
   343
    def GET(self):
BarthaM@151
   344
        # pick the arguments
BarthaM@151
   345
        args = web.input()
BarthaM@151
   346
        
BarthaM@151
   347
        # we _need_ a drive_letter
BarthaM@151
   348
        if not "drive_letter" in args:
BarthaM@151
   349
            raise web.badrequest('no drive_letter given')
BarthaM@151
   350
        
BarthaM@151
   351
        driveHandler = UmountNetworkDriveHandler(args['drive_letter'])
BarthaM@151
   352
        driveHandler.start()
BarthaM@163
   353
        driveHandler.join(None)
BarthaM@151
   354
        return 'Ok'
BarthaM@151
   355
    
om@13
   356
om@13
   357
class os_root:
oliver@134
   358
om@13
   359
    """OpenSecurity '/' handler"""
om@13
   360
    
om@13
   361
    def GET(self):
om@13
   362
    
oliver@144
   363
        res = "OpenSecurity-Client RESTFul Server { \"version\": \"%s\" }" % opensecurity.__version__
om@13
   364
        
om@13
   365
        # add some sample links
om@13
   366
        res = res + """
om@13
   367
        
om@13
   368
USAGE EXAMPLES:
om@13
   369
        
om@13
   370
Request a password: 
om@13
   371
    (copy paste this into your browser's address field after the host:port)
om@13
   372
    
om@13
   373
    /password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0)
om@13
   374
    
om@13
   375
    (eg.: http://127.0.0.1:8090/password?text=Give+me+a+password+for+device+%22My+USB+Drive%22+(ID%3A+32090-AAA-X0))
om@13
   376
    NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
om@13
   377
    
om@13
   378
    
om@13
   379
Request a combination of user and password:
om@13
   380
    (copy paste this into your browser's address field after the host:port)
om@13
   381
    
om@13
   382
    /credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.
om@13
   383
    
om@13
   384
    (eg.: http://127.0.0.1:8090/credentials?text=Tell+the+NSA+which+credentials+to+use+in+order+to+avoid+hacking+noise+on+wire.)
om@13
   385
    NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
om@13
   386
    
om@13
   387
oliver@134
   388
Request a combination of password and keyfile:
oliver@134
   389
    (copy paste this into your browser's address field after the host:port)
oliver@134
   390
    
oliver@134
   391
    /keyfile?text=Your%20private%20RSA%20Keyfile%3A
oliver@134
   392
    
oliver@134
   393
    (eg.: http://127.0.0.1:8090//keyfile?text=Your%20private%20RSA%20Keyfile%3A)
oliver@134
   394
    NOTE: check yout taskbar, the dialog window may not pop up in front of your browser window.
oliver@134
   395
    
oliver@134
   396
om@13
   397
Start a Browser:
om@13
   398
    (copy paste this into your browser's address field after the host:port)
om@13
   399
om@13
   400
    /application?vm=Debian+7&app=Browser
om@13
   401
om@13
   402
    (e.g. http://127.0.0.1:8090/application?vm=Debian+7&app=Browser)
om@13
   403
        """
om@13
   404
    
om@13
   405
        return res
om@13
   406
om@13
   407
oliver@136
   408
class ProcessResultBouncer(threading.Thread):
oliver@134
   409
oliver@136
   410
    """A class to post the result of a given process - assuming it to be in JSON - to a REST Api."""
oliver@134
   411
oliver@134
   412
    def __init__(self, process, remote_ip, resource): 
oliver@134
   413
oliver@134
   414
        """ctor"""
oliver@134
   415
oliver@134
   416
        threading.Thread.__init__(self)
oliver@134
   417
        self._process = process
oliver@134
   418
        self._remote_ip = remote_ip
oliver@134
   419
        self._resource = resource
oliver@134
   420
 
oliver@134
   421
    
oliver@134
   422
    def stop(self):
oliver@134
   423
oliver@134
   424
        """stop thread"""
oliver@134
   425
        self.running = False
oliver@134
   426
        
oliver@134
   427
    
oliver@134
   428
    def run(self):
oliver@134
   429
oliver@134
   430
        """run the thread"""
oliver@134
   431
oliver@134
   432
        # invoke the user dialog as a subprocess
oliver@134
   433
        result = self._process.communicate()[0]
oliver@134
   434
        if self._process.returncode != 0:
oliver@134
   435
            print 'user request has been aborted.'
oliver@134
   436
            return
oliver@134
   437
        
oliver@134
   438
        # all ok, tell send request back appropriate destination
oliver@134
   439
        try:
oliver@134
   440
            j = json.loads(result)
oliver@134
   441
        except:
oliver@134
   442
            print 'error in password parsing'
oliver@134
   443
            return
oliver@134
   444
        
BarthaM@163
   445
        # TODO: it would be WAY easier and secure if we just 
BarthaM@163
   446
        #       add the result json to a HTTP-POST here.
BarthaM@163
   447
        url_addr = 'http://' + self._remote_ip + ':58080' + self._resource
BarthaM@163
   448
oliver@156
   449
        # by provided a 'data' we turn this into a POST statement
oliver@142
   450
        req = urllib2.Request(url_addr, urllib.urlencode(j))
oliver@134
   451
        try:
oliver@134
   452
            res = urllib2.urlopen(req)
oliver@134
   453
        except:
oliver@134
   454
            print 'failed to contact: ' + url_addr
oliver@134
   455
            return 
oliver@134
   456
oliver@134
   457
oliver@136
   458
class RESTServerThread(threading.Thread):
oliver@136
   459
oliver@136
   460
    """Thread for serving the REST API."""
oliver@136
   461
oliver@136
   462
    def __init__(self, port): 
oliver@136
   463
oliver@136
   464
        """ctor"""
oliver@136
   465
        threading.Thread.__init__(self)
oliver@136
   466
        self._port = port 
oliver@136
   467
    
oliver@136
   468
    def stop(self):
oliver@136
   469
oliver@136
   470
        """stop thread"""
oliver@136
   471
        self.running = False
oliver@136
   472
        
oliver@136
   473
    
oliver@136
   474
    def run(self):
oliver@136
   475
oliver@136
   476
        """run the thread"""
oliver@136
   477
        _serve(self._port)
oliver@136
   478
oliver@136
   479
oliver@136
   480
oliver@136
   481
def is_already_running(port = 8090):
oliver@136
   482
oliver@136
   483
    """check if this is started twice"""
oliver@136
   484
oliver@136
   485
    try:
oliver@136
   486
        s = socket.create_connection(('127.0.0.1', port), 0.5)
oliver@136
   487
    except:
oliver@136
   488
        return False
oliver@136
   489
oliver@136
   490
    return True
oliver@136
   491
oliver@136
   492
oliver@136
   493
def _serve(port):
oliver@136
   494
oliver@136
   495
    """Start the REST server"""
oliver@136
   496
oliver@136
   497
    global server
oliver@136
   498
oliver@136
   499
    # trick the web.py server 
oliver@136
   500
    sys.argv = [__file__, str(port)]
om@13
   501
    server = web.application(opensecurity_urls, globals())
om@13
   502
    server.run()
oliver@134
   503
oliver@136
   504
oliver@136
   505
def serve(port = 8090, background = False):
oliver@136
   506
oliver@136
   507
    """Start serving the REST Api
oliver@136
   508
    port ... port number to listen on
oliver@136
   509
    background ... cease into background (spawn thread) and return immediately"""
oliver@136
   510
oliver@136
   511
    # start threaded or direct version
oliver@136
   512
    if background == True:
oliver@136
   513
        t = RESTServerThread(port)
oliver@136
   514
        t.start()
oliver@136
   515
    else:
oliver@136
   516
        _serve(port)
oliver@136
   517
oliver@136
   518
def stop():
oliver@136
   519
oliver@136
   520
    """Stop serving the REST Api"""
oliver@136
   521
oliver@136
   522
    global server
oliver@136
   523
    if server is None:
oliver@136
   524
        return
oliver@136
   525
BarthaM@163
   526
    server.stop()
oliver@154
   527
oliver@136
   528
oliver@136
   529
# start
oliver@136
   530
if __name__ == "__main__":
oliver@136
   531
    serve()
oliver@136
   532