diff --git a/woof b/woof index b1f509b..2d6b1ea 100755 --- a/woof +++ b/woof @@ -24,17 +24,19 @@ # FreeBSD support with the help from Andy Gimblett, # Cygwin support by Stefan Reichör # tarfile usage suggested by Morgan Lefieux +# File upload support loosely based on code from Stephen English -import sys, os, socket, getopt, commands -import urllib, BaseHTTPServer +import sys, os, errno, socket, getopt, commands, tempfile +import cgi, urllib, BaseHTTPServer +from SocketServer import ThreadingMixIn import ConfigParser import shutil, tarfile, zipfile import struct maxdownloads = 1 TM = object -cpid = -1 compressed = 'gz' +upload = False class EvilZipStreamWrapper(TM): @@ -91,59 +93,24 @@ class EvilZipStreamWrapper(TM): # 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 + # 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. - os.environ["PATH"] = "/sbin:/usr/sbin:/usr/local/sbin:" + os.environ["PATH"] - platform = os.uname()[0]; + 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) - 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 + return candidates[0] # Main class implementing an HTTP-Requesthandler, that serves just a single @@ -156,15 +123,114 @@ class FileServHTTPRequestHandler (BaseHTTPServer.BaseHTTPRequestHandler): server_version = "Simons FileServer" protocol_version = "HTTP/1.0" - filename = "." + filename = "-" def log_request (self, code='-', size='-'): if code == 200: BaseHTTPServer.BaseHTTPRequestHandler.log_request (self, code, size) + def do_POST (self): + global maxdownloads, upload + + if not upload: + self.send_error (501, "Unsupported method (POST)") + return + + maxdownloads -= 1 + + if maxdownloads < 1: + httpd.shutdown() + + # taken from + # http://mail.python.org/pipermail/python-list/2006-September/402441.html + + ctype, pdict = cgi.parse_header (self.headers.getheader ('Content-Type')) + form = cgi.FieldStorage (fp = self.rfile, + headers = self.headers, + environ = {'REQUEST_METHOD' : 'POST'}, + keep_blank_values = 1, + strict_parsing = 1) + if not form.has_key ("upfile"): + 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, 0644) + break + except OSError, e: + if e.errno == errno.EEXIST: + continue + raise + + if not destfile: + upfilename += "." + destfile, destfilename = tempfile.mkstemp (prefix = upfilename, dir = ".") + + print >>sys.stderr, "accepting uploaded file: %s -> %s" % (upfilename, destfilename) + + shutil.copyfileobj (upfile.file, os.fdopen (destfile, "w")) + + if upfile.done == -1: + self.send_error (408, "upload interrupted") + + txt = """\ + + Woof Upload + +

Woof Upload complete

+

Thanks a lot!

+ + + """ + 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) + + return + + def do_GET (self): - global maxdownloads, cpid, compressed + global maxdownloads, compressed, upload + + # Form for uploading a file + if upload: + txt = """\ + + Woof Upload + +

Woof Upload

+
+

+

+
+ + + """ + 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) + return # Redirect any request to the filename of the file to serve. # This hands over the filename to the client. @@ -189,7 +255,7 @@ class FileServHTTPRequestHandler (BaseHTTPServer.BaseHTTPRequestHandler): \n""" % location self.send_response (302) self.send_header ("Location", location) - self.send_header ("Content-type", "text/html") + self.send_header ("Content-Type", "text/html") self.send_header ("Content-Length", str (len (txt))) self.end_headers () self.wfile.write (txt) @@ -197,63 +263,67 @@ class FileServHTTPRequestHandler (BaseHTTPServer.BaseHTTPRequestHandler): maxdownloads -= 1 - # let a separate process handle the actual download, so that - # multiple downloads can happen simultaneously. + if maxdownloads < 1: + httpd.shutdown() - cpid = os.fork () + type = None + + if os.path.isfile (self.filename): + type = "file" + elif os.path.isdir (self.filename): + type = "dir" + elif self.filename == "-": + type = "stdin" - 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, directories or stdin. Aborting." + sys.exit (1) - 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 () - 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 - 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 () + elif type == "stdin": + datafile = sys.stdin + shutil.copyfileobj (datafile, self.wfile) + except Exception, e: + print e + print >>sys.stderr, "Connection broke. Aborting" - 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" + +class ThreadedHTTPServer(ThreadingMixIn, BaseHTTPServer.HTTPServer): + """Handle requests in a separate thread""" def serve_files (filename, maxdown = 1, ip_addr = '', port = 8080): - global maxdownloads + global maxdownloads, httpd maxdownloads = maxdown @@ -263,8 +333,7 @@ def serve_files (filename, maxdown = 1, ip_addr = '', port = 8080): FileServHTTPRequestHandler.filename = filename try: - httpd = BaseHTTPServer.HTTPServer ((ip_addr, port), - FileServHTTPRequestHandler) + httpd = ThreadedHTTPServer ((ip_addr, port), FileServHTTPRequestHandler) except socket.error: print >>sys.stderr, "cannot bind to IP address '%s' port %d" % (ip_addr, port) sys.exit (1) @@ -274,20 +343,21 @@ def serve_files (filename, maxdown = 1, ip_addr = '', port = 8080): if ip_addr: print "Now serving on http://%s:%s/" % (ip_addr, httpd.server_port) - while cpid != 0 and maxdownloads > 0: - httpd.handle_request () + httpd.serve_forever () def usage (defport, defmaxdown, errmsg = None): name = os.path.basename (sys.argv[0]) print >>sys.stderr, """ - Usage: %s [-i ] [-p ] [-c ] + Usage: %s [-i ] [-p ] [-c ] [] %s [-i ] [-p ] [-c ] [-z|-j|-Z|-u] %s [-i ] [-p ] [-c ] -s + %s [-i ] [-p ] [-c ] -U Serves a single file times via http on port on IP address . + When no filename is specified, or set to '-', then stdin will be read. 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. @@ -295,6 +365,8 @@ def usage (defport, defmaxdown, errmsg = None): file described below. When -s is specified instead of a filename, %s distributes itself. + + When -U is specified, woof provides an upload form and allows uploading files. defaults: count = %d, port = %d @@ -310,7 +382,8 @@ def usage (defport, defmaxdown, errmsg = None): count = 2 ip = 127.0.0.1 compressed = gz - """ % (name, name, name, name, defmaxdown, defport) + """ % (name, name, name, name, name, defmaxdown, defport) + if errmsg: print >>sys.stderr, errmsg print >>sys.stderr @@ -319,7 +392,9 @@ def usage (defport, defmaxdown, errmsg = None): def main (): - global cpid, compressed + global cpid, upload, compressed + + filename = '-' maxdown = 1 port = 8080 @@ -352,7 +427,7 @@ def main (): defaultmaxdown = maxdown try: - options, filenames = getopt.getopt (sys.argv[1:], "hszjZui:c:p:") + options, filenames = getopt.getopt (sys.argv[1:], "hUszjZui:c:p:") except getopt.GetoptError, desc: usage (defaultport, defaultmaxdown, desc) @@ -383,6 +458,9 @@ def main (): elif option == '-h': usage (defaultport, defaultmaxdown) + elif option == '-U': + upload = True + elif option == '-z': compressed = 'gz' elif option == '-j': @@ -395,30 +473,29 @@ def main (): else: usage (defaultport, defaultmaxdown, "Unknown option: %r" % option) - if len (filenames) == 1: - filename = os.path.abspath (filenames[0]) + if upload: + if len (filenames) > 0: + usage (defaultport, defaultmaxdown, + "Conflicting usage: simultaneous up- and download not supported.") + filename = None + else: - usage (defaultport, defaultmaxdown, - "Can only serve single files/directories.") + if len (filenames) == 1: + if filenames[0] != "-": + filename = os.path.abspath (filenames[0]) - if not os.path.exists (filename): - usage (defaultport, defaultmaxdown, - "%s: No such file or directory" % filenames[0]) + if not os.path.exists (filename): + usage (defaultport, defaultmaxdown, + "Can only serve single files/directories.") - if not (os.path.isfile (filename) or os.path.isdir (filename)): - usage (defaultport, defaultmaxdown, - "%s: Neither file nor directory" % filenames[0]) + if not (os.path.isfile (filename) or os.path.isdir (filename)): + usage (defaultport, defaultmaxdown, + "%s: Neither file nor directory" % filenames[0]) + else: + filename = "-" 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__':