mirror of
https://github.com/aler9/rtsp-simple-server
synced 2025-09-26 19:51:26 +08:00
208 lines
4.7 KiB
Go
208 lines
4.7 KiB
Go
package hls
|
|
|
|
import (
|
|
_ "embed"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
gopath "path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/bluenviron/mediamtx/internal/auth"
|
|
"github.com/bluenviron/mediamtx/internal/conf"
|
|
"github.com/bluenviron/mediamtx/internal/defs"
|
|
"github.com/bluenviron/mediamtx/internal/logger"
|
|
"github.com/bluenviron/mediamtx/internal/protocols/httpp"
|
|
)
|
|
|
|
//go:generate go run ./hlsjsdownloader
|
|
|
|
//go:embed index.html
|
|
var hlsIndex []byte
|
|
|
|
//go:embed hls.min.js
|
|
var hlsMinJS []byte
|
|
|
|
func mergePathAndQuery(path string, rawQuery string) string {
|
|
res := path
|
|
if rawQuery != "" {
|
|
res += "?" + rawQuery
|
|
}
|
|
return res
|
|
}
|
|
|
|
type httpServer struct {
|
|
address string
|
|
encryption bool
|
|
serverKey string
|
|
serverCert string
|
|
allowOrigin string
|
|
trustedProxies conf.IPNetworks
|
|
readTimeout conf.Duration
|
|
pathManager serverPathManager
|
|
parent *Server
|
|
|
|
inner *httpp.Server
|
|
}
|
|
|
|
func (s *httpServer) initialize() error {
|
|
router := gin.New()
|
|
router.SetTrustedProxies(s.trustedProxies.ToTrustedProxies()) //nolint:errcheck
|
|
|
|
router.Use(s.middlewareOrigin)
|
|
|
|
router.Use(s.onRequest)
|
|
|
|
s.inner = &httpp.Server{
|
|
Address: s.address,
|
|
ReadTimeout: time.Duration(s.readTimeout),
|
|
Encryption: s.encryption,
|
|
ServerCert: s.serverCert,
|
|
ServerKey: s.serverKey,
|
|
Handler: router,
|
|
Parent: s,
|
|
}
|
|
err := s.inner.Initialize()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Log implements logger.Writer.
|
|
func (s *httpServer) Log(level logger.Level, format string, args ...interface{}) {
|
|
s.parent.Log(level, format, args...)
|
|
}
|
|
|
|
func (s *httpServer) close() {
|
|
s.inner.Close()
|
|
}
|
|
|
|
func (s *httpServer) middlewareOrigin(ctx *gin.Context) {
|
|
ctx.Header("Access-Control-Allow-Origin", s.allowOrigin)
|
|
ctx.Header("Access-Control-Allow-Credentials", "true")
|
|
|
|
// preflight requests
|
|
if ctx.Request.Method == http.MethodOptions &&
|
|
ctx.Request.Header.Get("Access-Control-Request-Method") != "" {
|
|
ctx.Header("Access-Control-Allow-Methods", "OPTIONS, GET")
|
|
ctx.Header("Access-Control-Allow-Headers", "Authorization, Range")
|
|
ctx.AbortWithStatus(http.StatusNoContent)
|
|
return
|
|
}
|
|
}
|
|
|
|
func (s *httpServer) onRequest(ctx *gin.Context) {
|
|
if ctx.Request.Method != http.MethodGet {
|
|
return
|
|
}
|
|
|
|
// remove leading prefix
|
|
pa := ctx.Request.URL.Path[1:]
|
|
|
|
var dir string
|
|
var fname string
|
|
|
|
switch {
|
|
case strings.HasSuffix(pa, "/hls.min.js"):
|
|
ctx.Header("Cache-Control", "max-age=3600")
|
|
ctx.Header("Content-Type", "application/javascript")
|
|
ctx.Writer.WriteHeader(http.StatusOK)
|
|
ctx.Writer.Write(hlsMinJS)
|
|
return
|
|
|
|
case pa == "", pa == "favicon.ico", strings.HasSuffix(pa, "/hls.min.js.map"):
|
|
return
|
|
|
|
case strings.HasSuffix(pa, ".m3u8") ||
|
|
strings.HasSuffix(pa, ".ts") ||
|
|
strings.HasSuffix(pa, ".mp4") ||
|
|
strings.HasSuffix(pa, ".mp"):
|
|
dir, fname = gopath.Dir(pa), gopath.Base(pa)
|
|
|
|
if strings.HasSuffix(fname, ".mp") {
|
|
fname += "4"
|
|
}
|
|
|
|
default:
|
|
dir, fname = pa, ""
|
|
|
|
if !strings.HasSuffix(dir, "/") {
|
|
ctx.Header("Location", mergePathAndQuery(ctx.Request.URL.Path+"/", ctx.Request.URL.RawQuery))
|
|
ctx.Writer.WriteHeader(http.StatusMovedPermanently)
|
|
return
|
|
}
|
|
}
|
|
|
|
dir = strings.TrimSuffix(dir, "/")
|
|
if dir == "" {
|
|
return
|
|
}
|
|
|
|
pathConf, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{
|
|
AccessRequest: defs.PathAccessRequest{
|
|
Name: dir,
|
|
Query: ctx.Request.URL.RawQuery,
|
|
Publish: false,
|
|
Proto: auth.ProtocolHLS,
|
|
Credentials: httpp.Credentials(ctx.Request),
|
|
IP: net.ParseIP(ctx.ClientIP()),
|
|
},
|
|
})
|
|
if err != nil {
|
|
var terr *auth.Error
|
|
if errors.As(err, &terr) {
|
|
if terr.AskCredentials {
|
|
ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`)
|
|
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
s.Log(logger.Info, "connection %v failed to authenticate: %v", httpp.RemoteAddr(ctx), terr.Wrapped)
|
|
|
|
// wait some seconds to delay brute force attacks
|
|
<-time.After(auth.PauseAfterError)
|
|
|
|
ctx.Writer.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
ctx.Writer.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
switch fname {
|
|
case "":
|
|
ctx.Header("Cache-Control", "max-age=3600")
|
|
ctx.Header("Content-Type", "text/html")
|
|
ctx.Writer.WriteHeader(http.StatusOK)
|
|
ctx.Writer.Write(hlsIndex)
|
|
|
|
default:
|
|
var mux *muxer
|
|
mux, err = s.parent.getMuxer(serverGetMuxerReq{
|
|
path: dir,
|
|
remoteAddr: httpp.RemoteAddr(ctx),
|
|
query: ctx.Request.URL.RawQuery,
|
|
sourceOnDemand: pathConf.SourceOnDemand,
|
|
})
|
|
if err != nil {
|
|
ctx.Writer.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
mi := mux.getInstance()
|
|
if mi == nil {
|
|
ctx.Writer.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
ctx.Request.URL.Path = fname
|
|
mi.handleRequest(ctx)
|
|
}
|
|
}
|