1 """Simple HTTP Server. 2 3 This module builds on BaseHTTPServer by implementing the standard GET 4 and HEAD requests in a fairly straightforward manner. 5 6 """ 7 8 9 __version__ = "0.6" 10 11 __all__ = ["SimpleHTTPRequestHandler"] 12 13 import os 14 import posixpath 15 import BaseHTTPServer 16 import urllib 17 import urlparse 18 import cgi 19 import sys 20 import shutil 21 import mimetypes 22 try: 23 from cStringIO import StringIO 24 except ImportError: 25 from StringIO import StringIO 26 27 28 class 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 if os.path.dirname(word) or word in (os.curdir, os.pardir): 171 # Ignore components that are not a simple file/directory name 172 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 229 def test(HandlerClass = SimpleHTTPRequestHandler, 230 ServerClass = BaseHTTPServer.HTTPServer): 231 BaseHTTPServer.test(HandlerClass, ServerClass) 232 233 234 if __name__ == '__main__': 235 test() 236