summaryrefslogtreecommitdiff
path: root/plugins/http/plugin.go
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/http/plugin.go')
-rw-r--r--plugins/http/plugin.go423
1 files changed, 423 insertions, 0 deletions
diff --git a/plugins/http/plugin.go b/plugins/http/plugin.go
new file mode 100644
index 00000000..7c46c814
--- /dev/null
+++ b/plugins/http/plugin.go
@@ -0,0 +1,423 @@
+package http
+
+import (
+ "context"
+ "crypto/tls"
+ "crypto/x509"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/http/fcgi"
+ "net/url"
+ "strings"
+ "sync"
+
+ "github.com/spiral/roadrunner/v2/interfaces/log"
+ "github.com/spiral/roadrunner/v2/plugins/config"
+ "github.com/spiral/roadrunner/v2/plugins/rpc"
+ "golang.org/x/net/http2"
+ "golang.org/x/net/http2/h2c"
+ "golang.org/x/sys/cpu"
+)
+
+const (
+ // ID contains default service name.
+ ID = "http"
+
+ // EventInitSSL thrown at moment of https initialization. SSL server passed as context.
+ EventInitSSL = 750
+)
+
+var couldNotAppendPemError = errors.New("could not append Certs from PEM")
+
+// http middleware type.
+type middleware func(f http.HandlerFunc) http.HandlerFunc
+
+// Service manages rr, http servers.
+type Service struct {
+ sync.Mutex
+ sync.WaitGroup
+
+ cfg *Config
+ log *logrus.Logger
+ cprod roadrunner.CommandProducer
+ env env.Environment
+ lsns []func(event int, ctx interface{})
+ mdwr []middleware
+
+ rr *roadrunner.Server
+ controller roadrunner.Controller
+ handler *Handler
+
+ http *http.Server
+ https *http.Server
+ fcgi *http.Server
+}
+
+// Attach attaches controller. Currently only one controller is supported.
+func (s *Service) Attach(w roadrunner.Controller) {
+ s.controller = w
+}
+
+// ProduceCommands changes the default command generator method
+func (s *Service) ProduceCommands(producer roadrunner.CommandProducer) {
+ s.cprod = producer
+}
+
+// AddMiddleware adds new net/http mdwr.
+func (s *Service) AddMiddleware(m middleware) {
+ s.mdwr = append(s.mdwr, m)
+}
+
+// AddListener attaches server event controller.
+func (s *Service) AddListener(l func(event int, ctx interface{})) {
+ s.lsns = append(s.lsns, l)
+}
+
+// Init must return configure svc and return true if svc 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.Configurer, r rpc.Plugin, log log.Logger) (bool, error) {
+ s.cfg = cfg
+ s.log = log
+
+ //if r != nil {
+ // if err := r.Register(ID, &rpcServer{s}); err != nil {
+ // return false, err
+ // }
+ //}
+ //
+ //if !cfg.EnableHTTP() && !cfg.EnableTLS() && !cfg.EnableFCGI() {
+ // return false, nil
+ //}
+
+ return true, nil
+}
+
+// Serve serves the svc.
+func (s *Service) Serve() error {
+ s.Lock()
+
+ if s.env != nil {
+ if err := s.env.Copy(s.cfg.Workers); err != nil {
+ return nil
+ }
+ }
+
+ s.cfg.Workers.CommandProducer = s.cprod
+ s.cfg.Workers.SetEnv("RR_HTTP", "true")
+
+ s.rr = roadrunner.NewServer(s.cfg.Workers)
+ s.rr.Listen(s.throw)
+
+ if s.controller != nil {
+ s.rr.Attach(s.controller)
+ }
+
+ s.handler = &Handler{cfg: s.cfg, rr: s.rr}
+ s.handler.Listen(s.throw)
+
+ if s.cfg.EnableHTTP() {
+ if s.cfg.EnableH2C() {
+ s.http = &http.Server{Addr: s.cfg.Address, Handler: h2c.NewHandler(s, &http2.Server{})}
+ } else {
+ s.http = &http.Server{Addr: s.cfg.Address, Handler: s}
+ }
+ }
+
+ if s.cfg.EnableTLS() {
+ s.https = s.initSSL()
+ if s.cfg.SSL.RootCA != "" {
+ err := s.appendRootCa()
+ if err != nil {
+ return err
+ }
+ }
+
+ if s.cfg.EnableHTTP2() {
+ if err := s.initHTTP2(); err != nil {
+ return err
+ }
+ }
+ }
+
+ if s.cfg.EnableFCGI() {
+ s.fcgi = &http.Server{Handler: s}
+ }
+
+ s.Unlock()
+
+ if err := s.rr.Start(); err != nil {
+ return err
+ }
+ defer s.rr.Stop()
+
+ err := make(chan error, 3)
+
+ if s.http != nil {
+ go func() {
+ httpErr := s.http.ListenAndServe()
+ if httpErr != nil && httpErr != http.ErrServerClosed {
+ err <- httpErr
+ } else {
+ err <- nil
+ }
+ }()
+ }
+
+ if s.https != nil {
+ go func() {
+ httpErr := s.https.ListenAndServeTLS(
+ s.cfg.SSL.Cert,
+ s.cfg.SSL.Key,
+ )
+
+ if httpErr != nil && httpErr != http.ErrServerClosed {
+ err <- httpErr
+ return
+ }
+ err <- nil
+ }()
+ }
+
+ if s.fcgi != nil {
+ go func() {
+ httpErr := s.serveFCGI()
+ if httpErr != nil && httpErr != http.ErrServerClosed {
+ err <- httpErr
+ return
+ }
+ err <- nil
+ }()
+ }
+ return <-err
+}
+
+// Stop stops the http.
+func (s *Service) Stop() {
+ s.Lock()
+ defer s.Unlock()
+
+ if s.fcgi != nil {
+ s.Add(1)
+ go func() {
+ defer s.Done()
+ err := s.fcgi.Shutdown(context.Background())
+ if err != nil && err != http.ErrServerClosed {
+ // Stop() error
+ // push error from goroutines to the channel and block unil error or success shutdown or timeout
+ s.log.Error(fmt.Errorf("error shutting down the fcgi server, error: %v", err))
+ return
+ }
+ }()
+ }
+
+ if s.https != nil {
+ s.Add(1)
+ go func() {
+ defer s.Done()
+ err := s.https.Shutdown(context.Background())
+ if err != nil && err != http.ErrServerClosed {
+ s.log.Error(fmt.Errorf("error shutting down the https server, error: %v", err))
+ return
+ }
+ }()
+ }
+
+ if s.http != nil {
+ s.Add(1)
+ go func() {
+ defer s.Done()
+ err := s.http.Shutdown(context.Background())
+ if err != nil && err != http.ErrServerClosed {
+ s.log.Error(fmt.Errorf("error shutting down the http server, error: %v", err))
+ return
+ }
+ }()
+ }
+
+ s.Wait()
+}
+
+// Server returns associated rr server (if any).
+func (s *Service) Server() *roadrunner.Server {
+ s.Lock()
+ defer s.Unlock()
+
+ return s.rr
+}
+
+// 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
+ }
+
+ if s.https != nil && r.TLS != nil {
+ w.Header().Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
+ }
+
+ r = attributes.Init(r)
+
+ // chaining middleware
+ f := s.handler.ServeHTTP
+ for _, m := range s.mdwr {
+ f = m(f)
+ }
+ f(w, r)
+}
+
+// append RootCA to the https server TLS config
+func (s *Service) appendRootCa() error {
+ rootCAs, err := x509.SystemCertPool()
+ if err != nil {
+ s.throw(EventInitSSL, nil)
+ return nil
+ }
+ if rootCAs == nil {
+ rootCAs = x509.NewCertPool()
+ }
+
+ CA, err := ioutil.ReadFile(s.cfg.SSL.RootCA)
+ if err != nil {
+ s.throw(EventInitSSL, nil)
+ return err
+ }
+
+ // should append our CA cert
+ ok := rootCAs.AppendCertsFromPEM(CA)
+ if !ok {
+ return couldNotAppendPemError
+ }
+ config := &tls.Config{
+ InsecureSkipVerify: false,
+ RootCAs: rootCAs,
+ }
+ s.http.TLSConfig = config
+
+ return nil
+}
+
+// Init https server
+func (s *Service) initSSL() *http.Server {
+ var topCipherSuites []uint16
+ var defaultCipherSuitesTLS13 []uint16
+
+ hasGCMAsmAMD64 := cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ
+ hasGCMAsmARM64 := cpu.ARM64.HasAES && cpu.ARM64.HasPMULL
+ // Keep in sync with crypto/aes/cipher_s390x.go.
+ hasGCMAsmS390X := cpu.S390X.HasAES && cpu.S390X.HasAESCBC && cpu.S390X.HasAESCTR && (cpu.S390X.HasGHASH || cpu.S390X.HasAESGCM)
+
+ hasGCMAsm := hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X
+
+ if hasGCMAsm {
+ // If AES-GCM hardware is provided then prioritise AES-GCM
+ // cipher suites.
+ topCipherSuites = []uint16{
+ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+ tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
+ tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
+ }
+ defaultCipherSuitesTLS13 = []uint16{
+ tls.TLS_AES_128_GCM_SHA256,
+ tls.TLS_CHACHA20_POLY1305_SHA256,
+ tls.TLS_AES_256_GCM_SHA384,
+ }
+ } else {
+ // Without AES-GCM hardware, we put the ChaCha20-Poly1305
+ // cipher suites first.
+ topCipherSuites = []uint16{
+ tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
+ tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
+ tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+ tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+ }
+ defaultCipherSuitesTLS13 = []uint16{
+ tls.TLS_CHACHA20_POLY1305_SHA256,
+ tls.TLS_AES_128_GCM_SHA256,
+ tls.TLS_AES_256_GCM_SHA384,
+ }
+ }
+
+ DefaultCipherSuites := make([]uint16, 0, 22)
+ DefaultCipherSuites = append(DefaultCipherSuites, topCipherSuites...)
+ DefaultCipherSuites = append(DefaultCipherSuites, defaultCipherSuitesTLS13...)
+
+ server := &http.Server{
+ Addr: s.tlsAddr(s.cfg.Address, true),
+ Handler: s,
+ TLSConfig: &tls.Config{
+ CurvePreferences: []tls.CurveID{
+ tls.CurveP256,
+ tls.CurveP384,
+ tls.CurveP521,
+ tls.X25519,
+ },
+ CipherSuites: DefaultCipherSuites,
+ MinVersion: tls.VersionTLS12,
+ PreferServerCipherSuites: true,
+ },
+ }
+ s.throw(EventInitSSL, server)
+
+ return server
+}
+
+// init http/2 server
+func (s *Service) initHTTP2() error {
+ return http2.ConfigureServer(s.https, &http2.Server{
+ MaxConcurrentStreams: s.cfg.HTTP2.MaxConcurrentStreams,
+ })
+}
+
+// serveFCGI starts FastCGI server.
+func (s *Service) serveFCGI() 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
+}
+
+// throw handles service, server and pool events.
+func (s *Service) throw(event int, ctx interface{}) {
+ for _, l := range s.lsns {
+ l(event, ctx)
+ }
+
+ if event == roadrunner.EventServerFailure {
+ // underlying rr server is dead
+ 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
+}