Capstan is a Golang web framework that shares some similarities with others in its segment.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

244 lines
6.9KB

  1. package capstan
  2. import (
  3. "bytes"
  4. "net/http"
  5. "net/url"
  6. "strings"
  7. . "git.destrealm.org/go/capstan/errors"
  8. "git.destrealm.org/go/capstan/internal/api"
  9. "github.com/go-chi/chi"
  10. )
  11. // ProxyHandler is the interface that must be implemented by types that intend
  12. // to be used as proxy handlers. Proxy handlers may be comparatively simple,
  13. // such as the `proxy` type, or they may be more complex and implement
  14. // host-lookup functionality such as the `multiAppProxy` type.
  15. //
  16. // Proxies are required in order to support rebinding and endpoint deletion
  17. // since go-chi doesn't currently allow us to overwrite or delete endpoints
  18. // directly. So, what we do instead, is to regenerate the go-chi bindings when a
  19. // rebind or endpoint deletion is requested, call Switch() on the proxy, and
  20. // "switch" to the new chi.Router.
  21. //
  22. // Multiple proxies are arranged in a hierarchical structure, such as for
  23. // multiapp support. In this case, the multiapp proxy handles dispatching
  24. // requests based on the incoming domain, path, or domain + path, and then
  25. // passes it to the underlying `proxy` which performs the rest of the work. This
  26. // allows us to Switch() on a per-application bases, as required, while still
  27. // supporting multiple applications within the same Capstan-hosted instance.
  28. type ProxyHandler interface {
  29. // ServeHTTP allows ProxyHandler to implement http.Handler.
  30. ServeHTTP(http.ResponseWriter, *http.Request)
  31. // Switch the current router to a new router instance.
  32. Switch(chi.Router)
  33. }
  34. // MultiAppProxyHandler defines the interface to expose for types supporting
  35. // multi-application loading.
  36. type MultiAppProxyHandler interface {
  37. ProxyHandler
  38. Loader() api.ApplicationLoader
  39. }
  40. // The router proxy provides an intermediary between go-chi's handler and
  41. // Capstan. This allows us to rebind or rename mount points by reinitializing
  42. // the proxy's internal pointer to the new muxer.
  43. type proxy struct {
  44. Hostname string
  45. FullHost string
  46. EnforceHost bool
  47. EnforcePort bool
  48. MaxRequestDuration int
  49. router *Router
  50. mux *chi.Mux
  51. }
  52. // `proxy` type configuration options.
  53. type proxyOptions struct {
  54. ListenAddr string
  55. Hostname string
  56. EnforceHost bool
  57. EnforcePort bool
  58. MaxRequestDuration int
  59. }
  60. // Returns a new proxy object with its router and mux unset.
  61. //
  62. // TODO: Replace or add feature for using a radix trie to determine the
  63. // appropriate muxer for a given host name. Or, optionally, implement a
  64. // different proxy type when multiple host names are configured/requested.
  65. func newProxy(options *proxyOptions) *proxy {
  66. prx := &proxy{
  67. Hostname: options.Hostname,
  68. EnforceHost: options.EnforceHost,
  69. EnforcePort: options.EnforcePort,
  70. MaxRequestDuration: options.MaxRequestDuration,
  71. }
  72. if options.EnforcePort && strings.IndexRune(options.Hostname, ':') == -1 {
  73. prx.FullHost = options.Hostname + options.ListenAddr[strings.IndexRune(options.ListenAddr, ':'):]
  74. }
  75. return prx
  76. }
  77. func (d *proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  78. // Host enforcement is not enabled.
  79. if !d.EnforceHost {
  80. d.mux.ServeHTTP(w, r)
  81. return
  82. }
  83. host := r.Host
  84. if !d.EnforcePort {
  85. if i := strings.IndexRune(host, ':'); i >= 0 {
  86. host = host[:i]
  87. }
  88. }
  89. if bytes.Compare([]byte(d.Hostname), []byte(host)) == 0 {
  90. d.mux.ServeHTTP(w, r)
  91. return
  92. }
  93. w.WriteHeader(404)
  94. }
  95. func (d *proxy) Switch(router chi.Router) {
  96. if mux, ok := router.(*chi.Mux); ok {
  97. d.mux = mux
  98. }
  99. }
  100. type multiAppProxy struct {
  101. loader api.ApplicationLoader
  102. }
  103. func NewMultiAppProxy(loader api.ApplicationLoader) *multiAppProxy {
  104. return &multiAppProxy{loader}
  105. }
  106. func (m *multiAppProxy) Loader() api.ApplicationLoader {
  107. return m.loader
  108. }
  109. func (m *multiAppProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  110. // TODO: Support host + bind path. Requires loader support.
  111. uri := url.URL{
  112. Scheme: r.URL.Scheme,
  113. Opaque: r.URL.Opaque,
  114. Host: r.Host,
  115. Path: r.URL.Path,
  116. RawPath: r.URL.RawPath,
  117. }
  118. r.URL.Host = r.Host
  119. if loader, ok := m.loader.Handler(uri); ok {
  120. loader.ServeHTTP(w, r)
  121. return
  122. }
  123. m.loader.DefaultHandler().ServeHTTP(w, r)
  124. }
  125. func (m *multiAppProxy) Switch(router chi.Router) {}
  126. // mappedLoader is a map-backed loader for multi-application support.
  127. type mappedLoader struct {
  128. def http.Handler
  129. mapper map[string]http.Handler
  130. }
  131. // NewMappedLoader returns a map-backed loader for multi-application support.
  132. // Map-backed loaders can only map hostnames to handlers and cannot map base
  133. // paths.
  134. //
  135. // The application loader interface is defined as an internal API interface (see
  136. // internal/api/application.go for ApplicationLoader).
  137. func NewMappedLoader(def http.Handler) *mappedLoader {
  138. return &mappedLoader{
  139. def: def,
  140. mapper: make(map[string]http.Handler),
  141. }
  142. }
  143. func (m *mappedLoader) AddApplicationHandler(uri string, handler http.Handler) error {
  144. _, ok := m.mapper[uri]
  145. if !ok {
  146. m.mapper[uri] = handler
  147. return nil
  148. }
  149. return ErrAppHandlerAlreadyAdded
  150. }
  151. func (m *mappedLoader) Handler(uri url.URL) (http.Handler, bool) {
  152. handler, ok := m.mapper[uri.Host]
  153. return handler, ok
  154. }
  155. func (m *mappedLoader) DefaultHandler() http.Handler {
  156. return m.def
  157. }
  158. type radixLoader struct {
  159. trie *proxyTrie
  160. def http.Handler
  161. }
  162. // NewRadixLoader returns a loader backed by a radix trie providing longest
  163. // matching prefix support. If you need to match both the hostname and the base
  164. // path for routing incoming requests per-application, use this loader.
  165. //
  166. // Be aware that there are some limitations with assigning handlers via longest
  167. // matching prefixes. In particular, the radix loader is NOT path aware, meaning
  168. // that a request containing the hostname + base path assignment of
  169. // "example.com/store" will, by its nature, also match "example.com/stores" and
  170. // any derivative thereafter.
  171. //
  172. // The intent behind this loader is to match the first hostname + base path
  173. // segment of the incoming request, pass it along to the assigned application,
  174. // and allow that application to determine whether the request should be handled
  175. // or an error should be returned.
  176. func NewRadixLoader(def http.Handler) *radixLoader {
  177. return &radixLoader{
  178. trie: NewProxyTrie(),
  179. def: def,
  180. }
  181. }
  182. func (r *radixLoader) AddApplicationHandler(uri string, handler http.Handler) error {
  183. p, err := url.Parse("url://" + uri)
  184. if err != nil {
  185. return err
  186. }
  187. uri = p.String()[6:]
  188. if p.Path == "/" {
  189. uri = uri[:len(uri)-1]
  190. }
  191. if h := r.trie.Lookup(uri); h != nil {
  192. r.trie.Overwrite(uri, handler)
  193. }
  194. r.trie.Insert(uri, handler)
  195. return nil
  196. }
  197. func (r *radixLoader) Handler(uri url.URL) (http.Handler, bool) {
  198. u := uri.String()[2:]
  199. // We don't include the trailing slash here since attached handlers should
  200. // not include it either.
  201. if strings.HasSuffix(u, "/") {
  202. u = u[:len(u)-1]
  203. }
  204. handler := r.trie.LongestMatch(u)
  205. if handler == nil {
  206. return nil, false
  207. }
  208. return handler, true
  209. }
  210. func (r *radixLoader) DefaultHandler() http.Handler {
  211. return r.def
  212. }