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