summaryrefslogtreecommitdiff
path: root/http
diff options
context:
space:
mode:
authorWolfy-J <[email protected]>2018-05-31 14:10:59 +0300
committerWolfy-J <[email protected]>2018-05-31 14:10:59 +0300
commit48f4f7a39a2336be24cc74b4116c02cc941dbd9a (patch)
tree3de1d379e43fe0772fb699852eac08ded6bbe644 /http
parentec2af29c17402145547699e719902d0f3f2ec8ec (diff)
http support
Diffstat (limited to 'http')
-rw-r--r--http/files.go58
-rw-r--r--http/post.go45
-rw-r--r--http/request.go127
-rw-r--r--http/response.go34
-rw-r--r--http/server.go129
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
+}