diff options
author | Wolfy-J <[email protected]> | 2018-06-03 12:54:43 +0300 |
---|---|---|
committer | Wolfy-J <[email protected]> | 2018-06-03 12:54:43 +0300 |
commit | 36ea77baa5a41de10bd604cd0e5b5b3cafaaeb64 (patch) | |
tree | 13ca8abd454a6668f490eec2e44b1520bd3953fe /http | |
parent | b02611b7266589d888e054a1d2e4432ae370617d (diff) |
service bus, http service, rpc bus, cli commands, new configs
Diffstat (limited to 'http')
-rw-r--r-- | http/config.go | 85 | ||||
-rw-r--r-- | http/data.go | 2 | ||||
-rw-r--r-- | http/request.go | 6 | ||||
-rw-r--r-- | http/rpc.go | 57 | ||||
-rw-r--r-- | http/server.go | 40 | ||||
-rw-r--r-- | http/service.go | 67 | ||||
-rw-r--r-- | http/uploads.go | 73 |
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)) +} |