| Abhay Kumar | a2ae599 | 2025-11-10 14:02:24 +0000 | [diff] [blame^] | 1 | // Copyright 2015 The etcd Authors |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | package embed |
| 16 | |
| 17 | import ( |
| 18 | "context" |
| 19 | "errors" |
| 20 | "fmt" |
| 21 | "io" |
| 22 | defaultLog "log" |
| 23 | "net" |
| 24 | "net/http" |
| 25 | "strings" |
| 26 | "sync" |
| 27 | |
| 28 | gw "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" |
| 29 | "github.com/soheilhy/cmux" |
| 30 | "github.com/tmc/grpc-websocket-proxy/wsproxy" |
| 31 | "go.uber.org/zap" |
| 32 | "golang.org/x/net/http2" |
| 33 | "golang.org/x/net/trace" |
| 34 | "google.golang.org/grpc" |
| 35 | "google.golang.org/protobuf/encoding/protojson" |
| 36 | |
| 37 | etcdservergw "go.etcd.io/etcd/api/v3/etcdserverpb/gw" |
| 38 | "go.etcd.io/etcd/client/pkg/v3/transport" |
| 39 | "go.etcd.io/etcd/pkg/v3/debugutil" |
| 40 | "go.etcd.io/etcd/pkg/v3/httputil" |
| 41 | "go.etcd.io/etcd/server/v3/config" |
| 42 | "go.etcd.io/etcd/server/v3/etcdserver" |
| 43 | "go.etcd.io/etcd/server/v3/etcdserver/api/v3client" |
| 44 | "go.etcd.io/etcd/server/v3/etcdserver/api/v3election" |
| 45 | "go.etcd.io/etcd/server/v3/etcdserver/api/v3election/v3electionpb" |
| 46 | v3electiongw "go.etcd.io/etcd/server/v3/etcdserver/api/v3election/v3electionpb/gw" |
| 47 | "go.etcd.io/etcd/server/v3/etcdserver/api/v3lock" |
| 48 | "go.etcd.io/etcd/server/v3/etcdserver/api/v3lock/v3lockpb" |
| 49 | v3lockgw "go.etcd.io/etcd/server/v3/etcdserver/api/v3lock/v3lockpb/gw" |
| 50 | "go.etcd.io/etcd/server/v3/etcdserver/api/v3rpc" |
| 51 | ) |
| 52 | |
| 53 | type serveCtx struct { |
| 54 | lg *zap.Logger |
| 55 | l net.Listener |
| 56 | |
| 57 | scheme string |
| 58 | addr string |
| 59 | network string |
| 60 | secure bool |
| 61 | insecure bool |
| 62 | httpOnly bool |
| 63 | |
| 64 | // ctx is used to control the grpc gateway. Terminate the grpc gateway |
| 65 | // by calling `cancel` when shutting down the etcd. |
| 66 | ctx context.Context |
| 67 | cancel context.CancelFunc |
| 68 | |
| 69 | userHandlers map[string]http.Handler |
| 70 | serviceRegister func(*grpc.Server) |
| 71 | |
| 72 | // serversC is used to receive the http and grpc server objects (created |
| 73 | // in `serve`), both of which will be closed when shutting down the etcd. |
| 74 | // Close it when `serve` returns or when etcd fails to bootstrap. |
| 75 | serversC chan *servers |
| 76 | // closeOnce is to ensure `serversC` is closed only once. |
| 77 | closeOnce sync.Once |
| 78 | |
| 79 | // wg is used to track the lifecycle of all sub goroutines created by `serve`. |
| 80 | wg sync.WaitGroup |
| 81 | } |
| 82 | |
| 83 | func (sctx *serveCtx) startHandler(errHandler func(error), handler func() error) { |
| 84 | // start each handler in a separate goroutine |
| 85 | sctx.wg.Add(1) |
| 86 | go func() { |
| 87 | defer sctx.wg.Done() |
| 88 | err := handler() |
| 89 | if errHandler != nil { |
| 90 | errHandler(err) |
| 91 | } |
| 92 | }() |
| 93 | } |
| 94 | |
| 95 | type servers struct { |
| 96 | secure bool |
| 97 | grpc *grpc.Server |
| 98 | http *http.Server |
| 99 | } |
| 100 | |
| 101 | func newServeCtx(lg *zap.Logger) *serveCtx { |
| 102 | ctx, cancel := context.WithCancel(context.Background()) |
| 103 | if lg == nil { |
| 104 | lg = zap.NewNop() |
| 105 | } |
| 106 | return &serveCtx{ |
| 107 | lg: lg, |
| 108 | ctx: ctx, |
| 109 | cancel: cancel, |
| 110 | userHandlers: make(map[string]http.Handler), |
| 111 | serversC: make(chan *servers, 2), // in case sctx.insecure,sctx.secure true |
| 112 | } |
| 113 | } |
| 114 | |
| 115 | // serve accepts incoming connections on the listener l, |
| 116 | // creating a new service goroutine for each. The service goroutines |
| 117 | // read requests and then call handler to reply to them. |
| 118 | func (sctx *serveCtx) serve( |
| 119 | s *etcdserver.EtcdServer, |
| 120 | tlsinfo *transport.TLSInfo, |
| 121 | handler http.Handler, |
| 122 | errHandler func(error), |
| 123 | grpcDialForRestGatewayBackends func(ctx context.Context) (*grpc.ClientConn, error), |
| 124 | splitHTTP bool, |
| 125 | gopts ...grpc.ServerOption, |
| 126 | ) (err error) { |
| 127 | logger := defaultLog.New(io.Discard, "etcdhttp", 0) |
| 128 | |
| 129 | // Make sure serversC is closed even if we prematurely exit the function. |
| 130 | defer sctx.close() |
| 131 | |
| 132 | select { |
| 133 | case <-s.StoppingNotify(): |
| 134 | return errors.New("server is stopping") |
| 135 | case <-s.ReadyNotify(): |
| 136 | } |
| 137 | |
| 138 | sctx.lg.Info("ready to serve client requests") |
| 139 | |
| 140 | m := cmux.New(sctx.l) |
| 141 | var server func() error |
| 142 | onlyGRPC := splitHTTP && !sctx.httpOnly |
| 143 | onlyHTTP := splitHTTP && sctx.httpOnly |
| 144 | grpcEnabled := !onlyHTTP |
| 145 | httpEnabled := !onlyGRPC |
| 146 | |
| 147 | v3c := v3client.New(s) |
| 148 | servElection := v3election.NewElectionServer(v3c) |
| 149 | servLock := v3lock.NewLockServer(v3c) |
| 150 | |
| 151 | var gwmux *gw.ServeMux |
| 152 | if s.Cfg.EnableGRPCGateway { |
| 153 | // GRPC gateway connects to grpc server via connection provided by grpc dial. |
| 154 | gwmux, err = sctx.registerGateway(grpcDialForRestGatewayBackends) |
| 155 | if err != nil { |
| 156 | sctx.lg.Error("registerGateway failed", zap.Error(err)) |
| 157 | return err |
| 158 | } |
| 159 | } |
| 160 | var traffic string |
| 161 | switch { |
| 162 | case onlyGRPC: |
| 163 | traffic = "grpc" |
| 164 | case onlyHTTP: |
| 165 | traffic = "http" |
| 166 | default: |
| 167 | traffic = "grpc+http" |
| 168 | } |
| 169 | |
| 170 | if sctx.insecure { |
| 171 | var gs *grpc.Server |
| 172 | var srv *http.Server |
| 173 | if httpEnabled { |
| 174 | httpmux := sctx.createMux(gwmux, handler) |
| 175 | srv = &http.Server{ |
| 176 | Handler: createAccessController(sctx.lg, s, httpmux), |
| 177 | ErrorLog: logger, // do not log user error |
| 178 | } |
| 179 | if err = configureHTTPServer(srv, s.Cfg); err != nil { |
| 180 | sctx.lg.Error("Configure http server failed", zap.Error(err)) |
| 181 | return err |
| 182 | } |
| 183 | } |
| 184 | if grpcEnabled { |
| 185 | gs = v3rpc.Server(s, nil, nil, gopts...) |
| 186 | v3electionpb.RegisterElectionServer(gs, servElection) |
| 187 | v3lockpb.RegisterLockServer(gs, servLock) |
| 188 | if sctx.serviceRegister != nil { |
| 189 | sctx.serviceRegister(gs) |
| 190 | } |
| 191 | defer func(gs *grpc.Server) { |
| 192 | if err != nil { |
| 193 | sctx.lg.Warn("stopping insecure grpc server due to error", zap.Error(err)) |
| 194 | gs.Stop() |
| 195 | sctx.lg.Warn("stopped insecure grpc server due to error", zap.Error(err)) |
| 196 | } |
| 197 | }(gs) |
| 198 | } |
| 199 | if onlyGRPC { |
| 200 | server = func() error { |
| 201 | return gs.Serve(sctx.l) |
| 202 | } |
| 203 | } else { |
| 204 | server = m.Serve |
| 205 | |
| 206 | httpl := m.Match(cmux.HTTP1()) |
| 207 | sctx.startHandler(errHandler, func() error { |
| 208 | return srv.Serve(httpl) |
| 209 | }) |
| 210 | |
| 211 | if grpcEnabled { |
| 212 | grpcl := m.Match(cmux.HTTP2()) |
| 213 | sctx.startHandler(errHandler, func() error { |
| 214 | return gs.Serve(grpcl) |
| 215 | }) |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | sctx.serversC <- &servers{grpc: gs, http: srv} |
| 220 | sctx.lg.Info( |
| 221 | "serving client traffic insecurely; this is strongly discouraged!", |
| 222 | zap.String("traffic", traffic), |
| 223 | zap.String("address", sctx.l.Addr().String()), |
| 224 | ) |
| 225 | } |
| 226 | |
| 227 | if sctx.secure { |
| 228 | var gs *grpc.Server |
| 229 | var srv *http.Server |
| 230 | |
| 231 | tlscfg, tlsErr := tlsinfo.ServerConfig() |
| 232 | if tlsErr != nil { |
| 233 | return tlsErr |
| 234 | } |
| 235 | |
| 236 | if grpcEnabled { |
| 237 | gs = v3rpc.Server(s, tlscfg, nil, gopts...) |
| 238 | v3electionpb.RegisterElectionServer(gs, servElection) |
| 239 | v3lockpb.RegisterLockServer(gs, servLock) |
| 240 | if sctx.serviceRegister != nil { |
| 241 | sctx.serviceRegister(gs) |
| 242 | } |
| 243 | defer func(gs *grpc.Server) { |
| 244 | if err != nil { |
| 245 | sctx.lg.Warn("stopping secure grpc server due to error", zap.Error(err)) |
| 246 | gs.Stop() |
| 247 | sctx.lg.Warn("stopped secure grpc server due to error", zap.Error(err)) |
| 248 | } |
| 249 | }(gs) |
| 250 | } |
| 251 | if httpEnabled { |
| 252 | if grpcEnabled { |
| 253 | handler = grpcHandlerFunc(gs, handler) |
| 254 | } |
| 255 | httpmux := sctx.createMux(gwmux, handler) |
| 256 | |
| 257 | srv = &http.Server{ |
| 258 | Handler: createAccessController(sctx.lg, s, httpmux), |
| 259 | TLSConfig: tlscfg, |
| 260 | ErrorLog: logger, // do not log user error |
| 261 | } |
| 262 | if err = configureHTTPServer(srv, s.Cfg); err != nil { |
| 263 | sctx.lg.Error("Configure https server failed", zap.Error(err)) |
| 264 | return err |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | if onlyGRPC { |
| 269 | server = func() error { return gs.Serve(sctx.l) } |
| 270 | } else { |
| 271 | server = m.Serve |
| 272 | |
| 273 | tlsl, tlsErr := transport.NewTLSListener(m.Match(cmux.Any()), tlsinfo) |
| 274 | if tlsErr != nil { |
| 275 | return tlsErr |
| 276 | } |
| 277 | sctx.startHandler(errHandler, func() error { |
| 278 | return srv.Serve(tlsl) |
| 279 | }) |
| 280 | } |
| 281 | |
| 282 | sctx.serversC <- &servers{secure: true, grpc: gs, http: srv} |
| 283 | sctx.lg.Info( |
| 284 | "serving client traffic securely", |
| 285 | zap.String("traffic", traffic), |
| 286 | zap.String("address", sctx.l.Addr().String()), |
| 287 | ) |
| 288 | } |
| 289 | |
| 290 | err = server() |
| 291 | sctx.close() |
| 292 | sctx.wg.Wait() |
| 293 | return err |
| 294 | } |
| 295 | |
| 296 | func configureHTTPServer(srv *http.Server, cfg config.ServerConfig) error { |
| 297 | // todo (ahrtr): should we support configuring other parameters in the future as well? |
| 298 | return http2.ConfigureServer(srv, &http2.Server{ |
| 299 | MaxConcurrentStreams: cfg.MaxConcurrentStreams, |
| 300 | }) |
| 301 | } |
| 302 | |
| 303 | // grpcHandlerFunc returns an http.Handler that delegates to grpcServer on incoming gRPC |
| 304 | // connections or otherHandler otherwise. Given in gRPC docs. |
| 305 | func grpcHandlerFunc(grpcServer *grpc.Server, otherHandler http.Handler) http.Handler { |
| 306 | if otherHandler == nil { |
| 307 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 308 | grpcServer.ServeHTTP(w, r) |
| 309 | }) |
| 310 | } |
| 311 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
| 312 | if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") { |
| 313 | grpcServer.ServeHTTP(w, r) |
| 314 | } else { |
| 315 | otherHandler.ServeHTTP(w, r) |
| 316 | } |
| 317 | }) |
| 318 | } |
| 319 | |
| 320 | type registerHandlerFunc func(context.Context, *gw.ServeMux, *grpc.ClientConn) error |
| 321 | |
| 322 | func (sctx *serveCtx) registerGateway(dial func(ctx context.Context) (*grpc.ClientConn, error)) (*gw.ServeMux, error) { |
| 323 | ctx := sctx.ctx |
| 324 | |
| 325 | conn, err := dial(ctx) |
| 326 | if err != nil { |
| 327 | return nil, err |
| 328 | } |
| 329 | |
| 330 | // Refer to https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/customizing_your_gateway/ |
| 331 | gwmux := gw.NewServeMux( |
| 332 | gw.WithMarshalerOption(gw.MIMEWildcard, |
| 333 | &gw.HTTPBodyMarshaler{ |
| 334 | Marshaler: &gw.JSONPb{ |
| 335 | MarshalOptions: protojson.MarshalOptions{ |
| 336 | UseProtoNames: true, |
| 337 | EmitUnpopulated: false, |
| 338 | }, |
| 339 | UnmarshalOptions: protojson.UnmarshalOptions{ |
| 340 | DiscardUnknown: true, |
| 341 | }, |
| 342 | }, |
| 343 | }, |
| 344 | ), |
| 345 | ) |
| 346 | |
| 347 | handlers := []registerHandlerFunc{ |
| 348 | etcdservergw.RegisterKVHandler, |
| 349 | etcdservergw.RegisterWatchHandler, |
| 350 | etcdservergw.RegisterLeaseHandler, |
| 351 | etcdservergw.RegisterClusterHandler, |
| 352 | etcdservergw.RegisterMaintenanceHandler, |
| 353 | etcdservergw.RegisterAuthHandler, |
| 354 | v3lockgw.RegisterLockHandler, |
| 355 | v3electiongw.RegisterElectionHandler, |
| 356 | } |
| 357 | for _, h := range handlers { |
| 358 | if err := h(ctx, gwmux, conn); err != nil { |
| 359 | return nil, err |
| 360 | } |
| 361 | } |
| 362 | sctx.startHandler(nil, func() error { |
| 363 | <-ctx.Done() |
| 364 | if cerr := conn.Close(); cerr != nil { |
| 365 | sctx.lg.Warn( |
| 366 | "failed to close connection", |
| 367 | zap.String("address", sctx.l.Addr().String()), |
| 368 | zap.Error(cerr), |
| 369 | ) |
| 370 | } |
| 371 | return nil |
| 372 | }) |
| 373 | |
| 374 | return gwmux, nil |
| 375 | } |
| 376 | |
| 377 | type wsProxyZapLogger struct { |
| 378 | *zap.Logger |
| 379 | } |
| 380 | |
| 381 | func (w wsProxyZapLogger) Warnln(i ...any) { |
| 382 | w.Warn(fmt.Sprint(i...)) |
| 383 | } |
| 384 | |
| 385 | func (w wsProxyZapLogger) Debugln(i ...any) { |
| 386 | w.Debug(fmt.Sprint(i...)) |
| 387 | } |
| 388 | |
| 389 | func (sctx *serveCtx) createMux(gwmux *gw.ServeMux, handler http.Handler) *http.ServeMux { |
| 390 | httpmux := http.NewServeMux() |
| 391 | for path, h := range sctx.userHandlers { |
| 392 | httpmux.Handle(path, h) |
| 393 | } |
| 394 | |
| 395 | if gwmux != nil { |
| 396 | httpmux.Handle( |
| 397 | "/v3/", |
| 398 | wsproxy.WebsocketProxy( |
| 399 | gwmux, |
| 400 | wsproxy.WithRequestMutator( |
| 401 | // Default to the POST method for streams |
| 402 | func(_ *http.Request, outgoing *http.Request) *http.Request { |
| 403 | outgoing.Method = "POST" |
| 404 | return outgoing |
| 405 | }, |
| 406 | ), |
| 407 | wsproxy.WithMaxRespBodyBufferSize(0x7fffffff), |
| 408 | wsproxy.WithLogger(wsProxyZapLogger{sctx.lg}), |
| 409 | ), |
| 410 | ) |
| 411 | } |
| 412 | if handler != nil { |
| 413 | httpmux.Handle("/", handler) |
| 414 | } |
| 415 | return httpmux |
| 416 | } |
| 417 | |
| 418 | // createAccessController wraps HTTP multiplexer: |
| 419 | // - mutate gRPC gateway request paths |
| 420 | // - check hostname whitelist |
| 421 | // client HTTP requests goes here first |
| 422 | func createAccessController(lg *zap.Logger, s *etcdserver.EtcdServer, mux *http.ServeMux) http.Handler { |
| 423 | if lg == nil { |
| 424 | lg = zap.NewNop() |
| 425 | } |
| 426 | return &accessController{lg: lg, s: s, mux: mux} |
| 427 | } |
| 428 | |
| 429 | type accessController struct { |
| 430 | lg *zap.Logger |
| 431 | s *etcdserver.EtcdServer |
| 432 | mux *http.ServeMux |
| 433 | } |
| 434 | |
| 435 | func (ac *accessController) ServeHTTP(rw http.ResponseWriter, req *http.Request) { |
| 436 | if req == nil { |
| 437 | http.Error(rw, "Request is nil", http.StatusBadRequest) |
| 438 | return |
| 439 | } |
| 440 | // redirect for backward compatibilities |
| 441 | if req.URL != nil && strings.HasPrefix(req.URL.Path, "/v3beta/") { |
| 442 | req.URL.Path = strings.Replace(req.URL.Path, "/v3beta/", "/v3/", 1) |
| 443 | } |
| 444 | |
| 445 | if req.TLS == nil { // check origin if client connection is not secure |
| 446 | host := httputil.GetHostname(req) |
| 447 | if !ac.s.AccessController.IsHostWhitelisted(host) { |
| 448 | ac.lg.Warn( |
| 449 | "rejecting HTTP request to prevent DNS rebinding attacks", |
| 450 | zap.String("host", host), |
| 451 | ) |
| 452 | http.Error(rw, errCVE20185702(host), http.StatusMisdirectedRequest) |
| 453 | return |
| 454 | } |
| 455 | } else if ac.s.Cfg.ClientCertAuthEnabled && ac.s.Cfg.EnableGRPCGateway && |
| 456 | ac.s.AuthStore().IsAuthEnabled() && strings.HasPrefix(req.URL.Path, "/v3/") { |
| 457 | for _, chains := range req.TLS.VerifiedChains { |
| 458 | if len(chains) < 1 { |
| 459 | continue |
| 460 | } |
| 461 | if len(chains[0].Subject.CommonName) != 0 { |
| 462 | http.Error(rw, "CommonName of client sending a request against gateway will be ignored and not used as expected", http.StatusBadRequest) |
| 463 | return |
| 464 | } |
| 465 | } |
| 466 | } |
| 467 | |
| 468 | // Write CORS header. |
| 469 | if ac.s.AccessController.OriginAllowed("*") { |
| 470 | addCORSHeader(rw, "*") |
| 471 | } else if origin := req.Header.Get("Origin"); ac.s.OriginAllowed(origin) { |
| 472 | addCORSHeader(rw, origin) |
| 473 | } |
| 474 | |
| 475 | if req.Method == http.MethodOptions { |
| 476 | rw.WriteHeader(http.StatusOK) |
| 477 | return |
| 478 | } |
| 479 | |
| 480 | ac.mux.ServeHTTP(rw, req) |
| 481 | } |
| 482 | |
| 483 | // addCORSHeader adds the correct cors headers given an origin |
| 484 | func addCORSHeader(w http.ResponseWriter, origin string) { |
| 485 | w.Header().Add("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE") |
| 486 | w.Header().Add("Access-Control-Allow-Origin", origin) |
| 487 | w.Header().Add("Access-Control-Allow-Headers", "accept, content-type, authorization") |
| 488 | } |
| 489 | |
| 490 | // https://github.com/transmission/transmission/pull/468 |
| 491 | func errCVE20185702(host string) string { |
| 492 | return fmt.Sprintf(` |
| 493 | etcd received your request, but the Host header was unrecognized. |
| 494 | |
| 495 | To fix this, choose one of the following options: |
| 496 | - Enable TLS, then any HTTPS request will be allowed. |
| 497 | - Add the hostname you want to use to the whitelist in settings. |
| 498 | - e.g. etcd --host-whitelist %q |
| 499 | |
| 500 | This requirement has been added to help prevent "DNS Rebinding" attacks (CVE-2018-5702). |
| 501 | `, host) |
| 502 | } |
| 503 | |
| 504 | // WrapCORS wraps existing handler with CORS. |
| 505 | // TODO: deprecate this after v2 proxy deprecate |
| 506 | func WrapCORS(cors map[string]struct{}, h http.Handler) http.Handler { |
| 507 | return &corsHandler{ |
| 508 | ac: &etcdserver.AccessController{CORS: cors}, |
| 509 | h: h, |
| 510 | } |
| 511 | } |
| 512 | |
| 513 | type corsHandler struct { |
| 514 | ac *etcdserver.AccessController |
| 515 | h http.Handler |
| 516 | } |
| 517 | |
| 518 | func (ch *corsHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { |
| 519 | if ch.ac.OriginAllowed("*") { |
| 520 | addCORSHeader(rw, "*") |
| 521 | } else if origin := req.Header.Get("Origin"); ch.ac.OriginAllowed(origin) { |
| 522 | addCORSHeader(rw, origin) |
| 523 | } |
| 524 | |
| 525 | if req.Method == http.MethodOptions { |
| 526 | rw.WriteHeader(http.StatusOK) |
| 527 | return |
| 528 | } |
| 529 | |
| 530 | ch.h.ServeHTTP(rw, req) |
| 531 | } |
| 532 | |
| 533 | func (sctx *serveCtx) registerUserHandler(s string, h http.Handler) { |
| 534 | if sctx.userHandlers[s] != nil { |
| 535 | sctx.lg.Warn("path is already registered by user handler", zap.String("path", s)) |
| 536 | return |
| 537 | } |
| 538 | sctx.userHandlers[s] = h |
| 539 | } |
| 540 | |
| 541 | func (sctx *serveCtx) registerPprof() { |
| 542 | for p, h := range debugutil.PProfHandlers() { |
| 543 | sctx.registerUserHandler(p, h) |
| 544 | } |
| 545 | } |
| 546 | |
| 547 | func (sctx *serveCtx) registerTrace() { |
| 548 | reqf := func(w http.ResponseWriter, r *http.Request) { trace.Render(w, r, true) } |
| 549 | sctx.registerUserHandler("/debug/requests", http.HandlerFunc(reqf)) |
| 550 | evf := func(w http.ResponseWriter, r *http.Request) { trace.RenderEvents(w, r, true) } |
| 551 | sctx.registerUserHandler("/debug/events", http.HandlerFunc(evf)) |
| 552 | } |
| 553 | |
| 554 | func (sctx *serveCtx) close() { |
| 555 | sctx.closeOnce.Do(func() { |
| 556 | close(sctx.serversC) |
| 557 | }) |
| 558 | } |