package hls import ( _ "embed" "errors" "fmt" "net" "net/http" gopath "path" "strings" "time" "github.com/gin-gonic/gin" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/httpserv" "github.com/bluenviron/mediamtx/internal/restrictnetwork" ) const ( hlsPauseAfterAuthError = 2 * time.Second ) //go:embed index.html var hlsIndex []byte //go:embed hls.min.js var hlsMinJS []byte type httpServer struct { address string encryption bool serverKey string serverCert string allowOrigin string trustedProxies conf.IPsOrCIDRs readTimeout conf.StringDuration pathManager defs.PathManager parent *Server inner *httpserv.WrappedServer } func (s *httpServer) initialize() error { if s.encryption { if s.serverCert == "" { return fmt.Errorf("server cert is missing") } } else { s.serverKey = "" s.serverCert = "" } router := gin.New() router.SetTrustedProxies(s.trustedProxies.ToTrustedProxies()) //nolint:errcheck router.NoRoute(s.onRequest) network, address := restrictnetwork.Restrict("tcp", s.address) var err error s.inner, err = httpserv.NewWrappedServer( network, address, time.Duration(s.readTimeout), s.serverCert, s.serverKey, router, s, ) 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) onRequest(ctx *gin.Context) { ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin) ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") switch ctx.Request.Method { case http.MethodOptions: ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Range") ctx.Writer.WriteHeader(http.StatusNoContent) return case http.MethodGet: default: 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.Writer.Header().Set("Cache-Control", "max-age=3600") ctx.Writer.Header().Set("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.Writer.Header().Set("Location", httpserv.LocationWithTrailingSlash(ctx.Request.URL)) ctx.Writer.WriteHeader(http.StatusMovedPermanently) return } } dir = strings.TrimSuffix(dir, "/") if dir == "" { return } user, pass, hasCredentials := ctx.Request.BasicAuth() res := s.pathManager.GetConfForPath(defs.PathGetConfForPathReq{ AccessRequest: defs.PathAccessRequest{ Name: dir, Query: ctx.Request.URL.RawQuery, Publish: false, IP: net.ParseIP(ctx.ClientIP()), User: user, Pass: pass, Proto: defs.AuthProtocolHLS, }, }) if res.Err != nil { var terr defs.AuthenticationError if errors.As(res.Err, &terr) { if !hasCredentials { ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) ctx.Writer.WriteHeader(http.StatusUnauthorized) return } ip := ctx.ClientIP() _, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr) remoteAddr := net.JoinHostPort(ip, port) s.Log(logger.Info, "connection %v failed to authenticate: %v", remoteAddr, terr.Message) // wait some seconds to stop brute force attacks <-time.After(hlsPauseAfterAuthError) ctx.Writer.WriteHeader(http.StatusUnauthorized) return } ctx.Writer.WriteHeader(http.StatusNotFound) return } switch fname { case "": ctx.Writer.Header().Set("Cache-Control", "max-age=3600") ctx.Writer.Header().Set("Content-Type", "text/html") ctx.Writer.WriteHeader(http.StatusOK) ctx.Writer.Write(hlsIndex) default: s.parent.handleRequest(muxerHandleRequestReq{ path: dir, file: fname, ctx: ctx, }) } }