root / trunk / twisted / web2 / static.py

Revision 25457, 18.8 kB (checked in by exarkun, 8 months ago)

Merge hashlib-2763-3

Author: wsanchez, exarkun
Reviewer: exarkun, mwhudson
Fixes: #2763

Replace uses of md5 and sha modules in Twisted with use of a new twisted.python.hashlib
module which transparently uses the new hashlib standard library module if it is available
or falls back to md5 and sha if not.

Line 
1 # Copyright (c) 2001-2008 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
13 # Sibling Imports
14 from twisted.web2 import http_headers, resource
15 from twisted.web2 import http, iweb, stream, responsecode, server, dirlist
16
17 # Twisted Imports
18 from twisted.python import filepath
19 from twisted.internet.defer import maybeDeferred
20 from zope.interface import implements
21
22 class MetaDataMixin(object):
23     """
24     Mix-in class for L{iweb.IResource} which provides methods for accessing resource
25     metadata specified by HTTP.
26     """
27     def etag(self):
28         """
29         @return: The current etag for the resource if available, None otherwise.
30         """
31         return None
32
33     def lastModified(self):
34         """
35         @return: The last modified time of the resource if available, None otherwise.
36         """
37         return None
38
39     def creationDate(self):
40         """
41         @return: The creation date of the resource if available, None otherwise.
42         """
43         return None
44
45     def contentLength(self):
46         """
47         @return: The size in bytes of the resource if available, None otherwise.
48         """
49         return None
50
51     def contentType(self):
52         """
53         @return: The MIME type of the resource if available, None otherwise.
54         """
55         return None
56
57     def contentEncoding(self):
58         """
59         @return: The encoding of the resource if available, None otherwise.
60         """
61         return None
62
63     def displayName(self):
64         """
65         @return: The display name of the resource if available, None otherwise.
66         """
67         return None
68
69     def exists(self):
70         """
71         @return: True if the resource exists on the server, False otherwise.
72         """
73         return True
74
75 class StaticRenderMixin(resource.RenderMixin, MetaDataMixin):
76     def checkPreconditions(self, request):
77         # This code replaces the code in resource.RenderMixin
78         if request.method not in ("GET", "HEAD"):
79             http.checkPreconditions(
80                 request,
81                 entityExists = self.exists(),
82                 etag = self.etag(),
83                 lastModified = self.lastModified(),
84             )
85
86         # Check per-method preconditions
87         method = getattr(self, "preconditions_" + request.method, None)
88         if method:
89             return method(request)
90
91     def renderHTTP(self, request):
92         """
93         See L{resource.RenderMixIn.renderHTTP}.
94
95         This implementation automatically sets some headers on the response
96         based on data available from L{MetaDataMixin} methods.
97         """
98         def setHeaders(response):
99             response = iweb.IResponse(response)
100
101             # Don't provide additional resource information to error responses
102             if response.code < 400:
103                 # Content-* headers refer to the response content, not
104                 # (necessarily) to the resource content, so they depend on the
105                 # request method, and therefore can't be set here.
106                 for (header, value) in (
107                     ("etag", self.etag()),
108                     ("last-modified", self.lastModified()),
109                 ):
110                     if value is not None:
111                         response.headers.setHeader(header, value)
112
113             return response
114
115         def onError(f):
116             # If we get an HTTPError, run its response through setHeaders() as
117             # well.
118             f.trap(http.HTTPError)
119             return setHeaders(f.value.response)
120
121         d = maybeDeferred(super(StaticRenderMixin, self).renderHTTP, request)
122         return d.addCallbacks(setHeaders, onError)
123
124 class Data(resource.Resource):
125     """
126     This is a static, in-memory resource.
127     """
128     def __init__(self, data, type):
129         self.data = data
130         self.type = http_headers.MimeType.fromString(type)
131         self.created_time = time.time()
132
133     def etag(self):
134         lastModified = self.lastModified()
135         return http_headers.ETag("%X-%X" % (lastModified, hash(self.data)),
136                                  weak=(time.time() - lastModified <= 1))
137
138     def lastModified(self):
139         return self.creationDate()
140
141     def creationDate(self):
142         return self.created_time
143
144     def contentLength(self):
145         return len(self.data)
146
147     def contentType(self):
148         return self.type
149
150     def render(self, req):
151         return http.Response(
152             responsecode.OK,
153             http_headers.Headers({'content-type': self.contentType()}),
154             stream=self.data)
155
156
157 class File(StaticRenderMixin):
158     """
159     File is a resource that represents a plain non-interpreted file
160     (although it can look for an extension like .rpy or .cgi and hand the
161     file to a processor for interpretation if you wish). Its constructor
162     takes a file path.
163
164     Alternatively, you can give a directory path to the constructor. In this
165     case the resource will represent that directory, and its children will
166     be files underneath that directory. This provides access to an entire
167     filesystem tree with a single Resource.
168
169     If you map the URL 'http://server/FILE' to a resource created as
170     File('/tmp'), then http://server/FILE/ will return an HTML-formatted
171     listing of the /tmp/ directory, and http://server/FILE/foo/bar.html will
172     return the contents of /tmp/foo/bar.html .
173     """
174     implements(iweb.IResource)
175
176     def _getContentTypes(self):
177         if not hasattr(File, "_sharedContentTypes"):
178             File._sharedContentTypes = loadMimeTypes()
179         return File._sharedContentTypes
180
181     contentTypes = property(_getContentTypes)
182
183     contentEncodings = {
184         ".gz" : "gzip",
185         ".bz2": "bzip2"
186         }
187
188     processors = {}
189
190     indexNames = ["index", "index.html", "index.htm", "index.trp", "index.rpy"]
191
192     type = None
193
194     def __init__(self, path, defaultType="text/plain", ignoredExts=(), processors=None, indexNames=None):
195         """Create a file with the given path.
196         """
197         super(File, self).__init__()
198
199         self.putChildren = {}
200         self.fp = filepath.FilePath(path)
201         # Remove the dots from the path to split
202         self.defaultType = defaultType
203         self.ignoredExts = list(ignoredExts)
204         if processors is not None:
205             self.processors = dict([
206                 (key.lower(), value)
207                 for key, value in processors.items()
208                 ])
209
210         if indexNames is not None:
211             self.indexNames = indexNames
212
213     def exists(self):
214         return self.fp.exists()
215
216     def etag(self):
217         if not self.fp.exists(): return None
218
219         st = self.fp.statinfo
220
221         #
222         # Mark ETag as weak if it was modified more recently than we can
223         # measure and report, as it could be modified again in that span
224         # and we then wouldn't know to provide a new ETag.
225         #
226         weak = (time.time() - st.st_mtime <= 1)
227
228         return http_headers.ETag(
229             "%X-%X-%X" % (st.st_ino, st.st_size, st.st_mtime),
230             weak=weak
231         )
232
233     def lastModified(self):
234         if self.fp.exists():
235             return self.fp.getmtime()
236         else:
237             return None
238
239     def creationDate(self):
240         if self.fp.exists():
241             return self.fp.getmtime()
242         else:
243             return None
244
245     def contentLength(self):
246         if self.fp.exists():
247             if self.fp.isfile():
248                 return self.fp.getsize()
249             else:
250                 # Computing this would require rendering the resource; let's
251                 # punt instead.
252                 return None
253         else:
254             return None
255
256     def _initTypeAndEncoding(self):
257         self._type, self._encoding = getTypeAndEncoding(
258             self.fp.basename(),
259             self.contentTypes,
260             self.contentEncodings,
261             self.defaultType
262         )
263
264         # Handle cases not covered by getTypeAndEncoding()
265         if self.fp.isdir(): self._type = "httpd/unix-directory"
266
267     def contentType(self):
268         if not hasattr(self, "_type"):
269             self._initTypeAndEncoding()
270         return http_headers.MimeType.fromString(self._type)
271
272     def contentEncoding(self):
273         if not hasattr(self, "_encoding"):
274             self._initTypeAndEncoding()
275         return self._encoding
276
277     def displayName(self):
278         if self.fp.exists():
279             return self.fp.basename()
280         else:
281             return None
282
283     def ignoreExt(self, ext):
284         """Ignore the given extension.
285
286         Serve file.ext if file is requested
287         """
288         self.ignoredExts.append(ext)
289
290     def directoryListing(self):
291         return dirlist.DirectoryLister(self.fp.path,
292                                        self.listChildren(),
293                                        self.contentTypes,
294                                        self.contentEncodings,
295                                        self.defaultType)
296
297     def putChild(self, name, child):
298         """
299         Register a child with the given name with this resource.
300         @param name: the name of the child (a URI path segment)
301         @param child: the child to register
302         """
303         self.putChildren[name] = child
304
305     def getChild(self, name):
306         """
307         Look up a child resource.
308         @return: the child of this resource with the given name.
309         """
310         if name == "":
311             return self
312
313         child = self.putChildren.get(name, None)
314         if child: return child
315
316         child_fp = self.fp.child(name)
317         if child_fp.exists():
318             return self.createSimilarFile(child_fp.path)
319         else:
320             return None
321
322     def listChildren(self):
323         """
324         @return: a sequence of the names of all known children of this resource.
325         """
326         children = self.putChildren.keys()
327         if self.fp.isdir():
328             children += [c for c in self.fp.listdir() if c not in children]
329         return children
330
331     def locateChild(self, req, segments):
332         """
333         See L{IResource}C{.locateChild}.
334         """
335         # If getChild() finds a child resource, return it
336         child = self.getChild(segments[0])
337         if child is not None: return (child, segments[1:])
338
339         # If we're not backed by a directory, we have no children.
340         # But check for existance first; we might be a collection resource
341         # that the request wants created.
342         self.fp.restat(False)
343         if self.fp.exists() and not self.fp.isdir(): return (None, ())
344
345         # OK, we need to return a child corresponding to the first segment
346         path = segments[0]
347
348         if path:
349             fpath = self.fp.child(path)
350         else:
351             # Request is for a directory (collection) resource
352             return (self, server.StopTraversal)
353
354         # Don't run processors on directories - if someone wants their own
355         # customized directory rendering, subclass File instead.
356         if fpath.isfile():
357             processor = self.processors.get(fpath.splitext()[1].lower())
358             if processor:
359                 return (
360                     processor(fpath.path),
361                     segments[1:])
362
363         elif not fpath.exists():
364             sibling_fpath = fpath.siblingExtensionSearch(*self.ignoredExts)
365             if sibling_fpath is not None:
366                 fpath = sibling_fpath
367
368         return self.createSimilarFile(fpath.path), segments[1:]
369
370     def renderHTTP(self, req):
371         self.fp.restat(False)
372         return super(File, self).renderHTTP(req)
373
374     def render(self, req):
375         """You know what you doing."""
376         if not self.fp.exists():
377             return responsecode.NOT_FOUND
378
379         if self.fp.isdir():
380             if req.uri[-1] != "/":
381                 # Redirect to include trailing '/' in URI
382                 return http.RedirectResponse(req.unparseURL(path=req.path+'/'))
383             else:
384                 ifp = self.fp.childSearchPreauth(*self.indexNames)
385                 if ifp:
386                     # Render from the index file
387                     standin = self.createSimilarFile(ifp.path)
388                 else:
389                     # Render from a DirectoryLister
390                     standin = dirlist.DirectoryLister(
391                         self.fp.path,
392                         self.listChildren(),
393                         self.contentTypes,
394                         self.contentEncodings,
395                         self.defaultType
396                     )
397                 return standin.render(req)
398
399         try:
400             f = self.fp.open()
401         except IOError, e:
402             import errno
403             if e[0] == errno.EACCES:
404                 return responsecode.FORBIDDEN
405             elif e[0] == errno.ENOENT:
406                 return responsecode.NOT_FOUND
407             else:
408                 raise
409
410         response = http.Response()
411         response.stream = stream.FileStream(f, 0, self.fp.getsize())
412
413         for (header, value) in (
414             ("content-type", self.contentType()),
415             ("content-encoding", self.contentEncoding()),
416         ):
417             if value is not None:
418                 response.headers.setHeader(header, value)
419
420         return response
421
422     def createSimilarFile(self, path):
423         return self.__class__(path, self.defaultType, self.ignoredExts,
424                               self.processors, self.indexNames[:])
425
426
427 class FileSaver(resource.PostableResource):
428     allowedTypes = (http_headers.MimeType('text', 'plain'),
429                     http_headers.MimeType('text', 'html'),
430                     http_headers.MimeType('text', 'css'))
431
432     def __init__(self, destination, expectedFields=[], allowedTypes=None, maxBytes=1000000, permissions=0644):
433         self.destination = destination
434         self.allowedTypes = allowedTypes or self.allowedTypes
435         self.maxBytes = maxBytes
436         self.expectedFields = expectedFields
437         self.permissions = permissions
438
439     def makeUniqueName(self, filename):
440         """Called when a unique filename is needed.
441
442         filename is the name of the file as given by the client.
443
444         Returns the fully qualified path of the file to create. The
445         file must not yet exist.
446         """
447
448         return tempfile.mktemp(suffix=os.path.splitext(filename)[1], dir=self.destination)
449
450     def isSafeToWrite(self, filename, mimetype, filestream):
451         """Returns True if it's "safe" to write this file,
452         otherwise it raises an exception.
453         """
454
455         if filestream.length > self.maxBytes:
456             raise IOError("%s: File exceeds maximum length (%d > %d)" % (filename,
457                                                                          filestream.length,
458                                                                          self.maxBytes))
459
460         if mimetype not in self.allowedTypes:
461             raise IOError("%s: File type not allowed %s" % (filename, mimetype))
462
463         return True
464
465     def writeFile(self, filename, mimetype, fileobject):
466         """Does the I/O dirty work after it calls isSafeToWrite to make
467         sure it's safe to write this file.
468         """
469         filestream = stream.FileStream(fileobject)
470
471         if self.isSafeToWrite(filename, mimetype, filestream):
472             outname = self.makeUniqueName(filename)
473
474             flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0)
475
476             fileobject = os.fdopen(os.open(outname, flags, self.permissions), 'wb', 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)
Note: See TracBrowser for help on using the browser.