kubernetes/vendor/github.com/emicklei/go-restful/v3/curly.go
Davanum Srinivas c40ea60b9f
Update OpenTelemetry dependencies to latest versions
Core packages (opentelemetry-go):
- go.opentelemetry.io/otel: v1.38.0 → v1.39.0
- go.opentelemetry.io/otel/metric: v1.38.0 → v1.39.0
- go.opentelemetry.io/otel/trace: v1.38.0 → v1.39.0
- go.opentelemetry.io/otel/sdk: v1.38.0 → v1.39.0

Exporters:
- go.opentelemetry.io/otel/exporters/otlp/otlptrace: v1.34.0 → v1.39.0
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc: v1.34.0 → v1.39.0

Contrib instrumentation (opentelemetry-go-contrib):
- go.opentelemetry.io/contrib/.../otelhttp: v0.61.0 → v0.64.0
- go.opentelemetry.io/contrib/.../otelrestful: v0.44.0 → v0.64.0

Protocol definitions (opentelemetry-proto-go):
- go.opentelemetry.io/proto/otlp: v1.5.0 → v1.9.0

Notable changes:
- Go 1.24 is now the minimum required version (Go 1.23 support dropped) for OTEL components
- Performance: ~4x improvement in histogram concurrent operations; xxhash
  replaces fnv for attribute hashing
- Fixed goroutine leak in span processors when context is canceled
- otelrestful migrated semantic conventions from v1.20.0 to v1.34.0
  (e.g., http.method → http.request.method)
- Partial OTLP export errors now surfaced instead of being silently dropped
- otelrestful no longer depends on json-iterator/go, modern-go/concurrent,
  or modern-go/reflect2; unwanted-dependencies.json updated accordingly

Signed-off-by: Davanum Srinivas <davanum@gmail.com>
2026-01-20 18:24:44 -05:00

225 lines
7.4 KiB
Go

package restful
// Copyright 2013 Ernest Micklei. All rights reserved.
// Use of this source code is governed by a license
// that can be found in the LICENSE file.
import (
"net/http"
"regexp"
"sort"
"strings"
"sync"
)
// CurlyRouter expects Routes with paths that contain zero or more parameters in curly brackets.
type CurlyRouter struct{}
var (
regexCache sync.Map // Cache for compiled regex patterns
pathTokenCacheEnabled = true // Enable/disable path token regex caching
)
// SetPathTokenCacheEnabled enables or disables path token regex caching for CurlyRouter.
// When disabled, regex patterns will be compiled on every request.
// When enabled (default), compiled regex patterns are cached for better performance.
func SetPathTokenCacheEnabled(enabled bool) {
pathTokenCacheEnabled = enabled
}
// getCachedRegexp retrieves a compiled regex from the cache if found and valid.
// Returns the regex and true if found and valid, nil and false otherwise.
func getCachedRegexp(cache *sync.Map, pattern string) (*regexp.Regexp, bool) {
if cached, found := cache.Load(pattern); found {
if regex, ok := cached.(*regexp.Regexp); ok {
return regex, true
}
}
return nil, false
}
// SelectRoute is part of the Router interface and returns the best match
// for the WebService and its Route for the given Request.
func (c CurlyRouter) SelectRoute(
webServices []*WebService,
httpRequest *http.Request) (selectedService *WebService, selected *Route, err error) {
requestTokens := tokenizePath(httpRequest.URL.Path)
detectedService := c.detectWebService(requestTokens, webServices)
if detectedService == nil {
if trace {
traceLogger.Printf("no WebService was found to match URL path:%s\n", httpRequest.URL.Path)
}
return nil, nil, NewError(http.StatusNotFound, "404: Page Not Found")
}
candidateRoutes := c.selectRoutes(detectedService, requestTokens)
if len(candidateRoutes) == 0 {
if trace {
traceLogger.Printf("no Route in WebService with path %s was found to match URL path:%s\n", detectedService.rootPath, httpRequest.URL.Path)
}
return detectedService, nil, NewError(http.StatusNotFound, "404: Page Not Found")
}
selectedRoute, err := c.detectRoute(candidateRoutes, httpRequest)
if selectedRoute == nil {
return detectedService, nil, err
}
return detectedService, selectedRoute, nil
}
// selectRoutes return a collection of Route from a WebService that matches the path tokens from the request.
func (c CurlyRouter) selectRoutes(ws *WebService, requestTokens []string) sortableCurlyRoutes {
candidates := make(sortableCurlyRoutes, 0, 8)
for _, eachRoute := range ws.routes {
matches, paramCount, staticCount := c.matchesRouteByPathTokens(eachRoute.pathParts, requestTokens, eachRoute.hasCustomVerb)
if matches {
candidates.add(curlyRoute{eachRoute, paramCount, staticCount}) // TODO make sure Routes() return pointers?
}
}
sort.Sort(candidates)
return candidates
}
// matchesRouteByPathTokens computes whether it matches, howmany parameters do match and what the number of static path elements are.
func (c CurlyRouter) matchesRouteByPathTokens(routeTokens, requestTokens []string, routeHasCustomVerb bool) (matches bool, paramCount int, staticCount int) {
if len(routeTokens) < len(requestTokens) {
// proceed in matching only if last routeToken is wildcard
count := len(routeTokens)
if count == 0 || !strings.HasSuffix(routeTokens[count-1], "*}") {
return false, 0, 0
}
// proceed
}
for i, routeToken := range routeTokens {
if i == len(requestTokens) {
// reached end of request path
return false, 0, 0
}
requestToken := requestTokens[i]
if routeHasCustomVerb && hasCustomVerb(routeToken) {
if !isMatchCustomVerb(routeToken, requestToken) {
return false, 0, 0
}
staticCount++
requestToken = removeCustomVerb(requestToken)
routeToken = removeCustomVerb(routeToken)
}
if strings.HasPrefix(routeToken, "{") {
paramCount++
if colon := strings.Index(routeToken, ":"); colon != -1 {
// match by regex
matchesToken, matchesRemainder := c.regularMatchesPathToken(routeToken, colon, requestToken)
if !matchesToken {
return false, 0, 0
}
if matchesRemainder {
break
}
}
} else { // no { prefix
if requestToken != routeToken {
return false, 0, 0
}
staticCount++
}
}
return true, paramCount, staticCount
}
// regularMatchesPathToken tests whether the regular expression part of routeToken matches the requestToken or all remaining tokens
// format routeToken is {someVar:someExpression}, e.g. {zipcode:[\d][\d][\d][\d][A-Z][A-Z]}
func (c CurlyRouter) regularMatchesPathToken(routeToken string, colon int, requestToken string) (matchesToken bool, matchesRemainder bool) {
regPart := routeToken[colon+1 : len(routeToken)-1]
if regPart == "*" {
if trace {
traceLogger.Printf("wildcard parameter detected in route token %s that matches %s\n", routeToken, requestToken)
}
return true, true
}
// Check cache first (if enabled)
if pathTokenCacheEnabled {
if regex, found := getCachedRegexp(&regexCache, regPart); found {
matched := regex.MatchString(requestToken)
return matched, false
}
}
// Compile the regex
regex, err := regexp.Compile(regPart)
if err != nil {
return false, false
}
// Cache the regex (if enabled)
if pathTokenCacheEnabled {
regexCache.Store(regPart, regex)
}
matched := regex.MatchString(requestToken)
return matched, false
}
var jsr311Router = RouterJSR311{}
// detectRoute selectes from a list of Route the first match by inspecting both the Accept and Content-Type
// headers of the Request. See also RouterJSR311 in jsr311.go
func (c CurlyRouter) detectRoute(candidateRoutes sortableCurlyRoutes, httpRequest *http.Request) (*Route, error) {
// tracing is done inside detectRoute
return jsr311Router.detectRoute(candidateRoutes.routes(), httpRequest)
}
// detectWebService returns the best matching webService given the list of path tokens.
// see also computeWebserviceScore
func (c CurlyRouter) detectWebService(requestTokens []string, webServices []*WebService) *WebService {
var bestWs *WebService
score := -1
for _, eachWS := range webServices {
matches, eachScore := c.computeWebserviceScore(requestTokens, eachWS.pathExpr.tokens)
if matches && (eachScore > score) {
bestWs = eachWS
score = eachScore
}
}
return bestWs
}
// computeWebserviceScore returns whether tokens match and
// the weighted score of the longest matching consecutive tokens from the beginning.
func (c CurlyRouter) computeWebserviceScore(requestTokens []string, routeTokens []string) (bool, int) {
if len(routeTokens) > len(requestTokens) {
return false, 0
}
score := 0
for i := 0; i < len(routeTokens); i++ {
eachRequestToken := requestTokens[i]
eachRouteToken := routeTokens[i]
if len(eachRequestToken) == 0 && len(eachRouteToken) == 0 {
score++
continue
}
if len(eachRouteToken) > 0 && strings.HasPrefix(eachRouteToken, "{") {
// no empty match
if len(eachRequestToken) == 0 {
return false, score
}
score++
if colon := strings.Index(eachRouteToken, ":"); colon != -1 {
// match by regex
matchesToken, _ := c.regularMatchesPathToken(eachRouteToken, colon, eachRequestToken)
if matchesToken {
score++ // extra score for regex match
}
}
} else {
// not a parameter
if eachRequestToken != eachRouteToken {
return false, score
}
score += (len(routeTokens) - i) * 10 //fuzzy
}
}
return true, score
}