Skip to content
Shiny.NET

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

That is the plain banner, which is what --no-tui prints and what a redirected run gets. In a terminal the tool opens a dashboard instead: the same addresses and code, plus every request as it arrives, uploads and downloads with live progress, and a form to change the setup without restarting.

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. In the dashboard, turns the log tab up to debug level
--no-tui off Prints the banner and a plain log instead of opening the dashboard

Run in a terminal, shinyhttpserver opens a full-screen dashboard built on XenoAtom.Terminal.UI. The flags are its starting point, not the last word: everything they set can be changed while it runs.

The dashboard's Overview tab: the directory, its local and LAN addresses, the allowed operations, basic auth for changes only, and a QR code of the LAN address
Tab Key Shows
Overview F1 The addresses, the tunnel, what is allowed and who can log in, the same warnings the banner gives, and the QR code
Requests F2 Every request answered, newest first: method, path, status, bytes in and out, time taken, client and user. Enter opens the full request and response headers; Ctrl+F searches; columns sort
Transfers F3 Uploads and downloads in flight, each with a progress bar, speed and time left, and the last few that finished
Settings F4 Every option on the command line as a form. Ctrl+S applies it
Log F5 The server’s own log, which would otherwise have gone to the console

Ctrl+L clears the request log and its totals, and Ctrl+Q quits. The status bar keeps a running count of requests, what is in flight, and bytes in and out.

The Transfers tab with a download and an upload in flight, each with a progress bar, bytes so far, speed and time left, and the recently finished transfers below

The monitor sits first in the pipeline, ahead of authentication, so a refused login is in the log with everything else — a 401 is exactly what someone watching a server wants to see. Connections through the tunnel are served by the same pipeline, so they are in it too, marked tunnel. For those, the client shown is the last address in the tunnel’s X-Forwarded-For, since the socket itself is the tunnel’s forward. On a direct connection the header is whatever the client chose to send, so it is ignored there.

Byte counts are read as the handler reads the request and writes the response, over HTTP/1.1 and HTTP/2 alike. A download’s progress is what the server has handed to the connection. On a fast link that is what the client has, but a small file on a slow one can read 100% while it is still draining from the socket’s buffer.

The Requests tab: a grid of GET, PUT, PROPFIND, MKCOL and DELETE requests with status, bytes in and out, time taken, client and user, including a 401 for a PUT without a login

Authorization headers are masked in the detail view: the scheme stays, the credential does not. The log keeps the last 1,000 requests.

The request detail dialog for a PUT, showing status, timing, client and user, the request headers with the Authorization value masked, and the response headers

The Settings tab reads its fields through the same parsers as the flags, so it accepts what the command line accepts and refuses it with the same message.

The Settings tab: the directory and prefix, address and port, HTTPS and QR code switches, the tunnel and its token, the create, update and delete operations, the upload limit, and the users

Applying a change does one of two things:

  • The tunnel, its token, the QR code and the log level change in place. Opening or closing the tunnel leaves the server, and every transfer on it, alone. A new token is a new tunnel, on a new address.
  • Anything else — the directory, address, port, prefix, operations, users, realm, TLS, hidden files, the upload limit — is baked into the server’s pipeline, so the server is rebuilt. If anything is in flight the dashboard says how many requests will be cut off and asks first. A rebuild waits up to three seconds for them to finish.

If the new settings will not start — the port is taken, say — the previous ones are started again, and the form goes back to what is running. The self-signed certificate is made once per run, so a browser that has already accepted it is not asked again because the port moved.

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:

Platform Details
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 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 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.