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