From 9db41d5681447e71b60ce4bb978036781142dced Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Wed, 6 Jun 2012 15:54:47 +0200 Subject: [PATCH 1/9] Moved Content-* header generation to extra function --- servefile | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/servefile b/servefile index 1690ca8..dde07a5 100755 --- a/servefile +++ b/servefile @@ -53,6 +53,21 @@ class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler): 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') + class FileHandler(FileBaseHandler): filePath = "/dev/null" fileLength = 0 @@ -62,10 +77,7 @@ 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): @@ -93,16 +105,14 @@ class FileHandler(FileBaseHandler): # now we can wind the file *brrrrrr* myfile.seek(fromto[0]) + fileLength = self.fileLength 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) + fileLength = 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.sendContentHeaders(self.fileName, fileLength, self.startTime) self.end_headers() block = self.getChunk(myfile, fromto) while block: @@ -133,9 +143,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 +162,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 From 94eea95d99120ad6c74c17839b2e02c8242cb2b6 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Wed, 6 Jun 2012 17:15:16 +0200 Subject: [PATCH 2/9] Moved continuation/range handling to extra function --- servefile | 65 ++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/servefile b/servefile index dde07a5..a20bd13 100755 --- a/servefile +++ b/servefile @@ -68,6 +68,43 @@ class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.send_header('Content-Disposition', 'attachment; filename="%s"' % fileName) self.send_header('Content-Transfer-Encoding', 'binary') + def isRangeRequest(self): + 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) + # now we can wind the file *brrrrrr* + myfile.seek(fromto[0]) + # broken request or no range header + pass + return (False, None) + class FileHandler(FileBaseHandler): filePath = "/dev/null" fileLength = 0 @@ -85,25 +122,14 @@ class FileHandler(FileBaseHandler): return myfile = open(self.filePath, 'rb') - # 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]) + (continueDownload, fromto) = self.handleRangeRequest(self.fileLength) + if continueDownload: + if not fromto: + # we are done + return + + # now we can wind the file *brrrrrr* + myfile.seek(fromto[0]) fileLength = self.fileLength if fromto != None: @@ -124,7 +150,6 @@ class FileHandler(FileBaseHandler): 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]: From 138940a804df1cfa84dec81ef7b7013132e10644 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sat, 16 Jun 2012 21:52:11 +0200 Subject: [PATCH 3/9] code cleanup with pyflakes --- servefile | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/servefile b/servefile index a20bd13..cd169ab 100755 --- a/servefile +++ b/servefile @@ -11,12 +11,10 @@ import argparse import base64 import cgi import BaseHTTPServer -import commands import datetime import urllib import os import re -import SimpleHTTPServer import SocketServer import socket from stat import ST_SIZE @@ -42,13 +40,15 @@ class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler): 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 @@ -99,8 +99,6 @@ class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler): self.end_headers() return (True, None) return (True, fromto) - # now we can wind the file *brrrrrr* - myfile.seek(fromto[0]) # broken request or no range header pass return (False, None) @@ -225,7 +223,7 @@ 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 FilePutter(BaseHTTPServer.BaseHTTPRequestHandler): """ Simple HTTP Server which allows uploading to a specified directory @@ -747,7 +745,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 From 790607eabcd84d13e2993c8d144b5313166ae23e Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Tue, 19 Jun 2012 18:24:22 +0200 Subject: [PATCH 4/9] Code cleanup: Added docstrings + newlines between classes --- servefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/servefile b/servefile index cd169ab..630479b 100755 --- a/servefile +++ b/servefile @@ -36,7 +36,7 @@ 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__ @@ -69,6 +69,7 @@ class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler): 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): @@ -383,6 +384,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) @@ -403,6 +405,7 @@ class SecureThreadedHTTPServer(ThreadedHTTPServer): def shutdown_request(self, request): request.shutdown() + class SecureHandler(): def setup(self): self.connection = self.request @@ -412,6 +415,7 @@ class SecureHandler(): class ServeFileException(Exception): pass + class ServeFile(): """ Main class to manage everything. """ @@ -631,6 +635,7 @@ class ServeFile(): handler = AlreadySecuredHandler return handler + class AuthenticationHandler(): # base64 encoded user:password string for authentication authString = None @@ -663,6 +668,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__) @@ -777,6 +783,7 @@ def main(): sys.exit(1) print "Good bye." + if __name__ == '__main__': main() From b11710da39cb82f0574cfdc18d61ffac7ce854b1 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Tue, 19 Jun 2012 18:26:05 +0200 Subject: [PATCH 5/9] Moved FileHandler functions to FileBaseHandler FileHandler hat core functionality used/needed by other Handlers. --- servefile | 134 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 97 insertions(+), 37 deletions(-) diff --git a/servefile b/servefile index 630479b..ad0eddd 100755 --- a/servefile +++ b/servefile @@ -101,9 +101,104 @@ class FileBaseHandler(BaseHTTPServer.BaseHTTPRequestHandler): return (True, None) return (True, fromto) # broken request or no range header - pass 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 @@ -119,43 +214,8 @@ class FileHandler(FileBaseHandler): def do_GET(self): if self.checkAndDoRedirect(): return - myfile = open(self.filePath, 'rb') + self.sendFile(self.filePath, self.fileLength, self.startTime) - (continueDownload, fromto) = self.handleRangeRequest(self.fileLength) - if continueDownload: - if not fromto: - # we are done - return - - # now we can wind the file *brrrrrr* - myfile.seek(fromto[0]) - - fileLength = self.fileLength - if fromto != None: - self.send_response(216) - self.send_header('Content-Range', 'bytes %s-%s/%s' % (fromto[0], fromto[1], self.fileLength)) - fileLength = fromto[1] - fromto[0] + 1 - else: - self.send_response(200) - self.sendContentHeaders(self.fileName, fileLength, self.startTime) - 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]) - - 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 From ee18c3e52e913182a95af3ce57afd9e753f9fab7 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Wed, 20 Jun 2012 22:55:43 +0200 Subject: [PATCH 6/9] Replaced SimpleHTTPServer Handler with own handler A directory index looks not that well but isn't influenced by a index.htm(l) and presents more metadata than before. --- servefile | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 118 insertions(+), 7 deletions(-) diff --git a/servefile b/servefile index ad0eddd..8050408 100755 --- a/servefile +++ b/servefile @@ -12,8 +12,10 @@ import base64 import cgi import BaseHTTPServer import datetime +import mimetypes import urllib import os +import posixpath import re import SocketServer import socket @@ -286,6 +288,119 @@ class TarFileHandler(FileBaseHandler): return ".tar.bz2" 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): + 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): + print "sending directory listing" + + header = """ + + + Index of %(path)s + + +

Index of %(path)s

+ + + """ % {'path': posixpath.normpath(urllib.unquote(self.path))} + footer = """
NameLast ModifiedSizeType
+
servefile %(version)s
+ +""" % {'version': __version__} + content = [] + for item in [".."] + 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 either via multipart/form-data or POST/PUT requests containing the file. @@ -665,7 +780,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.") @@ -673,12 +788,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 From b1891da41732ab38dc2a63d5a25c0a32ff043e0f Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Fri, 22 Jun 2012 13:52:05 +0200 Subject: [PATCH 7/9] Added lighttpd-like directory listing look --- servefile | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/servefile b/servefile index 8050408..10dac92 100755 --- a/servefile +++ b/servefile @@ -301,6 +301,7 @@ class DirListingHandler(FileBaseHandler): 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): @@ -327,20 +328,36 @@ class DirListingHandler(FileBaseHandler): self.end_headers() def sendDirectoryListing(self, path, head): - print "sending directory listing" - + """ Generate a directorylisting for path and send it """ header = """ Index of %(path)s + -

Index of %(path)s

+

Index of %(path)s

+
""" % {'path': posixpath.normpath(urllib.unquote(self.path))} - footer = """
NameLast ModifiedSizeType
-
servefile %(version)s
+ footer = """
+
servefile %(version)s
""" % {'version': __version__} content = [] @@ -368,10 +385,10 @@ class DirListingHandler(FileBaseHandler): fileType = "Directory" content.append(""" - %s - %s - %s - %s + %s + %s + %s + %s """ % (urllib.quote(item), item, lastModified, fileSize, fileType)) From 709c2ad9da5317c703e37e6300f76f7aaad0c4a3 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Mon, 25 Jun 2012 00:20:44 +0200 Subject: [PATCH 8/9] Directory index is now sorted alphabetical --- servefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/servefile b/servefile index 10dac92..b6649cb 100755 --- a/servefile +++ b/servefile @@ -361,7 +361,7 @@ class DirListingHandler(FileBaseHandler): """ % {'version': __version__} content = [] - for item in [".."] + os.listdir(path): + for item in [".."] + sorted(os.listdir(path)): # create path to item itemPath = os.path.join(path, item) From 845e34a29724f2cfa685c0fa8f1ac73cb86d6c40 Mon Sep 17 00:00:00 2001 From: Konrad Mohrfeldt Date: Sun, 24 Jun 2012 22:53:15 +0200 Subject: [PATCH 9/9] refactor css and html more contrast and slightly more spacing improve font readability add encoding to html switch to html5 doctype --- servefile | 56 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/servefile b/servefile index b6649cb..864dfcc 100755 --- a/servefile +++ b/servefile @@ -329,35 +329,41 @@ class DirListingHandler(FileBaseHandler): def sendDirectoryListing(self, path, head): """ Generate a directorylisting for path and send it """ - header = """ + header = """ + Index of %(path)s -

Index of %(path)s

-
- - +

Index of %(path)s

+
+
NameLast ModifiedSizeType
+ + + + + + + + + """ % {'path': posixpath.normpath(urllib.unquote(self.path))} - footer = """
NameLast ModifiedSizeType
-
servefile %(version)s
+ footer = """ + """ % {'version': __version__} content = [] @@ -385,10 +391,10 @@ class DirListingHandler(FileBaseHandler): fileType = "Directory" content.append(""" - %s - %s - %s - %s + %s + %s + %s + %s """ % (urllib.quote(item), item, lastModified, fileSize, fileType))