Files
seaweedfs/weed/server/filer_server_handlers.go
T
MorezMartinandChris Lu 6f1d4af035 fix(filer): propagate proxyChunkId query params to volume server (#10036)
* fix(filer): propagate proxyChunkId query params to volume server

When weed mount reads via filer proxy mode (-volumeServerAccess=filerProxy),
the mount adds query params like readDeleted=true to chunk read requests.

Two bugs prevented these from working:

1. filer_server_handlers.go extracted fileId from the raw RequestURI, which
   includes query params, corrupting the fileId (e.g. '6,abc&readDeleted=true').
   Fix: use r.URL.Query().Get("proxyChunkId") for clean extraction.

2. filer_server_handlers_proxy.go didn't forward query params to the volume
   server. The urlStrings from LookupFileId already contain the fileId in the
   path, so just append the original query string.

* filer: match chunk proxy by query param, not URI prefix order

Order-dependent prefix slicing missed proxyChunkId when it wasn't the
first query param. Gate on root path and read the parsed query value.

* filer: drop internal proxyChunkId from proxied volume query

Lookup URLs already carry the fileId in the path, so forwarding the raw
query duplicated proxyChunkId onto the volume server. Strip it and only
append the remaining caller params (e.g. readDeleted).

---------

Co-authored-by: Chris Lu <chris.lu@gmail.com>
2026-06-22 11:21:29 -07:00

344 lines
11 KiB
Go

package weed_server
import (
"context"
"errors"
"net/http"
"path"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/security"
"github.com/seaweedfs/seaweedfs/weed/stats"
"github.com/seaweedfs/seaweedfs/weed/util/version"
)
func (fs *FilerServer) filerHandler(w http.ResponseWriter, r *http.Request) {
start := time.Now()
inFlightGauge := stats.FilerInFlightRequestsGauge.WithLabelValues(r.Method)
inFlightGauge.Inc()
defer inFlightGauge.Dec()
statusRecorder := stats.NewStatusResponseWriter(w)
w = statusRecorder
origin := r.Header.Get("Origin")
if origin != "" {
if fs.option.AllowedOrigins == nil || len(fs.option.AllowedOrigins) == 0 || fs.option.AllowedOrigins[0] == "*" {
origin = "*"
} else {
originFound := false
for _, allowedOrigin := range fs.option.AllowedOrigins {
if origin == allowedOrigin {
originFound = true
}
}
if !originFound {
writeJsonError(w, r, http.StatusForbidden, errors.New("origin not allowed"))
return
}
}
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Expose-Headers", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "PUT, POST, GET, DELETE, OPTIONS")
}
if r.Method == http.MethodOptions {
OptionsHandler(w, r, false)
return
}
// proxy to volume servers
var fileId string
if r.URL.Path == "/" {
fileId = r.URL.Query().Get("proxyChunkId")
}
if fileId != "" {
fs.proxyToVolumeServer(w, r, fileId)
stats.FilerHandlerCounter.WithLabelValues(stats.ChunkProxy).Inc()
stats.FilerRequestHistogram.WithLabelValues(stats.ChunkProxy).Observe(time.Since(start).Seconds())
return
}
requestMethod := r.Method
defer func(method *string) {
stats.FilerRequestCounter.WithLabelValues(*method, strconv.Itoa(statusRecorder.Status)).Inc()
stats.FilerRequestHistogram.WithLabelValues(*method).Observe(time.Since(start).Seconds())
}(&requestMethod)
isReadHttpCall := r.Method == http.MethodGet || r.Method == http.MethodHead
if !fs.maybeCheckJwtAuthorization(r, !isReadHttpCall) {
writeJsonError(w, r, http.StatusUnauthorized, errors.New("wrong jwt"))
return
}
w.Header().Set("Server", "SeaweedFS "+version.VERSION)
switch r.Method {
case http.MethodGet, http.MethodHead:
fs.GetOrHeadHandler(w, r)
case http.MethodDelete:
if _, ok := r.URL.Query()["tagging"]; ok {
fs.DeleteTaggingHandler(w, r)
} else {
fs.DeleteHandler(w, r)
}
case http.MethodPost, http.MethodPut:
// wait until in flight data is less than the limit
contentLength := getContentLength(r)
fs.inFlightDataLimitCond.L.Lock()
inFlightDataSize := atomic.LoadInt64(&fs.inFlightDataSize)
inFlightUploads := atomic.LoadInt64(&fs.inFlightUploads)
// Wait if either data size limit or file count limit is exceeded
for (fs.option.ConcurrentUploadLimit != 0 && inFlightDataSize > fs.option.ConcurrentUploadLimit) || (fs.option.ConcurrentFileUploadLimit != 0 && inFlightUploads >= fs.option.ConcurrentFileUploadLimit) {
if fs.option.ConcurrentUploadLimit != 0 && inFlightDataSize > fs.option.ConcurrentUploadLimit {
glog.V(4).Infof("wait because inflight data %d > %d", inFlightDataSize, fs.option.ConcurrentUploadLimit)
}
if fs.option.ConcurrentFileUploadLimit != 0 && inFlightUploads >= fs.option.ConcurrentFileUploadLimit {
glog.V(4).Infof("wait because inflight uploads %d >= %d", inFlightUploads, fs.option.ConcurrentFileUploadLimit)
}
fs.inFlightDataLimitCond.Wait()
inFlightDataSize = atomic.LoadInt64(&fs.inFlightDataSize)
inFlightUploads = atomic.LoadInt64(&fs.inFlightUploads)
}
fs.inFlightDataLimitCond.L.Unlock()
// Increment counters
newUploads := atomic.AddInt64(&fs.inFlightUploads, 1)
newSize := atomic.AddInt64(&fs.inFlightDataSize, contentLength)
// Update metrics
stats.FilerInFlightUploadCountGauge.Set(float64(newUploads))
stats.FilerInFlightUploadBytesGauge.Set(float64(newSize))
defer func() {
// Decrement counters
newUploads := atomic.AddInt64(&fs.inFlightUploads, -1)
newSize := atomic.AddInt64(&fs.inFlightDataSize, -contentLength)
// Update metrics
stats.FilerInFlightUploadCountGauge.Set(float64(newUploads))
stats.FilerInFlightUploadBytesGauge.Set(float64(newSize))
fs.inFlightDataLimitCond.Signal()
}()
if r.Method == http.MethodPut {
if _, ok := r.URL.Query()["tagging"]; ok {
fs.PutTaggingHandler(w, r)
} else {
fs.PostHandler(w, r, contentLength)
}
} else { // method == "POST"
fs.PostHandler(w, r, contentLength)
}
default:
requestMethod = "INVALID"
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func (fs *FilerServer) readonlyFilerHandler(w http.ResponseWriter, r *http.Request) {
start := time.Now()
statusRecorder := stats.NewStatusResponseWriter(w)
w = statusRecorder
glog.V(4).Infof("Request: %s %s", r.Method, r.URL.Path)
origin := r.Header.Get("Origin")
if origin != "" {
if fs.option.AllowedOrigins == nil || len(fs.option.AllowedOrigins) == 0 || fs.option.AllowedOrigins[0] == "*" {
origin = "*"
} else {
originFound := false
for _, allowedOrigin := range fs.option.AllowedOrigins {
if origin == allowedOrigin {
originFound = true
}
}
if !originFound {
writeJsonError(w, r, http.StatusForbidden, errors.New("origin not allowed"))
return
}
}
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Headers", "OPTIONS, GET, HEAD")
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
requestMethod := r.Method
defer func(method *string) {
stats.FilerRequestCounter.WithLabelValues(*method, strconv.Itoa(statusRecorder.Status)).Inc()
stats.FilerRequestHistogram.WithLabelValues(*method).Observe(time.Since(start).Seconds())
}(&requestMethod)
// We handle OPTIONS first because it never should be authenticated
if r.Method == http.MethodOptions {
OptionsHandler(w, r, true)
return
}
if !fs.maybeCheckJwtAuthorization(r, false) {
writeJsonError(w, r, http.StatusUnauthorized, errors.New("wrong jwt"))
return
}
w.Header().Set("Server", "SeaweedFS "+version.VERSION)
switch r.Method {
case http.MethodGet, http.MethodHead:
fs.GetOrHeadHandler(w, r)
default:
requestMethod = "INVALID"
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func OptionsHandler(w http.ResponseWriter, r *http.Request, isReadOnly bool) {
if isReadOnly {
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
} else {
w.Header().Set("Access-Control-Allow-Methods", "PUT, POST, GET, DELETE, OPTIONS")
w.Header().Set("Access-Control-Expose-Headers", "*")
}
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
// maybeCheckJwtAuthorization returns true if access should be granted, false if it should be denied
func (fs *FilerServer) maybeCheckJwtAuthorization(r *http.Request, isWrite bool) bool {
if !isWrite && r.URL.Path == "/" {
return true
}
var signingKey security.SigningKey
if isWrite {
signingKey = fs.filerGuard.SigningKey()
if len(signingKey) == 0 {
return true
}
} else {
signingKey = fs.filerGuard.ReadSigningKey()
if len(signingKey) == 0 {
return true
}
}
tokenStr := security.GetJwt(r)
if tokenStr == "" {
glog.V(1).Infof("missing jwt from %s", r.RemoteAddr)
return false
}
token, err := security.DecodeJwt(signingKey, tokenStr, &security.SeaweedFilerClaims{})
if err != nil {
glog.V(1).Infof("jwt verification error from %s: %v", r.RemoteAddr, err)
return false
}
if !token.Valid {
glog.V(1).Infof("jwt invalid from %s: %v", r.RemoteAddr, tokenStr)
return false
}
claims, ok := token.Claims.(*security.SeaweedFilerClaims)
if !ok {
glog.V(1).Infof("jwt claims not of type *SeaweedFilerClaims from %s", r.RemoteAddr)
return false
}
if len(claims.AllowedPrefixes) > 0 {
// Copy and move name their source via a query parameter, not r.URL.Path.
// Scope every path the request reads or relocates, or a prefix-restricted
// token could reach data outside its allowed subtree.
for _, p := range jwtScopedRequestPaths(r) {
if !anyComponentPrefixMatches(claims.AllowedPrefixes, p) {
glog.V(1).Infof("jwt path not allowed from %s: %v", r.RemoteAddr, p)
return false
}
}
}
if len(claims.AllowedMethods) > 0 {
hasMethod := false
for _, method := range claims.AllowedMethods {
if method == r.Method {
hasMethod = true
break
}
}
if !hasMethod {
glog.V(1).Infof("jwt method not allowed from %s: %v", r.RemoteAddr, r.Method)
return false
}
}
return true
}
// jwtScopedRequestPaths returns every filer path a request touches that must be
// covered by the JWT AllowedPrefixes: the write target (r.URL.Path) plus any
// copy/move source named by the cp.from / mv.from query parameters.
func jwtScopedRequestPaths(r *http.Request) []string {
paths := []string{r.URL.Path}
if query := r.URL.Query(); query.Has("cp.from") || query.Has("mv.from") {
if from := query.Get("cp.from"); from != "" {
paths = append(paths, from)
}
if from := query.Get("mv.from"); from != "" {
paths = append(paths, from)
}
}
return paths
}
// anyComponentPrefixMatches reports whether p is within any of the prefixes.
func anyComponentPrefixMatches(prefixes []string, p string) bool {
for _, prefix := range prefixes {
if pathHasComponentPrefix(p, prefix) {
return true
}
}
return false
}
// pathHasComponentPrefix reports whether reqPath is contained within the
// directory subtree denoted by prefix, treating both as "/"-separated
// path components. Both inputs are normalised with path.Clean to neutralise
// "." and ".." segments and collapse duplicate slashes. A prefix of "/"
// matches any path.
func pathHasComponentPrefix(reqPath, prefix string) bool {
if prefix == "" {
return false
}
cleanedPath := path.Clean(reqPath)
if cleanedPath == "." {
cleanedPath = "/"
}
cleanedPrefix := path.Clean(prefix)
if cleanedPrefix == "." {
cleanedPrefix = "/"
}
if cleanedPrefix == "/" {
return true
}
if cleanedPath == cleanedPrefix {
return true
}
return strings.HasPrefix(cleanedPath, cleanedPrefix+"/")
}
func (fs *FilerServer) filerHealthzHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Server", "SeaweedFS "+version.VERSION)
if _, err := fs.filer.Store.FindEntry(context.Background(), filer.TopicsDir); err != nil && err != filer_pb.ErrNotFound {
glog.Warningf("filerHealthzHandler FindEntry: %+v", err)
w.WriteHeader(http.StatusServiceUnavailable)
} else {
w.WriteHeader(http.StatusOK)
}
}