This commit is contained in:
Meutel 2015-09-15 20:25:20 +02:00
parent c3d86ec53a
commit 1cd39658f0
1 changed files with 208 additions and 131 deletions

339
woof
View File

@ -24,17 +24,19 @@
# FreeBSD support with the help from Andy Gimblett, <A.M.Gimblett@swansea.ac.uk> # FreeBSD support with the help from Andy Gimblett, <A.M.Gimblett@swansea.ac.uk>
# Cygwin support by Stefan Reichör <stefan@xsteve.at> # Cygwin support by Stefan Reichör <stefan@xsteve.at>
# tarfile usage suggested by Morgan Lefieux <comete@geekandfree.org> # tarfile usage suggested by Morgan Lefieux <comete@geekandfree.org>
# File upload support loosely based on code from Stephen English <steve@secomputing.co.uk>
import sys, os, socket, getopt, commands import sys, os, errno, socket, getopt, commands, tempfile
import urllib, BaseHTTPServer import cgi, urllib, BaseHTTPServer
from SocketServer import ThreadingMixIn
import ConfigParser import ConfigParser
import shutil, tarfile, zipfile import shutil, tarfile, zipfile
import struct import struct
maxdownloads = 1 maxdownloads = 1
TM = object TM = object
cpid = -1
compressed = 'gz' compressed = 'gz'
upload = False
class EvilZipStreamWrapper(TM): class EvilZipStreamWrapper(TM):
@ -91,59 +93,24 @@ class EvilZipStreamWrapper(TM):
# reached from the outside. Quite nasty problem actually. # reached from the outside. Quite nasty problem actually.
def find_ip (): def find_ip ():
if sys.platform == "cygwin": # we get a UDP-socket for the TEST-networks reserved by IANA.
ipcfg = os.popen("ipconfig").readlines() # It is highly unlikely, that there is special routing used
for l in ipcfg: # for these networks, hence the socket later should give us
try: # the ip address of the default route.
candidat = l.split(":")[1].strip() # We're doing multiple tests, to guard against the computer being
if candidat[0].isdigit(): # part of a test installation.
break
except:
pass
return candidat
os.environ["PATH"] = "/sbin:/usr/sbin:/usr/local/sbin:" + os.environ["PATH"] candidates = []
platform = os.uname()[0]; 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": return candidates[0]
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 # Main class implementing an HTTP-Requesthandler, that serves just a single
@ -156,15 +123,114 @@ class FileServHTTPRequestHandler (BaseHTTPServer.BaseHTTPRequestHandler):
server_version = "Simons FileServer" server_version = "Simons FileServer"
protocol_version = "HTTP/1.0" protocol_version = "HTTP/1.0"
filename = "." filename = "-"
def log_request (self, code='-', size='-'): def log_request (self, code='-', size='-'):
if code == 200: if code == 200:
BaseHTTPServer.BaseHTTPRequestHandler.log_request (self, code, size) 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 = """\
<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)
return
def do_GET (self): def do_GET (self):
global maxdownloads, cpid, compressed global maxdownloads, 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)
return
# Redirect any request to the filename of the file to serve. # Redirect any request to the filename of the file to serve.
# This hands over the filename to the client. # This hands over the filename to the client.
@ -189,7 +255,7 @@ class FileServHTTPRequestHandler (BaseHTTPServer.BaseHTTPRequestHandler):
</html>\n""" % location </html>\n""" % location
self.send_response (302) self.send_response (302)
self.send_header ("Location", location) 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.send_header ("Content-Length", str (len (txt)))
self.end_headers () self.end_headers ()
self.wfile.write (txt) self.wfile.write (txt)
@ -197,63 +263,67 @@ class FileServHTTPRequestHandler (BaseHTTPServer.BaseHTTPRequestHandler):
maxdownloads -= 1 maxdownloads -= 1
# let a separate process handle the actual download, so that if maxdownloads < 1:
# multiple downloads can happen simultaneously. 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: if not type:
# Child process print >> sys.stderr, "can only serve files, directories or stdin. Aborting."
child = None sys.exit (1)
type = None
if os.path.isfile (self.filename):
type = "file"
elif os.path.isdir (self.filename):
type = "dir"
if not type: self.send_response (200)
print >> sys.stderr, "can only serve files or directories. Aborting." self.send_header ("Content-Type", "application/octet-stream")
sys.exit (1) if os.path.isfile (self.filename):
self.send_header ("Content-Length",
os.path.getsize (self.filename))
self.end_headers ()
self.send_response (200) try:
self.send_header ("Content-type", "application/octet-stream") if type == "file":
if os.path.isfile (self.filename): datafile = file (self.filename)
self.send_header ("Content-Length", shutil.copyfileobj (datafile, self.wfile)
os.path.getsize (self.filename)) datafile.close ()
self.end_headers () 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: for root, dirs, files in os.walk (self.filename):
if type == "file": for f in files:
datafile = file (self.filename) filename = os.path.join (root, f)
shutil.copyfileobj (datafile, self.wfile) if filename[:len (stripoff)] != stripoff:
datafile.close () raise RuntimeException, "invalid filename assumptions, please report!"
elif type == "dir": zfile.write (filename, filename[len (stripoff):])
if compressed == 'zip': zfile.close ()
ezfile = EvilZipStreamWrapper (self.wfile) else:
zfile = zipfile.ZipFile (ezfile, 'w', zipfile.ZIP_DEFLATED) tfile = tarfile.open (mode=('w|' + compressed),
stripoff = os.path.dirname (self.filename) + os.sep 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: class ThreadedHTTPServer(ThreadingMixIn, BaseHTTPServer.HTTPServer):
filename = os.path.join (root, f) """Handle requests in a separate thread"""
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): def serve_files (filename, maxdown = 1, ip_addr = '', port = 8080):
global maxdownloads global maxdownloads, httpd
maxdownloads = maxdown maxdownloads = maxdown
@ -263,8 +333,7 @@ def serve_files (filename, maxdown = 1, ip_addr = '', port = 8080):
FileServHTTPRequestHandler.filename = filename FileServHTTPRequestHandler.filename = filename
try: try:
httpd = BaseHTTPServer.HTTPServer ((ip_addr, port), httpd = ThreadedHTTPServer ((ip_addr, port), FileServHTTPRequestHandler)
FileServHTTPRequestHandler)
except socket.error: except socket.error:
print >>sys.stderr, "cannot bind to IP address '%s' port %d" % (ip_addr, port) print >>sys.stderr, "cannot bind to IP address '%s' port %d" % (ip_addr, port)
sys.exit (1) sys.exit (1)
@ -274,20 +343,21 @@ def serve_files (filename, maxdown = 1, ip_addr = '', port = 8080):
if ip_addr: if ip_addr:
print "Now serving on http://%s:%s/" % (ip_addr, httpd.server_port) print "Now serving on http://%s:%s/" % (ip_addr, httpd.server_port)
while cpid != 0 and maxdownloads > 0: httpd.serve_forever ()
httpd.handle_request ()
def usage (defport, defmaxdown, errmsg = None): def usage (defport, defmaxdown, errmsg = None):
name = os.path.basename (sys.argv[0]) name = os.path.basename (sys.argv[0])
print >>sys.stderr, """ print >>sys.stderr, """
Usage: %s [-i <ip_addr>] [-p <port>] [-c <count>] <file> 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>] [-z|-j|-Z|-u] <dir>
%s [-i <ip_addr>] [-p <port>] [-c <count>] -s %s [-i <ip_addr>] [-p <port>] [-c <count>] -s
%s [-i <ip_addr>] [-p <port>] [-c <count>] -U
Serves a single file <count> times via http on port <port> on IP Serves a single file <count> times via http on port <port> on IP
address <ip_addr>. address <ip_addr>.
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 When a directory is specified, an tar archive gets served. By default
it is gzip compressed. You can specify -z for gzip compression, it is gzip compressed. You can specify -z for gzip compression,
-j for bzip2 compression, -Z for ZIP compression or -u for no 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. file described below.
When -s is specified instead of a filename, %s distributes itself. 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 defaults: count = %d, port = %d
@ -310,7 +382,8 @@ def usage (defport, defmaxdown, errmsg = None):
count = 2 count = 2
ip = 127.0.0.1 ip = 127.0.0.1
compressed = gz compressed = gz
""" % (name, name, name, name, defmaxdown, defport) """ % (name, name, name, name, name, defmaxdown, defport)
if errmsg: if errmsg:
print >>sys.stderr, errmsg print >>sys.stderr, errmsg
print >>sys.stderr print >>sys.stderr
@ -319,7 +392,9 @@ def usage (defport, defmaxdown, errmsg = None):
def main (): def main ():
global cpid, compressed global cpid, upload, compressed
filename = '-'
maxdown = 1 maxdown = 1
port = 8080 port = 8080
@ -352,7 +427,7 @@ def main ():
defaultmaxdown = maxdown defaultmaxdown = maxdown
try: 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: except getopt.GetoptError, desc:
usage (defaultport, defaultmaxdown, desc) usage (defaultport, defaultmaxdown, desc)
@ -383,6 +458,9 @@ def main ():
elif option == '-h': elif option == '-h':
usage (defaultport, defaultmaxdown) usage (defaultport, defaultmaxdown)
elif option == '-U':
upload = True
elif option == '-z': elif option == '-z':
compressed = 'gz' compressed = 'gz'
elif option == '-j': elif option == '-j':
@ -395,30 +473,29 @@ def main ():
else: else:
usage (defaultport, defaultmaxdown, "Unknown option: %r" % option) usage (defaultport, defaultmaxdown, "Unknown option: %r" % option)
if len (filenames) == 1: if upload:
filename = os.path.abspath (filenames[0]) if len (filenames) > 0:
usage (defaultport, defaultmaxdown,
"Conflicting usage: simultaneous up- and download not supported.")
filename = None
else: else:
usage (defaultport, defaultmaxdown, if len (filenames) == 1:
"Can only serve single files/directories.") if filenames[0] != "-":
filename = os.path.abspath (filenames[0])
if not os.path.exists (filename): if not os.path.exists (filename):
usage (defaultport, defaultmaxdown, usage (defaultport, defaultmaxdown,
"%s: No such file or directory" % filenames[0]) "Can only serve single files/directories.")
if not (os.path.isfile (filename) or os.path.isdir (filename)): if not (os.path.isfile (filename) or os.path.isdir (filename)):
usage (defaultport, defaultmaxdown, usage (defaultport, defaultmaxdown,
"%s: Neither file nor directory" % filenames[0]) "%s: Neither file nor directory" % filenames[0])
else:
filename = "-"
serve_files (filename, maxdown, ip_addr, port) 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__': if __name__=='__main__':