Show HN: A pure bash web server. No netcat, socat, etc.
github.com
github.com
local tmpFile="${TMPDIR:-/tmp/bash-web-server.$$}"
# ...
rm "$tmpFile"
To avoid collisions when the PID is reused, and to clean up0 the tempfile on errors, I recommend using mktemp and trap: local tmpfile="$(mktemp --tmpdir="${TMPDIR:-/tmp}" bash-web-server.XXXXXX)"
trap "rm -f \"${tmpfile}\"" RETURN EXIT
# ...
# Do nothing at the end of the function; trap will
# remove the file at RETURN automatically.
Otherwise, I like the implementation! It's nice to see good bash techniques like parsing with "IFS='&' read -ra data" and rewriting variables with %%/etc.But in this script i would like to avoid external commands. I will even remove the use of tmp files kn thw future.
true | sleep infinity &
STDINPID="$!"
TMPPIPE="/proc/$STDINPID/fd/0"
echo hello > "$TMPPIPE"
echo write more > "$TMPPIPE"
cat "$TMPPIPE"
echo write again > "$TMPPIPE"
cat "$TMPPIPE"
kill -9 "$STDINPID" # clean up pipe
Now I would like to avoid the additional process by using the current shell process instead. true | sleep infinity &
STDINPID="$!"
exec {mypipe}<"/proc/$STDINPID/fd/0" # hijack FD
TMPPIPE="/proc/$$/fd/${mypipe}"
disown "$STDINPID" # suppress killed message on stderr
kill -9 "$STDINPID" # clean up process now already
echo hello > "$TMPPIPE"
echo write more > "$TMPPIPE"
cat "$TMPPIPE"
echo write again > "$TMPPIPE"
cat "$TMPPIPE" exec {checkfd}>/dev/null
CHECKFDPATH="/proc/$$/fd/${checkfd}"
(while [ -e "$CHECKFDPATH" ]; do sleep 1; done) > >(true) &
STDINPID="$!"
WRITER="/proc/$STDINPID/fd/1"
while IFS= read -r LINE; do
# only echo lines if we didn't close the connection yet
if [ -e "$CHECKFDPATH" ]; then
echo "$LINE" | tr '[:lower:]' '[:upper:]' > "$WRITER"
if [ "$LINE" = "bye" ]; then
exec {checkfd}<&-
fi
else
echo "received while closed: $LINE"
fi
done < <(nc -q 1 -l 8080 < "$WRITER")
if [ -e "$CHECKFDPATH" ]; then
# close checkfd to close writer pipe
exec {checkfd}<&-
fi exec {checkfd}>/dev/null
CHECKFDPATH="/proc/$$/fd/${checkfd}"
(while [ -e "$CHECKFDPATH" ]; do sleep 1; done) > >(true) &
STDINPID="$!"
disown "$STDINPID"
READER="/proc/$STDINPID/fd/1"
{ while IFS= read -r LINE; do
echo "$LINE" | tr '[:lower:]' '[:upper:]'
if [ "$LINE" = "bye" ]; then
echo exiting > /dev/stderr
break
fi
done < "$READER" ; kill -9 "$STDINPID" 2>/dev/null || true ; } | { nc -q 1 -l 8080 > "$READER" ; kill -9 "$STDINPID" 2>/dev/null || true ; }
exec {checkfd}<&-The fd will only be used to stor it temporary since, we first need to sent headers, and the headers need to be sent before the bofy. The server base os working fine now with accept and a patch request os already sent to bash. So probaböy on the next release accept will be full usable.
echo "" > /tmp/buffer; tail -f /tmp/buffer | netcat httpbin.org 80 | netcat -lp 8080 > /tmp/buffer
I call it a circular pipeline. mkfifo -m0600 /tmp/buffer && netcat httpbin.org 80 < /tmp/buffer | netcat -lp 8080 > /tmp/buffer
Might want the file if you want a record of course.Regarding keeping the file, if memory serves the "real" pipeline had some `tee`s in it. I recreated this much more recently to make a GitHub repo of funny code snippets (https://github.com/MaxBondABE/one-thread-crashing/).
You reminded me of a different toy I made many years ago. I created FIFOs for files that you might try to read when exploiting an arbitrary file read in a web app, say /etc/www/apache.conf (if that's the right path - it's been a while since I've configured a web server!) and I'd have a program that opened them. This would block until someone opened the other end of the FIFO, at which point I'd raise an alarm (which was just a print statement).
1) Create a temp buffer file
2) Read the file constantly to STDOUT?
3) What does netcat httpbin.org 80 | netcat -lp 8080 do?
What would be a good use of this circular pipeline? Can you give me some example of what you use this for? Thanks
A.)
Some context:
- Our goal here is to match up to two network connections, so that anything we receive on one is forwarded to the other.
- netcat is a "networking swiss army knife", it's a tool for making network connections
- HTTP and bash pipelines both happen to be "line oriented", meaning they operate one line at a time. Exploiting this happy coincedence is what makes this work
- httpbin.org is a metanym for any website you need.
So:
echo "" > /tmp/buffer
Initialize our buffer to an empty state tail -f /tmp/buffer
Read each line in the buffer, one at a time, and feed them to the next command. Do this forever (-f). netcat httpbin.org 80
Connect to the website httpbin.org. Transmit each line from our buffer to the website. netcat -lp 8080 > /tmp/buffer
Accept a connection. Transmit any data we receive from httpbin.org to this client; write any data we receive from the client to the buffer, so that it can be transmitted to httpbin.org.B.) I don't think there is one! I've never needed to do this again in the, gosh, seven intervening years. Normally bash pipelines operate like a bucket brigade, moving data unidirectionally through various processing steps, and usually dumping it into a file at the end.
They aren't normally for creating long-lived programs that manage bidirectional flows of data. Which is why writing a web server in bash is a fun challenge.
And I mean this as encouragement, but you should read the synopsis from the GNU netcat manual. It's short and explains it well enough. netcat httpbin.org 80 is using connect mode, the -l one is using listen mode, and -p is --local-port (the port to listen on in listen mode).
For anyone looking for an easy way to spin up a command line web server, python has one built in, so if you are running linux usually you can do:
`python3 -m http.server`
From a terminal and it will spin up a web server that serves the current directory. I use this all the time for quick tinkering vs. installing/configuring nginx/node.js/caddy or whatever the cool kids are using these days.
This makes it really really not great when trying to serve pages (with various attending static files), or to ad-hoc share large resources with other people on the network.
It's fine if you're also evented, but that's not the case here, http.server literally can only interact with one connection at a time, the next connection(s) will be queued up waiting for http.server to finish its thing and accept them.
> My understanding anyway is that http.server is explicitly not for production use!
Sure, but "I'm at a lan party and have the installer for $game so we don't explode the 'net connection" is not the sort of "production use" that's intended by this statement.
Neither is "I want to open this HTML file I wrote which has a bunch of static assets booting up the webapp".
Looking at you HttpClient-on-esp8266...
I think I see your problem.
https://stackoverflow.com/questions/12905426/what-is-a-faste...
Also, the python solution is only relevant if python is already installed. Something that's not true on Windows and is or will soon be true on MacOS.
Getting python up an running on Windows is fairly trivial though. You don't even need local admin permissions if your machine is locked down by your IT dept.
""" WARNING: Python 2.7 is not recommended. This version is included in macOS for compatibility with legacy software. Future versions of macOS will not include Python 2.7. Instead, it is recommended that you transition to using 'python3' from within Terminal. """
There is no version of Python 3 included with the OS. The XCode app/developer tools include Python 3.8. I expect macOS 13 won't include any Python interpreter out the box
"loadable accept builtin"
Ahh.
Gawk can be a web server also, typically just "as-shipped", without any new extensions. Doesn't scale well though :)
The /inet/tcp/8080/0/0 is a gawk-ism, versus a Linux thing:
https://www.gnu.org/software/gawk/manual/html_node/TCP_002fI...
Unfortunately, gawk doesn't let you control the listen/accept part of it, so it doesn't scale well. Not sure why they did it that way. A separate accept() call would have made it actually usable in a non-toy way.
This is how you get stuff like hiPHoP.
Definitely closest to the mark though, kudos!
while :; do
It's clearly a while true loop, but why does an ascii crying face produce true?https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V...
$ type :
: is a shell builtin while true; dohttps://pubs.opengroup.org/onlinepubs/9699919799/utilities/t...
: > $LOGFILE
which creates a zero-length log file, and removes the contents if it already existed.There was a post a bit ago that had a one liner for a basic web server using net cat:
while : ; do cat page.html | nc -l 80; done while :; do nc -l 80 < page.html; doneI always use the pipe, it is intuitive to me to think left-to-right, output of this becomes input of next. But I always see stackoverflow answers using the other approach.
In the second block, you're just launching one program that reads input, not two. It's slightly more efficient.
I also tend to build large *NIX pipelines using `cat $foo | [...]`, but it's some times regarded as bad form. See: "useless use of cat" for more examples.
Maybe it's lighter than cat, but whatever code path is triggered by the `<` can't be zero-cost
while :; do <page.html nc -l 80 ; doneThis would be very useful as a fallback - in case something like `socat` isn't installed, this can be used to display some sort of dumbed down version of the site
Falling back to something like socat, would destroy the whole idea behind it. I think the best way is patching accept and shipping it with the project. Like this no need to fallback.
And considering using something like socat, will force you to fork each request into a new process, which will be slow as hell. I worked before on a web framework, which was used behind httpd or nginx as cgi, it was fast. But the moment where you get more connections it was slow as hell.
The idea of this project is writing some kind of fastCGI (like php-fpm), but for this i would need, to allow multithreading etc..
https://gist.github.com/pmarreck/f4dbb02396c4532762a7a64511c...
It actually took an entire afternoon and went through a couple iterations, and I hadn't even written a test for it yet (and it just so happens that I started an MVP shell-script testing library for just such an occasion when I ALSO got frustrated with the bash-accessible testing libraries available to me, also in Bash: https://github.com/pmarreck/tinytestlib)
But then I immediately felt bad. I could have written all of this in less than an hour in a language like Elixir, with test coverage (and the testing/assertion lib is built-in, so it would have been free).
It is impressive and code-golfish that someone can "write a webserver in pure Bash" but Bash is a crap language to script in at this point, and adding more Bash code to the world just seems wrong now. Bourne shells were invented in the 70's (!), long before many language advancements and understandings, and they thus have MANY warts (many of which we're familiar with).
The next time I feel the urge to code anything more than a one-liner in Bash, I'm writing it in elixir script https://thinkingelixir.com/2019-04-running-an-elixir-file-as... and just sucking up the extra run time (or compiling it into an executable with something like https://github.com/spawnfest/bakeware/ and sucking up the extra megabytes), and then MAYBE writing a wrapper function for my .profile
Incidentally, if you DO want a shell that "tastes like" Bourne but is functional and thus sucks a lot less, check out https://github.com/wryun/es-shell, or my fork of it https://github.com/pmarreck/es-shell/
...it feels like "if it's possible to do in bash, this does it".
https://github.com/russellballestrini/bash-kira/blob/master/...
Kira (bash-kira): a small bash script for killing programs which run too long.
I found Bats straightforward and simple to use, never felt the need to reinvent the wheel there.
1) bloated
2) doesn't use a very nice syntax (hint: bashisms are not a very nice syntax). Compare BATS assertions with this for example (the test of my test library, which uses the library's own functions): https://github.com/pmarreck/tinytestlib/blob/yolo/test Call me crazy but I think `assert_equal "4" "$result"` is a lot more readable than `[ "$result" -eq 4 ]`
3) doesn't let you assert on all of (return code, stdout, stderr) from running a single command. I created a `capture` function to do that and bring those all into named variables that you can then assert on.
But overall I wanted something tiny that you could easily include alongside a bash script instead of establishing as a new dependency you'd have to install separately. Honestly, I feel that functions like assert_equal, assert_not_equal, assert_success, assert_failure, assert_match and assert_no_match (which all raise on failure) should be built into every programming language's standard library.
I know that you can sort of get by just using `set -e` and just do a comparison operation, but...
Example : help read
printf '%s\n' "$(<$tmpFile)"
Why not just cat $tmpFile
printf '\n'