-
Notifications
You must be signed in to change notification settings - Fork 1
/
middleware.go
231 lines (199 loc) · 7.39 KB
/
middleware.go
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
package aurora
import (
"context"
"net/http"
"strings"
"time"
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/diamnet/go/services/aurora/internal/errors"
"github.com/diamnet/go/services/aurora/internal/hchi"
"github.com/diamnet/go/services/aurora/internal/httpx"
"github.com/diamnet/go/services/aurora/internal/render"
hProblem "github.com/diamnet/go/services/aurora/internal/render/problem"
"github.com/diamnet/go/support/log"
"github.com/diamnet/go/support/render/problem"
)
// appContextMiddleware adds the "app" context into every request, so that subsequence appContextMiddleware
// or handlers can retrieve a aurora.App instance
func appContextMiddleware(app *App) func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := withAppContext(r.Context(), app)
h.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// requestCacheHeadersMiddleware adds caching headers to each response.
func requestCacheHeadersMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Before changing this read Stack Overflow answer about staled request
// in older versions of Chrome:
// https://stackoverflow.com/questions/27513994/chrome-stalls-when-making-multiple-requests-to-same-resource
w.Header().Set("Cache-Control", "no-cache, no-store, max-age=0")
h.ServeHTTP(w, r)
})
}
func contextMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = hchi.WithChiRequestID(ctx)
ctx, cancel := httpx.RequestContext(ctx, w, r)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
const (
clientNameHeader = "X-Client-Name"
clientVersionHeader = "X-Client-Version"
appNameHeader = "X-App-Name"
appVersionHeader = "X-App-Version"
)
// loggerMiddleware logs http requests and resposnes to the logging subsytem of aurora.
func loggerMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
mw := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
logger := log.WithField("req", middleware.GetReqID(ctx))
ctx = log.Set(ctx, logger)
// Checking `Accept` header from user request because if the streaming connection
// is reset before sending the first event no Content-Type header is sent in a response.
acceptHeader := r.Header.Get("Accept")
streaming := strings.Contains(acceptHeader, render.MimeEventStream)
logStartOfRequest(ctx, r, streaming)
then := time.Now()
h.ServeHTTP(mw, r.WithContext(ctx))
duration := time.Since(then)
logEndOfRequest(ctx, r, duration, mw, streaming)
})
}
// getClientData gets client data (name or version) from header or GET parameter
// (useful when not possible to set headers, like in EventStream).
func getClientData(r *http.Request, headerName string) string {
value := r.Header.Get(headerName)
if value != "" {
return value
}
value = r.URL.Query().Get(headerName)
if value == "" {
value = "undefined"
}
return value
}
func logStartOfRequest(ctx context.Context, r *http.Request, streaming bool) {
log.Ctx(ctx).WithFields(log.F{
"client_name": getClientData(r, clientNameHeader),
"client_version": getClientData(r, clientVersionHeader),
"app_name": getClientData(r, appNameHeader),
"app_version": getClientData(r, appVersionHeader),
"forwarded_ip": firstXForwardedFor(r),
"host": r.Host,
"ip": remoteAddrIP(r),
"ip_port": r.RemoteAddr,
"method": r.Method,
"path": r.URL.String(),
"streaming": streaming,
}).Info("Starting request")
}
func logEndOfRequest(ctx context.Context, r *http.Request, duration time.Duration, mw middleware.WrapResponseWriter, streaming bool) {
routePattern := chi.RouteContext(r.Context()).RoutePattern()
// Can be empty when request did not reached the final route (ex. blocked by
// a middleware). More info: https://github.com/go-chi/chi/issues/270
if routePattern == "" {
routePattern = "undefined"
}
log.Ctx(ctx).WithFields(log.F{
"bytes": mw.BytesWritten(),
"client_name": getClientData(r, clientNameHeader),
"client_version": getClientData(r, clientVersionHeader),
"app_name": getClientData(r, appNameHeader),
"app_version": getClientData(r, appVersionHeader),
"duration": duration.Seconds(),
"forwarded_ip": firstXForwardedFor(r),
"host": r.Host,
"ip": remoteAddrIP(r),
"ip_port": r.RemoteAddr,
"method": r.Method,
"path": r.URL.String(),
"route": routePattern,
"status": mw.Status(),
"streaming": streaming,
}).Info("Finished request")
}
func firstXForwardedFor(r *http.Request) string {
return strings.TrimSpace(strings.SplitN(r.Header.Get("X-Forwarded-For"), ",", 2)[0])
}
func (w *web) RateLimitMiddleware(next http.Handler) http.Handler {
if w.rateLimiter == nil {
return next
}
return w.rateLimiter.RateLimit(next)
}
// recoverMiddleware helps the server recover from panics. It ensures that
// no request can fully bring down the aurora server, and it also logs the
// panics to the logging subsystem.
func recoverMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
defer func() {
if rec := recover(); rec != nil {
err := errors.FromPanic(rec)
errors.ReportToSentry(err, r)
problem.Render(ctx, w, err)
}
}()
h.ServeHTTP(w, r)
})
}
// requestMetricsMiddleware records success and failures using a meter, and times every request
func requestMetricsMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app := AppFromContext(r.Context())
mw := middleware.NewWrapResponseWriter(w, r.ProtoMajor)
app.web.requestTimer.Time(func() {
h.ServeHTTP(mw.(http.ResponseWriter), r)
})
if 200 <= mw.Status() && mw.Status() < 400 {
// a success is in [200, 400)
app.web.successMeter.Mark(1)
} else if 400 <= mw.Status() && mw.Status() < 600 {
// a success is in [400, 600)
app.web.failureMeter.Mark(1)
}
})
}
// acceptOnlyJSON inspects the accept header of the request and responds with
// an error if the content type is not JSON
func acceptOnlyJSON(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
contentType := render.Negotiate(r)
if contentType != render.MimeHal && contentType != render.MimeJSON {
problem.Render(r.Context(), w, hProblem.NotAcceptable)
return
}
h.ServeHTTP(w, r)
})
}
// requiresExperimentalIngestion is a middleware which enables a handler
// if the experimental ingestion system is enabled and initialized
func requiresExperimentalIngestion(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
app := AppFromContext(ctx)
if !app.config.EnableExperimentalIngestion {
w.WriteHeader(http.StatusNotFound)
return
}
lastIngestedLedger, err := app.HistoryQ().GetLastLedgerExpIngestNonBlocking()
if err != nil {
problem.Render(r.Context(), w, err)
return
}
// expingest has not finished processing any ledger so no data.
if lastIngestedLedger == 0 {
problem.Render(r.Context(), w, hProblem.StillIngesting)
return
}
h.ServeHTTP(w, r)
})
}