summaryrefslogtreecommitdiff
path: root/http
diff options
context:
space:
mode:
authorWolfy-J <[email protected]>2018-06-10 15:25:20 +0300
committerWolfy-J <[email protected]>2018-06-10 15:25:20 +0300
commitee2e495a76c1cb0666f6f210ed0e59f94314b9e5 (patch)
tree5484792ff964cca69bb282d6ddabf323f3d57571 /http
parent3eb8396c501bc8b1048273e6ca88c91d3426b744 (diff)
tests
Diffstat (limited to 'http')
-rw-r--r--http/data.go67
-rw-r--r--http/fs_config.go29
-rw-r--r--http/parse.go147
-rw-r--r--http/response.go18
-rw-r--r--http/static.go57
-rw-r--r--http/uploads.go130
6 files changed, 378 insertions, 70 deletions
diff --git a/http/data.go b/http/data.go
deleted file mode 100644
index e6b8344f..00000000
--- a/http/data.go
+++ /dev/null
@@ -1,67 +0,0 @@
-package http
-
-import (
- "net/http"
- "strings"
-)
-
-const maxLevel = 127
-
-type dataTree map[string]interface{}
-
-// parsePost parses incoming request body into data tree.
-func parsePost(r *http.Request) (dataTree, error) {
- data := make(dataTree)
-
- for k, v := range r.PostForm {
- data.push(k, v)
- }
-
- for k, v := range r.MultipartForm.Value {
- data.push(k, v)
- }
-
- return data, nil
-}
-
-func (d dataTree) push(k string, v []string) {
- if len(v) == 0 {
- // skip empty values
- return
- }
-
- indexes := make([]string, 0)
- for _, index := range strings.Split(k, "[") {
- indexes = append(indexes, strings.Trim(index, "]"))
- }
-
- if len(indexes) <= maxLevel {
- d.mount(indexes, v)
- }
-}
-
-// mount mounts data tree recursively.
-func (d dataTree) mount(i []string, v []string) {
- if len(v) == 0 {
- return
- }
-
- if len(i) == 1 {
- // single value context
- d[i[0]] = v[0]
- return
- }
-
- if len(i) == 2 && i[1] == "" {
- // non associated array of elements
- d[i[0]] = v
- return
- }
-
- if p, ok := d[i[0]]; ok {
- p.(dataTree).mount(i[1:], v)
- }
-
- d[i[0]] = make(dataTree)
- d[i[0]].(dataTree).mount(i[1:], v)
-}
diff --git a/http/fs_config.go b/http/fs_config.go
new file mode 100644
index 00000000..de5b1389
--- /dev/null
+++ b/http/fs_config.go
@@ -0,0 +1,29 @@
+package http
+
+import (
+ "strings"
+ "path"
+)
+
+// FsConfig describes file location and controls access to them.
+type FsConfig struct {
+ // Dir contains name of directory to control access to.
+ Dir string
+
+ // Forbid specifies list of file extensions which are forbidden for access.
+ // Example: .php, .exe, .bat, .htaccess and etc.
+ Forbid []string
+}
+
+// Forbid must return true if file extension is not allowed for the upload.
+func (cfg FsConfig) Forbids(filename string) bool {
+ ext := strings.ToLower(path.Ext(filename))
+
+ for _, v := range cfg.Forbid {
+ if ext == v {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/http/parse.go b/http/parse.go
new file mode 100644
index 00000000..fe8361d6
--- /dev/null
+++ b/http/parse.go
@@ -0,0 +1,147 @@
+package http
+
+import (
+ "strings"
+ "net/http"
+ "os"
+)
+
+const maxLevel = 127
+
+type dataTree map[string]interface{}
+type fileTree map[string]interface{}
+
+// parseData parses incoming request body into data tree.
+func parseData(r *http.Request) (dataTree, error) {
+ data := make(dataTree)
+ for k, v := range r.PostForm {
+ data.push(k, v)
+ }
+
+ for k, v := range r.MultipartForm.Value {
+ data.push(k, v)
+ }
+
+ return data, nil
+}
+
+// pushes value into data tree.
+func (d dataTree) push(k string, v []string) {
+ if len(v) == 0 {
+ // skip empty values
+ return
+ }
+
+ indexes := make([]string, 0)
+ for _, index := range strings.Split(k, "[") {
+ indexes = append(indexes, strings.Trim(index, "]"))
+ }
+
+ if len(indexes) <= maxLevel {
+ d.mount(indexes, v)
+ }
+}
+
+// mount mounts data tree recursively.
+func (d dataTree) mount(i []string, v []string) {
+ if len(v) == 0 {
+ return
+ }
+
+ if len(i) == 1 {
+ // single value context
+ d[i[0]] = v[0]
+ return
+ }
+
+ if len(i) == 2 && i[1] == "" {
+ // non associated array of elements
+ d[i[0]] = v
+ return
+ }
+
+ if p, ok := d[i[0]]; ok {
+ p.(dataTree).mount(i[1:], v)
+ }
+
+ d[i[0]] = make(dataTree)
+ d[i[0]].(dataTree).mount(i[1:], v)
+}
+
+// parse incoming dataTree request into JSON (including multipart form dataTree)
+func parseUploads(r *http.Request, cfg *UploadsConfig) (*Uploads, error) {
+ u := &Uploads{
+ cfg: cfg,
+ tree: make(fileTree),
+ list: make([]*FileUpload, 0),
+ }
+
+ for k, v := range r.MultipartForm.File {
+ files := make([]*FileUpload, 0, len(v))
+ for _, f := range v {
+ files = append(files, NewUpload(f))
+ }
+
+ u.list = append(u.list, files...)
+ u.tree.push(k, files)
+ }
+
+ return u, nil
+}
+
+// exists if file exists.
+func exists(path string) bool {
+ _, err := os.Stat(path)
+ if err == nil {
+ return true
+ }
+
+ if os.IsNotExist(err) {
+ return false
+ }
+
+ return false
+}
+
+// pushes new file upload into it's proper place.
+func (d fileTree) push(k string, v []*FileUpload) {
+ if len(v) == 0 {
+ // skip empty values
+ return
+ }
+
+ indexes := make([]string, 0)
+ for _, index := range strings.Split(k, "[") {
+ indexes = append(indexes, strings.Trim(index, "]"))
+ }
+
+ if len(indexes) <= maxLevel {
+ d.mount(indexes, v)
+ }
+}
+
+// mount mounts data tree recursively.
+func (d fileTree) mount(i []string, v []*FileUpload) {
+ if len(v) == 0 {
+ return
+ }
+
+ if len(i) == 1 {
+ // single value context
+ d[i[0]] = v[0]
+ return
+ }
+
+ if len(i) == 2 && i[1] == "" {
+ // non associated array of elements
+ d[i[0]] = v
+ return
+ }
+
+ if p, ok := d[i[0]]; ok {
+ p.(fileTree).mount(i[1:], v)
+ }
+
+ d[i[0]] = make(fileTree)
+ d[i[0]].(fileTree).mount(i[1:], v)
+}
diff --git a/http/response.go b/http/response.go
index 2736c4ab..dd092353 100644
--- a/http/response.go
+++ b/http/response.go
@@ -4,6 +4,7 @@ import (
"encoding/json"
"github.com/spiral/roadrunner"
"net/http"
+ "io"
)
// Response handles PSR7 response logic.
@@ -15,7 +16,7 @@ type Response struct {
Headers map[string][]string `json:"headers"`
// associated body payload.
- body []byte
+ body interface{}
}
// NewResponse creates new response based on given roadrunner payload.
@@ -29,7 +30,7 @@ func NewResponse(p *roadrunner.Payload) (*Response, error) {
}
// Write writes response headers, status and body into ResponseWriter.
-func (r *Response) Write(w http.ResponseWriter) {
+func (r *Response) Write(w http.ResponseWriter) error {
for k, v := range r.Headers {
for _, h := range v {
w.Header().Add(k, h)
@@ -38,5 +39,16 @@ func (r *Response) Write(w http.ResponseWriter) {
}
w.WriteHeader(r.Status)
- w.Write(r.body)
+
+ if data, ok := r.body.([]byte); ok {
+ w.Write(data)
+ }
+
+ if rc, ok := r.body.(io.Reader); ok {
+ if _, err := io.Copy(w, rc); err != nil {
+ return err
+ }
+ }
+
+ return nil
}
diff --git a/http/static.go b/http/static.go
new file mode 100644
index 00000000..3bd69160
--- /dev/null
+++ b/http/static.go
@@ -0,0 +1,57 @@
+package http
+
+import (
+ "github.com/sirupsen/logrus"
+ "net/http"
+ "os"
+ "path"
+ "strings"
+)
+
+// staticServer serves static files
+type staticServer struct {
+ cfg *FsConfig
+ root http.Dir
+}
+
+// serve attempts to serve static file and returns true in case of success, will return false in case if file not
+// found, not allowed or on read error.
+func (svr *staticServer) serve(w http.ResponseWriter, r *http.Request) bool {
+ fPath := r.URL.Path
+ if !strings.HasPrefix(fPath, "/") {
+ fPath = "/" + fPath
+ }
+ fPath = path.Clean(fPath)
+
+ if svr.cfg.Forbids(fPath) {
+ logrus.Warningf("attempt to access forbidden file %s", fPath) // todo: better logs
+ return false
+ }
+
+ f, err := svr.root.Open(fPath)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ logrus.Error(err) //todo: rr or access error
+ }
+
+ return false
+ }
+ defer f.Close()
+
+ d, err := f.Stat()
+ if err != nil {
+ logrus.Error(err) //todo: rr or access error
+
+ // todo: do i need it, bypass log?
+
+ return false
+ }
+
+ if d.IsDir() {
+ // do not serve directories
+ return false
+ }
+
+ http.ServeContent(w, r, d.Name(), d.ModTime(), f)
+ return true
+}
diff --git a/http/uploads.go b/http/uploads.go
new file mode 100644
index 00000000..cdd3e52c
--- /dev/null
+++ b/http/uploads.go
@@ -0,0 +1,130 @@
+package http
+
+import (
+ "encoding/json"
+ "os"
+ "sync"
+ "mime/multipart"
+ "io/ioutil"
+ "io"
+)
+
+const (
+ // There is no error, the file uploaded with success.
+ UploadErrorOK = 0
+
+ // No file was uploaded.
+ UploadErrorNoFile = 4
+
+ // Missing a temporary folder.
+ UploadErrorNoTmpDir = 5
+
+ // Failed to write file to disk.
+ UploadErrorCantWrite = 6
+
+ // Forbid file extension.
+ UploadErrorExtension = 7
+)
+
+// tree manages uploaded files tree and temporary files.
+type Uploads struct {
+ // associated temp directory and forbidden extensions.
+ cfg *FsConfig
+
+ // pre processed data tree for Uploads.
+ tree fileTree
+
+ // flat list of all file Uploads.
+ list []*FileUpload
+}
+
+// MarshalJSON marshal tree tree into JSON.
+func (u *Uploads) MarshalJSON() ([]byte, error) {
+ return json.Marshal(u.tree)
+}
+
+// Open moves all uploaded files to temp directory, return error in case of issue with temp directory. File errors
+// will be handled individually.
+func (u *Uploads) Open() error {
+ var wg sync.WaitGroup
+ for _, f := range u.list {
+ wg.Add(1)
+ go func(f *FileUpload) {
+ defer wg.Done()
+ f.Open(u.cfg)
+ }(f)
+ }
+
+ wg.Wait()
+ return nil
+}
+
+// Clear deletes all temporary files.
+func (u *Uploads) Clear() {
+ for _, f := range u.list {
+ if f.TempFilename != "" && exists(f.TempFilename) {
+ os.Remove(f.TempFilename)
+ }
+ }
+}
+
+// FileUpload represents singular file NewUpload.
+type FileUpload struct {
+ // Name contains filename specified by the client.
+ Name string `json:"name"`
+
+ // MimeType contains mime-type provided by the client.
+ MimeType string `json:"type"`
+
+ // Size of the uploaded file.
+ Size int64 `json:"size"`
+
+ // Error indicates file upload error (if any). See http://php.net/manual/en/features.file-upload.errors.php
+ Error int `json:"error"`
+
+ // TempFilename points to temporary file location.
+ TempFilename string `json:"tmpName"`
+
+ // associated file header
+ header *multipart.FileHeader
+}
+
+// NewUpload wraps net/http upload into PRS-7 compatible structure.
+func NewUpload(f *multipart.FileHeader) *FileUpload {
+ return &FileUpload{
+ Name: f.Filename,
+ MimeType: f.Header.Get("Content-Type"),
+ Error: UploadErrorOK,
+ header: f,
+ }
+}
+
+func (f *FileUpload) Open(cfg *FsConfig) error {
+ if cfg.Forbids(f.Name) {
+ f.Error = UploadErrorExtension
+ return nil
+ }
+
+ file, err := f.header.Open()
+ if err != nil {
+ f.Error = UploadErrorNoFile
+ return err
+ }
+ defer file.Close()
+
+ tmp, err := ioutil.TempFile(cfg.Dir, "upload")
+ if err != nil {
+ // most likely cause of this issue is missing tmp dir
+ f.Error = UploadErrorNoTmpDir
+ return err
+ }
+
+ f.TempFilename = tmp.Name()
+ defer tmp.Close()
+
+ if f.Size, err = io.Copy(tmp, file); err != nil {
+ f.Error = UploadErrorCantWrite
+ }
+
+ return err
+}