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 |