#!/usr/bin/env python # -*- encoding: utf-8 -*- # # woof -- an ad-hoc single file webserver # Copyright (C) 2004-2009 Simon Budig # # 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, # Solaris support by Colin Marquardt, # FreeBSD support with the help from Andy Gimblett, # Cygwin support by Stefan Reichör # tarfile usage suggested by Morgan Lefieux import sys, os, socket, getopt, commands import urllib, BaseHTTPServer import ConfigParser import shutil, tarfile, zipfile import struct maxdownloads = 1 TM = object cpid = -1 compressed = 'gz' 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 (): if sys.platform == "cygwin": ipcfg = os.popen("ipconfig").readlines() for l in ipcfg: try: candidat = l.split(":")[1].strip() if candidat[0].isdigit(): break except: pass return candidat os.environ["PATH"] = "/sbin:/usr/sbin:/usr/local/sbin:" + os.environ["PATH"] platform = os.uname()[0]; if platform == "Linux": netstat = commands.getoutput ("LC_MESSAGES=C netstat -rn") defiface = [i.split ()[-1] for i in netstat.split ('\n') if i.split ()[0] == "0.0.0.0"] elif platform in ("Darwin", "FreeBSD", "NetBSD"): netstat = commands.getoutput ("LC_MESSAGES=C netstat -rn") defiface = [i.split ()[-1] for i in netstat.split ('\n') if len(i) > 2 and i.split ()[0] == "default"] elif platform == "SunOS": netstat = commands.getoutput ("LC_MESSAGES=C netstat -arn") defiface = [i.split ()[-1] for i in netstat.split ('\n') if len(i) > 2 and i.split ()[0] == "0.0.0.0"] else: print >>sys.stderr, "Unsupported platform; please add support for your platform in find_ip()."; return None if not defiface: return None if platform == "Linux": ifcfg = commands.getoutput ("LC_MESSAGES=C ifconfig " + defiface[0]).split ("inet addr:") elif platform in ("Darwin", "FreeBSD", "SunOS", "NetBSD"): ifcfg = commands.getoutput ("LC_MESSAGES=C ifconfig " + defiface[0]).split ("inet ") if len (ifcfg) != 2: return None ip_addr = ifcfg[1].split ()[0] # sanity check try: ints = [ i for i in ip_addr.split (".") if 0 <= int(i) <= 255] if len (ints) != 4: return None except ValueError: return None return ip_addr # 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 (BaseHTTPServer.BaseHTTPRequestHandler): server_version = "Simons FileServer" protocol_version = "HTTP/1.0" filename = "." def log_request (self, code='-', size='-'): if code == 200: BaseHTTPServer.BaseHTTPRequestHandler.log_request (self, code, size) def do_GET (self): global maxdownloads, cpid, compressed # Redirect any request to the filename of the file to serve. # This hands over the filename to the client. self.path = urllib.quote (urllib.unquote (self.path)) location = "/" + urllib.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 = """\ 302 Found 302 Found here. \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 child = None type = None if os.path.isfile (self.filename): type = "file" elif os.path.isdir (self.filename): type = "dir" if not type: print >> sys.stderr, "can only serve files or directories. Aborting." sys.exit (1) self.send_response (200) self.send_header ("Content-type", "application/octet-stream") if os.path.isfile (self.filename): self.send_header ("Content-Length", os.path.getsize (self.filename)) self.end_headers () try: if type == "file": datafile = file (self.filename) 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 RuntimeException, "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, e: print e print >>sys.stderr, "Connection broke. Aborting" 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 = BaseHTTPServer.HTTPServer ((ip_addr, port), FileServHTTPRequestHandler) except socket.error: print >>sys.stderr, "cannot bind to IP address '%s' port %d" % (ip_addr, port) sys.exit (1) if not ip_addr: ip_addr = find_ip () if ip_addr: print "Now serving on http://%s:%s/" % (ip_addr, httpd.server_port) while cpid != 0 and maxdownloads > 0: httpd.handle_request () def usage (defport, defmaxdown, errmsg = None): name = os.path.basename (sys.argv[0]) print >>sys.stderr, """ Usage: %s [-i ] [-p ] [-c ] %s [-i ] [-p ] [-c ] [-z|-j|-Z|-u] %s [-i ] [-p ] [-c ] -s Serves a single file times via http on port on IP address . 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. defaults: count = %d, port = %d 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, defmaxdown, defport) if errmsg: print >>sys.stderr, errmsg print >>sys.stderr sys.exit (1) def main (): global cpid, compressed maxdown = 1 port = 8080 ip_addr = '' config = ConfigParser.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:], "hszjZui:c:p:") except getopt.GetoptError, 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 == '-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 len (filenames) == 1: 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: pass