217 lines
5.6 KiB
Go
217 lines
5.6 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}[.]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()
|
||
|
}
|