diff --git a/http/pattern.go b/http/pattern.go new file mode 100644 index 0000000..f9aa67b --- /dev/null +++ b/http/pattern.go @@ -0,0 +1,208 @@ +// TINYGO: The following is copied and modified from Go 1.23.7 official implementation. + +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Patterns for ServeMux routing. + +package http + +import ( + "errors" + "fmt" + "net/url" + "strings" + "unicode" +) + +// A pattern is something that can be matched against an HTTP request. +// It has an optional method, an optional host, and a path. +type pattern struct { + str string // original string + method string + host string + // The representation of a path differs from the surface syntax, which + // simplifies most algorithms. + // + // Paths ending in '/' are represented with an anonymous "..." wildcard. + // For example, the path "a/" is represented as a literal segment "a" followed + // by a segment with multi==true. + // + // Paths ending in "{$}" are represented with the literal segment "/". + // For example, the path "a/{$}" is represented as a literal segment "a" followed + // by a literal segment "/". + segments []segment + loc string // source location of registering call, for helpful messages +} + +func (p *pattern) String() string { return p.str } + +func (p *pattern) lastSegment() segment { + return p.segments[len(p.segments)-1] +} + +// A segment is a pattern piece that matches one or more path segments, or +// a trailing slash. +// +// If wild is false, it matches a literal segment, or, if s == "/", a trailing slash. +// Examples: +// +// "a" => segment{s: "a"} +// "/{$}" => segment{s: "/"} +// +// If wild is true and multi is false, it matches a single path segment. +// Example: +// +// "{x}" => segment{s: "x", wild: true} +// +// If both wild and multi are true, it matches all remaining path segments. +// Example: +// +// "{rest...}" => segment{s: "rest", wild: true, multi: true} +type segment struct { + s string // literal or wildcard name or "/" for "/{$}". + wild bool + multi bool // "..." wildcard +} + +// parsePattern parses a string into a Pattern. +// The string's syntax is +// +// [METHOD] [HOST]/[PATH] +// +// where: +// - METHOD is an HTTP method +// - HOST is a hostname +// - PATH consists of slash-separated segments, where each segment is either +// a literal or a wildcard of the form "{name}", "{name...}", or "{$}". +// +// METHOD, HOST and PATH are all optional; that is, the string can be "/". +// If METHOD is present, it must be followed by at least one space or tab. +// Wildcard names must be valid Go identifiers. +// The "{$}" and "{name...}" wildcard must occur at the end of PATH. +// PATH may end with a '/'. +// Wildcard names in a path must be distinct. +func parsePattern(s string) (_ *pattern, err error) { + if len(s) == 0 { + return nil, errors.New("empty pattern") + } + off := 0 // offset into string + defer func() { + if err != nil { + err = fmt.Errorf("at offset %d: %w", off, err) + } + }() + + 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 = "" + } + if method != "" && !validMethod(method) { + return nil, fmt.Errorf("invalid method %q", method) + } + p := &pattern{str: s, method: method} + + if found { + off = len(method) + 1 + } + i := strings.IndexByte(rest, '/') + if i < 0 { + return nil, errors.New("host/path missing /") + } + p.host = rest[:i] + rest = rest[i:] + if j := strings.IndexByte(p.host, '{'); j >= 0 { + off += j + return nil, errors.New("host contains '{' (missing initial '/'?)") + } + // At this point, rest is the path. + off += i + + // An unclean path with a method that is not CONNECT can never match, + // because paths are cleaned before matching. + if method != "" && method != "CONNECT" && rest != cleanPath(rest) { + return nil, errors.New("non-CONNECT pattern with unclean path can never match") + } + + seenNames := map[string]bool{} // remember wildcard names to catch dups + for len(rest) > 0 { + // Invariant: rest[0] == '/'. + rest = rest[1:] + off = len(s) - len(rest) + if len(rest) == 0 { + // Trailing slash. + p.segments = append(p.segments, segment{wild: true, multi: true}) + 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 { + // Literal. + seg = pathUnescape(seg) + p.segments = append(p.segments, segment{s: seg}) + } else { + // 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") + } + p.segments = append(p.segments, segment{s: "/"}) + 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") + } + 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 + p.segments = append(p.segments, segment{s: name, wild: true, multi: multi}) + } + } + return p, 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 +} + +func pathUnescape(path string) string { + u, err := url.PathUnescape(path) + if err != nil { + // Invalidly escaped path; use the original + return path + } + return u +} diff --git a/http/request.go b/http/request.go index 146f783..3dac2b1 100644 --- a/http/request.go +++ b/http/request.go @@ -336,6 +336,11 @@ type Request struct { // TINYGO: Add onEOF func for callback when response is fully read // TINYGO: so we can close the connection. onEOF func() + + // The following fields are for requests matched by ServeMux. + pat *pattern // the pattern that matched + matches []string // values for the matching wildcards in pat + otherValues map[string]string // for calls to SetPathValue that don't match a wildcard } // Context returns the request's context. To change the context, use @@ -403,6 +408,20 @@ func (r *Request) Clone(ctx context.Context) *Request { r2.Form = cloneURLValues(r.Form) r2.PostForm = cloneURLValues(r.PostForm) r2.MultipartForm = cloneMultipartForm(r.MultipartForm) + + // Copy matches and otherValues. See issue 61410. + if s := r.matches; s != nil { + s2 := make([]string, len(s)) + copy(s2, s) + r2.matches = s2 + } + if s := r.otherValues; s != nil { + s2 := make(map[string]string, len(s)) + for k, v := range s { + s2[k] = v + } + r2.otherValues = s2 + } return r2 } @@ -1456,3 +1475,47 @@ func (r *Request) requiresHTTP1() bool { return hasToken(r.Header.Get("Connection"), "upgrade") && ascii.EqualFold(r.Header.Get("Upgrade"), "websocket") } + +// PathValue returns the value for the named path wildcard in the [ServeMux] pattern +// that matched the request. +// It returns the empty string if the request was not matched against a pattern +// or there is no such wildcard in the pattern. +func (r *Request) PathValue(name string) string { + if i := r.patIndex(name); i >= 0 { + return r.matches[i] + } + return r.otherValues[name] +} + +// SetPathValue sets name to value, so that subsequent calls to r.PathValue(name) +// return value. +func (r *Request) SetPathValue(name, value string) { + if i := r.patIndex(name); i >= 0 { + r.matches[i] = value + } else { + if r.otherValues == nil { + r.otherValues = map[string]string{} + } + r.otherValues[name] = value + } +} + +// patIndex returns the index of name in the list of named wildcards of the +// request's pattern, or -1 if there is no such name. +func (r *Request) patIndex(name string) int { + // The linear search seems expensive compared to a map, but just creating the map + // takes a lot of time, and most patterns will just have a couple of wildcards. + if r.pat == nil { + return -1 + } + i := 0 + for _, seg := range r.pat.segments { + if seg.wild && seg.s != "" { + if name == seg.s { + return i + } + i++ + } + } + return -1 +} diff --git a/http/server.go b/http/server.go index d108ae2..daa34d8 100644 --- a/http/server.go +++ b/http/server.go @@ -2361,6 +2361,33 @@ func (mux *ServeMux) match(path string) (h Handler, pattern string) { return v.h, v.pattern } + // Check for longest valid match with path placeholders (like /users/{id}) + // First collect all potential matching patterns + var matchingPatterns []string + for registeredPattern := range mux.m { + // Skip patterns ending with "/" as they are handled separately + if registeredPattern[len(registeredPattern)-1] == '/' { + continue + } + + // If the pattern contains placeholders and could match the path + if strings.Contains(registeredPattern, "{") && strings.Contains(registeredPattern, "}") { + if patternCouldMatch(registeredPattern, path) { + matchingPatterns = append(matchingPatterns, registeredPattern) + } + } + } + + // Find the longest matching pattern + if len(matchingPatterns) > 0 { + sort.Slice(matchingPatterns, func(i, j int) bool { + return len(matchingPatterns[i]) > len(matchingPatterns[j]) + }) + + pattern = matchingPatterns[0] + return mux.m[pattern].h, pattern + } + // Check for longest valid match. mux.es contains all patterns // that end in / sorted from longest to shortest. for _, e := range mux.es { @@ -2371,6 +2398,33 @@ func (mux *ServeMux) match(path string) (h Handler, pattern string) { return nil, "" } +// patternCouldMatch checks if a pattern with placeholders could match the given path. +// It handles patterns like /users/{id}/orders/{orderId}. +func patternCouldMatch(pattern, path string) bool { + patternParts := strings.Split(strings.Trim(pattern, "/"), "/") + pathParts := strings.Split(strings.Trim(path, "/"), "/") + + // If they have different number of parts, they can't match + if len(patternParts) != len(pathParts) { + return false + } + + // Check each part + for i, patternPart := range patternParts { + // If it's a placeholder, it matches anything + if strings.HasPrefix(patternPart, "{") && strings.HasSuffix(patternPart, "}") { + continue + } + + // If it's not a placeholder, it must match exactly + if patternPart != pathParts[i] { + return false + } + } + + return true +} + // redirectToPathSlash determines if the given path needs appending "/" to it. // This occurs when a handler for path + "/" was already registered, but // not for path itself. If the path needs appending to, it creates a new @@ -2480,6 +2534,36 @@ func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) { return } +// extractPathValues extracts path parameter values from a URL path based on a pattern. +// It handles patterns with placeholders like /users/{id}/orders/{orderId} +// and returns a map of parameter names to their values. +func extractPathValues(pattern, path string) map[string]string { + pathValues := make(map[string]string) + + // Split both pattern and path by "/" + patternParts := strings.Split(strings.Trim(pattern, "/"), "/") + pathParts := strings.Split(strings.Trim(path, "/"), "/") + + // If they have different lengths, they can't match (unless the pattern has a wildcard) + if len(patternParts) != len(pathParts) { + return pathValues + } + + // Compare each part + for i, patternPart := range patternParts { + if strings.HasPrefix(patternPart, "{") && strings.HasSuffix(patternPart, "}") { + // This is a placeholder like {id} + paramName := patternPart[1 : len(patternPart)-1] + pathValues[paramName] = pathParts[i] + } else if patternPart != pathParts[i] { + // If a non-placeholder part doesn't match, return empty + return make(map[string]string) + } + } + + return pathValues +} + // ServeHTTP dispatches the request to the handler whose // pattern most closely matches the request URL. func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { @@ -2490,7 +2574,18 @@ func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { w.WriteHeader(StatusBadRequest) return } - h, _ := mux.Handler(r) + h, pattern := mux.Handler(r) + + // Extract path values for patterns that contain placeholders like {id} + if strings.Contains(pattern, "{") && strings.Contains(pattern, "}") { + pathValues := extractPathValues(pattern, r.URL.Path) + + // Set path values in the request + for name, value := range pathValues { + r.SetPathValue(name, value) + } + } + h.ServeHTTP(w, r) }