Merge branch 'dirlisting'

This commit is contained in:
Sebastian Lohff 2012-06-25 00:25:18 +02:00
commit 0f54983a63
1 changed files with 305 additions and 74 deletions

379
servefile
View File

@ -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 = """<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Index of %(path)s</title>
<style type="text/css">
a { text-decoration: none; color: #0000BB;}
a:visited { color: #000066;}
a:hover, a:focus, a:active { text-decoration: underline; color: #cc0000; text-indent: 5px; }
body { background-color: #eaeaea; padding: 20px 0; margin: 0; font: 400 13px/1.2em Arial, sans-serif; }
h1 { margin: 0 10px 12px 10px; font-family: Arial, sans-serif; }
div.content { background-color: white; border-color: #ccc; border-width: 1px 0; border-style: solid; padding: 10px 10px 15px 10px; }
td { padding-right: 15px; text-align: left; font-family: monospace; }
th { font-weight: bold; font-size: 115%%; padding: 0 15px 5px 0; text-align: left; }
.size { text-align: right; }
.footer { font: 12px monospace; color: #333; margin: 5px 10px 0; }
.footer, h1 { text-shadow: 0 1px 0 white; }
</style>
</head>
<body>
<h1>Index of %(path)s</h1>
<div class="content">
<table summary="Directory Listing">
<thead>
<tr>
<th class="name">Name</th>
<th class="lastModified">Last Modified</th>
<th class="size">Size</th>
<th class="type">Type</th>
</tr>
</thead>
<tbody>
""" % {'path': posixpath.normpath(urllib.unquote(self.path))}
footer = """</tbody></table></div>
<div class="footer">servefile %(version)s</div>
</body>
</html>""" % {'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("""
<tr>
<td class="name"><a href="%s">%s</a></td>
<td class="last-modified">%s</td>
<td class="size">%s</td>
<td class="type">%s</td>
</tr>
""" % (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()