diff options
author | Wolfy-J <[email protected]> | 2018-05-31 14:10:59 +0300 |
---|---|---|
committer | Wolfy-J <[email protected]> | 2018-05-31 14:10:59 +0300 |
commit | 48f4f7a39a2336be24cc74b4116c02cc941dbd9a (patch) | |
tree | 3de1d379e43fe0772fb699852eac08ded6bbe644 /http | |
parent | ec2af29c17402145547699e719902d0f3f2ec8ec (diff) |
http support
Diffstat (limited to 'http')
-rw-r--r-- | http/files.go | 58 | ||||
-rw-r--r-- | http/post.go | 45 | ||||
-rw-r--r-- | http/request.go | 127 | ||||
-rw-r--r-- | http/response.go | 34 | ||||
-rw-r--r-- | http/server.go | 129 |
5 files changed, 393 insertions, 0 deletions
diff --git a/http/files.go b/http/files.go new file mode 100644 index 00000000..06e5fd24 --- /dev/null +++ b/http/files.go @@ -0,0 +1,58 @@ +package http + +import ( + "mime/multipart" + "strings" + "github.com/sirupsen/logrus" +) + +type fileData map[string]interface{} + +type FileUpload struct { + Name string `json:"name"` + MimeType string `json:"mimetype"` +} + +func (d fileData) push(k string, v []*multipart.FileHeader) { + if len(v) == 0 { + // doing nothing + return + } + + chunks := make([]string, 0) + for _, chunk := range strings.Split(k, "[") { + chunks = append(chunks, strings.Trim(chunk, "]")) + } + + d.pushChunk(chunks, v) +} + +func (d fileData) pushChunk(k []string, v []*multipart.FileHeader) { + logrus.Print(v) + if len(v) == 0 || v[0] == nil { + return + } + + head := k[0] + tail := k[1:] + if len(k) == 1 { + d[head] = FileUpload{ + Name: v[0].Filename, + MimeType: v[0].Header.Get("Content-Type"), + } + return + } + + // unnamed array + if len(tail) == 1 && tail[0] == "" { + d[head] = v + return + } + + if p, ok := d[head]; !ok { + d[head] = make(fileData) + d[head].(fileData).pushChunk(tail, v) + } else { + p.(fileData).pushChunk(tail, v) + } +} diff --git a/http/post.go b/http/post.go new file mode 100644 index 00000000..64c60d98 --- /dev/null +++ b/http/post.go @@ -0,0 +1,45 @@ +package http + +import "strings" + +type postData map[string]interface{} + +func (d postData) push(k string, v []string) { + if len(v) == 0 { + // doing nothing + return + } + + chunks := make([]string, 0) + for _, chunk := range strings.Split(k, "[") { + chunks = append(chunks, strings.Trim(chunk, "]")) + } + + d.pushChunk(chunks, v) +} + +func (d postData) pushChunk(k []string, v []string) { + if len(v) == 0 || v[0] == "" { + return + } + + head := k[0] + tail := k[1:] + if len(k) == 1 { + d[head] = v[0] + return + } + + // unnamed array + if len(tail) == 1 && tail[0] == "" { + d[head] = v + return + } + + if p, ok := d[head]; !ok { + d[head] = make(postData) + d[head].(postData).pushChunk(tail, v) + } else { + p.(postData).pushChunk(tail, v) + } +} diff --git a/http/request.go b/http/request.go new file mode 100644 index 00000000..032c70b5 --- /dev/null +++ b/http/request.go @@ -0,0 +1,127 @@ +package http + +import ( + "net/http" + "fmt" + "encoding/json" + "github.com/spiral/roadrunner" + "github.com/sirupsen/logrus" + "strings" + "io/ioutil" +) + +type Request struct { + Protocol string `json:"protocol"` + Uri string `json:"uri"` + Method string `json:"method"` + Headers http.Header `json:"headers"` + Cookies map[string]string `json:"cookies"` + RawQuery string `json:"rawQuery"` + Uploads fileData `json:"fileUploads"` + ParsedBody bool `json:"parsedBody"` + + // buffers + postData postData + body []byte +} + +func ParseRequest(r *http.Request) (req *Request, err error) { + req = &Request{ + Protocol: r.Proto, + Uri: fmt.Sprintf("%s%s", r.Host, r.URL.String()), + Method: r.Method, + 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.HasBody() { + r.ParseMultipartForm(32 << 20) + + if req.postData, err = parseData(r); err != nil { + return nil, err + } + + if req.Uploads, err = parseFiles(r); err != nil { + return nil, err + } + + if req.Uploads != nil { + logrus.Debug("opening files") + } + req.ParsedBody = true + } else { + req.body, _ = ioutil.ReadAll(r.Body) + } + + return req, nil +} + +func (r *Request) Payload() *roadrunner.Payload { + ctx, err := json.Marshal(r) + if err != nil { + panic(err) //todo: change it + } + + var body []byte + if r.ParsedBody { + // todo: non parseble payloads + body, err = json.Marshal(r.postData) + if err != nil { + panic(err) //todo: change it + } + } else { + body = r.body + } + + return &roadrunner.Payload{Context: ctx, Body: body} +} + +func (r *Request) Close() { + if r.Uploads != nil { + + } +} + +// HasBody returns true if request might include POST data or file uploads. +func (r *Request) HasBody() bool { + if r.Method != "POST" && r.Method != "PUT" && r.Method != "PATCH" { + return false + } + + contentType := r.Headers.Get("content-type") + + if strings.Contains(contentType, "multipart/form-data") { + return true + } + + if contentType == "application/x-www-form-urlencoded" { + return true + } + + return false +} + +// parse incoming data request into JSON (including multipart form data) +func parseData(r *http.Request) (postData, error) { + data := make(postData) + for k, v := range r.MultipartForm.Value { + data.push(k, v) + } + + return data, nil +} + +// parse incoming data request into JSON (including multipart form data) +func parseFiles(r *http.Request) (fileData, error) { + data := make(fileData) + for k, v := range r.MultipartForm.File { + data.push(k, v) + } + + return data, nil +} diff --git a/http/response.go b/http/response.go new file mode 100644 index 00000000..6a094bf3 --- /dev/null +++ b/http/response.go @@ -0,0 +1,34 @@ +package http + +import ( + "net/http" + "github.com/sirupsen/logrus" +) + +type Response struct { + Status int `json:"status"` + Headers map[string][]string `json:"headers"` +} + +func (r *Response) Write(w http.ResponseWriter) { + push := make([]string, 0) + for k, v := range r.Headers { + for _, h := range v { + if k == "http2-push" { + push = append(push, h) + } else { + w.Header().Add(k, h) + } + } + } + + if p, ok := w.(http.Pusher); ok { + logrus.Info("PUSH SUPPORTED") + for _, f := range push { + logrus.Info("pushing HTTP2 file ", f) + p.Push(f, nil) + } + } + + w.WriteHeader(r.Status) +} diff --git a/http/server.go b/http/server.go new file mode 100644 index 00000000..b0d0b56a --- /dev/null +++ b/http/server.go @@ -0,0 +1,129 @@ +package http + +import ( + "github.com/spiral/roadrunner" + "net/http" + "strings" + "path" + "github.com/sirupsen/logrus" + "os" + "path/filepath" + "encoding/json" +) + +var ( + excludeFiles = []string{".php", ".htaccess"} +) + +// Configures http rr +type Config struct { + // ServeStatic enables static file serving from desired root directory. + ServeStatic bool + + // Root directory, required when ServeStatic set to true. + Root string +} + +// Server serves http connections to underlying PHP application using PSR-7 protocol. Context will include request headers, +// parsed files and query, payload will include parsed form data (if any) - todo: do we need to do that?. +type Server struct { + cfg Config + root http.Dir + rr *roadrunner.Server +} + +// NewServer returns new instance of Server PSR7 server. +func NewServer(cfg Config, server *roadrunner.Server) *Server { + h := &Server{cfg: cfg, rr: server} + if cfg.ServeStatic { + h.root = http.Dir(h.cfg.Root) + } + + return h +} + +// ServeHTTP serve using PSR-7 requests passed to underlying application. +func (svr *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) () { + if svr.cfg.ServeStatic && svr.serveStatic(w, r) { + // server always attempt to serve static files first + return + } + + req, err := ParseRequest(r) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) //todo: better errors + return + } + defer req.Close() + + rsp, err := svr.rr.Exec(req.Payload()) + if err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) //todo: better errors + return + } + + resp := &Response{} + if err = json.Unmarshal(rsp.Context, resp); err != nil { + w.WriteHeader(500) + w.Write([]byte(err.Error())) //todo: better errors + return + } + + resp.Write(w) + w.Write(rsp.Body) +} + +// serveStatic 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 *Server) serveStatic(w http.ResponseWriter, r *http.Request) bool { + fpath := r.URL.Path + if !strings.HasPrefix(fpath, "/") { + fpath = "/" + fpath + } + fpath = path.Clean(fpath) + + if svr.excluded(fpath) { + logrus.Warningf("attempt to access forbidden file %s", fpath) + return false + } + + f, err := svr.root.Open(fpath) + if err != nil { + if !os.IsNotExist(err) { + // rr or access error + logrus.Error(err) + } + + return false + } + defer f.Close() + + d, err := f.Stat() + if err != nil { + // rr error + logrus.Error(err) + return false + } + + if d.IsDir() { + // we are not serving directories + return false + } + + http.ServeContent(w, r, d.Name(), d.ModTime(), f) + return true +} + +// excluded returns true if file has forbidden extension. +func (svr *Server) excluded(path string) bool { + ext := strings.ToLower(filepath.Ext(path)) + for _, exl := range excludeFiles { + if ext == exl { + return true + } + } + + return false +} |