summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWolfy-J <[email protected]>2018-09-30 19:10:43 +0300
committerGitHub <[email protected]>2018-09-30 19:10:43 +0300
commit427b060039b65dda9497fad8c1c719b8d06c7996 (patch)
treec1f9a3ca639cc52ba9c144d9c3c2bba444bf78aa
parent6122fca108c20984732c969fb1ba53cce5b3c44a (diff)
parent50873a2adabc54aff53fac814d770e3571e37efb (diff)
Merge pull request #41 from spiral/feature/https-server
Feature/https server
-rw-r--r--.travis.yml1
-rw-r--r--CHANGELOG.md8
-rw-r--r--README.md2
-rwxr-xr-xbuild.sh2
-rw-r--r--cmd/rr/.rr.yaml23
-rw-r--r--go.mod51
-rw-r--r--server_config.go1
-rw-r--r--service/env/service.go11
-rw-r--r--service/env/service_test.go10
-rw-r--r--service/http/config.go51
-rw-r--r--service/http/config_test.go80
-rw-r--r--service/http/fixtures/server.crt15
-rw-r--r--service/http/fixtures/server.key9
-rw-r--r--service/http/handler.go2
-rw-r--r--service/http/response.go14
-rw-r--r--service/http/rpc.go4
-rw-r--r--service/http/service.go91
-rw-r--r--service/http/ssl_test.go215
-rw-r--r--service/rpc/service.go12
-rw-r--r--service/rpc/service_test.go2
-rw-r--r--service/static/config.go7
-rw-r--r--service/static/config_test.go7
-rw-r--r--service/static/service.go10
-rw-r--r--service/static/service_test.go16
-rw-r--r--tests/http/push.php10
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
diff --git a/README.md b/README.md
index 9cc36e5c..5f8707c9 100644
--- a/README.md
+++ b/README.md
@@ -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))
diff --git a/build.sh b/build.sh
index 32964a57..4540c912 100755
--- a/build.sh
+++ b/build.sh
@@ -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"
diff --git a/go.mod b/go.mod
index 28a6fac0..c0470c75 100644
--- a/go.mod
+++ b/go.mod
@@ -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