summaryrefslogtreecommitdiff
path: root/http
diff options
context:
space:
mode:
authorWolfy-J <[email protected]>2018-06-03 12:54:43 +0300
committerWolfy-J <[email protected]>2018-06-03 12:54:43 +0300
commit36ea77baa5a41de10bd604cd0e5b5b3cafaaeb64 (patch)
tree13ca8abd454a6668f490eec2e44b1520bd3953fe /http
parentb02611b7266589d888e054a1d2e4432ae370617d (diff)
service bus, http service, rpc bus, cli commands, new configs
Diffstat (limited to 'http')
-rw-r--r--http/config.go85
-rw-r--r--http/data.go2
-rw-r--r--http/request.go6
-rw-r--r--http/rpc.go57
-rw-r--r--http/server.go40
-rw-r--r--http/service.go67
-rw-r--r--http/uploads.go73
7 files changed, 296 insertions, 34 deletions
diff --git a/http/config.go b/http/config.go
new file mode 100644
index 00000000..fe5fab36
--- /dev/null
+++ b/http/config.go
@@ -0,0 +1,85 @@
+package http
+
+import (
+ "strings"
+ "path"
+ "github.com/spiral/roadrunner/service"
+ "os"
+ "github.com/spiral/roadrunner/utils"
+ "fmt"
+)
+
+// 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
+
+ // TmpDir contains name of temporary directory to store uploaded files passed to underlying PHP process.
+ TmpDir string
+
+ // MaxRequest specified max size for payload body in bytes, set 0 to unlimited.
+ MaxRequest int64
+
+ // ForbidUploads specifies list of file extensions which are forbidden for uploads.
+ // Example: .php, .exe, .bat, .htaccess and etc.
+ ForbidUploads []string
+}
+
+// ForbidUploads must return true if file extension is not allowed for the upload.
+func (cfg Config) Forbidden(filename string) bool {
+ ext := strings.ToLower(path.Ext(filename))
+
+ for _, v := range cfg.ForbidUploads {
+ if ext == v {
+ return true
+ }
+ }
+
+ return false
+}
+
+type serviceConfig struct {
+ Enabled bool
+ Host string
+ Port string
+ MaxRequest string
+ Static struct {
+ Serve bool
+ Root string
+ }
+
+ Uploads struct {
+ TmpDir string
+ Forbid []string
+ }
+
+ Pool service.PoolConfig
+
+ //todo: verbose ?
+}
+
+func (cfg *serviceConfig) httpAddr() string {
+ return fmt.Sprintf("%s:%v", cfg.Host, cfg.Port)
+}
+
+func (cfg *serviceConfig) httpConfig() *Config {
+ tmpDir := cfg.Uploads.TmpDir
+ if tmpDir == "" {
+ tmpDir = os.TempDir()
+ }
+
+ return &Config{
+ ServeStatic: cfg.Static.Serve,
+ Root: cfg.Static.Root,
+ TmpDir: tmpDir,
+ MaxRequest: utils.ParseSize(cfg.MaxRequest),
+ ForbidUploads: cfg.Uploads.Forbid,
+ }
+}
+
+func (cfg *serviceConfig) Valid() error {
+ return nil
+}
diff --git a/http/data.go b/http/data.go
index 865e4760..b84150ee 100644
--- a/http/data.go
+++ b/http/data.go
@@ -42,7 +42,7 @@ func (d dataTree) push(k string, v []string) {
// mount mounts data tree recursively.
func (d dataTree) mount(i []string, v []string) {
- if len(v) == 0 || v[0] == "" {
+ if len(v) == 0 {
return
}
diff --git a/http/request.go b/http/request.go
index 572d7d6a..516deda2 100644
--- a/http/request.go
+++ b/http/request.go
@@ -79,13 +79,13 @@ func NewRequest(r *http.Request) (req *Request, err error) {
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 {
+// 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.OpenUploads(tmpDir)
+ return r.Uploads.Open(cfg)
}
// Close clears all temp file uploads
diff --git a/http/rpc.go b/http/rpc.go
new file mode 100644
index 00000000..c096ff77
--- /dev/null
+++ b/http/rpc.go
@@ -0,0 +1,57 @@
+package http
+
+import (
+ "github.com/sirupsen/logrus"
+)
+
+type RPCServer struct {
+ Service *Service
+}
+
+// WorkerList contains list of workers.
+type WorkerList struct {
+ // Workers is list of workers.
+ Workers []Worker `json:"workers"`
+}
+
+// Worker provides information about specific worker.
+type Worker struct {
+ // Pid contains process id.
+ Pid int `json:"pid"`
+
+ // Status of the worker.
+ Status string `json:"status"`
+
+ // Number of worker executions.
+ NumExecs uint64 `json:"numExecs"`
+
+ // Created is unix nano timestamp of worker creation time.
+ Created int64 `json:"created"`
+
+ // Updated is unix nano timestamp of last worker execution.
+ Updated int64 `json:"updated"`
+}
+
+// Reset resets underlying RR worker pool and restarts all of it's workers.
+func (rpc *RPCServer) Reset(reset bool, r *string) error {
+ logrus.Info("resetting worker pool")
+ *r = "OK"
+
+ return rpc.Service.srv.rr.Reset()
+}
+
+// Workers returns list of active workers and their stats.
+func (rpc *RPCServer) Workers(list bool, r *WorkerList) error {
+ for _, w := range rpc.Service.srv.rr.Workers() {
+ state := w.State()
+ r.Workers = append(r.Workers, Worker{
+ Pid: *w.Pid,
+ Status: state.String(),
+ NumExecs: state.NumExecs(),
+ Created: w.Created.UnixNano(),
+ Updated: state.Updated().UnixNano(),
+ })
+ }
+
+ return nil
+}
diff --git a/http/server.go b/http/server.go
index 363dca2d..039dba02 100644
--- a/http/server.go
+++ b/http/server.go
@@ -3,30 +3,21 @@ package http
import (
"github.com/spiral/roadrunner"
"net/http"
+ "strconv"
+ "errors"
+ "github.com/sirupsen/logrus"
)
-// 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,
+// Service 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
+ cfg *Config
static *staticServer
rr *roadrunner.Server
}
// NewServer returns new instance of HTTP PSR7 server.
-func NewServer(cfg Config, server *roadrunner.Server) *Server {
+func NewServer(cfg *Config, server *roadrunner.Server) *Server {
h := &Server{cfg: cfg, rr: server}
if cfg.ServeStatic {
@@ -42,13 +33,26 @@ func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) () {
return
}
+ // validating request size
+ if srv.cfg.MaxRequest != 0 {
+ if length := r.Header.Get("content-length"); length != "" {
+ if size, err := strconv.ParseInt(length, 10, 64); err != nil {
+ srv.sendError(w, r, err)
+ return
+ } else if size > srv.cfg.MaxRequest {
+ srv.sendError(w, r, errors.New("request body max size is exceeded"))
+ return
+ }
+ }
+ }
+
req, err := NewRequest(r)
if err != nil {
srv.sendError(w, r, err)
return
}
- if err = req.OpenUploads(srv.cfg.UploadsDir); err != nil {
+ if err = req.Open(srv.cfg); err != nil {
srv.sendError(w, r, err)
return
}
@@ -77,6 +81,10 @@ func (srv *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) () {
// sendError sends error
func (srv *Server) sendError(w http.ResponseWriter, r *http.Request, err error) {
+ if _, job := err.(roadrunner.JobError); !job {
+ logrus.Error(err)
+ }
+
w.WriteHeader(500)
w.Write([]byte(err.Error()))
}
diff --git a/http/service.go b/http/service.go
new file mode 100644
index 00000000..e38e1a03
--- /dev/null
+++ b/http/service.go
@@ -0,0 +1,67 @@
+package http
+
+import (
+ "github.com/sirupsen/logrus"
+ "github.com/spiral/roadrunner/service"
+ "net/http"
+ "context"
+)
+
+type Service struct {
+ cfg *serviceConfig
+ http *http.Server
+ srv *Server
+}
+
+func (s *Service) Name() string {
+ return "http"
+}
+
+func (s *Service) Configure(cfg service.Config) (bool, error) {
+ config := &serviceConfig{}
+ if err := cfg.Unmarshal(config); err != nil {
+ return false, err
+ }
+
+ if !config.Enabled {
+ return false, nil
+ }
+
+ if err := config.Valid(); err != nil {
+ return false, err
+ }
+
+ s.cfg = config
+ return true, nil
+}
+
+func (s *Service) RPC() interface{} {
+ return &RPCServer{s}
+}
+
+func (s *Service) Serve() error {
+ logrus.Debugf("http: started")
+ defer logrus.Debugf("http: stopped")
+
+ rr, term, err := s.cfg.Pool.NewServer()
+ if err != nil {
+ return err
+ }
+ defer term()
+
+ s.srv = NewServer(s.cfg.httpConfig(), rr)
+ s.http = &http.Server{
+ Addr: s.cfg.httpAddr(),
+ Handler: s.srv,
+ }
+
+ if err := s.http.ListenAndServe(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (s *Service) Stop() error {
+ return s.http.Shutdown(context.Background())
+}
diff --git a/http/uploads.go b/http/uploads.go
index 1b851e6e..370e73e6 100644
--- a/http/uploads.go
+++ b/http/uploads.go
@@ -3,12 +3,30 @@ package http
import (
"mime/multipart"
"encoding/json"
- "log"
"strings"
"net/http"
"io/ioutil"
"io"
"sync"
+ "os"
+ "fmt"
+)
+
+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.
@@ -17,46 +35,56 @@ type FileUpload struct {
Name string `json:"name"`
// MimeType contains mime-type provided by the client.
- MimeType string `json:"mimetype"`
+ 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
+ Error int `json:"error"`
// TempFilename points to temporary file location.
- TempFilename string `json:"tempFilename"`
+ TempFilename string `json:"tmpName"`
// associated file header
header *multipart.FileHeader
}
-func (f *FileUpload) Open(tmpDir string) error {
+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(tmpDir, "upload")
+ 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()
- f.Size, err = io.Copy(tmp, file)
+ if f.Size, err = io.Copy(tmp, file); err != nil {
+ f.Error = UploadErrorCantWrite
+ }
+
return err
}
func wrapUpload(f *multipart.FileHeader) *FileUpload {
- log.Print(f.Header)
return &FileUpload{
Name: f.Filename,
MimeType: f.Header.Get("Content-Type"),
+ Error: UploadErrorOK,
header: f,
}
}
@@ -119,26 +147,29 @@ 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
+// 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) OpenUploads(tmpDir string) error {
+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(tmpDir)
+ f.Open(cfg)
}(f)
}
wg.Wait()
- log.Print(u.list)
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)
@@ -160,3 +191,17 @@ func parseUploads(r *http.Request) (*Uploads, error) {
return u, nil
}
+
+// exists if file exists. by osutils; todo: better?
+func exists(path string) bool {
+ _, err := os.Stat(path)
+ if err == nil {
+ return true
+ }
+
+ if os.IsNotExist(err) {
+ return false
+ }
+
+ panic(fmt.Errorf("unable to stat path %q; %v", path, err))
+}