Files
mediamtx/internal/api/api_recordings.go
T

155 lines
3.3 KiB
Go

package api //nolint:revive
import (
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/bluenviron/mediamtx/internal/conf"
"github.com/bluenviron/mediamtx/internal/defs"
"github.com/bluenviron/mediamtx/internal/recordstore"
"github.com/gin-gonic/gin"
)
// this prevents directory traversal.
// functionally it's useless since there's already conf.IsValidPathName, but it's needed by CodeQL.
func isSubdirectoryOf(base string, path string) error {
baseAbs, err := filepath.Abs(filepath.Clean(base))
if err != nil {
return err
}
candidateAbs, err := filepath.Abs(filepath.Join(baseAbs, path))
if err != nil {
return err
}
if !strings.HasPrefix(candidateAbs, baseAbs) {
return fmt.Errorf("path escapes base directory")
}
return nil
}
func recordingsOfPath(
pathConf *conf.Path,
pathName string,
) *defs.APIRecording {
ret := &defs.APIRecording{
Name: pathName,
}
segments, _ := recordstore.FindSegments(pathConf, pathName, nil, nil)
ret.Segments = make([]defs.APIRecordingSegment, len(segments))
for i, seg := range segments {
ret.Segments[i] = defs.APIRecordingSegment{
Start: seg.Start,
}
}
return ret
}
func (a *API) onRecordingsList(ctx *gin.Context) {
a.mutex.RLock()
c := a.Conf
a.mutex.RUnlock()
pathNames := recordstore.FindAllPathsWithSegments(c.Paths)
data := defs.APIRecordingList{}
data.ItemCount = len(pathNames)
pageCount, err := paginate(&pathNames, ctx.Query("itemsPerPage"), ctx.Query("page"))
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
}
data.PageCount = pageCount
data.Items = make([]defs.APIRecording, len(pathNames))
for i, pathName := range pathNames {
pathConf, _, _ := conf.FindPathConf(c.Paths, pathName)
data.Items[i] = *recordingsOfPath(pathConf, pathName)
}
ctx.JSON(http.StatusOK, data)
}
func (a *API) onRecordingsGet(ctx *gin.Context) {
pathName, ok := paramName(ctx)
if !ok {
a.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid name"))
return
}
a.mutex.RLock()
c := a.Conf
a.mutex.RUnlock()
pathConf, _, err := conf.FindPathConf(c.Paths, pathName)
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
}
ctx.JSON(http.StatusOK, recordingsOfPath(pathConf, pathName))
}
func (a *API) onRecordingDeleteSegment(ctx *gin.Context) {
pathName := ctx.Query("path")
start, err := time.Parse(time.RFC3339, ctx.Query("start"))
if err != nil {
a.writeError(ctx, http.StatusBadRequest, fmt.Errorf("invalid 'start' parameter: %w", err))
return
}
a.mutex.RLock()
c := a.Conf
a.mutex.RUnlock()
pathConf, _, err := conf.FindPathConf(c.Paths, pathName)
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
}
commonPath := recordstore.CommonPath(pathConf.RecordPath)
pathFormat := recordstore.PathAddExtension(
strings.ReplaceAll(pathConf.RecordPath, "%path", pathName),
pathConf.RecordFormat,
)
err = isSubdirectoryOf(commonPath, pathFormat)
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
}
segmentPath := recordstore.Path{
Start: start,
}.Encode(pathFormat)
err = isSubdirectoryOf(pathFormat, segmentPath)
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
}
err = os.Remove(segmentPath)
if err != nil {
a.writeError(ctx, http.StatusBadRequest, err)
return
}
a.writeOK(ctx)
}