2023-07-10 16:24:30 +02:00
|
|
|
package webserver
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2024-01-11 21:13:25 +01:00
|
|
|
"crypto/aes"
|
|
|
|
"crypto/cipher"
|
2023-07-10 16:24:30 +02:00
|
|
|
crand "crypto/rand"
|
|
|
|
"encoding/base64"
|
2024-01-11 21:13:25 +01:00
|
|
|
"errors"
|
2023-07-10 16:24:30 +02:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"log"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"path"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"github.com/pion/webrtc/v3"
|
|
|
|
|
|
|
|
"github.com/jech/galene/group"
|
|
|
|
"github.com/jech/galene/ice"
|
|
|
|
"github.com/jech/galene/rtpconn"
|
|
|
|
)
|
|
|
|
|
2024-01-11 21:13:25 +01:00
|
|
|
var idSecret []byte
|
|
|
|
var idCipher cipher.Block
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
idSecret = make([]byte, 16)
|
|
|
|
_, err := crand.Read(idSecret)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("crand.Read: %v", err)
|
|
|
|
}
|
|
|
|
idCipher, err = aes.NewCipher(idSecret)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalf("NewCipher: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-10 16:24:30 +02:00
|
|
|
func newId() string {
|
2024-01-11 21:13:25 +01:00
|
|
|
b := make([]byte, idCipher.BlockSize())
|
2023-07-10 16:24:30 +02:00
|
|
|
crand.Read(b)
|
|
|
|
return base64.RawURLEncoding.EncodeToString(b)
|
|
|
|
}
|
|
|
|
|
2024-01-11 21:13:25 +01:00
|
|
|
// we obfuscate ids to avoid exposing the WHIP session URL
|
|
|
|
func obfuscate(id string) (string, error) {
|
|
|
|
v, err := base64.RawURLEncoding.DecodeString(id)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(v) != idCipher.BlockSize() {
|
|
|
|
return "", errors.New("bad length")
|
|
|
|
}
|
|
|
|
|
|
|
|
idCipher.Encrypt(v, v)
|
|
|
|
|
|
|
|
return base64.RawURLEncoding.EncodeToString(v), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func deobfuscate(id string) (string, error) {
|
|
|
|
v, err := base64.RawURLEncoding.DecodeString(id)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(v) != idCipher.BlockSize() {
|
|
|
|
return "", errors.New("bad length")
|
|
|
|
}
|
|
|
|
|
|
|
|
idCipher.Decrypt(v, v)
|
|
|
|
|
|
|
|
return base64.RawURLEncoding.EncodeToString(v), nil
|
|
|
|
}
|
|
|
|
|
2023-07-10 16:24:30 +02:00
|
|
|
func canPresent(perms []string) bool {
|
|
|
|
for _, p := range perms {
|
|
|
|
if p == "present" {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2023-12-09 20:46:45 +01:00
|
|
|
func parseBearerToken(auth string) string {
|
2023-07-10 16:24:30 +02:00
|
|
|
auths := strings.Split(auth, ",")
|
|
|
|
for _, a := range auths {
|
|
|
|
a = strings.Trim(a, " \t")
|
|
|
|
s := strings.Split(a, " ")
|
|
|
|
if len(s) == 2 && strings.EqualFold(s[0], "bearer") {
|
|
|
|
return s[1]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
var iceServerReplacer = strings.NewReplacer(`\`, `\\`, `"`, `\"`)
|
|
|
|
|
|
|
|
func formatICEServer(server webrtc.ICEServer, u string) string {
|
|
|
|
quote := func(s string) string {
|
|
|
|
return iceServerReplacer.Replace(s)
|
|
|
|
}
|
|
|
|
uu, err := url.Parse(u)
|
|
|
|
if err != nil {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
if strings.EqualFold(uu.Scheme, "stun") {
|
|
|
|
return fmt.Sprintf("<%v>; rel=\"ice-server\"", u)
|
|
|
|
} else if strings.EqualFold(uu.Scheme, "turn") ||
|
|
|
|
strings.EqualFold(uu.Scheme, "turns") {
|
|
|
|
pw, ok := server.Credential.(string)
|
|
|
|
if !ok {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
return fmt.Sprintf("<%v>; rel=\"ice-server\"; "+
|
|
|
|
"username=\"%v\"; "+
|
|
|
|
"credential=\"%v\"; "+
|
|
|
|
"credential-type=\"%v\"",
|
|
|
|
u,
|
|
|
|
quote(server.Username),
|
|
|
|
quote(pw),
|
|
|
|
quote(server.CredentialType.String()))
|
|
|
|
}
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
func whipICEServers(w http.ResponseWriter) {
|
|
|
|
conf := ice.ICEConfiguration()
|
|
|
|
for _, server := range conf.ICEServers {
|
|
|
|
for _, u := range server.URLs {
|
|
|
|
v := formatICEServer(server, u)
|
|
|
|
if v != "" {
|
|
|
|
w.Header().Add("Link", v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-09 15:51:35 +01:00
|
|
|
const sdpLimit = 1024 * 1024
|
|
|
|
|
2023-07-10 16:24:30 +02:00
|
|
|
func whipEndpointHandler(w http.ResponseWriter, r *http.Request) {
|
|
|
|
if redirect(w, r) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-01-17 22:12:22 +01:00
|
|
|
pth, kind, pthid := splitPath(r.URL.Path)
|
2024-02-22 23:31:03 +01:00
|
|
|
if kind != ".whip" || pthid != "" {
|
2023-07-10 16:24:30 +02:00
|
|
|
http.Error(w, "Internal server error",
|
|
|
|
http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
name := parseGroupName("/group/", pth)
|
|
|
|
if name == "" {
|
|
|
|
notFound(w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
g, err := group.Add(name, nil)
|
|
|
|
if err != nil {
|
2024-01-18 01:02:56 +01:00
|
|
|
httpError(w, err)
|
2023-07-10 16:24:30 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
conf, err := group.GetConfiguration()
|
|
|
|
if err != nil {
|
2024-01-18 01:02:56 +01:00
|
|
|
httpError(w, err)
|
2023-07-10 16:24:30 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if conf.PublicServer {
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Method == "OPTIONS" {
|
|
|
|
w.Header().Set("Access-Control-Allow-Methods", "OPTIONS, POST")
|
|
|
|
w.Header().Set("Access-Control-Allow-Headers",
|
|
|
|
"Authorization, Content-Type",
|
|
|
|
)
|
|
|
|
w.Header().Set("Access-Control-Expose-Headers", "Link")
|
|
|
|
whipICEServers(w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Method != "POST" {
|
|
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
ctype := r.Header.Get("content-type")
|
|
|
|
if !strings.EqualFold(ctype, "application/sdp") {
|
|
|
|
http.Error(w, "bad content type", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-12-09 15:51:35 +01:00
|
|
|
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, sdpLimit))
|
2023-07-10 16:24:30 +02:00
|
|
|
if err != nil {
|
|
|
|
httpError(w, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-12-09 20:46:45 +01:00
|
|
|
token := parseBearerToken(r.Header.Get("Authorization"))
|
|
|
|
|
2023-07-10 16:24:30 +02:00
|
|
|
whip := "whip"
|
|
|
|
creds := group.ClientCredentials{
|
|
|
|
Username: &whip,
|
|
|
|
Token: token,
|
|
|
|
}
|
|
|
|
|
|
|
|
id := newId()
|
2024-01-11 21:13:25 +01:00
|
|
|
obfuscated, err := obfuscate(id)
|
|
|
|
if err != nil {
|
2024-01-18 01:02:56 +01:00
|
|
|
httpError(w, err)
|
2024-01-11 21:13:25 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-07-10 16:24:30 +02:00
|
|
|
c := rtpconn.NewWhipClient(g, id, token)
|
|
|
|
|
|
|
|
_, err = group.AddClient(g.Name(), c, creds)
|
|
|
|
if err == group.ErrNotAuthorised ||
|
|
|
|
err == group.ErrAnonymousNotAuthorised {
|
|
|
|
http.Error(w, "Authentication failed", http.StatusUnauthorized)
|
|
|
|
return
|
|
|
|
} else if err != nil {
|
|
|
|
log.Printf("WHIP: %v", err)
|
2024-01-18 01:02:56 +01:00
|
|
|
httpError(w, err)
|
2023-07-10 16:24:30 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if !canPresent(c.Permissions()) {
|
|
|
|
group.DelClient(c)
|
|
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
answer, err := c.NewConnection(r.Context(), body)
|
|
|
|
if err != nil {
|
|
|
|
group.DelClient(c)
|
|
|
|
log.Printf("WHIP offer: %v", err)
|
2024-01-18 01:02:56 +01:00
|
|
|
httpError(w, err)
|
|
|
|
return
|
2023-07-10 16:24:30 +02:00
|
|
|
}
|
|
|
|
|
2024-01-11 21:13:25 +01:00
|
|
|
w.Header().Set("Location", path.Join(r.URL.Path, obfuscated))
|
2023-07-10 16:24:30 +02:00
|
|
|
w.Header().Set("Access-Control-Expose-Headers",
|
|
|
|
"Location, Content-Type, Link")
|
|
|
|
whipICEServers(w)
|
|
|
|
w.Header().Set("Content-Type", "application/sdp")
|
|
|
|
w.WriteHeader(http.StatusCreated)
|
|
|
|
w.Write(answer)
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func whipResourceHandler(w http.ResponseWriter, r *http.Request) {
|
2024-01-17 22:12:22 +01:00
|
|
|
pth, kind, rest := splitPath(r.URL.Path)
|
|
|
|
if kind != ".whip" || rest == "" {
|
2024-01-11 21:13:25 +01:00
|
|
|
http.Error(w, "Internal server error",
|
|
|
|
http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
2024-01-17 22:12:22 +01:00
|
|
|
id, err := deobfuscate(rest[1:])
|
2024-01-11 21:13:25 +01:00
|
|
|
if err != nil {
|
2024-01-18 01:02:56 +01:00
|
|
|
httpError(w, err)
|
2023-07-10 16:24:30 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
name := parseGroupName("/group/", pth)
|
|
|
|
if name == "" {
|
|
|
|
notFound(w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
g := group.Get(name)
|
|
|
|
if g == nil {
|
|
|
|
notFound(w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
cc := g.GetClient(id)
|
|
|
|
if cc == nil {
|
|
|
|
notFound(w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
c, ok := cc.(*rtpconn.WhipClient)
|
|
|
|
if !ok {
|
|
|
|
notFound(w)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if t := c.Token(); t != "" {
|
2023-12-09 20:46:45 +01:00
|
|
|
token := parseBearerToken(r.Header.Get("Authorization"))
|
2023-07-10 16:24:30 +02:00
|
|
|
if token != t {
|
|
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
conf, err := group.GetConfiguration()
|
|
|
|
if err != nil {
|
2024-01-18 01:02:56 +01:00
|
|
|
httpError(w, err)
|
2023-07-10 16:24:30 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if conf.PublicServer {
|
|
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Method == "OPTIONS" {
|
|
|
|
w.Header().Set("Access-Control-Allow-Methods",
|
|
|
|
"OPTIONS, PATCH, DELETE",
|
|
|
|
)
|
|
|
|
w.Header().Set("Access-Control-Allow-Headers",
|
|
|
|
"Authorization, Content-Type",
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Method == "DELETE" {
|
|
|
|
c.Close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Method != "PATCH" {
|
|
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
2023-12-20 00:40:30 +01:00
|
|
|
return
|
2023-07-10 16:24:30 +02:00
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
ctype := r.Header.Get("content-type")
|
|
|
|
if !strings.EqualFold(ctype, "application/trickle-ice-sdpfrag") {
|
|
|
|
http.Error(w, "bad content type", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-12-09 15:51:35 +01:00
|
|
|
body, err := io.ReadAll(http.MaxBytesReader(w, r.Body, sdpLimit))
|
2023-07-10 16:24:30 +02:00
|
|
|
if err != nil {
|
|
|
|
httpError(w, err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(body) < 2 {
|
|
|
|
http.Error(w, "SDP truncated", http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// RFC 8840
|
|
|
|
lines := bytes.Split(body, []byte{'\n'})
|
2023-12-20 01:42:11 +01:00
|
|
|
mLineIndex := -1
|
|
|
|
var mid, ufrag []byte
|
2023-07-10 16:24:30 +02:00
|
|
|
for _, l := range lines {
|
|
|
|
l = bytes.TrimRight(l, " \r")
|
|
|
|
if bytes.HasPrefix(l, []byte("a=ice-ufrag:")) {
|
|
|
|
ufrag = l[len("a=ice-ufrag:"):]
|
2023-12-20 01:42:11 +01:00
|
|
|
} else if bytes.HasPrefix(l, []byte("m=")) {
|
|
|
|
mLineIndex++
|
|
|
|
mid = nil
|
|
|
|
} else if bytes.HasPrefix(l, []byte("a=mid:")) {
|
|
|
|
mid = l[len("a=mid:"):]
|
2023-07-10 16:24:30 +02:00
|
|
|
} else if bytes.HasPrefix(l, []byte("a=candidate:")) {
|
2023-12-20 01:42:11 +01:00
|
|
|
init := webrtc.ICECandidateInit{
|
|
|
|
Candidate: string(l[2:]),
|
|
|
|
}
|
|
|
|
if len(mid) > 0 {
|
|
|
|
s := string(mid)
|
|
|
|
init.SDPMid = &s
|
|
|
|
}
|
|
|
|
if mLineIndex >= 0 {
|
|
|
|
i := uint16(mLineIndex)
|
|
|
|
init.SDPMLineIndex = &i
|
|
|
|
}
|
|
|
|
if len(ufrag) > 0 {
|
|
|
|
s := string(ufrag)
|
|
|
|
init.UsernameFragment = &s
|
|
|
|
}
|
|
|
|
err := c.GotICECandidate(init)
|
2023-07-10 16:24:30 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Printf("WHIP candidate: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
|
|
return
|
|
|
|
}
|