summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWolfy-J <[email protected]>2018-06-01 17:48:43 +0300
committerWolfy-J <[email protected]>2018-06-01 17:48:43 +0300
commitb02611b7266589d888e054a1d2e4432ae370617d (patch)
tree8e658d448aeb645ceff0229655fb53a542a84087
parent91a081e3ec43302ca1df8d436e48c2a14d7c76b9 (diff)
file uploads support
-rw-r--r--cmd/rr-php/cmd/serve.go10
-rw-r--r--http/data.go67
-rw-r--r--http/request.go137
-rw-r--r--http/response.go42
-rw-r--r--http/server.go82
-rw-r--r--http/static.go70
-rw-r--r--http/uploads.go162
-rw-r--r--payload.go2
-rw-r--r--psr7/files.go58
-rw-r--r--psr7/post.go45
-rw-r--r--psr7/request.go127
-rw-r--r--psr7/response.go34
-rw-r--r--psr7/server.go129
13 files changed, 567 insertions, 398 deletions
diff --git a/cmd/rr-php/cmd/serve.go b/cmd/rr-php/cmd/serve.go
index 69b53e28..24ec4043 100644
--- a/cmd/rr-php/cmd/serve.go
+++ b/cmd/rr-php/cmd/serve.go
@@ -20,8 +20,9 @@ import (
"os/exec"
"time"
"github.com/sirupsen/logrus"
- rrhttp "github.com/spiral/roadrunner/psr7"
+ rrttp "github.com/spiral/roadrunner/http"
"net/http"
+ "os"
)
func init() {
@@ -61,7 +62,7 @@ func serveHandler(cmd *cobra.Command, args []string) {
// Addr: ":8080",
// Handler: rrhttp.NewServer(
// rrhttp.Config{
- // ServeStatic: true,
+ // serveStatic: true,
// Root: "/Users/wolfy-j/Projects/phpapp/webroot",
// },
// rr,
@@ -73,10 +74,11 @@ func serveHandler(cmd *cobra.Command, args []string) {
//http2.ConfigureServer(&srv, nil)
//srv.ListenAndServeTLS("localhost.cert", "localhost.key")
- http.ListenAndServe(":8080", rrhttp.NewServer(
- rrhttp.Config{
+ http.ListenAndServe(":8080", rrttp.NewServer(
+ rrttp.Config{
ServeStatic: true,
Root: "/Users/wolfy-j/Projects/phpapp/webroot",
+ UploadsDir: os.TempDir(),
},
rr,
))
diff --git a/http/data.go b/http/data.go
new file mode 100644
index 00000000..865e4760
--- /dev/null
+++ b/http/data.go
@@ -0,0 +1,67 @@
+package http
+
+import (
+ "strings"
+ "net/http"
+)
+
+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 || 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/request.go b/http/request.go
new file mode 100644
index 00000000..572d7d6a
--- /dev/null
+++ b/http/request.go
@@ -0,0 +1,137 @@
+package http
+
+import (
+ "net/http"
+ "encoding/json"
+ "github.com/spiral/roadrunner"
+ "strings"
+ "io/ioutil"
+ "fmt"
+)
+
+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
+}
+
+// OpenUploads moves all uploaded files to temporary directory so it can be given to php later.
+func (r *Request) OpenUploads(tmpDir string) error {
+ if r.Uploads == nil {
+ return nil
+ }
+
+ return r.Uploads.OpenUploads(tmpDir)
+}
+
+// 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/http/response.go b/http/response.go
new file mode 100644
index 00000000..d35669c4
--- /dev/null
+++ b/http/response.go
@@ -0,0 +1,42 @@
+package http
+
+import (
+ "net/http"
+ "encoding/json"
+ "github.com/spiral/roadrunner"
+)
+
+// Response handles PSR7 response logic.
+type Response struct {
+ // Status contains response status.
+ Status int `json:"status"`
+
+ // Headers contains list of response headers.
+ Headers map[string][]string `json:"headers"`
+
+ // associated body payload.
+ body []byte
+}
+
+// NewResponse creates new response based on given roadrunner payload.
+func NewResponse(p *roadrunner.Payload) (*Response, error) {
+ r := &Response{body: p.Body}
+ if err := json.Unmarshal(p.Context, r); err != nil {
+ return nil, err
+ }
+
+ return r, nil
+}
+
+// Write writes response headers, status and body into ResponseWriter.
+func (r *Response) Write(w http.ResponseWriter) {
+ for k, v := range r.Headers {
+ for _, h := range v {
+ w.Header().Add(k, h)
+
+ }
+ }
+
+ w.WriteHeader(r.Status)
+ w.Write(r.body)
+}
diff --git a/http/server.go b/http/server.go
new file mode 100644
index 00000000..363dca2d
--- /dev/null
+++ b/http/server.go
@@ -0,0 +1,82 @@
+package http
+
+import (
+ "github.com/spiral/roadrunner"
+ "net/http"
+)
+
+// Configures RoadRunner HTTP server.
+type Config struct {
+ // serve enables static file serving from desired root directory.
+ ServeStatic bool
+
+ // Root directory, required when serve set to true.
+ Root string
+
+ // UploadsDir contains name of temporary directory to store uploaded files passed to underlying PHP process.
+ UploadsDir 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 dataTree (if any).
+type Server struct {
+ cfg Config
+ static *staticServer
+ rr *roadrunner.Server
+}
+
+// NewServer returns new instance of HTTP PSR7 server.
+func NewServer(cfg Config, server *roadrunner.Server) *Server {
+ h := &Server{cfg: cfg, rr: server}
+
+ if cfg.ServeStatic {
+ h.static = &staticServer{root: http.Dir(h.cfg.Root)}
+ }
+
+ return h
+}
+
+// ServeHTTP serve using PSR-7 requests passed to underlying application. Attempts to serve static files first if enabled.
+func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) () {
+ if srv.cfg.ServeStatic && srv.static.serve(w, r) {
+ return
+ }
+
+ req, err := NewRequest(r)
+ if err != nil {
+ srv.sendError(w, r, err)
+ return
+ }
+
+ if err = req.OpenUploads(srv.cfg.UploadsDir); err != nil {
+ srv.sendError(w, r, err)
+ return
+ }
+ defer req.Close()
+
+ p, err := req.Payload()
+ if err != nil {
+ srv.sendError(w, r, err)
+ return
+ }
+
+ rsp, err := srv.rr.Exec(p)
+ if err != nil {
+ srv.sendError(w, r, err)
+ return
+ }
+
+ resp, err := NewResponse(rsp)
+ if err != nil {
+ srv.sendError(w, r, err)
+ return
+ }
+
+ resp.Write(w)
+}
+
+// sendError sends error
+func (srv *Server) sendError(w http.ResponseWriter, r *http.Request, err error) {
+ w.WriteHeader(500)
+ w.Write([]byte(err.Error()))
+}
diff --git a/http/static.go b/http/static.go
new file mode 100644
index 00000000..bfcb87c2
--- /dev/null
+++ b/http/static.go
@@ -0,0 +1,70 @@
+package http
+
+import (
+ "net/http"
+ "strings"
+ "path"
+ "github.com/sirupsen/logrus"
+ "os"
+ "path/filepath"
+)
+
+var (
+ forbiddenFiles = []string{".php", ".htaccess"}
+)
+
+// staticServer serves static files
+type staticServer struct {
+ 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 = path.Clean(fpath)
+
+ if svr.forbidden(fpath) {
+ logrus.Warningf("attempt to access forbidden file %s", fpath) // todo: better logs
+ return false
+ }
+
+ f, err := svr.root.Open(fpath)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ logrus.Error(err) //todo: rr or access error
+ }
+
+ return false
+ }
+ defer f.Close()
+
+ d, err := f.Stat()
+ if err != nil {
+ logrus.Error(err) //todo: rr or access error
+ return false
+ }
+
+ if d.IsDir() {
+ // do not serve directories
+ return false
+ }
+
+ 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
new file mode 100644
index 00000000..1b851e6e
--- /dev/null
+++ b/http/uploads.go
@@ -0,0 +1,162 @@
+package http
+
+import (
+ "mime/multipart"
+ "encoding/json"
+ "log"
+ "strings"
+ "net/http"
+ "io/ioutil"
+ "io"
+ "sync"
+)
+
+// 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:"mimetype"`
+
+ // 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
+
+ // TempFilename points to temporary file location.
+ TempFilename string `json:"tempFilename"`
+
+ // associated file header
+ header *multipart.FileHeader
+}
+
+func (f *FileUpload) Open(tmpDir string) error {
+ file, err := f.header.Open()
+ if err != nil {
+ return err
+ }
+
+ defer file.Close()
+
+ tmp, err := ioutil.TempFile(tmpDir, "upload")
+ if err != nil {
+ return err
+ }
+
+ f.TempFilename = tmp.Name()
+ defer tmp.Close()
+
+ f.Size, err = io.Copy(tmp, file)
+ return err
+}
+
+func wrapUpload(f *multipart.FileHeader) *FileUpload {
+ log.Print(f.Header)
+ return &FileUpload{
+ Name: f.Filename,
+ MimeType: f.Header.Get("Content-Type"),
+ header: f,
+ }
+}
+
+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)
+}
+
+// OpenUploads 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) OpenUploads(tmpDir string) error {
+ var wg sync.WaitGroup
+ for _, f := range u.list {
+ wg.Add(1)
+ go func(f *FileUpload) {
+ defer wg.Done()
+ f.Open(tmpDir)
+ }(f)
+ }
+
+ wg.Wait()
+ log.Print(u.list)
+ return nil
+}
+
+// Clear deletes all temporary files.
+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),
+ }
+
+ 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
+}
diff --git a/payload.go b/payload.go
index cc714eb3..4cfcb68c 100644
--- a/payload.go
+++ b/payload.go
@@ -6,7 +6,7 @@ type Payload struct {
// Context represent payload context, might be omitted
Context []byte
- // Body contains binary payload to be processed by worker
+ // body contains binary payload to be processed by worker
Body []byte
}
diff --git a/psr7/files.go b/psr7/files.go
deleted file mode 100644
index 31ddfec8..00000000
--- a/psr7/files.go
+++ /dev/null
@@ -1,58 +0,0 @@
-package psr7
-
-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/psr7/post.go b/psr7/post.go
deleted file mode 100644
index 30af7e3a..00000000
--- a/psr7/post.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package psr7
-
-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/psr7/request.go b/psr7/request.go
deleted file mode 100644
index 41fbb4bf..00000000
--- a/psr7/request.go
+++ /dev/null
@@ -1,127 +0,0 @@
-package psr7
-
-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/psr7/response.go b/psr7/response.go
deleted file mode 100644
index 6c5f3e17..00000000
--- a/psr7/response.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package psr7
-
-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/psr7/server.go b/psr7/server.go
deleted file mode 100644
index a1c26146..00000000
--- a/psr7/server.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package psr7
-
-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
-}