This is a ported to python3 version of classic woof utility.
In this fork some bugs have been fixed:
- upload (option -U) crashes program (FIXED)
| #!/usr/bin/env python3 | |
| # | |
| # woof -- an ad-hoc single file webserver | |
| # Copyright (C) 2004-2009 Simon Budig <[email protected]> | |
| # | |
| # This program is free software; you can redistribute it and/or modify | |
| # it under the terms of the GNU General Public License as published by | |
| # the Free Software Foundation; either version 2 of the License, or | |
| # (at your option) any later version. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU General Public License for more details. | |
| # | |
| # A copy of the GNU General Public License is available at | |
| # http://www.fsf.org/licenses/gpl.txt, you can also write to the | |
| # Free Software Foundation, Inc., 59 Temple Place - Suite 330, | |
| # Boston, MA 02111-1307, USA. | |
| # Darwin support with the help from Mat Caughron, <[email protected]> | |
| # Solaris support by Colin Marquardt, <[email protected]> | |
| # FreeBSD support with the help from Andy Gimblett, <[email protected]> | |
| # Cygwin support by Stefan Reichör <[email protected]> | |
| # tarfile usage suggested by Morgan Lefieux <[email protected]> | |
| # File upload support loosely based on code from Stephen English | |
| # <[email protected]> | |
| import sys | |
| import os | |
| import errno | |
| import socket | |
| import getopt | |
| import tempfile | |
| import cgi | |
| import urllib | |
| import readline | |
| import shutil | |
| import tarfile | |
| import zipfile | |
| import struct | |
| from configparser import ConfigParser | |
| from urllib.parse import urlparse, quote, unquote | |
| from http.server import BaseHTTPRequestHandler, HTTPServer | |
| maxdownloads = 1 | |
| TM = object | |
| cpid = -1 | |
| compressed = 'gz' | |
| upload = False | |
| class EvilZipStreamWrapper(TM): | |
| def __init__(self, victim): | |
| self.victim_fd = victim | |
| self.position = 0 | |
| self.tells = [] | |
| self.in_file_data = 0 | |
| def tell(self): | |
| self.tells.append(self.position) | |
| return self.position | |
| def seek(self, offset, whence=0): | |
| if offset != 0: | |
| if offset == self.tells[0] + 14: | |
| # the zipfile module tries to fix up the file header. | |
| # write Data descriptor header instead, | |
| # the next write from zipfile | |
| # is CRC, compressed_size and file_size (as required) | |
| self.write("PK\007\010") | |
| elif offset == self.tells[1]: | |
| # the zipfile module goes to the end of the file. The next | |
| # data written definitely is infrastructure (in_file_data = 0) | |
| self.tells = [] | |
| self.in_file_data = 0 | |
| else: | |
| raise "unexpected seek for EvilZipStreamWrapper" | |
| def write(self, data): | |
| # only test for headers if we know that we're not writing | |
| # (potentially compressed) data. | |
| if self.in_file_data == 0: | |
| if data[:4] == zipfile.stringFileHeader: | |
| # fix the file header for extra Data descriptor | |
| hdr = list(struct.unpack(zipfile.structFileHeader, data[:30])) | |
| hdr[3] |= (1 << 3) | |
| data = struct.pack(zipfile.structFileHeader, *hdr) + data[30:] | |
| self.in_file_data = 1 | |
| elif data[:4] == zipfile.stringCentralDir: | |
| # fix the directory entry to match file header. | |
| hdr = list(struct.unpack(zipfile.structCentralDir, data[:46])) | |
| hdr[5] |= (1 << 3) | |
| data = struct.pack(zipfile.structCentralDir, *hdr) + data[46:] | |
| self.position += len(data) | |
| self.victim_fd.write(data) | |
| def __getattr__(self, name): | |
| return getattr(self.victim_fd, name) | |
| # Utility function to guess the IP (as a string) where the server can be | |
| # reached from the outside. Quite nasty problem actually. | |
| def find_ip(): | |
| # we get a UDP-socket for the TEST-networks reserved by IANA. | |
| # It is highly unlikely, that there is special routing used | |
| # for these networks, hence the socket later should give us | |
| # the ip address of the default route. | |
| # We're doing multiple tests, to guard against the computer being | |
| # part of a test installation. | |
| candidates = [] | |
| for test_ip in ["192.0.2.0", "198.51.100.0", "203.0.113.0"]: | |
| s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
| s.connect((test_ip, 80)) | |
| ip_addr = s.getsockname()[0] | |
| s.close() | |
| if ip_addr in candidates: | |
| return ip_addr | |
| candidates.append(ip_addr) | |
| return candidates[0] | |
| # our own HTTP server class, fixing up a change in python 2.7 | |
| # since we do our fork() in the request handler | |
| # the server must not shutdown() the socket. | |
| class ForkingHTTPServer (HTTPServer): | |
| def process_request(self, request, client_address): | |
| self.finish_request(request, client_address) | |
| self.close_request(request) | |
| # Main class implementing an HTTP-Requesthandler, that serves just a single | |
| # file and redirects all other requests to this file (this passes the actual | |
| # filename to the client). | |
| # Currently it is impossible to serve different files with different | |
| # instances of this class. | |
| class FileServHTTPRequestHandler (BaseHTTPRequestHandler): | |
| server_version = "Simons FileServer" | |
| protocol_version = "HTTP/1.0" | |
| filename = "." | |
| def log_request(self, code='-', size='-'): | |
| if code == 200: | |
| BaseHTTPRequestHandler.log_request(self, code, size) | |
| def do_POST(self): | |
| global maxdownloads, upload | |
| if not upload: | |
| self.send_error(501, "Unsupported method (POST)") | |
| return | |
| # taken from | |
| # http://mail.python.org/pipermail/python-list/2006-September/402441.html | |
| ctype, pdict = cgi.parse_header(self.headers.get('Content-Type')) | |
| form = cgi.FieldStorage(fp=self.rfile, | |
| headers=self.headers, | |
| environ={'REQUEST_METHOD': 'POST'}, | |
| keep_blank_values=1, | |
| strict_parsing=1) | |
| if "upfile" not in form: | |
| self.send_error(403, "No upload provided") | |
| return | |
| upfile = form["upfile"] | |
| if not upfile.file or not upfile.filename: | |
| self.send_error(403, "No upload provided") | |
| return | |
| upfilename = upfile.filename | |
| if "\\" in upfilename: | |
| upfilename = upfilename.split("\\")[-1] | |
| upfilename = os.path.basename(upfile.filename) | |
| destfile = None | |
| for suffix in ["", ".1", ".2", ".3", ".4", ".5", ".6", ".7", ".8", ".9"]: | |
| destfilename = os.path.join(".", upfilename + suffix) | |
| try: | |
| destfile = os.open(destfilename, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o644) | |
| break | |
| except OSError as e: | |
| if e.errno == errno.EEXIST: | |
| continue | |
| raise | |
| if not destfile: | |
| upfilename += "." | |
| destfile, destfilename = tempfile.mkstemp( | |
| prefix=upfilename, dir=".") | |
| print("accepting uploaded file: %s -> %s" % ( | |
| upfilename, destfilename), file=sys.stderr) | |
| shutil.copyfileobj(upfile.file, os.fdopen(destfile, "bw")) | |
| if upfile.done == -1: | |
| self.send_error(408, "upload interrupted") | |
| txt = """\ | |
| <html> | |
| <head><title>Woof Upload</title></head> | |
| <body> | |
| <h1>Woof Upload complete</title></h1> | |
| <p>Thanks a lot!</p> | |
| </body> | |
| </html> | |
| """ | |
| self.send_response(200) | |
| self.send_header("Content-Type", "text/html") | |
| self.send_header("Content-Length", str(len(txt))) | |
| self.end_headers() | |
| self.wfile.write(txt.encode('utf8')) | |
| maxdownloads -= 1 | |
| return | |
| def do_GET(self): | |
| global maxdownloads, cpid, compressed, upload | |
| # Form for uploading a file | |
| if upload: | |
| txt = """\ | |
| <html> | |
| <head><title>Woof Upload</title></head> | |
| <body> | |
| <h1>Woof Upload</title></h1> | |
| <form name="upload" method="POST" enctype="multipart/form-data"> | |
| <p><input type="file" name="upfile" /></p> | |
| <p><input type="submit" value="Upload!" /></p> | |
| </form> | |
| </body> | |
| </html> | |
| """ | |
| self.send_response(200) | |
| self.send_header("Content-Type", "text/html") | |
| self.send_header("Content-Length", str(len(txt))) | |
| self.end_headers() | |
| self.wfile.write(txt.encode('utf8')) | |
| return | |
| # Redirect any request to the filename of the file to serve. | |
| # This hands over the filename to the client. | |
| self.path = quote(unquote(self.path)) | |
| location = "/" + quote(os.path.basename(self.filename)) | |
| if os.path.isdir(self.filename): | |
| if compressed == 'gz': | |
| location += ".tar.gz" | |
| elif compressed == 'bz2': | |
| location += ".tar.bz2" | |
| elif compressed == 'zip': | |
| location += ".zip" | |
| else: | |
| location += ".tar" | |
| if self.path != location: | |
| txt = """\ | |
| <html> | |
| <head><title>302 Found</title></head> | |
| <body>302 Found <a href="%s">here</a>.</body> | |
| </html>\n""" % location | |
| self.send_response(302) | |
| self.send_header("Location", location) | |
| self.send_header("Content-Type", "text/html") | |
| self.send_header("Content-Length", str(len(txt))) | |
| self.end_headers() | |
| self.wfile.write(txt) | |
| return | |
| maxdownloads -= 1 | |
| # let a separate process handle the actual download, so that | |
| # multiple downloads can happen simultaneously. | |
| cpid = os.fork() | |
| if cpid == 0: | |
| # Child process | |
| type = None | |
| if os.path.isfile(self.filename): | |
| type = "file" | |
| elif os.path.isdir(self.filename): | |
| type = "dir" | |
| if not type: | |
| print("can only serve files or irectories. Aborting.", file=sys.stderr) | |
| sys.exit(1) | |
| self.send_response(200) | |
| self.send_header("Content-Type", "application/octet-stream") | |
| self.send_header("Content-Disposition", "attachment;filename=%s" % | |
| quote(os.path.basename(self.filename))) | |
| if os.path.isfile(self.filename): | |
| self.send_header("Content-Length", | |
| os.path.getsize(self.filename)) | |
| self.end_headers() | |
| try: | |
| if type == "file": | |
| datafile = open(self.filename, 'rb') | |
| shutil.copyfileobj(datafile, self.wfile) | |
| datafile.close() | |
| elif type == "dir": | |
| if compressed == 'zip': | |
| ezfile = EvilZipStreamWrapper(self.wfile) | |
| zfile = zipfile.ZipFile( | |
| ezfile, 'w', zipfile.ZIP_DEFLATED) | |
| stripoff = os.path.dirname(self.filename) + os.sep | |
| for root, dirs, files in os.walk(self.filename): | |
| for f in files: | |
| filename = os.path.join(root, f) | |
| if filename[:len(stripoff)] != stripoff: | |
| raise Exception("invalid filename assumptions, please report!") | |
| zfile.write(filename, filename[len(stripoff):]) | |
| zfile.close() | |
| else: | |
| tfile = tarfile.open(mode=('w|' + compressed), | |
| fileobj=self.wfile) | |
| tfile.add(self.filename, | |
| arcname=os.path.basename(self.filename)) | |
| tfile.close() | |
| except Exception as e: | |
| print(e) | |
| print("Connection broke. Aborting", file=sys.stderr) | |
| def serve_files(filename, maxdown=1, ip_addr='', port=8080): | |
| global maxdownloads | |
| maxdownloads = maxdown | |
| # We have to somehow push the filename of the file to serve to the | |
| # class handling the requests. This is an evil way to do this... | |
| FileServHTTPRequestHandler.filename = filename | |
| try: | |
| httpd = ForkingHTTPServer((ip_addr, port), FileServHTTPRequestHandler) | |
| except socket.error: | |
| print("cannot bind to IP address '%s' port %d" % ( | |
| ip_addr, port), file=sys.stderr) | |
| sys.exit(1) | |
| if not ip_addr: | |
| ip_addr = find_ip() | |
| if ip_addr: | |
| if filename: | |
| location = "http://%s:%s/%s" % (ip_addr, httpd.server_port, | |
| quote(os.path.basename(filename))) | |
| if os.path.isdir(filename): | |
| if compressed == 'gz': | |
| location += ".tar.gz" | |
| elif compressed == 'bz2': | |
| location += ".tar.bz2" | |
| elif compressed == 'zip': | |
| location += ".zip" | |
| else: | |
| location += ".tar" | |
| else: | |
| location = "http://%s:%s/" % (ip_addr, httpd.server_port) | |
| print("Now serving on %s" % location) | |
| while cpid != 0 and maxdownloads > 0: | |
| httpd.handle_request() | |
| def usage(defport, defmaxdown, errmsg=None): | |
| name = os.path.basename(sys.argv[0]) | |
| print(""" | |
| Usage: %s [-i <ip_addr>] [-p <port>] [-c <count>] <file> | |
| %s [-i <ip_addr>] [-p <port>] [-c <count>] [-z|-j|-Z|-u] <dir> | |
| %s [-i <ip_addr>] [-p <port>] [-c <count>] -s | |
| %s [-i <ip_addr>] [-p <port>] [-c <count>] -U | |
| %s <url> | |
| Serves a single file <count> times via http on port <port> on IP | |
| address <ip_addr>. | |
| When a directory is specified, an tar archive gets served. By default | |
| it is gzip compressed. You can specify -z for gzip compression, | |
| -j for bzip2 compression, -Z for ZIP compression or -u for no compression. | |
| You can configure your default compression method in the configuration | |
| file described below. | |
| When -s is specified instead of a filename, %s distributes itself. | |
| When -U is specified, woof provides an upload form, allowing file uploads. | |
| defaults: count = %d, port = %d | |
| If started with an url as an argument, woof acts as a client, | |
| downloading the file and saving it in the current directory. | |
| You can specify different defaults in two locations: /etc/woofrc | |
| and ~/.woofrc can be INI-style config files containing the default | |
| port and the default count. The file in the home directory takes | |
| precedence. The compression methods are "off", "gz", "bz2" or "zip". | |
| Sample file: | |
| [main] | |
| port = 8008 | |
| count = 2 | |
| ip = 127.0.0.1 | |
| compressed = gz | |
| """ % (name, name, name, name, name, name, defmaxdown, defport), | |
| file=sys.stderr) | |
| if errmsg: | |
| print(errmsg, file=sys.stderr) | |
| sys.exit(1) | |
| def woof_client(url): | |
| urlparts = urlparse(url, "http") | |
| if urlparts[0] not in ["http", "https"] or urlparts[1] == '': | |
| return None | |
| fname = None | |
| f = urllib.urlopen(url) | |
| f_meta = f.info() | |
| disp = f_meta.get("Content-Disposition") | |
| if disp: | |
| disp = disp.split(";") | |
| if disp and disp[0].lower() == 'attachment': | |
| fname = [x[9:] for x in disp[1:] if x[:9].lower() == "filename="] | |
| if len(fname): | |
| fname = fname[0] | |
| else: | |
| fname = None | |
| if fname is None: | |
| url = f.geturl() | |
| urlparts = urlparse(url) | |
| fname = urlparts[2] | |
| if not fname: | |
| fname = "woof-out.bin" | |
| if fname: | |
| fname = unquote(fname) | |
| fname = os.path.basename(fname) | |
| readline.set_startup_hook(lambda: readline.insert_text(fname)) | |
| fname = input("Enter target filename: ") | |
| readline.set_startup_hook(None) | |
| override = False | |
| destfile = None | |
| destfilename = os.path.join(".", fname) | |
| try: | |
| destfile = os.open(destfilename, | |
| os.O_WRONLY | os.O_CREAT | os.O_EXCL, "0644") | |
| except OSError as e: | |
| if e.errno == errno.EEXIST: | |
| override = input("File exists. Overwrite (y/n)? ") | |
| override = override.lower() in ["y", "yes"] | |
| else: | |
| raise | |
| if destfile is None: | |
| if override is True: | |
| destfile = os.open(destfilename, os.O_WRONLY | os.O_CREAT, "0644") | |
| else: | |
| for suffix in [".1", ".2", ".3", ".4", ".5", ".6", ".7", ".8", ".9"]: | |
| destfilename = os.path.join(".", fname + suffix) | |
| try: | |
| destfile = os.open(destfilename, | |
| os.O_WRONLY | os.O_CREAT | os.O_EXCL, "0644") | |
| break | |
| except OSError as e: | |
| if e.errno == errno.EEXIST: | |
| continue | |
| raise | |
| if not destfile: | |
| destfile, destfilename = tempfile.mkstemp(prefix=fname + ".", | |
| dir=".") | |
| print("alternate filename is:", destfilename) | |
| print("downloading file: %s -> %s" % (fname, destfilename)) | |
| shutil.copyfileobj(f, os.fdopen(destfile, "w")) | |
| return 1 | |
| def main(): | |
| global cpid, upload, compressed | |
| maxdown = 1 | |
| port = 8080 | |
| ip_addr = '' | |
| config = ConfigParser() | |
| config.read(['/etc/woofrc', os.path.expanduser('~/.woofrc')]) | |
| if config.has_option('main', 'port'): | |
| port = config.getint('main', 'port') | |
| if config.has_option('main', 'count'): | |
| maxdown = config.getint('main', 'count') | |
| if config.has_option('main', 'ip'): | |
| ip_addr = config.get('main', 'ip') | |
| if config.has_option('main', 'compressed'): | |
| formats = {'gz': 'gz', | |
| 'true': 'gz', | |
| 'bz': 'bz2', | |
| 'bz2': 'bz2', | |
| 'zip': 'zip', | |
| 'off': '', | |
| 'false': ''} | |
| compressed = config.get('main', 'compressed') | |
| compressed = formats.get(compressed, 'gz') | |
| defaultport = port | |
| defaultmaxdown = maxdown | |
| try: | |
| options, filenames = getopt.getopt(sys.argv[1:], "hUszjZui:c:p:") | |
| except getopt.GetoptError as desc: | |
| usage(defaultport, defaultmaxdown, desc) | |
| for option, val in options: | |
| if option == '-c': | |
| try: | |
| maxdown = int(val) | |
| if maxdown <= 0: | |
| raise ValueError | |
| except ValueError: | |
| usage(defaultport, defaultmaxdown, | |
| "invalid download count: %r. " | |
| "Please specify an integer >= 0." % val) | |
| elif option == '-i': | |
| ip_addr = val | |
| elif option == '-p': | |
| try: | |
| port = int(val) | |
| except ValueError: | |
| usage(defaultport, defaultmaxdown, | |
| "invalid port number: %r. Please specify an integer" % val) | |
| elif option == '-s': | |
| filenames.append(__file__) | |
| elif option == '-h': | |
| usage(defaultport, defaultmaxdown) | |
| elif option == '-U': | |
| upload = True | |
| elif option == '-z': | |
| compressed = 'gz' | |
| elif option == '-j': | |
| compressed = 'bz2' | |
| elif option == '-Z': | |
| compressed = 'zip' | |
| elif option == '-u': | |
| compressed = '' | |
| else: | |
| usage(defaultport, defaultmaxdown, "Unknown option: %r" % option) | |
| if upload: | |
| if len(filenames) > 0: | |
| usage(defaultport, defaultmaxdown, | |
| "Conflicting usage: simultaneous up- and download not supported.") | |
| filename = None | |
| else: | |
| if len(filenames) == 1: | |
| if woof_client(filenames[0]) is not None: | |
| sys.exit(0) | |
| filename = os.path.abspath(filenames[0]) | |
| else: | |
| usage(defaultport, defaultmaxdown, | |
| "Can only serve single files/directories.") | |
| if not os.path.exists(filename): | |
| usage(defaultport, defaultmaxdown, | |
| "%s: No such file or directory" % filenames[0]) | |
| if not (os.path.isfile(filename) or os.path.isdir(filename)): | |
| usage(defaultport, defaultmaxdown, | |
| "%s: Neither file nor directory" % filenames[0]) | |
| serve_files(filename, maxdown, ip_addr, port) | |
| # wait for child processes to terminate | |
| if cpid != 0: | |
| try: | |
| while 1: | |
| os.wait() | |
| except OSError: | |
| pass | |
| if __name__ == '__main__': | |
| try: | |
| main() | |
| except KeyboardInterrupt: | |