381 lines
12 KiB
Python
Executable File
381 lines
12 KiB
Python
Executable File
#!/usr/bin/python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
# Licensed under GNU General Public License v3 or later
|
|
# Written by Sebastian Lohff (seba@seba-geek.de)
|
|
# http://seba-geek.de/stuff/servefile/
|
|
|
|
__version__ = '0.3.2'
|
|
|
|
import argparse
|
|
import cgi
|
|
import BaseHTTPServer
|
|
import commands
|
|
import datetime
|
|
import urllib
|
|
import os
|
|
import SocketServer
|
|
import socket
|
|
from stat import ST_SIZE
|
|
from subprocess import Popen, PIPE
|
|
import sys
|
|
import time
|
|
|
|
def getDateStrNow():
|
|
""" Get the current time formatted for HTTP header """
|
|
now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime()))
|
|
return now.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
|
|
|
class FileHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
|
fileName = "Undefined"
|
|
filePath = "/dev/null"
|
|
fileLength = 0
|
|
startTime = getDateStrNow()
|
|
blockSize = 1024 * 1024
|
|
|
|
|
|
def checkAndDoRedirect(self):
|
|
""" 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:
|
|
self.send_response(302)
|
|
self.send_header('Location', '/' + self.fileName)
|
|
self.end_headers()
|
|
return True
|
|
return False
|
|
|
|
def do_HEAD(self):
|
|
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.end_headers()
|
|
|
|
def do_GET(self):
|
|
if self.checkAndDoRedirect():
|
|
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])
|
|
|
|
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 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.
|
|
"""
|
|
|
|
targetDir = "unknown"
|
|
uploadPage = """
|
|
<!docype html>
|
|
<html>
|
|
<form action="/" method="post" enctype="multipart/form-data">
|
|
<label for="file">Filename:</label>
|
|
<input type="file" name="file" id="file" />
|
|
<br />
|
|
<input type="submit" name="submit" value="Upload" />
|
|
</form>
|
|
</html>
|
|
"""
|
|
|
|
def do_GET(self):
|
|
""" Answer every GET request with the upload form """
|
|
self.sendResponse(200, self.uploadPage)
|
|
|
|
def do_POST(self):
|
|
""" Upload a file via POST
|
|
|
|
If the content-type is multipart/form-data it checks for the file
|
|
field and saves the data to disk. For other content-types it just
|
|
calls do_PUT and is handled as such except for the http response code.
|
|
|
|
Files can be uploaded with wget --post-file=path/to/file <url> or
|
|
curl -X POST -d @file <url> .
|
|
"""
|
|
ctype = self.headers.getheader('content-type')
|
|
|
|
# check for multipart/form-data.
|
|
if not (ctype and ctype.lower().startswith("multipart/form-data")):
|
|
# not a normal multipart request ==> handle as PUT request
|
|
return self.do_PUT(fromPost=True)
|
|
|
|
# create FieldStorage object for multipart parsing
|
|
env = os.environ
|
|
env['REQUEST_METHOD'] = "POST"
|
|
fstorage = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=env)
|
|
if not "file" in fstorage:
|
|
self.sendResponse(400, "No file found in request.")
|
|
return
|
|
|
|
destFileName = self.getTargetName(fstorage["file"].filename)
|
|
if destFileName == "":
|
|
self.sendResponse(400, "Filename was empty or invalid")
|
|
return
|
|
|
|
# write file down to disk, send an
|
|
target = open(destFileName, "w")
|
|
target.write(fstorage["file"].file.read())
|
|
target.close()
|
|
self.sendResponse(200, "OK!")
|
|
|
|
def do_PUT(self, fromPost=False):
|
|
""" Upload a file via PUT
|
|
|
|
The request path is used as filename, so uploading a file to the url
|
|
http://host:8080/testfile will cause the file to be named testfile. If
|
|
no filename is given, a random name will be generated.
|
|
|
|
Files can be uploaded with e.g. curl -X POST -d @file <url> .
|
|
"""
|
|
length = 0
|
|
try:
|
|
length = int(self.headers['Content-Length'])
|
|
except (ValueError, KeyError):
|
|
pass
|
|
if length <= 0:
|
|
self.sendResponse(400, "Content-Length was invalid or not set.")
|
|
return
|
|
|
|
fileName = urllib.unquote(self.path)
|
|
if fileName == "/":
|
|
# if no filename was given we have to generate one
|
|
fileName = str(time.time())
|
|
|
|
cleanFileName = self.getTargetName(fileName)
|
|
if cleanFileName == "":
|
|
self.sendResponse(400, "Filename was invalid")
|
|
return
|
|
|
|
# Sometimes clients want to be told to continue with their transfer
|
|
if self.headers.getheader("Expect") == "100-continue":
|
|
self.send_response(100)
|
|
self.end_headers()
|
|
|
|
print "Saving uploaded file to %s" % cleanFileName
|
|
target = open(cleanFileName, "w")
|
|
target.write(self.rfile.read(int(self.headers['Content-Length'])))
|
|
target.close()
|
|
self.sendResponse(fromPost and 200 or 201, "OK!")
|
|
|
|
def sendResponse(self, code, msg):
|
|
""" Send a HTTP response with code and msg, providing the correct
|
|
content-length.
|
|
"""
|
|
self.send_response(code)
|
|
self.send_header('Content-Type', 'text/html')
|
|
self.send_header('Content-Length', str(len(msg)))
|
|
self.end_headers()
|
|
self.wfile.write(msg)
|
|
|
|
def getTargetName(self, fname):
|
|
""" Generate a clean and secure filename.
|
|
|
|
This function takes a filename and strips all the slashes out of it.
|
|
If the file already exists in the target directory, a (NUM) will be
|
|
appended, so no file will be overwritten.
|
|
"""
|
|
cleanFileName = fname.replace("/", "")
|
|
if cleanFileName == "":
|
|
return ""
|
|
destFileName = self.targetDir + "/" + cleanFileName
|
|
if not os.path.exists(destFileName):
|
|
return destFileName
|
|
else:
|
|
i = 1
|
|
extraDestFileName = destFileName + "(%s)" % i
|
|
while os.path.exists(extraDestFileName):
|
|
i += 1
|
|
extraDestFileName = destFileName + "(%s)" % i
|
|
return extraDestFileName
|
|
# never reached
|
|
|
|
class ThreadedHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
|
|
pass
|
|
|
|
class ServeFileException(Exception):
|
|
pass
|
|
|
|
class ServeFile():
|
|
""" Main class to manage everything. """
|
|
|
|
(MODE_SINGLE, MODE_UPLOAD, MODE_DIRLIST) = range(3)
|
|
|
|
def __init__(self, target, port=8080, serveMode=0):
|
|
self.target = target
|
|
self.port = port
|
|
self.serveMode = serveMode
|
|
self.dirCreated = False
|
|
|
|
if self.serveMode not in range(3):
|
|
self.serveMode = None
|
|
raise ValueError("Unknown serve mode, needs to be MODE_SINGLE, MODE_UPLOAD or MODE_DIRLIST")
|
|
|
|
def getIPs(self):
|
|
""" Get IPs from all interfaces via ip or ifconfig. """
|
|
# ip and ifconfig sometimes are located in /sbin/
|
|
os.environ['PATH'] += ':/sbin:/usr/sbin'
|
|
proc = Popen(r"ip addr|" + \
|
|
"sed -n -e 's/.*inet6\? \([0-9.a-fA-F:]\+\)\/.*/\\1/ p'|" + \
|
|
"grep -v '^fe80\|^127.0.0.1\|^::1'", shell=True, stdout=PIPE)
|
|
if proc.wait() != 0:
|
|
# ip failed somehow, falling back to ifconfig
|
|
oldLang = os.environ.get("LC_ALL", None)
|
|
os.environ['LC_ALL'] = "C"
|
|
proc = Popen(r"ifconfig|" + \
|
|
"sed -n 's/.*inet6\? addr: \?\([0-9a-fA-F.:]*\).*/" + \
|
|
"\\1/p'|" + \
|
|
"grep -v '^fe80\|^127.0.0.1\|^::1'", \
|
|
shell=True, stdout=PIPE, stderr=PIPE)
|
|
if oldLang:
|
|
os.environ['LC_ALL'] = oldLang
|
|
else:
|
|
del(os.environ['LC_ALL'])
|
|
if proc.wait() != 0:
|
|
print "Error: Could not locate any ips for you."
|
|
proc = None
|
|
if proc:
|
|
ips = proc.stdout.read().strip().split("\n")
|
|
# FIXME: When BaseHTTP supports ipv6 properly, delete this line
|
|
ips = filter(lambda ip: ip.find(":") == -1, ips)
|
|
return ips
|
|
return None
|
|
|
|
def serve(self):
|
|
self.handler = self._confAndFindHandler()
|
|
server = ThreadedHTTPServer(('', self.port), self.handler)
|
|
if self.serveMode != self.MODE_UPLOAD:
|
|
print "Serving \"%s\" under port %d" % (self.target, self.port)
|
|
else:
|
|
print "Serving \"%s\" for uploads under port %d" % (self.target, self.port)
|
|
|
|
# print urls with local network adresses
|
|
print "\nSome addresses this will be available under:"
|
|
ips = self.getIPs()
|
|
if not ips or len(ips) == 0 or ips[0] == '':
|
|
print "Could not find any addresses"
|
|
else:
|
|
for ip in ips:
|
|
print "http://%s:%d/" % (ip, self.port)
|
|
print ""
|
|
|
|
try:
|
|
server.serve_forever()
|
|
except KeyboardInterrupt:
|
|
server.socket.close()
|
|
|
|
# cleanup potential upload directory
|
|
if self.dirCreated and len(os.listdir(self.target)) == 0:
|
|
# created upload dir was not used
|
|
os.rmdir(self.target)
|
|
|
|
def _confAndFindHandler(self):
|
|
handler = None
|
|
if self.serveMode == self.MODE_SINGLE:
|
|
try:
|
|
testit = open(self.target, 'r')
|
|
testit.close()
|
|
FileHandler.filePath = self.target
|
|
FileHandler.fileName = os.path.basename(self.target)
|
|
FileHandler.fileLength = os.stat(self.target)[ST_SIZE]
|
|
except IOError:
|
|
raise ServeFileException("Error: Could not open file!")
|
|
handler = FileHandler
|
|
elif self.serveMode == self.MODE_UPLOAD:
|
|
if os.path.isdir(self.target):
|
|
print "Warning: Uploading to an already existing directory"
|
|
elif not os.path.exists(self.target):
|
|
self.dirCreated = True
|
|
try:
|
|
os.mkdir(self.target)
|
|
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")
|
|
FilePutter.targetDir = self.target
|
|
handler = FilePutter
|
|
return handler
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Serve a single file via HTTP')
|
|
parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
|
|
parser.add_argument('target', metavar='file/directory', type=str)
|
|
parser.add_argument('-p', '--port', type=int, default=8080, \
|
|
help='port to listen on')
|
|
parser.add_argument('-u', '--upload', action="store_true", default=False, \
|
|
help="Enable uploads to a given directory")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# check for invalid option combinations
|
|
|
|
mode = None
|
|
if args.upload:
|
|
mode = ServeFile.MODE_UPLOAD
|
|
#elif args.listdir:
|
|
# mode = ServeFile.MODE_LISTDIR
|
|
else:
|
|
mode = ServeFile.MODE_SINGLE
|
|
|
|
server = None
|
|
try:
|
|
server = ServeFile(args.target, args.port, mode)
|
|
server.serve()
|
|
except ServeFileException, e:
|
|
print e
|
|
sys.exit(1)
|
|
print "Good bye.."
|
|
|
|
if __name__ == '__main__':
|
|
main()
|
|
|