diff --git a/internal/reqdata/errors.go b/internal/reqdata/errors.go new file mode 100644 index 0000000..8706dbe --- /dev/null +++ b/internal/reqdata/errors.go @@ -0,0 +1,30 @@ +package reqdata + +// Error allows you to create constant "const" error with type boxing. +type Error string + +// Error implements the error builtin interface. +func (err Error) Error() string { + return string(err) +} + +// ErrUnknownType is returned when encountering an unknown type +const ErrUnknownType = Error("unknown type") + +// ErrInvalidJSON is returned when json parse failed +const ErrInvalidJSON = Error("invalid json") + +// ErrInvalidRootType is returned when json is a map +const ErrInvalidRootType = Error("invalid json root type") + +// ErrInvalidParamName - parameter has an invalid +const ErrInvalidParamName = Error("invalid parameter name") + +// ErrMissingRequiredParam - required param is missing +const ErrMissingRequiredParam = Error("missing required param") + +// ErrInvalidType - parameter value does not satisfy its type +const ErrInvalidType = Error("invalid type") + +// ErrMissingURIParameter - missing an URI parameter +const ErrMissingURIParameter = Error("missing URI parameter") diff --git a/internal/reqdata/parameter.go b/internal/reqdata/parameter.go index 2c80455..d7b5648 100644 --- a/internal/reqdata/parameter.go +++ b/internal/reqdata/parameter.go @@ -4,19 +4,8 @@ import ( "encoding/json" "fmt" "reflect" - - "git.xdrm.io/go/aicra/internal/cerr" ) -// ErrUnknownType is returned when encountering an unknown type -const ErrUnknownType = cerr.Error("unknown type") - -// ErrInvalidJSON is returned when json parse failed -const ErrInvalidJSON = cerr.Error("invalid json") - -// ErrInvalidRootType is returned when json is a map -const ErrInvalidRootType = cerr.Error("invalid json root type") - // Parameter represents an http request parameter // that can be of type URL, GET, or FORM (multipart, json, urlencoded) type Parameter struct { diff --git a/internal/reqdata/set.go b/internal/reqdata/set.go new file mode 100644 index 0000000..4b05d68 --- /dev/null +++ b/internal/reqdata/set.go @@ -0,0 +1,254 @@ +package reqdata + +import ( + "encoding/json" + "fmt" + + "git.xdrm.io/go/aicra/internal/config" + "git.xdrm.io/go/aicra/internal/multipart" + + "net/http" + "strings" +) + +// Set represents all data that can be caught: +// - URI (from the URI) +// - GET (default url data) +// - POST (from json, form-data, url-encoded) +// - 'application/json' => key-value pair is parsed as json into the map +// - 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters +// - 'multipart/form-data' => parse form-data format +type Set struct { + service *config.Service + + // contains URL+GET+FORM data with prefixes: + // - FORM: no prefix + // - URL: '{uri_var}' + // - GET: 'GET@' followed by the key in GET + Data map[string]*Parameter +} + +// New creates a new empty store. +func New(service *config.Service) *Set { + return &Set{ + service: service, + Data: make(map[string]*Parameter), + } +} + +// ExtractURI fills 'Set' with creating pointers inside 'Url' +func (i *Set) ExtractURI(req http.Request) error { + uriparts := config.SplitURL(req.RequestURI) + + for _, capture := range i.service.Captures { + // out of range + if capture.Index > len(uriparts)-1 { + return fmt.Errorf("%s: %w", capture.Name, ErrMissingURIParameter) + } + value := uriparts[capture.Index] + + // should not happen + if capture.Ref == nil { + return fmt.Errorf("%s: %w", capture.Name, ErrUnknownType) + } + + // check type + cast, valid := capture.Ref.Validator(value) + if !valid { + return fmt.Errorf("%s: %w", capture.Name, ErrInvalidType) + } + + // store cast value in 'Set' + i.Data[capture.Ref.Rename] = &Parameter{ + Value: cast, + } + + } + + return nil +} + +// ExtractQuery data from the url query parameters +func (i *Set) ExtractQuery(req *http.Request) error { + query := req.URL.Query() + + for name, param := range i.service.Query { + value, exist := query[name] + + // fail on missing required + if !exist && !param.Optional { + return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam) + } + + // optional + if !exist { + continue + } + + // check type + cast, valid := param.Validator(value) + if !valid { + return fmt.Errorf("%s: %w", name, ErrInvalidType) + } + + // store value + i.Data[param.Rename] = &Parameter{ + Value: cast, + } + } + + return nil +} + +// ExtractForm data from request +// +// - parse 'form-data' if not supported for non-POST requests +// - parse 'x-www-form-urlencoded' +// - parse 'application/json' +func (i *Set) ExtractForm(req *http.Request) error { + + // ignore GET method + if req.Method == http.MethodGet { + return nil + } + + contentType := req.Header.Get("Content-Type") + + // parse json + if strings.HasPrefix(contentType, "application/json") { + return i.parseJSON(req) + } + + // parse urlencoded + if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") { + return i.parseUrlencoded(req) + } + + // parse multipart + if strings.HasPrefix(contentType, "multipart/form-data; boundary=") { + return i.parseMultipart(req) + } + + // nothing to parse + return nil +} + +// parseJSON parses JSON from the request body inside 'Form' +// and 'Set' +func (i *Set) parseJSON(req *http.Request) error { + + parsed := make(map[string]interface{}, 0) + + decoder := json.NewDecoder(req.Body) + if err := decoder.Decode(&parsed); err != nil { + return err + } + + for name, param := range i.service.Form { + value, exist := parsed[name] + + // fail on missing required + if !exist && !param.Optional { + return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam) + } + + // optional + if !exist { + continue + } + + // fail on invalid type + cast, valid := param.Validator(value) + if !valid { + return fmt.Errorf("%s: %w", name, ErrInvalidType) + } + + // store value + i.Data[param.Rename] = &Parameter{ + Value: cast, + } + } + + return nil +} + +// parseUrlencoded parses urlencoded from the request body inside 'Form' +// and 'Set' +func (i *Set) parseUrlencoded(req *http.Request) error { + // use http.Request interface + if err := req.ParseForm(); err != nil { + return err + } + + for name, param := range i.service.Form { + value, exist := req.PostForm[name] + + // fail on missing required + if !exist && !param.Optional { + return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam) + } + + // optional + if !exist { + continue + } + + // check type + cast, valid := param.Validator(value) + if !valid { + return fmt.Errorf("%s: %w", name, ErrInvalidType) + } + + // store value + i.Data[param.Rename] = &Parameter{ + Value: cast, + } + } + + return nil +} + +// parseMultipart parses multi-part from the request body inside 'Form' +// and 'Set' +func (i *Set) parseMultipart(req *http.Request) error { + + // 1. create reader + boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):] + mpr, err := multipart.NewReader(req.Body, boundary) + if err != nil { + return err + } + + // 2. parse multipart + if err = mpr.Parse(); err != nil { + return err + } + + for name, param := range i.service.Form { + value, exist := mpr.Data[name] + + // fail on missing required + if !exist && !param.Optional { + return fmt.Errorf("%s: %w", name, ErrMissingRequiredParam) + } + + // optional + if !exist { + continue + } + + // fail on invalid type + cast, valid := param.Validator(value) + if !valid { + return fmt.Errorf("%s: %w", name, ErrInvalidType) + } + + // store value + i.Data[param.Rename] = &Parameter{ + Value: cast, + } + } + + return nil + +} diff --git a/internal/reqdata/store_test.go b/internal/reqdata/set_test.go similarity index 100% rename from internal/reqdata/store_test.go rename to internal/reqdata/set_test.go diff --git a/internal/reqdata/store.go b/internal/reqdata/store.go deleted file mode 100644 index 0a4d81e..0000000 --- a/internal/reqdata/store.go +++ /dev/null @@ -1,301 +0,0 @@ -package reqdata - -import ( - "encoding/json" - "fmt" - "log" - - "git.xdrm.io/go/aicra/internal/multipart" - - "net/http" - "strings" -) - -// Store represents all data that can be caught: -// - URI (guessed from the URI by removing the service path) -// - GET (default url data) -// - POST (from json, form-data, url-encoded) -type Store struct { - - // ordered values from the URI - // catches all after the service path - // - // points to Store.Data - URI []*Parameter - - // uri parameters following the QUERY format - // - // points to Store.Data - Get map[string]*Parameter - - // form data depending on the Content-Type: - // 'application/json' => key-value pair is parsed as json into the map - // 'application/x-www-form-urlencoded' => standard parameters as QUERY parameters - // 'multipart/form-data' => parse form-data format - // - // points to Store.Data - Form map[string]*Parameter - - // contains URL+GET+FORM data with prefixes: - // - FORM: no prefix - // - URL: 'URL#' followed by the index in Uri - // - GET: 'GET@' followed by the key in GET - Set map[string]*Parameter -} - -// New creates a new store from an http request. -// URI params is required because it only takes into account after service path -// we do not know in this scope. -func New(uriParams []string, req *http.Request) *Store { - ds := &Store{ - URI: make([]*Parameter, 0), - Get: make(map[string]*Parameter), - Form: make(map[string]*Parameter), - Set: make(map[string]*Parameter), - } - - // 1. set URI parameters - ds.setURIParams(uriParams) - - // ignore nil requests - if req == nil { - return ds - } - - // 2. GET (query) data - ds.readQuery(req) - - // 3. We are done if GET method - if req.Method == http.MethodGet { - return ds - } - - // 4. POST (body) data - ds.readForm(req) - - return ds -} - -// setURIParameters fills 'Set' with creating pointers inside 'Url' -func (i *Store) setURIParams(orderedUParams []string) { - - for index, value := range orderedUParams { - - // create set index - setindex := fmt.Sprintf("URL#%d", index) - - // store value in 'Set' - i.Set[setindex] = &Parameter{ - Parsed: false, - Value: value, - } - - // create link in 'Url' - i.URI = append(i.URI, i.Set[setindex]) - - } - -} - -// readQuery stores data from the QUERY (in url parameters) -func (i *Store) readQuery(req *http.Request) { - - for name, value := range req.URL.Query() { - - // prevent invalid names - if !isNameValid(name) { - log.Printf("invalid variable name: '%s'\n", name) - continue - } - - // prevent injections - if hasNameInjection(name) { - log.Printf("get.injection: '%s'\n", name) - continue - } - - // create set index - setindex := fmt.Sprintf("GET@%s", name) - - // store value in 'Set' - i.Set[setindex] = &Parameter{ - Parsed: false, - Value: value, - } - - // create link in 'Get' - i.Get[name] = i.Set[setindex] - - } - -} - -// readForm stores FORM data -// -// - parse 'form-data' if not supported (not POST requests) -// - parse 'x-www-form-urlencoded' -// - parse 'application/json' -func (i *Store) readForm(req *http.Request) { - - contentType := req.Header.Get("Content-Type") - - // parse json - if strings.HasPrefix(contentType, "application/json") { - i.parseJSON(req) - return - } - - // parse urlencoded - if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") { - i.parseUrlencoded(req) - return - } - - // parse multipart - if strings.HasPrefix(contentType, "multipart/form-data; boundary=") { - i.parseMultipart(req) - return - } - - // if unknown type store nothing -} - -// parseJSON parses JSON from the request body inside 'Form' -// and 'Set' -func (i *Store) parseJSON(req *http.Request) { - - parsed := make(map[string]interface{}, 0) - - decoder := json.NewDecoder(req.Body) - - // if parse error: do nothing - if err := decoder.Decode(&parsed); err != nil { - log.Printf("json.parse() %s\n", err) - return - } - - // else store values 'parsed' values - for name, value := range parsed { - - // prevent invalid names - if !isNameValid(name) { - log.Printf("invalid variable name: '%s'\n", name) - continue - } - - // prevent injections - if hasNameInjection(name) { - log.Printf("post.injection: '%s'\n", name) - continue - } - - // store value in 'Set' - i.Set[name] = &Parameter{ - Parsed: true, - Value: value, - } - - // create link in 'Form' - i.Form[name] = i.Set[name] - - } - -} - -// parseUrlencoded parses urlencoded from the request body inside 'Form' -// and 'Set' -func (i *Store) parseUrlencoded(req *http.Request) { - - // use http.Request interface - if err := req.ParseForm(); err != nil { - log.Printf("urlencoded.parse() %s\n", err) - return - } - - for name, value := range req.PostForm { - - // prevent invalid names - if !isNameValid(name) { - log.Printf("invalid variable name: '%s'\n", name) - continue - } - - // prevent injections - if hasNameInjection(name) { - log.Printf("post.injection: '%s'\n", name) - continue - } - - // store value in 'Set' - i.Set[name] = &Parameter{ - Parsed: false, - Value: value, - } - - // create link in 'Form' - i.Form[name] = i.Set[name] - } - -} - -// parseMultipart parses multi-part from the request body inside 'Form' -// and 'Set' -func (i *Store) parseMultipart(req *http.Request) { - - /* (1) Create reader */ - boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):] - mpr, err := multipart.NewReader(req.Body, boundary) - if err != nil { - return - } - - /* (2) Parse multipart */ - if err = mpr.Parse(); err != nil { - log.Printf("multipart.parse() %s\n", err) - return - } - - /* (3) Store data into 'Form' and 'Set */ - for name, data := range mpr.Data { - - // prevent invalid names - if !isNameValid(name) { - log.Printf("invalid variable name: '%s'\n", name) - continue - } - - // prevent injections - if hasNameInjection(name) { - log.Printf("post.injection: '%s'\n", name) - continue - } - - // store value in 'Set' - i.Set[name] = &Parameter{ - Parsed: false, - File: len(data.GetHeader("filename")) > 0, - Value: string(data.Data), - } - - // create link in 'Form' - i.Form[name] = i.Set[name] - - } - - return - -} - -// hasNameInjection returns whether there is -// a parameter name injection: -// - inferred GET parameters -// - inferred URL parameters -func hasNameInjection(pName string) bool { - return strings.HasPrefix(pName, "GET@") || strings.HasPrefix(pName, "URL#") -} - -// isNameValid returns whether a parameter name (without the GET@ or URL# prefix) is valid -// if fails if the name begins/ends with underscores -func isNameValid(pName string) bool { - return strings.Trim(pName, "_") == pName -}