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:
Adrien Marquès 2018-05-29 15:42:41 +02:00
parent 0641bb9131
commit 8f9c86c391
8 changed files with 478 additions and 95 deletions

View File

@ -0,0 +1,9 @@
package main
func Match(name string) bool {
return name == "any"
}
func Check(value interface{}) bool {
return true
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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
}

220
request_data.go Normal file
View File

@ -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#")
}

View File

@ -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
}

View File

@ -13,10 +13,56 @@ type Server struct {
}
type Request struct {
// 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{}
}