Files
photog/main.go
T
puttaalu 434d2437bf Move upload auth from nginx into the app
Add HTTP Basic auth on /upload, reading an Apache htpasswd file (bcrypt
only, `htpasswd -B`) via a new -htpasswd flag. This lets nginx drop to a
plain TLS-terminating reverse proxy with no auth of its own; the existing
bcrypt htpasswd file is reused unchanged.

auth.go:
- LoadHtpasswd parses user:hash lines, keeps bcrypt entries ($2a/$2b/$2y,
  so Apache's $2y$ works), skips MD5/SHA/crypt with a warning.
- Unknown users still pay for one bcrypt comparison against a dummy hash,
  so response timing can't enumerate valid usernames.
- Failed attempts draw from a token-bucket limiter (x/time/rate, ~1/s,
  burst 10) returning 429 once drained; successful and bulk uploads never
  touch it.

server.go guards both /upload routes (nil authenticator = passthrough for
local --serve). /health stays open. Empty -htpasswd disables auth and logs
a warning.

module.nix: new top-level services.photogallery.htpasswdFile, delivered to
the service via systemd LoadCredential (so a root-owned 0400 secret works).
Removed nginx.htpasswdFile and basic_auth from the vhost.

Both uploader apps already send Basic credentials, so no client change.

Deps: golang.org/x/crypto/bcrypt, golang.org/x/time/rate (vendorHash bump).
2026-10-18 14:45:33 +02:00

59 lines
2.6 KiB
Go

package main
import (
"flag"
"log"
"os"
)
func main() {
contentDir := flag.String("content", "./content", "directory containing images and .toml sidecars")
outputDir := flag.String("output", "./public", "directory for generated site output")
baseURL := flag.String("base-url", "http://localhost:8080", "public base URL, no trailing slash")
port := flag.String("port", "8080", "port to listen on")
serve := flag.Bool("serve", false, "serve the generated site on the same port (for local development)")
author := flag.String("author", "Ashik Salahudeen", "author name shown on the profile header, photo pages and in the copyright footer")
handle := flag.String("handle", "@puttaalu@inflo.ws", "handle line shown under the author name on the profile header")
bio := flag.String("bio", "Personal photo journal.", "primary bio line shown on the profile header")
bioAlt := flag.String("bio-alt", "എന്തിനോ വേണ്ടി തിളയ്ക്കുന്ന സാമ്പാർ", "secondary bio line rendered on a new line in the alt-script font")
fontsURL := flag.String("fonts-url", "https://fonts.googleapis.com/css2?family=Crimson+Pro:ital,wght@0,400;0,600;0,700;1,400&family=Manjari:wght@100;400;700&display=swap", "stylesheet URL loading the two web fonts")
serifFamily := flag.String("serif-family", "Crimson Pro", "CSS font-family for the primary serif text")
mlFamily := flag.String("ml-family", "Manjari", "CSS font-family for the alt-script (Malayalam) bio line")
pageSize := flag.Int("page-size", 60, "photos per page on the timeline and grid views (timeline packs whole days, so pages vary slightly around this)")
htpasswd := flag.String("htpasswd", "", "path to an htpasswd file (bcrypt, `htpasswd -B`) guarding /upload; empty disables auth")
flag.Parse()
for _, d := range []string{*contentDir, *outputDir} {
if err := os.MkdirAll(d, 0755); err != nil {
log.Fatalf("mkdir %s: %v", d, err)
}
}
g := NewGenerator(*contentDir, *outputDir, *baseURL, *author, *handle, *bio, *bioAlt, *fontsURL, *serifFamily, *mlFamily, *pageSize)
if err := g.Build(); err != nil {
log.Printf("initial build: %v", err)
}
w, err := NewWatcher(*contentDir, g)
if err != nil {
log.Fatalf("watcher: %v", err)
}
defer w.Close()
go w.Watch()
var auth *Authenticator
if *htpasswd != "" {
auth, err = LoadHtpasswd(*htpasswd)
if err != nil {
log.Fatalf("htpasswd: %v", err)
}
log.Printf("upload auth: enabled (%s)", *htpasswd)
} else {
log.Print("upload auth: DISABLED (no -htpasswd); /upload is open")
}
s := NewServer(*port, *contentDir, *outputDir, *serve, g, auth)
log.Fatal(s.Listen())
}