diff --git a/README.md b/README.md index 7d64fe37642..25ec0c46688 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,8 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi * [SRT-specific features](#srt-specific-features) * [Standard stream ID syntax](#standard-stream-id-syntax) * [WebRTC-specific features](#webrtc-specific-features) - * [Connectivity issues](#connectivity-issues) + * [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep) + * [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues) * [RTSP-specific features](#rtsp-specific-features) * [Transport protocols](#transport-protocols) * [Encryption](#encryption) @@ -338,6 +339,7 @@ Latest versions of OBS Studio can publish to the server with the [WebRTC / WHIP * Service: `WHIP` * Server: `http://localhost:8889/mystream/whip` +* Bearer Token: `myuser:mypass` (if internal authentication is enabled) or JWT (if JWT-based authentication is enabled) Save the configuration and click `Start streaming`. @@ -610,7 +612,9 @@ WHIP is a WebRTC extensions that allows to publish streams by using a URL, witho http://localhost:8889/mystream/whip ``` -Depending on the network it may be difficult to establish a connection between server and clients, see [WebRTC-specific features](#webrtc-specific-features) for remediations. +Regarding authentication, read [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep). + +Depending on the network it may be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues). Known clients that can publish with WebRTC and WHIP are [FFmpeg](#ffmpeg), [GStreamer](#gstreamer), [OBS Studio](#obs-studio). @@ -876,7 +880,9 @@ WHEP is a WebRTC extensions that allows to read streams by using a URL, without http://localhost:8889/mystream/whep ``` -Depending on the network it may be difficult to establish a connection between server and clients, see [WebRTC-specific features](#webrtc-specific-features) for remediations. +Regarding authentication, read [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep). + +Depending on the network it may be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues). Known clients that can read with WebRTC and WHEP are [FFmpeg](#ffmpeg-1), [GStreamer](#gstreamer-1) and [web browsers](#web-browsers-1). @@ -1838,7 +1844,35 @@ Where: ### WebRTC-specific features -#### Connectivity issues +#### Authenticating with WHIP/WHEP + +When using WHIP or WHEP to establish a WebRTC connection, there are multiple ways to provide credentials. + +If internal authentication or HTTP-based authentication is enabled, username and password can be passed through the `Authentication: Basic` header: + +``` +Authentication: Basic [base64_encoded_credentials] +``` + +Username and password can be also passed through the `Authentication: Bearer` header (since it's mandated by the specification): + +``` +Authentication: Bearer username:password +``` + +If JWT-based authentication is enabled, JWT can be passed through the `Authentication: Bearer` header: + +``` +Authentication: Bearer [jwt] +``` + +The JWT can also be passed through query parameters: + +``` +http://localhost:8889/mystream/whip?jwt=[jwt] +``` + +#### Solving WebRTC connectivity issues If the server is hosted inside a container or is behind a NAT, additional configuration is required in order to allow the two WebRTC parts (server and client) to establish a connection. diff --git a/internal/servers/webrtc/http_server.go b/internal/servers/webrtc/http_server.go index 665f6abe661..4255b9076d1 100644 --- a/internal/servers/webrtc/http_server.go +++ b/internal/servers/webrtc/http_server.go @@ -121,10 +121,17 @@ func (s *httpServer) close() { func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool { user, pass, hasCredentials := ctx.Request.BasicAuth() - q := ctx.Request.URL.RawQuery + if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") { + // JWT in authorization bearer -> JWT in query parameters q = addJWTFromAuthorization(q, h) + + // credentials in authorization bearer -> credentials in authorization basic + if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 { + user = parts[0] + pass = parts[1] + } } _, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{ @@ -194,10 +201,17 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool) } user, pass, _ := ctx.Request.BasicAuth() - q := ctx.Request.URL.RawQuery + if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") { + // JWT in authorization bearer -> JWT in query parameters q = addJWTFromAuthorization(q, h) + + // credentials in authorization bearer -> credentials in authorization basic + if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 { + user = parts[0] + pass = parts[1] + } } res := s.parent.newSession(webRTCNewSessionReq{ diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index a8252d67136..0fcbf124f49 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -603,7 +603,7 @@ func TestServerRead(t *testing.T) { } } -func TestServerReadAuthorizationHeader(t *testing.T) { +func TestServerReadAuthorizationBearerJWT(t *testing.T) { desc := &description.Session{Medias: []*description.Media{test.MediaH264}} str, err := stream.New( @@ -680,6 +680,85 @@ func TestServerReadAuthorizationHeader(t *testing.T) { require.Equal(t, http.StatusCreated, res.StatusCode) } +func TestServerReadAuthorizationUserPass(t *testing.T) { + desc := &description.Session{Medias: []*description.Media{test.MediaH264}} + + str, err := stream.New( + 1460, + desc, + true, + test.NilLogger, + ) + require.NoError(t, err) + + path := &dummyPath{stream: str} + + pm := &dummyPathManager{ + findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return &conf.Path{}, nil + }, + addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return path, str, nil + }, + } + + s := &Server{ + Address: "127.0.0.1:8886", + Encryption: false, + ServerKey: "", + ServerCert: "", + AllowOrigin: "", + TrustedProxies: conf.IPNetworks{}, + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteQueueSize: 512, + LocalUDPAddress: "127.0.0.1:8887", + LocalTCPAddress: "127.0.0.1:8887", + IPsFromInterfaces: true, + IPsFromInterfacesList: []string{}, + AdditionalHosts: []string{}, + ICEServers: []conf.WebRTCICEServer{}, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + ExternalCmdPool: nil, + PathManager: pm, + Parent: test.NilLogger, + } + err = s.Initialize() + require.NoError(t, err) + defer s.Close() + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + hc := &http.Client{Transport: tr} + + pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{}) + require.NoError(t, err) + defer pc.Close() //nolint:errcheck + + _, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo) + require.NoError(t, err) + + offer, err := pc.CreateOffer(nil) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, + "http://localhost:8886/teststream/whep", bytes.NewReader([]byte(offer.SDP))) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/sdp") + req.Header.Set("Authorization", "Bearer myuser:mypass") + + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + require.Equal(t, http.StatusCreated, res.StatusCode) +} + func TestServerReadNotFound(t *testing.T) { pm := &dummyPathManager{ findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) {