servefile/servefile

514 lines
16 KiB
Plaintext
Raw Normal View History

2012-03-12 15:41:55 +01:00
#!/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/
2012-03-18 04:53:28 +01:00
2012-04-05 16:30:36 +02:00
__version__ = '0.3.2'
2012-03-12 15:41:55 +01:00
2012-03-18 02:04:58 +01:00
import argparse
2012-04-07 02:57:06 +02:00
import cgi
2012-03-12 15:41:55 +01:00
import BaseHTTPServer
2012-03-18 01:42:03 +01:00
import commands
2012-04-09 15:32:57 +02:00
import datetime
2012-03-12 15:41:55 +01:00
import urllib
2012-03-18 01:42:03 +01:00
import os
2012-03-12 15:41:55 +01:00
import SocketServer
import socket
2012-03-18 01:42:03 +01:00
from stat import ST_SIZE
from subprocess import Popen, PIPE
2012-03-18 01:42:03 +01:00
import sys
2012-04-07 18:50:24 +02:00
import time
2012-03-12 15:41:55 +01:00
2012-04-14 22:31:09 +02:00
# only activate SSL if available
HAVE_SSL = False
try:
from OpenSSL import SSL, crypto
HAVE_SSL = True
except ImportError:
pass
2012-04-09 15:32:57 +02:00
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")
2012-03-12 15:41:55 +01:00
class FileHandler(BaseHTTPServer.BaseHTTPRequestHandler):
fileName = "Undefined"
filePath = "/dev/null"
fileLength = 0
2012-04-09 15:32:57 +02:00
startTime = getDateStrNow()
2012-03-12 15:41:55 +01:00
blockSize = 1024 * 1024
2012-04-09 15:32:57 +02:00
def checkAndDoRedirect(self):
""" If request didn't request self.fileName redirect to self.fileName.
Returns True if a redirect was issued. """
2012-03-12 15:41:55 +01:00
if urllib.unquote(self.path) != "/" + self.fileName:
self.send_response(302)
self.send_header('Location', '/' + self.fileName)
self.end_headers()
2012-04-09 15:32:57 +02:00
return True
return False
def do_HEAD(self):
if self.checkAndDoRedirect():
2012-03-12 15:41:55 +01:00
return
2012-04-09 15:32:57 +02:00
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')
2012-04-12 17:38:05 +02:00
self.send_header('Content-Disposition', 'attachment; filename="%s"' % self.fileName)
2012-04-09 15:32:57 +02:00
self.end_headers()
2012-03-12 15:41:55 +01:00
2012-04-09 15:32:57 +02:00
def do_GET(self):
if self.checkAndDoRedirect():
return
2012-03-12 15:41:55 +01:00
myfile = open(self.filePath, 'rb')
# find out if this is a continuing download
fromto = None
2012-03-18 00:21:55 +01:00
if "Range" in self.headers:
cont = self.headers.get("Range").split("=")
2012-03-12 15:41:55 +01:00
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)
2012-04-09 15:32:57 +02:00
self.send_header('Content-Type', 'application/octet-stream')
self.send_header('Content-Transfer-Encoding', 'binary')
2012-03-12 15:41:55 +01:00
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):
2012-03-18 00:21:55 +01:00
if fromto and myfile.tell()+self.blockSize >= fromto[1]:
2012-03-12 15:41:55 +01:00
readsize = fromto[1]-myfile.tell()+1
else:
readsize = self.blockSize
return myfile.read(readsize)
2012-04-05 15:56:28 +02:00
class FilePutter(BaseHTTPServer.BaseHTTPRequestHandler):
2012-04-12 13:17:55 +02:00
""" Simple HTTP Server which allows uploading to a specified directory
either via multipart/form-data or POST/PUT requests containing the file.
"""
2012-04-07 02:57:06 +02:00
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>
"""
2012-04-05 15:56:28 +02:00
def do_GET(self):
2012-04-12 13:17:55 +02:00
""" Answer every GET request with the upload form """
2012-04-07 02:57:06 +02:00
self.sendResponse(200, self.uploadPage)
2012-04-05 15:56:28 +02:00
def do_POST(self):
2012-04-12 13:17:55 +02:00
""" 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> .
"""
2012-04-07 02:57:06 +02:00
ctype = self.headers.getheader('content-type')
2012-04-12 13:17:55 +02:00
# check for multipart/form-data.
2012-04-07 02:57:06 +02:00
if not (ctype and ctype.lower().startswith("multipart/form-data")):
# not a normal multipart request ==> handle as PUT request
2012-04-12 13:17:55 +02:00
return self.do_PUT(fromPost=True)
2012-04-07 02:57:06 +02:00
2012-04-12 13:17:55 +02:00
# create FieldStorage object for multipart parsing
env = os.environ
env['REQUEST_METHOD'] = "POST"
fstorage = cgi.FieldStorage(fp=self.rfile, headers=self.headers, environ=env)
2012-04-07 02:57:06 +02:00
if not "file" in fstorage:
self.sendResponse(400, "No file found in request.")
return
2012-04-12 13:17:55 +02:00
destFileName = self.getTargetName(fstorage["file"].filename)
if destFileName == "":
2012-04-12 13:17:55 +02:00
self.sendResponse(400, "Filename was empty or invalid")
2012-04-07 02:57:06 +02:00
return
2012-04-12 13:17:55 +02:00
# write file down to disk, send an
2012-04-07 02:57:06 +02:00
target = open(destFileName, "w")
target.write(fstorage["file"].file.read())
target.close()
self.sendResponse(200, "OK!")
2012-04-12 13:17:55 +02:00
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> .
"""
2012-04-07 18:50:24 +02:00
length = 0
try:
length = int(self.headers['Content-Length'])
except (ValueError, KeyError):
2012-04-07 18:50:24 +02:00
pass
if length <= 0:
self.sendResponse(400, "Content-Length was invalid or not set.")
return
fileName = urllib.unquote(self.path)
if fileName == "/":
2012-04-12 13:17:55 +02:00
# if no filename was given we have to generate one
2012-04-07 18:50:24 +02:00
fileName = str(time.time())
2012-04-12 13:17:55 +02:00
2012-04-07 18:50:24 +02:00
cleanFileName = self.getTargetName(fileName)
if cleanFileName == "":
self.sendResponse(400, "Filename was invalid")
return
2012-04-12 13:17:55 +02:00
# Sometimes clients want to be told to continue with their transfer
2012-04-07 18:50:24 +02:00
if self.headers.getheader("Expect") == "100-continue":
self.send_response(100)
self.end_headers()
2012-04-12 13:17:55 +02:00
2012-04-07 18:50:24 +02:00
target = open(cleanFileName, "w")
target.write(self.rfile.read(int(self.headers['Content-Length'])))
target.close()
2012-04-12 13:17:55 +02:00
self.sendResponse(fromPost and 200 or 201, "OK!")
2012-04-07 18:50:24 +02:00
2012-04-07 02:57:06 +02:00
def sendResponse(self, code, msg):
2012-04-12 13:17:55 +02:00
""" Send a HTTP response with code and msg, providing the correct
content-length.
"""
2012-04-07 02:57:06 +02:00
self.send_response(code)
self.send_header('Content-Type', 'text/html')
self.send_header('Content-Length', str(len(msg)))
2012-04-05 15:56:28 +02:00
self.end_headers()
2012-04-07 02:57:06 +02:00
self.wfile.write(msg)
2012-04-07 18:50:24 +02:00
def getTargetName(self, fname):
2012-04-12 13:17:55 +02:00
""" 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
2012-04-07 02:57:06 +02:00
class ThreadedHTTPServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
2012-04-05 15:56:28 +02:00
pass
2012-04-14 22:31:09 +02:00
def catchSSLErrors(BaseSSLClass):
""" Class decorator which catches SSL errors and prints them. """
class X(BaseSSLClass):
def handle_one_request(self, *args, **kwargs):
try:
BaseSSLClass.handle_one_request(self, *args, **kwargs)
except SSL.Error, e:
if str(e) == "":
print "%s SSL Error (Empty error message)" % (self.client_address[0],)
else:
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)
ctx = SSL.Context(SSL.SSLv23_METHOD)
if type(pubKey) == crypto.X509 and type(privKey) == crypto.PKey:
ctx.use_certificate(pubKey)
ctx.use_privatekey(privKey)
else:
ctx.use_certificate_file(pubKey)
ctx.use_privatekey_file(privKey)
2012-04-14 22:31:09 +02:00
self.bsocket = socket.socket(self.address_family, self.socket_type)
self.socket = SSL.Connection(ctx, self.bsocket)
self.server_bind()
self.server_activate()
def shutdown_request(self, request):
request.shutdown()
class SecureHandler():
def setup(self):
self.connection = self.request
self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
class ServeFileException(Exception):
pass
class ServeFile():
""" Main class to manage everything. """
(MODE_SINGLE, MODE_UPLOAD, MODE_DIRLIST) = range(3)
2012-04-14 22:31:09 +02:00
def __init__(self, target, port=8080, serveMode=0, useSSL=False):
self.target = target
self.port = port
self.serveMode = serveMode
self.dirCreated = False
2012-04-14 22:31:09 +02:00
self.useSSL = useSSL
self.cert = self.key = None
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:
2012-04-14 22:31:09 +02:00
# we couldn't find any ip address
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
2012-04-14 22:31:09 +02:00
def setSSLKeys(self, cert, key):
""" Set SSL cert/key. Can be either path to file or pyssl X509/PKey object. """
self.cert = cert
self.key = key
def genKeyPair(self):
pkey = crypto.PKey()
pkey.generate_key(crypto.TYPE_RSA, 2048)
req = crypto.X509Req()
subj = req.get_subject()
subj.CN = "127.0.0.1"
subj.O = "servefile laboratories"
subj.OU = "servefile"
# generate altnames
altNames = []
for ip in self.getIPs() + ["127.0.0.1"]:
altNames.append("IP:%s" % ip)
altNames.append("DNS:localhost")
ext = crypto.X509Extension("subjectAltName", False, ",".join(altNames))
req.add_extensions([ext])
req.set_pubkey(pkey)
req.sign(pkey, "sha1")
cert = crypto.X509()
# some browsers complain if they see a cert from the same authority
# with the same serial ==> we just use the seconds as serial.
cert.set_serial_number(int(time.time()))
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(365*24*60*60)
cert.set_issuer(req.get_subject())
cert.set_subject(req.get_subject())
cert.add_extensions([ext])
cert.set_pubkey(req.get_pubkey())
cert.sign(pkey, "sha1")
self.cert = cert
self.key = pkey
2012-04-14 22:31:09 +02:00
def _getCert(self):
return self.cert
2012-04-14 22:31:09 +02:00
def _getKey(self):
return self.key
2012-04-14 22:31:09 +02:00
def _createServer(self, handler):
server = None
if self.useSSL:
if not self._getKey():
self.genKeyPair()
2012-04-14 22:31:09 +02:00
server = SecureThreadedHTTPServer(self._getCert(), self._getKey(), ('', self.port), handler)
else:
server = ThreadedHTTPServer(('', self.port), handler)
return server
def serve(self):
self.handler = self._confAndFindHandler()
2012-04-14 22:31:09 +02:00
self.server = self._createServer(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:
2012-04-14 22:31:09 +02:00
print "http%s://%s:%d/" % (self.useSSL and "s" or "", ip, self.port)
print ""
try:
2012-04-14 22:31:09 +02:00
self.server.serve_forever()
except KeyboardInterrupt:
2012-04-14 22:31:09 +02:00
self.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
2012-04-14 22:31:09 +02:00
if self.useSSL:
# secure handler
@catchSSLErrors
class AlreadySecuredHandler(SecureHandler, handler):
pass
handler = AlreadySecuredHandler
return handler
2012-03-12 15:41:55 +01:00
def main():
2012-03-18 04:53:39 +01:00
parser = argparse.ArgumentParser(description='Serve a single file via HTTP')
2012-03-18 04:53:28 +01:00
parser.add_argument('--version', action='version', version='%(prog)s ' + __version__)
parser.add_argument('target', metavar='file/directory', type=str)
2012-03-18 02:04:58 +01:00
parser.add_argument('-p', '--port', type=int, default=8080, \
help='port to listen on')
2012-04-05 15:56:28 +02:00
parser.add_argument('-u', '--upload', action="store_true", default=False, \
help="Enable uploads to a given directory")
2012-04-14 22:31:09 +02:00
parser.add_argument('--ssl', action="store_true", default=False, \
help="Enable SSL. If no key/cert is specified one will be generated.")
parser.add_argument('--key', type=str, \
help="Keyfile to use for SSL. If no cert is given with --cert the keyfile will also be searched for a cert")
parser.add_argument('--cert', type=str, \
help="Certfile to use for SSL")
2012-03-12 15:41:55 +01:00
2012-03-18 02:04:58 +01:00
args = parser.parse_args()
2012-04-07 02:57:06 +02:00
# check for invalid option combinations
2012-04-14 22:31:09 +02:00
if args.ssl and not HAVE_SSL:
print "Error: SSL is not available, please install pyssl (python-openssl)"
sys.exit(1)
if args.cert and not args.key:
print "Error: Please specify a key along with your cert"
sys.exit(1)
if not args.ssl and (args.cert or args.key):
print "Error: You need to turn on ssl with --ssl when specifying certs/keys"
sys.exit(1)
mode = None
2012-04-07 03:02:20 +02:00
if args.upload:
mode = ServeFile.MODE_UPLOAD
#elif args.listdir:
# mode = ServeFile.MODE_LISTDIR
2012-04-07 03:02:20 +02:00
else:
mode = ServeFile.MODE_SINGLE
2012-03-18 02:04:58 +01:00
server = None
2012-03-12 15:41:55 +01:00
try:
2012-04-14 22:31:09 +02:00
server = ServeFile(args.target, args.port, mode, args.ssl)
if args.ssl and args.key:
cert = args.cert or args.key
server.setSSLKeys(cert, args.key)
server.serve()
except ServeFileException, e:
print e
sys.exit(1)
2012-03-12 15:41:55 +01:00
print "Good bye.."
2012-03-12 15:41:55 +01:00
if __name__ == '__main__':
main()
2012-04-05 15:56:28 +02:00