add hackyplayer/hacky-vouchproxy/emfminiserv bits

This commit is contained in:
Luke Granger-Brown 2024-06-21 22:34:53 +01:00
parent bcb2f287e1
commit 93d5a104da
8 changed files with 624 additions and 1 deletions

View file

@ -15,6 +15,7 @@ args: {
access = import ./access args;
vault = import ./vault args;
tumblrandom = import ./tumblrandom args;
emfminiserv = import ./emfminiserv args;
buildgo2 = import ./buildgo2 args;
}

View file

@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: 2024 Luke Granger-Brown <depot@lukegb.com>
#
# SPDX-License-Identifier: Apache-2.0
{ depot, ... }:
depot.third_party.buildGo.program {
name = "emfminiserv";
srcs = [ ./emfminiserv.go ];
deps = with depot.third_party; [
# gopkgs."github.com".golang.glog
];
}

View file

@ -0,0 +1,216 @@
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()
}

View file

@ -0,0 +1,86 @@
# SPDX-FileCopyrightText: 2024 Luke Granger-Brown <depot@lukegb.com>
#
# SPDX-License-Identifier: Apache-2.0
{ depot, pkgs, lib, config, ... }:
let
emfminiserv = depot.go.emfminiserv;
cfg = config.my.emfminiserv;
in
{
options.my.emfminiserv = {
enable = lib.mkEnableOption "emfminiserv";
hostname = lib.mkOption {
type = lib.types.str;
default = "prerelease.voc.emf.camp";
};
listenAddresses = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
};
serveDir = lib.mkOption {
type = lib.types.str;
default = "/store/emf/2024/video/output/";
};
};
config = lib.mkMerge [
(lib.mkIf cfg.enable {
users.groups.hackyplayer = {};
systemd.services.emfminiserv = {
serviceConfig = {
User = "emfminiserv";
Group = "hackyplayer";
RuntimeDirectory = "emfminiserv";
DynamicUser = true;
ExecStart = "${emfminiserv}/bin/emfminiserv -http_listen_unix /run/emfminiserv/listen.sock -base_dir '${cfg.serveDir}'";
EnvironmentFile = config.my.vault.secrets.emfminiserv-environment.path;
};
wantedBy = [ "multi-user.target" ];
};
my.vault.secrets.emfminiserv-environment = {
reloadOrRestartUnits = ["emfminiserv.service"];
group = "hackyplayer";
template = ''
{{ with secret "kv/apps/emfminiserv" }}
{{ .Data.data.environment }}
{{ end }}
'';
};
environment.systemPackages = [
(pkgs.writeShellApplication {
name = "emfminiserv";
text = ''
read -ra vars < <(xargs <"${config.my.vault.secrets.emfminiserv-environment.path}")
export "''${vars[@]}"
exec "${emfminiserv}/bin/emfminiserv" -base_dir '${cfg.serveDir}' "$@"
'';
})
];
services.caddy = {
enable = true;
virtualHosts."${cfg.hostname}" = {
listenAddresses = lib.mkIf (cfg.listenAddresses != null) cfg.listenAddresses;
extraConfig = ''
reverse_proxy unix//run/emfminiserv/listen.sock {
@accel header X-Accel-Redir *
handle_response @accel {
root * ${cfg.serveDir}
rewrite * {rp.header.X-Accel-Redir}
method * GET
file_server
}
}
'';
};
};
systemd.services.caddy.serviceConfig.SupplementaryGroups = lib.mkAfter [ "hackyplayer" ];
})
];
}

View file

@ -0,0 +1,271 @@
# SPDX-FileCopyrightText: 2024 Luke Granger-Brown <depot@lukegb.com>
#
# SPDX-License-Identifier: Apache-2.0
{ depot, pkgs, lib, config, ... }:
let
hackyplayerSrc = /home/lukegb/Projects/hackyplayer;
hackyplayer = import hackyplayerSrc { inherit pkgs; };
yamlFormat = pkgs.formats.yaml {};
cfg = config.my.hackyplayer;
processedConfig = lib.mapAttrs' (name: value: lib.nameValuePair "FLASK_${name}" (builtins.toJSON value)) cfg.config;
readWritePaths = [
cfg.config.VIDEO_OUTPUT
cfg.config.VIDEO_TEMP
cfg.config.LOG_DIR
]
++ (map (value: value.OUTPUT_DIR) cfg.config.WATCHFOLDERS)
++ (map (value: value.FULLPATH) cfg.config.WATCHFOLDERS);
in
{
options.my.hackyplayer = {
enable = lib.mkEnableOption "hackyplayer";
hostname = lib.mkOption {
type = lib.types.str;
};
listenAddresses = lib.mkOption {
type = lib.types.nullOr (lib.types.listOf lib.types.str);
default = null;
};
config = lib.mkOption {
type = lib.types.anything;
default = let
paths = {
input = "/store/emf/2024/video/input";
source = "/store/emf/2024/video/source";
output = "/store/emf/2024/video/output";
temp = "/store/emf/2024/video/tmp";
logs = "/store/emf/2024/video/logs";
};
in {
VIDEO_SOURCES = [{
DISKDIR = paths.source;
WEBDIR = "source";
EXT = [".mp4"];
NAME = "Source";
} {
DISKDIR = paths.output;
WEBDIR = "output";
EXT = [".mp4"];
NAME = "Output";
}];
VIDEO_OUTPUT = paths.output;
VIDEO_TEMP = paths.temp;
WATCHFOLDERS = [{
NAME = "input";
FULLPATH = paths.input;
OUTPUT_DIR = paths.source;
}];
LOG_DIR = paths.logs;
};
};
caddyPrefixExtraHandles = lib.mkOption {
type = lib.types.lines;
default = "";
};
caddyPrefixExtraConfig = lib.mkOption {
type = lib.types.lines;
default = "";
};
gunicornConcurrency = lib.mkOption {
type = lib.types.int;
default = 4;
};
workerConcurrency = lib.mkOption {
type = lib.types.int;
default = 4;
};
vouch = {
enable = lib.mkEnableOption "hackyplayer-vouch";
config = lib.mkOption {
type = lib.types.submodule {
freeformType = yamlFormat.type;
};
default = {};
};
};
};
config = lib.mkMerge [
(lib.mkIf cfg.enable {
users.users.hackyplayer = {
isSystemUser = true;
group = "hackyplayer";
};
users.groups.hackyplayer = {};
systemd.services.hackyplayer = {
serviceConfig = {
User = "hackyplayer";
ProtectSystem = "strict";
ProtectHome = "read-only";
PrivateTmp = true;
RemoveIPC = true;
RuntimeDirectory = "hackyplayer";
ExecStart = "${hackyplayer}/bin/gunicorn -w ${toString cfg.gunicornConcurrency} --bind unix:/run/hackyplayer/hackyplayer.sock hackyplayer.app:app";
EnvironmentFile = config.my.vault.secrets.hackyplayer-environment.path;
};
wantedBy = [ "hackyplayer.target" ];
environment = processedConfig;
};
systemd.services.hackyplayer-celery = {
serviceConfig = {
User = "hackyplayer";
ProtectSystem = "strict";
ProtectHome = "read-only";
PrivateTmp = true;
RemoveIPC = true;
RuntimeDirectory = "hackyplayer-celery";
ExecStart = "${hackyplayer}/bin/celery -A hackyplayer.tasks worker --concurrency ${toString cfg.workerConcurrency} --loglevel INFO --statedb /run/hackyplayer-celery/state.db";
ReadWritePaths = readWritePaths;
EnvironmentFile = config.my.vault.secrets.hackyplayer-environment.path;
};
wantedBy = [ "hackyplayer.target" ];
environment = processedConfig;
};
systemd.targets.hackyplayer = {
wantedBy = [ "multi-user.target" ];
};
my.vault.secrets.hackyplayer-environment = {
reloadOrRestartUnits = ["hackyplayer.service"];
group = "root";
template = ''
{{ with secret "kv/apps/hackyplayer" }}
{{ .Data.data.environment }}
{{ end }}
'';
};
environment.systemPackages = let
envPrelude = lib.concatStringsSep "\n" (
lib.mapAttrsToList (var: val: "export ${var}=${lib.escapeShellArg val}") processedConfig);
in [
(pkgs.writeShellApplication {
name = "hackyplayer-celery";
text = ''
${envPrelude}
exec ${hackyplayer}/bin/celery -A hackyplayer.tasks "$@"
'';
})
(pkgs.writeShellApplication {
name = "hackyplayer-flask";
text = ''
${envPrelude}
exec ${hackyplayer}/bin/flask -A hackyplayer.app:app "$@"
'';
})
];
services.caddy = {
enable = true;
virtualHosts."${cfg.hostname}" = {
listenAddresses = lib.mkIf (cfg.listenAddresses != null) cfg.listenAddresses;
extraConfig = ''
${cfg.caddyPrefixExtraHandles}
handle {
${cfg.caddyPrefixExtraConfig}
${lib.concatMapStrings (src: ''
route /static/video/${src.WEBDIR}/* {
uri strip_prefix /static/video/${src.WEBDIR}
root * ${src.DISKDIR}
file_server
}
'') cfg.config.VIDEO_SOURCES}
route /static/* {
uri strip_prefix /static
root * ${hackyplayer}/${hackyplayer.python.sitePackages}/hackyplayer/static
file_server
}
reverse_proxy unix//run/hackyplayer/hackyplayer.sock
}
'';
};
};
systemd.services.caddy.serviceConfig.SupplementaryGroups = lib.mkAfter [ "hackyplayer" ];
services.redis.servers."".enable = true;
my.hackyplayer.vouch.enable = lib.mkDefault true;
my.hackyplayer.vouch.config = lib.mkDefault {
vouch = {
allowAllUsers = true;
cookie.domain = "voc.emf.camp";
listen = "unix:/run/hacky-vouchproxy/sock";
socket_mode = "0770";
socket_group = "hackyplayer";
document_root = "/_vouch";
};
oauth = let
base = "https://identity.emfcamp.org";
in {
provider = "oidc";
auth_url = "${base}/oauth2/authorize";
token_url = "${base}/oauth2/token";
user_info_url = "${base}/oauth2/userinfo";
scopes = [ "profile" ];
callback_url = "https://hackyplayer.voc.emf.camp/auth";
};
};
}) (lib.mkIf cfg.vouch.enable {
environment.etc."hacky-vouchproxy/config.yaml".source = yamlFormat.generate "hackyplayer-vouch-config.yaml" cfg.vouch.config;
systemd.services.hacky-vouchproxy = {
serviceConfig = {
User = "hacky-vouchproxy";
SupplementaryGroups = [ "hackyplayer" ];
DynamicUser = true;
RuntimeDirectory = "hacky-vouchproxy";
ExecStart = "${pkgs.vouch-proxy}/bin/vouch-proxy -config /etc/hacky-vouchproxy/config.yaml";
EnvironmentFile = config.my.vault.secrets.hacky-vouchproxy-environment.path;
};
wantedBy = [ "multi-user.target" ];
};
my.vault.secrets.hacky-vouchproxy-environment = {
reloadOrRestartUnits = ["hacky-vouchproxy.service"];
group = "root";
template = ''
{{ with secret "kv/apps/hacky-vouchproxy" }}
{{ .Data.data.environment }}
{{ end }}
'';
};
my.hackyplayer.caddyPrefixExtraHandles = lib.mkBefore ''
handle /auth {
reverse_proxy unix//run/hacky-vouchproxy/sock {
rewrite /_vouch/auth
}
}
handle /_vouch/* {
reverse_proxy unix//run/hacky-vouchproxy/sock
}
'';
my.hackyplayer.caddyPrefixExtraConfig = lib.mkBefore ''
reverse_proxy unix//run/hacky-vouchproxy/sock {
method GET
rewrite /_vouch/validate
header_up Host {host}
@good status 2xx
handle_response @good {
request_header X-Vouch-User {rp.header.X-Vouch-User}
}
@bad status 4xx
handle_response @bad {
header Location /_vouch/login?url=https://{host}{uri}&vouch-failcount={rp.header.X-Vouch-Failcount}&X-Vouch-Token={rp.header.X-Vouch-JWT}&error={rp.header.X-Vouch-Err}
respond 303 {
close
}
}
}
'';
})
];
}

View file

@ -15,6 +15,7 @@ in {
../lib/rexxar-distributed.nix
../lib/quadv-ca/default.nix
((import ../../../third_party/lanzaboote.nix { }).nixosModules.lanzaboote)
../lib/hackyplayer.nix
];
boot = lib.mkMerge [{
@ -299,6 +300,12 @@ in {
enable = true;
};
my.hackyplayer = {
enable = true;
listenAddresses = [ "127.0.0.1" "::1" ];
hostname = "hackyplayer.localhost";
};
# This value determines the NixOS release with which your system is to be
# compatible, in order to avoid breaking some software such as database
# servers. You should change this only after NixOS release notes say you

View file

@ -10,6 +10,8 @@
../lib/bgp.nix
../lib/gitlab-runner-cacher.nix
#../lib/nixbuild-distributed.nix # error: build of '/nix/store/3r7456yr8r9g4fl7w6xbgqlbsdjwfvr4-stdlib-pkgs.json.drv' on 'ssh://eu.nixbuild.net' failed: unexpected: Built outputs are invalid
../lib/hackyplayer.nix
../lib/emfminiserv.nix
];
# Otherwise _this_ machine won't enumerate things properly.
@ -56,6 +58,13 @@
"/store" = zfs "zu2/safe/store";
"/home" = zfs "zu2/safe/home";
"/store/emf" = zfs "zu2/safe/store/emf";
"/store/emf/2024" = zfs "zu2/safe/store/emf/2024";
"/store/emf/2024/video" = zfs "zu2/safe/store/emf/2024/video";
"/store/emf/2024/video/source" = zfs "zu2/safe/store/emf/2024/video/source";
"/store/emf/2024/video/input" = zfs "zu2/safe/store/emf/2024/video/input";
"/store/emf/2024/video/output" = zfs "zu2/safe/store/emf/2024/video/output";
"/boot" = {
device = "/dev/disk/by-label/ESP";
fsType = "vfat";
@ -81,6 +90,10 @@
allowedUDPPorts = [
51821 51822 51823
34197 # factorio
443
];
allowedTCPPorts = [
80 443
];
};
};
@ -348,8 +361,10 @@
wg-cofractal-ams01-private = wireguardSecret "privateKeyToCofractalAms01";
};
users.users.lukegb.extraGroups = lib.mkAfter [ "hackyplayer" ];
users.users.samw = {
isNormalUser = true;
extraGroups = lib.mkAfter [ "hackyplayer "];
openssh.authorizedKeys.keys = [
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBDQdGzwYiallvWXIHgSAf2GOwMJKA8bxPmwyuO+vsd1HwB65hMRPCpKS+FNLIpkrADNnuhGS3xGCGSSuQ+zAu/g="
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNLrJyUXmiFWb9vhlTWZLYr64IsKut+c9TGqq3/uwPDeF4X0Qb2jzxqXfQcDSztjR09JHbC8BOqfpYYT9LHahIo="
@ -377,5 +392,17 @@
};
};
my.hackyplayer = {
enable = true;
hostname = "hackyplayer.voc.emf.camp";
workerConcurrency = 13;
};
my.emfminiserv = {
enable = true;
hostname = "prerelease.voc.emf.camp";
};
hardware.rasdaemon.enable = true;
system.stateVersion = "24.05";
}

View file

@ -72,6 +72,9 @@
my.apps.nixbuild = {};
my.apps.tumblrandom = {};
my.apps.netbox = {};
my.apps.hacky-vouchproxy = {};
my.apps.hackyplayer = {};
my.apps.emfminiserv = {};
my.servers.etheroute-lon01.apps = [ "pomerium" ];
my.servers.howl.apps = [ "nixbuild" ];
@ -88,5 +91,5 @@
my.servers.bvm-nixosmgmt.apps = [ "plex-pass" ];
my.servers.blade-tuvok.apps = [ "fup" ];
my.servers.bvm-netbox.apps = [ "netbox" ];
my.servers.rexxar.apps = [ "deluge" "gitlab-runner" "nixbuild" ];
my.servers.rexxar.apps = [ "deluge" "gitlab-runner" "nixbuild" "hacky-vouchproxy" "hackyplayer" "emfminiserv" ];
}