Skip to content
Shiny Controls v1.0 - The Ultra Control Suite for .NET MAUI & BlazorO...M...G!

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.

Terminal window
dotnet tool install -g Shiny.Net.HttpServer.CommandLine
Terminal window
shinyhttpserver
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 stop

Reads 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 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.

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/, or net use * http://192.168.1.24:8080/
  • Linux — GNOME Files or Dolphin, dav://192.168.1.24:8080/
  • anything elserclone, 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 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 --tunnel gave 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.

--tunnel opens a quick tunnel through pinggy.io and shares the public HTTPS address it hands back:

Terminal window
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 localhost is 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 -u works over it without --allow-insecure-auth — the hop from the browser to pinggy.io is TLS and the hop from there to this machine is the SSH connection.
Terminal window
shinyhttpserver -a localhost --tunnel -u ada:s3cret -m all

An 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.

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
Terminal window
shinyhttpserver # read-only, the default
shinyhttpserver -m create # accept new files, never overwrite one
shinyhttpserver -m all # read, create, update, delete

The 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.

--user turns on Basic authentication and puts the whole mount behind it:

Terminal window
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:

Terminal window
shinyhttpserver -m all -u ada:s3cret --auth-changes-only

Basic 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:

Terminal window
shinyhttpserver -a any --https -u ada:s3cret -m all

The 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.

It is a WebDAV mount, so anything that speaks HTTP works — and PUT, DELETE and GET need nothing WebDAV-specific at all:

Terminal window
curl -O http://localhost:8080/notes.md # download a file
curl -X PUT --data-binary @notes.md http://localhost:8080/notes.md
curl -X DELETE http://localhost:8080/notes.md
curl -X MKCOL http://localhost:8080/archive
curl -X MOVE -H 'Destination: /archive/notes.md' -H 'Overwrite: F' \
http://localhost:8080/notes.md

A machine-readable listing is PROPFIND, which answers 207 with a DAV:multistatus document:

Terminal window
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.