diff --git a/reader/cmd/main.go b/reader/cmd/main.go index 0c3b5a8..eefca90 100644 --- a/reader/cmd/main.go +++ b/reader/cmd/main.go @@ -2,28 +2,17 @@ package main import ( "fmt" + "net/http" "os" "git.insit.tech/psa/rtsp_reader-writer/reader/internal/config" - "git.insit.tech/psa/rtsp_reader-writer/reader/internal/handlers" logger "git.insit.tech/psa/rtsp_reader-writer/reader/internal/log" "git.insit.tech/psa/rtsp_reader-writer/reader/internal/metrics" - jwtware "github.com/gofiber/contrib/jwt" - "github.com/gofiber/fiber/v2" - "github.com/sirupsen/logrus" + "git.insit.tech/psa/rtsp_reader-writer/reader/internal/web/handlers" + "git.insit.tech/psa/rtsp_reader-writer/reader/internal/web/middlewares" ) func main() { - //port := 8080 - // - //http.HandleFunc("GET /api/v1/download/", handlers.Download) // example request: {"date": "07-03-2025", "start_time": "16-43", "end_time": "16-44"} - //http.HandleFunc("GET /api/v1/hls/", handlers.HLS) - - //log.Println("Starting server on:") - //log.Printf("Serving on HTTP port: %d\n", port) - // - //log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil)) - go metrics.Metrics() logger.StartMainLogger(config.Local, "reader") @@ -38,34 +27,37 @@ func main() { // logger.Log.Error("failed to create flow", zap.Error(err)) //} - app := fiber.New() + config.Storage.Users = make(map[string]config.User) - authStorage := &handlers.AuthStorage{Users: map[string]handlers.User{}} - authHandler := &handlers.AuthHandler{Storage: authStorage} - userHandler := &handlers.UserHandler{Storage: authStorage} + mux := http.NewServeMux() - // Группа обработчиков, которые доступны неавторизованным пользователям - publicGroup := app.Group("") - publicGroup.Post("/register", authHandler.Register) - publicGroup.Post("/login", authHandler.Login) + // Public routes. + mux.HandleFunc("POST /register", handlers.Register) + mux.HandleFunc("POST /login", handlers.Login) - // Группа обработчиков, которые требуют авторизации - authorizedGroup := app.Group("") - authorizedGroup.Use(jwtware.New(jwtware.Config{ - SigningKey: jwtware.SigningKey{ - Key: handlers.JwtSecretKey, - }, - ContextKey: handlers.ContextKeyUser, - })) - authorizedGroup.Get("/profile", userHandler.Profile) + // Authorized routes. + //authorizedGroup := http.NewServeMux() + //authorizedGroup.HandleFunc("GET /profile", userHandlerQUIC.Profile) // API Flussonic Media Server adapted handlers. - authorizedGroup.Get("/vods/:id/:res/:file", handlers.SingleVodsHandler) - authorizedGroup.Delete("/vods/:id/:res/:file", handlers.DelSingleVodsHandler) - authorizedGroup.Get("/vods/:id/files", handlers.ListFilesVodsHandler) - authorizedGroup.Delete("/vods/:id", handlers.DelVodsHandler) - authorizedGroup.Get("/vods/:id", handlers.ConfigVodsHandler) - authorizedGroup.Get("/vods", handlers.ListVodsHandler) + //mux.HandleFunc("GET /vods/:id/:res/:file", handlersQUIC.SingleVodsHandler) + //mux.HandleFunc("DELETE /vods/:id/:res/:file", handlersQUIC.DelSingleVodsHandler) + //mux.HandleFunc("GET /vods/:id/files", handlersQUIC.ListFilesVodsHandler) + //mux.HandleFunc("DELETE /vods/:id", handlersQUIC.DelVodsHandler) + //mux.HandleFunc("GET /vods/:id", handlersQUIC.ConfigVodsHandler) + mux.Handle("GET /vods", middlewares.AuthMiddleware(handlers.ListVodsHandler())) - logrus.Fatal(app.Listen(":8100")) + http.ListenAndServe(":8080", mux) + + // HTTP/1.1 & HTTP/2 + go func() { + //http.ListenAndServeTLS(":8080", certFile, keyFile, mux) + + }() + + //server := &http3.Server{ + // Addr: ":8080", + // Handler: mux, + // TLSConfig: quic.GetTLSConfig(), + //} } diff --git a/reader/go.mod b/reader/go.mod index 1024035..cedfc6c 100644 --- a/reader/go.mod +++ b/reader/go.mod @@ -12,6 +12,7 @@ require ( github.com/gofiber/contrib/jwt v1.1.0 github.com/gofiber/fiber/v2 v2.52.6 github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/google/uuid v1.6.0 github.com/prometheus/client_golang v1.22.0 github.com/sirupsen/logrus v1.9.3 go.uber.org/zap v1.27.0 @@ -25,7 +26,6 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/golang/snappy v1.0.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/grafov/m3u8 v0.12.1 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/reader/internal/config/config.go b/reader/internal/config/config.go index a58bfe1..6642204 100644 --- a/reader/internal/config/config.go +++ b/reader/internal/config/config.go @@ -1,7 +1,53 @@ package config +import ( + "github.com/google/uuid" + "sync" + "time" +) + +const ( + ContextKeyUser = "user" + ContextKeySession = "session_data" +) + var ( Local = "storage" LogsDirectory string DirData string + + Storage AuthStorage + + JwtSecretKey = []byte(uuid.NewString()) + SessionStore sync.Map + SessionFile = "sessions.json" +) + +type ( + AuthStorage struct { + Users map[string]User + } + + User struct { + Email string + Name string + Password string + } +) + +// ///////////////// в handlers? +// Работа с файлом (дополнительная опция) +type ( + FileSession struct { + ID string `json:"id"` + Data *SessionData `json:"data"` + } + + SessionData struct { + Proto string `json:"proto"` + FileName string `json:"file_name"` + IP string `json:"ip"` + Token string `json:"token"` + LastCheck time.Time `json:"last_check"` + } ) diff --git a/reader/internal/web/handlers/handlers.go b/reader/internal/web/handlers/handlers.go new file mode 100644 index 0000000..dc2a7fa --- /dev/null +++ b/reader/internal/web/handlers/handlers.go @@ -0,0 +1,279 @@ +package handlers + +import ( + "encoding/json" + "errors" + "git.insit.tech/psa/rtsp_reader-writer/reader/internal/config" + "github.com/golang-jwt/jwt/v5" + "github.com/sirupsen/logrus" + "log" + "net/http" + "os" + "strconv" + "strings" + "time" +) + +// Структура HTTP-запроса на регистрацию пользователя +type registerRequest struct { + Email string `json:"email"` + Name string `json:"name"` + Password string `json:"password"` +} + +// Структура 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") +) + +// Структура HTTP-ответа с информацией о пользователе +type ProfileResponse struct { + Email string `json:"email"` + Name string `json:"name"` +} + +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"` +} + +// Обработчик HTTP-запросов на регистрацию пользователя +func Register(w http.ResponseWriter, r *http.Request) { + regReq := registerRequest{} + + if err := json.NewDecoder(r.Body).Decode(®Req); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Проверяем, что пользователь с таким email еще не зарегистрирован + if _, exists := config.Storage.Users[regReq.Email]; exists { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Сохраняем в память нового зарегистрированного пользователя + config.Storage.Users[regReq.Email] = config.User{ + Email: regReq.Email, + Name: regReq.Name, + Password: regReq.Password, + } + + w.WriteHeader(http.StatusCreated) +} + +// Обработчик HTTP-запросов на вход в аккаунт +func Login(w http.ResponseWriter, r *http.Request) { + regReq := loginRequest{} + if err := json.NewDecoder(r.Body).Decode(®Req); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Ищем пользователя в памяти приложения по электронной почте + user, exists := config.Storage.Users[regReq.Email] + // Если пользователь не найден, возвращаем ошибку + if !exists { + w.WriteHeader(http.StatusBadRequest) + return + } + // Если пользователь найден, но у него другой пароль, возвращаем ошибку + if user.Password != regReq.Password { + w.WriteHeader(http.StatusBadRequest) + return + } + + // Генерируем JWT-токен для пользователя, + // который он будет использовать в будущих HTTP-запросах + + // Генерируем полезные данные, которые будут храниться в токене + payload := jwt.MapClaims{ + "sub": user.Email, + "exp": time.Now().Add(time.Hour * 24).Unix(), + } + + // Создаем новый JWT-токен и подписываем его по алгоритму HS256 + token := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) + + t, err := token.SignedString(config.JwtSecretKey) + if err != nil { + logrus.WithError(err).Error("JWT token signing") + w.WriteHeader(http.StatusInternalServerError) + return + } + + res, err := json.Marshal(loginResponse{AccessToken: t}) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if _, err = w.Write(res); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + //w.WriteHeader(http.StatusOK) +} + +// 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 +} + +// ListVodsHandlerLogic implements main logic of handler ListVodsHandler. +func ListVodsHandlerLogic() (ListVodsResponse, error) { + // Read directory. + entries, err := os.ReadDir(config.DirData) + if err != nil { + return ListVodsResponse{}, err + } + + // 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, + } + + return VodsRes, nil +} + +// 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() http.Handler { + log.Println("before return ListVodsHandler") + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Println("hello fron ListVodsHandler") + + // Prepare the Response. + VodsRes, err := ListVodsHandlerLogic() + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + // Write header and code response. + w.Header().Set("Content-Type", "application/json") + if err = json.NewEncoder(w).Encode(VodsRes); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + log.Println("hello fron ListVodsHandlerLogic: StatusOK") + }) +} diff --git a/reader/internal/web/middlewares/auth.go b/reader/internal/web/middlewares/auth.go new file mode 100644 index 0000000..23998be --- /dev/null +++ b/reader/internal/web/middlewares/auth.go @@ -0,0 +1,464 @@ +package middlewares + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "log" + "net" + "net/http" + "os" + "time" + + "git.insit.tech/psa/rtsp_reader-writer/reader/internal/config" + jwtware "github.com/gofiber/contrib/jwt" + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" + "github.com/sirupsen/logrus" +) + +// CheckJWT is custom middleware that checks if token is valid. +func CheckJWT(c *fiber.Ctx) error { + // Пропускаем JWT проверку, если есть session_id + if c.Query("session_id") != "" { + return c.Next() + } + + // Выполняем JWT проверку только при наличии токена + return jwtware.New(jwtware.Config{ + SigningKey: jwtware.SigningKey{ + Key: config.JwtSecretKey, + }, + ContextKey: config.ContextKeyUser, + TokenLookup: "query:token", + })(c) +} + +// CheckJWT2 is custom middleware that checks if token is valid. +func CheckJWT2(next http.Handler) http.Handler { + log.Println("before return CheckJWT2") + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Println("hello from CheckJWT2") + tokenStr := r.URL.Query().Get("token") + if tokenStr == "" { + next.ServeHTTP(w, r) + return + } + + token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) { + return config.JwtSecretKey, nil + }) + + if err != nil || !token.Valid { + w.WriteHeader(http.StatusUnauthorized) + return + } + + ctx := context.WithValue(r.Context(), config.ContextKeyUser, token) + next.ServeHTTP(w, r.WithContext(ctx)) + log.Println("CheckJWT2 finished") + }) +} + +// CheckSessionID middleware. +func CheckSessionID(c *fiber.Ctx) error { + // Если есть валидный JWT в контексте + if c.Locals(config.ContextKeyUser) != nil { + return c.Next() + } + + // Проверка session_id + sessionID := c.Query("session_id") + if sessionID == "" { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Auth required"}) + } + + // Получаем данные из памяти + data, err := getSessionData(sessionID) + if err != nil { + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid session"}) + } + + if !validateSessionParams(c, data) { + deleteSession(sessionID) + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Session parameters mismatch"}) + } + + if time.Since(data.LastCheck) > 3*time.Minute { + if !checkTokenInDB(data.Token) { + deleteSession(sessionID) + return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Token expired"}) + } + data.LastCheck = time.Now() + saveSession(sessionID, data) + } + + c.Locals(config.ContextKeySession, data) + return c.Next() +} + +// CheckSessionID2 middleware. +func CheckSessionID2(next http.Handler) http.Handler { + log.Println("before return CheckSessionID2") + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Println("hello from CheckSessionID2") + if r.Context().Value(config.ContextKeyUser) != nil { + next.ServeHTTP(w, r) + return + } + + sessionID := r.URL.Query().Get("session_id") + if sessionID == "" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + data, err := getSessionData(sessionID) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if !validateSessionParam2(r, data) { + deleteSession(sessionID) + w.WriteHeader(http.StatusUnauthorized) + return + } + + if time.Since(data.LastCheck) > 3*time.Minute { + if !checkTokenInDB(data.Token) { + deleteSession(sessionID) + w.WriteHeader(http.StatusUnauthorized) + return + } + data.LastCheck = time.Now() + saveSession(sessionID, data) + } + + ctx := context.WithValue(r.Context(), config.ContextKeySession, data) + next.ServeHTTP(w, r.WithContext(ctx)) + log.Println("CheckSessionID2 finished") + }) +} + +// Работа с памятью +func getSessionData(sessionID string) (*config.SessionData, error) { + // Для работы с файлами (раскомментировать при необходимости): + // return loadFromFile(sessionID) + + val, ok := config.SessionStore.Load(sessionID) + if !ok { + return nil, fmt.Errorf("session not found") + } + + data, ok := val.(*config.SessionData) + if !ok { + return nil, fmt.Errorf("invalid session data format") + } + return data, nil +} + +// Остальные функции без изменений +func validateSessionParams(c *fiber.Ctx, data *config.SessionData) bool { + log.Printf("HTTP/1.1 && HTTP/2 Protocol: %s", c.Protocol()) + log.Printf("HTTP/1.1 && HTTP/2 File: %s", c.Params("file")) + log.Printf("HTTP/1.1 && HTTP/2 IP: %s", c.IP()) + + log.Printf("DB Protocol: %s", data.Proto) + log.Printf("DB File: %s", data.FileName) + log.Printf("DB IP: %s", data.IP) + + return c.Protocol() == data.Proto && + c.Params("file") == data.FileName && + c.IP() == data.IP +} + +// Остальные функции без изменений +func validateSessionParam2(r *http.Request, data *config.SessionData) bool { + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + + file := r.PathValue("file") + + clientIP, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + // Если SplitHostPort не сработал, оставляем как есть + clientIP = r.RemoteAddr + } + + log.Printf("HTTP/3 Protocol: %s", scheme) + log.Printf("HTTP/3 File: %s", file) + log.Printf("HTTP/3 IP: %s", clientIP) + + log.Printf("DB Protocol: %s", data.Proto) + log.Printf("DB File: %s", data.FileName) + log.Printf("DB IP: %s", data.IP) + + return scheme == data.Proto && + file == data.FileName && + clientIP == data.IP +} + +func checkTokenInDB(token string) bool { + // Ваша реализация проверки токена + return true // временная заглушка +} + +// Генерация session_id +func generateSessionID(c *fiber.Ctx, token string) string { + data := fmt.Sprintf("%s|%s|%s|%s", + c.Protocol(), + c.Params("file"), + c.IP(), + token, + ) + hash := sha256.Sum256([]byte(data)) + return hex.EncodeToString(hash[:]) +} + +// Генерация session_id +func generateSessionID2(r *http.Request, token string) string { + // Determine protocol. + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + + // Determine filename. + file := r.PathValue("file") + + // Determine IP. + clientIP, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + // Если SplitHostPort не сработал, оставляем как есть + clientIP = r.RemoteAddr + } + + data := fmt.Sprintf("%s|%s|%s|%s", + scheme, + file, + clientIP, + token, + ) + hash := sha256.Sum256([]byte(data)) + return hex.EncodeToString(hash[:]) +} + +func saveSession(sessionID string, data *config.SessionData) { + // Для работы с файлами (раскомментировать при необходимости): + // saveToFile(sessionID, data) + + config.SessionStore.Store(sessionID, data) +} + +func deleteSession(sessionID string) { + // Для работы с файлами (раскомментировать при необходимости): + // deleteFromFile(sessionID) + + config.SessionStore.Delete(sessionID) +} + +func saveToFile(sessionID string, data *config.SessionData) error { + file, _ := os.ReadFile(config.SessionFile) + var sessions []config.FileSession + json.Unmarshal(file, &sessions) + + // Удаляем старую сессию если существует + for i, s := range sessions { + if s.ID == sessionID { + sessions = append(sessions[:i], sessions[i+1:]...) + break + } + } + + sessions = append(sessions, config.FileSession{ID: sessionID, Data: data}) + bytes, _ := json.MarshalIndent(sessions, "", " ") + return os.WriteFile(config.SessionFile, bytes, 0644) +} + +func loadFromFile(sessionID string) (*config.SessionData, error) { + file, err := os.ReadFile(config.SessionFile) + if err != nil { + return nil, err + } + + var sessions []config.FileSession + json.Unmarshal(file, &sessions) + + for _, s := range sessions { + if s.ID == sessionID { + return s.Data, nil + } + } + return nil, fmt.Errorf("session not found") +} + +func deleteFromFile(sessionID string) error { + file, _ := os.ReadFile(config.SessionFile) + var sessions []config.FileSession + json.Unmarshal(file, &sessions) + + for i, s := range sessions { + if s.ID == sessionID { + sessions = append(sessions[:i], sessions[i+1:]...) + bytes, _ := json.MarshalIndent(sessions, "", " ") + return os.WriteFile(config.SessionFile, bytes, 0644) + } + } + return nil +} + +// CreateSessionID is custom middleware that creates session ID by using token. +func CreateSessionID(c *fiber.Ctx) error { + if token := c.Query("token"); token != "" { + // Get payload from token. + jwtPayload, ok := jwtPayloadFromRequest(c) + if !ok { + return c.SendStatus(fiber.StatusUnauthorized) + } + + // Check if the owner of the token is registered. + _, ok = config.Storage.Users[jwtPayload["sub"].(string)] + if !ok { + return errors.New("user not found") + } + + // Create session data. + sessionData := &config.SessionData{ + Proto: c.Protocol(), + FileName: c.Params("file"), + IP: c.IP(), + Token: token, + LastCheck: time.Now(), + } + + // Генерируем session_id + sessionID := generateSessionID(c, token) + + // Сохраняем сессию + saveSession(sessionID, sessionData) + + return c.JSON(fiber.Map{ + "session_id": sessionID, + }) + } + return c.Next() +} + +// CreateSessionID2 is custom middleware that creates session ID by using token. +func CreateSessionID2(next http.Handler) http.Handler { + log.Println("before return CreateSessionID2") + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log.Println("hello from CreateSessionID2") + + if token := r.URL.Query().Get("token"); token != "" { + // Get payload from token. + jwtPayload, ok := jwtPayloadFromRequest2(r) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + + // Check if the owner of the token is registered. + _, ok = config.Storage.Users[jwtPayload["sub"].(string)] + if !ok { + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("user not found")) + return + } + + // Determine protocol. + scheme := "http" + if r.TLS != nil { + scheme = "https" + } + + // Determine filename. + file := r.PathValue("file") + + // Determine IP. + clientIP, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + // Если SplitHostPort не сработал, оставляем как есть + clientIP = r.RemoteAddr + } + + // Create session data. + sessionData := &config.SessionData{ + Proto: scheme, + FileName: file, + IP: clientIP, + Token: token, + LastCheck: time.Now(), + } + + // Генерируем session_id + sessionID := generateSessionID2(r, token) + + // Сохраняем сессию + saveSession(sessionID, sessionData) + + w.Header().Add("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(sessionID); err != nil { + w.WriteHeader(http.StatusInternalServerError) + } + return + } + next.ServeHTTP(w, r) + log.Println("hello from CreateSessionID2: StatusOK") + }) +} + +func jwtPayloadFromRequest(c *fiber.Ctx) (jwt.MapClaims, bool) { + jwtToken, ok := c.Context().Value(config.ContextKeyUser).(*jwt.Token) + if !ok { + logrus.WithFields(logrus.Fields{ + "jwt_token_context_value": c.Context().Value(config.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 +} + +func jwtPayloadFromRequest2(r *http.Request) (jwt.MapClaims, bool) { + jwtToken, ok := r.Context().Value(config.ContextKeyUser).(*jwt.Token) + + if !ok { + logrus.WithFields(logrus.Fields{ + "jwt_token_context_value": r.Context().Value(config.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 +} + +// AuthMiddleware collects auth middlewares. +func AuthMiddleware(h http.Handler) http.Handler { + return CheckJWT2(CheckSessionID2(CreateSessionID2(h))) +}