moby/libnetwork/diagnostic/server.go
Cory Snider 1931a1bdc7 libnetwork/diagnostic: lock mutex in help handler
Acquire the mutex in the help handler to synchronize access to the
handlers map. While a trivial issue---a panic in the request handler if
the node joins a swarm at just the right time, which would only result
in an HTTP 500 response---it is also a trivial race condition to fix.

Signed-off-by: Cory Snider <csnider@mirantis.com>
2023-12-06 11:20:47 -05:00

235 lines
6.5 KiB
Go

package diagnostic
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/containerd/log"
"github.com/docker/docker/libnetwork/internal/caller"
"github.com/docker/docker/pkg/stack"
)
// Server when the debug is enabled exposes a
// This data structure is protected by the Agent mutex so does not require and additional mutex here
type Server struct {
mu sync.Mutex
enable int32
srv *http.Server
port int
mux *http.ServeMux
handlers map[string]http.Handler
}
// New creates a new diagnostic server
func New() *Server {
s := &Server{
mux: http.NewServeMux(),
handlers: make(map[string]http.Handler),
}
s.HandleFunc("/", notImplemented)
s.HandleFunc("/help", s.help)
s.HandleFunc("/ready", ready)
s.HandleFunc("/stackdump", stackTrace)
return s
}
// Handle registers the handler for the given pattern,
// replacing any existing handler.
func (s *Server) Handle(pattern string, handler http.Handler) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.handlers[pattern]; !ok {
// Register a handler on the mux which allows the underlying handler to
// be dynamically switched out. The http.ServeMux will panic if one
// attempts to register a handler for the same pattern twice.
s.mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
h := s.handlers[pattern]
s.mu.Unlock()
h.ServeHTTP(w, r)
})
}
s.handlers[pattern] = handler
}
// Handle registers the handler function for the given pattern,
// replacing any existing handler.
func (s *Server) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) {
s.Handle(pattern, http.HandlerFunc(handler))
}
// ServeHTTP this is the method called bu the ListenAndServe, and is needed to allow us to
// use our custom mux
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.mux.ServeHTTP(w, r)
}
// EnableDiagnostic opens a TCP socket to debug the passed network DB
func (s *Server) EnableDiagnostic(ip string, port int) {
s.mu.Lock()
defer s.mu.Unlock()
s.port = port
if s.enable == 1 {
log.G(context.TODO()).Info("The server is already up and running")
return
}
log.G(context.TODO()).Infof("Starting the diagnostic server listening on %d for commands", port)
srv := &http.Server{
Addr: net.JoinHostPort(ip, strconv.Itoa(port)),
Handler: s,
ReadHeaderTimeout: 5 * time.Minute, // "G112: Potential Slowloris Attack (gosec)"; not a real concern for our use, so setting a long timeout.
}
s.srv = srv
s.enable = 1
go func(n *Server) {
// Ignore ErrServerClosed that is returned on the Shutdown call
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.G(context.TODO()).Errorf("ListenAndServe error: %s", err)
atomic.SwapInt32(&n.enable, 0)
}
}(s)
}
// DisableDiagnostic stop the dubug and closes the tcp socket
func (s *Server) DisableDiagnostic() {
s.mu.Lock()
defer s.mu.Unlock()
s.srv.Shutdown(context.Background()) //nolint:errcheck
s.srv = nil
s.enable = 0
log.G(context.TODO()).Info("Disabling the diagnostic server")
}
// IsDiagnosticEnabled returns true when the debug is enabled
func (s *Server) IsDiagnosticEnabled() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.enable == 1
}
func notImplemented(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
_, jsonOutput := ParseHTTPFormOptions(r)
rsp := WrongCommand("not implemented", fmt.Sprintf("URL path: %s no method implemented check /help\n", r.URL.Path))
// audit logs
log.G(context.TODO()).WithFields(log.Fields{
"component": "diagnostic",
"remoteIP": r.RemoteAddr,
"method": caller.Name(0),
"url": r.URL.String(),
}).Info("command not implemented done")
_, _ = HTTPReply(w, rsp, jsonOutput)
}
func (s *Server) help(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
_, jsonOutput := ParseHTTPFormOptions(r)
// audit logs
log.G(context.TODO()).WithFields(log.Fields{
"component": "diagnostic",
"remoteIP": r.RemoteAddr,
"method": caller.Name(0),
"url": r.URL.String(),
}).Info("help done")
var result string
s.mu.Lock()
for path := range s.handlers {
result += fmt.Sprintf("%s\n", path)
}
s.mu.Unlock()
_, _ = HTTPReply(w, CommandSucceed(&StringCmd{Info: result}), jsonOutput)
}
func ready(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
_, jsonOutput := ParseHTTPFormOptions(r)
// audit logs
log.G(context.TODO()).WithFields(log.Fields{
"component": "diagnostic",
"remoteIP": r.RemoteAddr,
"method": caller.Name(0),
"url": r.URL.String(),
}).Info("ready done")
_, _ = HTTPReply(w, CommandSucceed(&StringCmd{Info: "OK"}), jsonOutput)
}
func stackTrace(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
_, jsonOutput := ParseHTTPFormOptions(r)
// audit logs
logger := log.G(context.TODO()).WithFields(log.Fields{"component": "diagnostic", "remoteIP": r.RemoteAddr, "method": caller.Name(0), "url": r.URL.String()})
logger.Info("stack trace")
path, err := stack.DumpToFile("/tmp/")
if err != nil {
logger.WithError(err).Error("failed to write goroutines dump")
_, _ = HTTPReply(w, FailCommand(err), jsonOutput)
} else {
logger.Info("stack trace done")
_, _ = HTTPReply(w, CommandSucceed(&StringCmd{Info: "goroutine stacks written to " + path}), jsonOutput)
}
}
// DebugHTTPForm helper to print the form url parameters
func DebugHTTPForm(r *http.Request) {
for k, v := range r.Form {
log.G(context.TODO()).Debugf("Form[%q] = %q\n", k, v)
}
}
// JSONOutput contains details on JSON output printing
type JSONOutput struct {
enable bool
prettyPrint bool
}
// ParseHTTPFormOptions easily parse the JSON printing options
func ParseHTTPFormOptions(r *http.Request) (bool, *JSONOutput) {
_, unsafe := r.Form["unsafe"]
v, enableJSON := r.Form["json"]
var pretty bool
if len(v) > 0 {
pretty = v[0] == "pretty"
}
return unsafe, &JSONOutput{enable: enableJSON, prettyPrint: pretty}
}
// HTTPReply helper function that takes care of sending the message out
func HTTPReply(w http.ResponseWriter, r *HTTPResult, j *JSONOutput) (int, error) {
var response []byte
if j.enable {
w.Header().Set("Content-Type", "application/json")
var err error
if j.prettyPrint {
response, err = json.MarshalIndent(r, "", " ")
if err != nil {
response, _ = json.MarshalIndent(FailCommand(err), "", " ")
}
} else {
response, err = json.Marshal(r)
if err != nil {
response, _ = json.Marshal(FailCommand(err))
}
}
} else {
response = []byte(r.String())
}
return fmt.Fprint(w, string(response))
}