2021-03-18 02:14:06 +00:00
|
|
|
package website
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"context"
|
2021-03-26 03:33:00 +00:00
|
|
|
"html"
|
2021-03-18 02:14:06 +00:00
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2021-03-26 03:33:00 +00:00
|
|
|
"path"
|
|
|
|
"strings"
|
2021-03-18 02:14:06 +00:00
|
|
|
|
|
|
|
"git.handmade.network/hmn/hmn/src/logging"
|
2021-03-21 20:38:37 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/models"
|
2021-04-26 06:56:49 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/perf"
|
2021-03-18 02:14:06 +00:00
|
|
|
"git.handmade.network/hmn/hmn/src/templates"
|
2021-04-06 05:06:19 +00:00
|
|
|
"github.com/jackc/pgx/v4/pgxpool"
|
2021-03-18 02:14:06 +00:00
|
|
|
"github.com/julienschmidt/httprouter"
|
|
|
|
"github.com/rs/zerolog"
|
|
|
|
)
|
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
// The typical handler. Handles a request and returns data about the response.
|
2021-04-06 03:30:11 +00:00
|
|
|
type HMNHandler func(c *RequestContext) ResponseData
|
2021-04-06 05:06:19 +00:00
|
|
|
|
|
|
|
// A special handler that runs before the primary handler. Intended to set
|
|
|
|
// information on the context for later handlers, or to give the request a
|
|
|
|
// means to bail out early if preconditions are not met (like auth). If `ok`
|
|
|
|
// is false, the request will immediately bail out, no further handlers will
|
|
|
|
// be run, and it will respond with the provided response data.
|
|
|
|
//
|
|
|
|
// The response data from this function will still be fed through any after
|
|
|
|
// handlers, to ensure that errors will get logged and whatnot.
|
|
|
|
type HMNBeforeHandler func(c *RequestContext) (ok bool, res ResponseData)
|
|
|
|
|
|
|
|
// A special handler that runs after the primary handler and can modify the
|
|
|
|
// response information. Intended for error logging, error pages,
|
|
|
|
// cleanup, etc.
|
|
|
|
type HMNAfterHandler func(c *RequestContext, res ResponseData) ResponseData
|
2021-03-18 02:14:06 +00:00
|
|
|
|
2021-04-06 03:30:11 +00:00
|
|
|
func (h HMNHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
2021-04-26 06:56:49 +00:00
|
|
|
c := NewRequestContext(rw, req, nil, "")
|
2021-04-06 03:30:11 +00:00
|
|
|
doRequest(rw, c, h)
|
2021-03-31 04:20:50 +00:00
|
|
|
}
|
|
|
|
|
2021-03-18 02:14:06 +00:00
|
|
|
type RequestContext struct {
|
2021-03-21 20:38:37 +00:00
|
|
|
Logger *zerolog.Logger
|
|
|
|
Req *http.Request
|
2021-04-06 03:30:11 +00:00
|
|
|
PathParams httprouter.Params
|
2021-03-21 20:38:37 +00:00
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
Conn *pgxpool.Pool
|
2021-04-26 06:56:49 +00:00
|
|
|
Perf *perf.RequestPerf
|
2021-04-06 03:30:11 +00:00
|
|
|
CurrentProject *models.Project
|
|
|
|
CurrentUser *models.User
|
2021-03-18 02:14:06 +00:00
|
|
|
}
|
|
|
|
|
2021-04-26 06:56:49 +00:00
|
|
|
func NewRequestContext(rw http.ResponseWriter, req *http.Request, pathParams httprouter.Params, route string) *RequestContext {
|
2021-03-18 02:14:06 +00:00
|
|
|
return &RequestContext{
|
2021-04-06 03:30:11 +00:00
|
|
|
Logger: logging.GlobalLogger(),
|
2021-03-21 20:38:37 +00:00
|
|
|
Req: req,
|
2021-04-06 03:30:11 +00:00
|
|
|
PathParams: pathParams,
|
2021-04-26 06:56:49 +00:00
|
|
|
Perf: perf.MakeNewRequestPerf(route),
|
2021-03-18 02:14:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *RequestContext) Context() context.Context {
|
2021-03-21 20:38:37 +00:00
|
|
|
return c.Req.Context()
|
2021-03-18 02:14:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *RequestContext) URL() *url.URL {
|
2021-03-21 20:38:37 +00:00
|
|
|
return c.Req.URL
|
2021-03-18 02:14:06 +00:00
|
|
|
}
|
|
|
|
|
2021-03-26 03:33:00 +00:00
|
|
|
func (c *RequestContext) GetFormValues() (url.Values, error) {
|
|
|
|
err := c.Req.ParseForm()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return c.Req.PostForm, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// The logic of this function is copy-pasted from the Go standard library.
|
|
|
|
// https://golang.org/pkg/net/http/#Redirect
|
2021-04-06 03:30:11 +00:00
|
|
|
func (c *RequestContext) Redirect(dest string, code int) ResponseData {
|
|
|
|
var res ResponseData
|
|
|
|
|
2021-03-26 03:33:00 +00:00
|
|
|
if u, err := url.Parse(dest); err == nil {
|
|
|
|
// If url was relative, make its path absolute by
|
|
|
|
// combining with request path.
|
|
|
|
// The client would probably do this for us,
|
|
|
|
// but doing it ourselves is more reliable.
|
|
|
|
// See RFC 7231, section 7.1.2
|
|
|
|
if u.Scheme == "" && u.Host == "" {
|
|
|
|
oldpath := c.Req.URL.Path
|
|
|
|
if oldpath == "" { // should not happen, but avoid a crash if it does
|
|
|
|
oldpath = "/"
|
|
|
|
}
|
|
|
|
|
|
|
|
// no leading http://server
|
|
|
|
if dest == "" || dest[0] != '/' {
|
|
|
|
// make relative path absolute
|
|
|
|
olddir, _ := path.Split(oldpath)
|
|
|
|
dest = olddir + dest
|
|
|
|
}
|
|
|
|
|
|
|
|
var query string
|
|
|
|
if i := strings.Index(dest, "?"); i != -1 {
|
|
|
|
dest, query = dest[:i], dest[i:]
|
|
|
|
}
|
|
|
|
|
|
|
|
// clean up but preserve trailing slash
|
|
|
|
trailing := strings.HasSuffix(dest, "/")
|
|
|
|
dest = path.Clean(dest)
|
|
|
|
if trailing && !strings.HasSuffix(dest, "/") {
|
|
|
|
dest += "/"
|
|
|
|
}
|
|
|
|
dest += query
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Escape stuff
|
|
|
|
destUrl, _ := url.Parse(dest)
|
|
|
|
dest = destUrl.String()
|
|
|
|
|
2021-04-06 03:30:11 +00:00
|
|
|
res.Headers().Set("Location", dest)
|
|
|
|
if c.Req.Method == "GET" || c.Req.Method == "HEAD" {
|
|
|
|
res.Headers().Set("Content-Type", "text/html; charset=utf-8")
|
2021-03-26 03:33:00 +00:00
|
|
|
}
|
2021-04-06 03:30:11 +00:00
|
|
|
res.StatusCode = code
|
2021-03-26 03:33:00 +00:00
|
|
|
|
|
|
|
// Shouldn't send the body for POST or HEAD; that leaves GET.
|
2021-04-06 03:30:11 +00:00
|
|
|
if c.Req.Method == "GET" {
|
|
|
|
res.Write([]byte("<a href=\"" + html.EscapeString(dest) + "\">" + http.StatusText(code) + "</a>.\n"))
|
2021-03-26 03:33:00 +00:00
|
|
|
}
|
|
|
|
|
2021-04-06 03:30:11 +00:00
|
|
|
return res
|
2021-03-18 02:14:06 +00:00
|
|
|
}
|
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
type ResponseData struct {
|
|
|
|
StatusCode int
|
|
|
|
Body *bytes.Buffer
|
|
|
|
Errors []error
|
|
|
|
|
|
|
|
header http.Header
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rd *ResponseData) Headers() http.Header {
|
|
|
|
if rd.header == nil {
|
|
|
|
rd.header = make(http.Header)
|
|
|
|
}
|
|
|
|
|
|
|
|
return rd.header
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rd *ResponseData) Write(p []byte) (n int, err error) {
|
|
|
|
if rd.Body == nil {
|
|
|
|
rd.Body = new(bytes.Buffer)
|
|
|
|
}
|
|
|
|
|
|
|
|
return rd.Body.Write(p)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (rd *ResponseData) SetCookie(cookie *http.Cookie) {
|
|
|
|
rd.Headers().Add("Set-Cookie", cookie.String())
|
|
|
|
}
|
|
|
|
|
2021-04-26 06:56:49 +00:00
|
|
|
func (rd *ResponseData) WriteTemplate(name string, data interface{}, rp *perf.RequestPerf) error {
|
|
|
|
if rp != nil {
|
|
|
|
rp.StartBlock("TEMPLATE", name)
|
|
|
|
defer rp.EndBlock()
|
|
|
|
}
|
2021-04-06 03:30:11 +00:00
|
|
|
return templates.Templates[name].Execute(rd, data)
|
2021-03-21 20:38:37 +00:00
|
|
|
}
|
|
|
|
|
2021-04-06 05:06:19 +00:00
|
|
|
type RouteBuilder struct {
|
|
|
|
Router *httprouter.Router
|
|
|
|
BeforeHandlers []HMNBeforeHandler
|
|
|
|
AfterHandlers []HMNAfterHandler
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b RouteBuilder) ChainHandlers(h HMNHandler) HMNHandler {
|
|
|
|
return func(c *RequestContext) ResponseData {
|
|
|
|
beforeOk := true
|
|
|
|
var res ResponseData
|
|
|
|
for _, before := range b.BeforeHandlers {
|
|
|
|
if ok, errorRes := before(c); !ok {
|
|
|
|
beforeOk = false
|
|
|
|
res = errorRes
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if beforeOk {
|
|
|
|
res = h(c)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, after := range b.AfterHandlers {
|
|
|
|
res = after(c, res)
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *RouteBuilder) Handle(method, route string, handler HMNHandler) {
|
|
|
|
h := b.ChainHandlers(handler)
|
|
|
|
b.Router.Handle(method, route, func(rw http.ResponseWriter, req *http.Request, p httprouter.Params) {
|
2021-04-26 06:56:49 +00:00
|
|
|
c := NewRequestContext(rw, req, p, route)
|
2021-04-06 05:06:19 +00:00
|
|
|
doRequest(rw, c, h)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *RouteBuilder) GET(route string, handler HMNHandler) {
|
|
|
|
b.Handle(http.MethodGet, route, handler)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *RouteBuilder) POST(route string, handler HMNHandler) {
|
|
|
|
b.Handle(http.MethodPost, route, handler)
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: More methods
|
|
|
|
|
|
|
|
func (b *RouteBuilder) ServeFiles(path string, root http.FileSystem) {
|
|
|
|
b.Router.ServeFiles(path, root)
|
|
|
|
}
|
|
|
|
|
2021-04-06 03:30:11 +00:00
|
|
|
func ErrorResponse(status int, errs ...error) ResponseData {
|
|
|
|
return ResponseData{
|
|
|
|
StatusCode: status,
|
|
|
|
Errors: errs,
|
|
|
|
}
|
2021-03-21 20:38:37 +00:00
|
|
|
}
|
|
|
|
|
2021-04-06 03:30:11 +00:00
|
|
|
func doRequest(rw http.ResponseWriter, c *RequestContext, h HMNHandler) {
|
|
|
|
defer func() {
|
|
|
|
/*
|
|
|
|
This panic recovery is the last resort. If you want to render
|
|
|
|
an error page or something, make it a request wrapper.
|
|
|
|
*/
|
|
|
|
if recovered := recover(); recovered != nil {
|
|
|
|
rw.WriteHeader(http.StatusInternalServerError)
|
|
|
|
logging.LogPanicValue(c.Logger, recovered, "request panicked and was not handled")
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
res := h(c)
|
2021-03-21 20:38:37 +00:00
|
|
|
|
2021-04-06 03:30:11 +00:00
|
|
|
if res.StatusCode == 0 {
|
|
|
|
res.StatusCode = http.StatusOK
|
|
|
|
}
|
2021-03-18 02:14:06 +00:00
|
|
|
|
2021-04-06 03:30:11 +00:00
|
|
|
for name, vals := range res.Headers() {
|
|
|
|
for _, val := range vals {
|
|
|
|
rw.Header().Add(name, val)
|
|
|
|
}
|
2021-03-18 02:14:06 +00:00
|
|
|
}
|
2021-04-06 03:30:11 +00:00
|
|
|
rw.WriteHeader(res.StatusCode)
|
2021-04-22 23:02:50 +00:00
|
|
|
|
|
|
|
if res.Body != nil {
|
|
|
|
io.Copy(rw, res.Body)
|
|
|
|
}
|
2021-03-18 02:14:06 +00:00
|
|
|
}
|