summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnton Titov <[email protected]>2019-06-14 12:44:45 +0300
committerGitHub <[email protected]>2019-06-14 12:44:45 +0300
commit78724c084ac4db42e713c45ef8cb1453e8352183 (patch)
treea85abba72b65134a067f4f799b5e1d88a5101d3b
parent27e8cb3c4086012968b586fcbd5fd40737be2510 (diff)
parentac126193fb36fdf248336fa433cbd13602f2ae75 (diff)
Merge pull request #162 from ovr/fcgi
Feature - FastCGI support for HTTP Service
-rw-r--r--.rr.yaml5
-rw-r--r--go.mod9
-rw-r--r--service/http/config.go17
-rw-r--r--service/http/config_test.go4
-rw-r--r--service/http/fcgi_test.go59
-rw-r--r--service/http/service.go58
-rw-r--r--service/http/service_test.go2
-rw-r--r--service/rpc/config.go17
-rw-r--r--service/rpc/config_test.go12
-rw-r--r--util/network.go25
-rw-r--r--util/network_test.go14
11 files changed, 190 insertions, 32 deletions
diff --git a/.rr.yaml b/.rr.yaml
index 4e00f0f7..973bc03f 100644
--- a/.rr.yaml
+++ b/.rr.yaml
@@ -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
diff --git a/go.mod b/go.mod
index 40692ea9..8ec6a1fb 100644
--- a/go.mod
+++ b/go.mod
@@ -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)")
+}