diff options
author | Mike Crute <mike@crute.us> | 2021-11-15 23:08:29 -0800 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2021-11-15 23:08:29 -0800 |
commit | 3d7f26e200d1edefe68eac3b761acde57e244e42 (patch) | |
tree | adeede77e290a6ae2a68f14aa757306710ef5397 | |
parent | c598aae837e6c989636881a04149be074a22a099 (diff) | |
download | golib-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.go | 59 | ||||
-rw-r--r-- | echo/cookie.go | 15 | ||||
-rw-r--r-- | echo/echo_default.go | 2 | ||||
-rw-r--r-- | echo/error_handler.go | 120 | ||||
-rw-r--r-- | echo/go.mod | 1 | ||||
-rw-r--r-- | echo/go.sum | 2 | ||||
-rw-r--r-- | echo/middleware/cache_headers_middleware.go | 20 | ||||
-rw-r--r-- | echo/middleware/vary.go | 24 | ||||
-rw-r--r-- | echo/url_builder.go | 49 |
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 @@ | |||
1 | package controller | ||
2 | |||
3 | import ( | ||
4 | "errors" | ||
5 | "net/http" | ||
6 | "sync" | ||
7 | |||
8 | "github.com/elnormous/contenttype" | ||
9 | "github.com/labstack/echo/v4" | ||
10 | ) | ||
11 | |||
12 | type ContentTypeNegotiatingHandler struct { | ||
13 | Handlers map[string]echo.HandlerFunc | ||
14 | DefaultHandler echo.HandlerFunc | ||
15 | mediaTypes []contenttype.MediaType | ||
16 | once sync.Once | ||
17 | } | ||
18 | |||
19 | func errorIsNotAcceptable(err error) bool { | ||
20 | return errors.Is(err, contenttype.ErrNoAcceptableTypeFound) || | ||
21 | errors.Is(err, contenttype.ErrNoAvailableTypeGiven) | ||
22 | } | ||
23 | |||
24 | func 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 | |||
32 | func (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 @@ | |||
1 | package echo | ||
2 | |||
3 | import ( | ||
4 | "time" | ||
5 | |||
6 | "github.com/labstack/echo/v4" | ||
7 | ) | ||
8 | |||
9 | func 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 | ||
14 | func ErrorHandler(templates fs.FS, funcs template.FuncMap) func(error, echo.Context) { | 16 | func 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 | ||
7 | require ( | 7 | require ( |
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 | |||
53 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | 53 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
54 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | 54 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
55 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | 55 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
56 | github.com/elnormous/contenttype v1.0.0 h1:cTLou7K7uQMsPEmRiTJosAznsPcYuoBmXMrFAf86t2A= | ||
57 | github.com/elnormous/contenttype v1.0.0/go.mod h1:ngVcyGGU8pnn4QJ5sL4StrNgc/wmXZXy5IQSBuHOFPg= | ||
56 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= | 58 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= |
57 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= | 59 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= |
58 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= | 60 | github.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 | ||
12 | var ( | 12 | var ( |
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 | ||
19 | func 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 | |||
18 | func CacheHeadersMiddleware(d time.Duration) echo.MiddlewareFunc { | 26 | func 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 @@ | |||
1 | package middleware | ||
2 | |||
3 | import ( | ||
4 | "github.com/labstack/echo/v4" | ||
5 | ) | ||
6 | |||
7 | type VaryConfig struct { | ||
8 | Vary []string | ||
9 | } | ||
10 | |||
11 | func VaryCookie() echo.MiddlewareFunc { | ||
12 | return VaryWithConfig(VaryConfig{Vary: []string{"Cookie"}}) | ||
13 | } | ||
14 | |||
15 | func 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 @@ | |||
1 | package echo | ||
2 | |||
3 | import ( | ||
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. | ||
18 | type URLBuilder struct { | ||
19 | c echo.Context | ||
20 | u *url.URL | ||
21 | q url.Values | ||
22 | } | ||
23 | |||
24 | func 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 | |||
36 | func (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 | |||
44 | func (b *URLBuilder) String() string { | ||
45 | if b.q != nil { | ||
46 | b.u.RawQuery = b.q.Encode() | ||
47 | } | ||
48 | return b.u.String() | ||
49 | } | ||