~divmod-dev/divmod.org/trunk

« back to all changes in this revision

Viewing changes to Nevow/nevow/static.py

  • Committer: Jean-Paul Calderone
  • Date: 2014-06-29 20:33:04 UTC
  • mfrom: (2749.1.1 remove-epsilon-1325289)
  • Revision ID: exarkun@twistedmatrix.com-20140629203304-gdkmbwl1suei4m97
mergeĀ lp:~exarkun/divmod.org/remove-epsilon-1325289

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- test-case-name: twisted.test.test_web -*-
2
 
# Copyright (c) 2004 Divmod.
3
 
# See LICENSE for details.
4
 
 
5
 
"""I deal with static resources.
6
 
"""
7
 
 
8
 
# System Imports
9
 
import os, string, time
10
 
import cStringIO
11
 
import traceback
12
 
import warnings
13
 
StringIO = cStringIO
14
 
del cStringIO
15
 
from zope.interface import implements
16
 
 
17
 
try:
18
 
    from twisted.web.resource import NoResource, ForbiddenResource
19
 
except ImportError:
20
 
    from twisted.web.error import NoResource, ForbiddenResource
21
 
from twisted.web.util import redirectTo
22
 
 
23
 
try:
24
 
    from twisted.web import http
25
 
except ImportError:
26
 
    from twisted.protocols import http
27
 
from twisted.python import threadable, log, components, filepath
28
 
from twisted.internet import abstract
29
 
from twisted.spread import pb
30
 
from twisted.python.util import InsensitiveDict
31
 
from twisted.python.runtime import platformType
32
 
 
33
 
from nevow import appserver, dirlist, inevow, rend
34
 
 
35
 
 
36
 
dangerousPathError = NoResource("Invalid request URL.")
37
 
 
38
 
def isDangerous(path):
39
 
    return path == '..' or '/' in path or os.sep in path
40
 
 
41
 
class Data:
42
 
    """
43
 
    This is a static, in-memory resource.
44
 
    """
45
 
    implements(inevow.IResource)
46
 
 
47
 
    def __init__(self, data, type, expires=None):
48
 
        self.data = data
49
 
        self.type = type
50
 
        self.expires = expires
51
 
 
52
 
 
53
 
    def time(self):
54
 
        """
55
 
        Return the current time as a float.
56
 
 
57
 
        The default implementation simply uses L{time.time}.  This is mainly
58
 
        provided as a hook for tests to override.
59
 
        """
60
 
        return time.time()
61
 
 
62
 
 
63
 
    def locateChild(self, ctx, segments):
64
 
        return appserver.NotFound
65
 
 
66
 
 
67
 
    def renderHTTP(self, ctx):
68
 
        request = inevow.IRequest(ctx)
69
 
        request.setHeader("content-type", self.type)
70
 
        request.setHeader("content-length", str(len(self.data)))
71
 
        if self.expires is not None:
72
 
            request.setHeader("expires",
73
 
                              http.datetimeToString(self.time() + self.expires))
74
 
        if request.method == "HEAD":
75
 
            return ''
76
 
        return self.data
77
 
 
78
 
def staticHTML(someString):
79
 
    return Data(someString, 'text/html')
80
 
 
81
 
 
82
 
def addSlash(request):
83
 
    return "http%s://%s%s/" % (
84
 
        request.isSecure() and 's' or '',
85
 
        request.getHeader("host"),
86
 
        (string.split(request.uri,'?')[0]))
87
 
 
88
 
class Registry(components.Componentized):
89
 
    """
90
 
    I am a Componentized object that will be made available to internal Twisted
91
 
    file-based dynamic web content such as .rpy and .epy scripts.
92
 
    """
93
 
 
94
 
    def __init__(self):
95
 
        components.Componentized.__init__(self)
96
 
        self._pathCache = {}
97
 
 
98
 
    def cachePath(self, path, rsrc):
99
 
        self._pathCache[path] = rsrc
100
 
 
101
 
    def getCachedPath(self, path):
102
 
        return self._pathCache.get(path)
103
 
 
104
 
 
105
 
def loadMimeTypes(mimetype_locations=['/etc/mime.types']):
106
 
    """
107
 
    Multiple file locations containing mime-types can be passed as a list.
108
 
    The files will be sourced in that order, overriding mime-types from the
109
 
    files sourced beforehand, but only if a new entry explicitly overrides
110
 
    the current entry.
111
 
    """
112
 
    import mimetypes
113
 
    # Grab Python's built-in mimetypes dictionary.
114
 
    contentTypes = mimetypes.types_map
115
 
    # Update Python's semi-erroneous dictionary with a few of the
116
 
    # usual suspects.
117
 
    contentTypes.update(
118
 
        {
119
 
            '.conf':  'text/plain',
120
 
            '.diff':  'text/plain',
121
 
            '.exe':   'application/x-executable',
122
 
            '.flac':  'audio/x-flac',
123
 
            '.java':  'text/plain',
124
 
            '.ogg':   'application/ogg',
125
 
            '.oz':    'text/x-oz',
126
 
            '.swf':   'application/x-shockwave-flash',
127
 
            '.tgz':   'application/x-gtar',
128
 
            '.wml':   'text/vnd.wap.wml',
129
 
            '.xul':   'application/vnd.mozilla.xul+xml',
130
 
            '.py':    'text/plain',
131
 
            '.patch': 'text/plain',
132
 
            '.pjpeg': 'image/pjpeg',
133
 
            '.tac':   'text/x-python',
134
 
        }
135
 
    )
136
 
    # Users can override these mime-types by loading them out configuration
137
 
    # files (this defaults to ['/etc/mime.types']).
138
 
    for location in mimetype_locations:
139
 
        if os.path.exists(location):
140
 
            contentTypes.update(mimetypes.read_mime_types(location))
141
 
            
142
 
    return contentTypes
143
 
 
144
 
def getTypeAndEncoding(filename, types, encodings, defaultType):
145
 
    p, ext = os.path.splitext(filename)
146
 
    ext = ext.lower()
147
 
    if encodings.has_key(ext):
148
 
        enc = encodings[ext]
149
 
        ext = os.path.splitext(p)[1].lower()
150
 
    else:
151
 
        enc = None
152
 
    type = types.get(ext, defaultType)
153
 
    return type, enc
154
 
 
155
 
class File:
156
 
    """
157
 
    File is a resource that represents a plain non-interpreted file
158
 
    (although it can look for an extension like .rpy or .cgi and hand the
159
 
    file to a processor for interpretation if you wish). Its constructor
160
 
    takes a file path.
161
 
 
162
 
    Alternatively, you can give a directory path to the constructor. In this
163
 
    case the resource will represent that directory, and its children will
164
 
    be files underneath that directory. This provides access to an entire
165
 
    filesystem tree with a single Resource.
166
 
 
167
 
    If you map the URL 'http://server/FILE' to a resource created as
168
 
    File('/tmp'), then http://server/FILE/ will return an HTML-formatted
169
 
    listing of the /tmp/ directory, and http://server/FILE/foo/bar.html will
170
 
    return the contents of /tmp/foo/bar.html .
171
 
    """
172
 
 
173
 
    implements(inevow.IResource)
174
 
 
175
 
    contentTypes = loadMimeTypes()
176
 
 
177
 
    contentEncodings = {
178
 
        ".gz" : "application/x-gzip",
179
 
        ".bz2": "application/x-bzip2"
180
 
        }
181
 
 
182
 
    processors = {}
183
 
 
184
 
    indexNames = ["index", "index.html", "index.htm", "index.trp", "index.rpy"]
185
 
 
186
 
    type = None
187
 
 
188
 
    def __init__(self, path, defaultType="text/html", ignoredExts=(), registry=None, allowExt=0):
189
 
        """Create a file with the given path.
190
 
        """
191
 
        self.fp = filepath.FilePath(path)
192
 
        # Remove the dots from the path to split
193
 
        self.defaultType = defaultType
194
 
        if ignoredExts in (0, 1) or allowExt:
195
 
            warnings.warn("ignoredExts should receive a list, not a boolean")
196
 
            if ignoredExts or allowExt:
197
 
                self.ignoredExts = ['*']
198
 
            else:
199
 
                self.ignoredExts = []
200
 
        else:
201
 
            self.ignoredExts = list(ignoredExts)
202
 
        self.registry = registry or Registry()
203
 
        self.children = {}
204
 
 
205
 
    def ignoreExt(self, ext):
206
 
        """Ignore the given extension.
207
 
 
208
 
        Serve file.ext if file is requested
209
 
        """
210
 
        self.ignoredExts.append(ext)
211
 
 
212
 
    def directoryListing(self):
213
 
        return dirlist.DirectoryLister(self.fp.path,
214
 
                                       self.listNames(),
215
 
                                       self.contentTypes,
216
 
                                       self.contentEncodings,
217
 
                                       self.defaultType)
218
 
 
219
 
    def putChild(self, name, child):
220
 
        self.children[name] = child
221
 
        
222
 
    def locateChild(self, ctx, segments):
223
 
        r = self.children.get(segments[0], None)
224
 
        if r:
225
 
            return r, segments[1:]
226
 
        
227
 
        path=segments[0]
228
 
        
229
 
        self.fp.restat()
230
 
        
231
 
        if not self.fp.isdir():
232
 
            return rend.NotFound
233
 
 
234
 
        if path:
235
 
            fpath = self.fp.child(path)
236
 
        else:
237
 
            fpath = self.fp.childSearchPreauth(*self.indexNames)
238
 
            if fpath is None:
239
 
                return self.directoryListing(), segments[1:]
240
 
 
241
 
        if not fpath.exists():
242
 
            fpath = fpath.siblingExtensionSearch(*self.ignoredExts)
243
 
            if fpath is None:
244
 
                return rend.NotFound
245
 
 
246
 
        # Don't run processors on directories - if someone wants their own
247
 
        # customized directory rendering, subclass File instead.
248
 
        if fpath.isfile():
249
 
            if platformType == "win32":
250
 
                # don't want .RPY to be different than .rpy, since that
251
 
                # would allow source disclosure.
252
 
                processor = InsensitiveDict(self.processors).get(fpath.splitext()[1])
253
 
            else:
254
 
                processor = self.processors.get(fpath.splitext()[1])
255
 
            if processor:
256
 
                return (
257
 
                    inevow.IResource(processor(fpath.path, self.registry)),
258
 
                    segments[1:])
259
 
 
260
 
        return self.createSimilarFile(fpath.path), segments[1:]
261
 
 
262
 
    # methods to allow subclasses to e.g. decrypt files on the fly:
263
 
    def openForReading(self):
264
 
        """Open a file and return it."""
265
 
        return self.fp.open()
266
 
 
267
 
    def getFileSize(self):
268
 
        """Return file size."""
269
 
        return self.fp.getsize()
270
 
 
271
 
 
272
 
    def renderHTTP(self, ctx):
273
 
        """You know what you doing."""
274
 
        self.fp.restat()
275
 
 
276
 
        if self.type is None:
277
 
            self.type, self.encoding = getTypeAndEncoding(self.fp.basename(),
278
 
                                                          self.contentTypes,
279
 
                                                          self.contentEncodings,
280
 
                                                          self.defaultType)
281
 
 
282
 
        if not self.fp.exists():
283
 
            return rend.FourOhFour()
284
 
 
285
 
        request = inevow.IRequest(ctx)
286
 
 
287
 
        if self.fp.isdir():
288
 
            return self.redirect(request)
289
 
 
290
 
        # fsize is the full file size
291
 
        # size is the length of the part actually transmitted
292
 
        fsize = size = self.getFileSize()
293
 
 
294
 
        request.setHeader('accept-ranges','bytes')
295
 
 
296
 
        if self.type:
297
 
            request.setHeader('content-type', self.type)
298
 
        if self.encoding:
299
 
            request.setHeader('content-encoding', self.encoding)
300
 
 
301
 
        try:
302
 
            f = self.openForReading()
303
 
        except IOError, e:
304
 
            import errno
305
 
            if e[0] == errno.EACCES:
306
 
                return ForbiddenResource().render(request)
307
 
            else:
308
 
                raise
309
 
 
310
 
        if request.setLastModified(self.fp.getmtime()) is http.CACHED:
311
 
            return ''
312
 
 
313
 
        try:
314
 
            range = request.getHeader('range')
315
 
 
316
 
            if range is not None:
317
 
                # This is a request for partial data...
318
 
                bytesrange = string.split(range, '=')
319
 
                assert bytesrange[0] == 'bytes',\
320
 
                       "Syntactically invalid http range header!"
321
 
                start, end = string.split(bytesrange[1],'-')
322
 
                if start:
323
 
                    f.seek(int(start))
324
 
                if end:
325
 
                    end = int(end)
326
 
                else:
327
 
                    end = fsize-1
328
 
                request.setResponseCode(http.PARTIAL_CONTENT)
329
 
                request.setHeader('content-range',"bytes %s-%s/%s" % (
330
 
                    str(start), str(end), str(fsize)))
331
 
                #content-length should be the actual size of the stuff we're
332
 
                #sending, not the full size of the on-server entity.
333
 
                size = 1 + end - int(start)
334
 
 
335
 
            request.setHeader('content-length', str(size))
336
 
        except:
337
 
            traceback.print_exc(file=log.logfile)
338
 
 
339
 
        if request.method == 'HEAD':
340
 
            return ''
341
 
 
342
 
        # return data
343
 
        FileTransfer(f, size, request)
344
 
        # and make sure the connection doesn't get closed
345
 
        return request.deferred
346
 
 
347
 
    def redirect(self, request):
348
 
        return redirectTo(addSlash(request), request)
349
 
 
350
 
    def listNames(self):
351
 
        if not self.fp.isdir():
352
 
            return []
353
 
        directory = self.fp.listdir()
354
 
        directory.sort()
355
 
        return directory
356
 
 
357
 
    def createSimilarFile(self, path):
358
 
        f = self.__class__(path, self.defaultType, self.ignoredExts, self.registry)
359
 
        # refactoring by steps, here - constructor should almost certainly take these
360
 
        f.processors = self.processors
361
 
        f.indexNames = self.indexNames[:]
362
 
        return f
363
 
 
364
 
 
365
 
class FileTransfer(pb.Viewable):
366
 
    """
367
 
    A class to represent the transfer of a file over the network.
368
 
    """
369
 
    request = None
370
 
    def __init__(self, file, size, request):
371
 
        self.file = file
372
 
        self.size = size
373
 
        self.request = request
374
 
        request.registerProducer(self, 0)
375
 
 
376
 
    def resumeProducing(self):
377
 
        if not self.request:
378
 
            return
379
 
        data = self.file.read(min(abstract.FileDescriptor.bufferSize, self.size))
380
 
        if data:
381
 
            self.request.write(data)
382
 
            self.size -= len(data)
383
 
        if self.size <= 0:
384
 
            self.request.unregisterProducer()
385
 
            self.request.finish()
386
 
            self.request = None
387
 
 
388
 
    def pauseProducing(self):
389
 
        pass
390
 
 
391
 
    def stopProducing(self):
392
 
        self.file.close()
393
 
        self.request = None
394
 
 
395
 
    # Remotely relay producer interface.
396
 
 
397
 
    def view_resumeProducing(self, issuer):
398
 
        self.resumeProducing()
399
 
 
400
 
    def view_pauseProducing(self, issuer):
401
 
        self.pauseProducing()
402
 
 
403
 
    def view_stopProducing(self, issuer):
404
 
        self.stopProducing()
405
 
 
406
 
 
407
 
    synchronized = ['resumeProducing', 'stopProducing']
408
 
 
409
 
threadable.synchronize(FileTransfer)
410
 
 
411
 
"""I contain AsIsProcessor, which serves files 'As Is'
412
 
   Inspired by Apache's mod_asis
413
 
"""
414
 
 
415
 
class ASISProcessor:
416
 
    implements(inevow.IResource)
417
 
    
418
 
    def __init__(self, path, registry=None):
419
 
        self.path = path
420
 
        self.registry = registry or Registry()
421
 
 
422
 
    def renderHTTP(self, ctx):
423
 
        request = inevow.IRequest(ctx)
424
 
        request.startedWriting = 1
425
 
        return File(self.path, registry=self.registry)
426
 
 
427
 
    def locateChild(self, ctx, segments):
428
 
        return appserver.NotFound