2025-04-14 11:51:02 +05:00

550 lines
17 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handlers
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"git.insit.tech/psa/rtsp_reader-writer/reader/internal/config"
"git.insit.tech/psa/rtsp_reader-writer/reader/internal/processor"
"git.insit.tech/psa/rtsp_reader-writer/writer/pkg/storage"
jwtware "github.com/gofiber/contrib/jwt"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/sirupsen/logrus"
)
type VideoRequest struct {
Date string `json:"date"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
}
type ListVodsResponse struct {
EstimatedCount int `json:"estimated_count"` // Estimated total number of records for the query.
Vods []string `json:"vods"` // List of vods.
Files map[string][]string `json:"files"` // List files and folders of a specific VOD location.
}
type ConfigVodsResponse struct {
Prefix string `json:"prefix"` // The unique name of VOD location.
AutoMbr bool `json:"auto_mbr"` // Turns on automatic creation of a multi-bitrate HLS playlist from several files with different bitrates.
Disabled bool `json:"disabled"` // Whether this VOD location is disabled.
Protocols struct { // Configuraton of play protocols.
Hls bool `json:"hls"` // Whether to allow or deny an HLS stream playback.
Cmaf bool `json:"cmaf"` // Whether to allow or deny an LL-HLS stream playback.
Dash bool `json:"dash"` // Whether to allow or deny a DASH stream playback.
Player bool `json:"player"` // Whether to allow or deny playback in embed.html.
Mss bool `json:"mss"` // Whether to allow or deny an MSS stream playback.
Rtmp bool `json:"rtmp"` // Whether to allow or deny an RTMP stream playback.
Rtsp bool `json:"rtsp"` // Whether to allow or deny an RTSP stream playback.
M4F bool `json:"m4f"` // Whether to allow or deny an M4F stream playback.
M4S bool `json:"m4s"` // Whether to allow or deny an M4S stream playback.
Mseld bool `json:"mseld"` // Whether to allow or deny an MSE-LD stream playback.
Tshttp bool `json:"tshttp"` // Whether to allow or deny an MPEG-TS stream playback over HTTP(S).
Webrtc bool `json:"webrtc"` // Whether to allow or deny an WebRTC stream playback.
Srt bool `json:"srt"` // Whether to allow or deny an SRT stream playback.
Shoutcast bool `json:"shoutcast"` // Whether to allow or deny a SHOUTcast/Icecast stream playback.
Mp4 bool `json:"mp4"` // Whether to allow or deny an MP4 file download over HTTP(S).
Jpeg bool `json:"jpeg"` // Whether to allow or deny delivering JPEG thumbnails over HTTP(S).
Api bool `json:"api"` // Whether to allow or deny API requests.
} `json:"protocols"`
SegmentDuration int `json:"segment_duration"` // The time, in seconds, of the segment duration. Used for the protocols like HLS or DASH.
AddAudioOnly bool `json:"add_audio_only"` // Whether to add an audio-only version of an HLS stream. Used to create App Store compliant HLS streams to deliver the content to Apple iOS devices. Add audio-only HLS playlist to variant MBR playlist for iOS compliant streaming.
Provider string `json:"provider"` // Human-readable name of the content provider. Applicable to MPEG-TS.
}
type SingleVodsResponse struct {
Name string `json:"name"`
Prefix string `json:"prefix"`
Url string `json:"url"`
Folder string `json:"folder"`
Bytes int64 `json:"bytes"`
MediaInfo MediaSingleVodsResponse `json:"media_info"`
}
type MediaSingleVodsResponse struct {
Tracks map[string]string `json:"tracks"`
Duration int `json:"duration"`
Provider string `json:"provider"`
Title string `json:"title"`
}
// Inactive5Minutes checks if a folder has files that was not created or modified for last 5 minutes.
func Inactive5Minutes(entries []os.DirEntry) (bool, error) {
threshold := time.Now().Add(-5 * time.Minute)
disabled := true
for _, entry := range entries {
info, err := entry.Info()
if err != nil {
return true, errors.New("Info error: " + err.Error())
}
if info.ModTime().After(threshold) {
disabled = false
return disabled, nil
}
}
return true, nil
}
// recDurationMilliseconds calculates the difference between first existing rec file and the last one in milliseconds.
func recDurationMilliseconds(entries []os.DirEntry) (int, error) {
// Find last file and first file; get timestamps.
lastFile := entries[len(entries)-1].Name()
lastTime := strings.Split(lastFile, "_")[0]
firstFile := entries[0].Name()
firstTime := strings.Split(firstFile, "_")[0]
// Convert string timestamps to int.
lastTimeInt, err := strconv.Atoi(lastTime)
if err != nil {
return 0, errors.New("convert last time error: " + err.Error())
}
firstTimeInt, err := strconv.Atoi(firstTime)
if err != nil {
return 0, errors.New("convert first time error: " + err.Error())
}
// Calculate the difference.
difference := lastTimeInt - firstTimeInt
return difference * 1000, nil
}
// Download processes Download request.
func Download(w http.ResponseWriter, r *http.Request) {
log.Printf("new download request: %+v\n", r)
downloadRequest := VideoRequest{}
err := json.NewDecoder(r.Body).Decode(&downloadRequest)
if err != nil {
log.Printf("json decode error: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
pathFileNameRes, err := processor.Process(downloadRequest.Date, downloadRequest.StartTime, downloadRequest.EndTime)
if err != nil {
log.Printf("process error: %v\n", err)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "video/mp4")
// Разрешаем частичную загрузку (поддержка перемотки)
w.Header().Set("Accept-Ranges", "bytes")
http.ServeFile(w, r, pathFileNameRes)
}
// HLS processes Download request.
func HLS(w http.ResponseWriter, r *http.Request) {
log.Printf("new hls request: %+v\n", r)
path := "/home/psa/GoRepository/data/1280x720/"
w.Header().Set("Access-Control-Allow-Origin", "*")
http.StripPrefix("/hls", http.FileServer(http.Dir(path))).ServeHTTP(w, r)
}
// ListVodsHandler returns the list of VOD locations.
//
// This method allows to get the list of all VOD locations. VOD location is a virtual filepath used to place files for
// VOD (Video on Demand) broadcasting.
func ListVodsHandler(c *fiber.Ctx) error {
// Read directory.
entries, err := os.ReadDir(config.DirData)
if err != nil {
log.Println("StatusInternalServerError")
return c.SendStatus(fiber.StatusInternalServerError)
}
// Filter only folders.
var dirs []string
for _, entry := range entries {
if entry.IsDir() {
dirs = append(dirs, entry.Name())
}
}
// Prepare the Response.
VodsRes := ListVodsResponse{
EstimatedCount: len(dirs),
Vods: dirs,
}
// Write header and code response.
return c.JSON(VodsRes)
}
// ConfigVodsHandler returns configuration of the requested VOD location.
//
// This method allows to get a single VOD location.
func ConfigVodsHandler(c *fiber.Ctx) error {
// Read camera id.
id := c.Params("id")
// Get resolutions.
resolutions, err := storage.GetResolutions(id)
if err != nil {
return c.SendStatus(fiber.StatusBadRequest)
}
// Calculate response fields.
// Create path to first resolution.
resDir := fmt.Sprintf("%s/%s/%s", config.DirData, id, resolutions[0])
// Read directory.
entries, err := os.ReadDir(resDir)
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
// Check if a folder has files that was not created or modified for last 5 minutes.
disabled, err := Inactive5Minutes(entries)
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
// Calculate the difference between first existing rec file and the last one in milliseconds.
segmentDuration, err := recDurationMilliseconds(entries)
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
// Prepare the Response.
VodsRes := ConfigVodsResponse{
Prefix: id,
AutoMbr: false, // Always false. Temporary.
Disabled: disabled,
SegmentDuration: segmentDuration,
AddAudioOnly: false, // Always false. Temporary.
Provider: "Insit",
}
// Write header and code response.
return c.JSON(VodsRes)
}
// DelVodsHandler delete archive of the requested VOD location.
//
// This method delete a single VOD location by its prefix.
func DelVodsHandler(c *fiber.Ctx) error {
// Read camera id.
id := c.Params("id")
err := os.Remove(fmt.Sprintf("%s/%s", config.DirData, id))
if err != nil {
return c.SendStatus(fiber.StatusBadRequest)
}
return c.SendStatus(fiber.StatusNoContent)
}
// ListFilesVodsHandler returns the list of all files and folders in archive for a specific VOD location.
//
// This method allows to get the list of all files and folders for a specific VOD location.
func ListFilesVodsHandler(c *fiber.Ctx) error {
// Read camera id.
id := c.Params("id")
// Create map for response.
files := make(map[string][]string)
// Get resolutions.
resolutions, err := storage.GetResolutions(id)
if err != nil {
return c.SendStatus(fiber.StatusBadRequest)
}
for _, resolution := range resolutions {
// Create path to the resolutions.
resDir := fmt.Sprintf("%s/%s/%s", config.DirData, id, resolution)
// Read directory.
entries, err := os.ReadDir(resDir)
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
// Create slice for files in folders.
filesInFolder := make([]string, 0)
// Add all files to the slice with files.
for _, entry := range entries {
filesInFolder = append(filesInFolder, entry.Name())
}
// Add resolution and all files to the response map.
files[resolution] = filesInFolder
}
// Prepare the Response.
vodsRes := ListVodsResponse{
EstimatedCount: len(files),
Files: files,
}
// Write header and code response.
return c.JSON(vodsRes)
}
// SingleVodsHandler returns a specific file in archive for a specific resolution and VOD location.
//
// This method allows to get a single VOD file.
func SingleVodsHandler(c *fiber.Ctx) error {
// Read camera id, res, filename.
id := c.Params("id")
res := c.Params("res")
file := c.Params("file")
// Calculate file size in bytes.
FileBytes, err := storage.FileBytes(id, res, file)
if err != nil {
return c.SendStatus(fiber.StatusBadRequest)
}
dur, tracks, err := storage.GetDurAndTracks(id, res, file)
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
media := MediaSingleVodsResponse{
Tracks: tracks,
Duration: dur,
Provider: "Insit",
Title: id,
}
// Prepare the Response.
vodsRes := SingleVodsResponse{
Name: file,
Prefix: id,
Url: c.OriginalURL(),
Folder: res,
Bytes: FileBytes,
MediaInfo: media,
}
// Write header and code response.
return c.JSON(vodsRes)
}
// DelSingleVodsHandler deletes a VOD file by its name.
//
// This method deletes a VOD file by its name.
func DelSingleVodsHandler(c *fiber.Ctx) error {
// Read camera id, res, filename.
id := c.Params("id")
res := c.Params("res")
file := c.Params("file")
if err := os.Remove(fmt.Sprintf("%s/%s/%s/%s", config.DirData, id, res, file)); err != nil {
return c.SendStatus(fiber.StatusNotFound)
}
return c.SendStatus(fiber.StatusNoContent)
}
// //////////////////////////////////////////////////////////////////////////
// //////////////////////////////////////////////////////////////////////////
const (
ContextKeyUser = "user"
)
func Auth() {
app := fiber.New()
authStorage := &AuthStorage{map[string]User{}}
authHandler := &AuthHandler{Storage: authStorage}
userHandler := &UserHandler{Storage: authStorage}
// Группа обработчиков, которые доступны неавторизованным пользователям
publicGroup := app.Group("")
publicGroup.Post("/register", authHandler.Register)
publicGroup.Post("/login", authHandler.Login)
// Группа обработчиков, которые требуют авторизации
authorizedGroup := app.Group("")
authorizedGroup.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{
Key: JwtSecretKey,
},
ContextKey: ContextKeyUser,
}))
authorizedGroup.Get("/profile", userHandler.Profile)
logrus.Fatal(app.Listen(":8100"))
}
type (
// Обработчик HTTP-запросов на регистрацию и аутентификацию пользователей
AuthHandler struct {
Storage *AuthStorage
}
// Хранилище зарегистрированных пользователей
// Данные хранятся в оперативной памяти
AuthStorage struct {
Users map[string]User
}
// Структура данных с информацией о пользователе
User struct {
Email string
Name string
password string
}
)
// Структура HTTP-запроса на регистрацию пользователя
type RegisterRequest struct {
Email string `json:"email"`
Name string `json:"name"`
Password string `json:"password"`
}
// Обработчик HTTP-запросов на регистрацию пользователя
func (h *AuthHandler) Register(c *fiber.Ctx) error {
regReq := RegisterRequest{}
if err := c.BodyParser(&regReq); err != nil {
return fmt.Errorf("body parser: %w", err)
}
// Проверяем, что пользователь с таким email еще не зарегистрирован
if _, exists := h.Storage.Users[regReq.Email]; exists {
return errors.New("the user already exists")
}
// Сохраняем в память нового зарегистрированного пользователя
h.Storage.Users[regReq.Email] = User{
Email: regReq.Email,
Name: regReq.Name,
password: regReq.Password,
}
return c.SendStatus(fiber.StatusCreated)
}
// Структура HTTP-запроса на вход в аккаунт
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
// Структура HTTP-ответа на вход в аккаунт
// В ответе содержится JWT-токен авторизованного пользователя
type LoginResponse struct {
AccessToken string `json:"access_token"`
}
var (
errBadCredentials = errors.New("email or password is incorrect")
)
// Секретный ключ для подписи JWT-токена
// Необходимо хранить в безопасном месте
var JwtSecretKey = []byte("very-secret-key")
// Обработчик HTTP-запросов на вход в аккаунт
func (h *AuthHandler) Login(c *fiber.Ctx) error {
regReq := LoginRequest{}
if err := c.BodyParser(&regReq); err != nil {
return fmt.Errorf("body parser: %w", err)
}
// Ищем пользователя в памяти приложения по электронной почте
user, exists := h.Storage.Users[regReq.Email]
// Если пользователь не найден, возвращаем ошибку
if !exists {
return errBadCredentials
}
// Если пользователь найден, но у него другой пароль, возвращаем ошибку
if user.password != regReq.Password {
return errBadCredentials
}
// Генерируем JWT-токен для пользователя,
// который он будет использовать в будущих HTTP-запросах
// Генерируем полезные данные, которые будут храниться в токене
payload := jwt.MapClaims{
"sub": user.Email,
"exp": time.Now().Add(time.Hour * 72).Unix(),
}
// Создаем новый JWT-токен и подписываем его по алгоритму HS256
token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload)
t, err := token.SignedString(JwtSecretKey)
if err != nil {
logrus.WithError(err).Error("JWT token signing")
return c.SendStatus(fiber.StatusInternalServerError)
}
return c.JSON(LoginResponse{AccessToken: t})
}
// Обработчик HTTP-запросов, которые связаны с пользователем
type UserHandler struct {
Storage *AuthStorage
}
// Структура HTTP-ответа с информацией о пользователе
type ProfileResponse struct {
Email string `json:"email"`
Name string `json:"name"`
}
func jwtPayloadFromRequest(c *fiber.Ctx) (jwt.MapClaims, bool) {
jwtToken, ok := c.Context().Value(ContextKeyUser).(*jwt.Token)
if !ok {
logrus.WithFields(logrus.Fields{
"jwt_token_context_value": c.Context().Value(ContextKeyUser),
}).Error("wrong type of JWT token in context")
return nil, false
}
payload, ok := jwtToken.Claims.(jwt.MapClaims)
if !ok {
logrus.WithFields(logrus.Fields{
"jwt_token_claims": jwtToken.Claims,
}).Error("wrong type of JWT token claims")
return nil, false
}
return payload, true
}
// Обработчик HTTP-запросов на получение информации о пользователе
func (h *UserHandler) Profile(c *fiber.Ctx) error {
jwtPayload, ok := jwtPayloadFromRequest(c)
if !ok {
return c.SendStatus(fiber.StatusUnauthorized)
}
userInfo, ok := h.Storage.Users[jwtPayload["sub"].(string)]
if !ok {
return errors.New("user not found")
}
return c.JSON(ProfileResponse{
Email: userInfo.Email,
Name: userInfo.Name,
})
}