| // 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) |
| } |
| }) |
| } |