Live video on the AT Protocol
at eli/fix-gitlab 371 lines 9.7 kB view raw
1package config 2 3import ( 4 "crypto/rsa" 5 "crypto/x509" 6 "encoding/pem" 7 "errors" 8 "flag" 9 "fmt" 10 "io" 11 "net" 12 "os" 13 "path/filepath" 14 "runtime" 15 "strconv" 16 "strings" 17 "time" 18 19 "github.com/lestrrat-go/jwx/v2/jwk" 20 "github.com/peterbourgon/ff/v3" 21 "golang.org/x/exp/rand" 22 "stream.place/streamplace/pkg/aqtime" 23 "stream.place/streamplace/pkg/constants" 24 "stream.place/streamplace/pkg/crypto/aqpub" 25) 26 27const SPDataDir = "$SP_DATA_DIR" 28const SegmentsDir = "segments" 29 30type BuildFlags struct { 31 Version string 32 BuildTime int64 33 UUID string 34} 35 36func (b BuildFlags) BuildTimeStr() string { 37 ts := time.Unix(b.BuildTime, 0) 38 return ts.UTC().Format(time.RFC3339) 39} 40 41func (b BuildFlags) BuildTimeStrExpo() string { 42 ts := time.Unix(b.BuildTime, 0) 43 return ts.UTC().Format("2006-01-02T15:04:05.000Z") 44} 45 46type CLI struct { 47 AdminAccount string 48 Build *BuildFlags 49 DataDir string 50 DBPath string 51 EthAccountAddr string 52 EthKeystorePath string 53 EthPassword string 54 FirebaseServiceAccount string 55 GitLabURL string 56 HTTPAddr string 57 HTTPInternalAddr string 58 HTTPSAddr string 59 RtmpsAddr string 60 Secure bool 61 NoMist bool 62 MistAdminPort int 63 MistHTTPPort int 64 MistRTMPPort int 65 SigningKeyPath string 66 TAURL string 67 TLSCertPath string 68 TLSKeyPath string 69 PKCS11ModulePath string 70 PKCS11Pin string 71 PKCS11TokenSlot string 72 PKCS11TokenLabel string 73 PKCS11TokenSerial string 74 PKCS11KeypairLabel string 75 PKCS11KeypairID string 76 StreamerName string 77 RelayHost string 78 Debug map[string]map[string]int 79 AllowedStreams []string 80 WideOpen bool 81 Peers []string 82 Redirects []string 83 TestStream bool 84 FrontendProxy string 85 AppBundleID string 86 NoFirehose bool 87 PrintChat bool 88 Color string 89 LivepeerGatewayURL string 90 WHIPTest string 91 Thumbnail bool 92 SmearAudio bool 93 ExternalSigning bool 94 RTMPServerAddon string 95 TracingEndpoint string 96 PublicHost string 97 RateLimitPerSecond int 98 RateLimitBurst int 99 RateLimitWebsocket int 100 JWK jwk.Key 101 AccessJWK jwk.Key 102 dataDirFlags []*string 103} 104 105var StreamplaceSchemePrefix = "streamplace://" 106 107func (cli *CLI) OwnInternalURL() string { 108 // No errors because we know it's valid from AddrFlag 109 host, port, _ := net.SplitHostPort(cli.HTTPInternalAddr) 110 ip := net.ParseIP(host) 111 if ip.IsUnspecified() { 112 host = "127.0.0.1" 113 } 114 addr := net.JoinHostPort(host, port) 115 return fmt.Sprintf("http://%s", addr) 116} 117 118func (cli *CLI) ParseSigningKey() (*rsa.PrivateKey, error) { 119 bs, err := os.ReadFile(cli.SigningKeyPath) 120 if err != nil { 121 return nil, err 122 } 123 block, _ := pem.Decode(bs) 124 if block == nil { 125 return nil, fmt.Errorf("no RSA key found in signing key") 126 } 127 key, err := x509.ParsePKCS1PrivateKey(block.Bytes) 128 if err != nil { 129 return nil, err 130 } 131 return key, nil 132} 133 134func RandomTrailer(length int) string { 135 const charset = "abcdefghijklmnopqrstuvwxyz0123456789" 136 137 res := make([]byte, length) 138 for i := 0; i < length; i++ { 139 res[i] = charset[rand.Intn(len(charset))] 140 } 141 return string(res) 142} 143 144func DefaultDataDir() string { 145 home, err := os.UserHomeDir() 146 if err != nil { 147 // not fatal unless the user doesn't set one later 148 return "" 149 } 150 return filepath.Join(home, ".streamplace") 151} 152 153func (cli *CLI) Parse(fs *flag.FlagSet, args []string) error { 154 err := ff.Parse( 155 fs, os.Args[1:], 156 ff.WithEnvVarPrefix("SP"), 157 ) 158 if err != nil { 159 return err 160 } 161 if cli.DataDir == "" { 162 return fmt.Errorf("could not determine default data dir (no $HOME) and none provided, please set --data-dir") 163 } 164 for _, dest := range cli.dataDirFlags { 165 *dest = strings.Replace(*dest, SPDataDir, cli.DataDir, 1) 166 } 167 return nil 168} 169 170func (cli *CLI) DataFilePath(fpath []string) string { 171 if cli.DataDir == "" { 172 panic("no data dir configured") 173 } 174 // windows does not like colons 175 safe := []string{} 176 for _, p := range fpath { 177 safe = append(safe, strings.ReplaceAll(p, ":", "-")) 178 } 179 fpath = append([]string{cli.DataDir}, safe...) 180 fdpath := filepath.Join(fpath...) 181 return fdpath 182} 183 184// does a file exist in our data dir? 185func (cli *CLI) DataFileExists(fpath []string) (bool, error) { 186 ddpath := cli.DataFilePath(fpath) 187 _, err := os.Stat(ddpath) 188 if err == nil { 189 return true, nil 190 } 191 if errors.Is(err, os.ErrNotExist) { 192 return false, nil 193 } 194 return false, err 195} 196 197// write a file to our data dir 198func (cli *CLI) DataFileWrite(fpath []string, r io.Reader, overwrite bool) error { 199 fd, err := cli.DataFileCreate(fpath, overwrite) 200 if err != nil { 201 return err 202 } 203 defer fd.Close() 204 _, err = io.Copy(fd, r) 205 if err != nil { 206 return err 207 } 208 209 return nil 210} 211 212// create a file in our data dir. don't forget to close it! 213func (cli *CLI) DataFileCreate(fpath []string, overwrite bool) (*os.File, error) { 214 ddpath := cli.DataFilePath(fpath) 215 if !overwrite { 216 exists, err := cli.DataFileExists(fpath) 217 if err != nil { 218 return nil, err 219 } 220 if exists { 221 return nil, fmt.Errorf("refusing to overwrite file that exists: %s", ddpath) 222 } 223 } 224 if len(fpath) > 1 { 225 dirs, _ := filepath.Split(ddpath) 226 err := os.MkdirAll(dirs, os.ModePerm) 227 if err != nil { 228 return nil, fmt.Errorf("error creating subdirectories for %s: %w", ddpath, err) 229 } 230 } 231 return os.Create(ddpath) 232} 233 234// get a path to a segment file in our database 235func (cli *CLI) SegmentFilePath(user string, file string) (string, error) { 236 ext := filepath.Ext(file) 237 base := strings.TrimSuffix(file, ext) 238 aqt, err := aqtime.FromString(base) 239 if err != nil { 240 return "", err 241 } 242 fname := fmt.Sprintf("%s%s", aqt.FileSafeString(), ext) 243 yr, mon, day, hr, min, _, _ := aqt.Parts() 244 return cli.DataFilePath([]string{SegmentsDir, user, yr, mon, day, hr, min, fname}), nil 245} 246 247// get a path to a segment file in our database 248func (cli *CLI) HLSDir(user string) (string, error) { 249 return cli.DataFilePath([]string{SegmentsDir, "hls", user}), nil 250} 251 252// create a segment file in our database 253func (cli *CLI) SegmentFileCreate(user string, aqt aqtime.AQTime, ext string) (*os.File, error) { 254 fname := fmt.Sprintf("%s.%s", aqt.FileSafeString(), ext) 255 yr, mon, day, hr, min, _, _ := aqt.Parts() 256 return cli.DataFileCreate([]string{SegmentsDir, user, yr, mon, day, hr, min, fname}, false) 257} 258 259// read a file from our data dir 260func (cli *CLI) DataFileRead(fpath []string, w io.Writer) error { 261 ddpath := cli.DataFilePath(fpath) 262 263 fd, err := os.Open(ddpath) 264 if err != nil { 265 return err 266 } 267 _, err = io.Copy(w, fd) 268 if err != nil { 269 return err 270 } 271 272 return nil 273} 274 275func (cli *CLI) DataDirFlag(fs *flag.FlagSet, dest *string, name, defaultValue, usage string) { 276 cli.dataDirFlags = append(cli.dataDirFlags, dest) 277 *dest = filepath.Join(SPDataDir, defaultValue) 278 usage = fmt.Sprintf(`%s (default: "%s")`, usage, *dest) 279 fs.Func(name, usage, func(s string) error { 280 *dest = s 281 return nil 282 }) 283} 284 285func (cli *CLI) HasMist() bool { 286 return runtime.GOOS == "linux" 287} 288 289// type for comma-separated ethereum addresses 290func (cli *CLI) AddressSliceFlag(fs *flag.FlagSet, dest *[]aqpub.Pub, name, defaultValue, usage string) { 291 *dest = []aqpub.Pub{} 292 usage = fmt.Sprintf(`%s (default: "%s")`, usage, *dest) 293 fs.Func(name, usage, func(s string) error { 294 if s == "" { 295 return nil 296 } 297 strs := strings.Split(s, ",") 298 for _, str := range strs { 299 pub, err := aqpub.FromHexString(str) 300 if err != nil { 301 return err 302 } 303 *dest = append(*dest, pub) 304 } 305 return nil 306 }) 307} 308 309func (cli *CLI) StringSliceFlag(fs *flag.FlagSet, dest *[]string, name, defaultValue, usage string) { 310 *dest = []string{} 311 usage = fmt.Sprintf(`%s (default: "%s")`, usage, *dest) 312 fs.Func(name, usage, func(s string) error { 313 if s == "" { 314 return nil 315 } 316 strs := strings.Split(s, ",") 317 *dest = append(*dest, strs...) 318 return nil 319 }) 320} 321 322// debug flag for turning func=ToHLS:3,file=gstreamer.go:4 into {"func": {"ToHLS": 3}, "file": {"gstreamer.go": 4}} 323func (cli *CLI) DebugFlag(fs *flag.FlagSet, dest *map[string]map[string]int, name, defaultValue, usage string) { 324 *dest = map[string]map[string]int{} 325 fs.Func(name, usage, func(s string) error { 326 if s == "" { 327 return nil 328 } 329 pairs := strings.Split(s, ",") 330 for _, pair := range pairs { 331 scoreSplit := strings.Split(pair, ":") 332 if len(scoreSplit) != 2 { 333 return fmt.Errorf("invalid debug flag: %s", pair) 334 } 335 score, err := strconv.Atoi(scoreSplit[1]) 336 if err != nil { 337 return fmt.Errorf("invalid debug flag: %s", pair) 338 } 339 selectorSplit := strings.Split(scoreSplit[0], "=") 340 if len(selectorSplit) != 2 { 341 return fmt.Errorf("invalid debug flag: %s", pair) 342 } 343 _, ok := (*dest)[selectorSplit[0]] 344 if !ok { 345 (*dest)[selectorSplit[0]] = map[string]int{} 346 } 347 (*dest)[selectorSplit[0]][selectorSplit[1]] = score 348 } 349 350 return nil 351 }) 352} 353 354func (cli *CLI) StreamIsAllowed(did string) error { 355 if cli.WideOpen { 356 return nil 357 } 358 // if the user set no test streams, anyone can stream 359 openServer := len(cli.AllowedStreams) == 0 || (cli.TestStream && len(cli.AllowedStreams) == 1) 360 // but only valid atproto accounts! did:key is only allowed for our local test stream 361 isDIDKey := strings.HasPrefix(did, constants.DID_KEY_PREFIX) 362 if openServer && !isDIDKey { 363 return nil 364 } 365 for _, a := range cli.AllowedStreams { 366 if a == did { 367 return nil 368 } 369 } 370 return fmt.Errorf("user is not allowed to stream") 371}