moved data parser into 'request_data' + created internal lib to parse multipart inside 'internal/multipart' + added default type 'any' + more things...
This commit is contained in:
parent
0641bb9131
commit
8f9c86c391
|
@ -0,0 +1,9 @@
|
|||
package main
|
||||
|
||||
func Match(name string) bool {
|
||||
return name == "any"
|
||||
}
|
||||
|
||||
func Check(value interface{}) bool {
|
||||
return true
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package multipart
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Read all until the next boundary is found
|
||||
func (i *MultipartReader) readComponent() ([]string, error) {
|
||||
|
||||
component := make([]string, 0)
|
||||
|
||||
for { // Read until boundary or error
|
||||
|
||||
line, _, err := i.reader.ReadLine()
|
||||
|
||||
/* (1) Stop on error */
|
||||
if err != nil {
|
||||
return component, err
|
||||
}
|
||||
|
||||
/* (2) Stop at boundary */
|
||||
if strings.HasPrefix(string(line), i.boundary) {
|
||||
return component, err
|
||||
}
|
||||
|
||||
/* (3) Ignore empty lines */
|
||||
if len(line) > 0 {
|
||||
component = append(component, string(line))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Parses a single component from its raw lines
|
||||
func (i *MultipartReader) parseComponent(line []string) error {
|
||||
|
||||
// next line index to use
|
||||
cursor := 1
|
||||
|
||||
/* (1) Fail if invalid line count */
|
||||
if len(line) < 2 {
|
||||
return fmt.Errorf("Missing data to parse component")
|
||||
}
|
||||
|
||||
/* (2) Split meta data */
|
||||
meta := strings.Split(line[0], "; ")
|
||||
|
||||
if len(meta) < 2 {
|
||||
return fmt.Errorf("Missing component meta data")
|
||||
}
|
||||
|
||||
/* (3) Extract name */
|
||||
if !strings.HasPrefix(meta[1], `name="`) {
|
||||
return fmt.Errorf("Cannot extract component name")
|
||||
}
|
||||
name := meta[1][len(`name="`) : len(meta[1])-1]
|
||||
|
||||
/* (4) Check if it is a file */
|
||||
isFile := len(meta) > 2 && strings.HasPrefix(meta[2], `filename="`)
|
||||
|
||||
// skip next line (Content-Type) if file
|
||||
if isFile {
|
||||
cursor++
|
||||
}
|
||||
|
||||
/* (5) Create index if name not already used */
|
||||
already, isset := i.Components[name]
|
||||
if !isset {
|
||||
|
||||
i.Components[name] = &MultipartComponent{
|
||||
File: isFile,
|
||||
Data: make([]string, 0),
|
||||
}
|
||||
already = i.Components[name]
|
||||
|
||||
}
|
||||
|
||||
/* (6) Store new value */
|
||||
already.Data = append(already.Data, strings.Join(line[cursor:], "\n"))
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package multipart
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Creates a new multipart reader from an http.Request
|
||||
func CreateReader(req *http.Request) *MultipartReader {
|
||||
|
||||
/* (1) extract boundary */
|
||||
boundary := req.Header.Get("Content-Type")[len("multipart/form-data; boundary="):]
|
||||
boundary = fmt.Sprintf("--%s", boundary)
|
||||
|
||||
/* (2) init reader */
|
||||
i := &MultipartReader{
|
||||
reader: bufio.NewReader(req.Body),
|
||||
boundary: boundary,
|
||||
Components: make(map[string]*MultipartComponent),
|
||||
}
|
||||
|
||||
/* (3) Place reader cursor after first boundary */
|
||||
var (
|
||||
err error
|
||||
line []byte
|
||||
)
|
||||
|
||||
for err == nil && string(line) != boundary {
|
||||
line, _, err = i.reader.ReadLine()
|
||||
}
|
||||
|
||||
return i
|
||||
|
||||
}
|
||||
|
||||
// Parses the multipart components from the request
|
||||
func (i *MultipartReader) Parse() error {
|
||||
|
||||
/* (1) For each component (until boundary) */
|
||||
for {
|
||||
|
||||
// 1. Read component
|
||||
component, err := i.readComponent()
|
||||
|
||||
// 2. Stop at EOF
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 3. Dispatch error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 4. parse component
|
||||
err = i.parseComponent(component)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("%s\n", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package multipart
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
)
|
||||
|
||||
type MultipartReader struct {
|
||||
// reader used for http.Request.Body reading
|
||||
reader *bufio.Reader
|
||||
|
||||
// boundary used to separate multipart components
|
||||
boundary string
|
||||
|
||||
// result will be inside this field
|
||||
Components map[string]*MultipartComponent
|
||||
}
|
||||
|
||||
// Represents a multipart component
|
||||
type MultipartComponent struct {
|
||||
// whether this component is a file
|
||||
// if not, it is a simple variable data
|
||||
File bool
|
||||
|
||||
// actual data
|
||||
Data []string
|
||||
}
|
|
@ -14,61 +14,15 @@ import (
|
|||
// from a http.Request
|
||||
func buildRequest(req *http.Request) (*Request, error) {
|
||||
|
||||
/* (1) Init request */
|
||||
/* (1) Get useful data */
|
||||
uri := NormaliseUri(req.URL.Path)
|
||||
rawpost := FetchFormData(req)
|
||||
rawget := FetchGetData(req)
|
||||
uriparts := strings.Split(uri, "/")
|
||||
|
||||
/* (2) Init request */
|
||||
inst := &Request{
|
||||
Uri: strings.Split(uri, "/"),
|
||||
GetData: make(map[string]interface{}, 0),
|
||||
FormData: make(map[string]interface{}, 0),
|
||||
UrlData: make([]interface{}, 0),
|
||||
Data: make(map[string]interface{}, 0),
|
||||
}
|
||||
inst.ControllerUri = make([]string, 0, len(inst.Uri))
|
||||
|
||||
/* (2) Fill 'Data' with GET data */
|
||||
for name, rawdata := range rawget {
|
||||
|
||||
// 1. Parse arguments
|
||||
data := parseHttpData(rawdata)
|
||||
|
||||
if data == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. prevent injections
|
||||
if isParameterNameInjection(name) {
|
||||
log.Printf("get.name_injection: '%s'\n", name)
|
||||
delete(inst.GetData, name)
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. add into data
|
||||
inst.GetData[name] = data
|
||||
inst.Data[fmt.Sprintf("GET@%s", name)] = data
|
||||
}
|
||||
|
||||
/* (3) Fill 'Data' with POST data */
|
||||
for name, rawdata := range rawpost {
|
||||
|
||||
// 1. Parse arguments
|
||||
data := parseHttpData(rawdata)
|
||||
|
||||
if data == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. prevent injections
|
||||
if isParameterNameInjection(name) {
|
||||
log.Printf("post.name_injection: '%s'\n", name)
|
||||
delete(inst.FormData, name)
|
||||
continue
|
||||
}
|
||||
|
||||
// 3. add into data
|
||||
inst.Data[name] = data
|
||||
inst.FormData[name] = data
|
||||
Uri: uriparts,
|
||||
ControllerUri: make([]string, 0, len(uriparts)),
|
||||
Data: buildRequestDataFromRequest(req),
|
||||
}
|
||||
|
||||
return inst, nil
|
||||
|
@ -93,20 +47,6 @@ func NormaliseUri(uri string) string {
|
|||
return uri
|
||||
}
|
||||
|
||||
// FetchGetData extracts the GET data
|
||||
// from an HTTP request
|
||||
func FetchGetData(req *http.Request) map[string]interface{} {
|
||||
|
||||
res := make(map[string]interface{})
|
||||
|
||||
for name, value := range req.URL.Query() {
|
||||
res[name] = value
|
||||
}
|
||||
|
||||
return res
|
||||
|
||||
}
|
||||
|
||||
// FetchFormData extracts FORM data
|
||||
//
|
||||
// - parse 'form-data' if not supported (not POST requests)
|
||||
|
@ -167,16 +107,10 @@ func FetchFormData(req *http.Request) map[string]interface{} {
|
|||
return res
|
||||
}
|
||||
|
||||
// isParameterNameInjection returns whether there is
|
||||
// a parameter name injection:
|
||||
// - inferred GET parameters
|
||||
// - inferred URL parameters
|
||||
func isParameterNameInjection(pName string) bool {
|
||||
return strings.HasPrefix(pName, "GET@") || strings.HasPrefix(pName, "URL#")
|
||||
}
|
||||
|
||||
// parseHttpData parses http GET/POST data
|
||||
// - []string of 1 element : return json of element 0
|
||||
// - []string
|
||||
// - size = 1 : return json of first element
|
||||
// - size > 1 : return array of json elements
|
||||
// - string : return json if valid, else return raw string
|
||||
func parseHttpData(data interface{}) interface{} {
|
||||
dtype := reflect.TypeOf(data)
|
||||
|
@ -252,6 +186,6 @@ func parseHttpData(data interface{}) interface{} {
|
|||
}
|
||||
|
||||
/* (3) NIL if unknown type */
|
||||
return nil
|
||||
return dvalue
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
package gfw
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.xdrm.io/gfw/internal/multipart"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// buildRequestDataFromRequest builds a 'RequestData'
|
||||
// from an http request
|
||||
func buildRequestDataFromRequest(req *http.Request) *RequestData {
|
||||
|
||||
i := &RequestData{
|
||||
Url: make([]*RequestParameter, 0),
|
||||
Get: make(map[string]*RequestParameter),
|
||||
Form: make(map[string]*RequestParameter),
|
||||
Set: make(map[string]*RequestParameter),
|
||||
}
|
||||
|
||||
// GET (query) data
|
||||
i.fetchGet(req)
|
||||
|
||||
// no Form if GET
|
||||
if req.Method == "GET" {
|
||||
return i
|
||||
}
|
||||
|
||||
// POST (body) data
|
||||
i.fetchForm(req)
|
||||
|
||||
return i
|
||||
|
||||
}
|
||||
|
||||
// bindUrl stores URL data and fills 'Set'
|
||||
// with creating pointers inside 'Url'
|
||||
func (i *RequestData) fillUrl(data []string) {
|
||||
|
||||
for index, value := range data {
|
||||
|
||||
// create set index
|
||||
setindex := fmt.Sprintf("URL#%d", index)
|
||||
|
||||
// store value in 'Set'
|
||||
i.Set[setindex] = &RequestParameter{
|
||||
Parsed: false,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
// create link in 'Url'
|
||||
i.Url = append(i.Url, i.Set[setindex])
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// fetchGet stores data from the QUERY (in url parameters)
|
||||
func (i *RequestData) fetchGet(req *http.Request) {
|
||||
|
||||
for name, value := range req.URL.Query() {
|
||||
|
||||
// prevent injections
|
||||
if isParameterNameInjection(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] = &RequestParameter{
|
||||
Parsed: false,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
// create link in 'Get'
|
||||
i.Get[name] = i.Set[setindex]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// fetchForm stores FORM data
|
||||
//
|
||||
// - parse 'form-data' if not supported (not POST requests)
|
||||
// - parse 'x-www-form-urlencoded'
|
||||
// - parse 'application/json'
|
||||
func (i *RequestData) fetchForm(req *http.Request) {
|
||||
|
||||
contentType := req.Header.Get("Content-Type")
|
||||
|
||||
// parse json
|
||||
if strings.HasPrefix(contentType, "application/json") {
|
||||
i.parseJsonForm(req)
|
||||
return
|
||||
}
|
||||
|
||||
// parse urlencoded
|
||||
if strings.HasPrefix(contentType, "application/x-www-form-urlencoded") {
|
||||
i.parseUrlencodedForm(req)
|
||||
return
|
||||
}
|
||||
|
||||
// parse multipart
|
||||
if strings.HasPrefix(contentType, "multipart/form-data; boundary=") {
|
||||
i.parseMultipartForm(req)
|
||||
return
|
||||
}
|
||||
|
||||
// if unknown type store nothing
|
||||
}
|
||||
|
||||
// parseJsonForm parses JSON from the request body inside 'Form'
|
||||
// and 'Set'
|
||||
func (i *RequestData) parseJsonForm(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 {
|
||||
return
|
||||
}
|
||||
|
||||
// else store values 'parsed' values
|
||||
for name, value := range parsed {
|
||||
|
||||
// prevent injections
|
||||
if isParameterNameInjection(name) {
|
||||
log.Printf("post.injection: '%s'\n", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// store value in 'Set'
|
||||
i.Set[name] = &RequestParameter{
|
||||
Parsed: true,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
// create link in 'Form'
|
||||
i.Form[name] = i.Set[name]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// parseUrlencodedForm parses urlencoded from the request body inside 'Form'
|
||||
// and 'Set'
|
||||
func (i *RequestData) parseUrlencodedForm(req *http.Request) {
|
||||
|
||||
// use http.Request interface
|
||||
req.ParseForm()
|
||||
|
||||
for name, value := range req.PostForm {
|
||||
|
||||
// prevent injections
|
||||
if isParameterNameInjection(name) {
|
||||
log.Printf("post.injection: '%s'\n", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// store value in 'Set'
|
||||
i.Set[name] = &RequestParameter{
|
||||
Parsed: false,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
// create link in 'Form'
|
||||
i.Form[name] = i.Set[name]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// parseMultipartForm parses multi-part from the request body inside 'Form'
|
||||
// and 'Set'
|
||||
func (i *RequestData) parseMultipartForm(req *http.Request) {
|
||||
|
||||
/* (1) Create reader */
|
||||
mpr := multipart.CreateReader(req)
|
||||
|
||||
/* (2) Parse multipart */
|
||||
mpr.Parse()
|
||||
|
||||
/* (3) Store data into 'Form' and 'Set */
|
||||
for name, component := range mpr.Components {
|
||||
|
||||
// prevent injections
|
||||
if isParameterNameInjection(name) {
|
||||
log.Printf("post.injection: '%s'\n", name)
|
||||
continue
|
||||
}
|
||||
|
||||
// store value in 'Set'
|
||||
i.Set[name] = &RequestParameter{
|
||||
Parsed: false,
|
||||
File: component.File,
|
||||
Value: component.Data,
|
||||
}
|
||||
|
||||
// create link in 'Form'
|
||||
i.Form[name] = i.Set[name]
|
||||
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// isParameterNameInjection returns whether there is
|
||||
// a parameter name injection:
|
||||
// - inferred GET parameters
|
||||
// - inferred URL parameters
|
||||
func isParameterNameInjection(pName string) bool {
|
||||
return strings.HasPrefix(pName, "GET@") || strings.HasPrefix(pName, "URL#")
|
||||
}
|
23
router.go
23
router.go
|
@ -41,14 +41,8 @@ func (s *Server) route(res http.ResponseWriter, req *http.Request) {
|
|||
|
||||
}
|
||||
|
||||
/* (3) Extract URI params */
|
||||
uriParams := request.Uri[uriIndex:]
|
||||
|
||||
/* (4) Store them as Data */
|
||||
for i, data := range uriParams {
|
||||
request.UrlData = append(request.UrlData, data)
|
||||
request.Data[fmt.Sprintf("URL#%d", i)] = data
|
||||
}
|
||||
/* (3) Extract & store URI params */
|
||||
request.Data.fillUrl(request.Uri[uriIndex:])
|
||||
|
||||
/* (3) Check method
|
||||
---------------------------------------------------------*/
|
||||
|
@ -82,7 +76,7 @@ func (s *Server) route(res http.ResponseWriter, req *http.Request) {
|
|||
fmt.Printf("- %s: %v | '%v'\n", name, *param.Optional, *param.Rename)
|
||||
|
||||
/* (1) Extract value */
|
||||
value, isset := request.Data[name]
|
||||
p, isset := request.Data.Set[name]
|
||||
|
||||
/* (2) OPTIONAL ? */
|
||||
if !isset {
|
||||
|
@ -99,20 +93,23 @@ func (s *Server) route(res http.ResponseWriter, req *http.Request) {
|
|||
paramError.BindArgument(name)
|
||||
break
|
||||
|
||||
// set default value if optional
|
||||
// set default p if optional
|
||||
} else {
|
||||
value = *param.Default
|
||||
p = &RequestParameter{
|
||||
Parsed: true,
|
||||
Value: *param.Default,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* (3) Check type */
|
||||
isValid := s.Checker.Run(param.Type, value)
|
||||
isValid := s.Checker.Run(param.Type, p.Value)
|
||||
if isValid != nil {
|
||||
paramError = ErrInvalidParam
|
||||
paramError.BindArgument(name)
|
||||
paramError.BindArgument(param.Type)
|
||||
paramError.BindArgument(value)
|
||||
paramError.BindArgument(p.Value)
|
||||
break
|
||||
}
|
||||
|
||||
|
|
56
types.go
56
types.go
|
@ -13,10 +13,56 @@ type Server struct {
|
|||
}
|
||||
|
||||
type Request struct {
|
||||
Uri []string
|
||||
// corresponds to the list of uri components
|
||||
// featuring in the request URI
|
||||
Uri []string
|
||||
|
||||
// portion of the URI that corresponds to the controllerpath
|
||||
ControllerUri []string
|
||||
FormData map[string]interface{}
|
||||
GetData map[string]interface{}
|
||||
UrlData []interface{}
|
||||
Data map[string]interface{}
|
||||
|
||||
// contains all data from URL, GET, and FORM
|
||||
Data *RequestData
|
||||
}
|
||||
|
||||
type RequestData struct {
|
||||
|
||||
// ordered values from the URI
|
||||
// catches all after the controller path
|
||||
//
|
||||
// points to Request.Data
|
||||
Url []*RequestParameter
|
||||
|
||||
// uri parameters following the QUERY format
|
||||
//
|
||||
// points to Request.Data
|
||||
Get map[string]*RequestParameter
|
||||
|
||||
// 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 Request.Data
|
||||
Form map[string]*RequestParameter
|
||||
|
||||
// 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]*RequestParameter
|
||||
}
|
||||
|
||||
// RequestParameter represents an http request parameter
|
||||
// that can be of type URL, GET, or FORM (multipart, json, urlencoded)
|
||||
type RequestParameter struct {
|
||||
// whether the value has been json-parsed
|
||||
// for optimisation purpose, parameters are only parsed
|
||||
// if they are required by the current controller
|
||||
Parsed bool
|
||||
|
||||
// whether the value is a file
|
||||
File bool
|
||||
|
||||
// the actual parameter value
|
||||
Value interface{}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue