summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--_____/http/config.go11
-rw-r--r--_____/http/rpc.go34
-rw-r--r--_____/utils/workers.go37
-rw-r--r--cmd/rr/utils/size.go (renamed from _____/utils/size.go)0
-rw-r--r--cmd/rr/utils/utils_test.go12
-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.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
}