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, "") 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() }