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 }