Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webrtc: support passing username and password through Bearer Token (#3248) #3459

Merged
merged 1 commit into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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`.

Expand Down Expand Up @@ -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).

Expand Down Expand Up @@ -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).

Expand Down Expand Up @@ -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.

Expand Down
18 changes: 16 additions & 2 deletions internal/servers/webrtc/http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down
81 changes: 80 additions & 1 deletion internal/servers/webrtc/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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) {
Expand Down
Loading