simon-budig / woof Goto Github PK
View Code? Open in Web Editor NEWan ad-hoc single file webserver
an ad-hoc single file webserver
I would like to use a few parts of your code in a project of mine, and I was wondering what license this project is under. I don't see a license file, so I thought I'd ask.
Using woof on Python 3.11+ now throws this warning every time:
woof:30: DeprecationWarning: 'cgi' is deprecated and slated for removal in Python 3.13
import cgi, urllib.request, urllib.parse, http.server
The Python documentation on cgi recommends:
Deprecated since version 3.11, will be removed in version 3.13: The cgi module is deprecated (see PEP 594 for details and alternatives).
The FieldStorage class can typically be replaced with urllib.parse.parse_qsl() for GET and HEAD requests, and the email.message module or multipart for POST and PUT. Most utility functions have replacements.
Hi,
I started to create a py3 version of woof a couple of months ago, and now I noticed that you already pushed it here.
It's practically the same, but:
Just wanted to put this on your radar. I auropep8ed my code, so below a diff between our versions.
My code is at https://github.com/beardhatcode/python3-woof
If you are interested, I can make a PR for the tests and/or the removal of the evilZipWrapper (or you can just steal it).
1c1
< #!/usr/bin/env python3
---
> #!/usr/bin/env python
27a28
> # Python 3 support by Robbert Gurdeep Singh <[email protected]>
34d34
< import subprocess
37c37
< import urllib.request
---
> import urllib
54,103d53
< 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(b"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 IOError("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)
<
<
164d113
< ctype, pdict = cgi.parse_header(self.headers['Content-Type'])
170c119
< if "upfile" not in form:
---
> if not "upfile" in form:
191,192c140,141
< destfile = os.open(destfilename, os.O_WRONLY |
< os.O_CREAT | os.O_EXCL, 0o644)
---
> destfile = os.open(destfilename, os.O_WRONLY | os.O_CREAT |
> os.O_EXCL, 0o644)
204,205c153,154
< print("accepting uploaded file: %s -> %s" %
< (upfilename, destfilename), file=sys.stderr)
---
> print(
> f"accepting uploaded file: {upfilename} -> {destfilename}", file=sys.stderr)
212c161,162
< txt = b"""\
---
> txt = """\
> <!DOCTYPE html>
225c175
< self.wfile.write(txt)
---
> self.wfile.write(txt.encode("utf-8"))
236,247c186,201
< txt = b"""\
< <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>
< """
---
> txt = """\
> <!DOCTYPE html>
> <html lang="en">
> <head>
> <meta charset="utf-8"/>
> <title>Woof Upload</title>
> </head>
> <body>
> <h1>Woof Upload</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>"""
>
252c206
< self.wfile.write(txt)
---
> self.wfile.write(txt.encode("utf-8"))
271c225,226
< txt = """\
---
> txt = f"""\
> <!DOCTYPE html>
274,276c229,230
< <body>302 Found <a href="%s">here</a>.</body>
< </html>\n""" % location
< txt = txt.encode('ascii')
---
> <body>302 Found <a href="{location}">here</a>.</body>
> </html>\n"""
282c236
< self.wfile.write(txt)
---
> self.wfile.write(txt.encode("utf-8"))
303c257
< print("can only serve files or directories. Aborting.",
---
> print("can only serve files or directories. Aborting.",
309,310c263,265
< self.send_header("Content-Disposition", "attachment;filename=%s" %
< urllib.parse.quote(os.path.basename(self.filename)))
---
> safe_filename = urllib.parse.quote(os.path.basename(self.filename))
> self.send_header("Content-Disposition",
> f"attachment;filename={safe_filename}")
318,320c273,274
< datafile = open(self.filename, "rb")
< shutil.copyfileobj(datafile, self.wfile)
< datafile.close()
---
> with open(self.filename, "rb") as datafile:
> shutil.copyfileobj(datafile, self.wfile)
323,335c277,288
< 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()
---
> 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 Exception(
> "invalid filename assumptions, please report!")
> zfile.write(
> filename, filename[len(stripoff):])
337,341c290,293
< tfile = tarfile.open(mode=('w|' + compressed),
< fileobj=self.wfile)
< tfile.add(self.filename,
< arcname=os.path.basename(self.filename))
< tfile.close()
---
> with tarfile.open(mode=('w|' + compressed),
> fileobj=self.wfile) as tfile:
> tfile.add(self.filename,
> arcname=os.path.basename(self.filename))
343a296
> print(e.with_traceback())
360,361c313,314
< print("cannot bind to IP address '%s' port %d" %
< (ip_addr, port), file=sys.stderr)
---
> print(f"cannot bind to IP address '{ip_addr}' port {port}",
> file=sys.stderr)
368,369c321
< location = "http://%s:%s/%s" % (ip_addr, httpd.server_port,
< urllib.parse.quote(os.path.basename(filename)))
---
> location = f"http://{ip_addr}:{httpd.server_port}/{urllib.parse.quote(os.path.basename(filename))}"
380c332
< location = "http://%s:%s/" % (ip_addr, httpd.server_port)
---
> location = f"http://{ip_addr}:{httpd.server_port}"
382c334
< print("Now serving on %s" % location)
---
> print(f"Now serving on {location}")
390,394c342,348
< 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
---
> print(f"""
> Usage: {name} [-i <ip_addr>] [-p <port>] [-c <count>] <file>
> {name} [-i <ip_addr>] [-p <port>] [-c <count>] [-z|-j|-Z|-u] <dir>
> {name} [-i <ip_addr>] [-p <port>] [-c <count>] -s
> {name} [-i <ip_addr>] [-p <port>] [-c <count>] -U
>
> {name} <url>
396,397d349
< %s <url>
<
401,403c353,355
< 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
---
> it is gzip compressed. You can specify -z for gzip compression, -j for
> bzip2 compression, -Z for ZIP compression or -u for no compression (tar).
> You can configure your default compression method in the configuration
406c358
< When -s is specified instead of a filename, %s distributes itself.
---
> When -s is specified instead of a filename, {name} distributes itself.
409,410c361,362
<
< defaults: count = %d, port = %d
---
>
> defaults: count = {defmaxdown}, port = {defport}
427c379
< """ % (name, name, name, name, name, name, defmaxdown, defport), file=sys.stderr)
---
> """, file=sys.stderr)
431c383
< print(file=sys.stderr)
---
> print("", file=sys.stderr)
442c394
< f = urllib.request.urlopen(url)
---
> f = urllib.urlopen(url)
445c397
< disp = f_meta["Content-Disposition"]
---
> disp = f_meta.getheader("Content-Disposition")
507c459
< print("downloading file: %s -> %s" % (fname, destfilename))
---
> print(f"downloading file: {fname} -> {destfilename}")
509c461
< shutil.copyfileobj(f, os.fdopen(destfile, "wb"))
---
> shutil.copyfileobj(f, os.fdopen(destfile, "w"))
560,561c512,513
< "invalid download count: %r. "
< "Please specify an integer >= 0." % val)
---
> f"invalid download count: {val}. "
> "Please specify an integer >= 0.")
571c523
< "invalid port number: %r. Please specify an integer" % val)
---
> f"invalid port number: {val}. Please specify an integer")
592c544
< usage(defaultport, defaultmaxdown, "Unknown option: %r" % option)
---
> usage(defaultport, defaultmaxdown, f"Unknown option: {option}")
602c554
< if woof_client(filenames[0]) != None:
---
> if woof_client(filenames[0]) is not None:
612c564
< "%s: No such file or directory" % filenames[0])
---
> f"{filenames[0]}: No such file or directory")
616c568
< "%s: Neither file nor directory" % filenames[0])
---
> f"{filenames[0]}: Neither file nor directory")
633c585
< print()
---
> print("")
I'd like to upload multiple files from the "browse" form when using "woof -U".
When you use woof to send (in my testing) any directory, the default filename sent to clients to use is missing the compression file extension.
For example, trying to sent the directory "Documents" to someone with gzip compression, the filename should be "Documents.tar.gz", but when you download the file, firefox and woof's own tiny web browser (and probably everything else) wants to save the file as only "Documents", no "tar.gz" extension.
If you try to send a directory with woof using ZIP compression, woof crashes with this traceback (at least in my testing):
Now serving on http://192.168.1.200:8080/<directory name here>.zip 192.168.1.200 - - [08/Jun/2021 20:45:34] "GET /<directory name here>.zip HTTP/1.1" 200 - unexpected seek for EvilZipStreamWrapper Connection broke. Aborting Exception ignored in: <function ZipFile.__del__ at 0x7f690e446700> Traceback (most recent call last): File "/usr/lib/python3.8/zipfile.py", line 1821, in __del__ self.close() File "/usr/lib/python3.8/zipfile.py", line 1838, in close self.fp.seek(self.start_dir) File "Desktop/interwebs-woof.py", line 68, in seek raise IOError ("unexpected seek for EvilZipStreamWrapper") OSError: unexpected seek for EvilZipStreamWrapper
As you can see, the "EvilZipStreamWrapper" is getting an invalid seek value. And to be honest, looking at the code, I'm not quite sure how you'd fix it, or what purpose it serves in the first place.
Now serving on http://192.168.0.3:8091/1.tar.gz
192.168.0.100 - - [09/Jul/2021 00:42:12] "GET /1.tar.gz HTTP/1.1" 200 -
[Errno 104] Connection reset by peer
Connection broke. Aborting
Exception ignored in: <function _Stream.__del__ at 0x7f9901c31f70>
Traceback (most recent call last):
File "/usr/lib/python3.9/tarfile.py", line 410, in __del__
self.close()
File "/usr/lib/python3.9/tarfile.py", line 460, in close
self.fileobj.write(self.buf)
File "/usr/lib/python3.9/socketserver.py", line 826, in write
self._sock.sendall(b)
OSError: [Errno 9] Bad file descriptor
192.168.0.100 - - [09/Jul/2021 00:42:14] "GET /1.tar.gz HTTP/1.1" 200 -
[Errno 104] Connection reset by peer
Connection broke. Aborting
Exception ignored in: <function _Stream.__del__ at 0x7f9901c31f70>
Traceback (most recent call last):
File "/usr/lib/python3.9/tarfile.py", line 410, in __del__
self.close()
File "/usr/lib/python3.9/tarfile.py", line 460, in close
self.fileobj.write(self.buf)
File "/usr/lib/python3.9/socketserver.py", line 826, in write
self._sock.sendall(b)
OSError: [Errno 9] Bad file descriptor
Hi, we're working on updating the woof formula in Homebrew (Homebrew/homebrew-core#94258) to a version that supports Python 3, in anticipation of the system Python 2 being removed in macOS 12.3. However, the woof repository doesn't have any version tags or releases and one of our Project Leadership Committee (PLC) members has made it clear that we should only update the formula to a new upstream release.
That is to say, it's not appropriate for us to simply pick the newest commit and use the commit date as the version, as it's not an official release. Not having any version information to reference also makes it difficult for us to check for new versions, as we would have to manually check the commit history instead of programmatically checking the repository tags/releases for new versions.
The homepage previously published date-based versions (e.g., woof-2012-05-31.py
, woof-2009-12-27.py
, etc.) but a new version hasn't been officially released since 2012-05-31. Unfortunately, that release came from the commit before Python 3 support was added, so the woof formula is facing removal if it can't be updated to a newer version.
With this in mind, could you please create version tags (e.g., 2021-06-09
) for releases? If it makes sense for you, it would be helpful to create a version tag for an up-to-date version, so there's something we can use in the related Homebrew PR.
I think I've spotted something in the help/documentation on Debian; the manpage says:
-Z <dir>
Used on a directory, it creates a tarball with ZIP compression
However, I am fairly confident, that would actually create a proper zip archive file. Otherwise this is a kind of a trap for people if they share files with non-*nix users.
One of the things that's always annoyed me with woof
is that it requires all of the options before the arguments.
Example:
$ woof /tmp/Screenshot_2022-01-13_11-12-40.png
cannot bind to IP address '' port 8080
$ woof /tmp/Screenshot_2022-01-13_11-12-40.png -p 8000
Usage: woof [-i <ip_addr>] [-p <port>] [-c <count>] <file>
woof [-i <ip_addr>] [-p <port>] [-c <count>] [-z|-j|-Z|-u] <dir>
woof [-i <ip_addr>] [-p <port>] [-c <count>] -s
woof [-i <ip_addr>] [-p <port>] [-c <count>] -U
woof <url> ... [Snip]
But this is easily fixed by just calling gnu_getopt()
instead of getopt()
$ git diff
diff --git a/woof b/woof
index 74784af..414deca 100755
--- a/woof
+++ b/woof
@@ -483,7 +483,7 @@ def main ():
defaultmaxdown = maxdown
try:
- options, filenames = getopt.getopt (sys.argv[1:], "hUszjZui:c:p:")
+ options, filenames = getopt.gnu_getopt (sys.argv[1:], "hUszjZui:c:p:")
except getopt.GetoptError as desc:
usage (defaultport, defaultmaxdown, desc)
$ ./woof /tmp/Screenshot_2022-01-13_11-12-40.png
cannot bind to IP address '' port 8080
$ ./woof /tmp/Screenshot_2022-01-13_11-12-40.png -p 8000
Now serving on http://10.0.1.104:8000/Screenshot_2022-01-13_11-12-40.png
And possibly give it its own counter that defaults to unlimited or the same as the GET counter's value?
$ woof foo.bar
Now serving on http://0.0.0.0:54321/foo.bar
127.0.0.1 - - [28/Jun/2022 11:56:53] code 501, message Unsupported method ('HEAD')
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.