You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
servefile/servefile

381 lines
12 KiB

12 years ago
#!/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/
12 years ago
__version__ = '0.3.2'
12 years ago
import argparse
import cgi
12 years ago
import BaseHTTPServer
import commands
import datetime
12 years ago
import urllib
import os
12 years ago
import SocketServer
import socket
from stat import ST_SIZE
from subprocess import Popen, PIPE
import sys
12 years ago
import time
12 years ago
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")
12 years ago
class FileHandler(BaseHTTPServer.BaseHTTPRequestHandler):
fileName = "Undefined"
filePath = "/dev/null"
fileLength = 0
startTime = getDateStrNow()
12 years ago
blockSize = 1024 * 1024
def checkAndDoRedirect(self):
""" If request didn't request self.fileName redirect to self.fileName.
Returns True if a redirect was issued. """
12 years ago
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():
12 years ago
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()
12 years ago
def do_GET(self):
if self.checkAndDoRedirect():
return
12 years ago
myfile = open(self.filePath, 'rb')
# find out if this is a continuing download
fromto = None
12 years ago
if "Range" in self.headers:
cont = self.headers.get("Range").split("=")
12 years ago
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')
12 years ago
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):
12 years ago
if fromto and myfile.tell()+self.blockSize >= fromto[1]:
12 years ago
readsize = fromto[1]-myfile.tell()+1
else:
readsize = self.blockSize
return myfile.read(readsize)
class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
12 years ago
""" 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):
12 years ago
""" Answer every GET request with the upload form """
self.sendResponse(200, self.uploadPage)
def do_POST(self):
12 years ago
""" 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')
12 years ago
# check for multipart/form-data.
if not (ctype and ctype.lower().startswith("multipart/form-data")):
# not a normal multipart request ==> handle as PUT request
12 years ago
return self.do_PUT(fromPost=True)
12 years ago
# 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
12 years ago
destFileName = self.getTargetName(fstorage["file"].filename)
if destFileName == "":
12 years ago
self.sendResponse(400, "Filename was empty or invalid")
return
12 years ago
# write file down to disk, send an
target = open(destFileName, "w")
target.write(fstorage["file"].file.read())
target.close()
self.sendResponse(200, "OK!")
12 years ago
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> .
"""
12 years ago
length = 0
try:
length = int(self.headers['Content-Length'])
except (ValueError, KeyError):
12 years ago
pass
if length <= 0:
self.sendResponse(400, "Content-Length was invalid or not set.")
return
fileName = urllib.unquote(self.path)
if fileName == "/":
12 years ago
# if no filename was given we have to generate one
12 years ago
fileName = str(time.time())
12 years ago
12 years ago
cleanFileName = self.getTargetName(fileName)
if cleanFileName == "":
self.sendResponse(400, "Filename was invalid")
return
12 years ago
# Sometimes clients want to be told to continue with their transfer
12 years ago
if self.headers.getheader("Expect") == "100-continue":
self.send_response(100)
self.end_headers()
12 years ago
print "Saving uploaded file to %s" % cleanFileName
12 years ago
target = open(cleanFileName, "w")
target.write(self.rfile.read(int(self.headers['Content-Length'])))
target.close()
12 years ago
self.sendResponse(fromPost and 200 or 201, "OK!")
12 years ago
def sendResponse(self, code, msg):
12 years ago
""" 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)
12 years ago
def getTargetName(self, fname):
12 years ago
""" 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
12 years ago
def main():
parser = argparse.ArgumentParser(description='Serve a single file via HTTP')
12 years ago
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")
12 years ago
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
12 years ago
try:
server = ServeFile(args.target, args.port, mode)
server.serve()
except ServeFileException, e:
print e
sys.exit(1)
12 years ago
print "Good bye.."
12 years ago
if __name__ == '__main__':
main()