diff options
Diffstat (limited to 'plugins/http')
-rw-r--r-- | plugins/http/attributes/attributes.go | 89 | ||||
-rw-r--r-- | plugins/http/config/fcgi.go | 7 | ||||
-rw-r--r-- | plugins/http/config/http.go | 187 | ||||
-rw-r--r-- | plugins/http/config/http2.go | 28 | ||||
-rw-r--r-- | plugins/http/config/ip.go | 26 | ||||
-rw-r--r-- | plugins/http/config/ssl.go | 84 | ||||
-rw-r--r-- | plugins/http/config/ssl_config_test.go | 116 | ||||
-rw-r--r-- | plugins/http/config/uploads_config.go | 46 | ||||
-rw-r--r-- | plugins/http/metrics.go | 92 | ||||
-rw-r--r-- | plugins/http/plugin.go | 412 | ||||
-rw-r--r-- | plugins/http/serve.go | 254 |
11 files changed, 0 insertions, 1341 deletions
diff --git a/plugins/http/attributes/attributes.go b/plugins/http/attributes/attributes.go deleted file mode 100644 index 243b6c78..00000000 --- a/plugins/http/attributes/attributes.go +++ /dev/null @@ -1,89 +0,0 @@ -package attributes - -import ( - "context" - "errors" - "net/http" -) - -// contextKey is a value for use with context.WithValue. It's used as -// a pointer fits an interface{} without allocation. -type contextKey struct { - name string -} - -func (k *contextKey) String() string { return k.name } - -var ( - // PsrContextKey is a context key. It can be used in the http attributes - PsrContextKey = &contextKey{"psr_attributes"} -) - -type attrs map[string]interface{} - -func (v attrs) get(key string) interface{} { - if v == nil { - return "" - } - - return v[key] -} - -func (v attrs) set(key string, value interface{}) { - v[key] = value -} - -func (v attrs) del(key string) { - delete(v, key) -} - -// Init returns request with new context and attribute bag. -func Init(r *http.Request) *http.Request { - // do not overwrite psr attributes - if val := r.Context().Value(PsrContextKey); val == nil { - return r.WithContext(context.WithValue(r.Context(), PsrContextKey, attrs{})) - } - return r -} - -// All returns all context attributes. -func All(r *http.Request) map[string]interface{} { - v := r.Context().Value(PsrContextKey) - if v == nil { - return attrs{} - } - - return v.(attrs) -} - -// Get gets the value from request context. It replaces any existing -// values. -func Get(r *http.Request, key string) interface{} { - v := r.Context().Value(PsrContextKey) - if v == nil { - return nil - } - - return v.(attrs).get(key) -} - -// Set sets the key to value. It replaces any existing -// values. Context specific. -func Set(r *http.Request, key string, value interface{}) error { - v := r.Context().Value(PsrContextKey) - if v == nil { - return errors.New("unable to find `psr:attributes` context key") - } - - v.(attrs).set(key, value) - return nil -} - -// Delete deletes values associated with attribute key. -func (v attrs) Delete(key string) { - if v == nil { - return - } - - v.del(key) -} diff --git a/plugins/http/config/fcgi.go b/plugins/http/config/fcgi.go deleted file mode 100644 index 3d4acbe1..00000000 --- a/plugins/http/config/fcgi.go +++ /dev/null @@ -1,7 +0,0 @@ -package config - -// FCGI for FastCGI server. -type FCGI struct { - // Address and port to handle as http server. - Address string -} diff --git a/plugins/http/config/http.go b/plugins/http/config/http.go deleted file mode 100644 index f06adc49..00000000 --- a/plugins/http/config/http.go +++ /dev/null @@ -1,187 +0,0 @@ -package config - -import ( - "net" - "runtime" - "strings" - "time" - - "github.com/spiral/errors" - "github.com/spiral/roadrunner/v2/pkg/pool" -) - -// HTTP configures RoadRunner HTTP server. -type HTTP struct { - // Host and port to handle as http server. - Address string - - // InternalErrorCode used to override default 500 (InternalServerError) http code - InternalErrorCode uint64 `mapstructure:"internal_error_code"` - - // SSLConfig defines https server options. - SSLConfig *SSL `mapstructure:"ssl"` - - // FCGIConfig configuration. You can use FastCGI without HTTP server. - FCGIConfig *FCGI `mapstructure:"fcgi"` - - // HTTP2Config configuration - HTTP2Config *HTTP2 `mapstructure:"http2"` - - // MaxRequestSize specified max size for payload body in megabytes, set 0 to unlimited. - MaxRequestSize uint64 `mapstructure:"max_request_size"` - - // TrustedSubnets declare IP subnets which are allowed to set ip using X-Real-Ip and X-Forwarded-For - TrustedSubnets []string `mapstructure:"trusted_subnets"` - - // Uploads configures uploads configuration. - Uploads *Uploads `mapstructure:"uploads"` - - // Pool configures worker pool. - Pool *pool.Config `mapstructure:"pool"` - - // Env is environment variables passed to the http pool - Env map[string]string - - // List of the middleware names (order will be preserved) - Middleware []string - - // slice of net.IPNet - Cidrs Cidrs -} - -// EnableHTTP is true when http server must run. -func (c *HTTP) EnableHTTP() bool { - return c.Address != "" -} - -// EnableTLS returns true if pool must listen TLS connections. -func (c *HTTP) EnableTLS() bool { - return c.SSLConfig.Key != "" || c.SSLConfig.Cert != "" -} - -// EnableH2C when HTTP/2 extension must be enabled on TCP. -func (c *HTTP) EnableH2C() bool { - return c.HTTP2Config.H2C -} - -// EnableFCGI is true when FastCGI server must be enabled. -func (c *HTTP) EnableFCGI() bool { - return c.FCGIConfig.Address != "" -} - -// InitDefaults must populate HTTP values using given HTTP source. Must return error if HTTP is not valid. -func (c *HTTP) InitDefaults() error { - if c.Pool == nil { - // default pool - c.Pool = &pool.Config{ - Debug: false, - NumWorkers: uint64(runtime.NumCPU()), - MaxJobs: 0, - AllocateTimeout: time.Second * 60, - DestroyTimeout: time.Second * 60, - Supervisor: nil, - } - } - - if c.InternalErrorCode == 0 { - c.InternalErrorCode = 500 - } - - if c.HTTP2Config == nil { - c.HTTP2Config = &HTTP2{} - } - - if c.FCGIConfig == nil { - c.FCGIConfig = &FCGI{} - } - - if c.Uploads == nil { - c.Uploads = &Uploads{} - } - - if c.SSLConfig == nil { - c.SSLConfig = &SSL{} - } - - if c.SSLConfig.Address == "" { - c.SSLConfig.Address = "127.0.0.1:443" - } - - err := c.HTTP2Config.InitDefaults() - if err != nil { - return err - } - err = c.Uploads.InitDefaults() - if err != nil { - return err - } - - if c.TrustedSubnets == nil { - // @see https://en.wikipedia.org/wiki/Reserved_IP_addresses - c.TrustedSubnets = []string{ - "10.0.0.0/8", - "127.0.0.0/8", - "172.16.0.0/12", - "192.168.0.0/16", - "::1/128", - "fc00::/7", - "fe80::/10", - } - } - - cidrs, err := ParseCIDRs(c.TrustedSubnets) - if err != nil { - return err - } - c.Cidrs = cidrs - - return c.Valid() -} - -// ParseCIDRs parse IPNet addresses and return slice of its -func ParseCIDRs(subnets []string) (Cidrs, error) { - c := make(Cidrs, 0, len(subnets)) - for _, cidr := range subnets { - _, cr, err := net.ParseCIDR(cidr) - if err != nil { - return nil, err - } - - c = append(c, cr) - } - - return c, nil -} - -// Valid validates the configuration. -func (c *HTTP) Valid() error { - const op = errors.Op("validation") - if c.Uploads == nil { - return errors.E(op, errors.Str("malformed uploads config")) - } - - if c.HTTP2Config == nil { - return errors.E(op, errors.Str("malformed http2 config")) - } - - if c.Pool == nil { - return errors.E(op, "malformed pool config") - } - - if !c.EnableHTTP() && !c.EnableTLS() && !c.EnableFCGI() { - return errors.E(op, errors.Str("unable to run http service, no method has been specified (http, https, http/2 or FastCGI)")) - } - - if c.Address != "" && !strings.Contains(c.Address, ":") { - return errors.E(op, errors.Str("malformed http server address")) - } - - if c.EnableTLS() { - err := c.SSLConfig.Valid() - if err != nil { - return errors.E(op, err) - } - } - - return nil -} diff --git a/plugins/http/config/http2.go b/plugins/http/config/http2.go deleted file mode 100644 index b1e109e9..00000000 --- a/plugins/http/config/http2.go +++ /dev/null @@ -1,28 +0,0 @@ -package config - -// HTTP2 HTTP/2 server customizations. -type HTTP2 struct { - // h2cHandler is a Handler which implements h2c by hijacking the HTTP/1 traffic - // that should be h2c traffic. There are two ways to begin a h2c connection - // (RFC 7540 Section 3.2 and 3.4): (1) Starting with Prior Knowledge - this - // works by starting an h2c connection with a string of bytes that is valid - // HTTP/1, but unlikely to occur in practice and (2) Upgrading from HTTP/1 to - // h2c - this works by using the HTTP/1 Upgrade header to request an upgrade to - // h2c. When either of those situations occur we hijack the HTTP/1 connection, - // convert it to a HTTP/2 connection and pass the net.Conn to http2.ServeConn. - - // H2C enables HTTP/2 over TCP - H2C bool - - // MaxConcurrentStreams defaults to 128. - MaxConcurrentStreams uint32 `mapstructure:"max_concurrent_streams"` -} - -// InitDefaults sets default values for HTTP/2 configuration. -func (cfg *HTTP2) InitDefaults() error { - if cfg.MaxConcurrentStreams == 0 { - cfg.MaxConcurrentStreams = 128 - } - - return nil -} diff --git a/plugins/http/config/ip.go b/plugins/http/config/ip.go deleted file mode 100644 index c4981f74..00000000 --- a/plugins/http/config/ip.go +++ /dev/null @@ -1,26 +0,0 @@ -package config - -import "net" - -// Cidrs is a slice of IPNet addresses -type Cidrs []*net.IPNet - -// IsTrusted checks if the ip address exists in the provided in the config addresses -func (c *Cidrs) IsTrusted(ip string) bool { - if len(*c) == 0 { - return false - } - - i := net.ParseIP(ip) - if i == nil { - return false - } - - for _, cird := range *c { - if cird.Contains(i) { - return true - } - } - - return false -} diff --git a/plugins/http/config/ssl.go b/plugins/http/config/ssl.go deleted file mode 100644 index 0e3c0caf..00000000 --- a/plugins/http/config/ssl.go +++ /dev/null @@ -1,84 +0,0 @@ -package config - -import ( - "os" - "strconv" - "strings" - - "github.com/spiral/errors" -) - -// SSL defines https server configuration. -type SSL struct { - // Address to listen as HTTPS server, defaults to 0.0.0.0:443. - Address string - - // 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 - - // Root CA file - RootCA string `mapstructure:"root_ca"` - - // internal - host string - Port int -} - -func (s *SSL) Valid() error { - const op = errors.Op("ssl_valid") - - parts := strings.Split(s.Address, ":") - switch len(parts) { - // :443 form - // 127.0.0.1:443 form - // use 0.0.0.0 as host and 443 as port - case 2: - if parts[0] == "" { - s.host = "127.0.0.1" - } else { - s.host = parts[0] - } - - port, err := strconv.Atoi(parts[1]) - if err != nil { - return errors.E(op, err) - } - s.Port = port - default: - return errors.E(op, errors.Errorf("unknown format, accepted format is [:<port> or <host>:<port>], provided: %s", s.Address)) - } - - if _, err := os.Stat(s.Key); err != nil { - if os.IsNotExist(err) { - return errors.E(op, errors.Errorf("key file '%s' does not exists", s.Key)) - } - - return err - } - - if _, err := os.Stat(s.Cert); err != nil { - if os.IsNotExist(err) { - return errors.E(op, errors.Errorf("cert file '%s' does not exists", s.Cert)) - } - - return err - } - - // RootCA is optional, but if provided - check it - if s.RootCA != "" { - if _, err := os.Stat(s.RootCA); err != nil { - if os.IsNotExist(err) { - return errors.E(op, errors.Errorf("root ca path provided, but path '%s' does not exists", s.RootCA)) - } - return err - } - } - - return nil -} diff --git a/plugins/http/config/ssl_config_test.go b/plugins/http/config/ssl_config_test.go deleted file mode 100644 index 8f6cf40e..00000000 --- a/plugins/http/config/ssl_config_test.go +++ /dev/null @@ -1,116 +0,0 @@ -package config - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestSSL_Valid1(t *testing.T) { - conf := &SSL{ - Address: "", - Redirect: false, - Key: "", - Cert: "", - RootCA: "", - host: "", - Port: 0, - } - - err := conf.Valid() - assert.Error(t, err) -} - -func TestSSL_Valid2(t *testing.T) { - conf := &SSL{ - Address: ":hello", - Redirect: false, - Key: "", - Cert: "", - RootCA: "", - host: "", - Port: 0, - } - - err := conf.Valid() - assert.Error(t, err) -} - -func TestSSL_Valid3(t *testing.T) { - conf := &SSL{ - Address: ":555", - Redirect: false, - Key: "", - Cert: "", - RootCA: "", - host: "", - Port: 0, - } - - err := conf.Valid() - assert.Error(t, err) -} - -func TestSSL_Valid4(t *testing.T) { - conf := &SSL{ - Address: ":555", - Redirect: false, - Key: "../../../tests/plugins/http/fixtures/server.key", - Cert: "../../../tests/plugins/http/fixtures/server.crt", - RootCA: "", - host: "", - // private - Port: 0, - } - - err := conf.Valid() - assert.NoError(t, err) -} - -func TestSSL_Valid5(t *testing.T) { - conf := &SSL{ - Address: "a:b:c", - Redirect: false, - Key: "../../../tests/plugins/http/fixtures/server.key", - Cert: "../../../tests/plugins/http/fixtures/server.crt", - RootCA: "", - host: "", - // private - Port: 0, - } - - err := conf.Valid() - assert.Error(t, err) -} - -func TestSSL_Valid6(t *testing.T) { - conf := &SSL{ - Address: ":", - Redirect: false, - Key: "../../../tests/plugins/http/fixtures/server.key", - Cert: "../../../tests/plugins/http/fixtures/server.crt", - RootCA: "", - host: "", - // private - Port: 0, - } - - err := conf.Valid() - assert.Error(t, err) -} - -func TestSSL_Valid7(t *testing.T) { - conf := &SSL{ - Address: "127.0.0.1:555:1", - Redirect: false, - Key: "../../../tests/plugins/http/fixtures/server.key", - Cert: "../../../tests/plugins/http/fixtures/server.crt", - RootCA: "", - host: "", - // private - Port: 0, - } - - err := conf.Valid() - assert.Error(t, err) -} diff --git a/plugins/http/config/uploads_config.go b/plugins/http/config/uploads_config.go deleted file mode 100644 index 5edb0ab7..00000000 --- a/plugins/http/config/uploads_config.go +++ /dev/null @@ -1,46 +0,0 @@ -package config - -import ( - "os" - "path" - "strings" -) - -// Uploads describes file location and controls access to them. -type Uploads struct { - // Dir contains name of directory to control access to. - Dir string - - // Forbid specifies list of file extensions which are forbidden for access. - // Example: .php, .exe, .bat, .htaccess and etc. - Forbid []string -} - -// InitDefaults sets missing values to their default values. -func (cfg *Uploads) InitDefaults() error { - cfg.Forbid = []string{".php", ".exe", ".bat"} - cfg.Dir = os.TempDir() - return nil -} - -// TmpDir returns temporary directory. -func (cfg *Uploads) TmpDir() string { - if cfg.Dir != "" { - return cfg.Dir - } - - return os.TempDir() -} - -// Forbids must return true if file extension is not allowed for the upload. -func (cfg *Uploads) Forbids(filename string) bool { - ext := strings.ToLower(path.Ext(filename)) - - for _, v := range cfg.Forbid { - if ext == v { - return true - } - } - - return false -} diff --git a/plugins/http/metrics.go b/plugins/http/metrics.go deleted file mode 100644 index d7a9110b..00000000 --- a/plugins/http/metrics.go +++ /dev/null @@ -1,92 +0,0 @@ -package http - -import ( - "strconv" - - "github.com/prometheus/client_golang/prometheus" - handler "github.com/spiral/roadrunner/v2/pkg/worker_handler" -) - -func (p *Plugin) MetricsCollector() []prometheus.Collector { - // p - implements Exporter interface (workers) - // other - request duration and count - return []prometheus.Collector{p, p.requestsExporter.requestDuration, p.requestsExporter.requestCounter} -} - -func (p *Plugin) metricsCallback(event interface{}) { - switch e := event.(type) { - case handler.ResponseEvent: - p.requestsExporter.requestCounter.With(prometheus.Labels{ - "status": strconv.Itoa(e.Response.Status), - }).Inc() - - p.requestsExporter.requestDuration.With(prometheus.Labels{ - "status": strconv.Itoa(e.Response.Status), - }).Observe(e.Elapsed().Seconds()) - case handler.ErrorEvent: - p.requestsExporter.requestCounter.With(prometheus.Labels{ - "status": "500", - }).Inc() - - p.requestsExporter.requestDuration.With(prometheus.Labels{ - "status": "500", - }).Observe(e.Elapsed().Seconds()) - } -} - -type workersExporter struct { - wm *prometheus.Desc - workersMemory uint64 -} - -func newWorkersExporter() *workersExporter { - return &workersExporter{ - wm: prometheus.NewDesc("rr_http_workers_memory_bytes", "Memory usage by HTTP workers.", nil, nil), - workersMemory: 0, - } -} - -func (p *Plugin) Describe(d chan<- *prometheus.Desc) { - // send description - d <- p.workersExporter.wm -} - -func (p *Plugin) Collect(ch chan<- prometheus.Metric) { - // get the copy of the processes - workers := p.Workers() - - // cumulative RSS memory in bytes - var cum uint64 - - // collect the memory - for i := 0; i < len(workers); i++ { - cum += workers[i].MemoryUsage - } - - // send the values to the prometheus - ch <- prometheus.MustNewConstMetric(p.workersExporter.wm, prometheus.GaugeValue, float64(cum)) -} - -type requestsExporter struct { - requestCounter *prometheus.CounterVec - requestDuration *prometheus.HistogramVec -} - -func newRequestsExporter() *requestsExporter { - return &requestsExporter{ - requestCounter: prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "rr_http_request_total", - Help: "Total number of handled http requests after server restart.", - }, - []string{"status"}, - ), - requestDuration: prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Name: "rr_http_request_duration_seconds", - Help: "HTTP request duration.", - }, - []string{"status"}, - ), - } -} diff --git a/plugins/http/plugin.go b/plugins/http/plugin.go deleted file mode 100644 index dc887f87..00000000 --- a/plugins/http/plugin.go +++ /dev/null @@ -1,412 +0,0 @@ -package http - -import ( - "context" - "fmt" - "log" - "net/http" - "sync" - - endure "github.com/spiral/endure/pkg/container" - "github.com/spiral/errors" - "github.com/spiral/roadrunner/v2/pkg/pool" - "github.com/spiral/roadrunner/v2/pkg/state/process" - "github.com/spiral/roadrunner/v2/pkg/worker" - handler "github.com/spiral/roadrunner/v2/pkg/worker_handler" - "github.com/spiral/roadrunner/v2/plugins/config" - "github.com/spiral/roadrunner/v2/plugins/http/attributes" - httpConfig "github.com/spiral/roadrunner/v2/plugins/http/config" - "github.com/spiral/roadrunner/v2/plugins/logger" - "github.com/spiral/roadrunner/v2/plugins/server" - "github.com/spiral/roadrunner/v2/plugins/status" - "golang.org/x/net/http2" - "golang.org/x/net/http2/h2c" -) - -const ( - // PluginName declares plugin name. - PluginName = "http" - - // RrMode RR_HTTP env variable key (internal) if the HTTP presents - RrMode = "RR_MODE" - - HTTPSScheme = "https" -) - -// Middleware interface -type Middleware interface { - Middleware(f http.Handler) http.Handler -} - -type middleware map[string]Middleware - -// Plugin manages pool, http servers. The main http plugin structure -type Plugin struct { - sync.RWMutex - - // plugins - server server.Server - log logger.Logger - // stdlog passed to the http/https/fcgi servers to log their internal messages - stdLog *log.Logger - - // http configuration - cfg *httpConfig.HTTP `mapstructure:"http"` - - // middlewares to chain - mdwr middleware - - // Pool which attached to all servers - pool pool.Pool - - // servers RR handler - handler *handler.Handler - - // metrics - workersExporter *workersExporter - requestsExporter *requestsExporter - - // servers - http *http.Server - https *http.Server - fcgi *http.Server -} - -// 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 (p *Plugin) Init(cfg config.Configurer, rrLogger logger.Logger, server server.Server) error { - const op = errors.Op("http_plugin_init") - if !cfg.Has(PluginName) { - return errors.E(op, errors.Disabled) - } - - err := cfg.UnmarshalKey(PluginName, &p.cfg) - if err != nil { - return errors.E(op, err) - } - - err = p.cfg.InitDefaults() - if err != nil { - return errors.E(op, err) - } - - // rr logger (via plugin) - p.log = rrLogger - // use time and date in UTC format - p.stdLog = log.New(logger.NewStdAdapter(p.log), "http_plugin: ", log.Ldate|log.Ltime|log.LUTC) - - p.mdwr = make(map[string]Middleware) - - if !p.cfg.EnableHTTP() && !p.cfg.EnableTLS() && !p.cfg.EnableFCGI() { - return errors.E(op, errors.Disabled) - } - - // init if nil - if p.cfg.Env == nil { - p.cfg.Env = make(map[string]string) - } - - // initialize workersExporter - p.workersExporter = newWorkersExporter() - // initialize requests exporter - p.requestsExporter = newRequestsExporter() - - p.cfg.Env[RrMode] = "http" - p.server = server - - return nil -} - -func (p *Plugin) logCallback(event interface{}) { - if ev, ok := event.(handler.ResponseEvent); ok { - p.log.Debug(fmt.Sprintf("%d %s %s", ev.Response.Status, ev.Request.Method, ev.Request.URI), - "remote", ev.Request.RemoteAddr, - "elapsed", ev.Elapsed().String(), - ) - } -} - -// Serve serves the svc. -func (p *Plugin) Serve() chan error { - errCh := make(chan error, 2) - // run whole process in the goroutine - go func() { - // protect http initialization - p.Lock() - p.serve(errCh) - p.Unlock() - }() - - return errCh -} - -func (p *Plugin) serve(errCh chan error) { - var err error - const op = errors.Op("http_plugin_serve") - p.pool, err = p.server.NewWorkerPool(context.Background(), &pool.Config{ - Debug: p.cfg.Pool.Debug, - NumWorkers: p.cfg.Pool.NumWorkers, - MaxJobs: p.cfg.Pool.MaxJobs, - AllocateTimeout: p.cfg.Pool.AllocateTimeout, - DestroyTimeout: p.cfg.Pool.DestroyTimeout, - Supervisor: p.cfg.Pool.Supervisor, - }, p.cfg.Env, p.logCallback) - if err != nil { - errCh <- errors.E(op, err) - return - } - - p.handler, err = handler.NewHandler( - p.cfg.MaxRequestSize, - p.cfg.InternalErrorCode, - *p.cfg.Uploads, - p.cfg.Cidrs, - p.pool, - ) - if err != nil { - errCh <- errors.E(op, err) - return - } - - p.handler.AddListener(p.logCallback, p.metricsCallback) - - if p.cfg.EnableHTTP() { - if p.cfg.EnableH2C() { - p.http = &http.Server{ - Handler: h2c.NewHandler(p, &http2.Server{}), - ErrorLog: p.stdLog, - } - } else { - p.http = &http.Server{ - Handler: p, - ErrorLog: p.stdLog, - } - } - } - - if p.cfg.EnableTLS() { - p.https = p.initSSL() - if p.cfg.SSLConfig.RootCA != "" { - err = p.appendRootCa() - if err != nil { - errCh <- errors.E(op, err) - return - } - } - - // if HTTP2Config not nil - if p.cfg.HTTP2Config != nil { - if err := p.initHTTP2(); err != nil { - errCh <- errors.E(op, err) - return - } - } - } - - if p.cfg.EnableFCGI() { - p.fcgi = &http.Server{Handler: p, ErrorLog: p.stdLog} - } - - // start http, https and fcgi servers if requested in the config - go func() { - p.serveHTTP(errCh) - }() - - go func() { - p.serveHTTPS(errCh) - }() - - go func() { - p.serveFCGI(errCh) - }() -} - -// Stop stops the http. -func (p *Plugin) Stop() error { - p.Lock() - defer p.Unlock() - - if p.fcgi != nil { - err := p.fcgi.Shutdown(context.Background()) - if err != nil && err != http.ErrServerClosed { - p.log.Error("fcgi shutdown", "error", err) - } - } - - if p.https != nil { - err := p.https.Shutdown(context.Background()) - if err != nil && err != http.ErrServerClosed { - p.log.Error("https shutdown", "error", err) - } - } - - if p.http != nil { - err := p.http.Shutdown(context.Background()) - if err != nil && err != http.ErrServerClosed { - p.log.Error("http shutdown", "error", err) - } - } - - // check for safety - if p.pool != nil { - p.pool.Destroy(context.Background()) - } - - return nil -} - -// ServeHTTP handles connection using set of middleware and pool PSR-7 server. -func (p *Plugin) ServeHTTP(w http.ResponseWriter, r *http.Request) { - defer func() { - err := r.Body.Close() - if err != nil { - p.log.Error("body close", "error", err) - } - }() - if headerContainsUpgrade(r) { - http.Error(w, "server does not support upgrade header", http.StatusInternalServerError) - return - } - - if p.https != nil && r.TLS == nil && p.cfg.SSLConfig.Redirect { - p.redirect(w, r) - return - } - - if p.https != nil && r.TLS != nil { - w.Header().Add("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload") - } - - r = attributes.Init(r) - // protect the case, when user sendEvent Reset and we are replacing handler with pool - p.RLock() - p.handler.ServeHTTP(w, r) - p.RUnlock() -} - -// Workers returns slice with the process states for the workers -func (p *Plugin) Workers() []*process.State { - p.RLock() - defer p.RUnlock() - - workers := p.workers() - - ps := make([]*process.State, 0, len(workers)) - for i := 0; i < len(workers); i++ { - state, err := process.WorkerProcessState(workers[i]) - if err != nil { - return nil - } - ps = append(ps, state) - } - - return ps -} - -// internal -func (p *Plugin) workers() []worker.BaseProcess { - return p.pool.Workers() -} - -// Name returns endure.Named interface implementation -func (p *Plugin) Name() string { - return PluginName -} - -// Reset destroys the old pool and replaces it with new one, waiting for old pool to die -func (p *Plugin) Reset() error { - p.Lock() - defer p.Unlock() - const op = errors.Op("http_plugin_reset") - p.log.Info("HTTP plugin got restart request. Restarting...") - p.pool.Destroy(context.Background()) - p.pool = nil - - var err error - p.pool, err = p.server.NewWorkerPool(context.Background(), &pool.Config{ - Debug: p.cfg.Pool.Debug, - NumWorkers: p.cfg.Pool.NumWorkers, - MaxJobs: p.cfg.Pool.MaxJobs, - AllocateTimeout: p.cfg.Pool.AllocateTimeout, - DestroyTimeout: p.cfg.Pool.DestroyTimeout, - Supervisor: p.cfg.Pool.Supervisor, - }, p.cfg.Env, p.logCallback) - if err != nil { - return errors.E(op, err) - } - - p.log.Info("HTTP workers Pool successfully restarted") - - p.handler, err = handler.NewHandler( - p.cfg.MaxRequestSize, - p.cfg.InternalErrorCode, - *p.cfg.Uploads, - p.cfg.Cidrs, - p.pool, - ) - - if err != nil { - return errors.E(op, err) - } - - p.log.Info("HTTP handler listeners successfully re-added") - p.handler.AddListener(p.logCallback, p.metricsCallback) - - p.log.Info("HTTP plugin successfully restarted") - return nil -} - -// Collects collecting http middlewares -func (p *Plugin) Collects() []interface{} { - return []interface{}{ - p.AddMiddleware, - } -} - -// AddMiddleware is base requirement for the middleware (name and Middleware) -func (p *Plugin) AddMiddleware(name endure.Named, m Middleware) { - p.mdwr[name.Name()] = m -} - -// Status return status of the particular plugin -func (p *Plugin) Status() status.Status { - p.RLock() - defer p.RUnlock() - - workers := p.workers() - for i := 0; i < len(workers); i++ { - if workers[i].State().IsActive() { - return status.Status{ - Code: http.StatusOK, - } - } - } - // if there are no workers, threat this as error - return status.Status{ - Code: http.StatusServiceUnavailable, - } -} - -// Ready return readiness status of the particular plugin -func (p *Plugin) Ready() status.Status { - p.RLock() - defer p.RUnlock() - - workers := p.workers() - for i := 0; i < len(workers); i++ { - // If state of the worker is ready (at least 1) - // we assume, that plugin's worker pool is ready - if workers[i].State().Value() == worker.StateReady { - return status.Status{ - Code: http.StatusOK, - } - } - } - // if there are no workers, threat this as no content error - return status.Status{ - Code: http.StatusServiceUnavailable, - } -} - -// Available interface implementation -func (p *Plugin) Available() {} diff --git a/plugins/http/serve.go b/plugins/http/serve.go deleted file mode 100644 index 6d3f2228..00000000 --- a/plugins/http/serve.go +++ /dev/null @@ -1,254 +0,0 @@ -package http - -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "net/http" - "net/http/fcgi" - "net/url" - "os" - "strings" - - "github.com/spiral/errors" - "github.com/spiral/roadrunner/v2/plugins/logger" - "github.com/spiral/roadrunner/v2/utils" - "golang.org/x/net/http2" - "golang.org/x/sys/cpu" -) - -func (p *Plugin) serveHTTP(errCh chan error) { - if p.http == nil { - return - } - const op = errors.Op("serveHTTP") - - if len(p.mdwr) > 0 { - applyMiddlewares(p.http, p.mdwr, p.cfg.Middleware, p.log) - } - l, err := utils.CreateListener(p.cfg.Address) - if err != nil { - errCh <- errors.E(op, err) - return - } - - err = p.http.Serve(l) - if err != nil && err != http.ErrServerClosed { - errCh <- errors.E(op, err) - return - } -} - -func (p *Plugin) serveHTTPS(errCh chan error) { - if p.https == nil { - return - } - const op = errors.Op("serveHTTPS") - if len(p.mdwr) > 0 { - applyMiddlewares(p.https, p.mdwr, p.cfg.Middleware, p.log) - } - l, err := utils.CreateListener(p.cfg.SSLConfig.Address) - if err != nil { - errCh <- errors.E(op, err) - return - } - - err = p.https.ServeTLS( - l, - p.cfg.SSLConfig.Cert, - p.cfg.SSLConfig.Key, - ) - - if err != nil && err != http.ErrServerClosed { - errCh <- errors.E(op, err) - return - } -} - -// serveFCGI starts FastCGI server. -func (p *Plugin) serveFCGI(errCh chan error) { - if p.fcgi == nil { - return - } - const op = errors.Op("serveFCGI") - - if len(p.mdwr) > 0 { - applyMiddlewares(p.fcgi, p.mdwr, p.cfg.Middleware, p.log) - } - - l, err := utils.CreateListener(p.cfg.FCGIConfig.Address) - if err != nil { - errCh <- errors.E(op, err) - return - } - - err = fcgi.Serve(l, p.fcgi.Handler) - if err != nil && err != http.ErrServerClosed { - errCh <- errors.E(op, err) - return - } -} - -func (p *Plugin) redirect(w http.ResponseWriter, r *http.Request) { - target := &url.URL{ - Scheme: HTTPSScheme, - // host or host:port - Host: p.tlsAddr(r.Host, false), - Path: r.URL.Path, - RawQuery: r.URL.RawQuery, - } - - http.Redirect(w, r, target.String(), http.StatusPermanentRedirect) -} - -// https://golang.org/pkg/net/http/#Hijacker -//go:inline -func headerContainsUpgrade(r *http.Request) bool { - if _, ok := r.Header["Upgrade"]; ok { - return true - } - return false -} - -// append RootCA to the https server TLS config -func (p *Plugin) appendRootCa() error { - const op = errors.Op("http_plugin_append_root_ca") - rootCAs, err := x509.SystemCertPool() - if err != nil { - return nil - } - if rootCAs == nil { - rootCAs = x509.NewCertPool() - } - - CA, err := os.ReadFile(p.cfg.SSLConfig.RootCA) - if err != nil { - return err - } - - // should append our CA cert - ok := rootCAs.AppendCertsFromPEM(CA) - if !ok { - return errors.E(op, errors.Str("could not append Certs from PEM")) - } - // disable "G402 (CWE-295): TLS MinVersion too low. (Confidence: HIGH, Severity: HIGH)" - // #nosec G402 - cfg := &tls.Config{ - InsecureSkipVerify: false, - RootCAs: rootCAs, - } - p.http.TLSConfig = cfg - - return nil -} - -// Init https server -func (p *Plugin) 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 priorities 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...) - - sslServer := &http.Server{ - Addr: p.tlsAddr(p.cfg.Address, true), - Handler: p, - ErrorLog: p.stdLog, - TLSConfig: &tls.Config{ - CurvePreferences: []tls.CurveID{ - tls.CurveP256, - tls.CurveP384, - tls.CurveP521, - tls.X25519, - }, - CipherSuites: DefaultCipherSuites, - MinVersion: tls.VersionTLS12, - PreferServerCipherSuites: true, - }, - } - - return sslServer -} - -// init http/2 server -func (p *Plugin) initHTTP2() error { - return http2.ConfigureServer(p.https, &http2.Server{ - MaxConcurrentStreams: p.cfg.HTTP2Config.MaxConcurrentStreams, - }) -} - -// tlsAddr replaces listen or host port with port configured by SSLConfig config. -func (p *Plugin) tlsAddr(host string, forcePort bool) string { - // remove current forcePort first - host = strings.Split(host, ":")[0] - - if forcePort || p.cfg.SSLConfig.Port != 443 { - host = fmt.Sprintf("%s:%v", host, p.cfg.SSLConfig.Port) - } - - return host -} - -// static plugin name -const static string = "static" - -func applyMiddlewares(server *http.Server, middlewares map[string]Middleware, order []string, log logger.Logger) { - for i := len(order) - 1; i >= 0; i-- { - // set static last in the row - if order[i] == static { - continue - } - if mdwr, ok := middlewares[order[i]]; ok { - server.Handler = mdwr.Middleware(server.Handler) - } else { - log.Warn("requested middleware does not exist", "requested", order[i]) - } - } - - // set static if exists - if mdwr, ok := middlewares[static]; ok { - server.Handler = mdwr.Middleware(server.Handler) - } -} |