diff options
author | Wolfy-J <[email protected]> | 2018-09-30 19:10:43 +0300 |
---|---|---|
committer | GitHub <[email protected]> | 2018-09-30 19:10:43 +0300 |
commit | 427b060039b65dda9497fad8c1c719b8d06c7996 (patch) | |
tree | c1f9a3ca639cc52ba9c144d9c3c2bba444bf78aa | |
parent | 6122fca108c20984732c969fb1ba53cce5b3c44a (diff) | |
parent | 50873a2adabc54aff53fac814d770e3571e37efb (diff) |
Merge pull request #41 from spiral/feature/https-server
Feature/https server
-rw-r--r-- | .travis.yml | 1 | ||||
-rw-r--r-- | CHANGELOG.md | 8 | ||||
-rw-r--r-- | README.md | 2 | ||||
-rwxr-xr-x | build.sh | 2 | ||||
-rw-r--r-- | cmd/rr/.rr.yaml | 23 | ||||
-rw-r--r-- | go.mod | 51 | ||||
-rw-r--r-- | server_config.go | 1 | ||||
-rw-r--r-- | service/env/service.go | 11 | ||||
-rw-r--r-- | service/env/service_test.go | 10 | ||||
-rw-r--r-- | service/http/config.go | 51 | ||||
-rw-r--r-- | service/http/config_test.go | 80 | ||||
-rw-r--r-- | service/http/fixtures/server.crt | 15 | ||||
-rw-r--r-- | service/http/fixtures/server.key | 9 | ||||
-rw-r--r-- | service/http/handler.go | 2 | ||||
-rw-r--r-- | service/http/response.go | 14 | ||||
-rw-r--r-- | service/http/rpc.go | 4 | ||||
-rw-r--r-- | service/http/service.go | 91 | ||||
-rw-r--r-- | service/http/ssl_test.go | 215 | ||||
-rw-r--r-- | service/rpc/service.go | 12 | ||||
-rw-r--r-- | service/rpc/service_test.go | 2 | ||||
-rw-r--r-- | service/static/config.go | 7 | ||||
-rw-r--r-- | service/static/config_test.go | 7 | ||||
-rw-r--r-- | service/static/service.go | 10 | ||||
-rw-r--r-- | service/static/service_test.go | 16 | ||||
-rw-r--r-- | tests/http/push.php | 10 |
25 files changed, 539 insertions, 115 deletions
diff --git a/.travis.yml b/.travis.yml index 78d66eb5..13d0d7a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ install: - go get -u "github.com/pkg/errors" - go get -u "github.com/stretchr/testify/assert" - go get -u "github.com/shirou/gopsutil/process" + - go get -u "golang.org/x/net/http2" - composer install --no-interaction --prefer-source --ignore-platform-reqs script: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d147c5d..a5b35725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +v1.2.4 (30.09.2018) +------ +- minor performance improvements (reduced number of syscalls) +- worker factory connection is not exposed to server using RR_RELAY env +- HTTPS support +- HTTP/2 and HTTP/2 Support +- Removed `disable` flag of static service + v1.2.3 (29.09.2018) ------ - reduced verbosity @@ -21,6 +21,7 @@ Table of Contents * [License](https://github.com/spiral/roadrunner/wiki/License) * Using RoadRunner * [Environment Configuration](https://github.com/spiral/roadrunner/wiki/Enviroment-Configuration) + * [HTTPS and HTTP/2](https://github.com/spiral/roadrunner/wiki/HTTPS-and-HTTP2) * [PHP Workers](https://github.com/spiral/roadrunner/wiki/PHP-Workers) * [Server Commands](https://github.com/spiral/roadrunner/wiki/Server-Commands) * [RPC Integration](https://github.com/spiral/roadrunner/wiki/RPC-Integration) @@ -40,6 +41,7 @@ Features: -------- - production ready - PSR-7 HTTP server (file uploads, error handling, static files, hot reload, middlewares, event listeners) +- HTTPS and HTTP/2 support (including HTTP/2 Push) - fully customizable server - flexible environment configuration - no external PHP dependencies, drop-in (based on [Goridge](https://github.com/spiral/goridge)) @@ -2,7 +2,7 @@ cd $(dirname "${BASH_SOURCE[0]}") OD="$(pwd)" # Pushes application version into the build information. -RR_VERSION=1.2.3 +RR_VERSION=1.2.4 # Hardcode some values to the core package LDFLAGS="$LDFLAGS -X github.com/spiral/roadrunner/cmd/rr/cmd.Version=${RR_VERSION}" diff --git a/cmd/rr/.rr.yaml b/cmd/rr/.rr.yaml index 401a42da..f50ff0e9 100644 --- a/cmd/rr/.rr.yaml +++ b/cmd/rr/.rr.yaml @@ -12,12 +12,22 @@ rpc: # http service configuration. http: - # set to false to disable http server. - enable: true - # http host to listen. address: 0.0.0.0:8080 + ssl: + # custom https port (default 443) + port: 443 + + # force redirect to https connection + redirect: true + + # ssl cert + cert: server.crt + + # ssl private key + key: server.key + # max POST request size, including file uploads in MB. maxRequest: 200 @@ -31,7 +41,7 @@ http: # php worker command. command: "php psr-worker.php pipes" - # connection method (pipes, tcp://:9000, unix://socket.unix). + # connection method (pipes, tcp://:9000, unix://socket.unix). default "pipes" relay: "pipes" # worker pool configuration. @@ -48,11 +58,8 @@ http: # amount of time given to worker to gracefully destruct itself. destroyTimeout: 60 -# static file serving. +# static file serving. remove this section to disable static file serving. static: - # serve http static files - enable: true - # root directory for static file (http would not serve .php and .htaccess files). dir: "public" @@ -1,45 +1,24 @@ module github.com/spiral/roadrunner require ( - github.com/BurntSushi/toml v0.3.0 - github.com/StackExchange/wmi v0.0.0-20180412205111-cdffdb33acae + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect github.com/buger/goterm v0.0.0-20180423150900-6d19e6a8df12 - github.com/davecgh/go-spew v1.1.0 - github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e - github.com/fsnotify/fsnotify v1.4.7 - github.com/go-ole/go-ole v1.2.1 - github.com/golang/protobuf v1.1.0 - github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce - github.com/inconshreveable/mousetrap v1.0.0 - github.com/magiconair/properties v1.8.0 - github.com/mattn/go-colorable v0.0.9 - github.com/mattn/go-isatty v0.0.3 - github.com/mattn/go-runewidth v0.0.2 + github.com/dustin/go-humanize v0.0.0-20180713052910-9f541cc9db5d + github.com/go-ole/go-ole v1.2.1 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/mattn/go-colorable v0.0.9 // indirect + github.com/mattn/go-isatty v0.0.4 // indirect + github.com/mattn/go-runewidth v0.0.3 // indirect github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b - github.com/mitchellh/mapstructure v0.0.0-20180511142126-bb74f1db0675 - github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 - github.com/onsi/ginkgo v1.5.0 - github.com/onsi/gomega v1.4.0 - github.com/pelletier/go-toml v1.2.0 + github.com/olekukonko/tablewriter v0.0.0-20180912035003-be2c049b30cc github.com/pkg/errors v0.8.0 - github.com/pmezard/go-difflib v1.0.0 - github.com/shirou/gopsutil v0.0.0-20180613084040-c23bcca55e77 - github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 - github.com/sirupsen/logrus v1.0.5 - github.com/spf13/afero v1.1.1 - github.com/spf13/cast v1.2.0 + github.com/shirou/gopsutil v2.17.12+incompatible + github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect + github.com/sirupsen/logrus v1.1.0 github.com/spf13/cobra v0.0.3 - github.com/spf13/jwalterweatherman v0.0.0-20180109140146-7c0cea34c8ec - github.com/spf13/pflag v1.0.1 - github.com/spf13/viper v1.0.2 - github.com/spiral/goridge v0.0.0-20180607130832-0351012be508 + github.com/spf13/viper v1.2.1 + github.com/spiral/goridge v2.1.2+incompatible github.com/stretchr/testify v1.2.2 - golang.org/x/crypto v0.0.0-20180614221331-a8fb68e7206f - golang.org/x/net v0.0.0-20180709032641-4d581e05a3ac - golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f - golang.org/x/sys v0.0.0-20180615093615-8014b7b116a6 - golang.org/x/text v0.3.0 - gopkg.in/airbrake/gobrake.v2 v2.0.9 - gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 - gopkg.in/yaml.v2 v2.2.1 + golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3 ) diff --git a/server_config.go b/server_config.go index 936744a1..db13382b 100644 --- a/server_config.go +++ b/server_config.go @@ -75,6 +75,7 @@ func (cfg *ServerConfig) makeCommand() func() *exec.Cmd { var cmd = strings.Split(cfg.Command, " ") return func() *exec.Cmd { cmd := exec.Command(cmd[0], cmd[1:]...) + cmd.Env = append(os.Environ(), fmt.Sprintf("RR_RELAY=%s", cfg.Relay)) cmd.Env = append(os.Environ(), cfg.env...) return cmd } diff --git a/service/env/service.go b/service/env/service.go index 41e70bee..4d1327d4 100644 --- a/service/env/service.go +++ b/service/env/service.go @@ -1,12 +1,7 @@ package env -const ( - // ID contains default service name. - ID = "env" - - // rrKey contains default env key to indicate than php running in RR mode. - rrKey = "rr" -) +// ID contains default service name. +const ID = "env" // Service provides ability to map _ENV values from config file. type Service struct { @@ -25,7 +20,7 @@ func NewService(defaults map[string]string) *Service { func (s *Service) Init(cfg *Config) (bool, error) { if s.values == nil { s.values = make(map[string]string) - s.values[rrKey] = "yes" + s.values["RR"] = "true" } for k, v := range cfg.Values { diff --git a/service/env/service_test.go b/service/env/service_test.go index 28e0d15b..61fecd28 100644 --- a/service/env/service_test.go +++ b/service/env/service_test.go @@ -17,11 +17,11 @@ func Test_Init(t *testing.T) { values, err := s.GetEnv() assert.NoError(t, err) - assert.Equal(t, "yes", values["rr"]) + assert.Equal(t, "true", values["RR"]) } func Test_Extend(t *testing.T) { - s := NewService(map[string]string{"rr": "version"}) + s := NewService(map[string]string{"RR": "version"}) s.Init(&Config{Values: map[string]string{"key": "value"}}) assert.Len(t, s.values, 2) @@ -29,12 +29,12 @@ func Test_Extend(t *testing.T) { values, err := s.GetEnv() assert.NoError(t, err) assert.Len(t, values, 2) - assert.Equal(t, "version", values["rr"]) + assert.Equal(t, "version", values["RR"]) assert.Equal(t, "value", values["key"]) } func Test_Set(t *testing.T) { - s := NewService(map[string]string{"rr": "version"}) + s := NewService(map[string]string{"RR": "version"}) s.Init(&Config{Values: map[string]string{"key": "value"}}) assert.Len(t, s.values, 2) @@ -45,7 +45,7 @@ func Test_Set(t *testing.T) { values, err := s.GetEnv() assert.NoError(t, err) assert.Len(t, values, 3) - assert.Equal(t, "version", values["rr"]) + assert.Equal(t, "version", values["RR"]) assert.Equal(t, "value-new", values["key"]) assert.Equal(t, "new", values["other"]) } diff --git a/service/http/config.go b/service/http/config.go index b11d807c..14738d2e 100644 --- a/service/http/config.go +++ b/service/http/config.go @@ -2,16 +2,21 @@ package http import ( "errors" + "fmt" "github.com/spiral/roadrunner" "github.com/spiral/roadrunner/service" + "os" "strings" ) // Config configures RoadRunner HTTP server. type Config struct { - // Address and port to handle as http server. + // Port and port to handle as http server. Address string + // SSL defines https server options. + SSL SSLConfig + // MaxRequest specified max size for payload body in megabytes, set 0 to unlimited. MaxRequest int64 @@ -22,6 +27,26 @@ type Config struct { Workers *roadrunner.ServerConfig } +// SSLConfig defines https server configuration. +type SSLConfig struct { + // Port to listen as HTTPS server, defaults to 443. + Port int + + // Redirect when enabled forces all http connections to switch to https. + Redirect bool + + // Key defined private server key. + Key string + + // Cert is https certificate. + Cert string +} + +// EnableTLS returns true if rr must listen TLS connections. +func (c *Config) EnableTLS() bool { + return c.SSL.Key != "" || c.SSL.Cert != "" +} + // 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 { @@ -32,6 +57,10 @@ func (c *Config) Hydrate(cfg service.Config) error { c.Uploads = &UploadsConfig{} } + if c.SSL.Port == 0 { + c.SSL.Port = 443 + } + c.Uploads.InitDefaults() c.Workers.InitDefaults() @@ -67,7 +96,25 @@ func (c *Config) Valid() error { } if !strings.Contains(c.Address, ":") { - return errors.New("mailformed server address") + return errors.New("mailformed http server address") + } + + if c.EnableTLS() { + if _, err := os.Stat(c.SSL.Key); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("key file '%s' does not exists", c.SSL.Key) + } + + return err + } + + if _, err := os.Stat(c.SSL.Cert); err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("cert file '%s' does not exists", c.SSL.Cert) + } + + return err + } } return nil diff --git a/service/http/config_test.go b/service/http/config_test.go index 823efb32..07901cb6 100644 --- a/service/http/config_test.go +++ b/service/http/config_test.go @@ -51,6 +51,86 @@ func Test_Config_Valid(t *testing.T) { assert.NoError(t, cfg.Valid()) } +func Test_Config_Valid_SSL(t *testing.T) { + cfg := &Config{ + Address: ":8080", + SSL: SSLConfig{ + Cert: "fixtures/server.crt", + Key: "fixtures/server.key", + }, + MaxRequest: 1024, + Uploads: &UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{".go"}, + }, + Workers: &roadrunner.ServerConfig{ + Command: "php tests/client.php echo pipes", + Relay: "pipes", + Pool: &roadrunner.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + }, + } + + assert.Error(t, cfg.Hydrate(&testCfg{httpCfg: "{}"})) + + assert.NoError(t, cfg.Valid()) + assert.True(t, cfg.EnableTLS()) + assert.Equal(t, 443, cfg.SSL.Port) +} + +func Test_Config_SSL_No_key(t *testing.T) { + cfg := &Config{ + Address: ":8080", + SSL: SSLConfig{ + Cert: "fixtures/server.crt", + }, + MaxRequest: 1024, + Uploads: &UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{".go"}, + }, + Workers: &roadrunner.ServerConfig{ + Command: "php tests/client.php echo pipes", + Relay: "pipes", + Pool: &roadrunner.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + }, + } + + assert.Error(t, cfg.Valid()) +} + +func Test_Config_SSL_No_Cert(t *testing.T) { + cfg := &Config{ + Address: ":8080", + SSL: SSLConfig{ + Key: "fixtures/server.key", + }, + MaxRequest: 1024, + Uploads: &UploadsConfig{ + Dir: os.TempDir(), + Forbid: []string{".go"}, + }, + Workers: &roadrunner.ServerConfig{ + Command: "php tests/client.php echo pipes", + Relay: "pipes", + Pool: &roadrunner.Config{ + NumWorkers: 1, + AllocateTimeout: time.Second, + DestroyTimeout: time.Second, + }, + }, + } + + assert.Error(t, cfg.Valid()) +} + func Test_Config_NoUploads(t *testing.T) { cfg := &Config{ Address: ":8080", diff --git a/service/http/fixtures/server.crt b/service/http/fixtures/server.crt new file mode 100644 index 00000000..24d67fd7 --- /dev/null +++ b/service/http/fixtures/server.crt @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICTTCCAdOgAwIBAgIJAOKyUd+llTRKMAoGCCqGSM49BAMCMGMxCzAJBgNVBAYT +AlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2Nv +MRMwEQYDVQQKDApSb2FkUnVubmVyMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgw +OTMwMTMzNDUzWhcNMjgwOTI3MTMzNDUzWjBjMQswCQYDVQQGEwJVUzETMBEGA1UE +CAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzETMBEGA1UECgwK +Um9hZFJ1bm5lcjESMBAGA1UEAwwJbG9jYWxob3N0MHYwEAYHKoZIzj0CAQYFK4EE +ACIDYgAEVnbShsM+l5RR3wfWWmGhzuFGwNzKCk7i9xyobDIyBUxG/UUSfj7KKlUX +puDnDEtF5xXcepl744CyIAYFLOXHb5WqI4jCOzG0o9f/00QQ4bQudJOdbqV910QF +C2vb7Fxro1MwUTAdBgNVHQ4EFgQU9xUexnbB6ORKayA7Pfjzs33otsAwHwYDVR0j +BBgwFoAU9xUexnbB6ORKayA7Pfjzs33otsAwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAgNoADBlAjEAue3HhR/MUhxoa9tSDBtOJT3FYbDQswrsdqBTz97CGKst +e7XeZ3HMEvEXy0hGGEMhAjAqcD/4k9vViVppgWFtkk6+NFbm+Kw/QeeAiH5FgFSj +8xQcb+b7nPwNLp3JOkXkVd4= +-----END CERTIFICATE----- diff --git a/service/http/fixtures/server.key b/service/http/fixtures/server.key new file mode 100644 index 00000000..7501dd46 --- /dev/null +++ b/service/http/fixtures/server.key @@ -0,0 +1,9 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAIg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDCQP8utxNbHR6xZOLAJgUhn88r6IrPqmN0MsgGJM/jePB+T9UhkmIU8 +PMm2HeScbcugBwYFK4EEACKhZANiAARWdtKGwz6XlFHfB9ZaYaHO4UbA3MoKTuL3 +HKhsMjIFTEb9RRJ+PsoqVRem4OcMS0XnFdx6mXvjgLIgBgUs5cdvlaojiMI7MbSj +1//TRBDhtC50k51upX3XRAULa9vsXGs= +-----END EC PRIVATE KEY----- diff --git a/service/http/handler.go b/service/http/handler.go index f719c751..d7521959 100644 --- a/service/http/handler.go +++ b/service/http/handler.go @@ -110,7 +110,7 @@ func (h *Handler) handleResponse(req *Request, resp *Response) { h.throw(EventResponse, &ResponseEvent{Request: req, Response: resp}) } -// throw invokes event srv if any. +// throw invokes event handler if any. func (h *Handler) throw(event int, ctx interface{}) { h.mul.Lock() defer h.mul.Unlock() diff --git a/service/http/response.go b/service/http/response.go index d43f514d..eb8ce32b 100644 --- a/service/http/response.go +++ b/service/http/response.go @@ -31,9 +31,17 @@ func NewResponse(p *roadrunner.Payload) (*Response, error) { // Write writes response headers, status and body into ResponseWriter. func (r *Response) Write(w http.ResponseWriter) error { - for k, v := range r.Headers { - for _, h := range v { - w.Header().Add(k, h) + for n, h := range r.Headers { + for _, v := range h { + if n == "http2-push" { + if pusher, ok := w.(http.Pusher); ok { + pusher.Push(v, nil) + } + + continue + } + + w.Header().Add(n, v) } } diff --git a/service/http/rpc.go b/service/http/rpc.go index 08b3f262..3390a93d 100644 --- a/service/http/rpc.go +++ b/service/http/rpc.go @@ -15,7 +15,7 @@ type WorkerList struct { // Reset resets underlying RR worker pool and restarts all of it's workers. func (rpc *rpcServer) Reset(reset bool, r *string) error { - if rpc.svc == nil || rpc.svc.srv == nil { + if rpc.svc == nil || rpc.svc.handler == nil { return errors.New("http server is not running") } @@ -25,7 +25,7 @@ func (rpc *rpcServer) Reset(reset bool, r *string) error { // Workers returns list of active workers and their stats. func (rpc *rpcServer) Workers(list bool, r *WorkerList) (err error) { - if rpc.svc == nil || rpc.svc.srv == nil { + if rpc.svc == nil || rpc.svc.handler == nil { return errors.New("http server is not running") } diff --git a/service/http/service.go b/service/http/service.go index bb75a2c0..1f999b8b 100644 --- a/service/http/service.go +++ b/service/http/service.go @@ -2,11 +2,15 @@ package http import ( "context" + "fmt" "github.com/spiral/roadrunner" "github.com/spiral/roadrunner/service/env" "github.com/spiral/roadrunner/service/http/attributes" "github.com/spiral/roadrunner/service/rpc" + "golang.org/x/net/http2" "net/http" + "net/url" + "strings" "sync" "sync/atomic" ) @@ -15,8 +19,8 @@ const ( // ID contains default svc name. ID = "http" - // httpKey indicates to php process that it's running under http service - httpKey = "rr_http" + // EventInitSSL thrown at moment of https initialization. SSL server passed as context. + EventInitSSL = 750 ) // http middleware type. @@ -31,8 +35,9 @@ type Service struct { mu sync.Mutex rr *roadrunner.Server stopping int32 - srv *Handler + handler *Handler http *http.Server + https *http.Server } // AddMiddleware adds new net/http mdwr. @@ -71,28 +76,35 @@ func (s *Service) Serve() error { s.cfg.Workers.SetEnv(k, v) } - s.cfg.Workers.SetEnv(httpKey, "true") + s.cfg.Workers.SetEnv("RR_HTTP", "true") } - rr := roadrunner.NewServer(s.cfg.Workers) + s.rr = roadrunner.NewServer(s.cfg.Workers) + s.rr.Listen(s.throw) - s.rr = rr - s.srv = &Handler{cfg: s.cfg, rr: s.rr} - s.http = &http.Server{Addr: s.cfg.Address} + s.handler = &Handler{cfg: s.cfg, rr: s.rr} + s.handler.Listen(s.throw) - s.rr.Listen(s.listener) - s.srv.Listen(s.listener) + s.http = &http.Server{Addr: s.cfg.Address, Handler: s} - s.http.Handler = s + if s.cfg.EnableTLS() { + s.https = s.initSSL() + } s.mu.Unlock() - if err := rr.Start(); err != nil { + if err := s.rr.Start(); err != nil { return err } - defer rr.Stop() + defer s.rr.Stop() + + err := make(chan error, 2) + go func() { err <- s.http.ListenAndServe() }() + if s.https != nil { + go func() { err <- s.https.ListenAndServeTLS(s.cfg.SSL.Cert, s.cfg.SSL.Key) }() + } - return s.http.ListenAndServe() + return <-err } // Stop stops the svc. @@ -108,23 +120,50 @@ func (s *Service) Stop() { return } - s.http.Shutdown(context.Background()) + if s.https != nil { + go s.https.Shutdown(context.Background()) + } + + go s.http.Shutdown(context.Background()) } -// mdwr handles connection using set of mdwr and rr PSR-7 server. +// 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 { + target := &url.URL{ + Scheme: "https", + Host: s.tlsAddr(r.Host, false), + Path: r.URL.Path, + RawQuery: r.URL.RawQuery, + } + + http.Redirect(w, r, target.String(), http.StatusTemporaryRedirect) + return + } + r = attributes.Init(r) - // chaining mdwr - f := s.srv.ServeHTTP + // chaining middleware + f := s.handler.ServeHTTP for _, m := range s.mdwr { f = m(f) } f(w, r) } -// listener handles service, server and pool events. -func (s *Service) listener(event int, ctx interface{}) { +// Init https server. +func (s *Service) initSSL() *http.Server { + server := &http.Server{Addr: s.tlsAddr(s.cfg.Address, true), Handler: s} + s.throw(EventInitSSL, server) + + // Enable HTTP/2 support by default + http2.ConfigureServer(server, &http2.Server{}) + + return server +} + +// throw handles service, server and pool events. +func (s *Service) throw(event int, ctx interface{}) { for _, l := range s.lsns { l(event, ctx) } @@ -134,3 +173,15 @@ func (s *Service) listener(event int, ctx interface{}) { s.Stop() } } + +// tlsAddr replaces listen or host port with port configured by SSL config. +func (s *Service) tlsAddr(host string, forcePort bool) string { + // remove current forcePort first + host = strings.Split(host, ":")[0] + + if forcePort || s.cfg.SSL.Port != 443 { + host = fmt.Sprintf("%s:%v", host, s.cfg.SSL.Port) + } + + return host +} diff --git a/service/http/ssl_test.go b/service/http/ssl_test.go new file mode 100644 index 00000000..63eb90b1 --- /dev/null +++ b/service/http/ssl_test.go @@ -0,0 +1,215 @@ +package http + +import ( + "crypto/tls" + "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus/hooks/test" + "github.com/spiral/roadrunner/service" + "github.com/stretchr/testify/assert" + "io/ioutil" + "net/http" + "testing" + "time" +) + +var sslClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, +} + +func Test_SSL_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: `{ + "address": ":6029", + "ssl": { + "port": 6900, + "key": "fixtures/server.key", + "cert": "fixtures/server.crt" + }, + "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() { c.Serve() }() + time.Sleep(time.Millisecond * 100) + defer c.Stop() + + req, err := http.NewRequest("GET", "https://localhost:6900?hello=world", nil) + assert.NoError(t, err) + + r, err := sslClient.Do(req) + assert.NoError(t, err) + defer r.Body.Close() + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) +} + +func Test_SSL_Service_NoRedirect(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: `{ + "address": ":6029", + "ssl": { + "port": 6900, + "key": "fixtures/server.key", + "cert": "fixtures/server.crt" + }, + "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() { c.Serve() }() + time.Sleep(time.Millisecond * 100) + defer c.Stop() + + req, err := http.NewRequest("GET", "http://localhost:6029?hello=world", nil) + assert.NoError(t, err) + + r, err := sslClient.Do(req) + assert.NoError(t, err) + defer r.Body.Close() + + assert.Nil(t, r.TLS) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) +} + +func Test_SSL_Service_Redirect(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: `{ + "address": ":6029", + "ssl": { + "port": 6900, + "redirect": true, + "key": "fixtures/server.key", + "cert": "fixtures/server.crt" + }, + "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() { c.Serve() }() + time.Sleep(time.Millisecond * 100) + defer c.Stop() + + req, err := http.NewRequest("GET", "http://localhost:6029?hello=world", nil) + assert.NoError(t, err) + + r, err := sslClient.Do(req) + assert.NoError(t, err) + defer r.Body.Close() + + assert.NotNil(t, r.TLS) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) +} + +func Test_SSL_Service_Push(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: `{ + "address": ":6029", + "ssl": { + "port": 6900, + "redirect": true, + "key": "fixtures/server.key", + "cert": "fixtures/server.crt" + }, + "workers":{ + "command": "php ../../tests/http/client.php push 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() { c.Serve() }() + time.Sleep(time.Millisecond * 100) + defer c.Stop() + + req, err := http.NewRequest("GET", "https://localhost:6900?hello=world", nil) + assert.NoError(t, err) + + r, err := sslClient.Do(req) + assert.NoError(t, err) + defer r.Body.Close() + + assert.NotNil(t, r.TLS) + + b, err := ioutil.ReadAll(r.Body) + assert.NoError(t, err) + + assert.Equal(t, "", r.Header.Get("http2-push")) + + assert.NoError(t, err) + assert.Equal(t, 201, r.StatusCode) + assert.Equal(t, "WORLD", string(b)) +} diff --git a/service/rpc/service.go b/service/rpc/service.go index 3ea6c5fc..0b957976 100644 --- a/service/rpc/service.go +++ b/service/rpc/service.go @@ -8,14 +8,8 @@ import ( "sync" ) -const ( - // ID contains default service name. - ID = "rpc" - - // rrKey defines environment key to be used to store information about - // rpc server connection. - envKey = "rr_rpc" -) +// ID contains default service name. +const ID = "rpc" // Service is RPC service. type Service struct { @@ -36,7 +30,7 @@ func (s *Service) Init(cfg *Config, env env.Environment) (bool, error) { s.rpc = rpc.NewServer() if env != nil { - env.SetEnv(envKey, cfg.Listen) + env.SetEnv("RR_RPC", cfg.Listen) } return true, nil diff --git a/service/rpc/service_test.go b/service/rpc/service_test.go index 467cbe3f..0278d287 100644 --- a/service/rpc/service_test.go +++ b/service/rpc/service_test.go @@ -91,5 +91,5 @@ func TestSetEnv(t *testing.T) { assert.True(t, ok) v, _ := e.GetEnv() - assert.Equal(t, "tcp://localhost:9018", v["rr_rpc"]) + assert.Equal(t, "tcp://localhost:9018", v["RR_RPC"]) } diff --git a/service/static/config.go b/service/static/config.go index be0ac3ed..5df7b013 100644 --- a/service/static/config.go +++ b/service/static/config.go @@ -10,9 +10,6 @@ import ( // Config describes file location and controls access to them. type Config struct { - // Enables StaticFile service. - Enable bool - // Dir contains name of directory to control access to. Dir string @@ -32,10 +29,6 @@ func (c *Config) Hydrate(cfg service.Config) error { // Valid returns nil if config is valid. func (c *Config) Valid() error { - if !c.Enable { - return nil - } - st, err := os.Stat(c.Dir) if err != nil { if os.IsNotExist(err) { diff --git a/service/static/config_test.go b/service/static/config_test.go index d36726c2..e3fa8d16 100644 --- a/service/static/config_test.go +++ b/service/static/config_test.go @@ -36,8 +36,7 @@ func TestConfig_Forbids(t *testing.T) { } func TestConfig_Valid(t *testing.T) { - assert.NoError(t, (&Config{Enable: true, Dir: "./"}).Valid()) - assert.Error(t, (&Config{Enable: true, Dir: "./config.go"}).Valid()) - assert.NoError(t, (&Config{Dir: "./dir/"}).Valid()) - assert.Error(t, (&Config{Enable: true, Dir: "./dir/"}).Valid()) + assert.NoError(t, (&Config{Dir: "./"}).Valid()) + assert.Error(t, (&Config{Dir: "./config.go"}).Valid()) + assert.Error(t, (&Config{Dir: "./dir/"}).Valid()) } diff --git a/service/static/service.go b/service/static/service.go index 98d8313c..b2723e42 100644 --- a/service/static/service.go +++ b/service/static/service.go @@ -4,7 +4,6 @@ import ( rrttp "github.com/spiral/roadrunner/service/http" "net/http" "path" - "strings" ) // ID contains default service name. @@ -22,7 +21,7 @@ type Service struct { // Init must return configure service and return true if service hasStatus enabled. Must return error in case of // misconfiguration. Services must not be used without proper configuration pushed first. func (s *Service) Init(cfg *Config, r *rrttp.Service) (bool, error) { - if !cfg.Enable || r == nil { + if r == nil { return false, nil } @@ -44,12 +43,7 @@ func (s *Service) middleware(f http.HandlerFunc) http.HandlerFunc { } func (s *Service) handleStatic(w http.ResponseWriter, r *http.Request) bool { - fPath := r.URL.Path - - if !strings.HasPrefix(fPath, "/") { - fPath = "/" + fPath - } - fPath = path.Clean(fPath) + fPath := path.Clean(r.URL.Path) if s.cfg.Forbids(fPath) { return false diff --git a/service/static/service_test.go b/service/static/service_test.go index 7b40b8ad..fbc26a58 100644 --- a/service/static/service_test.go +++ b/service/static/service_test.go @@ -84,6 +84,22 @@ func Test_Files(t *testing.T) { assert.Equal(t, "sample", b) } +func Test_Disabled(t *testing.T) { + logger, _ := test.NewNullLogger() + logger.SetLevel(logrus.DebugLevel) + + c := service.NewContainer(logger) + c.Register(ID, &Service{}) + + assert.NoError(t, c.Init(&testCfg{ + static: `{"enable":true, "dir":"../../tests", "forbid":[]}`, + })) + + s, st := c.Get(ID) + assert.NotNil(t, s) + assert.Equal(t, service.StatusRegistered, st) +} + func Test_Files_Disable(t *testing.T) { logger, _ := test.NewNullLogger() logger.SetLevel(logrus.DebugLevel) diff --git a/tests/http/push.php b/tests/http/push.php new file mode 100644 index 00000000..bf56dbb6 --- /dev/null +++ b/tests/http/push.php @@ -0,0 +1,10 @@ +<?php + +use \Psr\Http\Message\ServerRequestInterface; +use \Psr\Http\Message\ResponseInterface; + +function handleRequest(ServerRequestInterface $req, ResponseInterface $resp): ResponseInterface +{ + $resp->getBody()->write(strtoupper($req->getQueryParams()['hello'])); + return $resp->withAddedHeader("http2-push", __FILE__)->withStatus(201); +}
\ No newline at end of file |