github.com/nsqio/nsq@v1.3.0/nsqadmin/http.go (about)

     1  package nsqadmin
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"html/template"
     7  	"io"
     8  	"mime"
     9  	"net"
    10  	"net/http"
    11  	"net/http/httputil"
    12  	"net/url"
    13  	"os"
    14  	"path"
    15  	"reflect"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/julienschmidt/httprouter"
    20  	"github.com/nsqio/nsq/internal/clusterinfo"
    21  	"github.com/nsqio/nsq/internal/http_api"
    22  	"github.com/nsqio/nsq/internal/lg"
    23  	"github.com/nsqio/nsq/internal/protocol"
    24  	"github.com/nsqio/nsq/internal/version"
    25  )
    26  
    27  func maybeWarnMsg(msgs []string) string {
    28  	if len(msgs) > 0 {
    29  		return "WARNING: " + strings.Join(msgs, "; ")
    30  	}
    31  	return ""
    32  }
    33  
    34  // this is similar to httputil.NewSingleHostReverseProxy except it passes along basic auth
    35  func NewSingleHostReverseProxy(target *url.URL, connectTimeout time.Duration, requestTimeout time.Duration) *httputil.ReverseProxy {
    36  	director := func(req *http.Request) {
    37  		req.URL.Scheme = target.Scheme
    38  		req.URL.Host = target.Host
    39  		if target.User != nil {
    40  			passwd, _ := target.User.Password()
    41  			req.SetBasicAuth(target.User.Username(), passwd)
    42  		}
    43  	}
    44  	return &httputil.ReverseProxy{
    45  		Director:  director,
    46  		Transport: http_api.NewDeadlineTransport(connectTimeout, requestTimeout),
    47  	}
    48  }
    49  
    50  type httpServer struct {
    51  	nsqadmin     *NSQAdmin
    52  	router       http.Handler
    53  	client       *http_api.Client
    54  	ci           *clusterinfo.ClusterInfo
    55  	basePath     string
    56  	devStaticDir string
    57  }
    58  
    59  func NewHTTPServer(nsqadmin *NSQAdmin) *httpServer {
    60  	log := http_api.Log(nsqadmin.logf)
    61  
    62  	client := http_api.NewClient(nsqadmin.httpClientTLSConfig, nsqadmin.getOpts().HTTPClientConnectTimeout,
    63  		nsqadmin.getOpts().HTTPClientRequestTimeout)
    64  
    65  	router := httprouter.New()
    66  	router.HandleMethodNotAllowed = true
    67  	router.PanicHandler = http_api.LogPanicHandler(nsqadmin.logf)
    68  	router.NotFound = http_api.LogNotFoundHandler(nsqadmin.logf)
    69  	router.MethodNotAllowed = http_api.LogMethodNotAllowedHandler(nsqadmin.logf)
    70  
    71  	s := &httpServer{
    72  		nsqadmin: nsqadmin,
    73  		router:   router,
    74  		client:   client,
    75  		ci:       clusterinfo.New(nsqadmin.logf, client),
    76  
    77  		basePath:     nsqadmin.getOpts().BasePath,
    78  		devStaticDir: nsqadmin.getOpts().DevStaticDir,
    79  	}
    80  
    81  	bp := func(p string) string {
    82  		return path.Join(s.basePath, p)
    83  	}
    84  
    85  	router.Handle("GET", bp("/"), http_api.Decorate(s.indexHandler, log))
    86  	router.Handle("GET", bp("/ping"), http_api.Decorate(s.pingHandler, log, http_api.PlainText))
    87  
    88  	router.Handle("GET", bp("/topics"), http_api.Decorate(s.indexHandler, log))
    89  	router.Handle("GET", bp("/topics/:topic"), http_api.Decorate(s.indexHandler, log))
    90  	router.Handle("GET", bp("/topics/:topic/:channel"), http_api.Decorate(s.indexHandler, log))
    91  	router.Handle("GET", bp("/nodes"), http_api.Decorate(s.indexHandler, log))
    92  	router.Handle("GET", bp("/nodes/:node"), http_api.Decorate(s.indexHandler, log))
    93  	router.Handle("GET", bp("/counter"), http_api.Decorate(s.indexHandler, log))
    94  	router.Handle("GET", bp("/lookup"), http_api.Decorate(s.indexHandler, log))
    95  
    96  	router.Handle("GET", bp("/static/:asset"), http_api.Decorate(s.staticAssetHandler, log, http_api.PlainText))
    97  	router.Handle("GET", bp("/fonts/:asset"), http_api.Decorate(s.staticAssetHandler, log, http_api.PlainText))
    98  	if s.nsqadmin.getOpts().ProxyGraphite {
    99  		proxy := NewSingleHostReverseProxy(nsqadmin.graphiteURL, nsqadmin.getOpts().HTTPClientConnectTimeout,
   100  			nsqadmin.getOpts().HTTPClientRequestTimeout)
   101  		router.Handler("GET", bp("/render"), proxy)
   102  	}
   103  
   104  	// v1 endpoints
   105  	router.Handle("GET", bp("/api/topics"), http_api.Decorate(s.topicsHandler, log, http_api.V1))
   106  	router.Handle("GET", bp("/api/topics/:topic"), http_api.Decorate(s.topicHandler, log, http_api.V1))
   107  	router.Handle("GET", bp("/api/topics/:topic/:channel"), http_api.Decorate(s.channelHandler, log, http_api.V1))
   108  	router.Handle("GET", bp("/api/nodes"), http_api.Decorate(s.nodesHandler, log, http_api.V1))
   109  	router.Handle("GET", bp("/api/nodes/:node"), http_api.Decorate(s.nodeHandler, log, http_api.V1))
   110  	router.Handle("POST", bp("/api/topics"), http_api.Decorate(s.createTopicChannelHandler, log, http_api.V1))
   111  	router.Handle("POST", bp("/api/topics/:topic"), http_api.Decorate(s.topicActionHandler, log, http_api.V1))
   112  	router.Handle("POST", bp("/api/topics/:topic/:channel"), http_api.Decorate(s.channelActionHandler, log, http_api.V1))
   113  	router.Handle("DELETE", bp("/api/nodes/:node"), http_api.Decorate(s.tombstoneNodeForTopicHandler, log, http_api.V1))
   114  	router.Handle("DELETE", bp("/api/topics/:topic"), http_api.Decorate(s.deleteTopicHandler, log, http_api.V1))
   115  	router.Handle("DELETE", bp("/api/topics/:topic/:channel"), http_api.Decorate(s.deleteChannelHandler, log, http_api.V1))
   116  	router.Handle("GET", bp("/api/counter"), http_api.Decorate(s.counterHandler, log, http_api.V1))
   117  	router.Handle("GET", bp("/api/graphite"), http_api.Decorate(s.graphiteHandler, log, http_api.V1))
   118  	router.Handle("GET", bp("/config/:opt"), http_api.Decorate(s.doConfig, log, http_api.V1))
   119  	router.Handle("PUT", bp("/config/:opt"), http_api.Decorate(s.doConfig, log, http_api.V1))
   120  
   121  	return s
   122  }
   123  
   124  func (s *httpServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
   125  	s.router.ServeHTTP(w, req)
   126  }
   127  
   128  func (s *httpServer) pingHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   129  	return "OK", nil
   130  }
   131  
   132  func (s *httpServer) indexHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   133  	asset, _ := staticAsset("index.html")
   134  	t, _ := template.New("index").Funcs(template.FuncMap{
   135  		"basePath": func(p string) string {
   136  			return path.Join(s.basePath, p)
   137  		},
   138  	}).Parse(string(asset))
   139  
   140  	w.Header().Set("Content-Type", "text/html")
   141  	t.Execute(w, struct {
   142  		Version             string
   143  		ProxyGraphite       bool
   144  		GraphEnabled        bool
   145  		GraphiteURL         string
   146  		StatsdInterval      int
   147  		StatsdCounterFormat string
   148  		StatsdGaugeFormat   string
   149  		StatsdPrefix        string
   150  		NSQLookupd          []string
   151  		IsAdmin             bool
   152  	}{
   153  		Version:             version.Binary,
   154  		ProxyGraphite:       s.nsqadmin.getOpts().ProxyGraphite,
   155  		GraphEnabled:        s.nsqadmin.getOpts().GraphiteURL != "",
   156  		GraphiteURL:         s.nsqadmin.getOpts().GraphiteURL,
   157  		StatsdInterval:      int(s.nsqadmin.getOpts().StatsdInterval / time.Second),
   158  		StatsdCounterFormat: s.nsqadmin.getOpts().StatsdCounterFormat,
   159  		StatsdGaugeFormat:   s.nsqadmin.getOpts().StatsdGaugeFormat,
   160  		StatsdPrefix:        s.nsqadmin.getOpts().StatsdPrefix,
   161  		NSQLookupd:          s.nsqadmin.getOpts().NSQLookupdHTTPAddresses,
   162  		IsAdmin:             s.isAuthorizedAdminRequest(req),
   163  	})
   164  
   165  	return nil, nil
   166  }
   167  
   168  func (s *httpServer) staticAssetHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   169  	assetName := ps.ByName("asset")
   170  
   171  	var (
   172  		asset []byte
   173  		err   error
   174  	)
   175  	if s.devStaticDir != "" {
   176  		s.nsqadmin.logf(LOG_DEBUG, "using dev dir %q for static asset %q", s.devStaticDir, assetName)
   177  		fsPath := path.Join(s.devStaticDir, assetName)
   178  		asset, err = os.ReadFile(fsPath)
   179  	} else {
   180  		asset, err = staticAsset(assetName)
   181  	}
   182  	if err != nil {
   183  		return nil, http_api.Err{404, "NOT_FOUND"}
   184  	}
   185  
   186  	ext := path.Ext(assetName)
   187  	ct := mime.TypeByExtension(ext)
   188  	if ct == "" {
   189  		switch ext {
   190  		case ".map":
   191  			ct = "application/json"
   192  		case ".svg":
   193  			ct = "image/svg+xml"
   194  		case ".woff":
   195  			ct = "application/font-woff"
   196  		case ".ttf":
   197  			ct = "application/font-sfnt"
   198  		case ".eot":
   199  			ct = "application/vnd.ms-fontobject"
   200  		case ".woff2":
   201  			ct = "application/font-woff2"
   202  		}
   203  	}
   204  	if ct != "" {
   205  		w.Header().Set("Content-Type", ct)
   206  	}
   207  
   208  	return string(asset), nil
   209  }
   210  
   211  func (s *httpServer) topicsHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   212  	var messages []string
   213  
   214  	reqParams, err := http_api.NewReqParams(req)
   215  	if err != nil {
   216  		return nil, http_api.Err{400, err.Error()}
   217  	}
   218  
   219  	var topics []string
   220  	if len(s.nsqadmin.getOpts().NSQLookupdHTTPAddresses) != 0 {
   221  		topics, err = s.ci.GetLookupdTopics(s.nsqadmin.getOpts().NSQLookupdHTTPAddresses)
   222  	} else {
   223  		topics, err = s.ci.GetNSQDTopics(s.nsqadmin.getOpts().NSQDHTTPAddresses)
   224  	}
   225  	if err != nil {
   226  		pe, ok := err.(clusterinfo.PartialErr)
   227  		if !ok {
   228  			s.nsqadmin.logf(LOG_ERROR, "failed to get topics - %s", err)
   229  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   230  		}
   231  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   232  		messages = append(messages, pe.Error())
   233  	}
   234  
   235  	inactive, _ := reqParams.Get("inactive")
   236  	if inactive == "true" {
   237  		topicChannelMap := make(map[string][]string)
   238  		if len(s.nsqadmin.getOpts().NSQLookupdHTTPAddresses) == 0 {
   239  			goto respond
   240  		}
   241  		for _, topicName := range topics {
   242  			producers, _ := s.ci.GetLookupdTopicProducers(
   243  				topicName, s.nsqadmin.getOpts().NSQLookupdHTTPAddresses)
   244  			if len(producers) == 0 {
   245  				topicChannels, _ := s.ci.GetLookupdTopicChannels(
   246  					topicName, s.nsqadmin.getOpts().NSQLookupdHTTPAddresses)
   247  				topicChannelMap[topicName] = topicChannels
   248  			}
   249  		}
   250  	respond:
   251  		return struct {
   252  			Topics  map[string][]string `json:"topics"`
   253  			Message string              `json:"message"`
   254  		}{topicChannelMap, maybeWarnMsg(messages)}, nil
   255  	}
   256  
   257  	return struct {
   258  		Topics  []string `json:"topics"`
   259  		Message string   `json:"message"`
   260  	}{topics, maybeWarnMsg(messages)}, nil
   261  }
   262  
   263  func (s *httpServer) topicHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   264  	var messages []string
   265  
   266  	topicName := ps.ByName("topic")
   267  
   268  	producers, err := s.ci.GetTopicProducers(topicName,
   269  		s.nsqadmin.getOpts().NSQLookupdHTTPAddresses,
   270  		s.nsqadmin.getOpts().NSQDHTTPAddresses)
   271  	if err != nil {
   272  		pe, ok := err.(clusterinfo.PartialErr)
   273  		if !ok {
   274  			s.nsqadmin.logf(LOG_ERROR, "failed to get topic producers - %s", err)
   275  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   276  		}
   277  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   278  		messages = append(messages, pe.Error())
   279  	}
   280  	topicStats, _, err := s.ci.GetNSQDStats(producers, topicName, "", false)
   281  	if err != nil {
   282  		pe, ok := err.(clusterinfo.PartialErr)
   283  		if !ok {
   284  			s.nsqadmin.logf(LOG_ERROR, "failed to get topic metadata - %s", err)
   285  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   286  		}
   287  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   288  		messages = append(messages, pe.Error())
   289  	}
   290  
   291  	allNodesTopicStats := &clusterinfo.TopicStats{TopicName: topicName}
   292  	for _, t := range topicStats {
   293  		allNodesTopicStats.Add(t)
   294  	}
   295  
   296  	return struct {
   297  		*clusterinfo.TopicStats
   298  		Message string `json:"message"`
   299  	}{allNodesTopicStats, maybeWarnMsg(messages)}, nil
   300  }
   301  
   302  func (s *httpServer) channelHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   303  	var messages []string
   304  
   305  	topicName := ps.ByName("topic")
   306  	channelName := ps.ByName("channel")
   307  
   308  	producers, err := s.ci.GetTopicProducers(topicName,
   309  		s.nsqadmin.getOpts().NSQLookupdHTTPAddresses,
   310  		s.nsqadmin.getOpts().NSQDHTTPAddresses)
   311  	if err != nil {
   312  		pe, ok := err.(clusterinfo.PartialErr)
   313  		if !ok {
   314  			s.nsqadmin.logf(LOG_ERROR, "failed to get topic producers - %s", err)
   315  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   316  		}
   317  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   318  		messages = append(messages, pe.Error())
   319  	}
   320  	_, channelStats, err := s.ci.GetNSQDStats(producers, topicName, channelName, true)
   321  	if err != nil {
   322  		pe, ok := err.(clusterinfo.PartialErr)
   323  		if !ok {
   324  			s.nsqadmin.logf(LOG_ERROR, "failed to get channel metadata - %s", err)
   325  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   326  		}
   327  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   328  		messages = append(messages, pe.Error())
   329  	}
   330  
   331  	return struct {
   332  		*clusterinfo.ChannelStats
   333  		Message string `json:"message"`
   334  	}{channelStats[channelName], maybeWarnMsg(messages)}, nil
   335  }
   336  
   337  func (s *httpServer) nodesHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   338  	var messages []string
   339  
   340  	producers, err := s.ci.GetProducers(s.nsqadmin.getOpts().NSQLookupdHTTPAddresses, s.nsqadmin.getOpts().NSQDHTTPAddresses)
   341  	if err != nil {
   342  		pe, ok := err.(clusterinfo.PartialErr)
   343  		if !ok {
   344  			s.nsqadmin.logf(LOG_ERROR, "failed to get nodes - %s", err)
   345  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   346  		}
   347  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   348  		messages = append(messages, pe.Error())
   349  	}
   350  
   351  	return struct {
   352  		Nodes   clusterinfo.Producers `json:"nodes"`
   353  		Message string                `json:"message"`
   354  	}{producers, maybeWarnMsg(messages)}, nil
   355  }
   356  
   357  func (s *httpServer) nodeHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   358  	var messages []string
   359  
   360  	node := ps.ByName("node")
   361  
   362  	producers, err := s.ci.GetProducers(s.nsqadmin.getOpts().NSQLookupdHTTPAddresses, s.nsqadmin.getOpts().NSQDHTTPAddresses)
   363  	if err != nil {
   364  		pe, ok := err.(clusterinfo.PartialErr)
   365  		if !ok {
   366  			s.nsqadmin.logf(LOG_ERROR, "failed to get producers - %s", err)
   367  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   368  		}
   369  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   370  		messages = append(messages, pe.Error())
   371  	}
   372  
   373  	producer := producers.Search(node)
   374  	if producer == nil {
   375  		return nil, http_api.Err{404, "NODE_NOT_FOUND"}
   376  	}
   377  
   378  	topicStats, _, err := s.ci.GetNSQDStats(clusterinfo.Producers{producer}, "", "", true)
   379  	if err != nil {
   380  		s.nsqadmin.logf(LOG_ERROR, "failed to get nsqd stats - %s", err)
   381  		return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   382  	}
   383  
   384  	var totalClients int64
   385  	var totalMessages int64
   386  	for _, ts := range topicStats {
   387  		for _, cs := range ts.Channels {
   388  			totalClients += int64(len(cs.Clients))
   389  		}
   390  		totalMessages += ts.MessageCount
   391  	}
   392  
   393  	return struct {
   394  		Node          string                    `json:"node"`
   395  		TopicStats    []*clusterinfo.TopicStats `json:"topics"`
   396  		TotalMessages int64                     `json:"total_messages"`
   397  		TotalClients  int64                     `json:"total_clients"`
   398  		Message       string                    `json:"message"`
   399  	}{
   400  		Node:          node,
   401  		TopicStats:    topicStats,
   402  		TotalMessages: totalMessages,
   403  		TotalClients:  totalClients,
   404  		Message:       maybeWarnMsg(messages),
   405  	}, nil
   406  }
   407  
   408  func (s *httpServer) tombstoneNodeForTopicHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   409  	var messages []string
   410  
   411  	if !s.isAuthorizedAdminRequest(req) {
   412  		return nil, http_api.Err{403, "FORBIDDEN"}
   413  	}
   414  
   415  	node := ps.ByName("node")
   416  
   417  	var body struct {
   418  		Topic string `json:"topic"`
   419  	}
   420  	err := json.NewDecoder(req.Body).Decode(&body)
   421  	if err != nil {
   422  		return nil, http_api.Err{400, "INVALID_BODY"}
   423  	}
   424  
   425  	if !protocol.IsValidTopicName(body.Topic) {
   426  		return nil, http_api.Err{400, "INVALID_TOPIC"}
   427  	}
   428  
   429  	err = s.ci.TombstoneNodeForTopic(body.Topic, node,
   430  		s.nsqadmin.getOpts().NSQLookupdHTTPAddresses)
   431  	if err != nil {
   432  		pe, ok := err.(clusterinfo.PartialErr)
   433  		if !ok {
   434  			s.nsqadmin.logf(LOG_ERROR, "failed to tombstone node for topic - %s", err)
   435  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   436  		}
   437  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   438  		messages = append(messages, pe.Error())
   439  	}
   440  
   441  	s.notifyAdminAction("tombstone_topic_producer", body.Topic, "", node, req)
   442  
   443  	return struct {
   444  		Message string `json:"message"`
   445  	}{maybeWarnMsg(messages)}, nil
   446  }
   447  
   448  func (s *httpServer) createTopicChannelHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   449  	var messages []string
   450  
   451  	var body struct {
   452  		Topic   string `json:"topic"`
   453  		Channel string `json:"channel"`
   454  	}
   455  
   456  	if !s.isAuthorizedAdminRequest(req) {
   457  		return nil, http_api.Err{403, "FORBIDDEN"}
   458  	}
   459  
   460  	err := json.NewDecoder(req.Body).Decode(&body)
   461  	if err != nil {
   462  		return nil, http_api.Err{400, err.Error()}
   463  	}
   464  
   465  	if !protocol.IsValidTopicName(body.Topic) {
   466  		return nil, http_api.Err{400, "INVALID_TOPIC"}
   467  	}
   468  
   469  	if len(body.Channel) > 0 && !protocol.IsValidChannelName(body.Channel) {
   470  		return nil, http_api.Err{400, "INVALID_CHANNEL"}
   471  	}
   472  
   473  	err = s.ci.CreateTopicChannel(body.Topic, body.Channel,
   474  		s.nsqadmin.getOpts().NSQLookupdHTTPAddresses)
   475  	if err != nil {
   476  		pe, ok := err.(clusterinfo.PartialErr)
   477  		if !ok {
   478  			s.nsqadmin.logf(LOG_ERROR, "failed to create topic/channel - %s", err)
   479  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   480  		}
   481  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   482  		messages = append(messages, pe.Error())
   483  	}
   484  
   485  	s.notifyAdminAction("create_topic", body.Topic, "", "", req)
   486  	if len(body.Channel) > 0 {
   487  		s.notifyAdminAction("create_channel", body.Topic, body.Channel, "", req)
   488  	}
   489  
   490  	return struct {
   491  		Message string `json:"message"`
   492  	}{maybeWarnMsg(messages)}, nil
   493  }
   494  
   495  func (s *httpServer) deleteTopicHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   496  	var messages []string
   497  
   498  	if !s.isAuthorizedAdminRequest(req) {
   499  		return nil, http_api.Err{403, "FORBIDDEN"}
   500  	}
   501  
   502  	topicName := ps.ByName("topic")
   503  
   504  	err := s.ci.DeleteTopic(topicName,
   505  		s.nsqadmin.getOpts().NSQLookupdHTTPAddresses,
   506  		s.nsqadmin.getOpts().NSQDHTTPAddresses)
   507  	if err != nil {
   508  		pe, ok := err.(clusterinfo.PartialErr)
   509  		if !ok {
   510  			s.nsqadmin.logf(LOG_ERROR, "failed to delete topic - %s", err)
   511  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   512  		}
   513  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   514  		messages = append(messages, pe.Error())
   515  	}
   516  
   517  	s.notifyAdminAction("delete_topic", topicName, "", "", req)
   518  
   519  	return struct {
   520  		Message string `json:"message"`
   521  	}{maybeWarnMsg(messages)}, nil
   522  }
   523  
   524  func (s *httpServer) deleteChannelHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   525  	var messages []string
   526  
   527  	if !s.isAuthorizedAdminRequest(req) {
   528  		return nil, http_api.Err{403, "FORBIDDEN"}
   529  	}
   530  
   531  	topicName := ps.ByName("topic")
   532  	channelName := ps.ByName("channel")
   533  
   534  	err := s.ci.DeleteChannel(topicName, channelName,
   535  		s.nsqadmin.getOpts().NSQLookupdHTTPAddresses,
   536  		s.nsqadmin.getOpts().NSQDHTTPAddresses)
   537  	if err != nil {
   538  		pe, ok := err.(clusterinfo.PartialErr)
   539  		if !ok {
   540  			s.nsqadmin.logf(LOG_ERROR, "failed to delete channel - %s", err)
   541  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   542  		}
   543  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   544  		messages = append(messages, pe.Error())
   545  	}
   546  
   547  	s.notifyAdminAction("delete_channel", topicName, channelName, "", req)
   548  
   549  	return struct {
   550  		Message string `json:"message"`
   551  	}{maybeWarnMsg(messages)}, nil
   552  }
   553  
   554  func (s *httpServer) topicActionHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   555  	topicName := ps.ByName("topic")
   556  	return s.topicChannelAction(req, topicName, "")
   557  }
   558  
   559  func (s *httpServer) channelActionHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   560  	topicName := ps.ByName("topic")
   561  	channelName := ps.ByName("channel")
   562  	return s.topicChannelAction(req, topicName, channelName)
   563  }
   564  
   565  func (s *httpServer) topicChannelAction(req *http.Request, topicName string, channelName string) (interface{}, error) {
   566  	var messages []string
   567  
   568  	var body struct {
   569  		Action string `json:"action"`
   570  	}
   571  
   572  	if !s.isAuthorizedAdminRequest(req) {
   573  		return nil, http_api.Err{403, "FORBIDDEN"}
   574  	}
   575  
   576  	err := json.NewDecoder(req.Body).Decode(&body)
   577  	if err != nil {
   578  		return nil, http_api.Err{400, err.Error()}
   579  	}
   580  
   581  	switch body.Action {
   582  	case "pause":
   583  		if channelName != "" {
   584  			err = s.ci.PauseChannel(topicName, channelName,
   585  				s.nsqadmin.getOpts().NSQLookupdHTTPAddresses,
   586  				s.nsqadmin.getOpts().NSQDHTTPAddresses)
   587  
   588  			s.notifyAdminAction("pause_channel", topicName, channelName, "", req)
   589  		} else {
   590  			err = s.ci.PauseTopic(topicName,
   591  				s.nsqadmin.getOpts().NSQLookupdHTTPAddresses,
   592  				s.nsqadmin.getOpts().NSQDHTTPAddresses)
   593  
   594  			s.notifyAdminAction("pause_topic", topicName, "", "", req)
   595  		}
   596  	case "unpause":
   597  		if channelName != "" {
   598  			err = s.ci.UnPauseChannel(topicName, channelName,
   599  				s.nsqadmin.getOpts().NSQLookupdHTTPAddresses,
   600  				s.nsqadmin.getOpts().NSQDHTTPAddresses)
   601  
   602  			s.notifyAdminAction("unpause_channel", topicName, channelName, "", req)
   603  		} else {
   604  			err = s.ci.UnPauseTopic(topicName,
   605  				s.nsqadmin.getOpts().NSQLookupdHTTPAddresses,
   606  				s.nsqadmin.getOpts().NSQDHTTPAddresses)
   607  
   608  			s.notifyAdminAction("unpause_topic", topicName, "", "", req)
   609  		}
   610  	case "empty":
   611  		if channelName != "" {
   612  			err = s.ci.EmptyChannel(topicName, channelName,
   613  				s.nsqadmin.getOpts().NSQLookupdHTTPAddresses,
   614  				s.nsqadmin.getOpts().NSQDHTTPAddresses)
   615  
   616  			s.notifyAdminAction("empty_channel", topicName, channelName, "", req)
   617  		} else {
   618  			err = s.ci.EmptyTopic(topicName,
   619  				s.nsqadmin.getOpts().NSQLookupdHTTPAddresses,
   620  				s.nsqadmin.getOpts().NSQDHTTPAddresses)
   621  
   622  			s.notifyAdminAction("empty_topic", topicName, "", "", req)
   623  		}
   624  	default:
   625  		return nil, http_api.Err{400, "INVALID_ACTION"}
   626  	}
   627  
   628  	if err != nil {
   629  		pe, ok := err.(clusterinfo.PartialErr)
   630  		if !ok {
   631  			s.nsqadmin.logf(LOG_ERROR, "failed to %s topic/channel - %s", body.Action, err)
   632  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   633  		}
   634  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   635  		messages = append(messages, pe.Error())
   636  	}
   637  
   638  	return struct {
   639  		Message string `json:"message"`
   640  	}{maybeWarnMsg(messages)}, nil
   641  }
   642  
   643  type counterStats struct {
   644  	Node         string `json:"node"`
   645  	TopicName    string `json:"topic_name"`
   646  	ChannelName  string `json:"channel_name"`
   647  	MessageCount int64  `json:"message_count"`
   648  }
   649  
   650  func (s *httpServer) counterHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   651  	var messages []string
   652  	stats := make(map[string]*counterStats)
   653  
   654  	producers, err := s.ci.GetProducers(s.nsqadmin.getOpts().NSQLookupdHTTPAddresses, s.nsqadmin.getOpts().NSQDHTTPAddresses)
   655  	if err != nil {
   656  		pe, ok := err.(clusterinfo.PartialErr)
   657  		if !ok {
   658  			s.nsqadmin.logf(LOG_ERROR, "failed to get counter producer list - %s", err)
   659  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   660  		}
   661  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   662  		messages = append(messages, pe.Error())
   663  	}
   664  	_, channelStats, err := s.ci.GetNSQDStats(producers, "", "", false)
   665  	if err != nil {
   666  		pe, ok := err.(clusterinfo.PartialErr)
   667  		if !ok {
   668  			s.nsqadmin.logf(LOG_ERROR, "failed to get nsqd stats - %s", err)
   669  			return nil, http_api.Err{502, fmt.Sprintf("UPSTREAM_ERROR: %s", err)}
   670  		}
   671  		s.nsqadmin.logf(LOG_WARN, "%s", err)
   672  		messages = append(messages, pe.Error())
   673  	}
   674  
   675  	for _, channelStats := range channelStats {
   676  		for _, hostChannelStats := range channelStats.NodeStats {
   677  			key := fmt.Sprintf("%s:%s:%s", channelStats.TopicName, channelStats.ChannelName, hostChannelStats.Node)
   678  			s, ok := stats[key]
   679  			if !ok {
   680  				s = &counterStats{
   681  					Node:        hostChannelStats.Node,
   682  					TopicName:   channelStats.TopicName,
   683  					ChannelName: channelStats.ChannelName,
   684  				}
   685  				stats[key] = s
   686  			}
   687  			s.MessageCount += hostChannelStats.MessageCount
   688  		}
   689  	}
   690  
   691  	return struct {
   692  		Stats   map[string]*counterStats `json:"stats"`
   693  		Message string                   `json:"message"`
   694  	}{stats, maybeWarnMsg(messages)}, nil
   695  }
   696  
   697  func (s *httpServer) graphiteHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   698  	reqParams, err := http_api.NewReqParams(req)
   699  	if err != nil {
   700  		return nil, http_api.Err{400, "INVALID_REQUEST"}
   701  	}
   702  
   703  	metric, err := reqParams.Get("metric")
   704  	if err != nil || metric != "rate" {
   705  		return nil, http_api.Err{400, "INVALID_ARG_METRIC"}
   706  	}
   707  
   708  	target, err := reqParams.Get("target")
   709  	if err != nil {
   710  		return nil, http_api.Err{400, "INVALID_ARG_TARGET"}
   711  	}
   712  
   713  	params := url.Values{}
   714  	params.Set("from", fmt.Sprintf("-%dsec", s.nsqadmin.getOpts().StatsdInterval*2/time.Second))
   715  	params.Set("until", fmt.Sprintf("-%dsec", s.nsqadmin.getOpts().StatsdInterval/time.Second))
   716  	params.Set("format", "json")
   717  	params.Set("target", target)
   718  	query := fmt.Sprintf("/render?%s", params.Encode())
   719  	url := s.nsqadmin.getOpts().GraphiteURL + query
   720  
   721  	s.nsqadmin.logf(LOG_INFO, "GRAPHITE: %s", url)
   722  
   723  	var response []struct {
   724  		Target     string       `json:"target"`
   725  		DataPoints [][]*float64 `json:"datapoints"`
   726  	}
   727  	err = s.client.GETV1(url, &response)
   728  	if err != nil {
   729  		s.nsqadmin.logf(LOG_ERROR, "graphite request failed - %s", err)
   730  		return nil, http_api.Err{500, "INTERNAL_ERROR"}
   731  	}
   732  
   733  	var rateStr string
   734  	rate := *response[0].DataPoints[0][0]
   735  	if rate < 0 {
   736  		rateStr = "N/A"
   737  	} else {
   738  		rateDivisor := s.nsqadmin.getOpts().StatsdInterval / time.Second
   739  		rateStr = fmt.Sprintf("%.2f", rate/float64(rateDivisor))
   740  	}
   741  	return struct {
   742  		Rate string `json:"rate"`
   743  	}{rateStr}, nil
   744  }
   745  
   746  func (s *httpServer) doConfig(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
   747  	opt := ps.ByName("opt")
   748  
   749  	allowConfigFromCIDR := s.nsqadmin.getOpts().AllowConfigFromCIDR
   750  	if allowConfigFromCIDR != "" {
   751  		_, ipnet, _ := net.ParseCIDR(allowConfigFromCIDR)
   752  		addr, _, err := net.SplitHostPort(req.RemoteAddr)
   753  		if err != nil {
   754  			s.nsqadmin.logf(LOG_ERROR, "failed to parse RemoteAddr %s", req.RemoteAddr)
   755  			return nil, http_api.Err{400, "INVALID_REMOTE_ADDR"}
   756  		}
   757  		ip := net.ParseIP(addr)
   758  		if ip == nil {
   759  			s.nsqadmin.logf(LOG_ERROR, "failed to parse RemoteAddr %s", req.RemoteAddr)
   760  			return nil, http_api.Err{400, "INVALID_REMOTE_ADDR"}
   761  		}
   762  		if !ipnet.Contains(ip) {
   763  			return nil, http_api.Err{403, "FORBIDDEN"}
   764  		}
   765  	}
   766  
   767  	if req.Method == "PUT" {
   768  		// add 1 so that it's greater than our max when we test for it
   769  		// (LimitReader returns a "fake" EOF)
   770  		readMax := int64(1024*1024 + 1)
   771  		body, err := io.ReadAll(io.LimitReader(req.Body, readMax))
   772  		if err != nil {
   773  			return nil, http_api.Err{500, "INTERNAL_ERROR"}
   774  		}
   775  		if int64(len(body)) == readMax || len(body) == 0 {
   776  			return nil, http_api.Err{413, "INVALID_VALUE"}
   777  		}
   778  
   779  		opts := *s.nsqadmin.getOpts()
   780  		switch opt {
   781  		case "nsqlookupd_http_addresses":
   782  			err := json.Unmarshal(body, &opts.NSQLookupdHTTPAddresses)
   783  			if err != nil {
   784  				return nil, http_api.Err{400, "INVALID_VALUE"}
   785  			}
   786  		case "log_level":
   787  			logLevelStr := string(body)
   788  			logLevel, err := lg.ParseLogLevel(logLevelStr)
   789  			if err != nil {
   790  				return nil, http_api.Err{400, "INVALID_VALUE"}
   791  			}
   792  			opts.LogLevel = logLevel
   793  		default:
   794  			return nil, http_api.Err{400, "INVALID_OPTION"}
   795  		}
   796  		s.nsqadmin.swapOpts(&opts)
   797  	}
   798  
   799  	v, ok := getOptByCfgName(s.nsqadmin.getOpts(), opt)
   800  	if !ok {
   801  		return nil, http_api.Err{400, "INVALID_OPTION"}
   802  	}
   803  
   804  	return v, nil
   805  }
   806  
   807  func (s *httpServer) isAuthorizedAdminRequest(req *http.Request) bool {
   808  	adminUsers := s.nsqadmin.getOpts().AdminUsers
   809  	if len(adminUsers) == 0 {
   810  		return true
   811  	}
   812  	aclHTTPHeader := s.nsqadmin.getOpts().ACLHTTPHeader
   813  	user := req.Header.Get(aclHTTPHeader)
   814  	for _, v := range adminUsers {
   815  		if v == user {
   816  			return true
   817  		}
   818  	}
   819  	return false
   820  }
   821  
   822  func getOptByCfgName(opts interface{}, name string) (interface{}, bool) {
   823  	val := reflect.ValueOf(opts).Elem()
   824  	typ := val.Type()
   825  	for i := 0; i < typ.NumField(); i++ {
   826  		field := typ.Field(i)
   827  		flagName := field.Tag.Get("flag")
   828  		cfgName := field.Tag.Get("cfg")
   829  		if flagName == "" {
   830  			continue
   831  		}
   832  		if cfgName == "" {
   833  			cfgName = strings.Replace(flagName, "-", "_", -1)
   834  		}
   835  		if name != cfgName {
   836  			continue
   837  		}
   838  		return val.FieldByName(field.Name).Interface(), true
   839  	}
   840  	return nil, false
   841  }