mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-31 20:39:37 +08:00
* Track active ssh sessions established via the web Adds open session tracking in `web.Handler` and introduces a new `web.Server` to prevent closing the web `http.Server` during a graceful shutdown if there are still open sessions. From the `http.Server/Shutdown` docs: > Shutdown does not attempt to close nor wait for hijacked > connections such as WebSockets. The caller of Shutdown should > separately notify such long-lived connections of shutdown and wait > for them to close, if desired. Because the web sessions use WebSockets, `Shutdown` will not wait for them to be idle before terminating. This used to work due to the fact that web sessions dialed the Proxy SSH Port to establish the SSH session. Which meant that all accounting of active user connections by `sshutils/server.go` was accurate. But now that sessions are established directly via the `proxy.Router` the web sessions were unaccounted for. The `web.Server` now mimics the behavior of `sshutils/server.go` for Shutdown. Only once all the active web sessions have been terminated will the underlying http.Server be shutdown. Fixes #20227
141 lines
4.0 KiB
Go
141 lines
4.0 KiB
Go
// Copyright 2023 Gravitational, Inc
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package web
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
// ServerConfig provides dependencies required to create a [Server].
|
|
type ServerConfig struct {
|
|
// Server serves the web api
|
|
Server *http.Server
|
|
// Handler web handler
|
|
Handler *APIHandler
|
|
// Log to write log messages
|
|
Log logrus.FieldLogger
|
|
// ShutdownPollPeriod sets polling period for shutdown
|
|
ShutdownPollPeriod time.Duration
|
|
}
|
|
|
|
// CheckAndSetDefaults validates fields and populates empty fields with default values.
|
|
func (c *ServerConfig) CheckAndSetDefaults() error {
|
|
if c.Server == nil {
|
|
return trace.BadParameter("missing required parameter Server")
|
|
}
|
|
|
|
if c.Handler == nil {
|
|
return trace.BadParameter("missing required parameter Handler")
|
|
}
|
|
|
|
if c.ShutdownPollPeriod <= 0 {
|
|
c.ShutdownPollPeriod = defaults.ShutdownPollPeriod
|
|
}
|
|
|
|
if c.Log == nil {
|
|
c.Log = utils.NewLogger().WithField(trace.Component, teleport.ComponentProxy)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Server serves the web api.
|
|
type Server struct {
|
|
cfg ServerConfig
|
|
|
|
mu sync.Mutex
|
|
ln net.Listener
|
|
}
|
|
|
|
// NewServer constructs a [Server] from the provided [ServerConfig].
|
|
func NewServer(cfg ServerConfig) (*Server, error) {
|
|
if err := cfg.CheckAndSetDefaults(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &Server{
|
|
cfg: cfg,
|
|
}, nil
|
|
}
|
|
|
|
// Serve launches the configured [http.Server].
|
|
func (s *Server) Serve(l net.Listener) error {
|
|
s.mu.Lock()
|
|
s.ln = l
|
|
s.mu.Unlock()
|
|
return trace.Wrap(s.cfg.Server.Serve(l))
|
|
}
|
|
|
|
// Close immediately closes the [http.Server].
|
|
func (s *Server) Close() error {
|
|
return trace.NewAggregate(s.cfg.Handler.Close(), s.cfg.Server.Close())
|
|
}
|
|
|
|
// HandleConnection handles connections from plain TCP applications.
|
|
func (s *Server) HandleConnection(ctx context.Context, conn net.Conn) error {
|
|
return s.cfg.Handler.appHandler.HandleConnection(ctx, conn)
|
|
}
|
|
|
|
// Shutdown initiates graceful shutdown. The underlying [http.Server]
|
|
// is not shutdown until all active connections are terminated or
|
|
// the context times out. This is required because the [http.Server]
|
|
// does not attempt to close nor wait for hijacked connections such as
|
|
// WebSockets during Shutdown; which means that any open sessions in the
|
|
// web UI will not prevent the [http.Server] from shutting down.
|
|
func (s *Server) Shutdown(ctx context.Context) error {
|
|
s.mu.Lock()
|
|
err := s.ln.Close()
|
|
s.mu.Unlock()
|
|
|
|
activeConnections := s.cfg.Handler.handler.userConns.Load()
|
|
if activeConnections == 0 {
|
|
err := s.cfg.Server.Shutdown(ctx)
|
|
return trace.NewAggregate(err, s.cfg.Handler.Close())
|
|
}
|
|
|
|
s.cfg.Log.Infof("Shutdown: waiting for %v connections to finish.", activeConnections)
|
|
lastReport := time.Time{}
|
|
ticker := time.NewTicker(s.cfg.ShutdownPollPeriod)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
activeConnections = s.cfg.Handler.handler.userConns.Load()
|
|
if activeConnections == 0 {
|
|
err := s.cfg.Server.Shutdown(ctx)
|
|
return trace.NewAggregate(err, s.cfg.Handler.Close())
|
|
}
|
|
if time.Since(lastReport) > 10*s.cfg.ShutdownPollPeriod {
|
|
s.cfg.Log.Infof("Shutdown: waiting for %v connections to finish.", activeConnections)
|
|
lastReport = time.Now()
|
|
}
|
|
case <-ctx.Done():
|
|
s.cfg.Log.Infof("Context canceled wait, returning.")
|
|
return trace.ConnectionProblem(trace.NewAggregate(err, s.cfg.Handler.Close()), "context canceled")
|
|
}
|
|
}
|
|
}
|