171 lines
4.6 KiB
Go
171 lines
4.6 KiB
Go
package traefik_umami_feeder
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
// Config the plugin configuration.
|
|
type Config struct {
|
|
UmamiHost string `json:"umamiHost"`
|
|
// it is optional, but either UmamiToken or Websites should be set
|
|
UmamiToken string `json:"umamiToken"`
|
|
// as an alternative to UmamiToken, you can set UmamiUsername and UmamiPassword to authenticate
|
|
UmamiUsername string `json:"umamiUsername"`
|
|
UmamiPassword string `json:"umamiPassword"`
|
|
// if both UmamiToken and Websites are set, Websites will be used to override the websites in the API
|
|
Websites map[string]string `json:"websites"`
|
|
// if createNewWebsites is set to true, the plugin will create new websites in the API, UmamiToken is required
|
|
CreateNewWebsites bool `json:"createNewWebsites"`
|
|
Debug bool `json:"debug"`
|
|
}
|
|
|
|
// CreateConfig creates the default plugin configuration.
|
|
func CreateConfig() *Config {
|
|
return &Config{
|
|
UmamiHost: "",
|
|
UmamiToken: "",
|
|
UmamiUsername: "",
|
|
UmamiPassword: "",
|
|
Websites: map[string]string{},
|
|
CreateNewWebsites: false,
|
|
Debug: false,
|
|
}
|
|
}
|
|
|
|
// UmamiFeeder a UmamiFeeder plugin.
|
|
type UmamiFeeder struct {
|
|
next http.Handler
|
|
name string
|
|
debug bool
|
|
logHandler *log.Logger
|
|
|
|
UmamiHost string
|
|
UmamiToken string
|
|
Websites map[string]string
|
|
CreateNewWebsites bool
|
|
}
|
|
|
|
// New created a new Demo plugin.
|
|
func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
|
|
// construct
|
|
h := &UmamiFeeder{
|
|
next: next,
|
|
name: name,
|
|
debug: config.Debug,
|
|
logHandler: log.New(os.Stdout, "", 0),
|
|
|
|
UmamiHost: config.UmamiHost,
|
|
UmamiToken: config.UmamiToken,
|
|
Websites: config.Websites,
|
|
CreateNewWebsites: config.CreateNewWebsites,
|
|
}
|
|
|
|
if config.UmamiUsername != "" && config.UmamiPassword != "" {
|
|
token, err := getToken(h.UmamiHost, config.UmamiUsername, config.UmamiPassword)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get token: %w", err)
|
|
}
|
|
if token == "" {
|
|
return nil, fmt.Errorf("retrieved token is empty")
|
|
}
|
|
h.trace("token received %s", token)
|
|
h.UmamiToken = token
|
|
}
|
|
|
|
if h.UmamiHost == "" {
|
|
return nil, fmt.Errorf("`umamiHost` is not set")
|
|
}
|
|
if h.UmamiToken == "" && len(h.Websites) == 0 {
|
|
return nil, fmt.Errorf("either `umamiToken` or `websites` should be set")
|
|
}
|
|
if h.UmamiToken == "" && h.CreateNewWebsites {
|
|
return nil, fmt.Errorf("`umamiToken` is required to create new websites")
|
|
}
|
|
|
|
if h.UmamiToken != "" {
|
|
websites, err := fetchWebsites(h.UmamiHost, h.UmamiToken)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to fetch websites: %w", err)
|
|
}
|
|
|
|
for _, website := range *websites {
|
|
if _, ok := h.Websites[website.Domain]; ok {
|
|
continue
|
|
}
|
|
|
|
h.Websites[website.Domain] = website.ID
|
|
h.trace("fetched websiteId for: %s", website.Domain)
|
|
}
|
|
h.log("websites fetched")
|
|
}
|
|
|
|
return h, nil
|
|
}
|
|
|
|
func (h *UmamiFeeder) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
|
if h.shouldBeTracked(req) {
|
|
go h.trackRequest(req)
|
|
} else {
|
|
h.trace("Tracking skipped %v", req.URL)
|
|
}
|
|
|
|
h.next.ServeHTTP(rw, req)
|
|
}
|
|
|
|
func (h *UmamiFeeder) shouldBeTracked(req *http.Request) bool {
|
|
if h.CreateNewWebsites {
|
|
return true
|
|
}
|
|
|
|
hostname := parseDomainFromHost(req.Host)
|
|
if _, ok := h.Websites[hostname]; ok {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (h *UmamiFeeder) trackRequest(req *http.Request) {
|
|
hostname := parseDomainFromHost(req.Host)
|
|
websiteId, ok := h.Websites[hostname]
|
|
if !ok {
|
|
website, err := createWebsite(h.UmamiHost, h.UmamiToken, hostname)
|
|
if err != nil {
|
|
h.log("failed to create website: " + err.Error())
|
|
return
|
|
}
|
|
|
|
h.Websites[website.Domain] = website.ID
|
|
websiteId = website.ID
|
|
h.trace("created website for: %s", website.Domain)
|
|
}
|
|
|
|
sendBody, sendHeaders := buildSendBody(req, websiteId)
|
|
h.trace("sending tracking request %s with body %v %v", req.URL, sendBody, sendHeaders)
|
|
|
|
_, err := sendRequest(h.UmamiHost+"/api/send", sendBody, sendHeaders)
|
|
if err != nil {
|
|
h.trace("failed to send tracking: " + err.Error())
|
|
return
|
|
}
|
|
}
|
|
|
|
func (h *UmamiFeeder) log(message string) {
|
|
if h.logHandler != nil {
|
|
time := time.Now().Format("2006-01-02T15:04:05Z")
|
|
h.logHandler.Printf("time=\"%s\" level=info msg=\"[traefik-umami-feeder] %s\"", time, message)
|
|
}
|
|
}
|
|
|
|
// Arguments are handled in the manner of [fmt.Printf].
|
|
func (h *UmamiFeeder) trace(format string, v ...any) {
|
|
if h.logHandler != nil && h.debug {
|
|
time := time.Now().Format("2006-01-02T15:04:05Z")
|
|
h.logHandler.Printf("time=\"%s\" level=trace msg=\"[traefik-umami-feeder] %s\"", time, fmt.Sprintf(format, v...))
|
|
}
|
|
}
|