~ubuntu-branches/ubuntu/lucid/gnome-lirc-properties/lucid

« back to all changes in this revision

Viewing changes to web/service.wsgi

  • Committer: Bazaar Package Importer
  • Author(s): Mathias Hasselmann
  • Date: 2008-03-19 21:20:19 UTC
  • Revision ID: james.westby@ubuntu.com-20080319212019-nwnq1euypsb3wrj6
Tags: upstream-0.2.5
ImportĀ upstreamĀ versionĀ 0.2.5

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# WSGI script for hosting a database of infrared remote configurations
 
2
# Copyright (C) 2008 Openismus GmbH (www.openismus.com)
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify it under
 
5
# the terms of the GNU General Public License as published by the Free Software
 
6
# Foundation; either version 2 of the License, or (at your option) any later
 
7
# version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful, but WITHOUT
 
10
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 
11
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 
12
# details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License along with
 
15
# this program; if not, write to the Free Software Foundation, Inc.,
 
16
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 
17
#
 
18
# Authors:
 
19
#   Mathias Hasselmann <mathias@openismus.com>
 
20
'''
 
21
WSGI script for hosting a database of infrared remote configurations
 
22
'''
 
23
 
 
24
import cgi, fcntl, httplib, os, rfc822, tarfile, time
 
25
 
 
26
from errno   import ENOENT
 
27
from gettext import gettext as _
 
28
from locale  import LC_ALL, setlocale
 
29
from sha     import sha as SHA1
 
30
 
 
31
class Context(dict):
 
32
    '''A WSGI request context.'''
 
33
 
 
34
    def __init__(self, environ):
 
35
        super(Context, self).__init__(environ)
 
36
 
 
37
    content_type   = property(lambda self: self['CONTENT_TYPE'])
 
38
    request_method = property(lambda self: self['REQUEST_METHOD'].upper())
 
39
    request_uri    = property(lambda self: self['REQUEST_URI'])
 
40
    path_info      = property(lambda self: self['PATH_INFO'])
 
41
 
 
42
    errors = property(lambda self: self['wsgi.errors'])
 
43
    input  = property(lambda self: self['wsgi.input'])
 
44
 
 
45
class Response(dict):
 
46
    '''Wrapper arround the various parts of a WSGI response.'''
 
47
 
 
48
    def __init__(self, status=httplib.OK,
 
49
                 content_type='text/html',
 
50
                 content=''):
 
51
 
 
52
        super(Response, self).__init__()
 
53
 
 
54
        self['Content-Type'] = content_type
 
55
        self.__output = [content]
 
56
        self.__status = status
 
57
 
 
58
    def __get_status(self):
 
59
        '''Query response status as tuple of status code and message.'''
 
60
        return self.__status, __responses__.get(self.__status, '')
 
61
 
 
62
    def __set_status(self, status):
 
63
        '''Update response status (code).'''
 
64
        self.__status = int(status)
 
65
 
 
66
    def __get_output(self):
 
67
        '''Access the list of response chunks.'''
 
68
        return self.__output
 
69
 
 
70
    status = property(fget=__get_status, fset=__set_status)
 
71
    output = property(fget=__get_output)
 
72
 
 
73
def find_request_handler(request_method, path_info, default=None):
 
74
    '''Finds the request handler for the current request.'''
 
75
 
 
76
    handler_id = '%s:%s' % (request_method, path_info)
 
77
    return __request_handlers__.get(handler_id, default)
 
78
 
 
79
def find_locale(locale, localedir='/usr/lib/locale'):
 
80
    '''Finds the name of the matching locallly installed locale.'''
 
81
 
 
82
    for name in '%s.utf8' % locale, locale, locale[:2]:
 
83
        ident = os.path.join(localedir, name, 'LC_IDENTIFICATION')
 
84
 
 
85
        if os.path.isfile(ident):
 
86
            return name
 
87
 
 
88
    return None
 
89
 
 
90
def html_page(context, status, title=None, message=None, *args):
 
91
    '''Creates a response that contains a HTML page.'''
 
92
 
 
93
    template = '''\
 
94
<html>
 
95
 <head>
 
96
  <title>%(title)s</title>
 
97
 </head>
 
98
 
 
99
 <body>
 
100
  <h1>%(title)s</h1>
 
101
  <p>%(message)s</p>
 
102
  <p>%(signature)s</p>
 
103
 </body>
 
104
</htm>'''
 
105
 
 
106
    if not title:
 
107
        title = __responses__[status]
 
108
    if not message:
 
109
        message = _('Request failed.')
 
110
    if args:
 
111
        message %= args
 
112
 
 
113
    return Response(
 
114
        status=status, content=template % dict(vars(),
 
115
        signature=context.get('SERVER_SIGNATURE', ''),
 
116
    ))
 
117
 
 
118
def redirect(context, path, status=httplib.SEE_OTHER):
 
119
    '''Creates a response that redirects to another page.'''
 
120
 
 
121
    response = html_page(context, status, None,
 
122
                         _('See: <a href="%(path)s">%(path)s</a>.') %
 
123
                         dict(path=cgi.escape(path)))
 
124
 
 
125
    response['Location'] = path
 
126
 
 
127
    return response
 
128
 
 
129
def not_found(context):
 
130
    '''Creates a response that handles missing pages.'''
 
131
 
 
132
    uri = context.request_uri
 
133
 
 
134
    if not uri.endswith('/'):
 
135
        fallback = find_request_handler(context.request_method,
 
136
                                        context.path_info + '/')
 
137
 
 
138
        if fallback:
 
139
            return redirect(context, uri + '/')
 
140
 
 
141
    return html_page(context, httplib.NOT_FOUND,
 
142
                     _('Resource Not Found'),
 
143
                     _('Cannot find this resource: <b>%s</b>.'),
 
144
                     cgi.escape(uri))
 
145
 
 
146
def show_upload_form(context):
 
147
    '''Shows the HTML form for uploading LIRC configuration files.'''
 
148
 
 
149
    template = '''\
 
150
<html>
 
151
 <head>
 
152
  <title>%(title)s</title>
 
153
 </head>
 
154
 
 
155
 <body>
 
156
  <h1>%(title)s</h1>
 
157
 
 
158
  <form action="upload/" method="post" enctype="multipart/form-data">
 
159
   <table>
 
160
    <tr>
 
161
     <td>Configuration File:</td>
 
162
     <td><input name="config" type="file" size="40" value="/etc/lirc/lircd.conf"/></td>
 
163
    </tr>
 
164
 
 
165
    <tr>
 
166
     <td>SHA1 Digest:</td>
 
167
     <td><input name="digest" type="text" size="40" maxlength="40" /></td>
 
168
    </tr>
 
169
 
 
170
    <tr>
 
171
     <td>Language:</td>
 
172
     <td>
 
173
      <select name="locale">
 
174
       <option value="en_US">English</option>
 
175
       <option value="de_DE">German</option>
 
176
      </select>
 
177
    </tr>
 
178
 
 
179
    <tr>
 
180
     <td colspan="2" align="right">
 
181
      <button>Upload</button>
 
182
     </td>
 
183
    </tr>
 
184
   </table>
 
185
  </form>
 
186
 
 
187
  <p>Download <a href="%(dburi)s">current archive</a>.</p>
 
188
  <p>%(signature)s</p>
 
189
 </body>
 
190
</html>'''
 
191
 
 
192
    return Response(content=template % dict(
 
193
        signature=context.get('SERVER_SIGNATURE', ''),
 
194
        title='Upload LIRC Remote Control Configuration',
 
195
        dburi='remotes.tar.gz',
 
196
    ))
 
197
 
 
198
def process_upload(context):
 
199
    '''Processes an uploaded LIRC configuration file.'''
 
200
 
 
201
    # pylint: disable-msg=R0911
 
202
 
 
203
    form = cgi.FieldStorage(fp=context.input, environ=context)
 
204
 
 
205
    digest, config, locale = [
 
206
        form.has_key(key) and form[key]
 
207
        for key in ('digest', 'config', 'locale')]
 
208
 
 
209
    locale = locale is not False and find_locale(locale.value)
 
210
 
 
211
    if locale:
 
212
        setlocale(LC_ALL, locale)
 
213
 
 
214
    # validate request body:
 
215
 
 
216
    if form.type != 'multipart/form-data':
 
217
        return html_page(context, httplib.BAD_REQUEST, None,
 
218
                         _('Request has unexpected content type.'))
 
219
 
 
220
    if form.type != digest is False or config is False or locale is False:
 
221
        return html_page(context, httplib.BAD_REQUEST, None,
 
222
                         _('Some fields are missing in this request.'))
 
223
 
 
224
    if digest.value != SHA1(config.value).hexdigest():
 
225
        return html_page(context, httplib.BAD_REQUEST, None,
 
226
                         _('Checksum doesn\'t match the uploaded content.'))
 
227
 
 
228
    # process request body:
 
229
 
 
230
    workdir  = os.path.dirname(__file__)
 
231
    archive  = os.path.join(workdir, 'archive', 'remotes.tar.gz')
 
232
    filename = os.path.join(workdir, 'archive', 'incoming', '%s.conf' % digest.value)
 
233
 
 
234
    try:
 
235
        # force rebuild by removing the obsolete archive:
 
236
        os.unlink(archive)
 
237
 
 
238
    except OSError, ex:
 
239
        if ENOENT != ex.errno:
 
240
            return html_page(context, httplib.INTERNAL_SERVER_ERROR, None,
 
241
                             _('Cannot remove obsolete remotes archive: %s.'),
 
242
                             ex.strerror)
 
243
 
 
244
    # store the uploaded configuration file in the incoming folder:
 
245
    try:
 
246
        storage = open(filename, 'wb')
 
247
 
 
248
    except IOError, ex:
 
249
        return html_page(context, httplib.INTERNAL_SERVER_ERROR, None,
 
250
                         _('Cannot store configuration file: %s.'),
 
251
                         ex.strerror)
 
252
 
 
253
    try:
 
254
        fcntl.lockf(storage, fcntl.LOCK_EX)
 
255
 
 
256
    except IOError, ex:
 
257
        return html_page(context, httplib.INTERNAL_SERVER_ERROR, None,
 
258
                         _('Cannot get exclusive file access: %s.'),
 
259
                         ex.strerror)
 
260
 
 
261
    storage.write(config.file.read())
 
262
    storage.close()
 
263
 
 
264
    # use POST/REDIRECT/GET pattern to avoid duplicate uploads:
 
265
    return redirect(context, context.request_uri + 'success')
 
266
 
 
267
def show_upload_success(context):
 
268
    '''Shows success message after upload'''
 
269
 
 
270
    return html_page(context, httplib.OK, _('Upload Succeeded'),
 
271
                     _('Upload of your configuration file succeeded. '
 
272
                       'Thanks alot for contributing.'))
 
273
 
 
274
def send_archive(context, filename, must_exist=False):
 
275
    '''Sends the specified tarball.'''
 
276
 
 
277
    try:
 
278
        # Checks last-modified time, when requested:
 
279
        reference_time = context.get('HTTP_IF_MODIFIED_SINCE')
 
280
 
 
281
        if reference_time:
 
282
            reference_time = rfc822.parsedate(reference_time)
 
283
            reference_time = time.mktime(reference_time)
 
284
 
 
285
            if reference_time >= os.path.getmtime(filename):
 
286
                return Response(httplib.NOT_MODIFIED)
 
287
 
 
288
        # Deliver the file with checksum and last-modified header:
 
289
        response = Response(content_type='application/x-gzip',
 
290
                            content=open(filename, 'rb').read())
 
291
 
 
292
        digest = SHA1(response.output[0]).hexdigest()
 
293
        timestamp = time.ctime(os.path.getmtime(filename))
 
294
 
 
295
        response['X-Checksum-Sha1'] = digest
 
296
        response['Last-Modified'] = timestamp
 
297
 
 
298
        return response
 
299
 
 
300
    except (IOError, OSError), ex:
 
301
        if must_exist or ENOENT != ex.errno:
 
302
            return html_page(context,
 
303
                             httplib.INTERNAL_SERVER_ERROR, None,
 
304
                             _('Cannot read remotes archive: %s.'),
 
305
                             ex.strerror)
 
306
 
 
307
        return None
 
308
 
 
309
def send_remotes_archive(context):
 
310
    '''Sends the archive with uploaded LIRC configuration files.'''
 
311
 
 
312
    archive_root  = os.path.join(os.path.dirname(__file__), 'archive')
 
313
    archive_name = os.path.join(archive_root, 'remotes.tar.gz')
 
314
 
 
315
    response = send_archive(context, archive_name)
 
316
 
 
317
    if not response:
 
318
        try:
 
319
            archive = tarfile.open(archive_name, 'w:gz')
 
320
 
 
321
        except IOError, ex:
 
322
            return html_page(context, httplib.INTERNAL_SERVER_ERROR, None,
 
323
                             _('Cannot create remotes archive: %s.'),
 
324
                             ex.strerror)
 
325
 
 
326
        try:
 
327
            # pylint: disable-msg=W0612
 
328
            for path, subdirs, files in os.walk(archive_root):
 
329
                # drop folders with SCM meta-information:
 
330
                subdirs[:] = [name for name in subdirs if
 
331
                              not name.startswith('.')
 
332
                              and name != 'CVS']
 
333
 
 
334
                # drop archive_root from current folder name:
 
335
                dirname = path[len(archive_root):]
 
336
 
 
337
                # scan files in current folder:
 
338
                for name in files:
 
339
                    if name.startswith('.'):
 
340
                        continue
 
341
 
 
342
                    filename = os.path.join(path, name)
 
343
                    arcname = os.path.join(dirname, name)
 
344
 
 
345
                    if filename == archive_name:
 
346
                        continue
 
347
 
 
348
                    info = archive.gettarinfo(filename, arcname)
 
349
                    archive.addfile(info, open(filename))
 
350
 
 
351
            # Trigger finalization of the tarball instance,
 
352
            # since Python 2.4 doesn't create its file otherwise:
 
353
            archive.close()
 
354
            del archive
 
355
 
 
356
            response = send_archive(context, archive_name, must_exist=True)
 
357
 
 
358
        except:
 
359
            try:
 
360
                # Try to remove artifacts on error:
 
361
                os.unlink(archive.name)
 
362
 
 
363
            except:
 
364
                pass
 
365
 
 
366
            raise
 
367
 
 
368
    return response
 
369
 
 
370
def application(environ, start_response):
 
371
    '''The WSGI entry point of this service.'''
 
372
 
 
373
    context, response = Context(environ), None
 
374
 
 
375
    try:
 
376
        handler = find_request_handler(context.request_method,
 
377
                                       context.path_info,
 
378
                                       not_found)
 
379
        response = handler(context)
 
380
 
 
381
    except:
 
382
        import traceback
 
383
 
 
384
        response = Response(content_type='text/plain',
 
385
                            content=traceback.format_exc(),
 
386
                            status=httplib.INTERNAL_SERVER_ERROR)
 
387
 
 
388
        traceback.print_exc(context.errors)
 
389
 
 
390
    if (not response.has_key('Content-Length') and
 
391
        response.get('Transfer-Encoding', '').lower() != 'chunked'):
 
392
        response['Content-Length'] = str(len(''.join(response.output)))
 
393
 
 
394
    start_response('%d %s' % response.status, response.items())
 
395
 
 
396
    return response.output
 
397
 
 
398
# Map status codes to official W3C names (from httplib/2.5):
 
399
__responses__ = {
 
400
    100: 'Continue',
 
401
    101: 'Switching Protocols',
 
402
 
 
403
    200: 'OK',
 
404
    201: 'Created',
 
405
    202: 'Accepted',
 
406
    203: 'Non-Authoritative Information',
 
407
    204: 'No Content',
 
408
    205: 'Reset Content',
 
409
    206: 'Partial Content',
 
410
 
 
411
    300: 'Multiple Choices',
 
412
    301: 'Moved Permanently',
 
413
    302: 'Found',
 
414
    303: 'See Other',
 
415
    304: 'Not Modified',
 
416
    305: 'Use Proxy',
 
417
    306: '(Unused)',
 
418
    307: 'Temporary Redirect',
 
419
 
 
420
    400: 'Bad Request',
 
421
    401: 'Unauthorized',
 
422
    402: 'Payment Required',
 
423
    403: 'Forbidden',
 
424
    404: 'Not Found',
 
425
    405: 'Method Not Allowed',
 
426
    406: 'Not Acceptable',
 
427
    407: 'Proxy Authentication Required',
 
428
    408: 'Request Timeout',
 
429
    409: 'Conflict',
 
430
    410: 'Gone',
 
431
    411: 'Length Required',
 
432
    412: 'Precondition Failed',
 
433
    413: 'Request Entity Too Large',
 
434
    414: 'Request-URI Too Long',
 
435
    415: 'Unsupported Media Type',
 
436
    416: 'Requested Range Not Satisfiable',
 
437
    417: 'Expectation Failed',
 
438
 
 
439
    500: 'Internal Server Error',
 
440
    501: 'Not Implemented',
 
441
    502: 'Bad Gateway',
 
442
    503: 'Service Unavailable',
 
443
    504: 'Gateway Timeout',
 
444
    505: 'HTTP Version Not Supported',
 
445
}
 
446
 
 
447
# Mapping request paths to request handlers:
 
448
__request_handlers__ = {
 
449
    'GET:/remotes.tar.gz': send_remotes_archive,
 
450
    'GET:/upload/success': show_upload_success,
 
451
    'GET:/':               show_upload_form,
 
452
 
 
453
    'POST:/upload/':       process_upload,
 
454
}
 
455
 
 
456
 
 
457
# pylint: disable-msg=W0702,W0704
 
458
# vim: ft=python