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

629 lines
19 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.4.0'
12 years ago
import argparse
import base64
import cgi
12 years ago
import BaseHTTPServer
import commands
import datetime
12 years ago
import urllib
import os
import re
import SimpleHTTPServer
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
12 years ago
# only activate SSL if available
HAVE_SSL = False
try:
from OpenSSL import SSL, crypto
HAVE_SSL = True
except ImportError:
pass
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 = None
maxUploadSize = 0
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> .
"""
length = self.getContentLength()
if length < 0:
return
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(length))
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> .
"""
length = self.getContentLength()
if length < 0:
12 years ago
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
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 getContentLength(self):
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 -1
if length > self.maxUploadSize:
self.sendResponse(413, "Your file was too big! Maximum allowed size is %d byte. <a href=\"/\">back</a>" % self.maxUploadSize)
return -1
return length
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
12 years ago
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)
12 years ago
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_LISTDIR) = range(3)
12 years ago
def __init__(self, target, port=8080, serveMode=0, useSSL=False):
self.target = target
self.port = port
self.serveMode = serveMode
self.dirCreated = False
12 years ago
self.useSSL = useSSL
self.cert = self.key = None
self.auth = None
self.maxUploadSize = 0
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, stderr=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:
12 years ago
# 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
12 years ago
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 setMaxUploadSize(self, limit):
""" Set the maximum upload size in byte """
self.maxUploadSize = limit
def genKeyPair(self):
print "Generating SSL certificate...",
sys.stdout.flush()
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
print "done."
12 years ago
def _getCert(self):
return self.cert
12 years ago
def _getKey(self):
return self.key
def setAuth(self, user, password):
if len(user) == "" or len(password) == "":
raise ServeFileException("User and password both need to be at least one character long")
self.auth = base64.b64encode("%s:%s" % (user, password))
12 years ago
def _createServer(self, handler):
server = None
if self.useSSL:
if not self._getKey():
self.genKeyPair()
12 years ago
server = SecureThreadedHTTPServer(self._getCert(), self._getKey(), ('', self.port), handler)
else:
server = ThreadedHTTPServer(('', self.port), handler)
return server
def serve(self):
self.handler = self._confAndFindHandler()
12 years ago
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:
12 years ago
print "http%s://%s:%d/" % (self.useSSL and "s" or "", ip, self.port)
print ""
try:
12 years ago
self.server.serve_forever()
except KeyboardInterrupt:
12 years ago
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
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
12 years ago
if self.auth:
# do authentication
AuthenticationHandler.authString = self.auth
class AuthenticatedHandler(AuthenticationHandler, handler):
pass
handler = AuthenticatedHandler
12 years ago
if self.useSSL:
# secure handler
@catchSSLErrors
class AlreadySecuredHandler(SecureHandler, handler):
pass
handler = AlreadySecuredHandler
return handler
class AuthenticationHandler():
# base64 encoded user:password string for authentication
authString = None
realm = "Restricted area"
def handle_one_request(self):
""" Overloaded function to handle one request.
Before calling the responsible do_METHOD function, check credentials
"""
self.raw_requestline = self.rfile.readline()
if not self.raw_requestline:
self.close_connection = 1
return
if not self.parse_request(): # An error code has been sent, just exit
return
authorized = False
if "Authorization" in self.headers:
if self.headers["Authorization"] == ("Basic " + self.authString):
authorized = True
if authorized:
mname = 'do_' + self.command
if not hasattr(self, mname):
self.send_error(501, "Unsupported method (%r)" % self.command)
return
method = getattr(self, mname)
method()
else:
self.send_response(401)
self.send_header("WWW-Authenticate", "Basic realm=\"%s\"" % self.realm)
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")
parser.add_argument('-s', '--max-upload-size', type=str, \
help="Limit uploadsize in kb. Size modifiers are allowed, e.g. 2G, 12Mb, 1b.")
parser.add_argument('-l', '--list-dir', action="store_true", default=False, \
help="Show directory indexes and allow access to all subdirectories")
12 years ago
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")
parser.add_argument('-a', '--auth', type=str, metavar='user:password', \
help="Set user and password for HTTP basic authentication")
12 years ago
args = parser.parse_args()
maxUploadSize = 0
# check for invalid option combinations/preparse stuff
if args.max_upload_size and not args.upload:
print "Error: max upload size can only be specified when in upload mode"
sys.exit(1)
if args.max_upload_size:
sizeRe = re.match("^(\d+(?:[,.]\d+)?)(?:([bkmgtpe])(?:(?<!b)b?)?)?$", args.max_upload_size.lower())
if not sizeRe:
print "Error: Your max upload size param is broken."
sys.exit(1)
uploadSize, modifier = sizeRe.groups()
uploadSize = float(uploadSize.replace(",", "."))
sizes = ["b", "k", "m", "g", "t", "p", "e"]
maxUploadSize = int(uploadSize * pow(1024, sizes.index(modifier or "k")))
if maxUploadSize < 0:
print "Error: Your max upload size can't be negative"
sys.exit(1)
12 years ago
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)
if args.auth:
dpos = args.auth.find(":")
if dpos <= 0 or dpos == (len(args.auth)-1):
print "Error: User and password for HTTP basic auth need to be both at least one character long and have to be seperated by a \":\""
sys.exit(1)
mode = None
if args.upload:
mode = ServeFile.MODE_UPLOAD
elif args.list_dir:
mode = ServeFile.MODE_LISTDIR
else:
mode = ServeFile.MODE_SINGLE
server = None
12 years ago
try:
12 years ago
server = ServeFile(args.target, args.port, mode, args.ssl)
if maxUploadSize > 0:
server.setMaxUploadSize(maxUploadSize)
12 years ago
if args.ssl and args.key:
cert = args.cert or args.key
server.setSSLKeys(cert, args.key)
if args.auth:
user, password = args.auth.split(":", 1)
server.setAuth(user, password)
server.serve()
except ServeFileException, e:
print e
sys.exit(1)
12 years ago
print "Good bye.."
12 years ago
if __name__ == '__main__':
main()