~certify-web-dev/twisted/certify-trunk

« back to all changes in this revision

Viewing changes to twisted/web2/static.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2007-01-17 14:52:35 UTC
  • mfrom: (1.1.5 upstream) (2.1.2 etch)
  • Revision ID: james.westby@ubuntu.com-20070117145235-btmig6qfmqfen0om
Tags: 2.5.0-0ubuntu1
New upstream version, compatible with python2.5.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
 
2
# See LICENSE for details.
 
3
 
 
4
 
 
5
"""
 
6
I deal with static resources.
 
7
"""
 
8
 
 
9
# System Imports
 
10
import os, time, stat
 
11
import tempfile
 
12
import md5
 
13
 
 
14
# Sibling Imports
 
15
from twisted.web2 import http_headers, resource
 
16
from twisted.web2 import http, iweb, stream, responsecode, server, dirlist
 
17
 
 
18
# Twisted Imports
 
19
from twisted.python import filepath
 
20
from twisted.internet.defer import maybeDeferred
 
21
from zope.interface import implements
 
22
 
 
23
class MetaDataMixin(object):
 
24
    """
 
25
    Mix-in class for L{iweb.IResource} which provides methods for accessing resource
 
26
    metadata specified by HTTP.
 
27
    """
 
28
    def etag(self):
 
29
        """
 
30
        @return: The current etag for the resource if available, None otherwise.
 
31
        """
 
32
        return None
 
33
 
 
34
    def lastModified(self):
 
35
        """
 
36
        @return: The last modified time of the resource if available, None otherwise.
 
37
        """
 
38
        return None
 
39
 
 
40
    def creationDate(self):
 
41
        """
 
42
        @return: The creation date of the resource if available, None otherwise.
 
43
        """
 
44
        return None
 
45
 
 
46
    def contentLength(self):
 
47
        """
 
48
        @return: The size in bytes of the resource if available, None otherwise.
 
49
        """
 
50
        return None
 
51
 
 
52
    def contentType(self):
 
53
        """
 
54
        @return: The MIME type of the resource if available, None otherwise.
 
55
        """
 
56
        return None
 
57
 
 
58
    def contentEncoding(self):
 
59
        """
 
60
        @return: The encoding of the resource if available, None otherwise.
 
61
        """
 
62
        return None
 
63
 
 
64
    def displayName(self):
 
65
        """
 
66
        @return: The display name of the resource if available, None otherwise.
 
67
        """
 
68
        return None
 
69
 
 
70
    def exists(self):
 
71
        """
 
72
        @return: True if the resource exists on the server, False otherwise.
 
73
        """
 
74
        return True
 
75
 
 
76
class StaticRenderMixin(resource.RenderMixin, MetaDataMixin):
 
77
    def checkPreconditions(self, request):
 
78
        # This code replaces the code in resource.RenderMixin
 
79
        if request.method not in ("GET", "HEAD"):
 
80
            http.checkPreconditions(
 
81
                request,
 
82
                entityExists = self.exists(),
 
83
                etag = self.etag(),
 
84
                lastModified = self.lastModified(),
 
85
            )
 
86
 
 
87
        # Check per-method preconditions
 
88
        method = getattr(self, "preconditions_" + request.method, None)
 
89
        if method:
 
90
            return method(request)
 
91
 
 
92
    def renderHTTP(self, request):
 
93
        """
 
94
        See L{resource.RenderMixIn.renderHTTP}.
 
95
 
 
96
        This implementation automatically sets some headers on the response
 
97
        based on data available from L{MetaDataMixin} methods.
 
98
        """
 
99
        def setHeaders(response):
 
100
            response = iweb.IResponse(response)
 
101
 
 
102
            # Don't provide additional resource information to error responses
 
103
            if response.code < 400:
 
104
                # Content-* headers refer to the response content, not
 
105
                # (necessarily) to the resource content, so they depend on the
 
106
                # request method, and therefore can't be set here.
 
107
                for (header, value) in (
 
108
                    ("etag", self.etag()),
 
109
                    ("last-modified", self.lastModified()),
 
110
                ):
 
111
                    if value is not None:
 
112
                        response.headers.setHeader(header, value)
 
113
 
 
114
            return response
 
115
 
 
116
        def onError(f):
 
117
            # If we get an HTTPError, run its response through setHeaders() as
 
118
            # well.
 
119
            f.trap(http.HTTPError)
 
120
            return setHeaders(f.value.response)
 
121
 
 
122
        d = maybeDeferred(super(StaticRenderMixin, self).renderHTTP, request)
 
123
        return d.addCallbacks(setHeaders, onError)
 
124
 
 
125
class Data(resource.Resource):
 
126
    """
 
127
    This is a static, in-memory resource.
 
128
    """
 
129
    def __init__(self, data, type):
 
130
        self.data = data
 
131
        self.type = http_headers.MimeType.fromString(type)
 
132
        self.created_time = time.time()
 
133
    
 
134
    def etag(self):
 
135
        lastModified = self.lastModified()
 
136
        return http_headers.ETag("%X-%X" % (lastModified, hash(self.data)),
 
137
                                 weak=(time.time() - lastModified <= 1))
 
138
 
 
139
    def lastModified(self):
 
140
        return self.creationDate()
 
141
 
 
142
    def creationDate(self):
 
143
        return self.created_time
 
144
 
 
145
    def contentLength(self):
 
146
        return len(self.data)
 
147
 
 
148
    def contentType(self):
 
149
        return self.type
 
150
 
 
151
    def render(self, req):
 
152
        return http.Response(
 
153
            responsecode.OK,
 
154
            http_headers.Headers({'content-type': self.contentType()}),
 
155
            stream=self.data)
 
156
 
 
157
 
 
158
class File(StaticRenderMixin):
 
159
    """
 
160
    File is a resource that represents a plain non-interpreted file
 
161
    (although it can look for an extension like .rpy or .cgi and hand the
 
162
    file to a processor for interpretation if you wish). Its constructor
 
163
    takes a file path.
 
164
 
 
165
    Alternatively, you can give a directory path to the constructor. In this
 
166
    case the resource will represent that directory, and its children will
 
167
    be files underneath that directory. This provides access to an entire
 
168
    filesystem tree with a single Resource.
 
169
 
 
170
    If you map the URL 'http://server/FILE' to a resource created as
 
171
    File('/tmp'), then http://server/FILE/ will return an HTML-formatted
 
172
    listing of the /tmp/ directory, and http://server/FILE/foo/bar.html will
 
173
    return the contents of /tmp/foo/bar.html .
 
174
    """
 
175
    implements(iweb.IResource)
 
176
 
 
177
    def _getContentTypes(self):
 
178
        if not hasattr(File, "_sharedContentTypes"):
 
179
            File._sharedContentTypes = loadMimeTypes()
 
180
        return File._sharedContentTypes
 
181
 
 
182
    contentTypes = property(_getContentTypes)
 
183
 
 
184
    contentEncodings = {
 
185
        ".gz" : "gzip",
 
186
        ".bz2": "bzip2"
 
187
        }
 
188
 
 
189
    processors = {}
 
190
 
 
191
    indexNames = ["index", "index.html", "index.htm", "index.trp", "index.rpy"]
 
192
 
 
193
    type = None
 
194
 
 
195
    def __init__(self, path, defaultType="text/plain", ignoredExts=(), processors=None, indexNames=None):
 
196
        """Create a file with the given path.
 
197
        """
 
198
        super(File, self).__init__()
 
199
 
 
200
        self.putChildren = {}
 
201
        self.fp = filepath.FilePath(path)
 
202
        # Remove the dots from the path to split
 
203
        self.defaultType = defaultType
 
204
        self.ignoredExts = list(ignoredExts)
 
205
        if processors is not None:
 
206
            self.processors = dict([
 
207
                (key.lower(), value)
 
208
                for key, value in processors.items()
 
209
                ])
 
210
            
 
211
        if indexNames is not None:
 
212
            self.indexNames = indexNames
 
213
 
 
214
    def exists(self):
 
215
        return self.fp.exists()
 
216
 
 
217
    def etag(self):
 
218
        if not self.fp.exists(): return None
 
219
 
 
220
        st = self.fp.statinfo
 
221
 
 
222
        #
 
223
        # Mark ETag as weak if it was modified more recently than we can
 
224
        # measure and report, as it could be modified again in that span
 
225
        # and we then wouldn't know to provide a new ETag.
 
226
        #
 
227
        weak = (time.time() - st.st_mtime <= 1)
 
228
 
 
229
        return http_headers.ETag(
 
230
            "%X-%X-%X" % (st.st_ino, st.st_size, st.st_mtime),
 
231
            weak=weak
 
232
        )
 
233
 
 
234
    def lastModified(self):
 
235
        if self.fp.exists():
 
236
            return self.fp.getmtime()
 
237
        else:
 
238
            return None
 
239
 
 
240
    def creationDate(self):
 
241
        if self.fp.exists():
 
242
            return self.fp.getmtime()
 
243
        else:
 
244
            return None
 
245
 
 
246
    def contentLength(self):
 
247
        if self.fp.exists():
 
248
            if self.fp.isfile():
 
249
                return self.fp.getsize()
 
250
            else:
 
251
                # Computing this would require rendering the resource; let's
 
252
                # punt instead.
 
253
                return None
 
254
        else:
 
255
            return None
 
256
 
 
257
    def _initTypeAndEncoding(self):
 
258
        self._type, self._encoding = getTypeAndEncoding(
 
259
            self.fp.basename(),
 
260
            self.contentTypes,
 
261
            self.contentEncodings,
 
262
            self.defaultType
 
263
        )
 
264
 
 
265
        # Handle cases not covered by getTypeAndEncoding()
 
266
        if self.fp.isdir(): self._type = "httpd/unix-directory"
 
267
 
 
268
    def contentType(self):
 
269
        if not hasattr(self, "_type"):
 
270
            self._initTypeAndEncoding()
 
271
        return http_headers.MimeType.fromString(self._type)
 
272
 
 
273
    def contentEncoding(self):
 
274
        if not hasattr(self, "_encoding"):
 
275
            self._initTypeAndEncoding()
 
276
        return self._encoding
 
277
 
 
278
    def displayName(self):
 
279
        if self.fp.exists():
 
280
            return self.fp.basename()
 
281
        else:
 
282
            return None
 
283
 
 
284
    def ignoreExt(self, ext):
 
285
        """Ignore the given extension.
 
286
 
 
287
        Serve file.ext if file is requested
 
288
        """
 
289
        self.ignoredExts.append(ext)
 
290
 
 
291
    def directoryListing(self):
 
292
        return dirlist.DirectoryLister(self.fp.path,
 
293
                                       self.listChildren(),
 
294
                                       self.contentTypes,
 
295
                                       self.contentEncodings,
 
296
                                       self.defaultType)
 
297
 
 
298
    def putChild(self, name, child):
 
299
        """
 
300
        Register a child with the given name with this resource.
 
301
        @param name: the name of the child (a URI path segment)
 
302
        @param child: the child to register
 
303
        """
 
304
        self.putChildren[name] = child
 
305
 
 
306
    def getChild(self, name):
 
307
        """
 
308
        Look up a child resource.
 
309
        @return: the child of this resource with the given name.
 
310
        """
 
311
        if name == "":
 
312
            return self
 
313
 
 
314
        child = self.putChildren.get(name, None)
 
315
        if child: return child
 
316
 
 
317
        child_fp = self.fp.child(name)
 
318
        if child_fp.exists():
 
319
            return self.createSimilarFile(child_fp.path)
 
320
        else:
 
321
            return None
 
322
 
 
323
    def listChildren(self):
 
324
        """
 
325
        @return: a sequence of the names of all known children of this resource.
 
326
        """
 
327
        children = self.putChildren.keys()
 
328
        if self.fp.isdir():
 
329
            children += [c for c in self.fp.listdir() if c not in children]
 
330
        return children
 
331
 
 
332
    def locateChild(self, req, segments):
 
333
        """
 
334
        See L{IResource}C{.locateChild}.
 
335
        """
 
336
        # If getChild() finds a child resource, return it
 
337
        child = self.getChild(segments[0])
 
338
        if child is not None: return (child, segments[1:])
 
339
        
 
340
        # If we're not backed by a directory, we have no children.
 
341
        # But check for existance first; we might be a collection resource
 
342
        # that the request wants created.
 
343
        self.fp.restat(False)
 
344
        if self.fp.exists() and not self.fp.isdir(): return (None, ())
 
345
 
 
346
        # OK, we need to return a child corresponding to the first segment
 
347
        path = segments[0]
 
348
        
 
349
        if path:
 
350
            fpath = self.fp.child(path)
 
351
        else:
 
352
            # Request is for a directory (collection) resource
 
353
            return (self, server.StopTraversal)
 
354
 
 
355
        # Don't run processors on directories - if someone wants their own
 
356
        # customized directory rendering, subclass File instead.
 
357
        if fpath.isfile():
 
358
            processor = self.processors.get(fpath.splitext()[1].lower())
 
359
            if processor:
 
360
                return (
 
361
                    processor(fpath.path),
 
362
                    segments[1:])
 
363
 
 
364
        elif not fpath.exists():
 
365
            sibling_fpath = fpath.siblingExtensionSearch(*self.ignoredExts)
 
366
            if sibling_fpath is not None:
 
367
                fpath = sibling_fpath
 
368
 
 
369
        return self.createSimilarFile(fpath.path), segments[1:]
 
370
 
 
371
    def renderHTTP(self, req):
 
372
        self.fp.restat(False)
 
373
        return super(File, self).renderHTTP(req)
 
374
 
 
375
    def render(self, req):
 
376
        """You know what you doing."""
 
377
        if not self.fp.exists():
 
378
            return responsecode.NOT_FOUND
 
379
 
 
380
        if self.fp.isdir():
 
381
            if req.uri[-1] != "/":
 
382
                # Redirect to include trailing '/' in URI
 
383
                return http.RedirectResponse(req.unparseURL(path=req.path+'/'))
 
384
            else:
 
385
                ifp = self.fp.childSearchPreauth(*self.indexNames)
 
386
                if ifp:
 
387
                    # Render from the index file
 
388
                    standin = self.createSimilarFile(ifp.path)
 
389
                else:
 
390
                    # Render from a DirectoryLister
 
391
                    standin = dirlist.DirectoryLister(
 
392
                        self.fp.path,
 
393
                        self.listChildren(),
 
394
                        self.contentTypes,
 
395
                        self.contentEncodings,
 
396
                        self.defaultType
 
397
                    )
 
398
                return standin.render(req)
 
399
 
 
400
        try:
 
401
            f = self.fp.open()
 
402
        except IOError, e:
 
403
            import errno
 
404
            if e[0] == errno.EACCES:
 
405
                return responsecode.FORBIDDEN
 
406
            elif e[0] == errno.ENOENT:
 
407
                return responsecode.NOT_FOUND
 
408
            else:
 
409
                raise
 
410
 
 
411
        response = http.Response()
 
412
        response.stream = stream.FileStream(f, 0, self.fp.getsize())
 
413
 
 
414
        for (header, value) in (
 
415
            ("content-type", self.contentType()),
 
416
            ("content-encoding", self.contentEncoding()),
 
417
        ):
 
418
            if value is not None:
 
419
                response.headers.setHeader(header, value)
 
420
 
 
421
        return response
 
422
 
 
423
    def createSimilarFile(self, path):
 
424
        return self.__class__(path, self.defaultType, self.ignoredExts,
 
425
                              self.processors, self.indexNames[:])
 
426
 
 
427
 
 
428
class FileSaver(resource.PostableResource):
 
429
    allowedTypes = (http_headers.MimeType('text', 'plain'),
 
430
                    http_headers.MimeType('text', 'html'),
 
431
                    http_headers.MimeType('text', 'css'))
 
432
    
 
433
    def __init__(self, destination, expectedFields=[], allowedTypes=None, maxBytes=1000000, permissions=0644):
 
434
        self.destination = destination
 
435
        self.allowedTypes = allowedTypes or self.allowedTypes
 
436
        self.maxBytes = maxBytes
 
437
        self.expectedFields = expectedFields
 
438
        self.permissions = permissions
 
439
 
 
440
    def makeUniqueName(self, filename):
 
441
        """Called when a unique filename is needed.
 
442
        
 
443
        filename is the name of the file as given by the client.
 
444
        
 
445
        Returns the fully qualified path of the file to create. The
 
446
        file must not yet exist.
 
447
        """
 
448
        
 
449
        return tempfile.mktemp(suffix=os.path.splitext(filename)[1], dir=self.destination)
 
450
 
 
451
    def isSafeToWrite(self, filename, mimetype, filestream):
 
452
        """Returns True if it's "safe" to write this file,
 
453
        otherwise it raises an exception.
 
454
        """
 
455
        
 
456
        if filestream.length > self.maxBytes:
 
457
            raise IOError("%s: File exceeds maximum length (%d > %d)" % (filename,
 
458
                                                                         filestream.length,
 
459
                                                                         self.maxBytes))
 
460
 
 
461
        if mimetype not in self.allowedTypes:
 
462
            raise IOError("%s: File type not allowed %s" % (filename, mimetype))
 
463
        
 
464
        return True
 
465
    
 
466
    def writeFile(self, filename, mimetype, fileobject):
 
467
        """Does the I/O dirty work after it calls isWriteable to make
 
468
        sure it's safe to write this file.
 
469
        """
 
470
        filestream = stream.FileStream(fileobject)
 
471
        
 
472
        if self.isSafeToWrite(filename, mimetype, filestream):
 
473
            outname = self.makeUniqueName(filename)
 
474
            
 
475
            fileobject = os.fdopen(os.open(outname, os.O_WRONLY | os.O_CREAT | os.O_EXCL,
 
476
                                           self.permissions), 'w', 0)
 
477
            
 
478
            stream.readIntoFile(filestream, fileobject)
 
479
 
 
480
        return outname
 
481
 
 
482
    def render(self, req):
 
483
        content = ["<html><body>"]
 
484
 
 
485
        if req.files:
 
486
            for fieldName in req.files:
 
487
                if fieldName in self.expectedFields:
 
488
                    for finfo in req.files[fieldName]:
 
489
                        try:
 
490
                            outname = self.writeFile(*finfo)
 
491
                            content.append("Saved file %s<br />" % outname)
 
492
                        except IOError, err:
 
493
                            content.append(str(err) + "<br />")
 
494
                else:
 
495
                    content.append("%s is not a valid field" % fieldName)
 
496
 
 
497
        else:
 
498
            content.append("No files given")
 
499
 
 
500
        content.append("</body></html>")
 
501
 
 
502
        return http.Response(responsecode.OK, {}, stream='\n'.join(content))
 
503
 
 
504
 
 
505
# FIXME: hi there I am a broken class
 
506
# """I contain AsIsProcessor, which serves files 'As Is'
 
507
#    Inspired by Apache's mod_asis
 
508
# """
 
509
 
510
# class ASISProcessor:
 
511
#     implements(iweb.IResource)
 
512
#     
 
513
#     def __init__(self, path):
 
514
#         self.path = path
 
515
 
516
#     def renderHTTP(self, request):
 
517
#         request.startedWriting = 1
 
518
#         return File(self.path)
 
519
 
520
#     def locateChild(self, request):
 
521
#         return None, ()
 
522
 
 
523
##
 
524
# Utilities
 
525
##
 
526
 
 
527
dangerousPathError = http.HTTPError(responsecode.NOT_FOUND) #"Invalid request URL."
 
528
 
 
529
def isDangerous(path):
 
530
    return path == '..' or '/' in path or os.sep in path
 
531
 
 
532
def addSlash(request):
 
533
    return "http%s://%s%s/" % (
 
534
        request.isSecure() and 's' or '',
 
535
        request.getHeader("host"),
 
536
        (request.uri.split('?')[0]))
 
537
 
 
538
def loadMimeTypes(mimetype_locations=['/etc/mime.types']):
 
539
    """
 
540
    Multiple file locations containing mime-types can be passed as a list.
 
541
    The files will be sourced in that order, overriding mime-types from the
 
542
    files sourced beforehand, but only if a new entry explicitly overrides
 
543
    the current entry.
 
544
    """
 
545
    import mimetypes
 
546
    # Grab Python's built-in mimetypes dictionary.
 
547
    contentTypes = mimetypes.types_map
 
548
    # Update Python's semi-erroneous dictionary with a few of the
 
549
    # usual suspects.
 
550
    contentTypes.update(
 
551
        {
 
552
            '.conf':  'text/plain',
 
553
            '.diff':  'text/plain',
 
554
            '.exe':   'application/x-executable',
 
555
            '.flac':  'audio/x-flac',
 
556
            '.java':  'text/plain',
 
557
            '.ogg':   'application/ogg',
 
558
            '.oz':    'text/x-oz',
 
559
            '.swf':   'application/x-shockwave-flash',
 
560
            '.tgz':   'application/x-gtar',
 
561
            '.wml':   'text/vnd.wap.wml',
 
562
            '.xul':   'application/vnd.mozilla.xul+xml',
 
563
            '.py':    'text/plain',
 
564
            '.patch': 'text/plain',
 
565
        }
 
566
    )
 
567
    # Users can override these mime-types by loading them out configuration
 
568
    # files (this defaults to ['/etc/mime.types']).
 
569
    for location in mimetype_locations:
 
570
        if os.path.exists(location):
 
571
            contentTypes.update(mimetypes.read_mime_types(location))
 
572
            
 
573
    return contentTypes
 
574
 
 
575
def getTypeAndEncoding(filename, types, encodings, defaultType):
 
576
    p, ext = os.path.splitext(filename)
 
577
    ext = ext.lower()
 
578
    if encodings.has_key(ext):
 
579
        enc = encodings[ext]
 
580
        ext = os.path.splitext(p)[1].lower()
 
581
    else:
 
582
        enc = None
 
583
    type = types.get(ext, defaultType)
 
584
    return type, enc
 
585
 
 
586
##
 
587
# Test code
 
588
##
 
589
 
 
590
if __name__ == '__builtin__':
 
591
    # Running from twistd -y
 
592
    from twisted.application import service, strports
 
593
    from twisted.web2 import server
 
594
    res = File('/')
 
595
    application = service.Application("demo")
 
596
    s = strports.service('8080', server.Site(res))
 
597
    s.setServiceParent(application)