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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"fmt"
"net/http"
"sync"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/server/router"
)
// 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 {
p.mu.Lock()
defer p.mu.Unlock()
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 {
p.mu.Lock()
defer p.mu.Unlock()
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)
}
})
}