Skip to content
jtrotskyPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

tūī exhibit

A single-page interactive: one real tūī recording shown four ways at once — longitudinal air wave, voltage-over-time signal (both channels), digital samples, and frequency spectrum. Pure static: HTML + inline JS/CSS, with a 12-second tūī clip embedded as a base64 data URI. No backend, no database, no user input, no outbound calls from the page.

public/index.html                     the whole app (self-contained)
public/favicon.svg                    tūī mark (inline SVG, no raster fallback)
main.go  go.mod                        optional Go static server (embeds public/)
deploy/cloudflared.config.yml.example  Cloudflare Tunnel ingress

No third-party origins: fonts are system stacks, the audio is inlined, and the CSP is default-src 'none' with connect-src 'none'. The page makes no network requests of any kind once served.

Fastest public launch (no server)

It's one static file, so host it as static content:

  • Cloudflare Pages — new project → upload public/ → live on HTTPS. Free, nothing to patch, no tunnel. This is the cheapest way to meet "public, behind Cloudflare".
  • Any static host (Netlify, S3+CloudFront, a jail already running nginx) works the same way — just serve public/.

Skip the Go binary and the tunnel entirely if you go this route.

Self-hosted from your infra (Go binary + Cloudflare Tunnel)

Use this if you want it served from your own box rather than Cloudflare's edge.

1. Build

cd tui-exhibit
go build -o tui-exhibit          # single binary, assets baked in
./tui-exhibit -addr 127.0.0.1:8080

Cross-compiling from another machine to a FreeBSD host:

GOOS=freebsd GOARCH=amd64 go build -o tui-exhibit

Bind to loopback (default) when a tunnel fronts it — nothing needs a public port. In a Bastille jail, either bind the jail's address and point the tunnel at it, or run the binary and cloudflared in the same jail on 127.0.0.1.

Run it under your service manager. A minimal rc.d-style supervisor, daemon(8), or a tiny service wrapper is enough; it's a long-running foreground process.

2. Put Cloudflare in front (Tunnel)

Install cloudflared (FreeBSD: pkg install cloudflared, or grab a release binary). Then:

cloudflared tunnel login
cloudflared tunnel create tui-exhibit
cloudflared tunnel route dns tui-exhibit tui.example.com
cp deploy/cloudflared.config.yml.example ~/.cloudflared/config.yml
# edit: set the tunnel UUID, credentials path, and your hostname
cloudflared tunnel run tui-exhibit

The tunnel gives you public HTTPS with no inbound ports opened. Cloudflare handles TLS and edge compression (the page is ~230 KB because the audio is inlined; Brotli at the edge cuts it hard).

Optional: in the Cloudflare dashboard turn on Always Use HTTPS, and if you ever want to gate it, Cloudflare Access puts a login in front without any code change.

Alternative: Caddy (direct exposure, no tunnel)

If you'd rather expose a port and let Caddy do auto-TLS:

tui.example.com {
	root * /path/to/public
	file_server
	header {
		Content-Security-Policy "default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; media-src 'self' data:; connect-src 'none'; base-uri 'none'; frame-ancestors 'none'"
		X-Content-Type-Options nosniff
		Referrer-Policy no-referrer
		Permissions-Policy "geolocation=(), camera=(), microphone=()"
	}
}

Security notes

  • No backend and no user input, so the attack surface is the static asset itself. connect-src 'none' in the CSP means the page can't make network requests even if modified.
  • GET and HEAD only. http.FileServer answers 200 to any method, including POST, DELETE and TRACE. The secure middleware rejects everything but GET and HEAD with a 405, so a public endpoint never echoes a body to TRACE or appears to accept a write.
  • Request limits are set for public exposure: ReadTimeout 15s, ReadHeaderTimeout 5s, WriteTimeout 30s, MaxHeaderBytes 64 KiB.
  • The inline <script>/<style> require 'unsafe-inline' in the CSP. That's acceptable here because there is no injection surface; don't copy this CSP to a page that takes user input.
  • Permissions-Policy denies geolocation, camera and microphone — the app uses none of them.

Before it's public

  1. Fonts — done. The page originally pulled Fraunces/Inter from Google Fonts, which sent every visitor's IP to Google before a byte rendered. It now uses system font stacks (--serif/--sans/--mono in :root), so there are no third-party origins and the CSP is tightened to style-src 'self' 'unsafe-inline' and font-src 'self'.

  2. Recording licence. The embedded clip is xeno-canto XC378280 by Matthias Feuersenger, CC BY-NC-SA 4.0. Public display is fine as long as use stays non-commercial, the attribution stays on the page (it's in the footer), and any redistribution keeps the same licence. Swapping in your own recording removes the constraint.

Swapping the clip

Replace the base64 data URI. From a new mp3:

B64=$(base64 -w0 your-clip.mp3)     # macOS/BSD: base64 -i your-clip.mp3 | tr -d '\n'
# then replace the value of `var audioB64="..."` in public/index.html

Keep it short (10–15 s) so the page stays light and loops cleanly.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages