summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorWolfy-J <[email protected]>2018-06-06 16:01:06 +0300
committerWolfy-J <[email protected]>2018-06-06 16:01:06 +0300
commitc0abdf657a0a5de02b63683a60bf13b88895ef79 (patch)
treeb497e98a7c2cf73f7a8377de7566a3f7b4b72a5e /cmd
parente863c6cdcf7c318fb251e096bf92812ed98ea03c (diff)
better server re-configuration
Diffstat (limited to 'cmd')
-rw-r--r--cmd/_____/http/request.go137
-rw-r--r--cmd/_____/http/uploads.go206
-rw-r--r--cmd/_____/server.go64
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
-}