Skip to content

Commit

Permalink
webrtc: allow using special characters in ICE server credentials (#1953
Browse files Browse the repository at this point in the history
…) (#2000)
  • Loading branch information
aler9 authored Jun 30, 2023
1 parent 418f4a9 commit 1a748bb
Show file tree
Hide file tree
Showing 22 changed files with 238 additions and 132 deletions.
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1051,9 +1051,7 @@ vlc --network-caching=50 rtsp://...

### General usage

RTMP is a protocol that allows to read and publish streams, but is less versatile and less efficient than RTSP (doesn't support UDP, encryption, doesn't support most RTSP codecs, doesn't support feedback mechanism). It is used when there's need of publishing or reading streams from a software that supports only RTMP (for instance, OBS Studio and DJI drones).

At the moment, only the H264 and AAC codecs can be used with the RTMP protocol.
RTMP is a protocol that allows to read and publish streams, but is less versatile and less efficient than RTSP (doesn't support UDP, encryption, doesn't support most RTSP codecs, doesn't support feedback mechanism). It is used when there's need of publishing or reading streams from a software that supports RTMP only (for instance, OBS Studio and DJI drones).

Streams can be published or read with the RTMP protocol, for instance with _FFmpeg_:

Expand Down Expand Up @@ -1213,7 +1211,7 @@ http://localhost:8889/mystream

### WHIP and WHEP

WHIP and WHEP are two WebRTC extensions that allows to publish and read streams with WebRTC without passing through a web page. This allows to use WebRTC as a general purpose streaming protocol.
WHIP and WHEP are two WebRTC extensions that allow to publish and read streams with WebRTC without passing through a web page. This allows to use WebRTC as a general purpose streaming protocol.

If you are using a software that supports WHIP, you can publish a stream to the server by using this WHIP URL:

Expand Down Expand Up @@ -1270,15 +1268,21 @@ bluenviron/mediamtx
Finally, if none of these methods work, you can force all WebRTC/ICE connections to pass through a TURN server, like [coturn](https://github.com/coturn/coturn), that must be configured externally. The server address and credentials must be set in the configuration file:

```yml
webrtcICEServers: [turn:user:pass:host:port]
webrtcICEServers2:
- url: turn:host:port
username: user
password: password
```

Where `user` and `pass` are the username and password of the server. Note that `port` is not optional.

If the server uses a secret-based authentication (for instance, coturn with the `use-auth-secret` option), it must be configured in this way:
If the server uses a secret-based authentication (for instance, coturn with the `use-auth-secret` option), it must be configured by using `AUTH_SECRET` as username, and the secret as password:

```yml
webrtcICEServers: [turn:AUTH_SECRET:secret:host:port]
webrtcICEServers2:
- url: turn:host:port
username: AUTH_SECRET
password: secret
```

where `secret` is the secret of the TURN server. _MediaMTX_ will generate a set of credentials by using the secret, and credentials will be sent to clients before the WebRTC/ICE connection is established.
Expand Down
11 changes: 9 additions & 2 deletions apidocs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,17 @@ components:
type: array
items:
type: string
webrtcICEServers:
webrtcICEServers2:
type: array
items:
type: string
type: object
properties:
url:
type: string
username:
type: string
password:
type: string
webrtcICEHostNAT1To1IPs:
type: array
items:
Expand Down
File renamed without changes.
48 changes: 32 additions & 16 deletions internal/conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,18 @@ type Conf struct {
HLSDirectory string `json:"hlsDirectory"`

// WebRTC
WebRTCDisable bool `json:"webrtcDisable"`
WebRTCAddress string `json:"webrtcAddress"`
WebRTCEncryption bool `json:"webrtcEncryption"`
WebRTCServerKey string `json:"webrtcServerKey"`
WebRTCServerCert string `json:"webrtcServerCert"`
WebRTCAllowOrigin string `json:"webrtcAllowOrigin"`
WebRTCTrustedProxies IPsOrCIDRs `json:"webrtcTrustedProxies"`
WebRTCICEServers []string `json:"webrtcICEServers"`
WebRTCICEHostNAT1To1IPs []string `json:"webrtcICEHostNAT1To1IPs"`
WebRTCICEUDPMuxAddress string `json:"webrtcICEUDPMuxAddress"`
WebRTCICETCPMuxAddress string `json:"webrtcICETCPMuxAddress"`
WebRTCDisable bool `json:"webrtcDisable"`
WebRTCAddress string `json:"webrtcAddress"`
WebRTCEncryption bool `json:"webrtcEncryption"`
WebRTCServerKey string `json:"webrtcServerKey"`
WebRTCServerCert string `json:"webrtcServerCert"`
WebRTCAllowOrigin string `json:"webrtcAllowOrigin"`
WebRTCTrustedProxies IPsOrCIDRs `json:"webrtcTrustedProxies"`
WebRTCICEServers []string `json:"webrtcICEServers"` // deprecated
WebRTCICEServers2 []WebRTCICEServer `json:"webrtcICEServers2"`
WebRTCICEHostNAT1To1IPs []string `json:"webrtcICEHostNAT1To1IPs"`
WebRTCICEUDPMuxAddress string `json:"webrtcICEUDPMuxAddress"`
WebRTCICETCPMuxAddress string `json:"webrtcICETCPMuxAddress"`

// paths
Paths map[string]*PathConf `json:"paths"`
Expand Down Expand Up @@ -237,10 +238,25 @@ func (conf *Conf) Check() error {

// WebRTC
for _, server := range conf.WebRTCICEServers {
if !strings.HasPrefix(server, "stun:") &&
!strings.HasPrefix(server, "turn:") &&
!strings.HasPrefix(server, "turns:") {
return fmt.Errorf("invalid ICE server: '%s'", server)
parts := strings.Split(server, ":")
if len(parts) == 5 {
conf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{
URL: parts[0] + ":" + parts[3] + ":" + parts[4],
Username: parts[1],
Password: parts[2],
})
} else {
conf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{
URL: server,
})
}
}
conf.WebRTCICEServers = nil
for _, server := range conf.WebRTCICEServers2 {
if !strings.HasPrefix(server.URL, "stun:") &&
!strings.HasPrefix(server.URL, "turn:") &&
!strings.HasPrefix(server.URL, "turns:") {
return fmt.Errorf("invalid ICE server: '%s'", server.URL)
}
}

Expand Down Expand Up @@ -324,7 +340,7 @@ func (conf *Conf) UnmarshalJSON(b []byte) error {
conf.WebRTCServerKey = "server.key"
conf.WebRTCServerCert = "server.crt"
conf.WebRTCAllowOrigin = "*"
conf.WebRTCICEServers = []string{"stun:stun.l.google.com:19302"}
conf.WebRTCICEServers2 = []WebRTCICEServer{{URL: "stun:stun.l.google.com:19302"}}

type alias Conf
d := json.NewDecoder(bytes.NewReader(b))
Expand Down
27 changes: 27 additions & 0 deletions internal/conf/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ type envUnmarshaler interface {
UnmarshalEnv(string) error
}

func envHasAtLeastAKeyWithPrefix(env map[string]string, prefix string) bool {
for key := range env {
if strings.HasPrefix(key, prefix) {
return true
}
}
return false
}

func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) error {
rt := rv.Type()

Expand Down Expand Up @@ -148,6 +157,24 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
}
return nil
}

if rt.Elem().Kind() == reflect.Struct {
for i := 0; ; i++ {
itemPrefix := prefix + "_" + strconv.FormatInt(int64(i), 10)
if !envHasAtLeastAKeyWithPrefix(env, itemPrefix) {
break
}

elem := reflect.New(rt.Elem())
err := loadEnvInternal(env, itemPrefix, elem.Elem())
if err != nil {
return err
}

rv.Set(reflect.Append(rv, elem.Elem()))
}
return nil
}
}

return fmt.Errorf("unsupported type: %v", rt)
Expand Down
50 changes: 42 additions & 8 deletions internal/conf/env/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,22 @@ func (d *myDuration) UnmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`))
}

type mySubStruct struct {
URL string
Username string
Password string
}

type testStruct struct {
MyString string
MyInt int
MyFloat float64
MyBool bool
MyDuration myDuration
MyMap map[string]*mapEntry
MySlice []string
MySliceEmpty []string
MyString string
MyInt int
MyFloat float64
MyBool bool
MyDuration myDuration
MyMap map[string]*mapEntry
MySlice []string
MySliceEmpty []string
MySliceSubStruct []mySubStruct
}

func TestLoad(t *testing.T) {
Expand Down Expand Up @@ -82,6 +89,21 @@ func TestLoad(t *testing.T) {
os.Setenv("MYPREFIX_MYSLICEEMPTY", "")
defer os.Unsetenv("MYPREFIX_MYSLICEEMPTY")

os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_0_URL", "url1")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_0_URL")

os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_0_USERNAME", "user1")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_0_USERNAME")

os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_0_PASSWORD", "pass1")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_0_PASSWORD")

os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_1_URL", "url2")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_1_URL")

os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_1_PASSWORD", "pass2")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_1_PASSWORD")

var s testStruct
err := Load("MYPREFIX", &s)
require.NoError(t, err)
Expand All @@ -102,4 +124,16 @@ func TestLoad(t *testing.T) {

require.Equal(t, []string{"val1", "val2"}, s.MySlice)
require.Equal(t, []string{}, s.MySliceEmpty)

require.Equal(t, []mySubStruct{
{
URL: "url1",
Username: "user1",
Password: "pass1",
},
{
URL: "url2",
Password: "pass2",
},
}, s.MySliceSubStruct)
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions internal/conf/webrtc_ice_server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package conf

// WebRTCICEServer is a WebRTC ICE Server.
type WebRTCICEServer struct {
URL string `json:"url"`
Username string `json:"username"`
Password string `json:"password"`
}
4 changes: 2 additions & 2 deletions internal/core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ func (p *Core) createResources(initial bool) error {
p.conf.WebRTCServerCert,
p.conf.WebRTCAllowOrigin,
p.conf.WebRTCTrustedProxies,
p.conf.WebRTCICEServers,
p.conf.WebRTCICEServers2,
p.conf.ReadTimeout,
p.conf.ReadBufferCount,
p.pathManager,
Expand Down Expand Up @@ -594,7 +594,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.WebRTCServerCert != p.conf.WebRTCServerCert ||
newConf.WebRTCAllowOrigin != p.conf.WebRTCAllowOrigin ||
!reflect.DeepEqual(newConf.WebRTCTrustedProxies, p.conf.WebRTCTrustedProxies) ||
!reflect.DeepEqual(newConf.WebRTCICEServers, p.conf.WebRTCICEServers) ||
!reflect.DeepEqual(newConf.WebRTCICEServers2, p.conf.WebRTCICEServers2) ||
newConf.ReadTimeout != p.conf.ReadTimeout ||
newConf.ReadBufferCount != p.conf.ReadBufferCount ||
closeMetrics ||
Expand Down
61 changes: 58 additions & 3 deletions internal/core/webrtc_http_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package core

import (
_ "embed"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"regexp"
"strconv"
"strings"

Expand All @@ -24,6 +26,59 @@ var webrtcPublishIndex []byte
//go:embed webrtc_read_index.html
var webrtcReadIndex []byte

func quoteCredential(v string) string {
b, _ := json.Marshal(v)
s := string(b)
return s[1 : len(s)-1]
}

func unquoteCredential(v string) string {
var s string
json.Unmarshal([]byte("\""+v+"\""), &s)
return s
}

func iceServersToLinkHeader(iceServers []webrtc.ICEServer) []string {
ret := make([]string, len(iceServers))

for i, server := range iceServers {
link := "<" + server.URLs[0] + ">; rel=\"ice-server\""
if server.Username != "" {
link += "; username=\"" + quoteCredential(server.Username) + "\"" +
"; credential=\"" + quoteCredential(server.Credential.(string)) + "\"; credential-type=\"password\""
}
ret[i] = link
}

return ret
}

var reLink = regexp.MustCompile(`^<(.+?)>; rel="ice-server"(; username="(.+?)"` +
`; credential="(.+?)"; credential-type="password")?`)

func linkHeaderToIceServers(link []string) []webrtc.ICEServer {
var ret []webrtc.ICEServer

for _, li := range link {
m := reLink.FindStringSubmatch(li)
if m != nil {
s := webrtc.ICEServer{
URLs: []string{m[1]},
}

if m[3] != "" {
s.Username = unquoteCredential(m[3])
s.Credential = unquoteCredential(m[4])
s.CredentialType = webrtc.ICECredentialTypePassword
}

ret = append(ret, s)
}
}

return ret
}

func unmarshalICEFragment(buf []byte) ([]*webrtc.ICECandidateInit, error) {
buf = append([]byte("v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\n"), buf...)

Expand Down Expand Up @@ -104,7 +159,7 @@ func marshalICEFragment(offer *webrtc.SessionDescription, candidates []*webrtc.I

type webRTCHTTPServerParent interface {
logger.Writer
genICEServers() []webrtc.ICEServer
generateICEServers() []webrtc.ICEServer
sessionNew(req webRTCSessionNewReq) webRTCSessionNewRes
sessionAddCandidates(req webRTCSessionAddCandidatesReq) webRTCSessionAddCandidatesRes
}
Expand Down Expand Up @@ -282,7 +337,7 @@ func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) {
case http.MethodOptions:
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, If-Match")
ctx.Writer.Header()["Link"] = iceServersToLinkHeader(s.parent.genICEServers())
ctx.Writer.Header()["Link"] = iceServersToLinkHeader(s.parent.generateICEServers())
ctx.Writer.WriteHeader(http.StatusOK)

case http.MethodPost:
Expand Down Expand Up @@ -314,7 +369,7 @@ func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) {
ctx.Writer.Header().Set("E-Tag", res.sx.secret.String())
ctx.Writer.Header().Set("ID", res.sx.uuid.String())
ctx.Writer.Header().Set("Accept-Patch", "application/trickle-ice-sdpfrag")
ctx.Writer.Header()["Link"] = iceServersToLinkHeader(s.parent.genICEServers())
ctx.Writer.Header()["Link"] = iceServersToLinkHeader(s.parent.generateICEServers())
ctx.Writer.Header().Set("Location", ctx.Request.URL.String())
ctx.Writer.WriteHeader(http.StatusCreated)
ctx.Writer.Write(res.answer)
Expand Down
Loading

0 comments on commit 1a748bb

Please sign in to comment.