go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/port.go (about)

     1  // Copyright 2020 The LUCI 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 server
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net"
    21  	"net/http"
    22  	"sync"
    23  
    24  	"golang.org/x/net/http2"
    25  	"golang.org/x/net/http2/h2c"
    26  
    27  	"go.chromium.org/luci/common/errors"
    28  
    29  	"go.chromium.org/luci/server/router"
    30  )
    31  
    32  // PortOptions is a configuration of a single serving HTTP port.
    33  //
    34  // See Server's AddPort.
    35  type PortOptions struct {
    36  	Name           string // optional logical name of the port for logs
    37  	ListenAddr     string // local address to bind to or "-" for a dummy port
    38  	DisableMetrics bool   // do not collect HTTP metrics for requests on this port
    39  }
    40  
    41  // Port is returned by Server's AddPort and used to setup the request routing.
    42  //
    43  // It represents an HTTP port with a request router.
    44  type Port struct {
    45  	// Routes is a router for requests hitting this port.
    46  	//
    47  	// This router is used for all requests whose Host header does not match any
    48  	// specially registered per-host routers (see VirtualHost). Normally, there
    49  	// are no such per-host routers, so usually Routes is used for all requests.
    50  	//
    51  	// Should be populated before Server's Serve loop.
    52  	Routes *router.Router
    53  
    54  	parent   *Server      // the owning server
    55  	opts     PortOptions  // options passed to AddPort
    56  	allowH2C bool         // if set allow HTTP/2 Cleartext requests
    57  	listener net.Listener // listening socket if ListenAddr != "-"
    58  
    59  	mu      sync.Mutex
    60  	srv     *http.Server              // lazy-initialized in httpServer()
    61  	perHost map[string]*router.Router // routers added in VirtualHost(...)
    62  }
    63  
    64  // VirtualHost returns a router (registering it if necessary) used for requests
    65  // that have the given Host header.
    66  //
    67  // Note that requests that match some registered virtual host router won't
    68  // reach the default router (port.Routes), even if the virtual host router
    69  // doesn't have a route for them. Such requests finish with HTTP 404.
    70  //
    71  // Should be called before Server's Serve loop (panics otherwise).
    72  func (p *Port) VirtualHost(host string) *router.Router {
    73  	p.mu.Lock()
    74  	defer p.mu.Unlock()
    75  
    76  	if p.srv != nil {
    77  		p.parent.Fatal(errors.Reason("the server has already been started").Err())
    78  	}
    79  
    80  	r := p.perHost[host]
    81  	if r == nil {
    82  		r = p.parent.newRouter(p.opts)
    83  		if p.perHost == nil {
    84  			p.perHost = make(map[string]*router.Router, 1)
    85  		}
    86  		p.perHost[host] = r
    87  	}
    88  
    89  	return r
    90  }
    91  
    92  // nameForLog returns a string to identify this port in the server logs.
    93  //
    94  // Part of the servingPort interface.
    95  func (p *Port) nameForLog() string {
    96  	var pfx string
    97  	if p.listener != nil {
    98  		pfx = "http://" + p.listener.Addr().String()
    99  		if p.allowH2C {
   100  			pfx += " (h2c on)"
   101  		}
   102  	} else {
   103  		pfx = "-"
   104  	}
   105  	if p.opts.Name != "" {
   106  		return fmt.Sprintf("%s [%s]", pfx, p.opts.Name)
   107  	}
   108  	return pfx
   109  }
   110  
   111  // serve runs the serving loop until it is gracefully stopped.
   112  //
   113  // Part of the servingPort interface.
   114  func (p *Port) serve(baseCtx func() context.Context) error {
   115  	srv := p.httpServer()
   116  	srv.BaseContext = func(net.Listener) context.Context { return baseCtx() }
   117  	err := srv.Serve(p.listener)
   118  	if err != nil && err != http.ErrServerClosed {
   119  		return err
   120  	}
   121  	return nil
   122  }
   123  
   124  // shutdown gracefully stops the server, blocking until it is closed.
   125  //
   126  // Does nothing if the server is not running.
   127  //
   128  // Part of the servingPort interface.
   129  func (p *Port) shutdown(ctx context.Context) {
   130  	_ = p.httpServer().Shutdown(ctx)
   131  }
   132  
   133  // httpServer lazy-initializes and returns http.Server for this port.
   134  //
   135  // Once this function is called, no more virtual hosts can be added to the
   136  // server (an attempt to do so causes a panic).
   137  func (p *Port) httpServer() *http.Server {
   138  	p.mu.Lock()
   139  	defer p.mu.Unlock()
   140  
   141  	if p.listener == nil {
   142  		panic("httpServer must not be used with uninitialized ports")
   143  	}
   144  
   145  	if p.srv == nil {
   146  		p.srv = &http.Server{
   147  			Addr:     p.listener.Addr().String(),
   148  			Handler:  p.initHandlerLocked(),
   149  			ErrorLog: nil, // TODO(vadimsh): Log via 'logging' package.
   150  		}
   151  		// See https://pkg.go.dev/golang.org/x/net/http2/h2c.
   152  		if p.allowH2C {
   153  			p.srv.Handler = h2c.NewHandler(p.srv.Handler, &http2.Server{})
   154  		}
   155  	}
   156  	return p.srv
   157  }
   158  
   159  // initHandlerLocked initializes the top-level router for the server.
   160  func (p *Port) initHandlerLocked() http.Handler {
   161  	// These are immutable once the server has started, so its fine to copy them
   162  	// by pointer and use without any locking.
   163  	mapping := p.perHost
   164  	fallback := p.Routes
   165  
   166  	if len(mapping) == 0 {
   167  		// No need for an extra layer of per-host routing at all.
   168  		return p.parent.wrapHTTPHandler(fallback)
   169  	}
   170  
   171  	return p.parent.wrapHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
   172  		if router, ok := mapping[r.Host]; ok {
   173  			router.ServeHTTP(rw, r)
   174  		} else {
   175  			fallback.ServeHTTP(rw, r)
   176  		}
   177  	}))
   178  }