aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2021-11-15 23:08:29 -0800
committerMike Crute <mike@crute.us>2021-11-15 23:08:29 -0800
commit3d7f26e200d1edefe68eac3b761acde57e244e42 (patch)
treeadeede77e290a6ae2a68f14aa757306710ef5397
parentc598aae837e6c989636881a04149be074a22a099 (diff)
downloadgolib-3d7f26e200d1edefe68eac3b761acde57e244e42.tar.bz2
golib-3d7f26e200d1edefe68eac3b761acde57e244e42.tar.xz
golib-3d7f26e200d1edefe68eac3b761acde57e244e42.zip
echo: add many new thingsecho/v0.5.0
-rw-r--r--echo/controller/content_type_negotiator.go59
-rw-r--r--echo/cookie.go15
-rw-r--r--echo/echo_default.go2
-rw-r--r--echo/error_handler.go120
-rw-r--r--echo/go.mod1
-rw-r--r--echo/go.sum2
-rw-r--r--echo/middleware/cache_headers_middleware.go20
-rw-r--r--echo/middleware/vary.go24
-rw-r--r--echo/url_builder.go49
9 files changed, 259 insertions, 33 deletions
diff --git a/echo/controller/content_type_negotiator.go b/echo/controller/content_type_negotiator.go
new file mode 100644
index 0000000..273a118
--- /dev/null
+++ b/echo/controller/content_type_negotiator.go
@@ -0,0 +1,59 @@
1package controller
2
3import (
4 "errors"
5 "net/http"
6 "sync"
7
8 "github.com/elnormous/contenttype"
9 "github.com/labstack/echo/v4"
10)
11
12type ContentTypeNegotiatingHandler struct {
13 Handlers map[string]echo.HandlerFunc
14 DefaultHandler echo.HandlerFunc
15 mediaTypes []contenttype.MediaType
16 once sync.Once
17}
18
19func errorIsNotAcceptable(err error) bool {
20 return errors.Is(err, contenttype.ErrNoAcceptableTypeFound) ||
21 errors.Is(err, contenttype.ErrNoAvailableTypeGiven)
22}
23
24func errorIsBadRequest(err error) bool {
25 return errors.Is(err, contenttype.ErrInvalidMediaType) ||
26 errors.Is(err, contenttype.ErrInvalidMediaRange) ||
27 errors.Is(err, contenttype.ErrInvalidParameter) ||
28 errors.Is(err, contenttype.ErrInvalidExtensionParameter) ||
29 errors.Is(err, contenttype.ErrInvalidWeight)
30}
31
32func (h *ContentTypeNegotiatingHandler) Handle(c echo.Context) error {
33 h.once.Do(func() {
34 h.mediaTypes = []contenttype.MediaType{}
35 for k, _ := range h.Handlers {
36 h.mediaTypes = append(h.mediaTypes, contenttype.NewMediaType(k))
37 }
38 })
39
40 handler := h.DefaultHandler
41 ct, _, err := contenttype.GetAcceptableMediaType(c.Request(), h.mediaTypes)
42 if err == nil {
43 handler = h.Handlers[ct.String()]
44 } else if errorIsNotAcceptable(err) {
45 return echo.NewHTTPError(http.StatusNotAcceptable)
46 } else if errorIsBadRequest(err) {
47 return echo.NewHTTPError(http.StatusBadRequest)
48 }
49
50 // If negotiation failed but it wasn't an error and there is no default
51 // handler then the request is still not acceptable
52 if handler == nil {
53 return echo.NewHTTPError(http.StatusNotAcceptable)
54 }
55
56 // Don't force each handler to do this itself to eliminate redundant code
57 c.Response().Header().Set("Content-Type", ct.String())
58 return handler(c)
59}
diff --git a/echo/cookie.go b/echo/cookie.go
new file mode 100644
index 0000000..9f4f26a
--- /dev/null
+++ b/echo/cookie.go
@@ -0,0 +1,15 @@
1package echo
2
3import (
4 "time"
5
6 "github.com/labstack/echo/v4"
7)
8
9func DeleteAllCookies(c echo.Context) {
10 for _, k := range c.Request().Cookies() {
11 k.Expires = time.Unix(0, 0)
12 k.MaxAge = -1
13 c.SetCookie(k)
14 }
15}
diff --git a/echo/echo_default.go b/echo/echo_default.go
index 92a5cfd..569d686 100644
--- a/echo/echo_default.go
+++ b/echo/echo_default.go
@@ -173,6 +173,8 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
173 173
174 // Only install template handlers if the path and glob are set 174 // Only install template handlers if the path and glob are set
175 if templates != nil && c.TemplateGlob != nil { 175 if templates != nil && c.TemplateGlob != nil {
176 // TODO: Should assert the presence of required templates: 404.tpl
177 // 40x.tpl 50x.tpl header.tpl footer.tpl
176 e.HTTPErrorHandler = ErrorHandler(templates, c.TemplateFunctions) 178 e.HTTPErrorHandler = ErrorHandler(templates, c.TemplateFunctions)
177 179
178 tr, err := NewTemplateRenderer(templates, *c.TemplateGlob, c.TemplateFunctions) 180 tr, err := NewTemplateRenderer(templates, *c.TemplateGlob, c.TemplateFunctions)
diff --git a/echo/error_handler.go b/echo/error_handler.go
index bfbcfb6..bb4102b 100644
--- a/echo/error_handler.go
+++ b/echo/error_handler.go
@@ -7,19 +7,94 @@ import (
7 "io/fs" 7 "io/fs"
8 "net/http" 8 "net/http"
9 9
10 "github.com/elnormous/contenttype"
10 "github.com/labstack/echo/v4" 11 "github.com/labstack/echo/v4"
11) 12)
12 13
13// Copied from echo and tweaked to make our errors nicer 14// TODO: This should allow plugging in other content types
15// TODO: This should also be refactored into something prettier
14func ErrorHandler(templates fs.FS, funcs template.FuncMap) func(error, echo.Context) { 16func ErrorHandler(templates fs.FS, funcs template.FuncMap) func(error, echo.Context) {
17 handleHtml := func(c echo.Context, he *echo.HTTPError) error {
18 t, err := template.New("").Funcs(funcs).ParseFS(
19 templates,
20 "404.tpl",
21 "40x.tpl",
22 "50x.tpl",
23 "header.tpl",
24 "footer.tpl",
25 )
26 if err != nil {
27 return err
28 }
29
30 path := "50x.tpl"
31 if he.Code == 404 {
32 path = "404.tpl"
33 } else if he.Code >= 400 && he.Code <= 499 {
34 path = "40x.tpl"
35 }
36
37 buf := bytes.Buffer{}
38 if err = t.ExecuteTemplate(&buf, path, nil); err != nil {
39 err = c.String(he.Code, fmt.Sprintf("%s", he.Message))
40 }
41
42 return c.HTMLBlob(he.Code, buf.Bytes())
43 }
44
45 handlePlain := func(c echo.Context, he *echo.HTTPError) error {
46 return c.String(he.Code, fmt.Sprintf("%s", he.Message))
47 }
48
49 handleJson := func(c echo.Context, he *echo.HTTPError) error {
50 code := he.Code
51 message := he.Message
52 if m, ok := he.Message.(string); ok {
53 if c.Echo().Debug {
54 message = echo.Map{"message": m, "error": he.Error()}
55 } else {
56 message = echo.Map{"message": m}
57 }
58 }
59
60 return c.JSON(code, message)
61 }
62
63 errorWhileErroring := func(c echo.Context, err interface{}) {
64 c.Echo().Logger.Error(err)
65 if c.Echo().Debug {
66 c.JSON(http.StatusInternalServerError, &echo.HTTPError{
67 Code: http.StatusInternalServerError,
68 Message: fmt.Sprintf("Error while processing error page. %w", err),
69 })
70 } else {
71 c.JSON(http.StatusInternalServerError, &echo.HTTPError{
72 Code: http.StatusInternalServerError,
73 Message: http.StatusText(http.StatusInternalServerError),
74 })
75 }
76 }
77
78 handlers := map[string]func(echo.Context, *echo.HTTPError) error{
79 "text/plain": handlePlain,
80 "text/html": handleHtml,
81 "text/json": handleJson,
82 "application/json": handleJson,
83 }
84
85 // This is hand maintained here because order is important for negotiation,
86 // especially in the case of */*
87 hIndex := []contenttype.MediaType{
88 contenttype.NewMediaType("text/json"),
89 contenttype.NewMediaType("application/json"),
90 contenttype.NewMediaType("text/plain"),
91 contenttype.NewMediaType("text/html"),
92 }
93
15 return func(err error, c echo.Context) { 94 return func(err error, c echo.Context) {
16 defer func() { 95 defer func() {
17 if r := recover(); r != nil { 96 if r := recover(); r != nil {
18 if c.Echo().Debug { 97 errorWhileErroring(c, r)
19 c.String(http.StatusInternalServerError, fmt.Sprintf("Error while processing error page. %s", r))
20 } else {
21 c.String(http.StatusInternalServerError, "Error while processing error page.")
22 }
23 } 98 }
24 }() 99 }()
25 100
@@ -37,41 +112,26 @@ func ErrorHandler(templates fs.FS, funcs template.FuncMap) func(error, echo.Cont
37 } 112 }
38 } 113 }
39 114
40 t, err := template.New("").Funcs(funcs).ParseFS( 115 ct, _, err := contenttype.GetAcceptableMediaType(c.Request(), hIndex)
41 templates,
42 "404.tpl",
43 "40x.tpl",
44 "50x.tpl",
45 "header.tpl",
46 "footer.tpl",
47 )
48 if err != nil { 116 if err != nil {
49 he = &echo.HTTPError{ 117 c.Echo().Logger.Error("Error negotiating content type in error handler, using json")
50 Code: http.StatusInternalServerError, 118 ct = contenttype.NewMediaType("text/json")
51 Message: http.StatusText(http.StatusInternalServerError),
52 }
53 } 119 }
54 120
55 path := "50x.tpl" 121 handle, ok := handlers[ct.String()]
56 if he.Code == 404 { 122 if !ok {
57 path = "404.tpl" 123 c.Echo().Logger.Errorf("Error handler content type %s is unknown", ct.String())
58 } else if he.Code >= 400 && he.Code <= 499 { 124 handle = handleJson
59 path = "40x.tpl"
60 } 125 }
61 126
62 // Send response
63 if !c.Response().Committed { 127 if !c.Response().Committed {
64 if c.Request().Method == http.MethodHead { // Issue #608 128 if c.Request().Method == http.MethodHead { // Issue #608
65 err = c.NoContent(he.Code) 129 err = c.NoContent(he.Code)
66 } else { 130 } else {
67 buf := bytes.Buffer{} 131 err = handle(c, he)
68 if err = t.ExecuteTemplate(&buf, path, nil); err != nil {
69 err = c.String(he.Code, fmt.Sprintf("%s", he.Message))
70 }
71 c.HTMLBlob(he.Code, buf.Bytes())
72 } 132 }
73 if err != nil { 133 if err != nil {
74 c.Echo().Logger.Error(err) 134 errorWhileErroring(c, err)
75 } 135 }
76 } 136 }
77 } 137 }
diff --git a/echo/go.mod b/echo/go.mod
index 942b4e5..44b7e1d 100644
--- a/echo/go.mod
+++ b/echo/go.mod
@@ -6,6 +6,7 @@ replace code.crute.us/mcrute/golib => ../
6 6
7require ( 7require (
8 code.crute.us/mcrute/golib v0.1.1 8 code.crute.us/mcrute/golib v0.1.1
9 github.com/elnormous/contenttype v1.0.0
9 github.com/labstack/echo/v4 v4.6.1 10 github.com/labstack/echo/v4 v4.6.1
10 github.com/labstack/gommon v0.3.1 11 github.com/labstack/gommon v0.3.1
11 github.com/prometheus/client_golang v1.11.0 12 github.com/prometheus/client_golang v1.11.0
diff --git a/echo/go.sum b/echo/go.sum
index 4f3a541..7bcd71c 100644
--- a/echo/go.sum
+++ b/echo/go.sum
@@ -53,6 +53,8 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX
53github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 53github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
54github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 54github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
55github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 55github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
56github.com/elnormous/contenttype v1.0.0 h1:cTLou7K7uQMsPEmRiTJosAznsPcYuoBmXMrFAf86t2A=
57github.com/elnormous/contenttype v1.0.0/go.mod h1:ngVcyGGU8pnn4QJ5sL4StrNgc/wmXZXy5IQSBuHOFPg=
56github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 58github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
57github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 59github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
58github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 60github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
diff --git a/echo/middleware/cache_headers_middleware.go b/echo/middleware/cache_headers_middleware.go
index f168dfd..73df9b2 100644
--- a/echo/middleware/cache_headers_middleware.go
+++ b/echo/middleware/cache_headers_middleware.go
@@ -10,19 +10,33 @@ import (
10) 10)
11 11
12var ( 12var (
13 CacheNeverMiddleware = CacheHeadersMiddleware(0)
13 CacheOneHourMiddleware = CacheHeadersMiddleware(1 * time.Hour) 14 CacheOneHourMiddleware = CacheHeadersMiddleware(1 * time.Hour)
14 CacheOneDayMiddleware = CacheHeadersMiddleware(1 * gltime.Day) 15 CacheOneDayMiddleware = CacheHeadersMiddleware(1 * gltime.Day)
15 CacheOneMonthMiddleware = CacheHeadersMiddleware(30 * gltime.Day) 16 CacheOneMonthMiddleware = CacheHeadersMiddleware(30 * gltime.Day)
16) 17)
17 18
19func setHeaderMissing(c echo.Context, name string, value string) {
20 h := c.Response().Header()
21 if v := h.Get(name); v == "" {
22 h.Set(name, value)
23 }
24}
25
18func CacheHeadersMiddleware(d time.Duration) echo.MiddlewareFunc { 26func CacheHeadersMiddleware(d time.Duration) echo.MiddlewareFunc {
19 ds := int(d.Seconds()) 27 ds := int(d.Seconds())
20 28
21 return func(next echo.HandlerFunc) echo.HandlerFunc { 29 return func(next echo.HandlerFunc) echo.HandlerFunc {
22 return func(c echo.Context) error { 30 return func(c echo.Context) error {
23 c.Response().Header().Set("Vary", "Accept-Encoding") 31 c.Response().Header().Add(echo.HeaderVary, "Accept-Encoding")
24 c.Response().Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", ds)) 32
25 c.Response().Header().Set("Expires", time.Now().Add(d).Format(time.RFC1123)) 33 if ds == 0 {
34 setHeaderMissing(c, "Cache-Control", "private, max-age=0, no-cache, no-store")
35 setHeaderMissing(c, "Expires", time.Now().Add(-time.Hour).Format(time.RFC1123))
36 } else {
37 setHeaderMissing(c, "Cache-Control", fmt.Sprintf("public, max-age=%d", ds))
38 setHeaderMissing(c, "Expires", time.Now().Add(d).Format(time.RFC1123))
39 }
26 return next(c) 40 return next(c)
27 } 41 }
28 } 42 }
diff --git a/echo/middleware/vary.go b/echo/middleware/vary.go
new file mode 100644
index 0000000..8f87d29
--- /dev/null
+++ b/echo/middleware/vary.go
@@ -0,0 +1,24 @@
1package middleware
2
3import (
4 "github.com/labstack/echo/v4"
5)
6
7type VaryConfig struct {
8 Vary []string
9}
10
11func VaryCookie() echo.MiddlewareFunc {
12 return VaryWithConfig(VaryConfig{Vary: []string{"Cookie"}})
13}
14
15func VaryWithConfig(cfg VaryConfig) echo.MiddlewareFunc {
16 return func(next echo.HandlerFunc) echo.HandlerFunc {
17 return func(c echo.Context) error {
18 for _, v := range cfg.Vary {
19 c.Response().Header().Add(echo.HeaderVary, v)
20 }
21 return next(c)
22 }
23 }
24}
diff --git a/echo/url_builder.go b/echo/url_builder.go
new file mode 100644
index 0000000..955ecb5
--- /dev/null
+++ b/echo/url_builder.go
@@ -0,0 +1,49 @@
1package echo
2
3import (
4 "net/url"
5 "path"
6
7 "github.com/labstack/echo/v4"
8)
9
10// URLBuilder is used to build URLs with optional querystring arguments. This
11// is used to build URLs to REST resources within handlers.
12//
13// This exists because the default Echo reversing logic requires handlers to
14// hold references to other handlers to be able to build reverse URLs. This is
15// a bad solution to an ugly problem but as the router currently stands there's
16// not much that can be done about it. In the future this should go away and be
17// replaced by something like named routes in echo.
18type URLBuilder struct {
19 c echo.Context
20 u *url.URL
21 q url.Values
22}
23
24func URLFor(c echo.Context, parts ...string) *URLBuilder {
25 u := &url.URL{
26 Scheme: "http",
27 Host: c.Request().Host,
28 Path: path.Join(parts...),
29 }
30 if c.Request().TLS != nil {
31 u.Scheme = "https"
32 }
33 return &URLBuilder{c, u, nil}
34}
35
36func (b *URLBuilder) Query(k, v string) *URLBuilder {
37 if b.q == nil {
38 b.q = url.Values{}
39 }
40 b.q.Add(k, v)
41 return b
42}
43
44func (b *URLBuilder) String() string {
45 if b.q != nil {
46 b.u.RawQuery = b.q.Encode()
47 }
48 return b.u.String()
49}