package media import ( "errors" "fmt" "io" "net" "net/url" "os" "path/filepath" "strings" "time" ) var ErrUnsafeControlAPI = errors.New("MediaMTX Control API 必须使用本机回环地址") type RuntimeConfig struct { Binary string ConfigPath string APIBase string PollInterval time.Duration StartTimeout time.Duration } func ConfigFromEnvironment() (RuntimeConfig, error) { c := RuntimeConfig{ Binary: strings.TrimSpace(os.Getenv("SENSE_MEDIAMTX_BINARY")), ConfigPath: strings.TrimSpace(os.Getenv("SENSE_MEDIAMTX_CONFIG")), APIBase: strings.TrimSpace(os.Getenv("SENSE_MEDIAMTX_API")), PollInterval: 5 * time.Second, StartTimeout: 8 * time.Second, } if c.APIBase == "" { c.APIBase = "http://127.0.0.1:9997" } if err := validateControlAPI(c.APIBase); err != nil { return RuntimeConfig{}, err } if c.Binary != "" && c.ConfigPath == "" { return RuntimeConfig{}, errors.New("SENSE_MEDIAMTX_CONFIG is required when SENSE_MEDIAMTX_BINARY is configured") } return c, nil } func validateControlAPI(value string) error { u, err := url.Parse(value) if err != nil || u.Scheme != "http" || u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "" { return ErrUnsafeControlAPI } host := u.Hostname() ip := net.ParseIP(host) if host != "localhost" && (ip == nil || !ip.IsLoopback()) { return ErrUnsafeControlAPI } return nil } func RenderBaseConfig(w io.Writer, apiBase string) error { if err := validateControlAPI(apiBase); err != nil { return err } u, _ := url.Parse(apiBase) _, err := fmt.Fprintf(w, "logLevel: info\napi: true\napiAddress: %s\nmetrics: false\npaths: {}\n", u.Host) return err } func EnsureBaseConfig(path, apiBase string) error { if path == "" { return errors.New("MediaMTX config path is empty") } if _, err := os.Stat(path); err == nil { return nil } else if !errors.Is(err, os.ErrNotExist) { return err } if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil { return err } temporary := path + ".tmp" f, err := os.OpenFile(temporary, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600) if err != nil { return err } writeErr := RenderBaseConfig(f, apiBase) closeErr := f.Close() if writeErr != nil || closeErr != nil { _ = os.Remove(temporary) return errors.Join(writeErr, closeErr) } if err = os.Rename(temporary, path); err != nil { _ = os.Remove(temporary) } return err }