From 65fd4997cc705c3f80f246e6f775597efc74d5ea Mon Sep 17 00:00:00 2001 From: aler9 <46489434+aler9@users.noreply.github.com> Date: Mon, 26 Feb 2024 22:51:51 +0100 Subject: [PATCH] add new authentication system --- apidocs/openapi.yaml | 2 +- internal/auth/manager.go | 216 +++++++++++++++++++++++++ internal/auth/manager_test.go | 206 +++++++++++++++++++++++ internal/conf/auth_action.go | 52 ++++++ internal/conf/auth_internal_users.go | 13 ++ internal/conf/auth_method.go | 57 +++---- internal/conf/conf.go | 182 ++++++++++++++++----- internal/conf/conf_test.go | 11 -- internal/conf/ip_networks.go | 14 +- internal/conf/path.go | 118 +++++++++----- internal/conf/rtsp_auth_methods.go | 63 ++++++++ internal/conf/string_size.go | 2 +- internal/core/auth.go | 126 --------------- internal/core/auth_test.go | 155 ------------------ internal/core/core.go | 56 +++++-- internal/core/path_manager.go | 34 ++-- internal/defs/auth.go | 23 --- internal/defs/path.go | 24 ++- internal/servers/hls/http_server.go | 5 +- internal/servers/rtmp/conn.go | 9 +- internal/servers/rtsp/conn.go | 12 +- internal/servers/rtsp/session.go | 15 +- internal/servers/srt/conn.go | 9 +- internal/servers/webrtc/http_server.go | 5 +- internal/servers/webrtc/session.go | 9 +- mediamtx.yml | 106 +++++++----- 26 files changed, 980 insertions(+), 544 deletions(-) create mode 100644 internal/auth/manager.go create mode 100644 internal/auth/manager_test.go create mode 100644 internal/conf/auth_action.go create mode 100644 internal/conf/auth_internal_users.go create mode 100644 internal/conf/rtsp_auth_methods.go delete mode 100644 internal/core/auth.go delete mode 100644 internal/core/auth_test.go delete mode 100644 internal/defs/auth.go diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index 68dedbf2068..54c243d0523 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -97,7 +97,7 @@ components: type: string serverCert: type: string - authMethods: + rtspAuthMethods: type: array items: type: string diff --git a/internal/auth/manager.go b/internal/auth/manager.go new file mode 100644 index 00000000000..7ec2834f23d --- /dev/null +++ b/internal/auth/manager.go @@ -0,0 +1,216 @@ +package auth + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "regexp" + "strings" + "sync" + + "github.com/bluenviron/gortsplib/v4/pkg/auth" + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/headers" + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/google/uuid" +) + +const ( + rtspAuthRealm = "IPCAM" +) + +// Protocol is a protocol. +type Protocol string + +// protocols. +const ( + ProtocolRTSP Protocol = "rtsp" + ProtocolRTMP Protocol = "rtmp" + ProtocolHLS Protocol = "hls" + ProtocolWebRTC Protocol = "webrtc" + ProtocolSRT Protocol = "srt" +) + +// Request is an authentication request. +type Request struct { + User string + Pass string + IP net.IP + Action conf.AuthAction + + // only for ActionPublish and ActionRead + Path string + Protocol Protocol + ID *uuid.UUID + Query string + RTSPRequest *base.Request + RTSPBaseURL *base.URL + RTSPNonce string +} + +// Error is a authentication error. +type Error struct { + Message string +} + +// Error implements the error interface. +func (e Error) Error() string { + return "authentication failed: " + e.Message +} + +func userHasPermission(u *conf.AuthInternalUser, req *Request) bool { + for _, perm := range u.Permissions { + if perm.Action == req.Action { + if perm.Action == conf.AuthActionPublish || perm.Action == conf.AuthActionRead || perm.Action == conf.AuthActionPlayback { + if perm.Path == "any" { + return true + } else if strings.HasPrefix(perm.Path, "~") { + regexp, err := regexp.Compile(perm.Path[1:]) + if err == nil && regexp.MatchString(req.Path) { + return true + } + } else if perm.Path == req.Path { + return true + } + } else { + return true + } + } + } + + return false +} + +// Manager is the authentication manager. +type Manager struct { + Method conf.AuthMethod + InternalUsers []conf.AuthInternalUser + HTTPAddress string + RTSPAuthMethods []headers.AuthMethod + + mutex sync.RWMutex +} + +func (m *Manager) ReloadUsers(u []conf.AuthInternalUser) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.InternalUsers = u +} + +func (m *Manager) Authenticate(req *Request) error { + m.mutex.RLock() + defer m.mutex.RUnlock() + + // if this is a RTSP request, fill username and password + var rtspAuthHeader headers.Authorization + if req.RTSPRequest != nil { + err := rtspAuthHeader.Unmarshal(req.RTSPRequest.Header["Authorization"]) + if err == nil { + switch rtspAuthHeader.Method { + case headers.AuthBasic: + req.User = rtspAuthHeader.BasicUser + req.Pass = rtspAuthHeader.BasicPass + + case headers.AuthDigestMD5: + req.User = rtspAuthHeader.Username + + default: + return Error{Message: "unsupported RTSP authentication method"} + } + } + } + + if m.Method == conf.AuthMethodInternal { + return m.authenticateInternal(req, &rtspAuthHeader) + } + return m.authenticateHTTP(req) +} + +func (m *Manager) authenticateInternal(req *Request, rtspAuthHeader *headers.Authorization) error { + for _, u := range m.InternalUsers { + if err := m.authenticateWithUser(req, rtspAuthHeader, &u); err == nil { + return nil + } + } + + return Error{Message: "authentication failed"} +} + +func (m *Manager) authenticateWithUser( + req *Request, + rtspAuthHeader *headers.Authorization, + u *conf.AuthInternalUser, +) error { + if u.User != "any" && !u.User.Check(req.User) { + return Error{Message: "wrong user"} + } + + if !u.IPs.Contains(req.IP) { + return Error{Message: "IP not allowed"} + } + + if !userHasPermission(u, req) { + return Error{Message: "user doesn't have permission to perform action"} + } + + if u.User != "any" { + if req.RTSPRequest != nil && rtspAuthHeader.Method == headers.AuthDigestMD5 { + err := auth.Validate( + req.RTSPRequest, + string(u.User), + string(u.Pass), + req.RTSPBaseURL, + m.RTSPAuthMethods, + rtspAuthRealm, + req.RTSPNonce) + if err != nil { + return Error{Message: err.Error()} + } + } else if !u.Pass.Check(req.Pass) { + return Error{Message: "invalid credentials"} + } + } + + return nil +} + +func (m *Manager) authenticateHTTP(req *Request) error { + enc, _ := json.Marshal(struct { + IP string `json:"ip"` + User string `json:"user"` + Password string `json:"password"` + Action string `json:"action"` + Path string `json:"path"` + Protocol string `json:"protocol"` + ID *uuid.UUID `json:"id"` + Query string `json:"query"` + }{ + IP: req.IP.String(), + User: req.User, + Password: req.Pass, + Action: string(req.Action), + Path: req.Path, + Protocol: string(req.Protocol), + ID: req.ID, + Query: req.Query, + }) + + res, err := http.Post(m.HTTPAddress, "application/json", bytes.NewReader(enc)) + if err != nil { + return Error{Message: fmt.Sprintf("HTTP request failed: %v", err)} + } + defer res.Body.Close() + + if res.StatusCode < 200 || res.StatusCode > 299 { + if resBody, err := io.ReadAll(res.Body); err == nil && len(resBody) != 0 { + return Error{Message: fmt.Sprintf("server replied with code %d: %s", res.StatusCode, string(resBody))} + } + + return Error{Message: fmt.Sprintf("server replied with code %d", res.StatusCode)} + } + + return nil +} diff --git a/internal/auth/manager_test.go b/internal/auth/manager_test.go new file mode 100644 index 00000000000..30b88f8c17b --- /dev/null +++ b/internal/auth/manager_test.go @@ -0,0 +1,206 @@ +package auth + +import ( + "context" + "encoding/json" + "net" + "net/http" + "testing" + + "github.com/bluenviron/gortsplib/v4/pkg/auth" + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/headers" + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/stretchr/testify/require" +) + +func mustParseCIDR(v string) net.IPNet { + _, ne, err := net.ParseCIDR(v) + if err != nil { + panic(err) + } + if ipv4 := ne.IP.To4(); ipv4 != nil { + return net.IPNet{IP: ipv4, Mask: ne.Mask[len(ne.Mask)-4 : len(ne.Mask)]} + } + return *ne +} + +type testHTTPAuthenticator struct { + *http.Server +} + +func (ts *testHTTPAuthenticator) initialize(t *testing.T, protocol string, action string) { + firstReceived := false + + ts.Server = &http.Server{ + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "/auth", r.URL.Path) + + var in struct { + IP string `json:"ip"` + User string `json:"user"` + Password string `json:"password"` + Path string `json:"path"` + Protocol string `json:"protocol"` + ID string `json:"id"` + Action string `json:"action"` + Query string `json:"query"` + } + err := json.NewDecoder(r.Body).Decode(&in) + require.NoError(t, err) + + var user string + if action == "publish" { + user = "testpublisher" + } else { + user = "testreader" + } + + if in.IP != "127.0.0.1" || + in.User != user || + in.Password != "testpass" || + in.Path != "teststream" || + in.Protocol != protocol || + (firstReceived && in.ID == "") || + in.Action != action || + (in.Query != "user=testreader&pass=testpass¶m=value" && + in.Query != "user=testpublisher&pass=testpass¶m=value" && + in.Query != "param=value") { + w.WriteHeader(http.StatusBadRequest) + return + } + + firstReceived = true + }), + } + + ln, err := net.Listen("tcp", "127.0.0.1:9120") + require.NoError(t, err) + + go ts.Server.Serve(ln) +} + +func (ts *testHTTPAuthenticator) close() { + ts.Server.Shutdown(context.Background()) +} + +func TestAuthInternal(t *testing.T) { + for _, ca := range []string{ + "plain", + "sha256", + "argon2", + } { + t.Run(ca, func(t *testing.T) { + m := Manager{ + Method: conf.AuthMethodInternal, + InternalUsers: []conf.AuthInternalUser{ + { + IPs: conf.IPNetworks{mustParseCIDR("127.1.1.1/32")}, + Permissions: []conf.AuthInternalUserPermission{ + { + Action: conf.AuthActionPublish, + Path: "mypath", + }, + }, + }, + }, + HTTPAddress: "", + RTSPAuthMethods: nil, + } + + switch ca { + case "plain": + m.InternalUsers[0].User = conf.Credential("testuser") + m.InternalUsers[0].Pass = conf.Credential("testpass") + + case "sha256": + m.InternalUsers[0].User = conf.Credential("sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=") + m.InternalUsers[0].Pass = conf.Credential("sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w=") + + case "argon2": + m.InternalUsers[0].User = conf.Credential("argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$Ux/LWeTgJQPyfMMJo1myR64+o8rALHoPmlE1i/TR+58") + m.InternalUsers[0].Pass = conf.Credential("argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$/mrZ42TiTv1mcPnpMUera5oi0SFYbbyueAbdx5sUvWo") + } + + err := m.Authenticate(&Request{ + User: "testuser", + Pass: "testpass", + IP: net.ParseIP("127.1.1.1"), + Action: conf.AuthActionPublish, + Path: "mypath", + }) + require.NoError(t, err) + }) + } +} + +func TestAuthInternalRTSPDigest(t *testing.T) { + m := Manager{ + Method: conf.AuthMethodInternal, + InternalUsers: []conf.AuthInternalUser{ + { + User: "myuser", + Pass: "mypass", + IPs: conf.IPNetworks{mustParseCIDR("127.1.1.1/32")}, + Permissions: []conf.AuthInternalUserPermission{ + { + Action: conf.AuthActionPublish, + Path: "mypath", + }, + }, + }, + }, + HTTPAddress: "", + RTSPAuthMethods: []headers.AuthMethod{headers.AuthDigestMD5}, + } + + u, err := base.ParseURL("rtsp://127.0.0.1:8554/mypath") + require.NoError(t, err) + + s, err := auth.NewSender( + auth.GenerateWWWAuthenticate([]headers.AuthMethod{headers.AuthDigestMD5}, "IPCAM", "mynonce"), + "myuser", + "mypass", + ) + require.NoError(t, err) + + req := &base.Request{ + Method: "ANNOUNCE", + URL: u, + } + + s.AddAuthorization(req) + + err = m.Authenticate(&Request{ + IP: net.ParseIP("127.1.1.1"), + Action: conf.AuthActionPublish, + Path: "mypath", + RTSPRequest: req, + RTSPNonce: "mynonce", + }) + require.NoError(t, err) +} + +func TestAuthHTTP(t *testing.T) { + m := Manager{ + Method: conf.AuthMethodHTTP, + HTTPAddress: "http://127.0.0.1:9120/auth", + RTSPAuthMethods: nil, + } + + au := &testHTTPAuthenticator{} + au.initialize(t, "rtsp", "publish") + defer au.close() + + err := m.Authenticate(&Request{ + User: "testpublisher", + Pass: "testpass", + IP: net.ParseIP("127.0.0.1"), + Action: conf.AuthActionPublish, + Path: "teststream", + Protocol: ProtocolRTSP, + Query: "param=value", + }) + require.NoError(t, err) +} diff --git a/internal/conf/auth_action.go b/internal/conf/auth_action.go new file mode 100644 index 00000000000..2d6b361585a --- /dev/null +++ b/internal/conf/auth_action.go @@ -0,0 +1,52 @@ +package conf + +import ( + "encoding/json" + "fmt" +) + +// AuthAction is an authentication action. +type AuthAction string + +// auth actions +const ( + AuthActionPublish AuthAction = "publish" + AuthActionRead AuthAction = "read" + AuthActionPlayback AuthAction = "playback" + AuthActionAPI AuthAction = "api" + AuthActionMetrics AuthAction = "metrics" + AuthActionPprof AuthAction = "pprof" +) + +// MarshalJSON implements json.Marshaler. +func (d AuthAction) MarshalJSON() ([]byte, error) { + return json.Marshal(string(d)) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (d *AuthAction) UnmarshalJSON(b []byte) error { + var in string + if err := json.Unmarshal(b, &in); err != nil { + return err + } + + switch in { + case string(AuthActionPublish), + string(AuthActionRead), + string(AuthActionPlayback), + string(AuthActionAPI), + string(AuthActionMetrics), + string(AuthActionPprof): + *d = AuthAction(in) + + default: + return fmt.Errorf("invalid auth action: '%s'", in) + } + + return nil +} + +// UnmarshalEnv implements env.Unmarshaler. +func (d *AuthAction) UnmarshalEnv(_ string, v string) error { + return d.UnmarshalJSON([]byte(`"` + v + `"`)) +} diff --git a/internal/conf/auth_internal_users.go b/internal/conf/auth_internal_users.go new file mode 100644 index 00000000000..104726fb67c --- /dev/null +++ b/internal/conf/auth_internal_users.go @@ -0,0 +1,13 @@ +package conf + +type AuthInternalUserPermission struct { + Action AuthAction `json:"action"` + Path string `json:"path"` +} + +type AuthInternalUser struct { + User Credential `json:"user"` + Pass Credential `json:"pass"` + IPs IPNetworks `json:"ips"` + Permissions []AuthInternalUserPermission `json:"permissions"` +} diff --git a/internal/conf/auth_method.go b/internal/conf/auth_method.go index da123e89541..eddf5f4cd88 100644 --- a/internal/conf/auth_method.go +++ b/internal/conf/auth_method.go @@ -3,61 +3,52 @@ package conf import ( "encoding/json" "fmt" - "sort" - "strings" - - "github.com/bluenviron/gortsplib/v4/pkg/headers" ) -// AuthMethods is the authMethods parameter. -type AuthMethods []headers.AuthMethod +type AuthMethod int + +const ( + AuthMethodInternal AuthMethod = iota + AuthMethodHTTP +) // MarshalJSON implements json.Marshaler. -func (d AuthMethods) MarshalJSON() ([]byte, error) { - out := make([]string, len(d)) +func (d AuthMethod) MarshalJSON() ([]byte, error) { + var out string - for i, v := range d { - switch v { - case headers.AuthBasic: - out[i] = "basic" + switch d { + case AuthMethodInternal: + out = "internal" - default: - out[i] = "digest" - } + default: + out = "http" } - sort.Strings(out) - return json.Marshal(out) } // UnmarshalJSON implements json.Unmarshaler. -func (d *AuthMethods) UnmarshalJSON(b []byte) error { - var in []string +func (d *AuthMethod) UnmarshalJSON(b []byte) error { + var in string if err := json.Unmarshal(b, &in); err != nil { return err } - *d = nil - - for _, v := range in { - switch v { - case "basic": - *d = append(*d, headers.AuthBasic) + switch in { + case "internal": + *d = AuthMethodInternal - case "digest": - *d = append(*d, headers.AuthDigestMD5) + case "http": + *d = AuthMethodHTTP - default: - return fmt.Errorf("invalid authentication method: '%s'", v) - } + default: + return fmt.Errorf("invalid authMethod: '%s'", in) } return nil } // UnmarshalEnv implements env.Unmarshaler. -func (d *AuthMethods) UnmarshalEnv(_ string, v string) error { - byts, _ := json.Marshal(strings.Split(v, ",")) - return d.UnmarshalJSON(byts) +func (d *AuthMethod) UnmarshalEnv(_ string, v string) error { + return d.UnmarshalJSON([]byte(`"` + v + `"`)) } diff --git a/internal/conf/conf.go b/internal/conf/conf.go index 8caeabddf66..d43133d9ef6 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "net" "os" "reflect" "sort" @@ -82,25 +83,54 @@ func copyStructFields(dest interface{}, source interface{}) { } } +func mustParseCIDR(v string) net.IPNet { + _, ne, err := net.ParseCIDR(v) + if err != nil { + panic(err) + } + if ipv4 := ne.IP.To4(); ipv4 != nil { + return net.IPNet{IP: ipv4, Mask: ne.Mask[len(ne.Mask)-4 : len(ne.Mask)]} + } + return *ne +} + +func anyPathHasDeprecatedCredentials(paths map[string]*OptionalPath) bool { + for _, pa := range paths { + if pa != nil { + rva := reflect.ValueOf(pa.Values).Elem() + if !rva.FieldByName("PublishUser").IsNil() || !rva.FieldByName("PublishPass").IsNil() || !rva.FieldByName("PublishIPs").IsNil() || + !rva.FieldByName("ReadUser").IsNil() || !rva.FieldByName("ReadPass").IsNil() || !rva.FieldByName("ReadIPs").IsNil() { + return true + } + } + } + return false +} + // Conf is a configuration. type Conf struct { // General - LogLevel LogLevel `json:"logLevel"` - LogDestinations LogDestinations `json:"logDestinations"` - LogFile string `json:"logFile"` - ReadTimeout StringDuration `json:"readTimeout"` - WriteTimeout StringDuration `json:"writeTimeout"` - ReadBufferCount *int `json:"readBufferCount,omitempty"` // deprecated - WriteQueueSize int `json:"writeQueueSize"` - UDPMaxPayloadSize int `json:"udpMaxPayloadSize"` - ExternalAuthenticationURL string `json:"externalAuthenticationURL"` - Metrics bool `json:"metrics"` - MetricsAddress string `json:"metricsAddress"` - PPROF bool `json:"pprof"` - PPROFAddress string `json:"pprofAddress"` - RunOnConnect string `json:"runOnConnect"` - RunOnConnectRestart bool `json:"runOnConnectRestart"` - RunOnDisconnect string `json:"runOnDisconnect"` + LogLevel LogLevel `json:"logLevel"` + LogDestinations LogDestinations `json:"logDestinations"` + LogFile string `json:"logFile"` + ReadTimeout StringDuration `json:"readTimeout"` + WriteTimeout StringDuration `json:"writeTimeout"` + ReadBufferCount *int `json:"readBufferCount,omitempty"` // deprecated + WriteQueueSize int `json:"writeQueueSize"` + UDPMaxPayloadSize int `json:"udpMaxPayloadSize"` + Metrics bool `json:"metrics"` + MetricsAddress string `json:"metricsAddress"` + PPROF bool `json:"pprof"` + PPROFAddress string `json:"pprofAddress"` + RunOnConnect string `json:"runOnConnect"` + RunOnConnectRestart bool `json:"runOnConnectRestart"` + RunOnDisconnect string `json:"runOnDisconnect"` + + // Authentication + AuthMethod AuthMethod `json:"authMethod"` + AuthInternalUsers []AuthInternalUser `json:"authInternalUsers"` + AuthHTTPAddress string `json:"authHTTPAddress"` + ExternalAuthenticationURL *string `json:"externalAuthenticationURL,omitempty"` // deprecated // API API bool `json:"api"` @@ -111,20 +141,21 @@ type Conf struct { PlaybackAddress string `json:"playbackAddress"` // RTSP server - RTSP bool `json:"rtsp"` - RTSPDisable *bool `json:"rtspDisable,omitempty"` // deprecated - Protocols Protocols `json:"protocols"` - Encryption Encryption `json:"encryption"` - RTSPAddress string `json:"rtspAddress"` - RTSPSAddress string `json:"rtspsAddress"` - RTPAddress string `json:"rtpAddress"` - RTCPAddress string `json:"rtcpAddress"` - MulticastIPRange string `json:"multicastIPRange"` - MulticastRTPPort int `json:"multicastRTPPort"` - MulticastRTCPPort int `json:"multicastRTCPPort"` - ServerKey string `json:"serverKey"` - ServerCert string `json:"serverCert"` - AuthMethods AuthMethods `json:"authMethods"` + RTSP bool `json:"rtsp"` + RTSPDisable *bool `json:"rtspDisable,omitempty"` // deprecated + Protocols Protocols `json:"protocols"` + Encryption Encryption `json:"encryption"` + RTSPAddress string `json:"rtspAddress"` + RTSPSAddress string `json:"rtspsAddress"` + RTPAddress string `json:"rtpAddress"` + RTCPAddress string `json:"rtcpAddress"` + MulticastIPRange string `json:"multicastIPRange"` + MulticastRTPPort int `json:"multicastRTPPort"` + MulticastRTCPPort int `json:"multicastRTCPPort"` + ServerKey string `json:"serverKey"` + ServerCert string `json:"serverCert"` + AuthMethods *RTSPAuthMethods `json:"authMethods,omitempty"` // deprecated + RTSPAuthMethods RTSPAuthMethods `json:"rtspAuthMethods"` // RTMP server RTMP bool `json:"rtmp"` @@ -201,11 +232,53 @@ func (conf *Conf) setDefaults() { conf.WriteTimeout = 10 * StringDuration(time.Second) conf.WriteQueueSize = 512 conf.UDPMaxPayloadSize = 1472 - conf.MetricsAddress = "127.0.0.1:9998" - conf.PPROFAddress = "127.0.0.1:9999" + conf.MetricsAddress = ":9998" + conf.PPROFAddress = ":9999" + + // Authentication + conf.AuthInternalUsers = []AuthInternalUser{ + { + User: "any", + Pass: "", + IPs: IPNetworks{mustParseCIDR("0.0.0.0/0")}, + Permissions: []AuthInternalUserPermission{ + { + Action: "publish", + Path: "any", + }, + { + Action: "read", + Path: "any", + }, + { + Action: "playback", + Path: "any", + }, + }, + }, + { + User: "any", + Pass: "", + IPs: IPNetworks{mustParseCIDR("127.0.0.1/32")}, + Permissions: []AuthInternalUserPermission{ + { + Action: "api", + Path: "", + }, + { + Action: "metrics", + Path: "", + }, + { + Action: "pprof", + Path: "", + }, + }, + }, + } // API - conf.APIAddress = "127.0.0.1:9997" + conf.APIAddress = ":9997" // Playback server conf.PlaybackAddress = ":9996" @@ -226,7 +299,7 @@ func (conf *Conf) setDefaults() { conf.MulticastRTCPPort = 8003 conf.ServerKey = "server.key" conf.ServerCert = "server.crt" - conf.AuthMethods = AuthMethods{headers.AuthBasic} + conf.RTSPAuthMethods = RTSPAuthMethods{headers.AuthBasic} // RTMP server conf.RTMP = true @@ -361,16 +434,25 @@ func (conf *Conf) Validate() error { if conf.UDPMaxPayloadSize > 1472 { return fmt.Errorf("'udpMaxPayloadSize' must be less than 1472") } - if conf.ExternalAuthenticationURL != "" { - if !strings.HasPrefix(conf.ExternalAuthenticationURL, "http://") && - !strings.HasPrefix(conf.ExternalAuthenticationURL, "https://") { - return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL") - } - if contains(conf.AuthMethods, headers.AuthDigestMD5) { - return fmt.Errorf("'externalAuthenticationURL' can't be used when 'digest' is in authMethods") + // Authentication + + if conf.ExternalAuthenticationURL != nil { + conf.AuthHTTPAddress = *conf.ExternalAuthenticationURL + } + if conf.AuthHTTPAddress != "" { + if !strings.HasPrefix(conf.AuthHTTPAddress, "http://") && + !strings.HasPrefix(conf.AuthHTTPAddress, "https://") { + return fmt.Errorf("'externalAuthenticationURL' must be a HTTP URL") } } + deprecatedCredentialsMode := false + if conf.PathDefaults.PublishUser != nil || conf.PathDefaults.PublishPass != nil || conf.PathDefaults.PublishIPs != nil || + conf.PathDefaults.ReadUser != nil || conf.PathDefaults.ReadPass != nil || conf.PathDefaults.ReadIPs != nil || + anyPathHasDeprecatedCredentials(conf.OptionalPaths) { + conf.AuthInternalUsers = []AuthInternalUser{} + deprecatedCredentialsMode = true + } // RTSP @@ -385,6 +467,22 @@ func (conf *Conf) Validate() error { return fmt.Errorf("strict encryption can't be used with the UDP-multicast transport protocol") } } + if conf.AuthMethods != nil { + conf.RTSPAuthMethods = *conf.AuthMethods + } + if contains(conf.RTSPAuthMethods, headers.AuthDigestMD5) { + if conf.AuthMethod != AuthMethodInternal { + return fmt.Errorf("when RTSP digest is enabled, the only supported auth method is 'internal'") + } + for _, user := range conf.AuthInternalUsers { + if user.User.IsHashed() { + return fmt.Errorf("when RTSP digest is enabled, hashed credentials cannot be used") + } + if user.Pass.IsHashed() { + return fmt.Errorf("when RTSP digest is enabled, hashed credentials cannot be used") + } + } + } // RTMP @@ -490,7 +588,7 @@ func (conf *Conf) Validate() error { pconf := newPath(&conf.PathDefaults, optional) conf.Paths[name] = pconf - err := pconf.validate(conf, name) + err := pconf.validate(conf, name, deprecatedCredentialsMode) if err != nil { return err } diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index 40f48f3f11a..1018b2922da 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -214,17 +214,6 @@ func TestConfErrors(t *testing.T) { "udpMaxPayloadSize: 5000\n", "'udpMaxPayloadSize' must be less than 1472", }, - { - "invalid externalAuthenticationURL 1", - "externalAuthenticationURL: testing\n", - "'externalAuthenticationURL' must be a HTTP URL", - }, - { - "invalid externalAuthenticationURL 2", - "externalAuthenticationURL: http://myurl\n" + - "authMethods: [digest]\n", - "'externalAuthenticationURL' can't be used when 'digest' is in authMethods", - }, { "invalid strict encryption 1", "encryption: strict\n" + diff --git a/internal/conf/ip_networks.go b/internal/conf/ip_networks.go index 8f6bd63e27a..d9f50d9e47c 100644 --- a/internal/conf/ip_networks.go +++ b/internal/conf/ip_networks.go @@ -9,7 +9,7 @@ import ( ) // IPNetworks is a parameter that contains a list of IP networks. -type IPNetworks []*net.IPNet +type IPNetworks []net.IPNet // MarshalJSON implements json.Marshaler. func (d IPNetworks) MarshalJSON() ([]byte, error) { @@ -39,9 +39,17 @@ func (d *IPNetworks) UnmarshalJSON(b []byte) error { for _, t := range in { if _, ipnet, err := net.ParseCIDR(t); err == nil { - *d = append(*d, ipnet) + if ipv4 := ipnet.IP.To4(); ipv4 != nil { + *d = append(*d, net.IPNet{IP: ipv4, Mask: ipnet.Mask[len(ipnet.Mask)-4 : len(ipnet.Mask)]}) + } else { + *d = append(*d, *ipnet) + } } else if ip := net.ParseIP(t); ip != nil { - *d = append(*d, &net.IPNet{IP: ip, Mask: net.CIDRMask(len(ip)*8, len(ip)*8)}) + if ipv4 := ip.To4(); ipv4 != nil { + *d = append(*d, net.IPNet{IP: ipv4, Mask: net.CIDRMask(32, 32)}) + } else { + *d = append(*d, net.IPNet{IP: ip, Mask: net.CIDRMask(128, 128)}) + } } else { return fmt.Errorf("unable to parse IP/CIDR '%s'", t) } diff --git a/internal/conf/path.go b/internal/conf/path.go index 17a9d32ad07..d852b68d5d8 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -11,7 +11,6 @@ import ( "time" "github.com/bluenviron/gortsplib/v4/pkg/base" - "github.com/bluenviron/gortsplib/v4/pkg/headers" ) var rePathName = regexp.MustCompile(`^[0-9a-zA-Z_\-/\.~]+$`) @@ -105,13 +104,13 @@ type Path struct { RecordSegmentDuration StringDuration `json:"recordSegmentDuration"` RecordDeleteAfter StringDuration `json:"recordDeleteAfter"` - // Authentication - PublishUser Credential `json:"publishUser"` - PublishPass Credential `json:"publishPass"` - PublishIPs IPNetworks `json:"publishIPs"` - ReadUser Credential `json:"readUser"` - ReadPass Credential `json:"readPass"` - ReadIPs IPNetworks `json:"readIPs"` + // Authentication (deprecated) + PublishUser *Credential `json:"publishUser,omitempty"` // deprecated + PublishPass *Credential `json:"publishPass,omitempty"` // deprecated + PublishIPs *IPNetworks `json:"publishIPs,omitempty"` // deprecated + ReadUser *Credential `json:"readUser,omitempty"` // deprecated + ReadPass *Credential `json:"readPass,omitempty"` // deprecated + ReadIPs *IPNetworks `json:"readIPs,omitempty"` // deprecated // Publisher source OverridePublisher bool `json:"overridePublisher"` @@ -250,7 +249,11 @@ func (pconf Path) Clone() *Path { return &dest } -func (pconf *Path) validate(conf *Conf, name string) error { +func (pconf *Path) validate( + conf *Conf, + name string, + deprecatedCredentialsMode bool, +) error { pconf.Name = name switch { @@ -375,39 +378,72 @@ func (pconf *Path) validate(conf *Conf, name string) error { } } - // Authentication + // Authentication (deprecated) - if (pconf.PublishUser != "" && pconf.PublishPass == "") || - (pconf.PublishUser == "" && pconf.PublishPass != "") { - return fmt.Errorf("read username and password must be both filled") - } - if pconf.PublishUser != "" && pconf.Source != "publisher" { - return fmt.Errorf("'publishUser' is useless when source is not 'publisher', since " + - "the stream is not provided by a publisher, but by a fixed source") - } - if len(pconf.PublishIPs) > 0 && pconf.Source != "publisher" { - return fmt.Errorf("'publishIPs' is useless when source is not 'publisher', since " + - "the stream is not provided by a publisher, but by a fixed source") - } - if (pconf.ReadUser != "" && pconf.ReadPass == "") || - (pconf.ReadUser == "" && pconf.ReadPass != "") { - return fmt.Errorf("read username and password must be both filled") - } - if contains(conf.AuthMethods, headers.AuthDigestMD5) { - if pconf.PublishUser.IsHashed() || - pconf.PublishPass.IsHashed() || - pconf.ReadUser.IsHashed() || - pconf.ReadPass.IsHashed() { - return fmt.Errorf("hashed credentials can't be used when the digest auth method is available") - } - } - if conf.ExternalAuthenticationURL != "" { - if pconf.PublishUser != "" || - len(pconf.PublishIPs) > 0 || - pconf.ReadUser != "" || - len(pconf.ReadIPs) > 0 { - return fmt.Errorf("credentials or IPs can't be used together with 'externalAuthenticationURL'") - } + if deprecatedCredentialsMode { + func() { + var user Credential = "any" + if pconf.PublishUser != nil { + user = *pconf.PublishUser + } + + var pass Credential + if pconf.PublishPass != nil { + pass = *pconf.PublishPass + } + + var ips IPNetworks = IPNetworks{mustParseCIDR("0.0.0.0/0")} + if pconf.PublishIPs != nil { + ips = *pconf.PublishIPs + } + + pathName := name + if name == "all_others" || name == "all" { + pathName = "~^.*$" + } + + conf.AuthInternalUsers = append(conf.AuthInternalUsers, AuthInternalUser{ + User: user, + Pass: pass, + IPs: ips, + Permissions: []AuthInternalUserPermission{{ + Action: AuthActionPublish, + Path: pathName, + }}, + }) + }() + + func() { + var user Credential = "any" + if pconf.ReadUser != nil { + user = *pconf.ReadUser + } + + var pass Credential + if pconf.ReadPass != nil { + pass = *pconf.ReadPass + } + + var ips IPNetworks = IPNetworks{mustParseCIDR("0.0.0.0/0")} + if pconf.ReadIPs != nil { + ips = *pconf.ReadIPs + } + + pathName := name + if name == "all_others" || name == "all" { + pathName = "~^.*$" + } + + conf.AuthInternalUsers = append(conf.AuthInternalUsers, AuthInternalUser{ + User: user, + Pass: pass, + IPs: ips, + Permissions: []AuthInternalUserPermission{{ + Action: AuthActionRead, + Path: pathName, + }}, + }) + }() } // Publisher source diff --git a/internal/conf/rtsp_auth_methods.go b/internal/conf/rtsp_auth_methods.go new file mode 100644 index 00000000000..e594fe489b5 --- /dev/null +++ b/internal/conf/rtsp_auth_methods.go @@ -0,0 +1,63 @@ +package conf + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + + "github.com/bluenviron/gortsplib/v4/pkg/headers" +) + +// RTSPAuthMethods is the authMethods parameter. +type RTSPAuthMethods []headers.AuthMethod + +// MarshalJSON implements json.Marshaler. +func (d RTSPAuthMethods) MarshalJSON() ([]byte, error) { + out := make([]string, len(d)) + + for i, v := range d { + switch v { + case headers.AuthBasic: + out[i] = "basic" + + default: + out[i] = "digest" + } + } + + sort.Strings(out) + + return json.Marshal(out) +} + +// UnmarshalJSON implements json.Unmarshaler. +func (d *RTSPAuthMethods) UnmarshalJSON(b []byte) error { + var in []string + if err := json.Unmarshal(b, &in); err != nil { + return err + } + + *d = nil + + for _, v := range in { + switch v { + case "basic": + *d = append(*d, headers.AuthBasic) + + case "digest": + *d = append(*d, headers.AuthDigestMD5) + + default: + return fmt.Errorf("invalid authentication method: '%s'", v) + } + } + + return nil +} + +// UnmarshalEnv implements env.Unmarshaler. +func (d *RTSPAuthMethods) UnmarshalEnv(_ string, v string) error { + byts, _ := json.Marshal(strings.Split(v, ",")) + return d.UnmarshalJSON(byts) +} diff --git a/internal/conf/string_size.go b/internal/conf/string_size.go index e3a1dcc45d8..cd9f8df6ef6 100644 --- a/internal/conf/string_size.go +++ b/internal/conf/string_size.go @@ -25,8 +25,8 @@ func (s *StringSize) UnmarshalJSON(b []byte) error { if err != nil { return err } - *s = StringSize(v) + return nil } diff --git a/internal/core/auth.go b/internal/core/auth.go deleted file mode 100644 index b74efd702db..00000000000 --- a/internal/core/auth.go +++ /dev/null @@ -1,126 +0,0 @@ -package core - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/bluenviron/gortsplib/v4/pkg/auth" - "github.com/bluenviron/gortsplib/v4/pkg/headers" - "github.com/google/uuid" - - "github.com/bluenviron/mediamtx/internal/conf" - "github.com/bluenviron/mediamtx/internal/defs" -) - -func doExternalAuthentication( - ur string, - accessRequest defs.PathAccessRequest, -) error { - enc, _ := json.Marshal(struct { - IP string `json:"ip"` - User string `json:"user"` - Password string `json:"password"` - Path string `json:"path"` - Protocol string `json:"protocol"` - ID *uuid.UUID `json:"id"` - Action string `json:"action"` - Query string `json:"query"` - }{ - IP: accessRequest.IP.String(), - User: accessRequest.User, - Password: accessRequest.Pass, - Path: accessRequest.Name, - Protocol: string(accessRequest.Proto), - ID: accessRequest.ID, - Action: func() string { - if accessRequest.Publish { - return "publish" - } - return "read" - }(), - Query: accessRequest.Query, - }) - res, err := http.Post(ur, "application/json", bytes.NewReader(enc)) - if err != nil { - return err - } - defer res.Body.Close() - - if res.StatusCode < 200 || res.StatusCode > 299 { - if resBody, err := io.ReadAll(res.Body); err == nil && len(resBody) != 0 { - return fmt.Errorf("server replied with code %d: %s", res.StatusCode, string(resBody)) - } - return fmt.Errorf("server replied with code %d", res.StatusCode) - } - - return nil -} - -func doAuthentication( - externalAuthenticationURL string, - rtspAuthMethods conf.AuthMethods, - pathConf *conf.Path, - accessRequest defs.PathAccessRequest, -) error { - var rtspAuth headers.Authorization - if accessRequest.RTSPRequest != nil { - err := rtspAuth.Unmarshal(accessRequest.RTSPRequest.Header["Authorization"]) - if err == nil && rtspAuth.Method == headers.AuthBasic { - accessRequest.User = rtspAuth.BasicUser - accessRequest.Pass = rtspAuth.BasicPass - } - } - - if externalAuthenticationURL != "" { - err := doExternalAuthentication( - externalAuthenticationURL, - accessRequest, - ) - if err != nil { - return defs.AuthenticationError{Message: fmt.Sprintf("external authentication failed: %s", err)} - } - } - - var pathIPs conf.IPNetworks - var pathUser conf.Credential - var pathPass conf.Credential - - if accessRequest.Publish { - pathIPs = pathConf.PublishIPs - pathUser = pathConf.PublishUser - pathPass = pathConf.PublishPass - } else { - pathIPs = pathConf.ReadIPs - pathUser = pathConf.ReadUser - pathPass = pathConf.ReadPass - } - - if pathIPs != nil { - if !pathIPs.Contains(accessRequest.IP) { - return defs.AuthenticationError{Message: fmt.Sprintf("IP %s not allowed", accessRequest.IP)} - } - } - - if pathUser != "" { - if accessRequest.RTSPRequest != nil && rtspAuth.Method == headers.AuthDigestMD5 { - err := auth.Validate( - accessRequest.RTSPRequest, - string(pathUser), - string(pathPass), - accessRequest.RTSPBaseURL, - rtspAuthMethods, - "IPCAM", - accessRequest.RTSPNonce) - if err != nil { - return defs.AuthenticationError{Message: err.Error()} - } - } else if !pathUser.Check(accessRequest.User) || !pathPass.Check(accessRequest.Pass) { - return defs.AuthenticationError{Message: "invalid credentials"} - } - } - - return nil -} diff --git a/internal/core/auth_test.go b/internal/core/auth_test.go deleted file mode 100644 index 9a373164540..00000000000 --- a/internal/core/auth_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package core - -import ( - "context" - "encoding/json" - "net" - "net/http" - "testing" - - "github.com/bluenviron/gortsplib/v4/pkg/headers" - "github.com/bluenviron/mediamtx/internal/conf" - "github.com/bluenviron/mediamtx/internal/defs" - "github.com/stretchr/testify/require" -) - -type testHTTPAuthenticator struct { - *http.Server -} - -func (ts *testHTTPAuthenticator) initialize(t *testing.T, protocol string, action string) { - firstReceived := false - - ts.Server = &http.Server{ - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - require.Equal(t, "/auth", r.URL.Path) - - var in struct { - IP string `json:"ip"` - User string `json:"user"` - Password string `json:"password"` - Path string `json:"path"` - Protocol string `json:"protocol"` - ID string `json:"id"` - Action string `json:"action"` - Query string `json:"query"` - } - err := json.NewDecoder(r.Body).Decode(&in) - require.NoError(t, err) - - var user string - if action == "publish" { - user = "testpublisher" - } else { - user = "testreader" - } - - if in.IP != "127.0.0.1" || - in.User != user || - in.Password != "testpass" || - in.Path != "teststream" || - in.Protocol != protocol || - (firstReceived && in.ID == "") || - in.Action != action || - (in.Query != "user=testreader&pass=testpass¶m=value" && - in.Query != "user=testpublisher&pass=testpass¶m=value" && - in.Query != "param=value") { - w.WriteHeader(http.StatusBadRequest) - return - } - - firstReceived = true - }), - } - - ln, err := net.Listen("tcp", "127.0.0.1:9120") - require.NoError(t, err) - - go ts.Server.Serve(ln) -} - -func (ts *testHTTPAuthenticator) close() { - ts.Server.Shutdown(context.Background()) -} - -func TestAuthSha256(t *testing.T) { - err := doAuthentication( - "", - conf.AuthMethods{headers.AuthBasic}, - &conf.Path{ - PublishUser: conf.Credential("sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ="), - PublishPass: conf.Credential("sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w="), - }, - defs.PathAccessRequest{ - Name: "mypath", - Query: "", - Publish: true, - SkipAuth: false, - IP: net.ParseIP("127.0.0.1"), - User: "testuser", - Pass: "testpass", - Proto: defs.AuthProtocolRTSP, - ID: nil, - RTSPRequest: nil, - RTSPBaseURL: nil, - RTSPNonce: "", - }, - ) - require.NoError(t, err) -} - -func TestAuthArgon2(t *testing.T) { - err := doAuthentication( - "", - conf.AuthMethods{headers.AuthBasic}, - &conf.Path{ - PublishUser: conf.Credential( - "argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$Ux/LWeTgJQPyfMMJo1myR64+o8rALHoPmlE1i/TR+58"), - PublishPass: conf.Credential( - "argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$/mrZ42TiTv1mcPnpMUera5oi0SFYbbyueAbdx5sUvWo"), - }, - defs.PathAccessRequest{ - Name: "mypath", - Query: "", - Publish: true, - SkipAuth: false, - IP: net.ParseIP("127.0.0.1"), - User: "testuser", - Pass: "testpass", - Proto: defs.AuthProtocolRTSP, - ID: nil, - RTSPRequest: nil, - RTSPBaseURL: nil, - RTSPNonce: "", - }, - ) - require.NoError(t, err) -} - -func TestAuthExternal(t *testing.T) { - au := &testHTTPAuthenticator{} - au.initialize(t, "rtsp", "publish") - defer au.close() - - err := doAuthentication( - "http://127.0.0.1:9120/auth", - conf.AuthMethods{headers.AuthBasic}, - &conf.Path{}, - defs.PathAccessRequest{ - Name: "teststream", - Query: "param=value", - Publish: true, - SkipAuth: false, - IP: net.ParseIP("127.0.0.1"), - User: "testpublisher", - Pass: "testpass", - Proto: defs.AuthProtocolRTSP, - ID: nil, - RTSPRequest: nil, - RTSPBaseURL: nil, - RTSPNonce: "", - }, - ) - require.NoError(t, err) -} diff --git a/internal/core/core.go b/internal/core/core.go index 2e81a799b9e..8f5b0292f4f 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -17,6 +17,7 @@ import ( "github.com/gin-gonic/gin" "github.com/bluenviron/mediamtx/internal/api" + "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/confwatcher" "github.com/bluenviron/mediamtx/internal/externalcmd" @@ -88,6 +89,7 @@ type Core struct { conf *conf.Conf logger *logger.Logger externalCmdPool *externalcmd.Pool + authManager *auth.Manager metrics *metrics.Metrics pprof *pprof.PPROF recordCleaner *record.Cleaner @@ -278,6 +280,15 @@ func (p *Core) createResources(initial bool) error { p.externalCmdPool = externalcmd.NewPool() } + if p.authManager == nil { + p.authManager = &auth.Manager{ + Method: p.conf.AuthMethod, + InternalUsers: p.conf.AuthInternalUsers, + HTTPAddress: p.conf.AuthHTTPAddress, + RTSPAuthMethods: p.conf.RTSPAuthMethods, + } + } + if p.conf.Metrics && p.metrics == nil { i := &metrics.Metrics{ @@ -333,17 +344,16 @@ func (p *Core) createResources(initial bool) error { if p.pathManager == nil { p.pathManager = &pathManager{ - logLevel: p.conf.LogLevel, - externalAuthenticationURL: p.conf.ExternalAuthenticationURL, - rtspAddress: p.conf.RTSPAddress, - authMethods: p.conf.AuthMethods, - readTimeout: p.conf.ReadTimeout, - writeTimeout: p.conf.WriteTimeout, - writeQueueSize: p.conf.WriteQueueSize, - udpMaxPayloadSize: p.conf.UDPMaxPayloadSize, - pathConfs: p.conf.Paths, - externalCmdPool: p.externalCmdPool, - parent: p, + logLevel: p.conf.LogLevel, + authManager: p.authManager, + rtspAddress: p.conf.RTSPAddress, + readTimeout: p.conf.ReadTimeout, + writeTimeout: p.conf.WriteTimeout, + writeQueueSize: p.conf.WriteQueueSize, + udpMaxPayloadSize: p.conf.UDPMaxPayloadSize, + pathConfs: p.conf.Paths, + externalCmdPool: p.externalCmdPool, + parent: p, } p.pathManager.initialize() @@ -361,7 +371,7 @@ func (p *Core) createResources(initial bool) error { i := &rtsp.Server{ Address: p.conf.RTSPAddress, - AuthMethods: p.conf.AuthMethods, + AuthMethods: p.conf.RTSPAuthMethods, ReadTimeout: p.conf.ReadTimeout, WriteTimeout: p.conf.WriteTimeout, WriteQueueSize: p.conf.WriteQueueSize, @@ -401,7 +411,7 @@ func (p *Core) createResources(initial bool) error { p.rtspsServer == nil { i := &rtsp.Server{ Address: p.conf.RTSPSAddress, - AuthMethods: p.conf.AuthMethods, + AuthMethods: p.conf.RTSPAuthMethods, ReadTimeout: p.conf.ReadTimeout, WriteTimeout: p.conf.WriteTimeout, WriteQueueSize: p.conf.WriteQueueSize, @@ -629,6 +639,14 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { !reflect.DeepEqual(newConf.LogDestinations, p.conf.LogDestinations) || newConf.LogFile != p.conf.LogFile + closeAuthManager := newConf == nil || + newConf.AuthMethod != p.conf.AuthMethod || + newConf.AuthHTTPAddress != p.conf.AuthHTTPAddress || + !reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods) + if !closeAuthManager && !reflect.DeepEqual(newConf.AuthInternalUsers, p.conf.AuthInternalUsers) { + p.authManager.ReloadUsers(newConf.AuthInternalUsers) + } + closeMetrics := newConf == nil || newConf.Metrics != p.conf.Metrics || newConf.MetricsAddress != p.conf.MetricsAddress || @@ -656,14 +674,14 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { closePathManager := newConf == nil || newConf.LogLevel != p.conf.LogLevel || - newConf.ExternalAuthenticationURL != p.conf.ExternalAuthenticationURL || newConf.RTSPAddress != p.conf.RTSPAddress || - !reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) || + !reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods) || newConf.ReadTimeout != p.conf.ReadTimeout || newConf.WriteTimeout != p.conf.WriteTimeout || newConf.WriteQueueSize != p.conf.WriteQueueSize || newConf.UDPMaxPayloadSize != p.conf.UDPMaxPayloadSize || closeMetrics || + closeAuthManager || closeLogger if !closePathManager && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) { p.pathManager.ReloadPathConfs(newConf.Paths) @@ -673,7 +691,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { newConf.RTSP != p.conf.RTSP || newConf.Encryption != p.conf.Encryption || newConf.RTSPAddress != p.conf.RTSPAddress || - !reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) || + !reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods) || newConf.ReadTimeout != p.conf.ReadTimeout || newConf.WriteTimeout != p.conf.WriteTimeout || newConf.WriteQueueSize != p.conf.WriteQueueSize || @@ -696,7 +714,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { newConf.RTSP != p.conf.RTSP || newConf.Encryption != p.conf.Encryption || newConf.RTSPSAddress != p.conf.RTSPSAddress || - !reflect.DeepEqual(newConf.AuthMethods, p.conf.AuthMethods) || + !reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods) || newConf.ReadTimeout != p.conf.ReadTimeout || newConf.WriteTimeout != p.conf.WriteTimeout || newConf.WriteQueueSize != p.conf.WriteQueueSize || @@ -919,6 +937,10 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { p.metrics = nil } + if closeAuthManager && p.authManager != nil { + p.authManager = nil + } + if newConf == nil && p.externalCmdPool != nil { p.Log(logger.Info, "waiting for running hooks") p.externalCmdPool.Close() diff --git a/internal/core/path_manager.go b/internal/core/path_manager.go index 011275a5ffa..ae78ba1b1e2 100644 --- a/internal/core/path_manager.go +++ b/internal/core/path_manager.go @@ -6,6 +6,7 @@ import ( "sort" "sync" + "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" @@ -47,17 +48,16 @@ type pathManagerParent interface { } type pathManager struct { - logLevel conf.LogLevel - externalAuthenticationURL string - rtspAddress string - authMethods conf.AuthMethods - readTimeout conf.StringDuration - writeTimeout conf.StringDuration - writeQueueSize int - udpMaxPayloadSize int - pathConfs map[string]*conf.Path - externalCmdPool *externalcmd.Pool - parent pathManagerParent + logLevel conf.LogLevel + authManager *auth.Manager + rtspAddress string + readTimeout conf.StringDuration + writeTimeout conf.StringDuration + writeQueueSize int + udpMaxPayloadSize int + pathConfs map[string]*conf.Path + externalCmdPool *externalcmd.Pool + parent pathManagerParent ctx context.Context ctxCancel func() @@ -236,8 +236,7 @@ func (pm *pathManager) doFindPathConf(req defs.PathFindPathConfReq) { return } - err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, - pathConf, req.AccessRequest) + err = pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest()) if err != nil { req.Res <- defs.PathFindPathConfRes{Err: err} return @@ -253,8 +252,7 @@ func (pm *pathManager) doDescribe(req defs.PathDescribeReq) { return } - err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, - pathConf, req.AccessRequest) + err = pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest()) if err != nil { req.Res <- defs.PathDescribeRes{Err: err} return @@ -276,8 +274,7 @@ func (pm *pathManager) doAddReader(req defs.PathAddReaderReq) { } if !req.AccessRequest.SkipAuth { - err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, - pathConf, req.AccessRequest) + err = pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest()) if err != nil { req.Res <- defs.PathAddReaderRes{Err: err} return @@ -300,8 +297,7 @@ func (pm *pathManager) doAddPublisher(req defs.PathAddPublisherReq) { } if !req.AccessRequest.SkipAuth { - err = doAuthentication(pm.externalAuthenticationURL, pm.authMethods, - pathConf, req.AccessRequest) + err = pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest()) if err != nil { req.Res <- defs.PathAddPublisherRes{Err: err} return diff --git a/internal/defs/auth.go b/internal/defs/auth.go deleted file mode 100644 index a0d708f9b4f..00000000000 --- a/internal/defs/auth.go +++ /dev/null @@ -1,23 +0,0 @@ -package defs - -// AuthProtocol is a authentication protocol. -type AuthProtocol string - -// authentication protocols. -const ( - AuthProtocolRTSP AuthProtocol = "rtsp" - AuthProtocolRTMP AuthProtocol = "rtmp" - AuthProtocolHLS AuthProtocol = "hls" - AuthProtocolWebRTC AuthProtocol = "webrtc" - AuthProtocolSRT AuthProtocol = "srt" -) - -// AuthenticationError is a authentication error. -type AuthenticationError struct { - Message string -} - -// Error implements the error interface. -func (e AuthenticationError) Error() string { - return "authentication failed: " + e.Message -} diff --git a/internal/defs/path.go b/internal/defs/path.go index d9d98bb9a4a..c8aba2371fc 100644 --- a/internal/defs/path.go +++ b/internal/defs/path.go @@ -8,6 +8,7 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/google/uuid" + "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/stream" @@ -45,13 +46,34 @@ type PathAccessRequest struct { IP net.IP User string Pass string - Proto AuthProtocol + Proto auth.Protocol ID *uuid.UUID RTSPRequest *base.Request RTSPBaseURL *base.URL RTSPNonce string } +func (r *PathAccessRequest) ToAuthRequest() *auth.Request { + return &auth.Request{ + User: r.User, + Pass: r.Pass, + IP: r.IP, + Action: func() conf.AuthAction { + if r.Publish { + return conf.AuthActionPublish + } + return conf.AuthActionRead + }(), + Path: r.Name, + Protocol: r.Proto, + ID: r.ID, + Query: r.Query, + RTSPRequest: r.RTSPRequest, + RTSPBaseURL: r.RTSPBaseURL, + RTSPNonce: r.RTSPNonce, + } +} + // PathFindPathConfRes contains the response of FindPathConf(). type PathFindPathConfRes struct { Conf *conf.Path diff --git a/internal/servers/hls/http_server.go b/internal/servers/hls/http_server.go index 3f85aef5500..7aac816d806 100644 --- a/internal/servers/hls/http_server.go +++ b/internal/servers/hls/http_server.go @@ -12,6 +12,7 @@ import ( "github.com/gin-gonic/gin" + "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" @@ -158,11 +159,11 @@ func (s *httpServer) onRequest(ctx *gin.Context) { IP: net.ParseIP(ctx.ClientIP()), User: user, Pass: pass, - Proto: defs.AuthProtocolHLS, + Proto: auth.ProtocolHLS, }, }) if err != nil { - var terr defs.AuthenticationError + var terr auth.Error if errors.As(err, &terr) { if !hasCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) diff --git a/internal/servers/rtmp/conn.go b/internal/servers/rtmp/conn.go index f4b4774b9b2..833a944bfb3 100644 --- a/internal/servers/rtmp/conn.go +++ b/internal/servers/rtmp/conn.go @@ -18,6 +18,7 @@ import ( "github.com/google/uuid" "github.com/bluenviron/mediamtx/internal/asyncwriter" + "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" @@ -176,12 +177,12 @@ func (c *conn) runRead(conn *rtmp.Conn, u *url.URL) error { IP: c.ip(), User: query.Get("user"), Pass: query.Get("pass"), - Proto: defs.AuthProtocolRTMP, + Proto: auth.ProtocolRTMP, ID: &c.uuid, }, }) if err != nil { - var terr defs.AuthenticationError + var terr auth.Error if errors.As(err, &terr) { // wait some seconds to mitigate brute force attacks <-time.After(pauseAfterAuthError) @@ -405,12 +406,12 @@ func (c *conn) runPublish(conn *rtmp.Conn, u *url.URL) error { IP: c.ip(), User: query.Get("user"), Pass: query.Get("pass"), - Proto: defs.AuthProtocolRTMP, + Proto: auth.ProtocolRTMP, ID: &c.uuid, }, }) if err != nil { - var terr defs.AuthenticationError + var terr auth.Error if errors.As(err, &terr) { // wait some seconds to mitigate brute force attacks <-time.After(pauseAfterAuthError) diff --git a/internal/servers/rtsp/conn.go b/internal/servers/rtsp/conn.go index edd725e6ed5..8f7924c405b 100644 --- a/internal/servers/rtsp/conn.go +++ b/internal/servers/rtsp/conn.go @@ -7,11 +7,12 @@ import ( "time" "github.com/bluenviron/gortsplib/v4" - "github.com/bluenviron/gortsplib/v4/pkg/auth" + rtspauth "github.com/bluenviron/gortsplib/v4/pkg/auth" "github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/bluenviron/gortsplib/v4/pkg/headers" "github.com/google/uuid" + "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" @@ -21,6 +22,7 @@ import ( const ( pauseAfterAuthError = 2 * time.Second + rtspAuthRealm = "IPCAM" ) type conn struct { @@ -118,7 +120,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, if c.authNonce == "" { var err error - c.authNonce, err = auth.GenerateNonce() + c.authNonce, err = rtspauth.GenerateNonce() if err != nil { return &base.Response{ StatusCode: base.StatusInternalServerError, @@ -131,7 +133,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, Name: ctx.Path, Query: ctx.Query, IP: c.ip(), - Proto: defs.AuthProtocolRTSP, + Proto: auth.ProtocolRTSP, ID: &c.uuid, RTSPRequest: ctx.Request, RTSPNonce: c.authNonce, @@ -139,7 +141,7 @@ func (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx, }) if res.Err != nil { - var terr defs.AuthenticationError + var terr auth.Error if errors.As(res.Err, &terr) { res, err := c.handleAuthError(terr) return res, nil, err @@ -191,7 +193,7 @@ func (c *conn) handleAuthError(authErr error) (*base.Response, error) { return &base.Response{ StatusCode: base.StatusUnauthorized, Header: base.Header{ - "WWW-Authenticate": auth.GenerateWWWAuthenticate(c.authMethods, "IPCAM", c.authNonce), + "WWW-Authenticate": rtspauth.GenerateWWWAuthenticate(c.authMethods, rtspAuthRealm, c.authNonce), }, }, nil } diff --git a/internal/servers/rtsp/session.go b/internal/servers/rtsp/session.go index 2581b43b094..bc705ba88b6 100644 --- a/internal/servers/rtsp/session.go +++ b/internal/servers/rtsp/session.go @@ -9,11 +9,12 @@ import ( "time" "github.com/bluenviron/gortsplib/v4" - "github.com/bluenviron/gortsplib/v4/pkg/auth" + rtspauth "github.com/bluenviron/gortsplib/v4/pkg/auth" "github.com/bluenviron/gortsplib/v4/pkg/base" "github.com/google/uuid" "github.com/pion/rtp" + "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" @@ -102,7 +103,7 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx) if c.authNonce == "" { var err error - c.authNonce, err = auth.GenerateNonce() + c.authNonce, err = rtspauth.GenerateNonce() if err != nil { return &base.Response{ StatusCode: base.StatusInternalServerError, @@ -117,7 +118,7 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx) Query: ctx.Query, Publish: true, IP: c.ip(), - Proto: defs.AuthProtocolRTSP, + Proto: auth.ProtocolRTSP, ID: &c.uuid, RTSPRequest: ctx.Request, RTSPBaseURL: nil, @@ -125,7 +126,7 @@ func (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx) }, }) if err != nil { - var terr defs.AuthenticationError + var terr auth.Error if errors.As(err, &terr) { return c.handleAuthError(terr) } @@ -187,7 +188,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx, if c.authNonce == "" { var err error - c.authNonce, err = auth.GenerateNonce() + c.authNonce, err = rtspauth.GenerateNonce() if err != nil { return &base.Response{ StatusCode: base.StatusInternalServerError, @@ -201,7 +202,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx, Name: ctx.Path, Query: ctx.Query, IP: c.ip(), - Proto: defs.AuthProtocolRTSP, + Proto: auth.ProtocolRTSP, ID: &c.uuid, RTSPRequest: ctx.Request, RTSPBaseURL: baseURL, @@ -209,7 +210,7 @@ func (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx, }, }) if err != nil { - var terr defs.AuthenticationError + var terr auth.Error if errors.As(err, &terr) { res, err := c.handleAuthError(terr) return res, nil, err diff --git a/internal/servers/srt/conn.go b/internal/servers/srt/conn.go index 9ffd360a4f3..f38855e8e9a 100644 --- a/internal/servers/srt/conn.go +++ b/internal/servers/srt/conn.go @@ -15,6 +15,7 @@ import ( "github.com/google/uuid" "github.com/bluenviron/mediamtx/internal/asyncwriter" + "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" @@ -171,13 +172,13 @@ func (c *conn) runPublish(req srtNewConnReq, streamID *streamID) (bool, error) { Publish: true, User: streamID.user, Pass: streamID.pass, - Proto: defs.AuthProtocolSRT, + Proto: auth.ProtocolSRT, ID: &c.uuid, Query: streamID.query, }, }) if err != nil { - var terr defs.AuthenticationError + var terr auth.Error if errors.As(err, &terr) { // wait some seconds to mitigate brute force attacks <-time.After(pauseAfterAuthError) @@ -267,13 +268,13 @@ func (c *conn) runRead(req srtNewConnReq, streamID *streamID) (bool, error) { IP: c.ip(), User: streamID.user, Pass: streamID.pass, - Proto: defs.AuthProtocolSRT, + Proto: auth.ProtocolSRT, ID: &c.uuid, Query: streamID.query, }, }) if err != nil { - var terr defs.AuthenticationError + var terr auth.Error if errors.As(err, &terr) { // wait some seconds to mitigate brute force attacks <-time.After(pauseAfterAuthError) diff --git a/internal/servers/webrtc/http_server.go b/internal/servers/webrtc/http_server.go index e5468017ed9..e7dceb3397b 100644 --- a/internal/servers/webrtc/http_server.go +++ b/internal/servers/webrtc/http_server.go @@ -14,6 +14,7 @@ import ( "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" @@ -117,11 +118,11 @@ func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, path string, publ IP: net.ParseIP(ctx.ClientIP()), User: user, Pass: pass, - Proto: defs.AuthProtocolWebRTC, + Proto: auth.ProtocolWebRTC, }, }) if err != nil { - var terr defs.AuthenticationError + var terr auth.Error if errors.As(err, &terr) { if !hasCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) diff --git a/internal/servers/webrtc/session.go b/internal/servers/webrtc/session.go index 525235dcff5..dfdfe39191c 100644 --- a/internal/servers/webrtc/session.go +++ b/internal/servers/webrtc/session.go @@ -22,6 +22,7 @@ import ( pwebrtc "github.com/pion/webrtc/v3" "github.com/bluenviron/mediamtx/internal/asyncwriter" + "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/hooks" @@ -374,12 +375,12 @@ func (s *session) runPublish() (int, error) { IP: net.ParseIP(ip), User: s.req.user, Pass: s.req.pass, - Proto: defs.AuthProtocolWebRTC, + Proto: auth.ProtocolWebRTC, ID: &s.uuid, }, }) if err != nil { - var terr defs.AuthenticationError + var terr auth.Error if errors.As(err, &terr) { // wait some seconds to mitigate brute force attacks <-time.After(pauseAfterAuthError) @@ -505,12 +506,12 @@ func (s *session) runRead() (int, error) { IP: net.ParseIP(ip), User: s.req.user, Pass: s.req.pass, - Proto: defs.AuthProtocolWebRTC, + Proto: auth.ProtocolWebRTC, ID: &s.uuid, }, }) if err != nil { - var terr1 defs.AuthenticationError + var terr1 auth.Error if errors.As(err, &terr1) { // wait some seconds to mitigate brute force attacks <-time.After(pauseAfterAuthError) diff --git a/mediamtx.yml b/mediamtx.yml index 670a65c0d1d..5b75d10ec69 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -24,32 +24,15 @@ writeQueueSize: 512 # This can be decreased to avoid fragmentation on networks with a low UDP MTU. udpMaxPayloadSize: 1472 -# HTTP URL to perform external authentication. -# Every time a user wants to authenticate, the server calls this URL -# with the POST method and a body containing: -# { -# "ip": "ip", -# "user": "user", -# "password": "password", -# "path": "path", -# "protocol": "rtsp|rtmp|hls|webrtc", -# "id": "id", -# "action": "read|publish", -# "query": "query" -# } -# If the response code is 20x, authentication is accepted, otherwise -# it is discarded. -externalAuthenticationURL: - # Enable Prometheus-compatible metrics. metrics: no # Address of the metrics listener. -metricsAddress: 127.0.0.1:9998 +metricsAddress: :9998 # Enable pprof-compatible endpoint to monitor performances. pprof: no # Address of the pprof listener. -pprofAddress: 127.0.0.1:9999 +pprofAddress: :9999 # Command to run when a client connects to the server. # This is terminated with SIGINT when a client disconnects from the server. @@ -64,13 +47,71 @@ runOnConnectRestart: no # Environment variables are the same of runOnConnect. runOnDisconnect: +############################################### +# Global settings -> Authentication + +# Authentication method. Available values are: +# * internal: an internal list of users is used. +# * http: an external HTTP URL is contacted to perform authentication. +authMethod: internal + +# Internal authentication. +# list of users. +authInternalUsers: + # Default unprivileged user. + # Username. 'any' means any user, including anonymous ones. +- user: any + # Password. Not used in case of 'any' user. + pass: + # IPs or networks allowed to use this user. + ips: [0.0.0.0/0] + # List of permissions. + # Available actions are: publish, read, playback, api, metrics, pprof. + # Paths can be set to further restrict access to a specific path. + permissions: + - action: publish + path: any + - action: read + path: any + - action: playback + path: any + + # Default administrator. + # This allows to use API, metrics and PPROF without authentication, + # if the IP is localhost. +- user: any + pass: + ips: [127.0.0.1] + permissions: + - action: api + - action: metrics + - action: pprof + +# HTTP-based authentication. +# URL called to perform authentication. Every time a user wants +# to authenticate, the server calls this URL with the POST method +# and a body containing: +# { +# "user": "user", +# "password": "password", +# "ip": "ip", +# "action": "publish|read|playback|api|metrics|pprof", +# "path": "path", +# "protocol": "rtsp|rtmp|hls|webrtc|srt", +# "id": "id", +# "query": "query" +# } +# If the response code is 20x, authentication is accepted, otherwise +# it is discarded. +authHTTPAddress: + ############################################### # Global settings -> API # Enable controlling the server through the API. api: no # Address of the API listener. -apiAddress: 127.0.0.1:9997 +apiAddress: :9997 ############################################### # Global settings -> Playback server @@ -117,8 +158,8 @@ serverKey: server.key # Path to the server certificate. This is needed only when encryption is "strict" or "optional". serverCert: server.crt # Authentication methods. Available are "basic" and "digest". -# "digest" doesn't provide any additional security and is available for compatibility reasons only. -authMethods: [basic] +# "digest" doesn't provide any additional security and is available for compatibility only. +rtspAuthMethods: [basic] ############################################### # Global settings -> RTMP server @@ -327,27 +368,6 @@ pathDefaults: # Set to 0s to disable automatic deletion. recordDeleteAfter: 24h - ############################################### - # Default path settings -> Authentication - - # Username required to publish. - # Hashed values can be inserted with the "argon2:" or "sha256:" prefix. - publishUser: - # Password required to publish. - # Hashed values can be inserted with the "argon2:" or "sha256:" prefix. - publishPass: - # IPs or networks (x.x.x.x/24) allowed to publish. - publishIPs: [] - - # Username required to read. - # Hashed values can be inserted with the "argon2:" or "sha256:" prefix. - readUser: - # password required to read. - # Hashed values can be inserted with the "argon2:" or "sha256:" prefix. - readPass: - # IPs or networks (x.x.x.x/24) allowed to read. - readIPs: [] - ############################################### # Default path settings -> Publisher source (when source is "publisher")