diff options
-rw-r--r-- | _____/http/config.go | 11 | ||||
-rw-r--r-- | _____/http/rpc.go | 34 | ||||
-rw-r--r-- | _____/utils/workers.go | 37 | ||||
-rw-r--r-- | cmd/rr/utils/size.go (renamed from _____/utils/size.go) | 0 | ||||
-rw-r--r-- | cmd/rr/utils/utils_test.go | 12 | ||||
-rw-r--r-- | http/data.go | 67 | ||||
-rw-r--r-- | http/fs_config.go | 29 | ||||
-rw-r--r-- | http/parse.go | 147 | ||||
-rw-r--r-- | http/response.go | 18 | ||||
-rw-r--r-- | http/static.go (renamed from _____/http/static.go) | 33 | ||||
-rw-r--r-- | http/uploads.go (renamed from _____/http/uploads.go) | 180 |
11 files changed, 307 insertions, 261 deletions
diff --git a/_____/http/config.go b/_____/http/config.go index 4ea414c1..bd8cec5e 100644 --- a/_____/http/config.go +++ b/_____/http/config.go @@ -7,6 +7,7 @@ import ( "os" "path" "strings" + "github.com/spiral/roadrunner/http" ) // Configures RoadRunner HTTP server. @@ -14,21 +15,23 @@ type Config struct { // serve enables static file serving from desired root directory. ServeStatic bool + Static *http.FsConfig + // Root directory, required when serve set to true. Root string - // TmpDir contains name of temporary directory to store uploaded files passed to underlying PHP process. + // Dir contains name of temporary directory to store uploaded files passed to underlying PHP process. TmpDir string // MaxRequest specified max size for payload body in bytes, set 0 to unlimited. MaxRequest int64 - // ForbidUploads specifies list of file extensions which are forbidden for uploads. + // Forbid specifies list of file extensions which are forbidden for uploads. // Example: .php, .exe, .bat, .htaccess and etc. ForbidUploads []string } -// ForbidUploads must return true if file extension is not allowed for the upload. +// Forbid must return true if file extension is not allowed for the upload. func (cfg Config) Forbidden(filename string) bool { ext := strings.ToLower(path.Ext(filename)) @@ -46,7 +49,7 @@ type serviceConfig struct { Host string Port string MaxRequest string - Static struct { + Static struct { Serve bool Root string } diff --git a/_____/http/rpc.go b/_____/http/rpc.go index e54eae7c..673ff2bb 100644 --- a/_____/http/rpc.go +++ b/_____/http/rpc.go @@ -42,3 +42,37 @@ func (rpc *rpcServer) Workers(list bool, r *WorkerList) error { r.Workers = utils.FetchWorkers(rpc.service.srv.rr) return nil } + +// Worker provides information about specific worker. +type Worker struct { + // Pid contains process id. + Pid int `json:"pid"` + + // Status of the worker. + Status string `json:"status"` + + // Number of worker executions. + NumExecs uint64 `json:"numExecs"` + + // Created is unix nano timestamp of worker creation time. + Created int64 `json:"created"` + + // Updated is unix nano timestamp of last worker execution. + Updated int64 `json:"updated"` +} + +// FetchWorkers fetches list of workers from RR Server. +func FetchWorkers(srv *roadrunner.Server) (result []Worker) { + for _, w := range srv.Workers() { + state := w.State() + result = append(result, Worker{ + Pid: *w.Pid, + Status: state.String(), + NumExecs: state.NumExecs(), + Created: w.Created.UnixNano(), + Updated: state.Updated().UnixNano(), + }) + } + + return +}
\ No newline at end of file diff --git a/_____/utils/workers.go b/_____/utils/workers.go deleted file mode 100644 index 1024b4c6..00000000 --- a/_____/utils/workers.go +++ /dev/null @@ -1,37 +0,0 @@ -package utils - -import "github.com/spiral/roadrunner" - -// Worker provides information about specific worker. -type Worker struct { - // Pid contains process id. - Pid int `json:"pid"` - - // Status of the worker. - Status string `json:"status"` - - // Number of worker executions. - NumExecs uint64 `json:"numExecs"` - - // Created is unix nano timestamp of worker creation time. - Created int64 `json:"created"` - - // Updated is unix nano timestamp of last worker execution. - Updated int64 `json:"updated"` -} - -// FetchWorkers fetches list of workers from RR Server. -func FetchWorkers(srv *roadrunner.Server) (result []Worker) { - for _, w := range srv.Workers() { - state := w.State() - result = append(result, Worker{ - Pid: *w.Pid, - Status: state.String(), - NumExecs: state.NumExecs(), - Created: w.Created.UnixNano(), - Updated: state.Updated().UnixNano(), - }) - } - - return -}
\ No newline at end of file diff --git a/_____/utils/size.go b/cmd/rr/utils/size.go index 176cc9e1..176cc9e1 100644 --- a/_____/utils/size.go +++ b/cmd/rr/utils/size.go diff --git a/cmd/rr/utils/utils_test.go b/cmd/rr/utils/utils_test.go new file mode 100644 index 00000000..f67b2a10 --- /dev/null +++ b/cmd/rr/utils/utils_test.go @@ -0,0 +1,12 @@ +package utils + +import ( + "testing" + "github.com/magiconair/properties/assert" +) + +func TestUtils(t *testing.T) { + assert.Equal(t, int64(1024), ParseSize("1K")) + assert.Equal(t, int64(1024*1024), ParseSize("1M")) + assert.Equal(t, int64(2*1024*1024*1024), ParseSize("2G")) +} 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 index b055099f..3bd69160 100644 --- a/_____/http/static.go +++ b/http/static.go @@ -5,32 +5,30 @@ import ( "net/http" "os" "path" - "path/filepath" "strings" ) -var forbiddenFiles = []string{".php", ".htaccess"} - // 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 := r.URL.Path + if !strings.HasPrefix(fPath, "/") { + fPath = "/" + fPath } - fpath = path.Clean(fpath) + fPath = path.Clean(fPath) - if svr.forbidden(fpath) { - logrus.Warningf("attempt to access forbidden file %s", fpath) // todo: better logs + 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) + f, err := svr.root.Open(fPath) if err != nil { if !os.IsNotExist(err) { logrus.Error(err) //todo: rr or access error @@ -43,6 +41,9 @@ func (svr *staticServer) serve(w http.ResponseWriter, r *http.Request) bool { d, err := f.Stat() if err != nil { logrus.Error(err) //todo: rr or access error + + // todo: do i need it, bypass log? + return false } @@ -54,15 +55,3 @@ func (svr *staticServer) serve(w http.ResponseWriter, r *http.Request) bool { http.ServeContent(w, r, d.Name(), d.ModTime(), f) return true } - -// forbidden returns true if file has forbidden extension. -func (svr *staticServer) forbidden(path string) bool { - ext := strings.ToLower(filepath.Ext(path)) - for _, exl := range forbiddenFiles { - if ext == exl { - return true - } - } - - return false -} diff --git a/_____/http/uploads.go b/http/uploads.go index c3b18169..cdd3e52c 100644 --- a/_____/http/uploads.go +++ b/http/uploads.go @@ -2,13 +2,11 @@ package http import ( "encoding/json" - "io" - "io/ioutil" - "mime/multipart" - "net/http" "os" - "strings" "sync" + "mime/multipart" + "io/ioutil" + "io" ) const ( @@ -24,107 +22,15 @@ const ( // Failed to write file to disk. UploadErrorCantWrite = 6 - // ForbidUploads file extension. + // Forbid file extension. UploadErrorExtension = 7 ) -// FileUpload represents singular file wrapUpload. -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 -} - -func (f *FileUpload) Open(cfg *Config) error { - if cfg.Forbidden(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.TmpDir, "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 -} - -type fileTree map[string]interface{} - -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) -} - // 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 @@ -138,14 +44,14 @@ func (u *Uploads) MarshalJSON() ([]byte, error) { } // Open moves all uploaded files to temp directory, return error in case of issue with temp directory. File errors -// will be handled individually. @todo: do we need it? -func (u *Uploads) Open(cfg *Config) error { +// 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(cfg) + f.Open(u.cfg) }(f) } @@ -162,27 +68,29 @@ func (u *Uploads) Clear() { } } -// parse incoming dataTree request into JSON (including multipart form dataTree) -func parseUploads(r *http.Request) (*Uploads, error) { - u := &Uploads{ - tree: make(fileTree), - list: make([]*FileUpload, 0), - } +// FileUpload represents singular file NewUpload. +type FileUpload struct { + // Name contains filename specified by the client. + Name string `json:"name"` - for k, v := range r.MultipartForm.File { - files := make([]*FileUpload, 0, len(v)) - for _, f := range v { - files = append(files, wrapUpload(f)) - } + // MimeType contains mime-type provided by the client. + MimeType string `json:"type"` - u.list = append(u.list, files...) - u.tree.push(k, files) - } + // Size of the uploaded file. + Size int64 `json:"size"` - return u, nil + // 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 } -func wrapUpload(f *multipart.FileHeader) *FileUpload { +// 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"), @@ -191,16 +99,32 @@ func wrapUpload(f *multipart.FileHeader) *FileUpload { } } -// exists if file exists. -func exists(path string) bool { - _, err := os.Stat(path) - if err == nil { - return true +func (f *FileUpload) Open(cfg *FsConfig) error { + if cfg.Forbids(f.Name) { + f.Error = UploadErrorExtension + return nil } - if os.IsNotExist(err) { - return false + file, err := f.header.Open() + if err != nil { + f.Error = UploadErrorNoFile + return err } + defer file.Close() - return false + 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 } |