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(®Req); 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(®Req); 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, }) }