summaryrefslogtreecommitdiff
path: root/service/http
diff options
context:
space:
mode:
Diffstat (limited to 'service/http')
-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
9 files changed, 453 insertions, 28 deletions
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))
+}