depot/go/emfminiserv/emfminiserv.go

216 lines
5.7 KiB
Go

package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"flag"
"fmt"
"io/fs"
"log"
"net"
"net/http"
"os"
"regexp"
"sort"
"strings"
"sync"
)
var (
baseDir = flag.String("base_dir", "/store/emf/2024/video/output/", "base directory for output")
httpListen = flag.String("http_listen", "", "TCP address to listen&serve")
httpListenUNIX = flag.String("http_listen_unix", "", "UNIX socket path to listen&serve")
computeForBase = flag.String("compute_base", "https://prerelease.voc.emf.camp/", "base URL to prepend when computing a secret")
computeFor = flag.String("compute", "", "something to compute the secret for")
list = flag.Bool("list", false, "list the available content")
devMode = flag.Bool("dev_mode", false, "enable insecure dev mode")
)
func computeSignature(content, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(content))
return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(mac.Sum(nil))
}
func validateSignature(content, secret, signature string) bool {
computed := computeSignature(content, secret)
if *devMode {
log.Printf("got signature %q; computed signature %q", signature, computed)
}
return subtle.ConstantTimeCompare([]byte(computed), []byte(signature)) == 1
}
const (
talkRegexFragment = `[0-9]+(_[\p{L}\p{N}\-]+)?`
fileEndRegexFragment = `[0-9]{8}-[0-9]{6}(-[a-zA-Z0-9][a-zA-Z0-9\-]*)?[.]mp4`
)
var (
validContentRegex = regexp.MustCompile("^" + talkRegexFragment + "$")
validFileRegex = regexp.MustCompile("^" + fileEndRegexFragment + "$")
filenameRegex = regexp.MustCompile(`^(` + talkRegexFragment + ")-(" + fileEndRegexFragment + ")$")
)
func logResultFactory(r *http.Request, what string) func(string, ...any) {
return func(f string, bits ...any) {
log.Printf("%s{%s}: %s %s - %s", what, r.RemoteAddr, r.Method, r.RequestURI, fmt.Sprintf(f, bits...))
}
}
func main() {
flag.Parse()
contentFS := os.DirFS(*baseDir)
secret := os.Getenv("EMFMINISERV_SECRET")
if secret == "" {
if !*devMode {
log.Fatal("set EMFMINISERV_SECRET or -dev_mode")
}
secret = "testing-do-not-use-in-production"
log.Printf("secret is set to %q! development mode only.", secret)
}
if *httpListen == "" && *httpListenUNIX == "" && *computeFor == "" && !*list {
log.Printf("need -http_listen, -http_listen_unix, -compute or -list")
os.Exit(1)
}
if *list {
matches, err := fs.Glob(contentFS, "*.mp4")
if err != nil {
log.Fatalf("globbing for *.mp4: %v", err)
}
available := map[string]bool{}
for _, m := range matches {
bits := filenameRegex.FindStringSubmatch(m)
if bits == nil {
continue
}
available[bits[1]] = true
}
availableKeys := make([]string, 0, len(available))
for k := range available {
availableKeys = append(availableKeys, k)
}
sort.Strings(availableKeys)
for _, k := range availableKeys {
fmt.Printf("%s\n", k)
}
os.Exit(0)
}
if *computeFor != "" {
sig := computeSignature(*computeFor, secret)
fmt.Printf("%s%s/%s/\n", *computeForBase, *computeFor, sig)
os.Exit(0)
}
http.HandleFunc("/{content}/{signature}/{file}", func(rw http.ResponseWriter, r *http.Request) {
logResult := logResultFactory(r, "contentget")
if r.Method != "GET" {
logResult("method not allowed")
http.Error(rw, "GET only", http.StatusMethodNotAllowed)
return
}
log.Printf("contentget{%s}: %s %s", r.RemoteAddr, r.Method, r.RequestURI)
content := r.PathValue("content")
file := r.PathValue("file")
if !validContentRegex.MatchString(content) || !validFileRegex.MatchString(file) {
logResult("404 invalid content or file segment")
http.NotFound(rw, r)
return
}
if !validateSignature(content, secret, r.PathValue("signature")) {
logResult("404 invalid signature")
http.NotFound(rw, r)
return
}
rw.Header().Set("x-accel-redir", fmt.Sprintf("/%s-%s", content, file))
logResult("200 OK")
})
http.HandleFunc("/{content}/{signature}/{$}", func(rw http.ResponseWriter, r *http.Request) {
logResult := logResultFactory(r, "contentdir")
if r.Method != "GET" {
logResult("method not allowed")
http.Error(rw, "GET only", http.StatusMethodNotAllowed)
return
}
content := r.PathValue("content")
if !validContentRegex.MatchString(content) {
logResult("404 invalid content segment")
http.NotFound(rw, r)
return
}
if !validateSignature(content, secret, r.PathValue("signature")) {
logResult("404 invalid signature")
http.NotFound(rw, r)
return
}
matches, err := fs.Glob(contentFS, fmt.Sprintf("%s-*", content))
if err != nil {
logResult("500 globbing with %s-*: %v", content, err)
http.Error(rw, "internal server error finding content", http.StatusInternalServerError)
return
}
sort.Strings(matches)
unprefixed := make([]string, len(matches))
for n, m := range matches {
unprefixed[n] = strings.TrimPrefix(m, fmt.Sprintf("%s-", content))
}
rw.Header().Set("Content-type", "text/html; encoding=utf-8")
fmt.Fprintf(rw, "<ul>")
for _, m := range unprefixed {
fmt.Fprintf(rw, `<li><a href="%s">%s</a></li>`, m, m)
}
fmt.Fprintf(rw, "</ul>")
logResult("200 OK")
})
var wg sync.WaitGroup
if *httpListen != "" {
wg.Add(1)
go func() {
defer wg.Done()
log.Fatal(http.ListenAndServe(*httpListen, nil))
}()
}
if *httpListenUNIX != "" {
wg.Add(1)
go func() {
defer wg.Done()
l, err := net.Listen("unix", *httpListenUNIX)
if err != nil {
log.Fatalf("listening on unix:%v: %v", *httpListenUNIX, err)
}
defer l.Close()
if err := os.Chmod(*httpListenUNIX, 0770); err != nil {
log.Fatalf("chmodding unix:%v: %v", *httpListenUNIX, err)
}
log.Fatal(http.Serve(l, nil))
}()
}
wg.Wait()
}