-
Notifications
You must be signed in to change notification settings - Fork 441
Commit
Signed-off-by: Eliott Bouhana <[email protected]>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2025 Datadog, Inc. | ||
|
||
package http | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
"sync" | ||
"unicode" | ||
|
||
"gopkg.in/DataDog/dd-trace-go.v1/internal/log" | ||
) | ||
|
||
// patternRoute returns the route part of a go1.22 style ServeMux pattern. I.e. | ||
// it returns "/foo" for the pattern "/foo" as well as the pattern "GET /foo". | ||
func patternRoute(s string) string { | ||
// Support go1.22 serve mux patterns: [METHOD ][HOST]/[PATH] | ||
// Consider any text before a space or tab to be the method of the pattern. | ||
// See net/http.parsePattern and the link below for more information. | ||
// https://pkg.go.dev/net/http#hdr-Patterns-ServeMux | ||
if i := strings.IndexAny(s, " \t"); i > 0 && len(s) >= i+1 { | ||
return strings.TrimLeft(s[i+1:], " \t") | ||
} | ||
return s | ||
} | ||
|
||
var patternSegmentsCache sync.Map // map[string][]string | ||
|
||
// patternValues return the path parameter values and names from the request. | ||
func patternValues(r *http.Request) map[string]string { | ||
if r.Pattern == "" { // using <=1.21 serve mux behavior, aborting | ||
Check failure on line 36 in contrib/net/http/pattern.go GitHub Actions / PR Unit and Integration Tests / test-core
Check failure on line 36 in contrib/net/http/pattern.go GitHub Actions / PR Unit and Integration Tests / test-core
Check failure on line 36 in contrib/net/http/pattern.go GitHub Actions / PR Unit and Integration Tests / test-core
Check failure on line 36 in contrib/net/http/pattern.go GitHub Actions / PR Unit and Integration Tests / test-core
Check failure on line 36 in contrib/net/http/pattern.go GitHub Actions / PR Unit and Integration Tests / test-contrib
|
||
return nil | ||
} | ||
names := getPatternNames(r.Pattern) | ||
Check failure on line 39 in contrib/net/http/pattern.go GitHub Actions / PR Unit and Integration Tests / test-core
Check failure on line 39 in contrib/net/http/pattern.go GitHub Actions / PR Unit and Integration Tests / test-core
Check failure on line 39 in contrib/net/http/pattern.go GitHub Actions / PR Unit and Integration Tests / test-core
Check failure on line 39 in contrib/net/http/pattern.go GitHub Actions / PR Unit and Integration Tests / test-core
Check failure on line 39 in contrib/net/http/pattern.go GitHub Actions / PR Unit and Integration Tests / test-contrib
|
||
res := make(map[string]string, len(names)) | ||
for _, name := range names { | ||
res[name] = r.PathValue(name) | ||
} | ||
return res | ||
} | ||
|
||
func getPatternNames(pattern string) []string { | ||
if v, ok := patternSegmentsCache.Load(pattern); ok { | ||
return v.([]string) | ||
} | ||
|
||
segments, err := patternNames(pattern) | ||
if err != nil { | ||
log.Debug("contrib/net/http: failed to parse mux path pattern %q: %v", pattern, err) | ||
return nil | ||
} | ||
|
||
v, _ := patternSegmentsCache.LoadOrStore(pattern, segments) | ||
return v.([]string) | ||
} | ||
|
||
// patternNames returns the names of the wildcards in the pattern. | ||
// Based on https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/net/http/pattern.go;l=84 | ||
func patternNames(s string) ([]string, error) { | ||
if len(s) == 0 { | ||
return nil, errors.New("empty pattern") | ||
} | ||
method, rest, found := s, "", false | ||
if i := strings.IndexAny(s, " \t"); i >= 0 { | ||
method, rest, found = s[:i], strings.TrimLeft(s[i+1:], " \t"), true | ||
} | ||
if !found { | ||
rest = method | ||
method = "" | ||
} | ||
|
||
i := strings.IndexByte(rest, '/') | ||
if i < 0 { | ||
return nil, errors.New("host/path missing /") | ||
} | ||
host := rest[:i] | ||
rest = rest[i:] | ||
if j := strings.IndexByte(host, '{'); j >= 0 { | ||
return nil, errors.New("host contains '{' (missing initial '/'?)") | ||
} | ||
|
||
// At this point, rest is the path. | ||
var names []string | ||
seenNames := make(map[string]bool) | ||
for len(rest) > 0 { | ||
// Invariant: rest[0] == '/'. | ||
rest = rest[1:] | ||
if len(rest) == 0 { | ||
// Trailing slash. | ||
break | ||
} | ||
i := strings.IndexByte(rest, '/') | ||
if i < 0 { | ||
i = len(rest) | ||
} | ||
var seg string | ||
seg, rest = rest[:i], rest[i:] | ||
if i := strings.IndexByte(seg, '{'); i >= 0 { | ||
// Wildcard. | ||
if i != 0 { | ||
return nil, errors.New("bad wildcard segment (must start with '{')") | ||
} | ||
if seg[len(seg)-1] != '}' { | ||
return nil, errors.New("bad wildcard segment (must end with '}')") | ||
} | ||
name := seg[1 : len(seg)-1] | ||
if name == "$" { | ||
if len(rest) != 0 { | ||
return nil, errors.New("{$} not at end") | ||
} | ||
break | ||
} | ||
name, multi := strings.CutSuffix(name, "...") | ||
if multi && len(rest) != 0 { | ||
return nil, errors.New("{...} wildcard not at end") | ||
} | ||
if name == "" { | ||
return nil, errors.New("empty wildcard name") | ||
} | ||
if !isValidWildcardName(name) { | ||
return nil, fmt.Errorf("bad wildcard name %q", name) | ||
} | ||
if seenNames[name] { | ||
return nil, fmt.Errorf("duplicate wildcard name %q", name) | ||
} | ||
seenNames[name] = true | ||
names = append(names, name) | ||
} | ||
} | ||
|
||
return names, nil | ||
} | ||
|
||
func isValidWildcardName(s string) bool { | ||
if s == "" { | ||
return false | ||
} | ||
// Valid Go identifier. | ||
for i, c := range s { | ||
if !unicode.IsLetter(c) && c != '_' && (i == 0 || !unicode.IsDigit(c)) { | ||
return false | ||
} | ||
} | ||
return true | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
// Unless explicitly stated otherwise all files in this repository are licensed | ||
// under the Apache License Version 2.0. | ||
// This product includes software developed at Datadog (https://www.datadoghq.com/). | ||
// Copyright 2025 Datadog, Inc. | ||
|
||
package http | ||
|
||
import ( | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestPathParams(t *testing.T) { | ||
for _, tt := range []struct { | ||
name string | ||
pattern string | ||
url string | ||
expected map[string]string | ||
}{ | ||
{ | ||
name: "simple", | ||
pattern: "/foo/{bar}", | ||
url: "/foo/123", | ||
expected: map[string]string{"bar": "123"}, | ||
}, | ||
{ | ||
name: "multiple", | ||
pattern: "/foo/{bar}/{baz}", | ||
url: "/foo/123/456", | ||
expected: map[string]string{"bar": "123", "baz": "456"}, | ||
}, | ||
{ | ||
name: "nested", | ||
pattern: "/foo/{bar}/baz/{qux}", | ||
url: "/foo/123/baz/456", | ||
expected: map[string]string{"bar": "123", "qux": "456"}, | ||
}, | ||
{ | ||
name: "empty", | ||
pattern: "/foo/{bar}", | ||
url: "/foo/", | ||
expected: map[string]string{"bar": ""}, | ||
}, | ||
{ | ||
name: "http method", | ||
pattern: "GET /foo/{bar}", | ||
url: "/foo/123", | ||
expected: map[string]string{"bar": "123"}, | ||
}, | ||
{ | ||
name: "host", | ||
pattern: "example.com/foo/{bar}", | ||
url: "http://example.com/foo/123", | ||
expected: map[string]string{"bar": "123"}, | ||
}, | ||
{ | ||
name: "host and method", | ||
pattern: "GET example.com/foo/{bar}", | ||
url: "http://example.com/foo/123", | ||
expected: map[string]string{"bar": "123"}, | ||
}, | ||
} { | ||
t.Run(tt.name, func(t *testing.T) { | ||
mux := NewServeMux() | ||
mux.HandleFunc(tt.pattern, func(_ http.ResponseWriter, r *http.Request) { | ||
params := patternValues(r) | ||
assert.Equal(t, tt.expected, params) | ||
}) | ||
|
||
r := httptest.NewRequest("GET", tt.url, nil) | ||
w := httptest.NewRecorder() | ||
mux.ServeHTTP(w, r) | ||
}) | ||
} | ||
} | ||
|
||
func TestPatternNames(t *testing.T) { | ||
tests := []struct { | ||
pattern string | ||
expected []string | ||
err bool | ||
}{ | ||
{"/foo/{bar}", []string{"bar"}, false}, | ||
{"/foo/{bar}/{baz}", []string{"bar", "baz"}, false}, | ||
{"/foo/{bar}/{bar}", nil, true}, | ||
{"/foo/{bar}...", nil, true}, | ||
{"/foo/{bar}.../baz", nil, true}, | ||
{"/foo/{bar}/{baz}...", nil, true}, | ||
{"/foo/{bar", nil, true}, | ||
{"/foo/{bar{baz}}", nil, true}, | ||
{"/foo/{bar!}", nil, true}, | ||
{"/foo/{}", nil, true}, | ||
{"{}", nil, true}, | ||
{"GET /foo/{bar}", []string{"bar"}, false}, | ||
{"POST /foo/{bar}/{baz}", []string{"bar", "baz"}, false}, | ||
{"PUT /foo/{bar}/{bar}", nil, true}, | ||
{"DELETE /foo/{bar}...", nil, true}, | ||
{"PATCH /foo/{bar}.../baz", nil, true}, | ||
{"OPTIONS /foo/{bar}/{baz}...", nil, true}, | ||
{"GET /foo/{bar", nil, true}, | ||
{"POST /foo/{bar{baz}}", nil, true}, | ||
{"PUT /foo/{bar!}", nil, true}, | ||
{"DELETE /foo/{}", nil, true}, | ||
{"OPTIONS {}", nil, true}, | ||
{"GET example.com/foo/{bar}", []string{"bar"}, false}, | ||
{"POST example.com/foo/{bar}/{baz}", []string{"bar", "baz"}, false}, | ||
{"PUT example.com/foo/{bar}/{bar}", nil, true}, | ||
{"DELETE example.com/foo/{bar}...", nil, true}, | ||
{"PATCH example.com/foo/{bar}.../baz", nil, true}, | ||
{"OPTIONS example.com/foo/{bar}/{baz}...", nil, true}, | ||
{"GET example.com/foo/{bar", nil, true}, | ||
{"POST example.com/foo/{bar{baz}}", nil, true}, | ||
{"PUT example.com/foo/{bar!}", nil, true}, | ||
{"DELETE example.com/foo/{}", nil, true}, | ||
{"OPTIONS example.com/{}", nil, true}, | ||
} | ||
|
||
for _, tt := range tests { | ||
t.Run(tt.pattern, func(t *testing.T) { | ||
names, err := patternNames(tt.pattern) | ||
if tt.err { | ||
assert.Error(t, err) | ||
assert.Nil(t, names) | ||
} else { | ||
assert.NoError(t, err) | ||
assert.Equal(t, tt.expected, names) | ||
} | ||
}) | ||
} | ||
} |