package middleware import ( "net/http" "net/url" "strings" "github.com/gofiber/fiber/v2" "wucher/internal/service" "wucher/internal/shared/pkg/apperrorsx" "wucher/internal/shared/pkg/uuidv7" ) const ( wopiAccessTokenQuery = "access_token" wopiFileIDParam = "fileId" ) func WOPIRequired(svc *service.WOPIService) fiber.Handler { return func(c *fiber.Ctx) error { if svc == nil { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrFileManagerWOPIServiceUnavailable)) } fileID, err := parseWOPIFileID(c) if err != nil { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrFileManagerWOPIInvalidFileID)) } token := firstNonEmptyWOPIQueryValue(c, wopiAccessTokenQuery) if token == "" { token = firstNonEmptyWOPISrcQueryValue(c, wopiAccessTokenQuery) } if token == "" { authz := strings.TrimSpace(c.Get("Authorization")) if strings.HasPrefix(strings.ToLower(authz), "bearer ") { token = strings.TrimSpace(authz[7:]) } } claims, err := svc.VerifyAccessToken(token, fileID) if err != nil { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrFileManagerWOPIInvalidToken).WithCause(err)) } if err := svc.VerifyProof(c.Method(), c.Path(), token, c.Get("X-WOPI-TimeStamp"), c.Get("X-WOPI-Proof"), c.Get("X-WOPI-ProofOld")); err != nil { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrFileManagerWOPIInvalidProof).WithCause(err)) } userID, _ := uuidv7.ParsePathLikeString(claims.UserID) c.Locals("user_id", userID) c.Locals("wopi_claims", claims) c.Locals("wopi_permission", strings.ToLower(strings.TrimSpace(claims.Permission))) c.Locals("wopi_takeover_id", claims.TakeoverID) c.Locals("wopi_access_token", token) return c.Next() } } func firstNonEmptyWOPIQueryValue(c *fiber.Ctx, key string) string { if c == nil || strings.TrimSpace(key) == "" { return "" } found := "" c.Context().QueryArgs().VisitAll(func(k, v []byte) { if found != "" { return } if !strings.EqualFold(string(k), key) { return } value := strings.TrimSpace(string(v)) if value != "" { found = value } }) if found != "" { return found } return strings.TrimSpace(c.Query(key)) } func firstNonEmptyWOPISrcQueryValue(c *fiber.Ctx, key string) string { if c == nil || strings.TrimSpace(key) == "" { return "" } src := strings.TrimSpace(c.Query("WOPISrc")) if src == "" { return "" } decoded := src if unescaped, err := url.QueryUnescape(src); err == nil && strings.TrimSpace(unescaped) != "" { decoded = strings.TrimSpace(unescaped) } parsed, err := url.Parse(decoded) if err != nil { return "" } query := parsed.Query() if value := strings.TrimSpace(query.Get(key)); value != "" { return value } return "" } func parseWOPIFileID(c *fiber.Ctx) ([]byte, error) { if c == nil { return nil, fiber.ErrBadRequest } candidates := []string{ c.Params(wopiFileIDParam), c.Path(), c.OriginalURL(), c.Query("WOPISrc"), } for _, candidate := range candidates { if fileID, err := uuidv7.ParsePathLikeString(candidate); err == nil { return fileID, nil } } return nil, fiber.ErrBadRequest } func WOPIRequestBodyLimit(maxBytes int) fiber.Handler { return func(c *fiber.Ctx) error { if maxBytes <= 0 { return c.Next() } if c.Method() == http.MethodPost || c.Method() == http.MethodPut { if c.Request().Header.ContentLength() > maxBytes { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrFileManagerWOPIValidationFailed)) } } return c.Next() } } func WOPIPermissionRequired(required service.WOPIPermission) fiber.Handler { return func(c *fiber.Ctx) error { if strings.TrimSpace(string(required)) == "" { return c.Next() } raw := c.Locals("wopi_permission") current, _ := raw.(string) if !wopiPermissionAllowed(current, string(required)) { return apperrorsx.Write(c, apperrorsx.New(apperrorsx.ErrFileManagerWOPIPermissionDenied)) } return c.Next() } } func wopiPermissionAllowed(current, required string) bool { switch strings.ToLower(strings.TrimSpace(required)) { case string(service.WOPIPermissionRead): return wopiPermissionRank(current) >= wopiPermissionRank(string(service.WOPIPermissionRead)) case string(service.WOPIPermissionWrite): return wopiPermissionRank(current) >= wopiPermissionRank(string(service.WOPIPermissionWrite)) default: return false } } func wopiPermissionRank(v string) int { switch strings.ToLower(strings.TrimSpace(v)) { case string(service.WOPIPermissionRead): return 1 case string(service.WOPIPermissionWrite): return 2 default: return 0 } }