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