├── .gitignore ├── LICENSE ├── README └── httpserver.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-present Sanic Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | httpserver 2 | ======================================= 3 | This httpserver is a enhanced version of SimpleHTTPServer. 4 | 5 | It was write in python, I use some code from bottle[https://github.com/defnull/bottle] 6 | 7 | It support resuming download, you can set the document root, it has more 8 | 9 | friendly error hit, and it can handle mimetype gracefully 10 | -------------------------------------------------------------------------------- /httpserver.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | #coding=utf-8 3 | 4 | ''' 5 | by Lerry http://lerry.org 6 | Start from 2011/07/27 22:49:51 7 | Last edit at 2012/09/29 8 | ''' 9 | import os 10 | import posixpath 11 | import urllib 12 | import mimetypes 13 | import email.utils 14 | import shutil 15 | import time 16 | from BaseHTTPServer import HTTPServer 17 | from SimpleHTTPServer import SimpleHTTPRequestHandler 18 | from SocketServer import ThreadingMixIn 19 | try: 20 | from cStringIO import StringIO 21 | except ImportError: 22 | from StringIO import StringIO 23 | 24 | def parse_date(ims): 25 | """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ 26 | try: 27 | ts = email.utils.parsedate_tz(ims) 28 | return time.mktime(ts[:8] + (0,)) - (ts[9] or 0) - time.timezone 29 | except (TypeError, ValueError, IndexError, OverflowError): 30 | return None 31 | 32 | def get_mime_type(filename): 33 | #get mimetype by filename, if none, see as bin file 34 | mime, encoding = mimetypes.guess_type(filename) 35 | #add type not supported by gues_type 36 | if filename.split('.')[-1] in ('py','conf','ini','md','log','vim'): 37 | mime = 'text/plain' 38 | return 'application/octet-stream' if not mime else mime 39 | 40 | def parse_range_header(header, flen=0): 41 | ranges = header['range'] 42 | start, end = ranges.strip('bytes=').split('-') 43 | #print start, end 44 | try: 45 | if not start: # bytes=-100 -> last 100 bytes 46 | start, end = max(0, flen-int(end)), flen 47 | elif not end: # bytes=100- -> all but the first 99 bytes 48 | start, end = int(start), flen 49 | else: # bytes=100-200 -> bytes 100-200 (inclusive) 50 | start, end = int(start), min(int(end)+1, flen) 51 | if 0 <= start < end <= flen: 52 | return start, end 53 | except ValueError: 54 | pass 55 | return None,None 56 | 57 | 58 | def _file_iter_range(fp, offset, bytes, maxread=1024*1024): 59 | ''' Yield chunks from a range in a file. No chunk is bigger than maxread.''' 60 | fp.seek(offset) 61 | return fp.read(bytes) 62 | while bytes > 0: 63 | part = fp.read(min(bytes, maxread)) 64 | if not part: break 65 | bytes -= len(part) 66 | #yield part 67 | 68 | def get_handler(root_path): 69 | class _RerootedHTTPRequestHandler(SimpleHTTPRequestHandler): 70 | def send_response1(self, code, message=None): 71 | """Send the response header and log the response code. 72 | 73 | Also send two standard headers with the server software 74 | version and the current date. 75 | 76 | """ 77 | self.log_request(code) 78 | if message is None: 79 | if code in self.responses: 80 | message = self.responses[code][0] 81 | else: 82 | message = '' 83 | if self.request_version != 'HTTP/0.9': 84 | self.wfile.write("%s %d %s\r\n" % 85 | (self.protocol_version, code, message)) 86 | # print (self.protocol_version, code, message) 87 | self.send_header('Server', self.version_string()) 88 | self.send_header('Date', self.date_time_string()) 89 | 90 | def send_header1(self, keyword, value): 91 | """Send a MIME header.""" 92 | if self.request_version != 'HTTP/0.9': 93 | self.wfile.write("%s: %s\r\n" % (keyword, value)) 94 | 95 | if keyword.lower() == 'connection': 96 | if value.lower() == 'close': 97 | self.close_connection = 1 98 | elif value.lower() == 'keep-alive': 99 | self.close_connection = 0 100 | 101 | def copyfile(self, src_data, dst): 102 | shutil.copyfileobj(src_data, dst) 103 | #dst.write(src_data) 104 | 105 | def do_GET(self): 106 | """Serve a GET request.""" 107 | f = self.send_head() 108 | if f: 109 | self.copyfile(f, self.wfile) 110 | f.close() 111 | 112 | def do_HEAD(self): 113 | """Serve a HEAD request.""" 114 | f = self.send_head() 115 | if f: 116 | f.close() 117 | def send_head(self): 118 | """Common code for GET and HEAD commands. 119 | 120 | This sends the response code and MIME headers. 121 | 122 | Return value is either a file object (which has to be copied 123 | to the outputfile by the caller unless the command was HEAD, 124 | and must be closed by the caller under all circumstances), or 125 | None, in which case the caller has nothing further to do. 126 | 127 | """ 128 | path = self.translate_path(self.path) 129 | f = None 130 | if os.path.isdir(path): 131 | if not self.path.endswith('/'): 132 | # redirect browser - doing basically what apache does 133 | self.send_response(301) 134 | self.send_header("Location", self.path + "/") 135 | self.end_headers() 136 | return None 137 | for index in "index.html", "index.htm": 138 | index = os.path.join(path, index) 139 | if os.path.exists(index): 140 | path = index 141 | break 142 | else: 143 | #print self.list_directory(path) 144 | return self.list_directory(path) 145 | 146 | mimetype = get_mime_type(path) 147 | root = os.path.abspath(root_path) 148 | 149 | if not path.startswith(root): 150 | self.send_error(403, "Access denied.") 151 | return None 152 | if not os.path.exists(path) or not os.path.isfile(path): 153 | self.send_error(404, "File does not exist.") 154 | return None 155 | if not os.access(path, os.R_OK): 156 | self.send_error(403, "You do not have permission to access this file.") 157 | return None 158 | 159 | headers = dict(self.headers) 160 | fs = os.stat(path) 161 | 162 | self.send_header("Content-Length", str(fs[6])) 163 | self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) 164 | self.send_header("Accept-Ranges", "bytes") 165 | 166 | if 'if-modified-since' in headers: 167 | ims = headers['if-modified-since'] 168 | ims = parse_date(ims.split(";")[0].strip()) 169 | if ims >= int(fs.st_mtime): 170 | self.send_response(304) 171 | self.send_header('Date', time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime())) 172 | self.end_headers() 173 | return None 174 | 175 | if 'range' in headers: 176 | self.send_response(206) 177 | start, end = parse_range_header(headers, fs.st_size) 178 | if start!=None and end!=None: 179 | f = open(path, 'rb') 180 | #if f: f = _file_iter_range(f, start, end-start) 181 | f.seek(start) 182 | f = f.read(end-start) 183 | self.send_header("Content-Range","bytes %d-%d/%d" % (start, end-1, fs.st_size)) 184 | self.send_header("Content-Length", str(end-start)) 185 | else: 186 | self.send_error(416, "Requested Range Not Satisfiable") 187 | return None 188 | else: 189 | self.send_response(200) 190 | f = open(path, 'rb').read() 191 | if mimetype: 192 | self.send_header("Content-type", mimetype) 193 | #if encoding: 194 | # self.send_header("Content-Encoding", encoding) 195 | self.end_headers() 196 | result = StringIO() 197 | result.write(f) 198 | result.seek(0) 199 | return result 200 | 201 | def translate_path(self, path): 202 | path = path.split('?',1)[0] 203 | path = path.split('#',1)[0] 204 | path = posixpath.normpath(urllib.unquote(path)) 205 | words = path.split('/') 206 | words = filter(None, words) 207 | path = root_path 208 | for word in words: 209 | drive, word = os.path.splitdrive(word) 210 | head, word = os.path.split(word) 211 | if word in (os.curdir, os.pardir): continue 212 | path = os.path.join(path, word) 213 | #self._test() 214 | return path 215 | 216 | def _test(self): 217 | headers = str(self.headers).split() 218 | print 'Range' in self.headers 219 | for index,data in enumerate(headers): 220 | if data.strip().lower().startswith('range'):#.startswith('range:'): 221 | pass 222 | return _RerootedHTTPRequestHandler 223 | 224 | class ThreadingServer(ThreadingMixIn, HTTPServer): 225 | pass 226 | 227 | 228 | def run(port=8080, doc_root=os.getcwd()): 229 | serveraddr = ('', port) 230 | serv = ThreadingServer(serveraddr, get_handler(doc_root)) 231 | print 'Server Started at port:', port 232 | serv.serve_forever() 233 | 234 | def test(): 235 | import doctest 236 | print doctest.testmod() 237 | 238 | if __name__=='__main__': 239 | run() 240 | --------------------------------------------------------------------------------