1
0
mirror of https://github.com/rancher/norman.git synced 2025-06-02 20:25:13 +00:00
norman/parse/parse.go
Craig Jellick ba0bb125c9 Use request.PostForm instead of request.Form when
PostForm will ignore URL query parameters and will only look at the
body. This is much more useful for actions, which have a query param
that is the action name, but the a json body.  WIthout this change,
the only "field" found when parsing an action body is the name of the
action.
2018-03-15 09:10:03 -07:00

298 lines
7.2 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,
}
)
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) {
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"
}
return "json"
}
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
}