blob: 8bdca4a6f9078c215330e2bef4d8ab9b63d824a8 [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
// PortOptions is a configuration of a single serving HTTP port.
// See Server's AddPort.
type PortOptions struct {
Name string // optional logical name of the port for logs
ListenAddr string // local address to bind to or "-" for a dummy port
DisableMetrics bool // do not collect HTTP metrics for requests on this port
// Port is returned by Server's AddPort and used to setup the request routing.
type Port struct {
// Routes is a router for requests hitting this port.
// This router is used for all requests whose Host header does not match any
// specially registered per-host routers (see VirtualHost). Normally, there
// are no such per-host routers, so usually Routes is used for all requests.
// Should be populated before Server's ListenAndServe call.
Routes *router.Router
parent *Server // the owning server
opts PortOptions // options passed to AddPort
mu sync.Mutex
srv *http.Server // lazy-initialized in httpServer()
perHost map[string]*router.Router // routers added in VirtualHost(...)
// VirtualHost returns a router (registering it if necessary) used for requests
// that have the given Host header.
// Note that requests that match some registered virtual host router won't
// reach the default router (port.Routes), even if the virtual host router
// doesn't have a route for them. Such requests finish with HTTP 404.
// Should be called before Server's ListenAndServe (panics otherwise).
func (p *Port) VirtualHost(host string) *router.Router {
if p.srv != nil {
p.parent.Fatal(errors.Reason("the server has already been started").Err())
r := p.perHost[host]
if r == nil {
r = p.parent.newRouter(p.opts)
if p.perHost == nil {
p.perHost = make(map[string]*router.Router, 1)
p.perHost[host] = r
return r
// nameForLog returns a string to identify this port in the server logs.
func (p *Port) nameForLog() string {
var pfx string
if p.opts.ListenAddr == "-" {
pfx = "-"
} else {
pfx = "http://" + p.opts.ListenAddr
if p.opts.Name != "" {
return fmt.Sprintf("%s [%s]", pfx, p.opts.Name)
return pfx
// httpServer lazy-initializes and returns http.Server for this port.
// Called from Server's ListenAndServe.
// Once this function is called, no more virtual hosts can be added to the
// server (an attempt to do so causes a panic).
func (p *Port) httpServer() *http.Server {
if p.opts.ListenAddr == "-" {
panic("httpServer must not be used with dummy ports")
if p.srv == nil {
p.srv = &http.Server{
Addr: p.opts.ListenAddr,
Handler: p.initHandlerLocked(),
ErrorLog: nil, // TODO(vadimsh): Log via 'logging' package.
return p.srv
// initHandlerLocked initializes the top-level router for the server.
func (p *Port) initHandlerLocked() http.Handler {
// These are immutable once the server has started, so its fine to copy them
// by pointer and use without any locking.
mapping := p.perHost
fallback := p.Routes
if len(mapping) == 0 {
return fallback // no need for an extra layer of per-host routing at all
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if router, ok := mapping[r.Host]; ok {
router.ServeHTTP(rw, r)
} else {
fallback.ServeHTTP(rw, r)