diff --git a/lib/client/https_client.go b/lib/client/https_client.go index 11e263a6d519e..f382bb2354759 100644 --- a/lib/client/https_client.go +++ b/lib/client/https_client.go @@ -61,7 +61,7 @@ func httpTransport(insecure bool, pool *x509.CertPool) *http.Transport { func NewWebClient(url string, opts ...roundtrip.ClientParam) (*WebClient, error) { opts = append(opts, roundtrip.SanitizerEnabled(true)) - clt, err := roundtrip.NewClient(url, "", opts...) + clt, err := roundtrip.NewClient(url, "" /* version prefix */, opts...) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/httplib/httplib.go b/lib/httplib/httplib.go index 8b241584817da..b354f16c8de03 100644 --- a/lib/httplib/httplib.go +++ b/lib/httplib/httplib.go @@ -212,14 +212,25 @@ func ConvertResponse(re *roundtrip.Response, err error) (*roundtrip.Response, er return re, trace.ReadError(re.Code(), re.Bytes()) } +// Version describes the parts of a semver version +// in the format: major.minor.patch-preRelease type Version struct { - Major int64 `json:"major"` - Minor int64 `json:"minor"` - Patch int64 `json:"patch"` - String string `json:"string"` + // Major is the first part of version. + Major int64 `json:"major"` + // Minor is the second part of version. + Minor int64 `json:"minor"` + // Patch is the third part of version. + Patch int64 `json:"patch"` + // PreRelease is only defined if there was a hyphen + // and a word at the end of version eg: the prerelease + // value of version 18.0.0-dev is "dev". PreRelease string `json:"preRelease"` + // String contains the whole version. + String string `json:"string"` } +// ReplyRouteNotFoundJSONWithVersionField writes a JSON error reply containing +// a not found error, a Version object, and a not found HTTP status code. func ReplyRouteNotFoundJSONWithVersionField(w http.ResponseWriter, versionStr string) { SetDefaultSecurityHeaders(w.Header()) diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 5fbc854daf549..588d9a5eec4ac 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -614,6 +614,7 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { h.nodeWatcher = cfg.NodeWatcher } + v1Prefix := "/v1" notFoundRoutingHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Request is going to the API? // If no routes were matched, it could be because it's a path with `v1` prefix @@ -624,14 +625,18 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*APIHandler, error) { // and should be kept that way to prevent breakage. // // v2+ prefixes will be expected by both caller and definition and will not be stripped. - if matches := apiPrefixRegex.FindStringSubmatch(r.URL.Path); matches != nil && len(matches) == 3 { - postVersionPrefixPath := fmt.Sprintf("/%s", matches[2]) - versionNum := matches[1] - - // Regex check the rest of path to ensure we aren't allowing paths like /v1/v2/webapi - if matches := apiPrefixRegex.FindStringSubmatch(postVersionPrefixPath); matches == nil && versionNum == "1" { - http.StripPrefix("/v1", h).ServeHTTP(w, r) - return + if strings.HasPrefix(r.URL.Path, v1Prefix) { + pathParts := strings.Split(r.URL.Path, "/") + if len(pathParts) > 2 { + // check against known second part of path to ensure we + // aren't allowing paths like /v1/v2/webapi + // part[0] is empty space from leading slash "/" + // part[1] is the prefix "v1" + switch pathParts[2] { + case "webapi", "enterprise", "scripts", ".well-known", "workload-identity": + http.StripPrefix(v1Prefix, h).ServeHTTP(w, r) + return + } } httplib.ReplyRouteNotFoundJSONWithVersionField(w, teleport.Version) return diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index e1f1e87e4fcfe..df3a53159c741 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -3491,7 +3491,7 @@ func TestEndpointNotFoundHandling(t *testing.T) { }, { name: "invalid just prefix", - endpoint: "v1/", + endpoint: "v1", shouldErr: true, }, {