Repository navigation
Expand file tree
/
Copy pathserver.go
More file actions
353 lines (276 loc) · 9.81 KB
/
Copy pathserver.go
File metadata and controls
353 lines (276 loc) · 9.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
package ehttp
import (
"context"
"fmt"
"html/template"
"net"
"net/http"
"os/signal"
"strconv"
"syscall"
"time"
"go.uber.org/zap"
"github.com/EqualSoftware/ehttp/internal/redact"
)
type serverOption func(*Server)
const (
defaultReadHeaderTimeout = 10 * time.Second
defaultShutdownTimeout = 25 * time.Second
)
type Server struct {
router *Router
port string
readHeaderTimeout time.Duration
readTimeout time.Duration
writeTimeout time.Duration
idleTimeout time.Duration
maxHeaderBytes int
shutdownTimeout time.Duration
}
// Mode picks the preset of the server logger.
type Mode int
const (
// Production is the default. The logger writes JSON at info level.
Production Mode = iota
// Development writes console output at debug level.
Development
)
// WithMode picks the logger preset of the server. WithLogger wins over it,
// in any order of the options. An error from zap panics at boot.
func WithMode(m Mode) serverOption {
return func(s *Server) { s.router.mode = m }
}
// WithLogger sets the logger of the server instead of the preset of WithMode.
// A nil logger panics. The server never changes the global zap logger.
func WithLogger(l *zap.Logger) serverOption {
if l == nil {
bootPanic("WithLogger", "a nil logger", "the server needs a logger for its own messages",
"pass a *zap.Logger, or remove WithLogger to use the preset of WithMode")
}
return func(s *Server) { s.router.logger = l }
}
// WithRedactNames adds name patterns to the deny list that hides values in the
// strict-mode logs and messages. It never replaces the default list (passwords,
// secrets, tokens, authentication data, sessions, cookies and similar names).
// A pattern ignores case and treats "-" as "_". "*x*" matches a name that
// contains x, "*x" a name that ends with x, "x*" a name that starts with x and
// "x" the name x. An invalid pattern panics.
func WithRedactNames(patterns ...string) serverOption {
if _, err := redact.Default().With(patterns...); err != nil {
bootPanic("WithRedactNames", "an invalid pattern", err.Error(),
"use a name, or a name with one \"*\" at the start, at the end or at both, for example WithRedactNames(\"*pin\", \"x-internal-*\")")
}
return func(s *Server) {
s.router.redact, _ = s.router.redact.With(patterns...)
}
}
// presetLogger builds the logger of a mode. An error panics at boot.
func presetLogger(m Mode) *zap.Logger {
build := zap.NewProduction
if m == Development {
build = zap.NewDevelopment
}
l, err := build()
if err != nil {
bootPanic("WithMode", "the logger preset", "zap cannot build it: "+err.Error(),
"pass your own logger with WithLogger")
}
return l
}
// mustBeNonNegative panics when a duration option is negative.
func mustBeNonNegative(option string, d time.Duration) {
if d < 0 {
bootPanic(option, "duration "+d.String(), "a timeout cannot be negative (net/http.Server)",
"use zero for no limit, or a positive duration, for example "+option+"(30 * time.Second)")
}
}
// WithTrustedProxies sets the peers that can send X-Forwarded-* headers.
// A value is a CIDR, a single IP, or loopback, linklocal or uniquelocal.
// Without it, no peer is trusted. An invalid value panics.
func WithTrustedProxies(cidrs ...string) serverOption {
list := parseTrustedProxies(cidrs)
return func(s *Server) {
s.router.proxies = list
}
}
// WithReadHeaderTimeout sets http.Server.ReadHeaderTimeout. The default is 10 s.
func WithReadHeaderTimeout(d time.Duration) serverOption {
mustBeNonNegative("WithReadHeaderTimeout", d)
return func(s *Server) { s.readHeaderTimeout = d }
}
// WithReadTimeout sets http.Server.ReadTimeout.
func WithReadTimeout(d time.Duration) serverOption {
mustBeNonNegative("WithReadTimeout", d)
return func(s *Server) { s.readTimeout = d }
}
// WithWriteTimeout sets http.Server.WriteTimeout.
func WithWriteTimeout(d time.Duration) serverOption {
mustBeNonNegative("WithWriteTimeout", d)
return func(s *Server) { s.writeTimeout = d }
}
// WithIdleTimeout sets http.Server.IdleTimeout.
func WithIdleTimeout(d time.Duration) serverOption {
mustBeNonNegative("WithIdleTimeout", d)
return func(s *Server) { s.idleTimeout = d }
}
// WithMaxHeaderBytes sets http.Server.MaxHeaderBytes.
func WithMaxHeaderBytes(n int) serverOption {
if n < 0 {
bootPanic("WithMaxHeaderBytes", "size "+strconv.Itoa(n), "a size cannot be negative (net/http.Server)",
"use zero for the net/http default, or a positive size, for example WithMaxHeaderBytes(1 << 20)")
}
return func(s *Server) { s.maxHeaderBytes = n }
}
// WithShutdownTimeout sets how long Listen waits for in-flight requests after
// its context is done. The default is 25 s, below the 30 s grace period of
// Kubernetes. Zero means no limit.
func WithShutdownTimeout(d time.Duration) serverOption {
mustBeNonNegative("WithShutdownTimeout", d)
return func(s *Server) { s.shutdownTimeout = d }
}
// WithPort sets the TCP port. It is a decimal number from 0 to 65535, where 0
// lets the OS pick a free port. Any other value panics.
func WithPort(v string) serverOption {
mustBePort("WithPort", v)
return func(s *Server) {
s.port = v
}
}
// WithPortInt is WithPort with a number.
func WithPortInt(v int64) serverOption {
port := strconv.FormatInt(v, 10)
mustBePort("WithPortInt", port)
return func(s *Server) {
s.port = port
}
}
// mustBePort panics when v is not a decimal number from 0 to 65535.
func mustBePort(option, v string) {
if validPortNumber(v) {
return
}
bootPanic(option, "port "+quoteValue(v), "a TCP port is a decimal number from 0 to 65535",
"pass a port such as "+option+"(\"8080\"), or \"0\" for a free port")
}
func validPortNumber(v string) bool {
for i := 0; i < len(v); i++ {
if v[i] < '0' || v[i] > '9' {
return false
}
}
_, err := strconv.ParseUint(v, 10, 16)
return err == nil
}
func WithTemplates(v string) serverOption {
if v == "" {
bootPanic("WithTemplates", "pattern \"\"", "the glob is empty, so it matches no file",
"pass a glob that matches valid template files, for example \"views/*.html\"")
}
return func(s *Server) {
t, err := template.ParseGlob(v)
if err != nil {
panic(fmt.Errorf("ehttp: pattern %s in WithTemplates: html/template cannot load it (%w). Fix: %s", quoteValue(v), err, "pass a glob that matches valid template files, for example \"views/*.html\"."))
}
s.router.template = t
}
}
func NewServer(options ...serverOption) *Server {
server := &Server{
port: "3000",
readHeaderTimeout: defaultReadHeaderTimeout,
shutdownTimeout: defaultShutdownTimeout,
router: &Router{},
}
for _, apply := range options {
apply(server)
}
if server.router.logger == nil {
server.router.logger = presetLogger(server.router.mode)
}
return server
}
func (s *Server) Use(m ...MiddlewareHandlerFunc) {
s.router.Use(m...)
}
func (s *Server) Route(prefix string, m ...MiddlewareHandlerFunc) *Router {
return s.router.Route(prefix, m...)
}
func (s *Server) GET(pattern string, h HandlerFunc, m ...MiddlewareHandlerFunc) {
s.router.GET(pattern, h, m...)
}
func (s *Server) POST(pattern string, h HandlerFunc, m ...MiddlewareHandlerFunc) {
s.router.POST(pattern, h, m...)
}
func (s *Server) PUT(pattern string, h HandlerFunc, m ...MiddlewareHandlerFunc) {
s.router.PUT(pattern, h, m...)
}
func (s *Server) PATCH(pattern string, h HandlerFunc, m ...MiddlewareHandlerFunc) {
s.router.PATCH(pattern, h, m...)
}
func (s *Server) DELETE(pattern string, h HandlerFunc, m ...MiddlewareHandlerFunc) {
s.router.DELETE(pattern, h, m...)
}
func (s *Server) CONNECT(pattern string, h HandlerFunc, m ...MiddlewareHandlerFunc) {
s.router.CONNECT(pattern, h, m...)
}
func (s *Server) TRACE(pattern string, h HandlerFunc, m ...MiddlewareHandlerFunc) {
s.router.TRACE(pattern, h, m...)
}
// ServeHTTP makes Server an http.Handler that serves requests through its router.
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.router.ServeHTTP(w, r)
}
// Listen serves until c is done, then shuts down gracefully.
// It returns a bind or serve error. A clean shutdown returns nil.
func (s *Server) Listen(c context.Context) error {
server := s.httpServer()
ctx, quit := signal.NotifyContext(c, syscall.SIGINT, syscall.SIGTERM)
defer quit()
ln, err := (&net.ListenConfig{}).Listen(ctx, "tcp", server.Addr)
if err != nil {
return err
}
s.router.logger.Info("server is running", zap.String("port", s.port))
serveErr := make(chan error, 1)
go func() { serveErr <- server.Serve(ln) }()
select {
case err := <-serveErr:
_ = server.Close()
s.router.logger.Error("shutting down the server with error", zap.Error(err))
return err
case <-ctx.Done():
}
// Release the signal handler: a second signal takes the Go default and
// ends the process while the shutdown waits.
quit()
s.router.logger.Info("shutting down the server gracefully")
return s.shutdown(context.WithoutCancel(ctx), server, serveErr)
}
// httpServer builds the http.Server from the options of s.
func (s *Server) httpServer() *http.Server {
return &http.Server{
Addr: ":" + s.port,
Handler: s.router,
DisableGeneralOptionsHandler: true,
ReadHeaderTimeout: s.readHeaderTimeout,
ReadTimeout: s.readTimeout,
WriteTimeout: s.writeTimeout,
IdleTimeout: s.idleTimeout,
MaxHeaderBytes: s.maxHeaderBytes,
}
}
// shutdown gets a context without the cancel of the parent, which is done.
// Its deadline is the shutdown timeout.
func (s *Server) shutdown(ctx context.Context, server *http.Server, serveErr <-chan error) error {
if s.shutdownTimeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, s.shutdownTimeout)
defer cancel()
}
err := server.Shutdown(ctx)
if err != nil {
_ = server.Close()
}
<-serveErr //nolint:errcheck // Serve returns ErrServerClosed after Shutdown
return err
}