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