aboutsummaryrefslogtreecommitdiff
path: root/echo/echo_default.go
diff options
context:
space:
mode:
Diffstat (limited to 'echo/echo_default.go')
-rw-r--r--echo/echo_default.go185
1 files changed, 185 insertions, 0 deletions
diff --git a/echo/echo_default.go b/echo/echo_default.go
new file mode 100644
index 0000000..ab163ff
--- /dev/null
+++ b/echo/echo_default.go
@@ -0,0 +1,185 @@
1package echo
2
3import (
4 "context"
5 "crypto/tls"
6 "fmt"
7 "html/template"
8 "net/http"
9 "sync"
10
11 glmw "code.crute.us/mcrute/golib/echo/middleware"
12 gltls "code.crute.us/mcrute/golib/tls"
13
14 "github.com/labstack/echo/v4"
15 "github.com/labstack/echo/v4/middleware"
16 "github.com/labstack/gommon/log"
17)
18
19type EchoConfig struct {
20 Debug bool
21 BindAddress string
22 BindTLSAddress string
23 TLSCert string
24 TLSKey string
25 TrustedProxyIPRanges []string
26 TemplatePath *string
27 TemplateGlob *string
28 TemplateFunctions template.FuncMap
29 CombinedHostLogFile string
30 RedirectToWWW bool
31 ContentSecurityPolicy *glmw.ContentSecurityPolicyConfig
32}
33
34type EchoWrapper struct {
35 *echo.Echo
36 tlsServer http.Server
37 bindAddress string
38 ocspErrors chan gltls.OcspError
39 ocspManager *gltls.OcspManager
40 initDone bool
41}
42
43// Init does "expensive" work that requires network calls but must be called
44// before using the returned echo instance
45func (w *EchoWrapper) Init() error {
46 if err := w.ocspManager.Init(); err != nil {
47 return fmt.Errorf("Error loading TLS certificates and stapling: %w", err)
48 }
49 w.initDone = true
50 return nil
51}
52
53func (w *EchoWrapper) CachedStaticRoute(prefix, path string) {
54 w.Group(prefix, glmw.CacheOneMonthMiddleware).Static("/", path)
55}
56
57func (w *EchoWrapper) OcspErrors() chan gltls.OcspError {
58 return w.ocspErrors
59}
60
61func (w *EchoWrapper) RunCertificateManager(ctx context.Context, wg *sync.WaitGroup) error {
62 return w.ocspManager.Run(ctx, wg)
63}
64
65func (w *EchoWrapper) run(ctx context.Context, wg *sync.WaitGroup, f func() error, sf func(context.Context) error) error {
66 if !w.initDone {
67 return fmt.Errorf("Echo is not initialized. Call Init()")
68 }
69
70 wg.Add(1)
71 defer wg.Done()
72
73 err := make(chan error)
74 go func() { err <- f() }()
75 select {
76 case e := <-err:
77 return e
78 default:
79 }
80
81 select {
82 case <-ctx.Done():
83 w.Logger.Info("Shutting down web server")
84 return sf(ctx)
85 }
86}
87
88func (w *EchoWrapper) Serve(ctx context.Context, wg *sync.WaitGroup) error {
89 return w.run(
90 ctx,
91 wg,
92 func() error { return w.Echo.Start(w.bindAddress) },
93 func(ctx context.Context) error { return w.Echo.Shutdown(ctx) },
94 )
95}
96
97func (w *EchoWrapper) ServeTLS(ctx context.Context, wg *sync.WaitGroup) error {
98 return w.run(
99 ctx,
100 wg,
101 func() error { return w.tlsServer.ListenAndServeTLS("", "") },
102 func(ctx context.Context) error { return w.tlsServer.Shutdown(ctx) },
103 )
104}
105
106// NewDefaultEchoWithConfig builds a wrapper around an Echo instance and
107// configures it in the default way that it should probably be configured in
108// all cases. The struct returned from this function can be treated like a
109// normal Echo instance (because, for the most part it is).
110//
111// Consumers must call Init() on the returned object before serving with it.
112func NewDefaultEchoWithConfig(c EchoConfig) (*EchoWrapper, error) {
113 var err error
114
115 e := echo.New()
116 e.Debug = c.Debug
117
118 e.IPExtractor, err = glmw.ExtractIPFromXFFHeaders(false, c.TrustedProxyIPRanges)
119 if err != nil {
120 return nil, fmt.Errorf("Error building XFF IP extractor: %w", err)
121 }
122
123 // Only install template handlers if the path and glob are set
124 if c.TemplatePath != nil && c.TemplateGlob != nil {
125 e.HTTPErrorHandler = ErrorHandler(*c.TemplatePath, c.TemplateFunctions)
126
127 tr, err := NewTemplateRenderer(*c.TemplatePath, *c.TemplateGlob, c.TemplateFunctions)
128 if err != nil {
129 return nil, fmt.Errorf("Error loading template renderer: %w", err)
130 }
131 e.Renderer = tr
132 }
133
134 e.Logger.SetLevel(log.INFO)
135 if c.Debug {
136 e.Logger.SetLevel(log.DEBUG)
137 }
138
139 if c.CombinedHostLogFile != "" {
140 lc, err := NginxCombinedHostConfigToFile(c.CombinedHostLogFile)
141 if err != nil {
142 return nil, fmt.Errorf("Error opening log file: %w", err)
143 }
144 e.Use(middleware.LoggerWithConfig(lc))
145 }
146
147 cmek := make(chan gltls.OcspError)
148 cm := &gltls.OcspManager{
149 CertPath: c.TLSCert,
150 KeyPath: c.TLSKey,
151 Errors: cmek,
152 }
153
154 ts := http.Server{
155 Addr: c.BindTLSAddress,
156 TLSConfig: &tls.Config{
157 MinVersion: tls.VersionTLS12,
158 GetCertificate: cm.GetCertificate,
159 },
160 Handler: e,
161 }
162
163 e.Use(middleware.Logger())
164 e.Use(glmw.Recover())
165 e.Use(middleware.HTTPSRedirect())
166 if c.RedirectToWWW {
167 e.Use(middleware.WWWRedirect())
168 }
169 e.Use(middleware.GzipWithConfig(middleware.GzipConfig{Level: 5}))
170 e.Use(glmw.StrictSecure())
171
172 if c.ContentSecurityPolicy != nil {
173 e.Use(glmw.ContentSecurityPolicyWithConfig(*c.ContentSecurityPolicy))
174 } else {
175 return nil, fmt.Errorf("ContentSecurityPolicy is required")
176 }
177
178 return &EchoWrapper{
179 Echo: e,
180 tlsServer: ts,
181 bindAddress: c.BindAddress,
182 ocspErrors: cmek,
183 ocspManager: cm,
184 }, nil
185}