Files
teleport/lib/web/server.go
T
rosstimothy b17f687f5f Track active ssh sessions established via the web (#20231)
* 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
2023-01-27 14:38:39 +00:00

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")
}
}
}