diff --git a/servefile b/servefile index 1690ca8..864dfcc 100755 --- a/servefile +++ b/servefile @@ -11,12 +11,12 @@ import argparse import base64 import cgi import BaseHTTPServer -import commands import datetime +import mimetypes import urllib import os +import posixpath import re -import SimpleHTTPServer import SocketServer import socket from stat import ST_SIZE @@ -38,21 +38,169 @@ def getDateStrNow(): return now.strftime("%a, %d %b %Y %H:%M:%S GMT") class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler): - fileName = "Undefined" + fileName = None blockSize = 1024 * 1024 server_version = "servefile/" + __version__ - def checkAndDoRedirect(self): + def checkAndDoRedirect(self, fileName=None): """ If request didn't request self.fileName redirect to self.fileName. Returns True if a redirect was issued. """ - if urllib.unquote(self.path) != "/" + self.fileName: + if not fileName: + fileName = self.fileName + if urllib.unquote(self.path) != "/" + fileName: self.send_response(302) - self.send_header('Location', '/' + self.fileName) + self.send_header('Location', '/' + fileName) self.end_headers() return True return False + def sendContentHeaders(self, fileName, fileLength, lastModified=None): + """ Send default Content headers for given fileName and fileLength. + + If no lastModified is given the current date is taken. If + fileLength is lesser than 0 no Content-Length will be sent.""" + if not lastModified: + lastModified = getDateStrNow() + + if fileLength >= 0: + self.send_header('Content-Length', str(fileLength)) + self.send_header('Last-Modified', lastModified) + self.send_header('Content-Type', 'application/octet-stream') + self.send_header('Content-Disposition', 'attachment; filename="%s"' % fileName) + self.send_header('Content-Transfer-Encoding', 'binary') + + def isRangeRequest(self): + """ Return True if partial content is requestet """ + return "Range" in self.headers + + def handleRangeRequest(self, fileLength): + """ Find out and handle continuing downloads. + + Returns a tuple of a boolean, if this is a valid range request, + and a range. When the requested range is out of range, range is + set to None. + """ + fromto = None + if self.isRangeRequest(): + cont = self.headers.get("Range").split("=") + if len(cont) > 1 and cont[0] == 'bytes': + fromto = cont[1].split('-') + if len(fromto) > 1: + if fromto[1] == '': + fromto[1] = fileLength - 1 + try: + fromto[0] = int(fromto[0]) + fromto[1] = int(fromto[1]) + except: + return (False, None) + + if fromto[0] >= fileLength or fromto[0] < 0 or fromto[1] >= fileLength or fromto[1]-fromto[0] < 0: + # oops, already done! (requested range out of range) + self.send_response(416) + self.send_header('Content-Range', 'bytes */%s' % fileLength) + self.end_headers() + return (True, None) + return (True, fromto) + # broken request or no range header + return (False, None) + + def sendFile(self, filePath, fileLength=None, lastModified=None): + """ Send file with continuation support. + + filePath: path to file to be sent + fileLength: length of file (if None is given this will be found out) + lastModified: time the file was last modified, None for "now" + """ + if not fileLength: + fileLength = os.stat(filePath)[ST_SIZE] + + (responseCode, myfile) = self.getFileHandle(filePath) + if not myfile: + self.send_response(responseCode) + self.end_headers() + return + + (continueDownload, fromto) = self.handleRangeRequest(fileLength) + if continueDownload: + if not fromto: + # we are done + return True + + # now we can wind the file *brrrrrr* + myfile.seek(fromto[0]) + + if fromto != None: + self.send_response(216) + self.send_header('Content-Range', 'bytes %s-%s/%s' % (fromto[0], fromto[1], fileLength)) + fileLength = fromto[1] - fromto[0] + 1 + else: + self.send_response(200) + + fileName = self.fileName + if not fileName: + fileName = os.path.basename(filePath) + self.sendContentHeaders(fileName, fileLength, lastModified) + self.end_headers() + block = self.getChunk(myfile, fromto) + while block: + try: + self.wfile.write(block) + except socket.error, e: + print "%s ABORTED transmission (Reason %s: %s)" % (self.client_address[0], e[0], e[1]) + return False + block = self.getChunk(myfile, fromto) + myfile.close() + print "%s finished downloading %s" % (self.client_address[0], filePath) + return True + + def getChunk(self, myfile, fromto): + if fromto and myfile.tell()+self.blockSize >= fromto[1]: + readsize = fromto[1]-myfile.tell()+1 + else: + readsize = self.blockSize + return myfile.read(readsize) + + def getFileHandle(self, path): + """ Get handle to a file. + + Return a tuple of HTTP response code and file handle. + If the handle couldn't be acquired it is set to None + and an appropriate HTTP error code is returned. + """ + myfile = None + responseCode = 200 + try: + myfile = open(path, 'rb') + except IOError, e: + responseCode = self.getResponseForErrno(e.errno) + return (responseCode, myfile) + + def getFileLength(self, path): + """ Get length of a file. + + Return a tuple of HTTP response code and file length. + If filelength couldn't be determined, it is set to -1 + and an appropriate HTTP error code is returned. + """ + fileSize = -1 + responseCode = 200 + try: + fileSize = os.stat(path)[ST_SIZE] + except IOError, e: + responseCode = self.getResponseForErrno(e.errno) + return (responseCode, fileSize) + + def getResponseForErrno(self, errno): + """ Return HTTP response code for an IOError errno """ + if errno == 2: + return 404 + elif errno == 13: + return 403 + else: + return 500 + + class FileHandler(FileBaseHandler): filePath = "/dev/null" fileLength = 0 @@ -62,66 +210,14 @@ class FileHandler(FileBaseHandler): if self.checkAndDoRedirect(): return self.send_response(200) - self.send_header('Content-Length', self.fileLength) - self.send_header('Last-Modified', self.startTime) - self.send_header('Content-Type', 'application/octet-stream') - self.send_header('Content-Disposition', 'attachment; filename="%s"' % self.fileName) + self.sendContentHeaders(self.fileName, self.fileLength, self.startTime) self.end_headers() def do_GET(self): if self.checkAndDoRedirect(): return - myfile = open(self.filePath, 'rb') + self.sendFile(self.filePath, self.fileLength, self.startTime) - # find out if this is a continuing download - fromto = None - if "Range" in self.headers: - cont = self.headers.get("Range").split("=") - if len(cont) > 1 and cont[0] == 'bytes': - fromto = cont[1].split('-') - if len(fromto) > 1: - if fromto[1] == '': - fromto[1] = self.fileLength-1 - fromto[0] = int(fromto[0]) - fromto[1] = int(fromto[1]) - if fromto[0] >= self.fileLength or fromto[0] < 0 or fromto[1] >= self.fileLength or fromto[1]-fromto[0] < 0: - # oops, already done! - self.send_response(416) - self.send_header('Content-Range', 'bytes */%s' % self.fileLength) - self.end_headers() - return - # now we can wind the file *brrrrrr* - myfile.seek(fromto[0]) - - if fromto != None: - self.send_response(216) - self.send_header('Content-Range', 'bytes %s-%s/%s' % (fromto[0], fromto[1], self.fileLength)) - self.send_header('Content-Length', fromto[1]-fromto[0]+1) - else: - self.send_response(200) - self.send_header('Content-Length', self.fileLength) - self.send_header('Content-Disposition', 'attachment; filename="%s"' % self.fileName) - self.send_header('Content-Type', 'application/octet-stream') - self.send_header('Content-Transfer-Encoding', 'binary') - self.end_headers() - block = self.getChunk(myfile, fromto) - while block: - try: - self.wfile.write(block) - except socket.error, e: - print "%s ABORTED transmission (Reason %s: %s)" % (self.client_address[0], e[0], e[1]) - return - block = self.getChunk(myfile, fromto) - myfile.close() - print "%s finished downloading" % (self.client_address[0]) - return - - def getChunk(self, myfile, fromto): - if fromto and myfile.tell()+self.blockSize >= fromto[1]: - readsize = fromto[1]-myfile.tell()+1 - else: - readsize = self.blockSize - return myfile.read(readsize) class TarFileHandler(FileBaseHandler): target = None @@ -133,9 +229,7 @@ class TarFileHandler(FileBaseHandler): if self.checkAndDoRedirect(): return self.send_response(200) - self.send_header('Last-Modified', getDateStrNow()) - self.send_header('Content-Type', 'application/octet-stream') - self.send_header('Content-Disposition', 'attachment; filename="%s"' % self.fileName) + self.sendContentHeaders(self.fileName, -1) self.end_headers() def do_GET(self): @@ -154,8 +248,7 @@ class TarFileHandler(FileBaseHandler): return self.send_response(200) - self.send_header('Last-Modified', getDateStrNow()) - self.send_header('Content-Type', 'application/octet-stream') + self.sendContentHeaders(self.fileName, -1) self.end_headers() block = True @@ -193,7 +286,143 @@ class TarFileHandler(FileBaseHandler): return ".tar.gz" elif TarFileHandler.compression == "bzip2": return ".tar.bz2" - raise ValueError("Unknown compression mode '%s'." % self.compression) + raise ValueError("Unknown compression mode '%s'." % TarFileHandler.compression) + + +class DirListingHandler(FileBaseHandler): + """ DOCUMENTATION MISSING """ + + targetDir = None + + def do_HEAD(self): + self.getFileOrDirectory(head=True) + + def do_GET(self): + self.getFileOrDirectory(head=False) + + def getFileOrDirectory(self, head=False): + """ Send file or directory index, depending on requested path """ + path = self.getCleanPath() + + if os.path.isdir(path): + if not self.path.endswith('/'): + self.send_response(301) + self.send_header("Location", self.path + '/') + self.end_headers() + else: + self.sendDirectoryListing(path, head) + elif os.path.isfile(path): + if head: + (response, length) = self.getFileLength(path) + if length < 0: + self.send_response(response) + self.end_headers() + else: + self.send_response(200) + self.sendContentHeaders(self, path, length) + self.end_headers() + else: + self.sendFile(path, head) + else: + self.send_response(404) + self.end_headers() + + def sendDirectoryListing(self, path, head): + """ Generate a directorylisting for path and send it """ + header = """ + + + + Index of %(path)s + + + +

Index of %(path)s

+
+ + + + + + + + + + + """ % {'path': posixpath.normpath(urllib.unquote(self.path))} + footer = """
NameLast ModifiedSizeType
+ + +""" % {'version': __version__} + content = [] + for item in [".."] + sorted(os.listdir(path)): + # create path to item + itemPath = os.path.join(path, item) + + # try to stat file for size, last modified... continue on error + stat = None + try: + stat = os.stat(itemPath) + except IOError: + continue + + # Strings to display on directory listing + lastModifiedDate = datetime.datetime.fromtimestamp(stat.st_mtime) + lastModified = lastModifiedDate.strftime("%Y-%m-%d %H:%M") + fileSize = "%.1f%s" % self.convertSize(stat.st_size) + (fileType, _) = mimetypes.guess_type(itemPath) + if not fileType: + fileType = "-" + + if os.path.isdir(itemPath): + item += "/" + fileType = "Directory" + content.append(""" + + %s + %s + %s + %s + + """ % (urllib.quote(item), item, lastModified, fileSize, fileType)) + + listing = header + "\n".join(content) + footer + + # write listing + self.send_response(200) + self.send_header("Content-Type", "text/html") + if head: + self.end_headers() + return + self.send_header("Content-Length", str(len(listing))) + self.end_headers() + self.wfile.write(listing) + + def convertSize(self, size): + ext = None + for ext in "BKMGT": + if size < 1024.0: + break + size /= 1024.0 + return (size, ext) + + def getCleanPath(self): + urlPath = posixpath.normpath(urllib.unquote(self.path)).strip("/") + path = os.path.join(self.targetDir, urlPath) + return path + class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler): """ Simple HTTP Server which allows uploading to a specified directory @@ -353,6 +582,7 @@ def catchSSLErrors(BaseSSLClass): print "%s SSL Error: %s" % (self.client_address[0], e) return X + class SecureThreadedHTTPServer(ThreadedHTTPServer): def __init__(self, pubKey, privKey, *args, **kwargs): ThreadedHTTPServer.__init__(self, *args, **kwargs) @@ -373,6 +603,7 @@ class SecureThreadedHTTPServer(ThreadedHTTPServer): def shutdown_request(self, request): request.shutdown() + class SecureHandler(): def setup(self): self.connection = self.request @@ -382,6 +613,7 @@ class SecureHandler(): class ServeFileException(Exception): pass + class ServeFile(): """ Main class to manage everything. """ @@ -571,7 +803,7 @@ class ServeFile(): self.dirCreated = True try: os.mkdir(self.target) - except IOError, OSError: + except (IOError, OSError): raise ServeFileException("Error: Could not create directory '%s' for uploads." % (self.target,) ) else: raise ServeFileException("Error: Upload directory already exists and is a file.") @@ -579,12 +811,8 @@ class ServeFile(): FilePutter.maxUploadSize = self.maxUploadSize handler = FilePutter elif self.serveMode == self.MODE_LISTDIR: - try: - os.chdir(self.target) - except OSError: - raise ServeFileException("Error: Could not change directory to '%s'." % self.target) - handler = SimpleHTTPServer.SimpleHTTPRequestHandler - + handler = DirListingHandler + handler.targetDir = self.target if self.auth: # do authentication @@ -601,6 +829,7 @@ class ServeFile(): handler = AlreadySecuredHandler return handler + class AuthenticationHandler(): # base64 encoded user:password string for authentication authString = None @@ -633,6 +862,7 @@ class AuthenticationHandler(): self.send_response(401) self.send_header("WWW-Authenticate", "Basic realm=\"%s\"" % self.realm) + def main(): parser = argparse.ArgumentParser(description='Serve a single file via HTTP.') parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) @@ -715,7 +945,7 @@ def main(): if args.compression in TarFileHandler.compressionMethods: compression = args.compression else: - print "Error: Compression mode '%s' is unknown." % self.compression + print "Error: Compression mode '%s' is unknown." % TarFileHandler.compression sys.exit(1) mode = None @@ -747,6 +977,7 @@ def main(): sys.exit(1) print "Good bye." + if __name__ == '__main__': main()