diff options
Diffstat (limited to 'echo/echo_default.go')
-rw-r--r-- | echo/echo_default.go | 185 |
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 @@ | |||
1 | package echo | ||
2 | |||
3 | import ( | ||
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 | |||
19 | type 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 | |||
34 | type 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 | ||
45 | func (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 | |||
53 | func (w *EchoWrapper) CachedStaticRoute(prefix, path string) { | ||
54 | w.Group(prefix, glmw.CacheOneMonthMiddleware).Static("/", path) | ||
55 | } | ||
56 | |||
57 | func (w *EchoWrapper) OcspErrors() chan gltls.OcspError { | ||
58 | return w.ocspErrors | ||
59 | } | ||
60 | |||
61 | func (w *EchoWrapper) RunCertificateManager(ctx context.Context, wg *sync.WaitGroup) error { | ||
62 | return w.ocspManager.Run(ctx, wg) | ||
63 | } | ||
64 | |||
65 | func (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 | |||
88 | func (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 | |||
97 | func (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. | ||
112 | func 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 | } | ||