#!/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 = """

""" 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 or curl -X POST -d @file . """ 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 . """ 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): raise ServeFileException("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()