diff options
author | Anton Titov <[email protected]> | 2019-06-14 12:44:45 +0300 |
---|---|---|
committer | GitHub <[email protected]> | 2019-06-14 12:44:45 +0300 |
commit | 78724c084ac4db42e713c45ef8cb1453e8352183 (patch) | |
tree | a85abba72b65134a067f4f799b5e1d88a5101d3b | |
parent | 27e8cb3c4086012968b586fcbd5fd40737be2510 (diff) | |
parent | ac126193fb36fdf248336fa433cbd13602f2ae75 (diff) |
Merge pull request #162 from ovr/fcgi
Feature - FastCGI support for HTTP Service
-rw-r--r-- | .rr.yaml | 5 | ||||
-rw-r--r-- | go.mod | 9 | ||||
-rw-r--r-- | service/http/config.go | 17 | ||||
-rw-r--r-- | service/http/config_test.go | 4 | ||||
-rw-r--r-- | service/http/fcgi_test.go | 59 | ||||
-rw-r--r-- | service/http/service.go | 58 | ||||
-rw-r--r-- | service/http/service_test.go | 2 | ||||
-rw-r--r-- | service/rpc/config.go | 17 | ||||
-rw-r--r-- | service/rpc/config_test.go | 12 | ||||
-rw-r--r-- | util/network.go | 25 | ||||
-rw-r--r-- | util/network_test.go | 14 |
11 files changed, 190 insertions, 32 deletions
@@ -28,6 +28,11 @@ http: # ssl private key key: server.key + # HTTP service provides FastCGI as frontend + fcgi: + # FastCGI connection DSN. Supported TCP and Unix sockets. + address: tcp://0.0.0.0:6920 + # max POST request size, including file uploads in MB. maxRequestSize: 200 @@ -1,22 +1,27 @@ module github.com/spiral/roadrunner require ( + github.com/BurntSushi/toml v0.3.1 // indirect github.com/StackExchange/wmi v0.0.0-20181212234831-e0a55b97c705 // indirect github.com/buger/goterm v0.0.0-20181115115552-c206103e1f37 github.com/dustin/go-humanize v1.0.0 github.com/go-ole/go-ole v1.2.4 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/kr/pretty v0.1.0 // indirect github.com/mattn/go-colorable v0.1.1 // indirect github.com/mattn/go-runewidth v0.0.4 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b github.com/olekukonko/tablewriter v0.0.1 github.com/pkg/errors v0.8.1 github.com/shirou/gopsutil v2.17.12+incompatible + github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect github.com/sirupsen/logrus v1.3.0 github.com/spf13/cobra v0.0.3 github.com/spf13/viper v1.3.1 github.com/spiral/goridge v2.1.3+incompatible - github.com/spiral/jobs v1.1.1 // indirect - github.com/spiral/php-grpc v1.0.6 // indirect github.com/stretchr/testify v1.2.2 + github.com/yookoala/gofast v0.3.0 golang.org/x/net v0.0.0-20181017193950-04a2e542c03f + golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/service/http/config.go b/service/http/config.go index ecd5a3a8..4b5950b3 100644 --- a/service/http/config.go +++ b/service/http/config.go @@ -18,6 +18,8 @@ type Config struct { // SSL defines https server options. SSL SSLConfig + FCGI FCGIConfig + // MaxRequestSize specified max size for payload body in megabytes, set 0 to unlimited. MaxRequestSize int64 @@ -32,6 +34,11 @@ type Config struct { Workers *roadrunner.ServerConfig } +type FCGIConfig struct { + // Port and port to handle as http server. + Address string +} + // SSLConfig defines https server configuration. type SSLConfig struct { // Port to listen as HTTPS server, defaults to 443. @@ -47,11 +54,19 @@ type SSLConfig struct { Cert string } +func (c *Config) EnableHTTP() bool { + return c.Address != "" +} + // EnableTLS returns true if rr must listen TLS connections. func (c *Config) EnableTLS() bool { return c.SSL.Key != "" || c.SSL.Cert != "" } +func (c *Config) EnableFCGI() bool { + return c.FCGI.Address != "" +} + // Hydrate must populate Config values using given Config source. Must return error if Config is not valid. func (c *Config) Hydrate(cfg service.Config) error { if c.Workers == nil { @@ -146,7 +161,7 @@ func (c *Config) Valid() error { return err } - if !strings.Contains(c.Address, ":") { + if c.Address != "" && !strings.Contains(c.Address, ":") { return errors.New("mailformed http server address") } diff --git a/service/http/config_test.go b/service/http/config_test.go index 48651e16..54e5b27a 100644 --- a/service/http/config_test.go +++ b/service/http/config_test.go @@ -19,7 +19,7 @@ func Test_Config_Hydrate_Error1(t *testing.T) { cfg := &mockCfg{`{"enable": true}`} c := &Config{} - assert.Error(t, c.Hydrate(cfg)) + assert.NoError(t, c.Hydrate(cfg)) } func Test_Config_Hydrate_Error2(t *testing.T) { @@ -252,7 +252,7 @@ func Test_Config_DeadPool(t *testing.T) { func Test_Config_InvalidAddress(t *testing.T) { cfg := &Config{ - Address: "", + Address: "unexpected_address", MaxRequestSize: 1024, Uploads: &UploadsConfig{ Dir: os.TempDir(), diff --git a/service/http/fcgi_test.go b/service/http/fcgi_test.go new file mode 100644 index 00000000..6b6cd0f3 --- /dev/null +++ b/service/http/fcgi_test.go @@ -0,0 +1,59 @@ +package http + +import ( + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/spiral/roadrunner/service" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http/httptest" + "testing" + "time" + "github.com/yookoala/gofast" +) + +func Test_FCGI_Service_Echo(t *testing.T) { + logger, _ := test.NewNullLogger() + logger.SetLevel(logrus.DebugLevel) + + c := service.NewContainer(logger) + c.Register(ID, &Service{}) + + assert.NoError(t, c.Init(&testCfg{httpCfg: `{ + "fcgi": { + "address": "tcp://0.0.0.0:6082" + }, + "workers":{ + "command": "php ../../tests/http/client.php echo pipes", + "pool": {"numWorkers": 1} + } + }`})) + + s, st := c.Get(ID) + assert.NotNil(t, s) + assert.Equal(t, service.StatusOK, st) + + // should do nothing + s.(*Service).Stop() + + go func() { assert.NoError(t, c.Serve()) }() + time.Sleep(time.Millisecond * 100) + defer c.Stop() + + fcgiConnFactory := gofast.SimpleConnFactory("tcp", "0.0.0.0:6082") + + fcgiHandler := gofast.NewHandler( + gofast.BasicParamsMap(gofast.BasicSession), + gofast.SimpleClientFactory(fcgiConnFactory, 0), + ) + + w := httptest.NewRecorder() + req := httptest.NewRequest("GET", "http://site.local/?hello=world", nil) + fcgiHandler.ServeHTTP(w, req) + + body, err := ioutil.ReadAll(w.Result().Body); + + assert.NoError(t, err) + assert.Equal(t, 201, w.Result().StatusCode) + assert.Equal(t, "WORLD", string(body)) +} diff --git a/service/http/service.go b/service/http/service.go index 8105d218..b309cd45 100644 --- a/service/http/service.go +++ b/service/http/service.go @@ -7,8 +7,10 @@ import ( "github.com/spiral/roadrunner/service/env" "github.com/spiral/roadrunner/service/http/attributes" "github.com/spiral/roadrunner/service/rpc" + "github.com/spiral/roadrunner/util" "golang.org/x/net/http2" "net/http" + "net/http/fcgi" "net/url" "strings" "sync" @@ -37,6 +39,7 @@ type Service struct { handler *Handler http *http.Server https *http.Server + fcgi *http.Server } // Attach attaches controller. Currently only one controller is supported. @@ -66,6 +69,10 @@ func (s *Service) Init(cfg *Config, r *rpc.Service, e env.Environment) (bool, er } } + if !cfg.EnableHTTP() && !cfg.EnableTLS() && !cfg.EnableFCGI() { + return false, nil + } + return true, nil } @@ -91,12 +98,18 @@ func (s *Service) Serve() error { s.handler = &Handler{cfg: s.cfg, rr: s.rr} s.handler.Listen(s.throw) - s.http = &http.Server{Addr: s.cfg.Address, Handler: s} + if s.cfg.EnableHTTP() { + s.http = &http.Server{Addr: s.cfg.Address, Handler: s} + } if s.cfg.EnableTLS() { s.https = s.initSSL() } + if s.cfg.EnableFCGI() { + s.fcgi = &http.Server{Handler: s} + } + s.mu.Unlock() if err := s.rr.Start(); err != nil { @@ -104,10 +117,24 @@ func (s *Service) Serve() error { } defer s.rr.Stop() - err := make(chan error, 2) - go func() { err <- s.http.ListenAndServe() }() + err := make(chan error, 3) + + if s.http != nil { + go func() { + err <- s.http.ListenAndServe() + }() + } + if s.https != nil { - go func() { err <- s.https.ListenAndServeTLS(s.cfg.SSL.Cert, s.cfg.SSL.Key) }() + go func() { + err <- s.https.ListenAndServeTLS(s.cfg.SSL.Cert, s.cfg.SSL.Key) + }() + } + + if s.fcgi != nil { + go func() { + err <- s.ListenAndServeFCGI() + }() } return <-err @@ -117,15 +144,18 @@ func (s *Service) Serve() error { func (s *Service) Stop() { s.mu.Lock() defer s.mu.Unlock() - if s.http == nil { - return + + if s.fcgi != nil { + go s.fcgi.Shutdown(context.Background()) } if s.https != nil { go s.https.Shutdown(context.Background()) } - go s.http.Shutdown(context.Background()) + if s.http != nil { + go s.http.Shutdown(context.Background()) + } } // Server returns associated rr server (if any). @@ -136,6 +166,20 @@ func (s *Service) Server() *roadrunner.Server { return s.rr } +func (s *Service) ListenAndServeFCGI() error { + l, err := util.CreateListener(s.cfg.FCGI.Address); + if err != nil { + return err + } + + err = fcgi.Serve(l, s.fcgi.Handler) + if err != nil { + return err + } + + return nil +} + // ServeHTTP handles connection using set of middleware and rr PSR-7 server. func (s *Service) ServeHTTP(w http.ResponseWriter, r *http.Request) { if s.https != nil && r.TLS == nil && s.cfg.SSL.Redirect { diff --git a/service/http/service_test.go b/service/http/service_test.go index b3b001c7..69cb7003 100644 --- a/service/http/service_test.go +++ b/service/http/service_test.go @@ -53,7 +53,7 @@ func Test_Service_NoConfig(t *testing.T) { c := service.NewContainer(logger) c.Register(ID, &Service{}) - assert.Error(t, c.Init(&testCfg{httpCfg: `{"Enable":true}`})) + c.Init(&testCfg{httpCfg: `{"Enable":true}`}) s, st := c.Get(ID) assert.NotNil(t, s) diff --git a/service/rpc/config.go b/service/rpc/config.go index fc8cfdbb..8a29c2d8 100644 --- a/service/rpc/config.go +++ b/service/rpc/config.go @@ -3,9 +3,9 @@ package rpc import ( "errors" "github.com/spiral/roadrunner/service" + "github.com/spiral/roadrunner/util" "net" "strings" - "syscall" ) // Config defines RPC service config. @@ -37,7 +37,7 @@ func (c *Config) InitDefaults() error { // Valid returns nil if config is valid. func (c *Config) Valid() error { if dsn := strings.Split(c.Listen, "://"); len(dsn) != 2 { - return errors.New("invalid socket DSN (tcp://:6001, unix://rpc.sock)") + return errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)") } return nil @@ -45,23 +45,14 @@ func (c *Config) Valid() error { // Listener creates new rpc socket Listener. func (c *Config) Listener() (net.Listener, error) { - dsn := strings.Split(c.Listen, "://") - if len(dsn) != 2 { - return nil, errors.New("invalid socket DSN (tcp://:6001, unix://rpc.sock)") - } - - if dsn[0] == "unix" { - syscall.Unlink(dsn[1]) - } - - return net.Listen(dsn[0], dsn[1]) + return util.CreateListener(c.Listen); } // Dialer creates rpc socket Dialer. func (c *Config) Dialer() (net.Conn, error) { dsn := strings.Split(c.Listen, "://") if len(dsn) != 2 { - return nil, errors.New("invalid socket DSN (tcp://:6001, unix://rpc.sock)") + return nil, errors.New("invalid socket DSN (tcp://:6001, unix://file.sock)") } return net.Dial(dsn[0], dsn[1]) diff --git a/service/rpc/config_test.go b/service/rpc/config_test.go index 6a2138d2..af261698 100644 --- a/service/rpc/config_test.go +++ b/service/rpc/config_test.go @@ -51,7 +51,7 @@ func TestConfig_ListenerUnix(t *testing.T) { t.Skip("not supported on " + runtime.GOOS) } - cfg := &Config{Listen: "unix://rpc.sock"} + cfg := &Config{Listen: "unix://file.sock"} ln, err := cfg.Listener() assert.NoError(t, err) @@ -59,7 +59,7 @@ func TestConfig_ListenerUnix(t *testing.T) { defer ln.Close() assert.Equal(t, "unix", ln.Addr().Network()) - assert.Equal(t, "rpc.sock", ln.Addr().String()) + assert.Equal(t, "file.sock", ln.Addr().String()) } func Test_Config_Error(t *testing.T) { @@ -71,7 +71,7 @@ func Test_Config_Error(t *testing.T) { ln, err := cfg.Listener() assert.Nil(t, ln) assert.Error(t, err) - assert.Equal(t, "invalid socket DSN (tcp://:6001, unix://rpc.sock)", err.Error()) + assert.Equal(t, "Invalid DSN (tcp://:6001, unix://file.sock)", err.Error()) } func Test_Config_ErrorMethod(t *testing.T) { @@ -102,7 +102,7 @@ func TestConfig_DialerUnix(t *testing.T) { t.Skip("not supported on " + runtime.GOOS) } - cfg := &Config{Listen: "unix://rpc.sock"} + cfg := &Config{Listen: "unix://file.sock"} ln, _ := cfg.Listener() defer ln.Close() @@ -113,7 +113,7 @@ func TestConfig_DialerUnix(t *testing.T) { defer conn.Close() assert.Equal(t, "unix", conn.RemoteAddr().Network()) - assert.Equal(t, "rpc.sock", conn.RemoteAddr().String()) + assert.Equal(t, "file.sock", conn.RemoteAddr().String()) } func Test_Config_DialerError(t *testing.T) { @@ -125,7 +125,7 @@ func Test_Config_DialerError(t *testing.T) { ln, err := cfg.Dialer() assert.Nil(t, ln) assert.Error(t, err) - assert.Equal(t, "invalid socket DSN (tcp://:6001, unix://rpc.sock)", err.Error()) + assert.Equal(t, "invalid socket DSN (tcp://:6001, unix://file.sock)", err.Error()) } func Test_Config_DialerErrorMethod(t *testing.T) { diff --git a/util/network.go b/util/network.go new file mode 100644 index 00000000..4c393c37 --- /dev/null +++ b/util/network.go @@ -0,0 +1,25 @@ +package util + +import ( + "net" + "strings" + "syscall" + "errors" +) + +func CreateListener(address string) (net.Listener, error) { + dsn := strings.Split(address, "://") + if len(dsn) != 2 { + return nil, errors.New("Invalid DSN (tcp://:6001, unix://file.sock)") + } + + if dsn[0] != "unix" && dsn[0] != "tcp" { + return nil, errors.New("Invalid Protocol (tcp://:6001, unix://file.sock)") + } + + if dsn[0] == "unix" { + syscall.Unlink(dsn[1]) + } + + return net.Listen(dsn[0], dsn[1]) +}
\ No newline at end of file diff --git a/util/network_test.go b/util/network_test.go new file mode 100644 index 00000000..bdc7e0b7 --- /dev/null +++ b/util/network_test.go @@ -0,0 +1,14 @@ +package util + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestCreateListener(t *testing.T) { + _, err := CreateListener("unexpected dsn"); + assert.Error(t, err, "Invalid DSN (tcp://:6001, unix://file.sock)") + + _, err = CreateListener("aaa://192.168.0.1"); + assert.Error(t, err, "Invalid Protocol (tcp://:6001, unix://file.sock)") +} |