moby/libnetwork/diagnostic/server.go
Sebastiaan van Stijn 55fd77f724
set ReadHeaderTimeout to address G112: Potential Slowloris Attack (gosec)
After discussing in the maintainers meeting, we concluded that Slowloris attacks
are not a real risk other than potentially having some additional goroutines
lingering around, so setting a long timeout to satisfy the linter, and to at
least have "some" timeout.

    libnetwork/diagnostic/server.go:96:10: G112: Potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server (gosec)
        srv := &http.Server{
            Addr:    net.JoinHostPort(ip, strconv.Itoa(port)),
            Handler: s,
        }
    api/server/server.go:60:10: G112: Potential Slowloris Attack because ReadHeaderTimeout is not configured in the http.Server (gosec)
                srv: &http.Server{
                    Addr: addr,
                },
    daemon/metrics_unix.go:34:13: G114: Use of net/http serve function that has no support for setting timeouts (gosec)
            if err := http.Serve(l, mux); err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
                      ^
    cmd/dockerd/metrics.go:27:13: G114: Use of net/http serve function that has no support for setting timeouts (gosec)
            if err := http.Serve(l, mux); err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
                      ^

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2022-09-22 12:13:28 +02:00

234 lines
6.5 KiB
Go

package diagnostic
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"strconv"
"sync"
"sync/atomic"
"time"
"github.com/docker/docker/libnetwork/internal/caller"
"github.com/docker/docker/pkg/stack"
"github.com/sirupsen/logrus"
)
// HTTPHandlerFunc TODO
type HTTPHandlerFunc func(interface{}, http.ResponseWriter, *http.Request)
type httpHandlerCustom struct {
ctx interface{}
F func(interface{}, http.ResponseWriter, *http.Request)
}
// ServeHTTP TODO
func (h httpHandlerCustom) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.F(h.ctx, w, r)
}
var diagPaths2Func = map[string]HTTPHandlerFunc{
"/": notImplemented,
"/help": help,
"/ready": ready,
"/stackdump": stackTrace,
}
// 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 {
enable int32
srv *http.Server
port int
mux *http.ServeMux
registeredHanders map[string]bool
sync.Mutex
}
// New creates a new diagnostic server
func New() *Server {
return &Server{
registeredHanders: make(map[string]bool),
}
}
// Init initialize the mux for the http handling and register the base hooks
func (s *Server) Init() {
s.mux = http.NewServeMux()
// Register local handlers
s.RegisterHandler(s, diagPaths2Func)
}
// RegisterHandler allows to register new handlers to the mux and to a specific path
func (s *Server) RegisterHandler(ctx interface{}, hdlrs map[string]HTTPHandlerFunc) {
s.Lock()
defer s.Unlock()
for path, fun := range hdlrs {
if _, ok := s.registeredHanders[path]; ok {
continue
}
s.mux.Handle(path, httpHandlerCustom{ctx, fun})
s.registeredHanders[path] = true
}
}
// 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.Lock()
defer s.Unlock()
s.port = port
if s.enable == 1 {
logrus.Info("The server is already up and running")
return
}
logrus.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 {
logrus.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.Lock()
defer s.Unlock()
s.srv.Shutdown(context.Background()) //nolint:errcheck
s.srv = nil
s.enable = 0
logrus.Info("Disabling the diagnostic server")
}
// IsDiagnosticEnabled returns true when the debug is enabled
func (s *Server) IsDiagnosticEnabled() bool {
s.Lock()
defer s.Unlock()
return s.enable == 1
}
func notImplemented(ctx interface{}, w http.ResponseWriter, r *http.Request) {
r.ParseForm() //nolint:errcheck
_, json := ParseHTTPFormOptions(r)
rsp := WrongCommand("not implemented", fmt.Sprintf("URL path: %s no method implemented check /help\n", r.URL.Path))
// audit logs
log := logrus.WithFields(logrus.Fields{"component": "diagnostic", "remoteIP": r.RemoteAddr, "method": caller.Name(0), "url": r.URL.String()})
log.Info("command not implemented done")
HTTPReply(w, rsp, json) //nolint:errcheck
}
func help(ctx interface{}, w http.ResponseWriter, r *http.Request) {
r.ParseForm() //nolint:errcheck
_, json := ParseHTTPFormOptions(r)
// audit logs
log := logrus.WithFields(logrus.Fields{"component": "diagnostic", "remoteIP": r.RemoteAddr, "method": caller.Name(0), "url": r.URL.String()})
log.Info("help done")
n, ok := ctx.(*Server)
var result string
if ok {
for path := range n.registeredHanders {
result += fmt.Sprintf("%s\n", path)
}
HTTPReply(w, CommandSucceed(&StringCmd{Info: result}), json) //nolint:errcheck
}
}
func ready(ctx interface{}, w http.ResponseWriter, r *http.Request) {
r.ParseForm() //nolint:errcheck
_, json := ParseHTTPFormOptions(r)
// audit logs
log := logrus.WithFields(logrus.Fields{"component": "diagnostic", "remoteIP": r.RemoteAddr, "method": caller.Name(0), "url": r.URL.String()})
log.Info("ready done")
HTTPReply(w, CommandSucceed(&StringCmd{Info: "OK"}), json) //nolint:errcheck
}
func stackTrace(ctx interface{}, w http.ResponseWriter, r *http.Request) {
r.ParseForm() //nolint:errcheck
_, json := ParseHTTPFormOptions(r)
// audit logs
log := logrus.WithFields(logrus.Fields{"component": "diagnostic", "remoteIP": r.RemoteAddr, "method": caller.Name(0), "url": r.URL.String()})
log.Info("stack trace")
path, err := stack.DumpToFile("/tmp/")
if err != nil {
log.WithError(err).Error("failed to write goroutines dump")
HTTPReply(w, FailCommand(err), json) //nolint:errcheck
} else {
log.Info("stack trace done")
HTTPReply(w, CommandSucceed(&StringCmd{Info: fmt.Sprintf("goroutine stacks written to %s", path)}), json) //nolint:errcheck
}
}
// DebugHTTPForm helper to print the form url parameters
func DebugHTTPForm(r *http.Request) {
for k, v := range r.Form {
logrus.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, json := r.Form["json"]
var pretty bool
if len(v) > 0 {
pretty = v[0] == "pretty"
}
return unsafe, &JSONOutput{enable: json, 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))
}