Files
fm_be/internal/transport/http/middleware/wopi_middleware.go
2026-07-16 22:16:45 +07:00

173 lines
4.5 KiB
Go

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
}
}