diff --git a/forwarding.go b/forwarding.go deleted file mode 100644 index be316ec..0000000 --- a/forwarding.go +++ /dev/null @@ -1,80 +0,0 @@ -package traefik_umami_feeder - -// Copied source from -// https://github.com/kzmake/traefik-plugin-forward-request/blob/master/util.go - -import ( - "net" - "net/http" - "strings" -) - -const ( - xForwardedProto = "x-forwarded-proto" - xForwardedFor = "x-forwarded-for" - xForwardedHost = "x-forwarded-host" - xForwardedPort = "x-forwarded-port" - xForwardedURI = "x-forwarded-uri" - xForwardedMethod = "x-forwarded-method" -) - -func copyHeaders(dst, src http.Header, headersToCopy []string) { - for _, key := range headersToCopy { - if values := src.Values(key); len(values) > 0 { - dst[key] = values - } - } -} - -func writeXForwardedHeaders(dst http.Header, req *http.Request) { - if clientIP, _, err := net.SplitHostPort(req.RemoteAddr); err == nil { - if values := req.Header.Values(xForwardedFor); len(values) > 0 { - clientIP = strings.Join(values, ", ") + ", " + clientIP - } - dst.Set(xForwardedFor, clientIP) - } - - xfm := req.Header.Get(xForwardedMethod) - switch { - case xfm != "": - dst.Set(xForwardedMethod, xfm) - case req.Method != "": - dst.Set(xForwardedMethod, req.Method) - default: - dst.Del(xForwardedMethod) - } - - xfp := req.Header.Get(xForwardedProto) - switch { - case xfp != "": - dst.Set(xForwardedProto, xfp) - case req.TLS != nil: - dst.Set(xForwardedProto, "https") - default: - dst.Set(xForwardedProto, "http") - } - - if xfp := req.Header.Get(xForwardedPort); xfp != "" { - dst.Set(xForwardedPort, xfp) - } - - xfh := req.Header.Get(xForwardedHost) - switch { - case xfh != "": - dst.Set(xForwardedHost, xfh) - case req.Host != "": - dst.Set(xForwardedHost, req.Host) - default: - dst.Del(xForwardedHost) - } - - xfu := req.Header.Get(xForwardedURI) - switch { - case xfu != "": - dst.Set(xForwardedURI, xfu) - case req.URL.RequestURI() != "": - dst.Set(xForwardedURI, req.URL.RequestURI()) - default: - dst.Del(xForwardedURI) - } -} diff --git a/umami.go b/umami.go index 2cafacb..414f0b4 100644 --- a/umami.go +++ b/umami.go @@ -91,7 +91,7 @@ type UmamiFeeder struct { isDebug bool isDisabled bool logHandler *log.Logger - queue chan *UmamiPayload + queue chan *UmamiEvent umamiHost string umamiToken string @@ -119,7 +119,7 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h isDisabled: config.Disabled, logHandler: log.New(os.Stdout, "", 0), // Umami API does not support batching https://github.com/umami-software/umami/discussions/1473 - queue: make(chan *UmamiPayload, config.QueueSize), + queue: make(chan *UmamiEvent, config.QueueSize), umamiHost: config.UmamiHost, umamiToken: config.UmamiToken, diff --git a/umami_utils.go b/umami_utils.go index 848bca5..d54a751 100644 --- a/umami_utils.go +++ b/umami_utils.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" "regexp" "strings" @@ -83,7 +84,6 @@ func sendRequestAndParse(ctx context.Context, url string, body interface{}, head return nil } -// opts the port from the host. func parseDomainFromHost(host string) string { // check if the host has a port if strings.Contains(host, ":") { @@ -103,3 +103,36 @@ func parseAcceptLanguage(acceptLanguage string) string { } return matches[0][1] } + +func extractRemoteIP(req *http.Request) string { + if ip := req.Header.Get("CF-Connecting-IP"); ip != "" { + return ip + } + + if ip := req.Header.Get("x-vercel-ip"); ip != "" { + return ip + } + + // Standard proxy headers + if xff := req.Header.Get("X-Forwarded-For"); xff != "" { + ips := strings.Split(xff, ",") + if len(ips) > 0 { + return strings.TrimSpace(ips[0]) + } + } + + if xrip := req.Header.Get("X-Real-IP"); xrip != "" { + return xrip + } + + // Direct connection + if req.RemoteAddr != "" { + ip, _, err := net.SplitHostPort(req.RemoteAddr) + if err == nil { + return ip + } + return req.RemoteAddr + } + + return "" +} diff --git a/umami_worker.go b/umami_worker.go index d7bbbc5..ef7a691 100644 --- a/umami_worker.go +++ b/umami_worker.go @@ -4,67 +4,42 @@ import ( "context" "fmt" "net/http" + "time" ) -type SendPayload struct { - Website string `json:"website"` - Hostname string `json:"hostname"` - Language string `json:"language,omitempty"` - Referrer string `json:"referrer,omitempty"` - Url string `json:"url"` - //Ip string `json:"ip,omitempty"` - //Data map[string]interface{} `json:"data,omitempty"` // Additional data for the event - //Name string `json:"name,omitempty"` // Event name (for custom events) +type UmamiEvent struct { + Website string `json:"website"` // Website ID + Hostname string `json:"hostname"` // Name of host + Language string `json:"language,omitempty"` // Language of visitor (ex. "en-US") + Referrer string `json:"referrer,omitempty"` // Referrer URL + Url string `json:"url"` // Page URL + Ip string `json:"ip,omitempty"` // IP address + UserAgent string `json:"userAgent,omitempty"` // User agent + Timestamp int64 `json:"timestamp,omitempty"` // UNIX timestamp in seconds + //Data map[string]interface{} `json:"data,omitempty"` // Additional data for the event + //Name string `json:"name,omitempty"` // Event name (for custom events) //Screen string `json:"screen,omitempty"` // Screen resolution (ex. "1920x1080") - //Tag string `json:"tag,omitempty"` - //Title string `json:"title,omitempty"` // Page title + //Title string `json:"title,omitempty"` // Page title } type SendBody struct { - Payload SendPayload `json:"payload"` + Payload *UmamiEvent `json:"payload"` Type string `json:"type"` } -type UmamiPayload struct { - body SendBody - headers http.Header -} - -var headersToCopy = []string{ - "User-Agent", - "X-Real-Op", - "X-Forwarded-For", - "cf-ipcountry", - "cf-region-code", - "cf-ipcity", - "cf-connecting-ip", - "x-vercel-ip-country", - "x-vercel-ip-country-region", - "x-vercel-ip-city", -} - -// Copied and adapted from https://github.com/safing/plausiblefeeder/blob/master/event.go -// Licensed as MIT license - func (h *UmamiFeeder) submitToFeed(req *http.Request, code int) { - body := SendBody{ - Payload: SendPayload{ - Hostname: parseDomainFromHost(req.Host), - Language: parseAcceptLanguage(req.Header.Get("Accept-Language")), - Referrer: req.Referer(), - Url: req.URL.String(), - }, - Type: "event", + event := &UmamiEvent{ + Hostname: parseDomainFromHost(req.Host), + Language: parseAcceptLanguage(req.Header.Get("Accept-Language")), + Referrer: req.Referer(), + Url: req.URL.String(), + Ip: extractRemoteIP(req), + UserAgent: req.Header.Get("User-Agent"), + Timestamp: time.Now().Unix(), } - var headers = make(http.Header) - copyHeaders(headers, req.Header, headersToCopy) - writeXForwardedHeaders(headers, req) - - payload := &UmamiPayload{body: body, headers: headers} - select { - case h.queue <- payload: + case h.queue <- event: default: h.error("failed to submit event: queue full") } @@ -103,8 +78,8 @@ func (h *UmamiFeeder) umamiEventFeeder(ctx context.Context) (err error) { } } -func (h *UmamiFeeder) reportEventToUmami(ctx context.Context, event *UmamiPayload) { - hostname := event.body.Payload.Hostname +func (h *UmamiFeeder) reportEventToUmami(ctx context.Context, event *UmamiEvent) { + hostname := event.Hostname websiteId, ok := h.websites[hostname] if !ok { website, err := createWebsite(ctx, h.umamiHost, h.umamiToken, h.umamiTeamId, hostname) @@ -121,10 +96,15 @@ func (h *UmamiFeeder) reportEventToUmami(ctx context.Context, event *UmamiPayloa h.error("skip tracking, websiteId is unknown: " + hostname) return } - event.body.Payload.Website = websiteId + event.Website = websiteId - h.debug("sending tracking request %v %v", event.body, event.headers) - resp, err := sendRequest(ctx, h.umamiHost+"/api/send", event.body, event.headers) + body := SendBody{ + Payload: event, + Type: "event", + } + + h.debug("sending tracking request %v", event) + resp, err := sendRequest(ctx, h.umamiHost+"/api/send", body, nil) if err != nil { h.error("failed to send tracking: " + err.Error()) return