0xGA: Check-in [341670f849]

Yet another PHP framework, but made for org-mode and geeks.

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Add logging facility. Ready to serve my own website
Timelines: family | ancestors | descendants | both | narv
Files: files | file ages | folders
SHA1:341670f84985992cce383378e9a46532b1751d50
User & Date: milouse 2014-05-04 16:55:06
Context
2014-05-04
17:05
Update the doc check-in: 0fd14e3ac6 user: milouse tags: narv
16:55
Add logging facility. Ready to serve my own website check-in: 341670f849 user: milouse tags: narv
2014-05-03
20:12
Fix various bugs. Seems very close of a prod ready status. check-in: e913d85986 user: milouse tags: narv
Changes

Added example/milouse.py.

































































































































































































































>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import re
from datetime import datetime
from configparser import ConfigParser
from narv import IcanDoThat
from narv import Narv

class MilouseBlog(IcanDoThat):

    appname = 'milouse'

    monthes = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin',
    'juillet', 'août', 'septembre', 'octobre', 'novembre', 'decembre']

    def archive(self, pattern=None):
        if 'blog_folder' in self.config:
            blog_folder = self.config['blog_folder']
        else:
            blog_folder = 'blog'

        blog_index = []
        blog_folder = os.path.join('srv', self.appname, blog_folder)

        if os.path.isdir(blog_folder):

            for article_permalink in os.listdir(blog_folder):
                article_local_path = os.path.join(blog_folder, article_permalink)
                meta_file = os.path.join(article_local_path, 'meta.conf')

                if os.path.isdir(article_local_path) and os.path.exists(meta_file) and os.path.isfile(meta_file):
                    meta_infos = ConfigParser()
                    meta_infos.read(meta_file)

                    if 'metadata' in meta_infos:
                        meta_infos = meta_infos['metadata']

                        if 'timestamp' in meta_infos and (pattern == None or meta_infos['timestamp'][0:len(pattern)] == pattern):

                            title = ''
                            if 'title' in meta_infos:
                                title = meta_infos['title']
                                title = re.sub('"', '', title)

                            blog_index.append((
                                meta_infos['timestamp'],
                                title,
                                article_permalink))

        return sorted(blog_index, key=lambda art: art[0], reverse=True)


    def blog(self):
        self.extension = 'html'

        date_infos = re.search(r'(\d{4})/(?:(\d{2})/)?$', self.path)
        articles = []

        if date_infos and date_infos.groups()[0]:
            date_tuple = date_infos.groups()
            date_query = date_tuple[0]

            if date_tuple[1]:
                date_query = '{0}{1}'.format(*date_tuple)

            articles = self.archive(date_query)

        else:
            articles = self.archive()

        output = "<ul>"
        for art in articles:
            art_date = datetime.strptime(art[0], "%Y%m%d%H%M%S")
            output += '<li><a href="/a-écrit/{0}/{1}/">{2}</a> {3}</li>'.format(
                art_date.strftime("%Y/%m/%d"),
                art[2],
                art[1],
                'le {0} {1} {2} à {3}'.format(
                    art_date.day,
                    '<a href="/a-écrit/{0}/">{1}</a>'.format(
                        art_date.strftime("%Y/%m"),
                        self.monthes[int(art_date.month) - 1]
                    ),
                    '<a href="/a-écrit/{0}/">{0}</a>'.format(
                        art_date.year
                    ),
                    art_date.strftime("%H:%M")))

        output += '</ul>'

        if 'blog_template' in self.config:
            blog_template = os.path.join('srv', self.appname, self.config['blog_template'])

            if os.path.isfile(blog_template):
                temp_content = ''
                with open(blog_template) as f:
                    temp_content = f.read()

                output = re.sub(
                    r'<p>\s+Aucun article disponible&#x2026;\s+</p>',
                    output,
                    temp_content,
                    re.M)

        return bytes(output, 'utf-8')


if __name__ == "__main__":
    app = Narv(('localhost', 8000), MilouseBlog)
    app.start()

Changes to narv.

29
30
31
32
33
34
35

36
37
38
39
40
41
42
43
44
..
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
...
193
194
195
196
197
198
199



200
201
202

203
204
205
206
207
208
209
...
213
214
215
216
217
218
219


220
221
222
223
224
225
226
}

function narv_start {
    if [ -f "$WORKINGREP/var/$APPNAME.pid" ] ; then
        echo "Your Narv application is already running."
        exit 1;
    fi

    echo -n "Starting $WORKINGREP/usr/bin/$APPNAME.py"
    $WORKINGREP/usr/bin/$APPNAME.py &>> $WORKINGREP/var/log/$APPNAME.log &
    pgrep -f $APPNAME.py > $WORKINGREP/var/$APPNAME.pid
    echo "                   [OK]"
}

function narv_stop {
    if [ ! -f "$WORKINGREP/var/$APPNAME.pid" ] ; then
        echo "Your Narv application is NOT running."
................................................................................
        echo "Your Narv application is NOT running."
        exit 1;
    fi
    echo -n "Stopping $WORKINGREP/usr/bin/$APPNAME.py"
    pkill -f $APPNAME.py
    echo "                   ..."
    echo -n "Starting $WORKINGREP/usr/bin/$APPNAME.py"
    $WORKINGREP/usr/bin/$APPNAME.py &>> $WORKINGREP/var/log/$APPNAME.log &
    pgrep -f $APPNAME.py > $WORKINGREP/var/$APPNAME.pid
    echo "                   [OK]"
}

function init_chroot {
    if [ "$UID" != "0" ] ; then
        echo "You must be root to create the chroot"
................................................................................
        echo ":: configuring new Website"
        install -dm 755 $WORKINGREP/home/$APPNAME/.themes
        install -dm 755 $WORKINGREP/srv/$APPNAME

        exec 6>&1 # bind fd #6 to stdout (save stdout)
        exec > $WORKINGREP/etc/rc.conf # redirect stdout to etc/rc.conf
        cat <<EOF



[$DOMAINNAME]
debug = 0
appname = $APPNAME

EOF

        exec > $WORKINGREP/etc/routes.conf
        cat <<EOF
[$DOMAINNAME]
/ = my_first_custom_path
EOF
................................................................................
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from narv import IcanDoThat
from narv import Narv

class MyBeautifulApp(IcanDoThat):



    def my_first_custom_path(self):
        self.extension = 'html'
        return b'<h1>Hello World!</h1>'


if __name__ == "__main__":







>

|







 







|







 







>
>
>

<
<
>







 







>
>







29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
..
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
...
194
195
196
197
198
199
200
201
202
203
204


205
206
207
208
209
210
211
212
...
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
}

function narv_start {
    if [ -f "$WORKINGREP/var/$APPNAME.pid" ] ; then
        echo "Your Narv application is already running."
        exit 1;
    fi

    echo -n "Starting $WORKINGREP/usr/bin/$APPNAME.py"
    $WORKINGREP/usr/bin/$APPNAME.py &
    pgrep -f $APPNAME.py > $WORKINGREP/var/$APPNAME.pid
    echo "                   [OK]"
}

function narv_stop {
    if [ ! -f "$WORKINGREP/var/$APPNAME.pid" ] ; then
        echo "Your Narv application is NOT running."
................................................................................
        echo "Your Narv application is NOT running."
        exit 1;
    fi
    echo -n "Stopping $WORKINGREP/usr/bin/$APPNAME.py"
    pkill -f $APPNAME.py
    echo "                   ..."
    echo -n "Starting $WORKINGREP/usr/bin/$APPNAME.py"
    $WORKINGREP/usr/bin/$APPNAME.py &
    pgrep -f $APPNAME.py > $WORKINGREP/var/$APPNAME.pid
    echo "                   [OK]"
}

function init_chroot {
    if [ "$UID" != "0" ] ; then
        echo "You must be root to create the chroot"
................................................................................
        echo ":: configuring new Website"
        install -dm 755 $WORKINGREP/home/$APPNAME/.themes
        install -dm 755 $WORKINGREP/srv/$APPNAME

        exec 6>&1 # bind fd #6 to stdout (save stdout)
        exec > $WORKINGREP/etc/rc.conf # redirect stdout to etc/rc.conf
        cat <<EOF
[general]
debug = info

[$DOMAINNAME]


something = wonderfull
EOF

        exec > $WORKINGREP/etc/routes.conf
        cat <<EOF
[$DOMAINNAME]
/ = my_first_custom_path
EOF
................................................................................
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from narv import IcanDoThat
from narv import Narv

class MyBeautifulApp(IcanDoThat):

    appname = '$APPNAME'

    def my_first_custom_path(self):
        self.extension = 'html'
        return b'<h1>Hello World!</h1>'


if __name__ == "__main__":

Changes to narv.py.

7
8
9
10
11
12
13

14
15

16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
..
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
...
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
...
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
...
173
174
175
176
177
178
179

180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214


215
216
217
218
219
220
221
...
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
...
310
311
312
313
314
315
316


























317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
...
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
import pwd
import grp
import sys
import signal
import urllib
import shutil
import fnmatch

import threading
import configparser

from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
from configparser import ConfigParser


NARV_DEBUG = True
def log(text, force=False):
    global NARV_DEBUG

    if NARV_DEBUG or force:
        print(text)


class ResourceHelper:
    """ Utility to extract and expose various information on resources served """

    content_types = {
        'html': 'text/html; charset=utf-8',
        'jpg': 'image/jpeg',
................................................................................
            ext = ext[1:]

        if ext != '' and ext in self.content_types:

            self.resource_ext = ext
            self.content_type = self.content_types[ext]

            log('[{0}] has extension {1} and content_type {2}'.format(
                self.resource_path, self.resource_ext, self.content_type))


    def test_local_file_exists(self, request):

        log("[{0}] Test if local file {0} exists.".format(request))

        if os.path.isfile(request):
            log("[{0}] Match.".format(request))
            self.resource_path = request
            return True

        return False


    def test_method_exists(self, request):

        log("[{0}] Test if method {0} exists in your App".format(request))
        if request in dir(self.App):
            log("[{0}] Match".format(request))
            self.resource_path = request
            self.resource_is_method = True
            return True


    def extract_interesting_routes(self):
        interesting_routes = []
        tokens = False

        for potential_path in self.routes:
            if fnmatch.fnmatch(self.request_path, potential_path):
                log("[{0}] Found potential match with {1}".format(
                    self.request_path, potential_path
                ))

                expand_wildcards = re.sub(r'\*', '(.+)', potential_path)
                tokens = re.search(
                    expand_wildcards,
                    self.request_path
................................................................................

    def find_route(self):

        loc_path_test = 'srv/{0}{1}'.format(self.appname, self.request_path)
        if self.test_local_file_exists(loc_path_test):
            return True

        log("[{0}] Try to find a candidate in the user defined routes".format(self.request_path))

        for route in self.extract_interesting_routes():
            if self.test_method_exists(route):
                return True

            route = 'srv/{0}/{1}'.format(self.appname, route)
            if self.test_local_file_exists(route):
................................................................................

        return False


    def preprocess_content(self):
        success = True
        if self.resource_is_method:
            log("[{0}] Spawn custom content method".format(self.resource_path))
            self.content = getattr(self.App, self.resource_path)()

            if self.content == None:
                success = False

        self.guess_content_type()
        return success
................................................................................
                shutil.copyfileobj(f, self.App.wfile)



class IcanDoThat(BaseHTTPRequestHandler):
    """ Main HTTP handler """
    server_version = '0xGA/0.1'


    def __init__(self, request, client_address, server):
        self.error = False
        self.extension = None
        self.current_domain = server.server_name
        self.current_port = server.server_port

        BaseHTTPRequestHandler.__init__(self, request, client_address, server)


    def load_config(self):
        host_infos = self.headers.get('host','').split(':')
        self.current_domain = host_infos[0]
        log('Request for {0} processed by {1}'.format(
            self.current_domain,
            threading.currentThread().getName()))

        config = ConfigParser()
        config.read('etc/rc.conf')

        if not self.current_domain in config:
            self.error = 'Domain is not reachable in configuration file'

        else:
            self.config = config[self.current_domain]
            global NARV_DEBUG
            NARV_DEBUG = self.config.getboolean('debug')

            self.appname = None
            if 'appname' in self.config:
                self.appname = self.config['appname']
                log('Request is for App {0}'.format(self.appname))

            else:
                self.error = 'No claimed application in config file'




    def do_error_page(self, errno, reason):
        self.build_headers(int(errno), 'text/html; charset=utf-8')

        if errno in self.config:
            with open(self.config[errno], 'rb') as f:
................................................................................
</div>
</body>
</html>""".format(errno, reason)
            self.wfile.write(bytes(error_html, 'utf-8'))


    def do_404(self, reason='File not found'):
        log('[Error 404] {0}'.format(reason), True)
        self.do_error_page('404', reason)


    def do_501(self, reason='Lack of configuration'):
        log('[Error 501] {0}'.format(reason), True)
        self.do_error_page('501', reason)


    def build_headers(self, resno, content_type = 'text/plain'):
        self.send_response(resno)
        if content_type != None:
            self.send_header('Content-Type', content_type)
................................................................................
class NarvThreadedServer(ThreadingMixIn, HTTPServer):
    """Main threaded server"""


class Narv:
    def __init__(self, server_infos=('', 8000), request_handler=IcanDoThat):



























        signal.signal(signal.SIGINT, self.shutdown)
        signal.signal(signal.SIGTERM, self.shutdown)

        self.server = NarvThreadedServer(server_infos, request_handler)
        server_thread = threading.Thread(target=self.server.serve_forever)
        server_thread.daemon = True
        server_thread.start()

        narv_root_path = os.path.normpath(os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            '../../'))

        if os.getuid() == 0:
            os.chroot(narv_root_path)
            os.chdir("/")
            log("Dropping priviledges to UID {0} GID {1}".format('nobody', 'nobody'))

            # Remove group privileges
            os.setgroups([])

            # Try setting the new uid/gid
            os.setgid(grp.getgrnam('nobody').gr_gid)
            os.setuid(pwd.getpwnam('nobody').pw_uid)
................................................................................
            # Ensure a very conservative umask
            os.umask(int('077', 8))
        else:
            os.chdir(narv_root_path)


    def start(self):
        log("Serving {0} at port {1}".format(self.server.server_name, self.server.server_port))
        self.server.serve_forever()


    def shutdown(self, signal, frame):
        log("\nKthxbye", True)
        self.server.shutdown()
        sys.exit(0)



if __name__ == "__main__":
    app = Narv()
    app.start()







>


>






<
<
<
<
<
<
<







 







|





|


|








|

|











|







 







|







 







|







 







>













|











|
<

<
<
<
<

<
<
>
>







 







|




|







 







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>








<
<
<
<



|







 







|




|








7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23







24
25
26
27
28
29
30
..
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
...
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
...
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
...
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201

202




203


204
205
206
207
208
209
210
211
212
...
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
...
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341




342
343
344
345
346
347
348
349
350
351
352
...
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
import pwd
import grp
import sys
import signal
import urllib
import shutil
import fnmatch
import logging
import threading
import configparser
from datetime import date
from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
from socketserver import ThreadingMixIn
from configparser import ConfigParser










class ResourceHelper:
    """ Utility to extract and expose various information on resources served """

    content_types = {
        'html': 'text/html; charset=utf-8',
        'jpg': 'image/jpeg',
................................................................................
            ext = ext[1:]

        if ext != '' and ext in self.content_types:

            self.resource_ext = ext
            self.content_type = self.content_types[ext]

            logging.debug('[{0}] has extension {1} and content_type {2}'.format(
                self.resource_path, self.resource_ext, self.content_type))


    def test_local_file_exists(self, request):

        logging.debug("[{0}] Test if local file {0} exists.".format(request))

        if os.path.isfile(request):
            logging.debug("[{0}] Match.".format(request))
            self.resource_path = request
            return True

        return False


    def test_method_exists(self, request):

        logging.debug("[{0}] Test if method {0} exists in your App".format(request))
        if request in dir(self.App):
            logging.debug("[{0}] Match".format(request))
            self.resource_path = request
            self.resource_is_method = True
            return True


    def extract_interesting_routes(self):
        interesting_routes = []
        tokens = False

        for potential_path in self.routes:
            if fnmatch.fnmatch(self.request_path, potential_path):
                logging.debug("[{0}] Found potential match with {1}".format(
                    self.request_path, potential_path
                ))

                expand_wildcards = re.sub(r'\*', '(.+)', potential_path)
                tokens = re.search(
                    expand_wildcards,
                    self.request_path
................................................................................

    def find_route(self):

        loc_path_test = 'srv/{0}{1}'.format(self.appname, self.request_path)
        if self.test_local_file_exists(loc_path_test):
            return True

        logging.debug("[{0}] Try to find a candidate in the user defined routes".format(self.request_path))

        for route in self.extract_interesting_routes():
            if self.test_method_exists(route):
                return True

            route = 'srv/{0}/{1}'.format(self.appname, route)
            if self.test_local_file_exists(route):
................................................................................

        return False


    def preprocess_content(self):
        success = True
        if self.resource_is_method:
            logging.debug("[{0}] Spawn custom content method".format(self.resource_path))
            self.content = getattr(self.App, self.resource_path)()

            if self.content == None:
                success = False

        self.guess_content_type()
        return success
................................................................................
                shutil.copyfileobj(f, self.App.wfile)



class IcanDoThat(BaseHTTPRequestHandler):
    """ Main HTTP handler """
    server_version = '0xGA/0.1'
    appname = 'DEFAULT'

    def __init__(self, request, client_address, server):
        self.error = False
        self.extension = None
        self.current_domain = server.server_name
        self.current_port = server.server_port

        BaseHTTPRequestHandler.__init__(self, request, client_address, server)


    def load_config(self):
        host_infos = self.headers.get('host','').split(':')
        self.current_domain = host_infos[0]
        logging.debug('Request for {0} processed by {1}'.format(
            self.current_domain,
            threading.currentThread().getName()))

        config = ConfigParser()
        config.read('etc/rc.conf')

        if not self.current_domain in config:
            self.error = 'Domain is not reachable in configuration file'

        else:
            self.config = config[self.current_domain]
            logging.debug('Request is for App {0}'.format(self.appname))









    def log_message(self, message, *args):
        logging.info(message % args)


    def do_error_page(self, errno, reason):
        self.build_headers(int(errno), 'text/html; charset=utf-8')

        if errno in self.config:
            with open(self.config[errno], 'rb') as f:
................................................................................
</div>
</body>
</html>""".format(errno, reason)
            self.wfile.write(bytes(error_html, 'utf-8'))


    def do_404(self, reason='File not found'):
        logging.error('[Error 404] {0}'.format(reason))
        self.do_error_page('404', reason)


    def do_501(self, reason='Lack of configuration'):
        logging.critical('[Error 501] {0}'.format(reason))
        self.do_error_page('501', reason)


    def build_headers(self, resno, content_type = 'text/plain'):
        self.send_response(resno)
        if content_type != None:
            self.send_header('Content-Type', content_type)
................................................................................
class NarvThreadedServer(ThreadingMixIn, HTTPServer):
    """Main threaded server"""


class Narv:
    def __init__(self, server_infos=('', 8000), request_handler=IcanDoThat):

        config = ConfigParser()
        config.read('etc/rc.conf')

        log_level = 'info'
        drop_priviledges = True
        narv_root_path = os.path.normpath(os.path.join(
            os.path.dirname(os.path.abspath(__file__)),
            '../../'))

        if 'general' in config:
            if 'debug' in config['general']:
                log_level = config['general']['debug']

            if 'drop_priviledges' in config['general']:
                drop_priviledges = config['general'].getboolean('drop_priviledges')

        log_file = '{0}.{1}.log'.format(
            request_handler.appname,
            date.today().strftime("%Y%m%d")
        )
        logging.basicConfig(
            format='%(asctime)s -- [%(levelname)s] %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S',
            filename=os.path.join(narv_root_path, 'var/log/', log_file),
            level=getattr(logging, log_level.upper()))

        signal.signal(signal.SIGINT, self.shutdown)
        signal.signal(signal.SIGTERM, self.shutdown)

        self.server = NarvThreadedServer(server_infos, request_handler)
        server_thread = threading.Thread(target=self.server.serve_forever)
        server_thread.daemon = True
        server_thread.start()





        if os.getuid() == 0:
            os.chroot(narv_root_path)
            os.chdir("/")
            logging.debug("Dropping priviledges to UID {0} GID {1}".format('nobody', 'nobody'))

            # Remove group privileges
            os.setgroups([])

            # Try setting the new uid/gid
            os.setgid(grp.getgrnam('nobody').gr_gid)
            os.setuid(pwd.getpwnam('nobody').pw_uid)
................................................................................
            # Ensure a very conservative umask
            os.umask(int('077', 8))
        else:
            os.chdir(narv_root_path)


    def start(self):
        logging.info("Serving {0} at port {1}".format(self.server.server_name, self.server.server_port))
        self.server.serve_forever()


    def shutdown(self, signal, frame):
        logging.info("Kthxbye")
        self.server.shutdown()
        sys.exit(0)



if __name__ == "__main__":
    app = Narv()
    app.start()