mirror of https://github.com/k3s-io/k3s
311 lines
7.6 KiB
Go
311 lines
7.6 KiB
Go
package parse
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"sort"
|
|
|
|
"github.com/rancher/norman/api/builtin"
|
|
"github.com/rancher/norman/httperror"
|
|
"github.com/rancher/norman/types"
|
|
"github.com/rancher/norman/urlbuilder"
|
|
)
|
|
|
|
const (
|
|
maxFormSize = 2 * 1 << 20
|
|
)
|
|
|
|
var (
|
|
multiSlashRegexp = regexp.MustCompile("//+")
|
|
allowedFormats = map[string]bool{
|
|
"html": true,
|
|
"json": true,
|
|
"yaml": true,
|
|
}
|
|
)
|
|
|
|
type ParsedURL struct {
|
|
Version *types.APIVersion
|
|
SchemasVersion *types.APIVersion
|
|
Type string
|
|
ID string
|
|
Link string
|
|
Method string
|
|
Action string
|
|
SubContext map[string]string
|
|
SubContextPrefix string
|
|
Query url.Values
|
|
}
|
|
|
|
type ResolverFunc func(typeName string, context *types.APIContext) error
|
|
|
|
type URLParser func(schema *types.Schemas, url *url.URL) (ParsedURL, error)
|
|
|
|
func DefaultURLParser(schemas *types.Schemas, url *url.URL) (ParsedURL, error) {
|
|
result := ParsedURL{}
|
|
|
|
path := url.EscapedPath()
|
|
path = multiSlashRegexp.ReplaceAllString(path, "/")
|
|
schemaVersion, version, prefix, parts, subContext := parseVersionAndSubContext(schemas, path)
|
|
|
|
if version == nil {
|
|
return result, nil
|
|
}
|
|
|
|
result.Version = version
|
|
result.SchemasVersion = schemaVersion
|
|
result.SubContext = subContext
|
|
result.SubContextPrefix = prefix
|
|
result.Action, result.Method = parseAction(url)
|
|
result.Query = url.Query()
|
|
|
|
result.Type = safeIndex(parts, 0)
|
|
result.ID = safeIndex(parts, 1)
|
|
result.Link = safeIndex(parts, 2)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func Parse(rw http.ResponseWriter, req *http.Request, schemas *types.Schemas, urlParser URLParser, resolverFunc ResolverFunc) (*types.APIContext, error) {
|
|
var err error
|
|
|
|
result := types.NewAPIContext(req, rw, schemas)
|
|
result.Method = parseMethod(req)
|
|
result.ResponseFormat = parseResponseFormat(req)
|
|
result.URLBuilder, _ = urlbuilder.New(req, types.APIVersion{}, schemas)
|
|
|
|
// The response format is guarenteed to be set even in the event of an error
|
|
parsedURL, err := urlParser(schemas, req.URL)
|
|
// wait to check error, want to set as much as possible
|
|
|
|
result.SubContext = parsedURL.SubContext
|
|
result.Type = parsedURL.Type
|
|
result.ID = parsedURL.ID
|
|
result.Link = parsedURL.Link
|
|
result.Action = parsedURL.Action
|
|
result.Query = parsedURL.Query
|
|
if parsedURL.Method != "" {
|
|
result.Method = parsedURL.Method
|
|
}
|
|
|
|
result.Version = parsedURL.Version
|
|
result.SchemasVersion = parsedURL.SchemasVersion
|
|
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
if result.Version == nil {
|
|
result.Method = http.MethodGet
|
|
result.URLBuilder, err = urlbuilder.New(req, types.APIVersion{}, result.Schemas)
|
|
result.Type = "apiRoot"
|
|
result.Schema = result.Schemas.Schema(&builtin.Version, "apiRoot")
|
|
return result, nil
|
|
}
|
|
|
|
result.URLBuilder, err = urlbuilder.New(req, *result.Version, result.Schemas)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
|
|
if parsedURL.SubContextPrefix != "" {
|
|
result.URLBuilder.SetSubContext(parsedURL.SubContextPrefix)
|
|
}
|
|
|
|
if err := resolverFunc(result.Type, result); err != nil {
|
|
return result, err
|
|
}
|
|
|
|
if result.Schema == nil {
|
|
if result.Type != "" {
|
|
err = httperror.NewAPIError(httperror.NotFound, "failed to find schema "+result.Type)
|
|
}
|
|
result.Method = http.MethodGet
|
|
result.Type = "apiRoot"
|
|
result.Schema = result.Schemas.Schema(&builtin.Version, "apiRoot")
|
|
result.ID = result.Version.Path
|
|
return result, err
|
|
}
|
|
|
|
result.Type = result.Schema.ID
|
|
|
|
if err := ValidateMethod(result); err != nil {
|
|
return result, err
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func versionsForPath(schemas *types.Schemas, path string) []types.APIVersion {
|
|
var matchedVersion []types.APIVersion
|
|
for _, version := range schemas.Versions() {
|
|
if strings.HasPrefix(path, version.Path) {
|
|
afterPath := path[len(version.Path):]
|
|
// if version.Path is /v3/cluster allow /v3/clusters but not /v3/clusterstuff
|
|
if len(afterPath) < 3 || strings.Contains(afterPath[:3], "/") {
|
|
matchedVersion = append(matchedVersion, version)
|
|
}
|
|
}
|
|
}
|
|
sort.Slice(matchedVersion, func(i, j int) bool {
|
|
return len(matchedVersion[i].Path) > len(matchedVersion[j].Path)
|
|
})
|
|
return matchedVersion
|
|
}
|
|
|
|
func parseVersionAndSubContext(schemas *types.Schemas, escapedPath string) (*types.APIVersion, *types.APIVersion, string, []string, map[string]string) {
|
|
versions := versionsForPath(schemas, escapedPath)
|
|
if len(versions) == 0 {
|
|
return nil, nil, "", nil, nil
|
|
}
|
|
version := &versions[0]
|
|
|
|
if strings.HasSuffix(escapedPath, "/") {
|
|
escapedPath = escapedPath[:len(escapedPath)-1]
|
|
}
|
|
|
|
versionParts := strings.Split(version.Path, "/")
|
|
pp := strings.Split(escapedPath, "/")
|
|
var pathParts []string
|
|
for _, p := range pp {
|
|
part, err := url.PathUnescape(p)
|
|
if err == nil {
|
|
pathParts = append(pathParts, part)
|
|
} else {
|
|
pathParts = append(pathParts, p)
|
|
}
|
|
}
|
|
|
|
paths := pathParts[len(versionParts):]
|
|
|
|
if !version.SubContext || len(versions) < 2 {
|
|
return nil, version, "", paths, nil
|
|
}
|
|
|
|
// Handle the special case of /v3/clusters/schema(s)
|
|
if len(paths) >= 1 && (paths[0] == "schema" || paths[0] == "schemas") {
|
|
return nil, version, "", paths, nil
|
|
}
|
|
|
|
if len(paths) < 2 {
|
|
// Handle case like /v3/clusters/foo where /v3 and /v3/clusters are API versions.
|
|
// In this situation you want the version to be /v3 and the path "clusters", "foo"
|
|
newVersion := versions[0]
|
|
if len(paths) > 0 {
|
|
newVersion.Path = newVersion.Path + "/" + paths[0]
|
|
}
|
|
return &newVersion, &versions[1], "", pathParts[len(versionParts)-1:], nil
|
|
}
|
|
|
|
// Length is always >= 3
|
|
|
|
attrs := map[string]string{
|
|
version.SubContextSchema: paths[0],
|
|
}
|
|
|
|
for i, version := range versions {
|
|
schema := schemas.Schema(&version, paths[1])
|
|
if schema != nil {
|
|
if i == 0 {
|
|
break
|
|
}
|
|
return nil, &version, "", paths[1:], attrs
|
|
}
|
|
}
|
|
|
|
return nil, version, "/" + paths[0], paths[1:], attrs
|
|
}
|
|
|
|
func DefaultResolver(typeName string, apiContext *types.APIContext) error {
|
|
if typeName == "" {
|
|
return nil
|
|
}
|
|
|
|
schema := apiContext.Schemas.Schema(apiContext.Version, typeName)
|
|
if schema == nil && (typeName == builtin.Schema.ID || typeName == builtin.Schema.PluralName) {
|
|
// Schemas are special, we include it as though part of the API request version
|
|
schema = apiContext.Schemas.Schema(&builtin.Version, typeName)
|
|
}
|
|
if schema == nil {
|
|
return nil
|
|
}
|
|
|
|
apiContext.Schema = schema
|
|
return nil
|
|
}
|
|
|
|
func safeIndex(slice []string, index int) string {
|
|
if index >= len(slice) {
|
|
return ""
|
|
}
|
|
return slice[index]
|
|
}
|
|
|
|
func parseResponseFormat(req *http.Request) string {
|
|
format := req.URL.Query().Get("_format")
|
|
|
|
if format != "" {
|
|
format = strings.TrimSpace(strings.ToLower(format))
|
|
}
|
|
|
|
/* Format specified */
|
|
if allowedFormats[format] {
|
|
return format
|
|
}
|
|
|
|
// User agent has Mozilla and browser accepts */*
|
|
if IsBrowser(req, true) {
|
|
return "html"
|
|
}
|
|
|
|
if isYaml(req) {
|
|
return "yaml"
|
|
}
|
|
return "json"
|
|
}
|
|
|
|
func isYaml(req *http.Request) bool {
|
|
return strings.Contains(req.Header.Get("Accept"), "application/yaml")
|
|
}
|
|
|
|
func parseMethod(req *http.Request) string {
|
|
method := req.URL.Query().Get("_method")
|
|
if method == "" {
|
|
method = req.Method
|
|
}
|
|
return method
|
|
}
|
|
|
|
func parseAction(url *url.URL) (string, string) {
|
|
action := url.Query().Get("action")
|
|
if action == "remove" {
|
|
return "", http.MethodDelete
|
|
}
|
|
|
|
return action, ""
|
|
}
|
|
|
|
func Body(req *http.Request) (map[string]interface{}, error) {
|
|
req.ParseMultipartForm(maxFormSize)
|
|
if req.MultipartForm != nil {
|
|
return valuesToBody(req.MultipartForm.Value), nil
|
|
}
|
|
|
|
if req.PostForm != nil && len(req.PostForm) > 0 {
|
|
return valuesToBody(map[string][]string(req.Form)), nil
|
|
}
|
|
|
|
return ReadBody(req)
|
|
}
|
|
|
|
func valuesToBody(input map[string][]string) map[string]interface{} {
|
|
result := map[string]interface{}{}
|
|
for k, v := range input {
|
|
result[k] = v
|
|
}
|
|
return result
|
|
}
|