|
- package capstan
-
- import (
- "bytes"
- "net/http"
- "net/url"
- "strings"
-
- . "git.destrealm.org/go/capstan/errors"
- "git.destrealm.org/go/capstan/internal/api"
- "github.com/go-chi/chi"
- )
-
- // ProxyHandler is the interface that must be implemented by types that intend
- // to be used as proxy handlers. Proxy handlers may be comparatively simple,
- // such as the `proxy` type, or they may be more complex and implement
- // host-lookup functionality such as the `multiAppProxy` type.
- //
- // Proxies are required in order to support rebinding and endpoint deletion
- // since go-chi doesn't currently allow us to overwrite or delete endpoints
- // directly. So, what we do instead, is to regenerate the go-chi bindings when a
- // rebind or endpoint deletion is requested, call Switch() on the proxy, and
- // "switch" to the new chi.Router.
- //
- // Multiple proxies are arranged in a hierarchical structure, such as for
- // multiapp support. In this case, the multiapp proxy handles dispatching
- // requests based on the incoming domain, path, or domain + path, and then
- // passes it to the underlying `proxy` which performs the rest of the work. This
- // allows us to Switch() on a per-application bases, as required, while still
- // supporting multiple applications within the same Capstan-hosted instance.
- type ProxyHandler interface {
- // ServeHTTP allows ProxyHandler to implement http.Handler.
- ServeHTTP(http.ResponseWriter, *http.Request)
-
- // Switch the current router to a new router instance.
- Switch(chi.Router)
- }
-
- // MultiAppProxyHandler defines the interface to expose for types supporting
- // multi-application loading.
- type MultiAppProxyHandler interface {
- ProxyHandler
- Loader() api.ApplicationLoader
- }
-
- // The router proxy provides an intermediary between go-chi's handler and
- // Capstan. This allows us to rebind or rename mount points by reinitializing
- // the proxy's internal pointer to the new muxer.
- type proxy struct {
- Hostname string
- FullHost string
- EnforceHost bool
- EnforcePort bool
- MaxRequestDuration int
- router *Router
- mux *chi.Mux
- }
-
- // `proxy` type configuration options.
- type proxyOptions struct {
- ListenAddr string
- Hostname string
- EnforceHost bool
- EnforcePort bool
- MaxRequestDuration int
- }
-
- // Returns a new proxy object with its router and mux unset.
- //
- // TODO: Replace or add feature for using a radix trie to determine the
- // appropriate muxer for a given host name. Or, optionally, implement a
- // different proxy type when multiple host names are configured/requested.
- func newProxy(options *proxyOptions) *proxy {
- prx := &proxy{
- Hostname: options.Hostname,
- EnforceHost: options.EnforceHost,
- EnforcePort: options.EnforcePort,
- MaxRequestDuration: options.MaxRequestDuration,
- }
- if options.EnforcePort && strings.IndexRune(options.Hostname, ':') == -1 {
- prx.FullHost = options.Hostname + options.ListenAddr[strings.IndexRune(options.ListenAddr, ':'):]
- }
- return prx
- }
-
- func (d *proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- // Host enforcement is not enabled.
- if !d.EnforceHost {
- d.mux.ServeHTTP(w, r)
- return
- }
-
- host := r.Host
- if !d.EnforcePort {
- if i := strings.IndexRune(host, ':'); i >= 0 {
- host = host[:i]
- }
- }
-
- if bytes.Compare([]byte(d.Hostname), []byte(host)) == 0 {
- d.mux.ServeHTTP(w, r)
- return
- }
- w.WriteHeader(404)
- }
-
- func (d *proxy) Switch(router chi.Router) {
- if mux, ok := router.(*chi.Mux); ok {
- d.mux = mux
- }
- }
-
- type multiAppProxy struct {
- loader api.ApplicationLoader
- }
-
- func NewMultiAppProxy(loader api.ApplicationLoader) *multiAppProxy {
- return &multiAppProxy{loader}
- }
-
- func (m *multiAppProxy) Loader() api.ApplicationLoader {
- return m.loader
- }
-
- func (m *multiAppProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- // TODO: Support host + bind path. Requires loader support.
- uri := url.URL{
- Scheme: r.URL.Scheme,
- Opaque: r.URL.Opaque,
- Host: r.Host,
- Path: r.URL.Path,
- RawPath: r.URL.RawPath,
- }
- r.URL.Host = r.Host
- if loader, ok := m.loader.Handler(uri); ok {
- loader.ServeHTTP(w, r)
- return
- }
-
- m.loader.DefaultHandler().ServeHTTP(w, r)
- }
-
- func (m *multiAppProxy) Switch(router chi.Router) {}
-
- // mappedLoader is a map-backed loader for multi-application support.
- type mappedLoader struct {
- def http.Handler
- mapper map[string]http.Handler
- }
-
- // NewMappedLoader returns a map-backed loader for multi-application support.
- // Map-backed loaders can only map hostnames to handlers and cannot map base
- // paths.
- //
- // The application loader interface is defined as an internal API interface (see
- // internal/api/application.go for ApplicationLoader).
- func NewMappedLoader(def http.Handler) *mappedLoader {
- return &mappedLoader{
- def: def,
- mapper: make(map[string]http.Handler),
- }
- }
-
- func (m *mappedLoader) AddApplicationHandler(uri string, handler http.Handler) error {
- _, ok := m.mapper[uri]
- if !ok {
- m.mapper[uri] = handler
- return nil
- }
- return ErrAppHandlerAlreadyAdded
- }
-
- func (m *mappedLoader) Handler(uri url.URL) (http.Handler, bool) {
- handler, ok := m.mapper[uri.Host]
- return handler, ok
- }
-
- func (m *mappedLoader) DefaultHandler() http.Handler {
- return m.def
- }
-
- type radixLoader struct {
- trie *proxyTrie
- def http.Handler
- }
-
- // NewRadixLoader returns a loader backed by a radix trie providing longest
- // matching prefix support. If you need to match both the hostname and the base
- // path for routing incoming requests per-application, use this loader.
- //
- // Be aware that there are some limitations with assigning handlers via longest
- // matching prefixes. In particular, the radix loader is NOT path aware, meaning
- // that a request containing the hostname + base path assignment of
- // "example.com/store" will, by its nature, also match "example.com/stores" and
- // any derivative thereafter.
- //
- // The intent behind this loader is to match the first hostname + base path
- // segment of the incoming request, pass it along to the assigned application,
- // and allow that application to determine whether the request should be handled
- // or an error should be returned.
- func NewRadixLoader(def http.Handler) *radixLoader {
- return &radixLoader{
- trie: NewProxyTrie(),
- def: def,
- }
- }
-
- func (r *radixLoader) AddApplicationHandler(uri string, handler http.Handler) error {
- p, err := url.Parse("url://" + uri)
- if err != nil {
- return err
- }
- uri = p.String()[6:]
- if p.Path == "/" {
- uri = uri[:len(uri)-1]
- }
- if h := r.trie.Lookup(uri); h != nil {
- r.trie.Overwrite(uri, handler)
- }
- r.trie.Insert(uri, handler)
- return nil
- }
-
- func (r *radixLoader) Handler(uri url.URL) (http.Handler, bool) {
- u := uri.String()[2:]
-
- // We don't include the trailing slash here since attached handlers should
- // not include it either.
- if strings.HasSuffix(u, "/") {
- u = u[:len(u)-1]
- }
-
- handler := r.trie.LongestMatch(u)
- if handler == nil {
- return nil, false
- }
-
- return handler, true
- }
-
- func (r *radixLoader) DefaultHandler() http.Handler {
- return r.def
- }
|