/
router.go
334 lines (291 loc) · 9.3 KB
/
router.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
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
package api
import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"path"
"runtime/debug"
"strings"
"sync"
"time"
"github.com/gorilla/mux"
"github.com/khulnasoft-lab/gobaseline/log"
"github.com/khulnasoft-lab/gobaseline/utils"
)
// EnableServer defines if the HTTP server should be started.
var EnableServer = true
var (
// mainMux is the main mux router.
mainMux = mux.NewRouter()
// server is the main server.
server = &http.Server{
ReadHeaderTimeout: 10 * time.Second,
}
handlerLock sync.RWMutex
allowedDevCORSOrigins = []string{
"127.0.0.1",
"localhost",
}
)
// RegisterHandler registers a handler with the API endpoint.
func RegisterHandler(path string, handler http.Handler) *mux.Route {
handlerLock.Lock()
defer handlerLock.Unlock()
return mainMux.Handle(path, handler)
}
// RegisterHandleFunc registers a handle function with the API endpoint.
func RegisterHandleFunc(path string, handleFunc func(http.ResponseWriter, *http.Request)) *mux.Route {
handlerLock.Lock()
defer handlerLock.Unlock()
return mainMux.HandleFunc(path, handleFunc)
}
func startServer() {
// Check if server is enabled.
if !EnableServer {
return
}
// Configure server.
server.Addr = listenAddressConfig()
server.Handler = &mainHandler{
// TODO: mainMux should not be modified anymore.
mux: mainMux,
}
// Start server manager.
module.StartServiceWorker("http server manager", 0, serverManager)
}
func stopServer() error {
// Check if server is enabled.
if !EnableServer {
return nil
}
if server.Addr != "" {
return server.Shutdown(context.Background())
}
return nil
}
// Serve starts serving the API endpoint.
func serverManager(_ context.Context) error {
// start serving
log.Infof("api: starting to listen on %s", server.Addr)
backoffDuration := 10 * time.Second
for {
// always returns an error
err := module.RunWorker("http endpoint", func(ctx context.Context) error {
return server.ListenAndServe()
})
// return on shutdown error
if errors.Is(err, http.ErrServerClosed) {
return nil
}
// log error and restart
log.Errorf("api: http endpoint failed: %s - restarting in %s", err, backoffDuration)
time.Sleep(backoffDuration)
}
}
type mainHandler struct {
mux *mux.Router
}
func (mh *mainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
_ = module.RunWorker("http request", func(_ context.Context) error {
return mh.handle(w, r)
})
}
func (mh *mainHandler) handle(w http.ResponseWriter, r *http.Request) error {
// Setup context trace logging.
ctx, tracer := log.AddTracer(r.Context())
// Add request context.
apiRequest := &Request{
Request: r,
}
ctx = context.WithValue(ctx, RequestContextKey, apiRequest)
// Add context back to request.
r = r.WithContext(ctx)
lrw := NewLoggingResponseWriter(w, r)
tracer.Tracef("api request: %s ___ %s %s", r.RemoteAddr, lrw.Request.Method, r.RequestURI)
defer func() {
// Log request status.
if lrw.Status != 0 {
// If lrw.Status is 0, the request may have been hijacked.
tracer.Debugf("api request: %s %d %s %s", lrw.Request.RemoteAddr, lrw.Status, lrw.Request.Method, lrw.Request.RequestURI)
}
tracer.Submit()
}()
// Add security headers.
w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "deny")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("X-DNS-Prefetch-Control", "off")
// Add CSP Header in production mode.
if !devMode() {
w.Header().Set(
"Content-Security-Policy",
"default-src 'self'; "+
"connect-src https://*.safing.io 'self'; "+
"style-src 'self' 'unsafe-inline'; "+
"img-src 'self' data: blob:",
)
}
// Check Cross-Origin Requests.
origin := r.Header.Get("Origin")
isPreflighCheck := false
if origin != "" {
// Parse origin URL.
originURL, err := url.Parse(origin)
if err != nil {
tracer.Warningf("api: denied request from %s: failed to parse origin header: %s", r.RemoteAddr, err)
http.Error(lrw, "Invalid Origin.", http.StatusForbidden)
return nil
}
// Check if the Origin matches the Host.
switch {
case originURL.Host == r.Host:
// Origin (with port) matches Host.
case originURL.Hostname() == r.Host:
// Origin (without port) matches Host.
case originURL.Scheme == "chrome-extension":
// Allow access for the browser extension
// TODO(ppacher):
// This currently allows access from any browser extension.
// Can we reduce that to only our browser extension?
// Also, what do we need to support Firefox?
case devMode() &&
utils.StringInSlice(allowedDevCORSOrigins, originURL.Hostname()):
// We are in dev mode and the request is coming from the allowed
// development origins.
default:
// Origin and Host do NOT match!
tracer.Warningf("api: denied request from %s: Origin (`%s`) and Host (`%s`) do not match", r.RemoteAddr, origin, r.Host)
http.Error(lrw, "Cross-Origin Request Denied.", http.StatusForbidden)
return nil
// If the Host header has a port, and the Origin does not, requests will
// also end up here, as we cannot properly check for equality.
}
// Add Cross-Site Headers now as we need them in any case now.
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "*")
w.Header().Set("Access-Control-Allow-Headers", "*")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Expose-Headers", "*")
w.Header().Set("Access-Control-Max-Age", "60")
w.Header().Add("Vary", "Origin")
// if there's a Access-Control-Request-Method header this is a Preflight check.
// In that case, we will just check if the preflighMethod is allowed and then return
// success here
if preflighMethod := r.Header.Get("Access-Control-Request-Method"); r.Method == http.MethodOptions && preflighMethod != "" {
isPreflighCheck = true
}
}
// Clean URL.
cleanedRequestPath := cleanRequestPath(r.URL.Path)
// If the cleaned URL differs from the original one, redirect to there.
if r.URL.Path != cleanedRequestPath {
redirURL := *r.URL
redirURL.Path = cleanedRequestPath
http.Redirect(lrw, r, redirURL.String(), http.StatusMovedPermanently)
return nil
}
// Get handler for request.
// Gorilla does not support handling this on our own very well.
// See github.com/gorilla/mux.ServeHTTP for reference.
var match mux.RouteMatch
var handler http.Handler
if mh.mux.Match(r, &match) {
handler = match.Handler
apiRequest.Route = match.Route
apiRequest.URLVars = match.Vars
}
switch {
case match.MatchErr == nil:
// All good.
case errors.Is(match.MatchErr, mux.ErrMethodMismatch):
http.Error(lrw, "Method not allowed.", http.StatusMethodNotAllowed)
return nil
default:
tracer.Debug("api: no handler registered for this path")
http.Error(lrw, "Not found.", http.StatusNotFound)
return nil
}
// Be sure that URLVars always is a map.
if apiRequest.URLVars == nil {
apiRequest.URLVars = make(map[string]string)
}
// Check method.
_, readMethod, ok := getEffectiveMethod(r)
if !ok {
http.Error(lrw, "Method not allowed.", http.StatusMethodNotAllowed)
return nil
}
// At this point we know the method is allowed and there's a handler for the request.
// If this is just a CORS-Preflight, we'll accept the request with StatusOK now.
// There's no point in trying to authenticate the request because the Browser will
// not send authentication along a preflight check.
if isPreflighCheck && handler != nil {
lrw.WriteHeader(http.StatusOK)
return nil
}
// Check authentication.
apiRequest.AuthToken = authenticateRequest(lrw, r, handler, readMethod)
if apiRequest.AuthToken == nil {
// Authenticator already replied.
return nil
}
// Wait for the owning module to be ready.
if moduleHandler, ok := handler.(ModuleHandler); ok {
if !moduleIsReady(moduleHandler.BelongsTo()) {
http.Error(lrw, "The API endpoint is not ready yet. Please try again later.", http.StatusServiceUnavailable)
return nil
}
}
// Check if we have a handler.
if handler == nil {
http.Error(lrw, "Not found.", http.StatusNotFound)
return nil
}
// Format panics in handler.
defer func() {
if panicValue := recover(); panicValue != nil {
// Report failure via module system.
me := module.NewPanicError("api request", "custom", panicValue)
me.Report()
// Respond with a server error.
if devMode() {
http.Error(
lrw,
fmt.Sprintf(
"Internal Server Error: %s\n\n%s",
panicValue,
debug.Stack(),
),
http.StatusInternalServerError,
)
} else {
http.Error(lrw, "Internal Server Error.", http.StatusInternalServerError)
}
}
}()
// Handle with registered handler.
handler.ServeHTTP(lrw, r)
return nil
}
// cleanRequestPath cleans and returns a request URL.
func cleanRequestPath(requestPath string) string {
// If the request URL is empty, return a request for "root".
if requestPath == "" || requestPath == "/" {
return "/"
}
// If the request URL does not start with a slash, prepend it.
if !strings.HasPrefix(requestPath, "/") {
requestPath = "/" + requestPath
}
// Clean path to remove any relative parts.
cleanedRequestPath := path.Clean(requestPath)
// Because path.Clean removes a trailing slash, we need to add it back here
// if the original URL had one.
if strings.HasSuffix(requestPath, "/") {
cleanedRequestPath += "/"
}
return cleanedRequestPath
}