Ready-to-use SRT / WebRTC / RTSP / RTMP / LL-HLS media server and media proxy that allows to read, publish, proxy, record and playback video and audio streams.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

306 lines
7.6 KiB

package core
import (
_ "embed"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/pion/webrtc/v3"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/httpserv"
"github.com/bluenviron/mediamtx/internal/logger"
"github.com/bluenviron/mediamtx/internal/whip"
)
//go:embed webrtc_publish_index.html
var webrtcPublishIndex []byte
//go:embed webrtc_read_index.html
var webrtcReadIndex []byte
type webRTCHTTPServerParent interface {
logger.Writer
generateICEServers() ([]webrtc.ICEServer, error)
newSession(req webRTCNewSessionReq) webRTCNewSessionRes
addSessionCandidates(req webRTCAddSessionCandidatesReq) webRTCAddSessionCandidatesRes
}
type webRTCHTTPServer struct {
allowOrigin string
pathManager *pathManager
parent webRTCHTTPServerParent
inner *httpserv.WrappedServer
}
func newWebRTCHTTPServer( //nolint:dupl
address string,
encryption bool,
serverKey string,
serverCert string,
allowOrigin string,
trustedProxies conf.IPsOrCIDRs,
readTimeout conf.StringDuration,
pathManager *pathManager,
parent webRTCHTTPServerParent,
) (*webRTCHTTPServer, error) {
if encryption {
if serverCert == "" {
return nil, fmt.Errorf("server cert is missing")
}
} else {
serverKey = ""
serverCert = ""
}
s := &webRTCHTTPServer{
allowOrigin: allowOrigin,
pathManager: pathManager,
parent: parent,
}
router := gin.New()
router.SetTrustedProxies(trustedProxies.ToTrustedProxies())
router.NoRoute(httpserv.MiddlewareLogger(s), httpserv.MiddlewareServerHeader, s.onRequest)
network, address := restrictNetwork("tcp", address)
var err error
s.inner, err = httpserv.NewWrappedServer(
network,
address,
readTimeout,
serverCert,
serverKey,
router,
)
if err != nil {
return nil, err
}
return s, nil
}
func (s *webRTCHTTPServer) Log(level logger.Level, format string, args ...interface{}) {
s.parent.Log(level, format, args...)
}
func (s *webRTCHTTPServer) close() {
s.inner.Close()
}
func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) {
ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin)
ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
// remove leading prefix
pa := ctx.Request.URL.Path[1:]
isWHIPorWHEP := strings.HasSuffix(pa, "/whip") || strings.HasSuffix(pa, "/whep")
isPreflight := ctx.Request.Method == http.MethodOptions &&
ctx.Request.Header.Get("Access-Control-Request-Method") != ""
if !isWHIPorWHEP || isPreflight {
switch ctx.Request.Method {
case http.MethodOptions:
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, If-Match")
ctx.Writer.WriteHeader(http.StatusNoContent)
return
case http.MethodGet:
default:
return
}
}
var dir string
var fname string
var publish bool
switch {
case pa == "", pa == "favicon.ico":
return
case strings.HasSuffix(pa, "/publish"):
dir, fname = pa[:len(pa)-len("/publish")], "publish"
publish = true
case strings.HasSuffix(pa, "/whip"):
dir, fname = pa[:len(pa)-len("/whip")], "whip"
publish = true
case strings.HasSuffix(pa, "/whep"):
dir, fname = pa[:len(pa)-len("/whep")], "whep"
publish = false
default:
dir, fname = pa, ""
publish = false
if !strings.HasSuffix(dir, "/") {
l := "/" + dir + "/"
if ctx.Request.URL.RawQuery != "" {
l += "?" + ctx.Request.URL.RawQuery
}
ctx.Writer.Header().Set("Location", l)
ctx.Writer.WriteHeader(http.StatusMovedPermanently)
return
}
}
dir = strings.TrimSuffix(dir, "/")
if dir == "" {
return
}
ip := ctx.ClientIP()
_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr)
remoteAddr := net.JoinHostPort(ip, port)
user, pass, hasCredentials := ctx.Request.BasicAuth()
// if request doesn't belong to a session, check authentication here
if !isWHIPorWHEP || ctx.Request.Method == http.MethodOptions {
res := s.pathManager.getConfForPath(pathGetConfForPathReq{
name: dir,
publish: publish,
credentials: authCredentials{
query: ctx.Request.URL.RawQuery,
ip: net.ParseIP(ip),
user: user,
pass: pass,
proto: authProtocolWebRTC,
},
})
if res.err != nil {
if terr, ok := res.err.(*errAuthentication); ok {
if !hasCredentials {
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
ctx.Writer.WriteHeader(http.StatusUnauthorized)
return
}
s.Log(logger.Info, "connection %v failed to authenticate: %v", remoteAddr, terr.message)
// wait some seconds to stop brute force attacks
<-time.After(webrtcPauseAfterAuthError)
ctx.Writer.WriteHeader(http.StatusUnauthorized)
return
}
ctx.Writer.WriteHeader(http.StatusNotFound)
return
}
}
switch fname {
case "":
ctx.Writer.Header().Set("Content-Type", "text/html")
ctx.Writer.WriteHeader(http.StatusOK)
ctx.Writer.Write(webrtcReadIndex)
case "publish":
ctx.Writer.Header().Set("Content-Type", "text/html")
ctx.Writer.WriteHeader(http.StatusOK)
ctx.Writer.Write(webrtcPublishIndex)
case "whip", "whep":
switch ctx.Request.Method {
case http.MethodOptions:
servers, err := s.parent.generateICEServers()
if err != nil {
ctx.Writer.WriteHeader(http.StatusInternalServerError)
return
}
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, If-Match")
ctx.Writer.Header()["Link"] = whip.LinkHeaderMarshal(servers)
ctx.Writer.WriteHeader(http.StatusNoContent)
case http.MethodPost:
if ctx.Request.Header.Get("Content-Type") != "application/sdp" {
ctx.Writer.WriteHeader(http.StatusBadRequest)
return
}
offer, err := io.ReadAll(ctx.Request.Body)
if err != nil {
return
}
res := s.parent.newSession(webRTCNewSessionReq{
pathName: dir,
remoteAddr: remoteAddr,
query: ctx.Request.URL.RawQuery,
user: user,
pass: pass,
offer: offer,
publish: (fname == "whip"),
})
if res.err != nil {
ctx.Writer.WriteHeader(res.errStatusCode)
return
}
servers, err := s.parent.generateICEServers()
if err != nil {
ctx.Writer.WriteHeader(http.StatusInternalServerError)
return
}
ctx.Writer.Header().Set("Content-Type", "application/sdp")
ctx.Writer.Header().Set("Access-Control-Expose-Headers", "E-Tag, Accept-Patch, Link")
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"] = whip.LinkHeaderMarshal(servers)
ctx.Writer.Header().Set("Location", ctx.Request.URL.String())
ctx.Writer.WriteHeader(http.StatusCreated)
ctx.Writer.Write(res.answer)
case http.MethodPatch:
secret, err := uuid.Parse(ctx.Request.Header.Get("If-Match"))
if err != nil {
ctx.Writer.WriteHeader(http.StatusBadRequest)
return
}
if ctx.Request.Header.Get("Content-Type") != "application/trickle-ice-sdpfrag" {
ctx.Writer.WriteHeader(http.StatusBadRequest)
return
}
byts, err := io.ReadAll(ctx.Request.Body)
if err != nil {
return
}
candidates, err := whip.ICEFragmentUnmarshal(byts)
if err != nil {
ctx.Writer.WriteHeader(http.StatusBadRequest)
return
}
res := s.parent.addSessionCandidates(webRTCAddSessionCandidatesReq{
secret: secret,
candidates: candidates,
})
if res.err != nil {
ctx.Writer.WriteHeader(http.StatusBadRequest)
return
}
ctx.Writer.WriteHeader(http.StatusNoContent)
}
}
}