aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2021-11-12 20:54:39 -0800
committerMike Crute <mike@crute.us>2021-11-12 20:54:39 -0800
commit4d8fde6a8882e63e8dfafdf9f62c73b7b1036ebf (patch)
tree3e2380dc9bbae828fb0a6f72aae964742d24d953
parent6961a3dfea279d8ff636ca27e1c6b52f65130992 (diff)
downloadgolib-4d8fde6a8882e63e8dfafdf9f62c73b7b1036ebf.tar.bz2
golib-4d8fde6a8882e63e8dfafdf9f62c73b7b1036ebf.tar.xz
golib-4d8fde6a8882e63e8dfafdf9f62c73b7b1036ebf.zip
echo: add prometheus and multiple middlewarev0.3.0echo/v0.4.0
- Integrate prometheus - Integrate CORS - Access control prometheus by IP - Fix redirectors to consider port - Fix redirectors to not redirect IP addresses - Use body limit e.Use(middleware.BodyLimit("2M"))
-rw-r--r--echo/echo_default.go177
-rw-r--r--echo/middleware/ip_filter.go39
-rw-r--r--echo/middleware/redirect.go75
-rw-r--r--echo/prometheus/prometheus.go16
4 files changed, 244 insertions, 63 deletions
diff --git a/echo/echo_default.go b/echo/echo_default.go
index ddaab44..92a5cfd 100644
--- a/echo/echo_default.go
+++ b/echo/echo_default.go
@@ -6,13 +6,17 @@ import (
6 "fmt" 6 "fmt"
7 "html/template" 7 "html/template"
8 "io/fs" 8 "io/fs"
9 "net"
9 "net/http" 10 "net/http"
10 "path" 11 "path"
12 "strconv"
11 "sync" 13 "sync"
12 14
13 gltls "code.crute.us/mcrute/golib/crypto/tls" 15 gltls "code.crute.us/mcrute/golib/crypto/tls"
14 glmw "code.crute.us/mcrute/golib/echo/middleware" 16 glmw "code.crute.us/mcrute/golib/echo/middleware"
15 "code.crute.us/mcrute/golib/echo/prometheus" 17 "code.crute.us/mcrute/golib/echo/prometheus"
18 glnet "code.crute.us/mcrute/golib/net"
19 glservice "code.crute.us/mcrute/golib/service"
16 20
17 "github.com/labstack/echo/v4" 21 "github.com/labstack/echo/v4"
18 "github.com/labstack/echo/v4/middleware" 22 "github.com/labstack/echo/v4/middleware"
@@ -22,21 +26,23 @@ import (
22// Docs: https://echo.labstack.com/guide/ 26// Docs: https://echo.labstack.com/guide/
23 27
24// TODO: 28// TODO:
25// - Intgrate prometheus
26// - Integrate CORS
27// - Access control prometheus by IP
28// - Fix redirectors to consider port
29// - Fix redirectors to not redirect IP addresses
30// - Integrate CSRF 29// - Integrate CSRF
31// - Integrate session 30// - Integrate session
32// - Use bodylimit e.Use(middleware.BodyLimit("2M")) 31// - Enable auto cert management by passing hostnames
32
33const (
34 defaultBodySizeLimit = "10M"
35)
33 36
34type EchoConfig struct { 37type EchoConfig struct {
35 Debug bool 38 Debug bool
36 BindAddress string 39 Hostnames []string
37 BindTLSAddress string 40 BindAddresses []string
41 BindTLSAddresses []string
38 TLSCacheDir string 42 TLSCacheDir string
43 BodySizeLimit string
39 TrustedProxyIPRanges []string 44 TrustedProxyIPRanges []string
45 ManagementIPRanges []string
40 EmbeddedTemplates fs.FS 46 EmbeddedTemplates fs.FS
41 DiskTemplates fs.FS 47 DiskTemplates fs.FS
42 TemplateGlob *string 48 TemplateGlob *string
@@ -44,14 +50,16 @@ type EchoConfig struct {
44 CombinedHostLogFile string 50 CombinedHostLogFile string
45 RedirectToWWW bool 51 RedirectToWWW bool
46 ContentSecurityPolicy *glmw.ContentSecurityPolicyConfig 52 ContentSecurityPolicy *glmw.ContentSecurityPolicyConfig
53 DisablePrometheus bool
47 PrometheusConfig *prometheus.PrometheusConfig 54 PrometheusConfig *prometheus.PrometheusConfig
55 CORSConfig *middleware.CORSConfig
48} 56}
49 57
50type EchoWrapper struct { 58type EchoWrapper struct {
51 *echo.Echo 59 *echo.Echo
52 tlsServer http.Server 60 servers []*http.Server
61 tlsServers []*http.Server
53 templateFS fs.FS 62 templateFS fs.FS
54 bindAddress string
55 ocspErrors chan gltls.OcspError 63 ocspErrors chan gltls.OcspError
56 ocspManager *gltls.OcspManager 64 ocspManager *gltls.OcspManager
57 initDone bool 65 initDone bool
@@ -79,45 +87,56 @@ func (w *EchoWrapper) RunCertificateManager(ctx context.Context, wg *sync.WaitGr
79 return w.ocspManager.Run(ctx, wg) 87 return w.ocspManager.Run(ctx, wg)
80} 88}
81 89
82func (w *EchoWrapper) run(ctx context.Context, wg *sync.WaitGroup, f func() error, sf func(context.Context) error) error { 90func (w *EchoWrapper) makeServerJob(s *http.Server, echoInit bool) glservice.RunnerFunc {
83 if !w.initDone { 91 return func(ctx context.Context, wg *sync.WaitGroup) error {
84 return fmt.Errorf("Echo is not initialized. Call Init()") 92 if !w.initDone {
85 } 93 return fmt.Errorf("Echo is not initialized. Call Init()")
94 }
86 95
87 wg.Add(1) 96 wg.Add(1)
88 defer wg.Done() 97 defer wg.Done()
89 98
90 err := make(chan error) 99 w.Logger.Infof("Starting server with address: %s", s.Addr)
91 go func() { err <- f() }()
92 select {
93 case e := <-err:
94 return e
95 default:
96 }
97 100
98 select { 101 err := make(chan error)
99 case <-ctx.Done(): 102 go func() {
100 w.Logger.Info("Shutting down web server") 103 if s.TLSConfig == nil && echoInit {
101 return sf(ctx) 104 err <- w.Echo.StartServer(s)
105 } else if s.TLSConfig == nil && !echoInit {
106 err <- s.ListenAndServe()
107 } else {
108 err <- s.ListenAndServeTLS("", "")
109 }
110 }()
111 select {
112 case e := <-err:
113 return e
114 default:
115 }
116
117 select {
118 case <-ctx.Done():
119 w.Logger.Info("Shutting down web server")
120 return s.Shutdown(ctx)
121 }
102 } 122 }
103} 123}
104 124
105func (w *EchoWrapper) Serve(ctx context.Context, wg *sync.WaitGroup) error { 125func (w *EchoWrapper) MakeServerJobs() []glservice.RunnerFunc {
106 return w.run( 126 out := []glservice.RunnerFunc{}
107 ctx,
108 wg,
109 func() error { return w.Echo.Start(w.bindAddress) },
110 func(ctx context.Context) error { return w.Echo.Shutdown(ctx) },
111 )
112}
113 127
114func (w *EchoWrapper) ServeTLS(ctx context.Context, wg *sync.WaitGroup) error { 128 for i, s := range w.servers {
115 return w.run( 129 // The first http (not https) server should do an echo.StartServer to
116 ctx, 130 // configure some internal echo state and print the banner (if
117 wg, 131 // configured).
118 func() error { return w.tlsServer.ListenAndServeTLS("", "") }, 132 out = append(out, w.makeServerJob(s, i == 0))
119 func(ctx context.Context) error { return w.tlsServer.Shutdown(ctx) }, 133 }
120 ) 134
135 for _, s := range w.tlsServers {
136 out = append(out, w.makeServerJob(s, false))
137 }
138
139 return out
121} 140}
122 141
123func (w *EchoWrapper) GetTemplateFS() fs.FS { 142func (w *EchoWrapper) GetTemplateFS() fs.FS {
@@ -183,25 +202,55 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
183 Errors: cmek, 202 Errors: cmek,
184 } 203 }
185 204
186 ts := http.Server{ 205 servers := make([]*http.Server, len(c.BindAddresses))
187 Addr: c.BindTLSAddress, 206 for i, a := range c.BindAddresses {
188 TLSConfig: &tls.Config{ 207 servers[i] = &http.Server{
189 MinVersion: tls.VersionTLS12, 208 Addr: a,
190 GetCertificate: cm.GetCertificate, 209 Handler: e,
191 }, 210 }
192 Handler: e, 211 }
212
213 tlsServers := make([]*http.Server, len(c.BindTLSAddresses))
214 for i, a := range c.BindTLSAddresses {
215 tlsServers[i] = &http.Server{
216 Addr: a,
217 TLSConfig: &tls.Config{
218 MinVersion: tls.VersionTLS12,
219 GetCertificate: cm.GetCertificate,
220 },
221 Handler: e,
222 }
193 } 223 }
194 224
195 e.Use(middleware.Logger()) 225 e.Use(middleware.Logger())
196 e.Use(glmw.Recover()) 226 e.Use(glmw.Recover())
197 e.Use(middleware.HTTPSRedirect()) 227
228 if c.BodySizeLimit == "" {
229 e.Use(middleware.BodyLimit(defaultBodySizeLimit))
230 } else if c.BodySizeLimit != "0" {
231 e.Use(middleware.BodyLimit(c.BodySizeLimit))
232 }
233
234 _, tlsPort, err := net.SplitHostPort(tlsServers[0].Addr)
235 if err != nil {
236 return nil, fmt.Errorf("Unable to split TLS addr and port: %w", err)
237 }
238 tlsPortI, err := strconv.Atoi(tlsPort)
239 if err != nil {
240 return nil, fmt.Errorf("Unable to convert TLS port to int: %w", err)
241 }
242 e.Use(glmw.HTTPSRedirectWithConfig(glmw.HTTPSRedirectConfig{Port: tlsPortI}))
243
198 if c.RedirectToWWW { 244 if c.RedirectToWWW {
199 e.Use(middleware.WWWRedirect()) 245 e.Use(middleware.WWWRedirect())
200 } 246 }
247
201 e.Use(middleware.Decompress()) 248 e.Use(middleware.Decompress())
202 e.Use(middleware.GzipWithConfig(middleware.GzipConfig{ 249 e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
203 // TODO: This mw causes prometheus responses to show up compressed for 250 // TODO: This mw causes prometheus responses to show up compressed for
204 // browsers. Why? 251 // browsers. Why?
252 //
253 // Also, this path should use the config path if we keep it
205 Skipper: func(c echo.Context) bool { 254 Skipper: func(c echo.Context) bool {
206 if c.Path() == "/metrics" { 255 if c.Path() == "/metrics" {
207 return true 256 return true
@@ -210,24 +259,42 @@ func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
210 }, 259 },
211 Level: 5, 260 Level: 5,
212 })) 261 }))
262
213 e.Use(glmw.StrictSecure()) 263 e.Use(glmw.StrictSecure())
214 264
265 if c.CORSConfig != nil {
266 e.Use(middleware.CORSWithConfig(*c.CORSConfig))
267 } else {
268 e.Use(middleware.CORS())
269 }
270
215 if c.ContentSecurityPolicy != nil { 271 if c.ContentSecurityPolicy != nil {
216 e.Use(glmw.ContentSecurityPolicyWithConfig(*c.ContentSecurityPolicy)) 272 e.Use(glmw.ContentSecurityPolicyWithConfig(*c.ContentSecurityPolicy))
217 } else { 273 } else {
218 return nil, fmt.Errorf("ContentSecurityPolicy is required") 274 return nil, fmt.Errorf("ContentSecurityPolicy is required")
219 } 275 }
220 276
221 if c.PrometheusConfig != nil { 277 if !c.DisablePrometheus {
222 prom := prometheus.NewPrometheusWithConfig(c.PrometheusConfig) 278 mips, err := glnet.ParseCIDRSlice(c.ManagementIPRanges)
279 if err != nil {
280 return nil, err
281 }
282
283 var prom *prometheus.Prometheus
284 if c.PrometheusConfig != nil {
285 prom = prometheus.NewPrometheusWithConfig(c.PrometheusConfig)
286 } else {
287 prom = prometheus.NewPrometheus()
288 }
289
223 e.Use(prom.MiddlewareHandler) 290 e.Use(prom.MiddlewareHandler)
224 e.GET(c.PrometheusConfig.MetricsPath, prom.MetricsHandler) 291 e.GET(prom.Config.MetricsPath, prom.MetricsHandler, glmw.NewIPFilter(mips))
225 } 292 }
226 293
227 return &EchoWrapper{ 294 return &EchoWrapper{
228 Echo: e, 295 Echo: e,
229 tlsServer: ts, 296 servers: servers,
230 bindAddress: c.BindAddress, 297 tlsServers: tlsServers,
231 ocspErrors: cmek, 298 ocspErrors: cmek,
232 ocspManager: cm, 299 ocspManager: cm,
233 templateFS: templates, 300 templateFS: templates,
diff --git a/echo/middleware/ip_filter.go b/echo/middleware/ip_filter.go
new file mode 100644
index 0000000..007791e
--- /dev/null
+++ b/echo/middleware/ip_filter.go
@@ -0,0 +1,39 @@
1package middleware
2
3import (
4 "net"
5
6 "github.com/labstack/echo/v4"
7)
8
9func NewIPFilter(allowedRanges []*net.IPNet) echo.MiddlewareFunc {
10 return func(next echo.HandlerFunc) echo.HandlerFunc {
11 return func(c echo.Context) error {
12 if allowedRanges == nil {
13 c.Logger().Error("No allowed IPs configured for filter")
14 return echo.ErrNotFound
15 }
16
17 ip := net.ParseIP(c.RealIP())
18 if ip == nil {
19 c.Logger().Error("Unable to parse IP in IPFilter")
20 return echo.ErrNotFound
21 }
22
23 found := false
24 for _, ipnet := range allowedRanges {
25 if ipnet.Contains(ip) {
26 found = true
27 break
28 }
29 }
30
31 if !found {
32 c.Logger().Errorf("IP %s not in range for filter", c.RealIP())
33 return echo.ErrNotFound
34 }
35
36 return next(c)
37 }
38 }
39}
diff --git a/echo/middleware/redirect.go b/echo/middleware/redirect.go
new file mode 100644
index 0000000..134bfee
--- /dev/null
+++ b/echo/middleware/redirect.go
@@ -0,0 +1,75 @@
1package middleware
2
3/*
4HTTP to HTTPS Redirect Middleware
5
6This is a duplicate of existing functionality in Echo because the Echo default
7middleware doesn't support redirecting to a different HTTPS port which is
8needed in dev environments and some prod environments where the server runs on
9an off-port.
10*/
11
12import (
13 "fmt"
14 "net"
15 "net/http"
16
17 "github.com/labstack/echo/v4"
18 "github.com/labstack/echo/v4/middleware"
19)
20
21type HTTPSRedirectConfig struct {
22 Skipper middleware.Skipper
23 Port int
24 Code int
25}
26
27var DefaultHTTPSRedirectConfig = HTTPSRedirectConfig{
28 Skipper: middleware.DefaultSkipper,
29 Port: 443,
30 Code: http.StatusMovedPermanently,
31}
32
33func HTTPSRedirect() echo.MiddlewareFunc {
34 return HTTPSRedirectWithConfig(DefaultHTTPSRedirectConfig)
35}
36
37func HTTPSRedirectWithConfig(config HTTPSRedirectConfig) echo.MiddlewareFunc {
38 if config.Skipper == nil {
39 config.Skipper = DefaultHTTPSRedirectConfig.Skipper
40 }
41 if config.Code == 0 {
42 config.Code = DefaultHTTPSRedirectConfig.Code
43 }
44 if config.Port == 0 {
45 config.Port = DefaultHTTPSRedirectConfig.Port
46 }
47
48 return func(next echo.HandlerFunc) echo.HandlerFunc {
49 return func(c echo.Context) error {
50 if config.Skipper(c) || c.Scheme() == "https" {
51 return next(c)
52 }
53
54 var err error
55 req := c.Request()
56
57 host := req.URL.Host
58 if host == "" {
59 host, _, err = net.SplitHostPort(req.Host)
60 if err != nil {
61 return echo.ErrBadRequest
62 }
63 }
64
65 // Browers assume 443 if it's an https request, otherwise the port
66 // needs to be specified in the URL
67 redir := fmt.Sprintf("https://%s%s", host, req.RequestURI)
68 if config.Port != 443 {
69 redir = fmt.Sprintf("https://%s:%d%s", host, config.Port, req.RequestURI)
70 }
71
72 return c.Redirect(http.StatusMovedPermanently, redir)
73 }
74 }
75}
diff --git a/echo/prometheus/prometheus.go b/echo/prometheus/prometheus.go
index 616e425..2fbf252 100644
--- a/echo/prometheus/prometheus.go
+++ b/echo/prometheus/prometheus.go
@@ -14,7 +14,7 @@ import (
14) 14)
15 15
16type Prometheus struct { 16type Prometheus struct {
17 config *PrometheusConfig 17 Config *PrometheusConfig
18 requestCount *prometheus.CounterVec 18 requestCount *prometheus.CounterVec
19 requestDuration *prometheus.HistogramVec 19 requestDuration *prometheus.HistogramVec
20 requestSize *prometheus.HistogramVec 20 requestSize *prometheus.HistogramVec
@@ -65,7 +65,7 @@ func NewPrometheusWithConfig(c *PrometheusConfig) *Prometheus {
65 } 65 }
66 66
67 return &Prometheus{ 67 return &Prometheus{
68 config: c, 68 Config: c,
69 MetricsHandler: echo.WrapHandler(promhttp.Handler()), 69 MetricsHandler: echo.WrapHandler(promhttp.Handler()),
70 requestCount: promauto.NewCounterVec(prometheus.CounterOpts{ 70 requestCount: promauto.NewCounterVec(prometheus.CounterOpts{
71 Subsystem: c.Subsystem, 71 Subsystem: c.Subsystem,
@@ -92,11 +92,11 @@ func NewPrometheusWithConfig(c *PrometheusConfig) *Prometheus {
92 92
93func (p *Prometheus) MiddlewareHandler(next echo.HandlerFunc) echo.HandlerFunc { 93func (p *Prometheus) MiddlewareHandler(next echo.HandlerFunc) echo.HandlerFunc {
94 return func(c echo.Context) error { 94 return func(c echo.Context) error {
95 if c.Path() == p.config.MetricsPath { 95 if c.Path() == p.Config.MetricsPath {
96 return next(c) 96 return next(c)
97 } 97 }
98 98
99 if p.config.Skipper(c) { 99 if p.Config.Skipper(c) {
100 return next(c) 100 return next(c)
101 } 101 }
102 102
@@ -116,9 +116,9 @@ func (p *Prometheus) MiddlewareHandler(next echo.HandlerFunc) echo.HandlerFunc {
116 } 116 }
117 } 117 }
118 118
119 url := p.config.ExtractUrl(c) 119 url := p.Config.ExtractUrl(c)
120 if len(p.config.ContextLabel) > 0 { 120 if len(p.Config.ContextLabel) > 0 {
121 u := c.Get(p.config.ContextLabel) 121 u := c.Get(p.Config.ContextLabel)
122 if u == nil { 122 if u == nil {
123 u = "unknown" 123 u = "unknown"
124 } 124 }
@@ -128,7 +128,7 @@ func (p *Prometheus) MiddlewareHandler(next echo.HandlerFunc) echo.HandlerFunc {
128 s := strconv.Itoa(status) 128 s := strconv.Itoa(status)
129 m := c.Request().Method 129 m := c.Request().Method
130 p.requestDuration.WithLabelValues(s, m, url).Observe(elapsed) 130 p.requestDuration.WithLabelValues(s, m, url).Observe(elapsed)
131 p.requestCount.WithLabelValues(s, m, p.config.ExtractHost(c), url).Inc() 131 p.requestCount.WithLabelValues(s, m, p.Config.ExtractHost(c), url).Inc()
132 p.requestSize.WithLabelValues(s, m, url).Observe(float64(reqSz)) 132 p.requestSize.WithLabelValues(s, m, url).Observe(float64(reqSz))
133 p.responseSize.WithLabelValues(s, m, url).Observe(float64(c.Response().Size)) 133 p.responseSize.WithLabelValues(s, m, url).Observe(float64(c.Response().Size))
134 134