192 lines
4.5 KiB
Go
Executable File
192 lines
4.5 KiB
Go
Executable File
package ripsawlogger
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
MagicHeaderPrefix = "X-Log-"
|
|
DefaultTokenReader = func(key string, r *http.Request) interface{} {
|
|
return nil
|
|
}
|
|
|
|
tokens = []string{
|
|
"issuer",
|
|
"transactionId", "requestId", "parentRequestId",
|
|
"userId", "brandId",
|
|
"details",
|
|
}
|
|
)
|
|
|
|
type TokenReader func(key string, r *http.Request) interface{}
|
|
|
|
// Because we use X-Log- headers to add stuff to the access logs, we might wind up
|
|
// forwarding them elsewhere too if we try to proxy the request. This dumb piece of
|
|
// work strips them out, calls the handler, and then adds them back in to avoid badness.
|
|
type GhettoLogConcealer struct {
|
|
http.Handler
|
|
}
|
|
|
|
func (ghsp GhettoLogConcealer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// collect all the log headers
|
|
logs := make(map[string][]string)
|
|
for k, v := range r.Header {
|
|
if strings.HasPrefix(k, MagicHeaderPrefix) {
|
|
logs[k] = v
|
|
delete(r.Header, k)
|
|
}
|
|
}
|
|
|
|
// call the normal handler
|
|
ghsp.Handler.ServeHTTP(w, r)
|
|
|
|
// add the log headers back in
|
|
for k, v := range logs {
|
|
r.Header[k] = v
|
|
}
|
|
|
|
}
|
|
|
|
type accessLogger struct {
|
|
Delegate http.Handler
|
|
TokenReader TokenReader
|
|
Logger Logger
|
|
}
|
|
|
|
type monitoredResponseWriter struct {
|
|
http.ResponseWriter
|
|
Status int
|
|
Length int
|
|
}
|
|
|
|
func (rw *monitoredResponseWriter) Write(data []byte) (int, error) {
|
|
if rw.Status == 0 {
|
|
rw.Status = 200
|
|
}
|
|
rw.Length += len(data)
|
|
return rw.ResponseWriter.Write(data)
|
|
}
|
|
|
|
func (rw *monitoredResponseWriter) WriteHeader(status int) {
|
|
rw.Status = status
|
|
rw.ResponseWriter.WriteHeader(status)
|
|
}
|
|
|
|
func (al accessLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// start a timer
|
|
start := time.Now()
|
|
|
|
// determine the host and via headers
|
|
via := make([]string, 0, 1)
|
|
if r.Header.Get("X-Forwarded-For") != "" {
|
|
via = append(via, strings.Split(r.Header.Get("X-Forwarded-For"), ",")...)
|
|
}
|
|
via = append(via, r.RemoteAddr)
|
|
|
|
// trim whitespace from each IP
|
|
for idx, v := range via {
|
|
via[idx] = strings.TrimSpace(v)
|
|
}
|
|
|
|
accessLog := map[string]interface{}{
|
|
"clientIP": via[0],
|
|
"method": r.Method,
|
|
"url": r.URL.String(),
|
|
"httpVersion": fmt.Sprintf("%d.%d", r.ProtoMajor, r.ProtoMinor),
|
|
}
|
|
|
|
// add a variety of custom tokens, if any value is provided
|
|
for _, key := range tokens {
|
|
if value := al.TokenReader(key, r); value != nil {
|
|
accessLog[key] = value
|
|
}
|
|
}
|
|
|
|
// perform the request
|
|
watcher := monitoredResponseWriter{w, 0, 0}
|
|
al.Delegate.ServeHTTP(&watcher, r)
|
|
|
|
// add post-response metadata
|
|
accessLog["time"] = float64(time.Now().Sub(start)) / float64(time.Millisecond)
|
|
accessLog["status"] = watcher.Status
|
|
accessLog["bytes"] = watcher.Length
|
|
|
|
// add the User Agent if there is one
|
|
if len(r.Header["User-Agent"]) > 0 {
|
|
accessLog["userAgent"] = strings.Join(r.Header["User-Agent"], ", ")
|
|
}
|
|
|
|
// add the Referer if there is one
|
|
if len(r.Header["Referer"]) > 0 {
|
|
accessLog["referer"] = strings.Join(r.Header["Referer"], ", ")
|
|
}
|
|
|
|
// add the proxy hops if there are any
|
|
if len(via[1:]) > 0 {
|
|
accessLog["via"] = via[1:]
|
|
}
|
|
|
|
// read magic request headers and add them to the access logs
|
|
for key, values := range r.Header {
|
|
if strings.HasPrefix(key, MagicHeaderPrefix) {
|
|
key := denormalizeHeaderName(key[len(MagicHeaderPrefix):])
|
|
accessLog[key] = strings.Join(values, ", ")
|
|
}
|
|
}
|
|
|
|
// send the log line to the writer
|
|
al.Logger.Log(LogEntry{Level: LOG_ACCESS, Contents: accessLog})
|
|
}
|
|
|
|
func AccessLogger(h http.Handler, tokens TokenReader) http.Handler {
|
|
|
|
// default token implementation
|
|
if tokens == nil {
|
|
tokens = DefaultTokenReader
|
|
}
|
|
|
|
return accessLogger{h, tokens, getDefaultLogger()}
|
|
}
|
|
|
|
func denormalizeHeaderName(key string) string {
|
|
normalizedKey := strings.Split(key, "-")
|
|
normalizedKey[0] = strings.ToLower(normalizedKey[0])
|
|
return strings.Join(normalizedKey, "")
|
|
}
|
|
|
|
func GetTokenFromJWT(key string, r *http.Request) interface{} {
|
|
|
|
jwtTokens := map[string]string{
|
|
"issuer": "iss",
|
|
"userId": "userId",
|
|
"brandId": "brandId",
|
|
}
|
|
|
|
for tokenName, claimName := range jwtTokens {
|
|
if tokenName == key {
|
|
if tokenParts := strings.Split(r.Header.Get("X-JWT"), "."); len(tokenParts) == 3 {
|
|
if claimStr, err := base64.RawURLEncoding.DecodeString(tokenParts[1]); err == nil {
|
|
|
|
// parse out the JWT claims
|
|
claims := make(map[string]interface{})
|
|
if err := json.Unmarshal([]byte(claimStr), &claims); err != nil {
|
|
return nil
|
|
}
|
|
// check for our key in the claims
|
|
if value, ok := claims[claimName]; ok {
|
|
return value
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|