diff options
author | Wolfy-J <[email protected]> | 2018-06-06 16:01:06 +0300 |
---|---|---|
committer | Wolfy-J <[email protected]> | 2018-06-06 16:01:06 +0300 |
commit | c0abdf657a0a5de02b63683a60bf13b88895ef79 (patch) | |
tree | b497e98a7c2cf73f7a8377de7566a3f7b4b72a5e /cmd | |
parent | e863c6cdcf7c318fb251e096bf92812ed98ea03c (diff) |
better server re-configuration
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/_____/http/request.go | 137 | ||||
-rw-r--r-- | cmd/_____/http/uploads.go | 206 | ||||
-rw-r--r-- | cmd/_____/server.go | 64 |
3 files changed, 343 insertions, 64 deletions
diff --git a/cmd/_____/http/request.go b/cmd/_____/http/request.go new file mode 100644 index 00000000..fd483744 --- /dev/null +++ b/cmd/_____/http/request.go @@ -0,0 +1,137 @@ +package http + +import ( + "encoding/json" + "fmt" + "github.com/spiral/roadrunner" + "io/ioutil" + "net/http" + "strings" +) + +const ( + defaultMaxMemory = 32 << 20 // 32 MB +) + +// Request maps net/http requests to PSR7 compatible structure and managed state of temporary uploaded files. +type Request struct { + // Protocol includes HTTP protocol version. + Protocol string `json:"protocol"` + + // Method contains name of HTTP method used for the request. + Method string `json:"method"` + + // Uri contains full request Uri with scheme and query. + Uri string `json:"uri"` + + // Headers contains list of request headers. + Headers http.Header `json:"headers"` + + // Cookies contains list of request cookies. + Cookies map[string]string `json:"cookies"` + + // RawQuery contains non parsed query string (to be parsed on php end). + RawQuery string `json:"rawQuery"` + + // Parsed indicates that request body has been parsed on RR end. + Parsed bool `json:"parsed"` + + // Uploads contains list of uploaded files, their names, sized and associations with temporary files. + Uploads *Uploads `json:"uploads"` + + // request body can be parsedData or []byte + body interface{} +} + +// NewRequest creates new PSR7 compatible request using net/http request. +func NewRequest(r *http.Request) (req *Request, err error) { + req = &Request{ + Protocol: r.Proto, + Method: r.Method, + Uri: uri(r), + Headers: r.Header, + Cookies: make(map[string]string), + RawQuery: r.URL.RawQuery, + } + + for _, c := range r.Cookies() { + req.Cookies[c.Name] = c.Value + } + + if !req.parsable() { + req.body, err = ioutil.ReadAll(r.Body) + return req, err + } + + if err = r.ParseMultipartForm(defaultMaxMemory); err != nil { + return nil, err + } + + if req.body, err = parsePost(r); err != nil { + return nil, err + } + + if req.Uploads, err = parseUploads(r); err != nil { + return nil, err + } + + req.Parsed = true + return req, nil +} + +// Open moves all uploaded files to temporary directory so it can be given to php later. +func (r *Request) Open(cfg *Config) error { + if r.Uploads == nil { + return nil + } + + return r.Uploads.Open(cfg) +} + +// Close clears all temp file uploads +func (r *Request) Close() { + if r.Uploads == nil { + return + } + + r.Uploads.Clear() +} + +// Payload request marshaled RoadRunner payload based on PSR7 data. Default encode method is JSON. Make sure to open +// files prior to calling this method. +func (r *Request) Payload() (p *roadrunner.Payload, err error) { + p = &roadrunner.Payload{} + + if p.Context, err = json.Marshal(r); err != nil { + return nil, err + } + + if r.Parsed { + if p.Body, err = json.Marshal(r.body); err != nil { + return nil, err + } + } else if r.body != nil { + p.Body = r.body.([]byte) + } + + return p, nil +} + +// parsable returns true if request payload can be parsed (POST dataTree, file tree). +func (r *Request) parsable() bool { + if r.Method != "POST" && r.Method != "PUT" && r.Method != "PATCH" { + return false + } + + ct := r.Headers.Get("content-type") + return strings.Contains(ct, "multipart/form-data") || ct == "application/x-www-form-urlencoded" +} + +// uri fetches full uri from request in a form of string (including https scheme if TLS connection is enabled). +func uri(r *http.Request) string { + if r.TLS != nil { + return fmt.Sprintf("https://%s%s", r.Host, r.URL.String()) + } + + return fmt.Sprintf("http://%s%s", r.Host, r.URL.String()) +} diff --git a/cmd/_____/http/uploads.go b/cmd/_____/http/uploads.go new file mode 100644 index 00000000..c3b18169 --- /dev/null +++ b/cmd/_____/http/uploads.go @@ -0,0 +1,206 @@ +package http + +import ( + "encoding/json" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "os" + "strings" + "sync" +) + +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 + + // ForbidUploads 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 { + // 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. @todo: do we need it? +func (u *Uploads) Open(cfg *Config) error { + var wg sync.WaitGroup + for _, f := range u.list { + wg.Add(1) + go func(f *FileUpload) { + defer wg.Done() + f.Open(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) + } + } +} + +// 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), + } + + for k, v := range r.MultipartForm.File { + files := make([]*FileUpload, 0, len(v)) + for _, f := range v { + files = append(files, wrapUpload(f)) + } + + u.list = append(u.list, files...) + u.tree.push(k, files) + } + + return u, nil +} + +func wrapUpload(f *multipart.FileHeader) *FileUpload { + return &FileUpload{ + Name: f.Filename, + MimeType: f.Header.Get("Content-Type"), + Error: UploadErrorOK, + header: f, + } +} + +// 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 +} diff --git a/cmd/_____/server.go b/cmd/_____/server.go deleted file mode 100644 index 5542e7c9..00000000 --- a/cmd/_____/server.go +++ /dev/null @@ -1,64 +0,0 @@ -package roadrunner - -import ( - "os/exec" - "sync" -) - -const ( - // EventNewPool triggered when server creates new pool. - EventNewPool = 60 - - // EventDestroyPool triggered when server destroys existed pool. - EventDestroyPool = 61 -) - -// Service manages pool creation and swapping. -type Server struct { - // configures server, pool, cmd creation and factory. - scfg *ServerConfig - - // worker command creator - cmd func() *exec.Cmd - - // observes pool events (can be attached to multiple pools at the same time) - observer func(event int, ctx interface{}) - - // creates and connects to workers - factory Factory - - // protects pool while the switch - mu sync.Mutex -} - -// todo: do assignment - -// Reconfigure configures underlying pool and destroys it's previous version if any. -func (r *Server) Configure(cfg Config) error { - r.mu.Lock() - previous := r.pool - r.mu.Unlock() - - pool, err := NewPool(r.cmd, r.factory, cfg) - if err != nil { - return err - } - - r.throw(EventNewPool, pool) - - r.mu.Lock() - - r.cfg, r.pool = cfg, pool - r.pool.Observe(r.poolObserver) - - r.mu.Unlock() - - if previous != nil { - go func(p Pool) { - r.throw(EventDestroyPool, p) - p.Destroy() - }(previous) - } - - return nil -} |