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.

408 lines
12KB

  1. package capstan
  2. import (
  3. "context"
  4. "net/http"
  5. "strconv"
  6. "strings"
  7. "time"
  8. ce "git.destrealm.org/go/capstan/errors"
  9. "git.destrealm.org/go/capstan/render"
  10. "git.destrealm.org/go/capstan/status"
  11. "git.destrealm.org/go/errors"
  12. "git.destrealm.org/go/logging"
  13. "github.com/gorilla/websocket"
  14. )
  15. // Route descriptor ultimately used for constructing chi routes. This
  16. // encapsulates the actual user code handler and passes in the appropriate
  17. // arguments.
  18. type Route struct {
  19. router *Router
  20. special string
  21. // Name of this handler. This is a symbolic name
  22. Name string
  23. // Path derived from route descriptor.
  24. Path string
  25. // CapPath contains the original Capstan-formatted path for this route.
  26. CapPath string
  27. // BasePath as indicated by the router. This is used by the URL builder but
  28. // may provide informational context to controllers indicating that this
  29. // route isn't mounted at the site root.
  30. BasePath string
  31. // CapBasePath is similar to BasePath with the exception that it contains
  32. // Capstan-formatted path information.
  33. CapBasePath string
  34. // MandatoryNoSlash strips any trailing slash from the route and creates a
  35. // single route with no trailing slash. This enforces the absense of a
  36. // slash. No redirection may occur from routes with a trailing slash to
  37. // routes without a slash when flagged with this feature.
  38. //
  39. // Mandatory absense of a slash is enforced when the route is defined with a
  40. // trailing exclamation point, e.g. "/route!". Such routes may also include
  41. // a trailing slash to ensure the meaning is clearer (e.g. "not slash" or
  42. // "/route/!").
  43. MandatoryNoSlash bool
  44. // MandatorySlash is the precise opposite of MandatoryNoSlash: Routes are
  45. // required to terminate with a trailing slash and no redirection may occur
  46. // from routes without a slash.
  47. //
  48. // Routes with a mandatory slash must terminate with a trailing slash, e.g.
  49. // "/route/".
  50. MandatorySlash bool
  51. // OptionalSlash creates two separate routing table entries: One with a
  52. // trailing slash and one without. If the route ends with a slash, this is
  53. // considered the route's default state, and requests to this route without
  54. // a trailing slash will be redirected to a route of the same name with the
  55. // slash appended. Likewise, the converse is true.
  56. //
  57. // Optional slashes may be delineated with a terminating question mark, e.g.
  58. // "/route/?" for routes that will redirect to a trailing slash or "/route?"
  59. // for those that do not. The question mark itself is optional for routes
  60. // defined without a trailing slash and may be omitted, e.g., "/route" and
  61. // "/route?" define the same behavior. The question mark is advisable to
  62. // make clear the developer's intention, and may be required in some
  63. // contexts (such as defining the behavior of an index route).
  64. //
  65. // Note: A trailing slash, e.g. "/route/" indicates that the route must
  66. // terminate with a *mandatory* slash. Hence, a terminating ? is required
  67. // when defining a default redirection state from "/route" to "/route/",
  68. // e.g. "/route/?".
  69. //
  70. // This section will be clarified in the documentation.
  71. OptionalSlash bool
  72. // SlashIsDefault indicates that the default route state is to terminate
  73. // with a slash. The value of this flag is only applicable if OptionalSlash
  74. // is true.
  75. SlashIsDefault bool
  76. // RouteTimeout indicates this route should enforce a timeout of the
  77. // specified duration, in seconds. If ForceTimeout is true, the handler will
  78. // exit after RouteTimeout seconds. If ForceTimeout is false, then timeout
  79. // handling must be managed by the controller (and will be ignored
  80. // otherwise).
  81. RouteTimeout int
  82. // ForceTimeout will forcibly exit the handler after RouteTimeout seconds or
  83. // do nothing until the controller endpoint managed by this route exists of
  84. // its own volition.
  85. ForceTimeout bool
  86. // Paths for multiple routes that may be handled by the same controller.
  87. Paths map[string][]string
  88. // ContentType is a convenience mechanism for setting the content type of
  89. // the route if it is one of text/plain, text/json, or application/json. You
  90. // should use this instead of setting the headers directly.
  91. //
  92. // This value is mostly intended for API endpoints that generate JSON
  93. // output.
  94. ContentType string
  95. // Headers used as route defaults.
  96. Headers http.Header
  97. // Prefix to prepend to the handler's name to limit naming collisions when
  98. // reverse-mapping URLs.
  99. Prefix string
  100. // Suffix to append to the handler's name. This will typically be the
  101. // request method but may be overridden here. If this value isn't set, the
  102. // request method is used instead.
  103. Suffix string
  104. // ParamTypes maps parameter names to their type for use by *Param() context
  105. // functions.
  106. ParamTypes map[string]string
  107. // Method associated with this route.
  108. Method string
  109. // Middleware for this route.
  110. //
  111. // TODO: Eventually support redeclaration of middleware types such that it
  112. // accepts a Context. This will require also wrapping external middleware,
  113. // such as that which ships with chi and others.
  114. Middleware []func(http.Handler) http.Handler
  115. // Endpoint function.
  116. Endpoint Endpoint
  117. // Controller reference.
  118. Controller Controller
  119. // Upgrader for websocket connections.
  120. Upgrader *websocket.Upgrader
  121. // BeforeResponse call. If defined by the router, this function will be
  122. // called before any response is issued. If `error` contains a 400- or
  123. // 500-level error, the response is aborted.
  124. //
  125. // This will be nil unless defined by the controller.
  126. BeforeResponse []func(Context, *Route) error
  127. // AfterResponse call. If defined by the router, this function will be
  128. // called after every response made to this route. If `error` contains a
  129. // 400- or 500-level error, its value is returned instead of the value
  130. // returned by the route's handler.
  131. //
  132. // AfterResponse will also receive the error returned by Route.Endpoint.
  133. //
  134. // This will be nil unless defined by the controller.
  135. AfterResponse []func(Context, error) error
  136. // Renderer defines a controller-specific renderer to use for this handler.
  137. // If this is nil, the global renderer will be used instead.
  138. Renderer render.Renderer
  139. // Context generation function. This is set by the router.
  140. contextMaker ContextMaker
  141. }
  142. // Copy and return a new instance of the current route.
  143. //
  144. // Beware: Non-value types are only copied as pointers to their original values.
  145. // This means that maps local to the Route struct are not copied by value.
  146. func (rt *Route) Copy() *Route {
  147. return &Route{
  148. router: rt.router,
  149. Name: rt.Name,
  150. Path: rt.Path,
  151. CapPath: rt.CapPath,
  152. BasePath: rt.BasePath,
  153. MandatoryNoSlash: rt.MandatoryNoSlash,
  154. MandatorySlash: rt.MandatorySlash,
  155. OptionalSlash: rt.OptionalSlash,
  156. SlashIsDefault: rt.SlashIsDefault,
  157. RouteTimeout: rt.RouteTimeout,
  158. ForceTimeout: rt.ForceTimeout,
  159. Paths: rt.Paths,
  160. ContentType: rt.ContentType,
  161. Headers: rt.Headers,
  162. Prefix: rt.Prefix,
  163. Suffix: rt.Suffix,
  164. ParamTypes: rt.ParamTypes,
  165. Method: rt.Method,
  166. Controller: rt.Controller,
  167. Middleware: rt.Middleware,
  168. Endpoint: rt.Endpoint,
  169. BeforeResponse: rt.BeforeResponse,
  170. AfterResponse: rt.AfterResponse,
  171. Renderer: rt.Renderer,
  172. }
  173. }
  174. func (rt *Route) ForName() string {
  175. name := rt.Name
  176. if rt.Prefix != "" {
  177. name = rt.Prefix + "." + name
  178. }
  179. if rt.Suffix == "" {
  180. if rt.special != "" {
  181. rt.Suffix = rt.special
  182. } else {
  183. rt.Suffix = rt.Method
  184. }
  185. }
  186. return name + ":" + strings.ToLower(rt.Suffix)
  187. }
  188. func (rt *Route) FullPath() string {
  189. if rt.BasePath == "" || rt.BasePath == "/" {
  190. return rt.Path
  191. }
  192. if rt.BasePath[len(rt.BasePath)-1:] == "/" {
  193. return rt.BasePath[:len(rt.BasePath)-1] + rt.Path
  194. }
  195. return rt.BasePath + rt.Path
  196. }
  197. // ServeHTTP provides an HTTP handler implementation that makes routes
  198. // compatible with Go's default HTTP server.
  199. //
  200. // This function is somewhat messy and handles the entirety of Capstan's request
  201. // handling logic. Refactoring is welcome.
  202. func (rt *Route) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  203. var err error
  204. ctx := rt.contextMaker(w, r, rt)
  205. if rt.RouteTimeout > 0 {
  206. ct, cancel := context.WithTimeout(r.Context(),
  207. time.Second*time.Duration(rt.RouteTimeout))
  208. defer cancel()
  209. r = r.WithContext(ct)
  210. }
  211. // Copy default headers for group.
  212. if len(rt.Headers) > 0 {
  213. for k, v := range rt.Headers {
  214. for _, s := range v {
  215. w.Header().Add(k, s)
  216. }
  217. }
  218. }
  219. // BeforeResponse acts vaguely like middleware with the exception each
  220. // handler is isolated from the next and receives a Context instance.
  221. // BeforeResponse handlers can interrupt the chain by returning an error
  222. // containing a 4xx-level code or higher.
  223. if len(rt.BeforeResponse) > 0 {
  224. for _, fn := range rt.BeforeResponse {
  225. if err = fn(ctx, rt); err != nil {
  226. code := errors.Guarantee(err).Code()
  227. if code >= 400 {
  228. ctx.SetError(err)
  229. ctx.SetCode(code)
  230. goto ERRTYPES
  231. }
  232. break
  233. }
  234. }
  235. }
  236. // We don't handle templated errors here, because the only upgrader we
  237. // currently support is for websocket connections. JSON responses are fine.
  238. if rt.Upgrader != nil {
  239. ws, err := rt.Upgrader.Upgrade(w, r, nil)
  240. if err != nil {
  241. w.Header().Add("upgrade", "websocket")
  242. err = status.Code(426)
  243. ctx.ContentType(ApplicationJSON)
  244. goto ERRTYPES
  245. }
  246. if ws == nil {
  247. w.Header().Add("upgrade", "websocket")
  248. err = status.Code(426)
  249. ctx.ContentType(ApplicationJSON)
  250. goto ERRTYPES
  251. } else {
  252. ctx.SetWebSocket(ws)
  253. defer ws.Close()
  254. }
  255. }
  256. // Previous errors (above) should already be handled with the exception of
  257. // redirects. If we're already redirecting, ignore the new endpoint content.
  258. //
  259. // TODO: Cleanup.
  260. if redirect, ok := err.(ce.Redirector); ok && redirect.Redirect(rt.ForName()) {
  261. goto ERRTYPES
  262. }
  263. // Handle endpoint.
  264. err = rt.Endpoint(ctx)
  265. // AfterResponse handlers act akin to a post-request middleware that can
  266. // perform additional processing. AfterResponse handlers CANNOT change the
  267. // request body being sent to the client, but they can interrupt the control
  268. // flow by returning redirect codes.
  269. //
  270. // AfterResponse handlers are intended to perform any post-processing
  271. // required immediately following a response. Though it may be possible,
  272. // they are not intended to manipulate the result itself.
  273. if err == nil && len(rt.AfterResponse) > 0 {
  274. for _, fn := range rt.AfterResponse {
  275. err = fn(ctx, err)
  276. switch err.(type) {
  277. case *ce.Redirect, *ce.Internal:
  278. continue
  279. case error:
  280. goto ERRTYPES
  281. }
  282. }
  283. }
  284. ERRTYPES:
  285. logger := logging.MustInheritLogger("web.errors", "web")
  286. switch err.(type) {
  287. case Context:
  288. if ctx.Code() >= 400 {
  289. rt.router.HTTPError(ctx)
  290. if e := ctx.HasError(); e != nil {
  291. logger.Warning(e)
  292. }
  293. }
  294. case *ce.Redirect:
  295. if e, ok := err.(*ce.Redirect); ok {
  296. w.Header().Add("Location", e.URL)
  297. if e.Code != 0 {
  298. w.WriteHeader(e.Code)
  299. } else {
  300. w.WriteHeader(301)
  301. }
  302. }
  303. case *ce.Internal:
  304. if e, ok := err.(*ce.Internal); ok {
  305. if e.Redirect(rt.ForName()) {
  306. // TODO: Should handle parameters and "external" options.
  307. builder := rt.router.urls.For(e.Controller)
  308. builder.Params(e.Params)
  309. s := builder.Encode()
  310. w.Header().Add("Location", s)
  311. w.WriteHeader(301)
  312. }
  313. }
  314. case error:
  315. code := errors.Guarantee(err).Code()
  316. if code == 0 {
  317. code = 500
  318. }
  319. ctx.SetCode(code)
  320. if ctx.Code() >= 400 {
  321. rt.router.HTTPError(ctx)
  322. }
  323. w.WriteHeader(ctx.Code())
  324. default:
  325. if err == nil || ctx.Code() == 0 {
  326. ctx.SetCode(200)
  327. }
  328. w.WriteHeader(ctx.Code())
  329. }
  330. }
  331. func (rt *Route) WriteError(ctx Context, err error) {
  332. if strings.HasPrefix(rt.ContentType, "application/json") || strings.HasPrefix(rt.ContentType, "text/json") {
  333. ctx.Response().Header().Set("Content-Type", rt.ContentType+"; charset=utf-8")
  334. ctx.Response().WriteHeader(errors.Guarantee(err).Code())
  335. ctx.Response().Write([]byte(`{"error":"` + err.Error() + `"}`))
  336. } else {
  337. ctx.Response().WriteHeader(errors.Guarantee(err).Code())
  338. ctx.Response().Write([]byte(`<!doctype html>
  339. <title>` + err.Error() + `</title>
  340. <body>
  341. <h1>` + strconv.Itoa(errors.Guarantee(err).Code()) + ": " + err.Error() + `</h1>
  342. <p>Error while processing your request.</p>
  343. </body>`))
  344. }
  345. }
  346. func responseType(contentType string) ContentType {
  347. if strings.HasPrefix(contentType, "application/json") {
  348. return ApplicationJSON
  349. }
  350. if strings.HasPrefix(contentType, "text/json") {
  351. return ApplicationJSON
  352. }
  353. if strings.HasPrefix(contentType, "text/plain") {
  354. return TextPlain
  355. }
  356. return TextHTML
  357. }