Command Line Tool
Shiny.Net.HttpServer.CommandLine is a WebDAV mount with a command around it:
point it at a directory and it is served over HTTP. Open the address in a browser and you get a
file manager — browse, upload, rename, delete; open the same address in Finder,
Explorer or the GNOME and KDE file managers and the directory mounts as a drive. It installs as a
.NET tool and runs anywhere .NET runs — macOS, Linux and Windows.
dotnet tool install -g Shiny.Net.HttpServer.CommandLineshinyhttpserver Directory /Users/ada/projects/notes URL http://localhost:8080/ URL http://192.168.1.24:8080/ Operations read Mount WebDAV - Finder, Explorer and any WebDAV client can open the URL as a drive Auth none
█▀▀▀▀▀█ █▀▀▄▀▄ █▀▀▀▀▀█ █ ███ █ █▀██▄▄ ▀ █ ███ █ █ ▀▀▀ █ █ ▄ █▄▄ █ ▀▀▀ █ ▀▀▀▀▀▀▀ █▄▀ ▀ ▀▄▀ ▀▀▀▀▀▀▀ ▀▄▀███▀▄ █▄██▄▀▀▄▀▀▀▀▀▄ ▀ ▄▀▄▀▄█▄▀▀▀▀ ▀▄ ▀▀▄▀ ▀█ ▄▀▄▀ ▄▀▀▀▄ ▀█▄ ▀▀▀▄▀█▀ █ ▄█▀▄▀ █▀ █ ▀ ▀ █ ▀█ ▀ ▀▀ ▀ ▄ ▀▀█▀▄ █▀▀▀█▄█ █▀▀▀▀▀█ ▄▄▄▄ ▄█ ▀ ██▀██ █ ███ █ █▄▄███▄██▀███▀█▄▄ █ ▀▀▀ █ ▀ ▀ █ ▄▀▄▄▄▄▀ █ ▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀▀▀▀▀▀
Scan http://192.168.1.24:8080/
Ctrl+C to stopReads are allowed, nothing else is, and there is no authentication until you ask for one. Those are the same defaults the mount itself has, for the same reasons.
The tool listens on every interface, because a directory only this machine can reach is not what it is for, and the QR code carries the address another device can actually open. Point a phone at it and the file manager is on the phone.
shinyhttpserver [<path>] [options]| Option | Default | Notes |
|---|---|---|
<path> |
. |
The directory to serve. Nothing outside it is reachable |
-p, --port <port> |
8080 |
|
-a, --address <address> |
0.0.0.0 |
An IP, any for every interface, or localhost to keep it on this machine |
--prefix <prefix> |
/ |
Where the directory is mounted; anywhere else, / redirects to it |
-m, --allow <ops> |
read |
read, create, update, delete, all — repeatable or comma separated |
-u, --user <user:password> |
none | Turns on Basic authentication. Repeat for more than one user |
--realm <realm> |
shinyhttpserver |
The realm in the browser’s login prompt |
--auth-changes-only |
off | Leaves reads open, requires a login to change anything |
--allow-insecure-auth |
off | Allows Basic over unencrypted, non-loopback connections |
--https |
off | Serves over TLS with a self-signed certificate generated at startup |
--tunnel |
off | Opens a public pinggy.io tunnel and shares that address instead of the LAN one |
--tunnel-token <token> |
none | A pinggy.io access token, which lifts the 60 minute cap. Implies --tunnel |
--hidden |
off | Includes dotfiles in listings and downloads |
--max-upload <size> |
64mb |
500k, 64mb, 2gb, or a plain byte count |
--no-qr |
off | Leaves the QR code out of the banner |
-v, --verbose |
off | Logs every request |
The file manager
Section titled “The file manager”The address opens in a browser as a file manager, and it offers exactly what -m allows:
| Needs | |
|---|---|
| Browse, with sizes and modification times | always |
| Download a file, on a button beside it | always |
| Upload, by drag and drop or from the file picker — a dropped folder is walked and recreated | create or update |
| New folder | create |
| Rename | update and delete |
| Delete | delete |
The default run has none of those buttons, because the default run allows nothing but reading. A
button that could only ever earn a 403 is worse than no button: it reads as a server that is
broken rather than one that is read-only.
Every action is a WebDAV verb against the same address — an upload is a PUT, a new folder a
MKCOL, a rename a MOVE, a delete a DELETE — so the page can do nothing the tool would not
accept from curl. It is one self-contained response, with no scripts, styles or fonts fetched from
anywhere, which is what makes it work on a phone over a tunnel with no other connectivity at all.
Mounting it as a drive
Section titled “Mounting it as a drive”The same address is a WebDAV mount, so an operating system can open it as a folder with no client to install:
- macOS — Finder, Go ▸ Connect to Server,
http://192.168.1.24:8080/ - Windows — Explorer, Map network drive,
http://192.168.1.24:8080/, ornet use * http://192.168.1.24:8080/ - Linux — GNOME Files or Dolphin,
dav://192.168.1.24:8080/ - anything else —
rclone, Cyberduck, or any WebDAV client library
Windows and macOS both refuse to mount a WebDAV server read-write over plain HTTP with credentials,
and Windows wants TLS for Basic at all, so pair a mounted drive with --https or
--tunnel.
The QR code
Section titled “The QR code”The banner ends with a scannable QR code of the first address on the machine that another device can
reach — the LAN address, never localhost, because a phone cannot go there. The same URL is printed
underneath it in full, for anyone who would rather type it or paste it somewhere.
With --tunnel the code carries the public tunnel address instead. It is the better
one to scan by some distance: it opens on a phone that is not on this network at all, which the LAN
address does not.
It is drawn in half-block characters, two module rows to a text row, so an ordinary code fits a terminal window without wrapping. It is always drawn black on white whatever the terminal’s own colours are, since a reader needs dark modules on a light field and a dark theme would otherwise hand it the negative.
The code is left out when there is nothing useful to put in it or nowhere useful to draw it:
--no-qr-a localhost, or any loopback address — the server is only listening to this machine, unless--tunnelgave it a public address anyway- no non-loopback interface is up
- output is redirected to a file or a pipe, or the window is narrower than the code
--https changes nothing about the code beyond the scheme, but the certificate is self-signed, so
the phone will warn about it before it shows the listing.
The tunnel
Section titled “The tunnel”--tunnel opens a quick tunnel through pinggy.io and shares the public HTTPS
address it hands back:
shinyhttpserver --tunnel Directory /Users/ada/projects/notes URL http://localhost:8080/ URL http://192.168.1.24:8080/ Tunnel https://gyjjf-24-6-13-90.free.pinggy.net/ Operations read Auth none
! The tunnel is public: anyone holding the address can reach this directory, and the traffic passes through pinggy.io. An anonymous tunnel stops after 60 minutes.Nothing is installed and nothing is signed up for — the tool generates a throwaway key at startup, and the address is read back off the tunnel’s own session. The QR code carries the tunnel address, so a phone on cellular data opens the listing the same way one on the wifi does.
It is the same QuickTunnel the library exposes, which means the tunnel hands connections straight
to HttpServer.ServeAsync rather than dialling the local port. Two things follow from that:
--tunnel -a localhostis a real combination. Nothing is bound on the LAN; the directory is reachable through the tunnel and nowhere else.- A tunnelled connection counts as encrypted transport, so
-uworks over it without--allow-insecure-auth— the hop from the browser topinggy.iois TLS and the hop from there to this machine is the SSH connection.
shinyhttpserver -a localhost --tunnel -u ada:s3cret -m allAn anonymous tunnel stops after 60 minutes. --tunnel-token <token> passes a
pinggy access token instead, which lifts that, and implies --tunnel.
If the tunnel will not open — pinggy unreachable, the SSH connection refused — the tool says so and keeps serving on the local network, with the QR code falling back to the LAN address.
Operations
Section titled “Operations”read is always on. The rest map onto what the mount will accept:
| Operation | Allows |
|---|---|
read |
GET a file for its bytes, GET a collection for the file manager, PROPFIND, OPTIONS |
create |
PUT to a path that does not exist, MKCOL, and a COPY or MOVE onto a free name |
update |
PUT over a file that already exists, and a COPY or MOVE onto a name that is taken |
delete |
DELETE, and the source half of a MOVE |
shinyhttpserver # read-only, the defaultshinyhttpserver -m create # accept new files, never overwrite oneshinyhttpserver -m all # read, create, update, deleteThe mount has a single AllowWrite, so create and update are the same PUT to it — and the same
MKCOL, COPY and MOVE. The tool tells them apart itself: a write is checked against the file
system before the handler runs and refused with 403 when the operation it turns out to be was not
the one granted — so -m create cannot be talked into replacing a file, and -m update cannot be
talked into adding one. MOVE is judged by where it lands, since that is the end of it that writes.
Renaming needs update and delete: a MOVE writes at the destination and removes the source,
and the tool will not grant half of it.
Authentication
Section titled “Authentication”--user turns on Basic authentication and puts the whole mount
behind it:
shinyhttpserver -m all -u ada:s3cret -u grace:h0pper--auth-changes-only is RequireAuthorizationForChanges from the command line — reads stay open,
writes and deletes need the login:
shinyhttpserver -m all -u ada:s3cret --auth-changes-onlyBasic sends the password on every request, so on a non-loopback address over plain HTTP the tool
refuses to start rather than doing it quietly — and since it listens on every interface by default,
that is what -u on its own does. Serve it over TLS instead:
shinyhttpserver -a any --https -u ada:s3cret -m allThe certificate is generated at startup and self-signed, so clients will warn about it — curl -k,
or accept it once in the browser. --tunnel -a localhost is the other way out, and
the one with a certificate a phone will accept: nothing is bound on the LAN and every hop to the
tunnel is encrypted. --allow-insecure-auth sends the password in the clear anyway, on a network you
already trust.
Driving it
Section titled “Driving it”It is a WebDAV mount, so anything that speaks HTTP works — and PUT, DELETE
and GET need nothing WebDAV-specific at all:
curl -O http://localhost:8080/notes.md # download a filecurl -X PUT --data-binary @notes.md http://localhost:8080/notes.mdcurl -X DELETE http://localhost:8080/notes.mdcurl -X MKCOL http://localhost:8080/archivecurl -X MOVE -H 'Destination: /archive/notes.md' -H 'Overwrite: F' \ http://localhost:8080/notes.mdA machine-readable listing is PROPFIND, which answers 207 with a DAV:multistatus document:
curl -X PROPFIND -H 'Depth: 1' http://localhost:8080/A browser GET on a directory gets the file manager instead, since a person is
the one asking. Downloads go through the same code as every other
download, so byte ranges and conditional GETs work.


