1
# -*- test-case-name: twisted.test.test_web -*-
2
# Copyright (c) 2004 Divmod.
3
# See LICENSE for details.
5
"""I deal with static resources.
9
import os, string, time
15
from zope.interface import implements
18
from twisted.web.resource import NoResource, ForbiddenResource
20
from twisted.web.error import NoResource, ForbiddenResource
21
from twisted.web.util import redirectTo
24
from twisted.web import http
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
33
from nevow import appserver, dirlist, inevow, rend
36
dangerousPathError = NoResource("Invalid request URL.")
38
def isDangerous(path):
39
return path == '..' or '/' in path or os.sep in path
43
This is a static, in-memory resource.
45
implements(inevow.IResource)
47
def __init__(self, data, type, expires=None):
50
self.expires = expires
55
Return the current time as a float.
57
The default implementation simply uses L{time.time}. This is mainly
58
provided as a hook for tests to override.
63
def locateChild(self, ctx, segments):
64
return appserver.NotFound
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":
78
def staticHTML(someString):
79
return Data(someString, 'text/html')
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]))
88
class Registry(components.Componentized):
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.
95
components.Componentized.__init__(self)
98
def cachePath(self, path, rsrc):
99
self._pathCache[path] = rsrc
101
def getCachedPath(self, path):
102
return self._pathCache.get(path)
105
def loadMimeTypes(mimetype_locations=['/etc/mime.types']):
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
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
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',
126
'.swf': 'application/x-shockwave-flash',
127
'.tgz': 'application/x-gtar',
128
'.wml': 'text/vnd.wap.wml',
129
'.xul': 'application/vnd.mozilla.xul+xml',
131
'.patch': 'text/plain',
132
'.pjpeg': 'image/pjpeg',
133
'.tac': 'text/x-python',
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))
144
def getTypeAndEncoding(filename, types, encodings, defaultType):
145
p, ext = os.path.splitext(filename)
147
if encodings.has_key(ext):
149
ext = os.path.splitext(p)[1].lower()
152
type = types.get(ext, defaultType)
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
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.
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 .
173
implements(inevow.IResource)
175
contentTypes = loadMimeTypes()
178
".gz" : "application/x-gzip",
179
".bz2": "application/x-bzip2"
184
indexNames = ["index", "index.html", "index.htm", "index.trp", "index.rpy"]
188
def __init__(self, path, defaultType="text/html", ignoredExts=(), registry=None, allowExt=0):
189
"""Create a file with the given path.
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 = ['*']
199
self.ignoredExts = []
201
self.ignoredExts = list(ignoredExts)
202
self.registry = registry or Registry()
205
def ignoreExt(self, ext):
206
"""Ignore the given extension.
208
Serve file.ext if file is requested
210
self.ignoredExts.append(ext)
212
def directoryListing(self):
213
return dirlist.DirectoryLister(self.fp.path,
216
self.contentEncodings,
219
def putChild(self, name, child):
220
self.children[name] = child
222
def locateChild(self, ctx, segments):
223
r = self.children.get(segments[0], None)
225
return r, segments[1:]
231
if not self.fp.isdir():
235
fpath = self.fp.child(path)
237
fpath = self.fp.childSearchPreauth(*self.indexNames)
239
return self.directoryListing(), segments[1:]
241
if not fpath.exists():
242
fpath = fpath.siblingExtensionSearch(*self.ignoredExts)
246
# Don't run processors on directories - if someone wants their own
247
# customized directory rendering, subclass File instead.
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])
254
processor = self.processors.get(fpath.splitext()[1])
257
inevow.IResource(processor(fpath.path, self.registry)),
260
return self.createSimilarFile(fpath.path), segments[1:]
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()
267
def getFileSize(self):
268
"""Return file size."""
269
return self.fp.getsize()
272
def renderHTTP(self, ctx):
273
"""You know what you doing."""
276
if self.type is None:
277
self.type, self.encoding = getTypeAndEncoding(self.fp.basename(),
279
self.contentEncodings,
282
if not self.fp.exists():
283
return rend.FourOhFour()
285
request = inevow.IRequest(ctx)
288
return self.redirect(request)
290
# fsize is the full file size
291
# size is the length of the part actually transmitted
292
fsize = size = self.getFileSize()
294
request.setHeader('accept-ranges','bytes')
297
request.setHeader('content-type', self.type)
299
request.setHeader('content-encoding', self.encoding)
302
f = self.openForReading()
305
if e[0] == errno.EACCES:
306
return ForbiddenResource().render(request)
310
if request.setLastModified(self.fp.getmtime()) is http.CACHED:
314
range = request.getHeader('range')
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],'-')
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)
335
request.setHeader('content-length', str(size))
337
traceback.print_exc(file=log.logfile)
339
if request.method == 'HEAD':
343
FileTransfer(f, size, request)
344
# and make sure the connection doesn't get closed
345
return request.deferred
347
def redirect(self, request):
348
return redirectTo(addSlash(request), request)
351
if not self.fp.isdir():
353
directory = self.fp.listdir()
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[:]
365
class FileTransfer(pb.Viewable):
367
A class to represent the transfer of a file over the network.
370
def __init__(self, file, size, request):
373
self.request = request
374
request.registerProducer(self, 0)
376
def resumeProducing(self):
379
data = self.file.read(min(abstract.FileDescriptor.bufferSize, self.size))
381
self.request.write(data)
382
self.size -= len(data)
384
self.request.unregisterProducer()
385
self.request.finish()
388
def pauseProducing(self):
391
def stopProducing(self):
395
# Remotely relay producer interface.
397
def view_resumeProducing(self, issuer):
398
self.resumeProducing()
400
def view_pauseProducing(self, issuer):
401
self.pauseProducing()
403
def view_stopProducing(self, issuer):
407
synchronized = ['resumeProducing', 'stopProducing']
409
threadable.synchronize(FileTransfer)
411
"""I contain AsIsProcessor, which serves files 'As Is'
412
Inspired by Apache's mod_asis
416
implements(inevow.IResource)
418
def __init__(self, path, registry=None):
420
self.registry = registry or Registry()
422
def renderHTTP(self, ctx):
423
request = inevow.IRequest(ctx)
424
request.startedWriting = 1
425
return File(self.path, registry=self.registry)
427
def locateChild(self, ctx, segments):
428
return appserver.NotFound