Page MenuHomeDevCentral

No OneTemporary

diff --git a/woof b/woof
index 630a4a9..b219daf 100755
--- a/woof
+++ b/woof
@@ -1,549 +1,574 @@
#!/usr/bin/env python3
# woof -- an ad-hoc single file webserver
+from __future__ import annotations
+
+from typing import Generator, BinaryIO
+
import sys, os, errno, socket, getopt, subprocess, tempfile
-import cgi, urllib.request, urllib.parse, http.server
+import urllib.request, urllib.parse, http.server
+import email.parser
import readline
import configparser
import shutil, tarfile, zipfile
import struct
+from io import BytesIO, StringIO
maxdownloads = 1
cpid = -1
compressed = 'gz'
upload = False
# 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]
+def decode_multipart_form_data(
+ multipart_data: BinaryIO, headers: email.message.Message
+) -> list[tuple[dict[str, str], bytes]]:
+ """Decode multipart form data"""
+ content_type = headers["Content-Type"].encode("ascii")
+ content_len = int(headers["Content-Length"])
+ data = multipart_data.read(content_len)
+
+ content = b"Content-Type: %b\r\n%b" % (content_type, data)
+
+ parsed = email.parser.BytesParser().parsebytes(content)
+
+ results = []
+ for part in parsed.get_payload():
+ params = part.get_params(header="content-disposition")
+ payload: bytes = part.get_payload(decode=True)
+ result = dict(params), payload
+ results.append(result)
+ return results
+
+
# 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 (http.server.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 (http.server.BaseHTTPRequestHandler):
server_version = "Simons FileServer"
protocol_version = "HTTP/1.0"
filename = "."
def log_request (self, code='-', size='-'):
if code == 200:
- http.server.BaseHTTPRequestHandler.log_request (self, code, size)
+ super().log_request (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
+ multi_form = decode_multipart_form_data(self.rfile, self.headers)
- ctype, pdict = cgi.parse_header (self.headers['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")
+ for form_dict, content in multi_form:
+ if form_dict.get("name") == "upfile":
+ break
+ else:
+ # Went through without break, did not find
+ self.send_error(403, "No upload provided")
return
- upfile = form["upfile"]
-
- if not upfile.file or not upfile.filename:
+ if not content or not form_dict.get("filename"):
self.send_error (403, "No upload provided")
return
- upfilename = upfile.filename
+ upfilename = form_dict["filename"]
if "\\" in upfilename:
- upfilename = upfilename.split ("\\")[-1]
+ upfilename = upfilename.rsplit ("\\", 1)[-1]
- upfilename = os.path.basename (upfile.filename)
+ upfilename = os.path.basename (upfilename)
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:
+ except OSError as ex:
+ if ex.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)
+ print ("Accepting uploaded file: %s -> %s" % (upfilename, destfilename), file=sys.stderr)
- shutil.copyfileobj (upfile.file, os.fdopen (destfile, "wb"))
+ with BytesIO(content) as readfile:
+ with open(destfile, "wb") as writefile:
+ shutil.copyfileobj (readfile, writefile)
- if upfile.done == -1:
- self.send_error (408, "upload interrupted")
+ # if upfile.done == -1:
+ # self.send_error (408, "upload interrupted")
txt = b"""\
+ <!DOCTYPE html>
<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)
maxdownloads -= 1
return
def do_GET (self):
global maxdownloads, cpid, compressed, upload
# Form for uploading a file
if upload:
txt = b"""\
+ <!DOCTYPE html>
<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.
# This hands over the filename to the client.
self.path = urllib.parse.quote (urllib.parse.unquote (self.path))
location = "/" + urllib.parse.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 = """\
+ <!DOCTYPE html>
<html>
<head><title>302 Found</title></head>
<body>302 Found <a href="%s">here</a>.</body>
</html>\n""" % location
txt = txt.encode ('ascii')
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 ("can only serve files or directories. 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" % urllib.parse.quote (os.path.basename (self.filename + self.archive_ext)))
if os.path.isfile (self.filename):
self.send_header ("Content-Length",
- os.path.getsize (self.filename))
+ str(os.path.getsize (self.filename)))
self.end_headers ()
try:
if type == "file":
- datafile = open (self.filename, "rb")
- shutil.copyfileobj (datafile, self.wfile)
- datafile.close ()
+ with open (self.filename, "rb") as datafile:
+ shutil.copyfileobj (datafile, self.wfile)
elif type == "dir":
if compressed == 'zip':
- zfile = zipfile.ZipFile (self.wfile, '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 ()
+ with zipfile.ZipFile(
+ self.wfile, "w", zipfile.ZIP_DEFLATED
+ ) as zfile:
+ 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 RuntimeError ("invalid filename assumptions, please report!")
+ zfile.write (filename, filename[len (stripoff):])
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)
+ with tarfile.open(
+ mode=("w|" + compressed), fileobj=self.wfile
+ ) as tfile:
+ tfile.add (self.filename,
+ arcname=os.path.basename (self.filename))
+
+ except Exception as ex:
+ print (ex)
print ("Connection broke. Aborting", file=sys.stderr)
def serve_files (filename, maxdown = 1, ip_addr = '', port = 8080):
global maxdownloads
maxdownloads = maxdown
archive_ext = ""
if filename and os.path.isdir (filename):
if compressed == 'gz':
archive_ext = ".tar.gz"
elif compressed == 'bz2':
archive_ext = ".tar.bz2"
elif compressed == 'zip':
archive_ext = ".zip"
else:
archive_ext = ".tar"
# 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
FileServHTTPRequestHandler.archive_ext = archive_ext
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 = (f"http://{ip_addr}:{httpd.server_port}/" +
urllib.parse.quote (os.path.basename (filename + archive_ext)))
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)
print (file=sys.stderr)
sys.exit (1)
def woof_client (url):
urlparts = urllib.parse.urlparse (url, "http")
if urlparts[0] not in [ "http", "https" ] or urlparts[1] == '':
return None
fname = None
f = urllib.request.urlopen (url)
f_meta = f.info ()
disp = f_meta["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 == None:
url = f.geturl ()
urlparts = urllib.parse.urlparse (url)
fname = urlparts[2]
if not fname:
fname = "woof-out.bin"
if fname:
fname = urllib.parse.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, 0o644)
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 == None:
if override == True:
destfile = os.open (destfilename, os.O_WRONLY | os.O_CREAT, 0o644)
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, 0o644)
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, "wb"))
return 1;
def main ():
global cpid, upload, 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.gnu_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]) != 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:
print ()
-

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 25, 18:26 (11 h, 46 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
2257651
Default Alt Text
(20 KB)

Event Timeline