first commit

This commit is contained in:
Oleh Astappiev 2024-10-16 15:57:59 +02:00
commit 3ca46c9ea2
No known key found for this signature in database
GPG key ID: 37D7AC22589E9632
19 changed files with 958 additions and 0 deletions

BIN
.assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

47
.github/workflows/go-cross.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: Go Matrix
on: [push, pull_request]
jobs:
cross:
name: Go
runs-on: ${{ matrix.os }}
env:
CGO_ENABLED: 0
strategy:
matrix:
go-version: [ 1.19, 1.x ]
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
# https://github.com/marketplace/actions/checkout
- name: Checkout code
uses: actions/checkout@v2
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v3
with:
# In order:
# * Module download cache
# * Build cache (Linux)
# * Build cache (Mac)
# * Build cache (Windows)
path: |
~/go/pkg/mod
~/.cache/go-build
~/Library/Caches/go-build
%LocalAppData%\go-build
key: ${{ runner.os }}-${{ matrix.go-version }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-${{ matrix.go-version }}-go-
- name: Test
run: go test -v -cover ./...

72
.github/workflows/main.yml vendored Normal file
View file

@ -0,0 +1,72 @@
name: Main
on:
push:
branches:
- master
pull_request:
jobs:
main:
name: Main Process
runs-on: ubuntu-latest
env:
GO_VERSION: 1.19
GOLANGCI_LINT_VERSION: v1.50.0
YAEGI_VERSION: v0.14.2
CGO_ENABLED: 0
defaults:
run:
working-directory: ${{ github.workspace }}/go/src/github.com/${{ github.repository }}
steps:
# https://github.com/marketplace/actions/setup-go-environment
- name: Set up Go ${{ env.GO_VERSION }}
uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
# https://github.com/marketplace/actions/checkout
- name: Check out code
uses: actions/checkout@v2
with:
path: go/src/github.com/${{ github.repository }}
fetch-depth: 0
# https://github.com/marketplace/actions/cache
- name: Cache Go modules
uses: actions/cache@v3
with:
path: ${{ github.workspace }}/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
# https://golangci-lint.run/usage/install#other-ci
- name: Install golangci-lint ${{ env.GOLANGCI_LINT_VERSION }}
run: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCI_LINT_VERSION}
- name: Install Yaegi ${{ env.YAEGI_VERSION }}
run: curl -sfL https://raw.githubusercontent.com/traefik/yaegi/master/install.sh | bash -s -- -b $(go env GOPATH)/bin ${YAEGI_VERSION}
- name: Setup GOPATH
run: go env -w GOPATH=${{ github.workspace }}/go
- name: Check and get dependencies
run: |
go mod tidy
git diff --exit-code go.mod
# git diff --exit-code go.sum
go mod download
go mod vendor
# git diff --exit-code ./vendor/
- name: Lint and Tests
run: make
- name: Run tests with Yaegi
run: make yaegi_test
env:
GOPATH: ${{ github.workspace }}/go

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.idea/
.vscode/
.DS_Store
postgres/
go.work

75
.golangci.yml Normal file
View file

@ -0,0 +1,75 @@
run:
timeout: 3m
skip-files: []
skip-dirs: []
linters-settings:
govet:
enable-all: true
disable:
- fieldalignment
golint:
min-confidence: 0
gocyclo:
min-complexity: 12
goconst:
min-len: 5
min-occurrences: 4
misspell:
locale: US
funlen:
lines: -1
statements: 50
godox:
keywords:
- FIXME
gofumpt:
extra-rules: true
linters:
enable-all: true
disable:
- deadcode # deprecated
- exhaustivestruct # deprecated
- golint # deprecated
- ifshort # deprecated
- interfacer # deprecated
- maligned # deprecated
- nosnakecase # deprecated
- scopelint # deprecated
- scopelint # deprecated
- structcheck # deprecated
- varcheck # deprecated
- sqlclosecheck # not relevant (SQL)
- rowserrcheck # not relevant (SQL)
- execinquery # not relevant (SQL)
- cyclop # duplicate of gocyclo
- bodyclose # Too many false positives: https://github.com/timakin/bodyclose/issues/30
- dupl
- testpackage
- tparallel
- paralleltest
- nlreturn
- wsl
- exhaustive
- exhaustruct
- goerr113
- wrapcheck
- ifshort
- noctx
- lll
- gomnd
- forbidigo
- varnamelen
issues:
exclude-use-default: false
max-per-linter: 0
max-same-issues: 0
exclude: []
exclude-rules:
- path: (.+)_test.go
linters:
- goconst
- funlen
- godot

17
.traefik.yml Normal file
View file

@ -0,0 +1,17 @@
displayName: Umami Feeder
type: middleware
iconPath: .assets/icon.png
import: github.com/astappiev/traefik-umami-feeder
summary: 'Traefik plugin that sends HTTP requests to Umami as pageview events.'
testData:
umamiHost: ""
umamiToken: ""
umamiUsername: ""
umamiPassword: ""
websites:
domain: websiteId
createNewWebsites: false
debug: false

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Oleh Astappiev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

20
Makefile Normal file
View file

@ -0,0 +1,20 @@
.PHONY: lint test vendor clean
export GO111MODULE=on
default: lint test
lint:
golangci-lint run
test:
go test -v -cover ./...
yaegi_test:
yaegi test -v .
vendor:
go mod vendor
clean:
rm -rf ./vendor

10
demo/config.yml Normal file
View file

@ -0,0 +1,10 @@
http:
middlewares:
umami:
plugin:
umami-feeder:
debug: true
umamiHost: http://umami:3000
umamiUsername: "admin"
umamiPassword: "umami"
createNewWebsites: true

70
demo/docker-compose.yml Normal file
View file

@ -0,0 +1,70 @@
services:
traefik:
image: traefik:3
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.service=api@internal"
- "traefik.http.routers.traefik.rule=Host(`localhost`)"
- "traefik.http.routers.traefik.entrypoints=traefik"
networks:
- traefik
ports:
- target: 8080
published: 8080
protocol: tcp
- target: 8081
published: 8081
protocol: tcp
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
# To persist ACME data (certificates, account, etc)
- ./traefik.yml:/traefik.yml:ro
- ./config.yml:/config/config.yml:ro
# mount plugin
- ../:/plugins-local/src/github.com/astappiev/traefik-umami-feeder
restart: unless-stopped
whoami:
image: containous/whoami
labels:
- "traefik.enable=true"
- "traefik.http.services.whoami.loadbalancer.server.port=80"
- "traefik.http.routers.whoami.service=whoami"
- "traefik.http.routers.whoami.rule=Host(`localhost`)"
- "traefik.http.routers.whoami.entrypoints=whoami"
# middleware
- "traefik.http.routers.whoami.middlewares=umami@file"
networks:
- traefik
umami:
image: ghcr.io/umami-software/umami:postgresql-latest
depends_on:
- postgres
environment:
- DATABASE_URL=postgres://umami:umami@postgres:5432/umami
ports:
- target: 3000
published: 3000
protocol: tcp
networks:
- traefik
- umami_postgres
postgres:
image: postgres:16
environment:
- POSTGRES_PASSWORD=umami
- POSTGRES_DB=umami
- POSTGRES_USER=umami
networks:
- umami_postgres
volumes:
- ./postgres:/var/lib/postgresql/data
networks:
traefik:
umami_postgres:

30
demo/traefik.yml Normal file
View file

@ -0,0 +1,30 @@
log:
level: INFO
# accessLog: {}
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: traefik
watch: true
file:
directory: /config
watch: true
entryPoints:
"traefik": # traefik dashboard
address: ":8080"
"whoami": # http
address: ":8081"
"umami": # umami dashboard
address: ":3000"
api:
dashboard: true
experimental:
localPlugins:
umami-feeder:
moduleName: "github.com/astappiev/traefik-umami-feeder"

106
forwarding.go Normal file
View file

@ -0,0 +1,106 @@
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"
connection = "connection"
keepAlive = "keep-alive"
proxyAuthenticate = "proxy-authenticate"
proxyAuthorization = "proxy-authorization"
te = "te" // canonicalized version of "TE"
trailers = "trailers"
transferEncoding = "transfer-Encoding"
upgrade = "upgrade"
)
var hopHeaders = []string{
connection,
keepAlive,
proxyAuthenticate,
proxyAuthorization,
te, // canonicalized version of "TE"
trailers,
transferEncoding,
upgrade,
}
func copyHeaders(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func removeHeaders(headers http.Header, names ...string) {
for _, h := range names {
headers.Del(h)
}
}
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)
}
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module github.com/astappiev/traefik-umami-feeder
go 1.19

90
readme.md Normal file
View file

@ -0,0 +1,90 @@
# Umami Feeder for Traefik
This plugin for enables [Traefik Reverse Proxy](https://traefik.io/traefik/) to feed [Umami Analytics](https://umami.is)
with tracking events.
It was created as an alternative to [traefik-umami-plugin](https://github.com/1cedsoda/traefik-umami-plugin) and
inspired by idea of [Plausible Feeder Traefik Plugin](https://github.com/safing/plausiblefeeder).
## Features
- [X] Super easy to setup, one middleware for all websites
- [X] Server Side Tracking, no need to add JavaScript to your websites
- [X] Fast and private analytics
## Installation
To [add this plugin to traefik](https://plugins.traefik.io/install) reference this repository as a plugin in the static
config.
The version references a git tag.
```yaml
experimental:
plugins:
umami-feeder:
moduleName: github.com/astappiev/traefik-umami-feeder
version: v1.0.0 # replace with latest version available
```
```toml
[experimental.plugins.umami-feeder]
moduleName = "github.com/astappiev/traefik-umami-feeder"
version = "v1.0.0" # replace with latest version available
```
With the plugin installed, you can configure a middleware in a dynamic configuration such as a `config.yml` or docker
labels.
```yaml
http:
middlewares:
my-umami-middleware:
plugin:
umami-feeder:
umamiHost: "http://umami:3000"
websites:
"example.com": "d4617504-241c-4797-8eab-5939b367b3ad"
```
```toml
[http.middlewares]
[http.middlewares.my-umami-middleware.plugin.umami-feeder]
umamiHost = "umami:3000"
[http.middlewares.my-umami-middleware.plugin.umami-feeder.websites]
"example.com" = "d4617504-241c-4797-8eab-5939b367b3ad"
```
You have an option to give a list of domains to track (and their website IDs on Umami). \
Or, you can give a token and the list will be fetched from Umami. For this, you need either [retrieve the token yourself](https://umami.is/docs/api/authentication), or use
username/password instead.
After that, you need to add the middleware to a [router](https://doc.traefik.io/traefik/routing/routers/#middlewares_1).
Remember to reference the
correct [provider namespace](https://doc.traefik.io/traefik/providers/overview/#provider-namespace).
E.g. as Docker labels:
```yaml
- "traefik.http.routers.whoami.middlewares=my-umami-middleware@file"
```
Or, for all routers in a static configuration:
```yaml
entryPoints:
web:
http:
middlewares:
- my-umami-middleware@file
```
## Configuration
| key | default | type | description |
|---------------------|---------|----------|-------------------------------------------------------------------------------------------------------------|
| `umamiHost` | - | `string` | Umami server url, reachable from within traefik (container), e.g. `http://umami:3000` |
| `umamiToken` | - | `string` | An API Token, used to automatize work with websites, not needed if you provide `websites` |
| `umamiUsername` | - | `string` | An alternative to `umamiToken`, you can provide an username and password |
| `umamiPassword` | - | `string` | Only in combination with `umamiUsername` |
| `websites` | - | `map` | A map of hostnames and their associated Umami IDs. Can also be used to override or extend fetched websites |
| `createNewWebsites` | false | `bool` | If set to `true`, will try to create a new website on Umami, if domain not found there |
| `debug` | false | `bool` | Something doesn't work? Set to `true` to see more logs (plugins doesn't have access to Traefik's log level) |

171
umami.go Normal file
View file

@ -0,0 +1,171 @@
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...))
}
}

72
umami_send.go Normal file
View file

@ -0,0 +1,72 @@
package traefik_umami_feeder
import (
"net/http"
"regexp"
"strings"
)
// Copied from https://github.com/1cedsoda/traefik-umami-plugin/blob/master/umami_tracking.go
// Licensed as Apache-2.0 license
type SendPayload struct {
Website string `json:"website"`
Hostname string `json:"hostname"`
Language string `json:"language"`
Url string `json:"url"`
Referer string `json:"referer"`
Name string `json:"name"`
Data map[string]interface{} `json:"data"`
}
type SendBody struct {
Payload SendPayload `json:"payload"`
Type string `json:"type"`
}
func buildPayload(req *http.Request, websiteId string) SendPayload {
return SendPayload{
Website: websiteId,
Hostname: parseDomainFromHost(req.Host),
Language: parseAcceptLanguage(req.Header.Get("Accept-Language")),
Url: req.URL.String(),
Referer: req.Referer(),
Name: "traefik",
Data: map[string]interface{}{},
}
}
// opts the port from the host.
func parseDomainFromHost(host string) string {
// check if the host has a port
if strings.Contains(host, ":") {
host = strings.Split(host, ":")[0]
}
return host
}
const parseAcceptLanguagePattern = `([a-zA-Z\-]+)(?:;q=\d\.\d)?(?:,\s)?`
var parseAcceptLanguageRegexp = regexp.MustCompile(parseAcceptLanguagePattern)
func parseAcceptLanguage(acceptLanguage string) string {
matches := parseAcceptLanguageRegexp.FindAllStringSubmatch(acceptLanguage, -1)
if len(matches) == 0 {
return ""
}
return matches[0][1]
}
func buildSendBody(clientReq *http.Request, websiteId string) (SendBody, http.Header) {
body := SendBody{
Payload: buildPayload(clientReq, websiteId),
Type: "event",
}
var headers = make(http.Header)
headers.Set("Content-Type", "application/json")
copyHeaders(headers, clientReq.Header)
removeHeaders(headers, hopHeaders...)
writeXForwardedHeaders(headers, clientReq)
return body, headers
}

24
umami_token.go Normal file
View file

@ -0,0 +1,24 @@
package traefik_umami_feeder
type Auth struct {
Username string `json:"username"`
Password string `json:"password"`
}
type AuthResponse struct {
Token string `json:"token"`
}
func getToken(umamiHost string, umamiUsername string, umamiPassword string) (string, error) {
var result AuthResponse
err := sendRequestAndParse(umamiHost+"/api/auth/login", Auth{
Username: umamiUsername,
Password: umamiPassword,
}, nil, &result)
if err != nil {
return "", err
}
return result.Token, nil
}

72
umami_utils.go Normal file
View file

@ -0,0 +1,72 @@
package traefik_umami_feeder
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
)
func sendRequest(url string, body interface{}, headers http.Header) (*http.Response, error) {
var req *http.Request
var err error
if body != nil {
bodyJson, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err = http.NewRequestWithContext(context.Background(), http.MethodPost, url, bytes.NewReader(bodyJson))
} else {
req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
}
if err != nil {
return nil, err
}
if headers != nil {
req.Header = headers
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
client := &http.Client{}
response, err := client.Do(req)
if err != nil {
return nil, err
}
status := response.StatusCode
if status < 200 || status >= 300 {
return nil, fmt.Errorf("request failed with status %d", status)
}
return response, nil
}
func sendRequestAndParse(url string, body interface{}, headers http.Header, value interface{}) error {
resp, err := sendRequest(url, body, headers)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
err = json.Unmarshal(respBody, &value)
if err != nil {
return err
}
return nil
}

52
umami_websites.go Normal file
View file

@ -0,0 +1,52 @@
package traefik_umami_feeder
import (
"net/http"
"time"
)
type WebsitesResponse struct {
Data []Website `json:"data"`
Count int `json:"count"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
OrderBy string `json:"orderBy"`
}
type Website struct {
ID string `json:"id,omitempty"`
Name string `json:"name"`
Domain string `json:"domain"`
CreatedAt time.Time `json:"createdAt,omitempty"`
}
func createWebsite(umamiHost string, umamiToken string, websiteDomain string) (*Website, error) {
var headers = make(http.Header)
headers.Set("Authorization", "Bearer "+umamiToken)
var result Website
err := sendRequestAndParse(umamiHost+"/api/websites", Website{
Name: websiteDomain,
Domain: websiteDomain,
}, headers, &result)
if err != nil {
return nil, err
}
return &result, nil
}
func fetchWebsites(umamiHost string, umamiToken string) (*[]Website, error) {
var headers = make(http.Header)
headers.Set("Authorization", "Bearer "+umamiToken)
var result WebsitesResponse
err := sendRequestAndParse(umamiHost+"/api/websites?pageSize=200", nil, headers, &result)
if err != nil {
return nil, err
}
return &result.Data, nil
}