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.

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