diff --git a/checker/default/any/main.go b/checker/default/any/main.go new file mode 100644 index 0000000..294207a --- /dev/null +++ b/checker/default/any/main.go @@ -0,0 +1,9 @@ +package main + +func Match(name string) bool { + return name == "any" +} + +func Check(value interface{}) bool { + return true +} diff --git a/internal/multipart/private.go b/internal/multipart/private.go new file mode 100644 index 0000000..68e8fb6 --- /dev/null +++ b/internal/multipart/private.go @@ -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 +} diff --git a/internal/multipart/public.go b/internal/multipart/public.go new file mode 100644 index 0000000..94b89b0 --- /dev/null +++ b/internal/multipart/public.go @@ -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) + } + + } + +} diff --git a/internal/multipart/types.go b/internal/multipart/types.go new file mode 100644 index 0000000..6ac4775 --- /dev/null +++ b/internal/multipart/types.go @@ -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 +} diff --git a/request_builder.go b/request_builder.go index d8b569d..1ec6a9b 100644 --- a/request_builder.go +++ b/request_builder.go @@ -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 } diff --git a/request_data.go b/request_data.go new file mode 100644 index 0000000..f720502 --- /dev/null +++ b/request_data.go @@ -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#") +} diff --git a/router.go b/router.go index 0f32db5..5234f16 100644 --- a/router.go +++ b/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 } diff --git a/types.go b/types.go index c5d2ad4..4539611 100644 --- a/types.go +++ b/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{} }