1"""Simple HTTP Server.
2
3This module builds on BaseHTTPServer by implementing the standard GET
4and HEAD requests in a fairly straightforward manner.
5
6"""
7
8
9__version__ = "0.6"
10
11__all__ = ["SimpleHTTPRequestHandler"]
12
13import os
14import posixpath
15import BaseHTTPServer
16import urllib
17import cgi
18import sys
19import shutil
20import mimetypes
21try:
22    from cStringIO import StringIO
23except ImportError:
24    from StringIO import StringIO
25
26
27class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
28
29    """Simple HTTP request handler with GET and HEAD commands.
30
31    This serves files from the current directory and any of its
32    subdirectories.  The MIME type for files is determined by
33    calling the .guess_type() method.
34
35    The GET and HEAD requests are identical except that the HEAD
36    request omits the actual contents of the file.
37
38    """
39
40    server_version = "SimpleHTTP/" + __version__
41
42    def do_GET(self):
43        """Serve a GET request."""
44        f = self.send_head()
45        if f:
46            self.copyfile(f, self.wfile)
47            f.close()
48
49    def do_HEAD(self):
50        """Serve a HEAD request."""
51        f = self.send_head()
52        if f:
53            f.close()
54
55    def send_head(self):
56        """Common code for GET and HEAD commands.
57
58        This sends the response code and MIME headers.
59
60        Return value is either a file object (which has to be copied
61        to the outputfile by the caller unless the command was HEAD,
62        and must be closed by the caller under all circumstances), or
63        None, in which case the caller has nothing further to do.
64
65        """
66        path = self.translate_path(self.path)
67        f = None
68        if os.path.isdir(path):
69            if not self.path.endswith('/'):
70                # redirect browser - doing basically what apache does
71                self.send_response(301)
72                self.send_header("Location", self.path + "/")
73                self.end_headers()
74                return None
75            for index in "index.html", "index.htm":
76                index = os.path.join(path, index)
77                if os.path.exists(index):
78                    path = index
79                    break
80            else:
81                return self.list_directory(path)
82        ctype = self.guess_type(path)
83        try:
84            # Always read in binary mode. Opening files in text mode may cause
85            # newline translations, making the actual size of the content
86            # transmitted *less* than the content-length!
87            f = open(path, 'rb')
88        except IOError:
89            self.send_error(404, "File not found")
90            return None
91        self.send_response(200)
92        self.send_header("Content-type", ctype)
93        fs = os.fstat(f.fileno())
94        self.send_header("Content-Length", str(fs[6]))
95        self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
96        self.end_headers()
97        return f
98
99    def list_directory(self, path):
100        """Helper to produce a directory listing (absent index.html).
101
102        Return value is either a file object, or None (indicating an
103        error).  In either case, the headers are sent, making the
104        interface the same as for send_head().
105
106        """
107        try:
108            list = os.listdir(path)
109        except os.error:
110            self.send_error(404, "No permission to list directory")
111            return None
112        list.sort(key=lambda a: a.lower())
113        f = StringIO()
114        displaypath = cgi.escape(urllib.unquote(self.path))
115        f.write('<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">')
116        f.write("<html>\n<title>Directory listing for %s</title>\n" % displaypath)
117        f.write("<body>\n<h2>Directory listing for %s</h2>\n" % displaypath)
118        f.write("<hr>\n<ul>\n")
119        for name in list:
120            fullname = os.path.join(path, name)
121            displayname = linkname = name
122            # Append / for directories or @ for symbolic links
123            if os.path.isdir(fullname):
124                displayname = name + "/"
125                linkname = name + "/"
126            if os.path.islink(fullname):
127                displayname = name + "@"
128                # Note: a link to a directory displays with @ and links with /
129            f.write('<li><a href="%s">%s</a>\n'
130                    % (urllib.quote(linkname), cgi.escape(displayname)))
131        f.write("</ul>\n<hr>\n</body>\n</html>\n")
132        length = f.tell()
133        f.seek(0)
134        self.send_response(200)
135        encoding = sys.getfilesystemencoding()
136        self.send_header("Content-type", "text/html; charset=%s" % encoding)
137        self.send_header("Content-Length", str(length))
138        self.end_headers()
139        return f
140
141    def translate_path(self, path):
142        """Translate a /-separated PATH to the local filename syntax.
143
144        Components that mean special things to the local file system
145        (e.g. drive or directory names) are ignored.  (XXX They should
146        probably be diagnosed.)
147
148        """
149        # abandon query parameters
150        path = path.split('?',1)[0]
151        path = path.split('#',1)[0]
152        path = posixpath.normpath(urllib.unquote(path))
153        words = path.split('/')
154        words = filter(None, words)
155        path = os.getcwd()
156        for word in words:
157            drive, word = os.path.splitdrive(word)
158            head, word = os.path.split(word)
159            if word in (os.curdir, os.pardir): continue
160            path = os.path.join(path, word)
161        return path
162
163    def copyfile(self, source, outputfile):
164        """Copy all data between two file objects.
165
166        The SOURCE argument is a file object open for reading
167        (or anything with a read() method) and the DESTINATION
168        argument is a file object open for writing (or
169        anything with a write() method).
170
171        The only reason for overriding this would be to change
172        the block size or perhaps to replace newlines by CRLF
173        -- note however that this the default server uses this
174        to copy binary data as well.
175
176        """
177        shutil.copyfileobj(source, outputfile)
178
179    def guess_type(self, path):
180        """Guess the type of a file.
181
182        Argument is a PATH (a filename).
183
184        Return value is a string of the form type/subtype,
185        usable for a MIME Content-type header.
186
187        The default implementation looks the file's extension
188        up in the table self.extensions_map, using application/octet-stream
189        as a default; however it would be permissible (if
190        slow) to look inside the data to make a better guess.
191
192        """
193
194        base, ext = posixpath.splitext(path)
195        if ext in self.extensions_map:
196            return self.extensions_map[ext]
197        ext = ext.lower()
198        if ext in self.extensions_map:
199            return self.extensions_map[ext]
200        else:
201            return self.extensions_map['']
202
203    if not mimetypes.inited:
204        mimetypes.init() # try to read system mime.types
205    extensions_map = mimetypes.types_map.copy()
206    extensions_map.update({
207        '': 'application/octet-stream', # Default
208        '.py': 'text/plain',
209        '.c': 'text/plain',
210        '.h': 'text/plain',
211        })
212
213
214def test(HandlerClass = SimpleHTTPRequestHandler,
215         ServerClass = BaseHTTPServer.HTTPServer):
216    BaseHTTPServer.test(HandlerClass, ServerClass)
217
218
219if __name__ == '__main__':
220    test()
221