From 453e9805ef6583e2177fb55fa1e45cf5816a7e67 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Sun, 21 Dec 2025 19:01:42 +0100 Subject: feat(backend): connect to sqlite db --- backend/config.go | 2 ++ backend/db.go | 73 +++++++++++++++++++++++++++++++++++++++++ backend/migrations/000_init.sql | 4 +++ 3 files changed, 79 insertions(+) create mode 100644 backend/db.go create mode 100644 backend/migrations/000_init.sql (limited to 'backend') diff --git a/backend/config.go b/backend/config.go index e009b8a..b221978 100644 --- a/backend/config.go +++ b/backend/config.go @@ -35,6 +35,7 @@ type Config struct { DefaultImage string `toml:"default_image"` Quotes []string `toml:"quotes"` Language string `toml:"language"` + Database string `toml:"database"` Sections []Section `toml:"section"` @@ -73,6 +74,7 @@ func (c *Config) DefaultValues() { }} c.RootFolder = "data" c.PublicFolder = "public" + c.Database = "database.sqlite" c.Quotes = []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do."} c.Replacers = []Replacer{{"~", " "}} } diff --git a/backend/db.go b/backend/db.go new file mode 100644 index 0000000..3559b9c --- /dev/null +++ b/backend/db.go @@ -0,0 +1,73 @@ +package backend + +import ( + "context" + "database/sql" + "embed" + "fmt" + "log/slog" + "regexp" + "slices" + "strconv" + + _ "github.com/mattn/go-sqlite3" +) + +//go:embed migrations +var migrations embed.FS + +var nameReg = regexp.MustCompile(`(\d{3})_[a-zA-Z_-]+.sql`) + +func ConnectDatabase(cfg *Config) *sql.DB { + db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared", cfg.Database)) + if err != nil { + panic(err) + } + db.SetMaxOpenConns(1) + return db +} + +func RunMigration(ctx context.Context, db *sql.DB) error { + entries, err := migrations.ReadDir("migrations") + if err != nil { + return err + } + type dbConfig struct { + Id int + Migration int + } + type runMig struct { + val string + n int + } + var toRun []runMig + for _, e := range entries { + rawId := nameReg.FindStringSubmatch(e.Name()) + id, err := strconv.Atoi(rawId[1]) + if err != nil { + return err + } + b, err := migrations.ReadFile("migrations/" + e.Name()) + if err != nil { + return err + } + slog.Debug("loading migration", "n", id, "file", e.Name(), "content", string(b)) + toRun = append(toRun, runMig{ + val: string(b), n: id, + }) + } + if len(toRun) == 0 { + return nil + } + slices.SortFunc(toRun, func(a, b runMig) int { + return a.n - b.n + }) + for _, m := range toRun { + slog.Info("migrating", "n", m.n) + _, err := db.ExecContext(ctx, m.val) + if err != nil { + return err + } + } + return nil +} diff --git a/backend/migrations/000_init.sql b/backend/migrations/000_init.sql new file mode 100644 index 0000000..6bd9d3a --- /dev/null +++ b/backend/migrations/000_init.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS config( + id INTEGER PRIMARY KEY AUTOINCREMENT, + migration INTEGER +); -- cgit v1.2.3 From 322826d06e425297fcbbc976a0566868a74f2d87 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Sun, 21 Dec 2025 22:16:52 +0100 Subject: feat(backend): record every jump between pages --- backend/db.go | 5 ----- backend/migrations/000_init.sql | 8 ++++--- backend/router.go | 14 ++++++++++-- backend/stats.go | 47 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 backend/stats.go (limited to 'backend') diff --git a/backend/db.go b/backend/db.go index 3559b9c..3828880 100644 --- a/backend/db.go +++ b/backend/db.go @@ -23,7 +23,6 @@ func ConnectDatabase(cfg *Config) *sql.DB { if err != nil { panic(err) } - db.SetMaxOpenConns(1) return db } @@ -32,10 +31,6 @@ func RunMigration(ctx context.Context, db *sql.DB) error { if err != nil { return err } - type dbConfig struct { - Id int - Migration int - } type runMig struct { val string n int diff --git a/backend/migrations/000_init.sql b/backend/migrations/000_init.sql index 6bd9d3a..6dd385c 100644 --- a/backend/migrations/000_init.sql +++ b/backend/migrations/000_init.sql @@ -1,4 +1,6 @@ -CREATE TABLE IF NOT EXISTS config( - id INTEGER PRIMARY KEY AUTOINCREMENT, - migration INTEGER +CREATE TABLE IF NOT EXISTS stats( + id INTEGER PRIMARY KEY, + visit INTEGER NOT NULL, + origin TEXT NOT NULL, + target TEXT NOT NULL ); diff --git a/backend/router.go b/backend/router.go index b7403cf..7f9de5c 100644 --- a/backend/router.go +++ b/backend/router.go @@ -2,6 +2,7 @@ package backend import ( "context" + "database/sql" "embed" "fmt" "io/fs" @@ -45,7 +46,7 @@ func SetupLogger(debug bool) { slog.SetDefault(logger) } -func NewRouter(debug bool, cfg *Config, assets fs.FS) *chi.Mux { +func NewRouter(debug bool, cfg *Config, db *sql.DB, assets fs.FS) *chi.Mux { r := chi.NewRouter() logLevel := slog.LevelWarn @@ -70,7 +71,7 @@ func NewRouter(debug bool, cfg *Config, assets fs.FS) *chi.Mux { r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // prevent tracking - w.Header().Add("Referrer-Policy", "no-referrer") + w.Header().Add("Referrer-Policy", "same-origin") // prevent iframe w.Header().Add("X-Frame-Options", "deny") // prevent bad content being parsed @@ -94,6 +95,15 @@ func NewRouter(debug bool, cfg *Config, assets fs.FS) *chi.Mux { next.ServeHTTP(w, r.WithContext(ctx)) }) }) + // stats + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if err := UpdateStats(r.Context(), db, r); err != nil { + slog.Error("updating stats", "error", err) + } + next.ServeHTTP(w, r) + }) + }) return r } diff --git a/backend/stats.go b/backend/stats.go new file mode 100644 index 0000000..e2ad08b --- /dev/null +++ b/backend/stats.go @@ -0,0 +1,47 @@ +package backend + +import ( + "context" + "database/sql" + "net/http" + "regexp" + "strings" +) + +var trimRefererReg = regexp.MustCompile(`https?://([a-z-0-9.]+)(:\d+)?/.*`) + +func UpdateStats(ctx context.Context, db *sql.DB, r *http.Request) error { + target := r.URL.Path + if strings.HasPrefix(target, "/assets") || strings.HasPrefix(target, "/static") { + return nil + } + ref := r.Header.Get("Referer") + if ref == "" { + return nil + } + subs := trimRefererReg.FindStringSubmatch(ref) + if len(subs) < 2 { + return nil + } + ref = subs[1] + if ref == ctx.Value(configKey).(*Config).Domain { + ref = subs[0][strings.Index(subs[0], ref)+len(ref):] + } + rows, err := db.QueryContext(ctx, "SELECT id, visit FROM stats WHERE origin = ? AND target = ?", ref, target) + if err != nil { + return err + } + if !rows.Next() { + _, err = db.ExecContext(ctx, "INSERT INTO stats (origin, target, visit) VALUES (?, ?, 1)", ref, target) + return err + } + var id uint + var nb uint + rows.Scan(&id, &nb) + err = rows.Close() + if err != nil { + return err + } + _, err = db.ExecContext(ctx, "UPDATE stats SET visit = ? WHERE id = ?", nb+1, id) + return err +} -- cgit v1.2.3 From 93c4f9047426f9f56940ed2b733dcde2d98b0c0e Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Mon, 22 Dec 2025 13:36:18 +0100 Subject: perf(backend): decrease render time when request has referer header Made the update stats async --- backend/router.go | 22 +++++++++++++++------- backend/stats.go | 26 ++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 11 deletions(-) (limited to 'backend') diff --git a/backend/router.go b/backend/router.go index 7f9de5c..14abc06 100644 --- a/backend/router.go +++ b/backend/router.go @@ -22,6 +22,7 @@ const ( configKey = "config" assetsFSKey = "assets_fs" debugKey = "debug" + dbKey = "db" ) //go:embed templates @@ -87,20 +88,27 @@ func NewRouter(debug bool, cfg *Config, db *sql.DB, assets fs.FS) *chi.Mux { }) }) // context + setContext := func(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, configKey, cfg) + ctx = context.WithValue(ctx, assetsFSKey, assets) + ctx = context.WithValue(ctx, debugKey, debug) + return context.WithValue(ctx, dbKey, db) + } r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := context.WithValue(r.Context(), configKey, cfg) - ctx = context.WithValue(ctx, assetsFSKey, assets) - ctx = context.WithValue(ctx, debugKey, debug) - next.ServeHTTP(w, r.WithContext(ctx)) + next.ServeHTTP(w, r.WithContext(setContext(r.Context()))) }) }) // stats r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if err := UpdateStats(r.Context(), db, r); err != nil { - slog.Error("updating stats", "error", err) - } + go func(r *http.Request) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + if err := UpdateStats(setContext(ctx), r); err != nil { + slog.Error("updating stats", "error", err) + } + }(r) next.ServeHTTP(w, r) }) }) diff --git a/backend/stats.go b/backend/stats.go index e2ad08b..2b07221 100644 --- a/backend/stats.go +++ b/backend/stats.go @@ -3,14 +3,20 @@ package backend import ( "context" "database/sql" + "fmt" + "log/slog" "net/http" "regexp" "strings" ) -var trimRefererReg = regexp.MustCompile(`https?://([a-z-0-9.]+)(:\d+)?/.*`) +var trimRefererReg = regexp.MustCompile(`https?://([a-z-0-9.]+(:\d+)?)/.*`) -func UpdateStats(ctx context.Context, db *sql.DB, r *http.Request) error { +func getDB(ctx context.Context) *sql.DB { + return ctx.Value(dbKey).(*sql.DB) +} + +func UpdateStats(ctx context.Context, r *http.Request) error { target := r.URL.Path if strings.HasPrefix(target, "/assets") || strings.HasPrefix(target, "/static") { return nil @@ -24,20 +30,32 @@ func UpdateStats(ctx context.Context, db *sql.DB, r *http.Request) error { return nil } ref = subs[1] - if ref == ctx.Value(configKey).(*Config).Domain { + if ref == ctx.Value(configKey).(*Config).Domain || ref == fmt.Sprintf("localhost:%d", 8000) { ref = subs[0][strings.Index(subs[0], ref)+len(ref):] + if ref == target { + return nil + } } + db := getDB(ctx) rows, err := db.QueryContext(ctx, "SELECT id, visit FROM stats WHERE origin = ? AND target = ?", ref, target) if err != nil { return err } + defer func() { + if err == nil { + slog.Debug("stats updated") + } + }() if !rows.Next() { _, err = db.ExecContext(ctx, "INSERT INTO stats (origin, target, visit) VALUES (?, ?, 1)", ref, target) return err } var id uint var nb uint - rows.Scan(&id, &nb) + err = rows.Scan(&id, &nb) + if err != nil { + return err + } err = rows.Close() if err != nil { return err -- cgit v1.2.3 From 84af6427d8205b1882b9f9df11ce394f96d6b792 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Mon, 22 Dec 2025 15:07:55 +0100 Subject: feat(backend): admin dashboard --- backend/config.go | 16 ++++---- backend/router.go | 18 +++++++++ backend/section.go | 2 +- backend/stats.go | 81 ++++++++++++++++++++++++++++++++++++++- backend/templates/admin.html | 33 ++++++++++++++++ backend/templates/components.html | 24 ++++++------ 6 files changed, 152 insertions(+), 22 deletions(-) create mode 100644 backend/templates/admin.html (limited to 'backend') diff --git a/backend/config.go b/backend/config.go index b221978..33d5852 100644 --- a/backend/config.go +++ b/backend/config.go @@ -29,13 +29,14 @@ type Replacer struct { } type Config struct { - Domain string `toml:"domain"` - Name string `toml:"name"` - Description string `toml:"description"` - DefaultImage string `toml:"default_image"` - Quotes []string `toml:"quotes"` - Language string `toml:"language"` - Database string `toml:"database"` + Domain string `toml:"domain"` + Name string `toml:"name"` + Description string `toml:"description"` + DefaultImage string `toml:"default_image"` + Quotes []string `toml:"quotes"` + Language string `toml:"language"` + Database string `toml:"database"` + AdminPassword string `toml:"admin_password"` Sections []Section `toml:"section"` @@ -75,6 +76,7 @@ func (c *Config) DefaultValues() { c.RootFolder = "data" c.PublicFolder = "public" c.Database = "database.sqlite" + c.AdminPassword = "Ch@ngeM€Please!" c.Quotes = []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do."} c.Replacers = []Replacer{{"~", " "}} } diff --git a/backend/router.go b/backend/router.go index 14abc06..f2ed775 100644 --- a/backend/router.go +++ b/backend/router.go @@ -2,6 +2,8 @@ package backend import ( "context" + "crypto/sha256" + "crypto/subtle" "database/sql" "embed" "fmt" @@ -23,6 +25,7 @@ const ( assetsFSKey = "assets_fs" debugKey = "debug" dbKey = "db" + loginKey = "login" ) //go:embed templates @@ -112,6 +115,21 @@ func NewRouter(debug bool, cfg *Config, db *sql.DB, assets fs.FS) *chi.Mux { next.ServeHTTP(w, r) }) }) + // login + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, pass, ok := r.BasicAuth() + ctx := r.Context() + if ok { + cfg := ctx.Value(configKey).(*Config) + passHash := sha256.Sum256([]byte(pass)) + rightPassHash := sha256.Sum256([]byte(cfg.AdminPassword)) + ok = subtle.ConstantTimeCompare(passHash[:], rightPassHash[:]) == 1 + } + ctx = context.WithValue(ctx, loginKey, ok) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + }) return r } diff --git a/backend/section.go b/backend/section.go index e2150cb..1b5ab9f 100644 --- a/backend/section.go +++ b/backend/section.go @@ -270,7 +270,7 @@ func (s *Section) handlePagination(w http.ResponseWriter, r *http.Request, maxLo page, err = strconv.Atoi(rawPage) if err != nil || page < 1 { slog.Warn("invalid page number", "rawPage", rawPage) - w.WriteHeader(http.StatusBadRequest) + http.Error(w, "Bad request", http.StatusBadRequest) return nil } } diff --git a/backend/stats.go b/backend/stats.go index 2b07221..dfdf040 100644 --- a/backend/stats.go +++ b/backend/stats.go @@ -7,7 +7,10 @@ import ( "log/slog" "net/http" "regexp" + "strconv" "strings" + + "github.com/go-chi/chi/v5" ) var trimRefererReg = regexp.MustCompile(`https?://([a-z-0-9.]+(:\d+)?)/.*`) @@ -18,7 +21,9 @@ func getDB(ctx context.Context) *sql.DB { func UpdateStats(ctx context.Context, r *http.Request) error { target := r.URL.Path - if strings.HasPrefix(target, "/assets") || strings.HasPrefix(target, "/static") { + if strings.HasPrefix(target, "/assets") || + strings.HasPrefix(target, "/static") || + strings.HasPrefix(target, "/admin") { return nil } ref := r.Header.Get("Referer") @@ -32,7 +37,7 @@ func UpdateStats(ctx context.Context, r *http.Request) error { ref = subs[1] if ref == ctx.Value(configKey).(*Config).Domain || ref == fmt.Sprintf("localhost:%d", 8000) { ref = subs[0][strings.Index(subs[0], ref)+len(ref):] - if ref == target { + if ref == target || strings.HasPrefix(ref, "/admin") || ref == "/favicon.ico" { return nil } } @@ -63,3 +68,75 @@ func UpdateStats(ctx context.Context, r *http.Request) error { _, err = db.ExecContext(ctx, "UPDATE stats SET visit = ? WHERE id = ?", nb+1, id) return err } + +type statRow struct { + Origin string + Target string + Visit uint +} + +const statPerPage = 25 + +func GetStatRows(ctx context.Context, page uint) ([]statRow, error) { + rows, err := getDB(ctx).QueryContext( + ctx, + "SELECT origin, target, visit FROM stats ORDER BY visit DESC LIMIT ? OFFSET ?", + statPerPage, (page-1)*statPerPage, + ) + if err != nil { + return nil, err + } + defer rows.Close() + statRows := make([]statRow, statPerPage) + var i uint8 + for i = 0; rows.Next(); i++ { + var stat statRow + err = rows.Scan(&stat.Origin, &stat.Target, &stat.Visit) + if err != nil { + return nil, err + } + statRows[i] = stat + } + if i == 0 { + return nil, nil + } + return statRows[:i], nil +} + +type adminData struct { + *data + Rows []statRow + PagesNumber int + CurrentPage int +} + +func HandleAdmin(r *chi.Mux) { + r.Get("/admin", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if !ctx.Value(loginKey).(bool) { + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + d := new(adminData) + d.data = new(data) + rawPage := r.URL.Query().Get("page") + page := 1 + var err error + if rawPage != "" { + page, err = strconv.Atoi(rawPage) + if err != nil || page < 1 { + slog.Warn("invalid page number", "rawPage", rawPage) + http.Error(w, "Bad request", http.StatusBadRequest) + return + } + } + d.Rows, err = GetStatRows(ctx, uint(page)) + if err != nil { + panic(err) + } + d.PagesNumber = page + max(len(d.Rows)-statPerPage+1, 0) + d.CurrentPage = page + d.handleGeneric(w, r, "admin", d) + }) +} diff --git a/backend/templates/admin.html b/backend/templates/admin.html new file mode 100644 index 0000000..41addc9 --- /dev/null +++ b/backend/templates/admin.html @@ -0,0 +1,33 @@ +{{define "body"}} +
+
+

Administration

+

+ Panel d'administration très simple. Il affiche les stats et possiblement les webmentions et autre contenu à + modérer. +

+
+
+

Stats

+ + + + + + + + + + {{ range .Rows }} + + + + + + {{ end }} + +
OriginTargetVisits
{{ .Origin }}{{ .Target }}{{ .Visit }}
+ +
+
+{{end}} diff --git a/backend/templates/components.html b/backend/templates/components.html index b4ffbe6..b180123 100644 --- a/backend/templates/components.html +++ b/backend/templates/components.html @@ -1,4 +1,14 @@ -{{define "section_display"}} +{{ define "pagination" }} + +{{ end }} {{define "section_display"}}
{{ $uri := .URI }} {{ range .Data }}
@@ -11,17 +21,7 @@
{{ end }} -- cgit v1.2.3 From 9bba6dcbb2e83fe53604d38b89fb79ce47eacddd Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Mon, 22 Dec 2025 18:21:32 +0100 Subject: feat(backend): track page load without referer --- backend/stats.go | 90 ++++++++++++++++++++++++++++++++++++++++++-- backend/templates/admin.html | 18 +++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) (limited to 'backend') diff --git a/backend/stats.go b/backend/stats.go index dfdf040..f081194 100644 --- a/backend/stats.go +++ b/backend/stats.go @@ -7,23 +7,57 @@ import ( "log/slog" "net/http" "regexp" + "slices" "strconv" "strings" + "sync" + "time" "github.com/go-chi/chi/v5" ) +type loaded struct { + data map[string]struct{} + mu *sync.RWMutex +} + +func (l *loaded) Has(k string) bool { + l.mu.RLock() + defer l.mu.RUnlock() + _, ok := l.data[k] + return ok +} + +func (l *loaded) Add(k string) { + l.mu.Lock() + defer l.mu.Unlock() + l.data[k] = struct{}{} +} + +func (l *loaded) Remove(k string) { + l.mu.Lock() + defer l.mu.Unlock() + delete(l.data, k) +} + +func newLoaded() *loaded { + return &loaded{ + data: make(map[string]struct{}), + mu: new(sync.RWMutex), + } +} + var trimRefererReg = regexp.MustCompile(`https?://([a-z-0-9.]+(:\d+)?)/.*`) +var load = newLoaded() + func getDB(ctx context.Context) *sql.DB { return ctx.Value(dbKey).(*sql.DB) } func UpdateStats(ctx context.Context, r *http.Request) error { target := r.URL.Path - if strings.HasPrefix(target, "/assets") || - strings.HasPrefix(target, "/static") || - strings.HasPrefix(target, "/admin") { + if strings.HasPrefix(target, "/static") || strings.HasPrefix(target, "/admin") { return nil } ref := r.Header.Get("Referer") @@ -41,6 +75,14 @@ func UpdateStats(ctx context.Context, r *http.Request) error { return nil } } + // using /assets/styles.css to detect if a page is loaded → majority of bots will not load this + if target == "/assets/styles.css" { + target = ref + ref = "?" + } + if load.Has(target) { + return nil + } db := getDB(ctx) rows, err := db.QueryContext(ctx, "SELECT id, visit FROM stats WHERE origin = ? AND target = ?", ref, target) if err != nil { @@ -49,6 +91,11 @@ func UpdateStats(ctx context.Context, r *http.Request) error { defer func() { if err == nil { slog.Debug("stats updated") + load.Add(target) + go func(target string) { + time.Sleep(5 * time.Second) + load.Remove(target) + }(target) } }() if !rows.Next() { @@ -103,8 +150,41 @@ func GetStatRows(ctx context.Context, page uint) ([]statRow, error) { return statRows[:i], nil } +func GetUnionStatRows(ctx context.Context) ([]statRow, error) { + rows, err := getDB(ctx).QueryContext(ctx, "SELECT target, visit FROM stats ORDER BY visit DESC") + if err != nil { + return nil, err + } + defer rows.Close() + data := make(map[string]uint) + for rows.Next() { + var stat statRow + err = rows.Scan(&stat.Target, &stat.Visit) + if err != nil { + return nil, err + } + if _, ok := data[stat.Target]; !ok { + data[stat.Target] = stat.Visit + } else { + data[stat.Target] += stat.Visit + } + } + var statRows []statRow + for k, v := range data { + statRows = append(statRows, statRow{ + Target: k, + Visit: v, + }) + } + slices.SortFunc(statRows, func(a, b statRow) int { + return int(b.Visit) - int(a.Visit) + }) + return statRows, nil +} + type adminData struct { *data + Visits []statRow Rows []statRow PagesNumber int CurrentPage int @@ -135,6 +215,10 @@ func HandleAdmin(r *chi.Mux) { if err != nil { panic(err) } + d.Visits, err = GetUnionStatRows(ctx) + if err != nil { + panic(err) + } d.PagesNumber = page + max(len(d.Rows)-statPerPage+1, 0) d.CurrentPage = page d.handleGeneric(w, r, "admin", d) diff --git a/backend/templates/admin.html b/backend/templates/admin.html index 41addc9..008827f 100644 --- a/backend/templates/admin.html +++ b/backend/templates/admin.html @@ -9,6 +9,24 @@

Stats

+

Visits

+ + + + + + + + + {{ range .Visits }} + + + + + {{ end }} + +
TargetVisits
{{ .Target }}{{ .Visit }}
+

Origin

-- cgit v1.2.3 From cbd5c09c5e1403709d4aabf91051443f147689e5 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Mon, 22 Dec 2025 18:34:02 +0100 Subject: refactor(backend): move db related in new package --- backend/admin.go | 53 ++++++++ backend/db.go | 68 ---------- backend/migrations/000_init.sql | 6 - backend/router.go | 6 +- backend/stats.go | 226 -------------------------------- backend/storage/db.go | 68 ++++++++++ backend/storage/migrations/000_init.sql | 6 + backend/storage/stats.go | 182 +++++++++++++++++++++++++ 8 files changed, 312 insertions(+), 303 deletions(-) create mode 100644 backend/admin.go delete mode 100644 backend/db.go delete mode 100644 backend/migrations/000_init.sql delete mode 100644 backend/stats.go create mode 100644 backend/storage/db.go create mode 100644 backend/storage/migrations/000_init.sql create mode 100644 backend/storage/stats.go (limited to 'backend') diff --git a/backend/admin.go b/backend/admin.go new file mode 100644 index 0000000..087a3ea --- /dev/null +++ b/backend/admin.go @@ -0,0 +1,53 @@ +package backend + +import ( + "log/slog" + "net/http" + "strconv" + + "git.anhgelus.world/anhgelus/small-web/backend/storage" + "github.com/go-chi/chi/v5" +) + +type adminData struct { + *data + Visits []storage.StatsRow + Rows []storage.StatsRow + PagesNumber int + CurrentPage int +} + +func HandleAdmin(r *chi.Mux) { + r.Get("/admin", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if !ctx.Value(loginKey).(bool) { + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + d := new(adminData) + d.data = new(data) + rawPage := r.URL.Query().Get("page") + page := 1 + var err error + if rawPage != "" { + page, err = strconv.Atoi(rawPage) + if err != nil || page < 1 { + slog.Warn("invalid page number", "rawPage", rawPage) + http.Error(w, "Bad request", http.StatusBadRequest) + return + } + } + d.Rows, err = storage.GetStatsRows(ctx, uint(page)) + if err != nil { + panic(err) + } + d.Visits, err = storage.GetUnionStatsRows(ctx) + if err != nil { + panic(err) + } + d.PagesNumber = page + max(len(d.Rows)-storage.StatsPerPage+1, 0) + d.CurrentPage = page + d.handleGeneric(w, r, "admin", d) + }) +} diff --git a/backend/db.go b/backend/db.go deleted file mode 100644 index 3828880..0000000 --- a/backend/db.go +++ /dev/null @@ -1,68 +0,0 @@ -package backend - -import ( - "context" - "database/sql" - "embed" - "fmt" - "log/slog" - "regexp" - "slices" - "strconv" - - _ "github.com/mattn/go-sqlite3" -) - -//go:embed migrations -var migrations embed.FS - -var nameReg = regexp.MustCompile(`(\d{3})_[a-zA-Z_-]+.sql`) - -func ConnectDatabase(cfg *Config) *sql.DB { - db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared", cfg.Database)) - if err != nil { - panic(err) - } - return db -} - -func RunMigration(ctx context.Context, db *sql.DB) error { - entries, err := migrations.ReadDir("migrations") - if err != nil { - return err - } - type runMig struct { - val string - n int - } - var toRun []runMig - for _, e := range entries { - rawId := nameReg.FindStringSubmatch(e.Name()) - id, err := strconv.Atoi(rawId[1]) - if err != nil { - return err - } - b, err := migrations.ReadFile("migrations/" + e.Name()) - if err != nil { - return err - } - slog.Debug("loading migration", "n", id, "file", e.Name(), "content", string(b)) - toRun = append(toRun, runMig{ - val: string(b), n: id, - }) - } - if len(toRun) == 0 { - return nil - } - slices.SortFunc(toRun, func(a, b runMig) int { - return a.n - b.n - }) - for _, m := range toRun { - slog.Info("migrating", "n", m.n) - _, err := db.ExecContext(ctx, m.val) - if err != nil { - return err - } - } - return nil -} diff --git a/backend/migrations/000_init.sql b/backend/migrations/000_init.sql deleted file mode 100644 index 6dd385c..0000000 --- a/backend/migrations/000_init.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS stats( - id INTEGER PRIMARY KEY, - visit INTEGER NOT NULL, - origin TEXT NOT NULL, - target TEXT NOT NULL -); diff --git a/backend/router.go b/backend/router.go index f2ed775..161433d 100644 --- a/backend/router.go +++ b/backend/router.go @@ -14,6 +14,7 @@ import ( "strings" "time" + "git.anhgelus.world/anhgelus/small-web/backend/storage" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/httplog/v3" @@ -24,7 +25,6 @@ const ( configKey = "config" assetsFSKey = "assets_fs" debugKey = "debug" - dbKey = "db" loginKey = "login" ) @@ -95,7 +95,7 @@ func NewRouter(debug bool, cfg *Config, db *sql.DB, assets fs.FS) *chi.Mux { ctx = context.WithValue(ctx, configKey, cfg) ctx = context.WithValue(ctx, assetsFSKey, assets) ctx = context.WithValue(ctx, debugKey, debug) - return context.WithValue(ctx, dbKey, db) + return context.WithValue(ctx, storage.DBKey, db) } r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -108,7 +108,7 @@ func NewRouter(debug bool, cfg *Config, db *sql.DB, assets fs.FS) *chi.Mux { go func(r *http.Request) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - if err := UpdateStats(setContext(ctx), r); err != nil { + if err := storage.UpdateStats(setContext(ctx), r, cfg.Domain); err != nil { slog.Error("updating stats", "error", err) } }(r) diff --git a/backend/stats.go b/backend/stats.go deleted file mode 100644 index f081194..0000000 --- a/backend/stats.go +++ /dev/null @@ -1,226 +0,0 @@ -package backend - -import ( - "context" - "database/sql" - "fmt" - "log/slog" - "net/http" - "regexp" - "slices" - "strconv" - "strings" - "sync" - "time" - - "github.com/go-chi/chi/v5" -) - -type loaded struct { - data map[string]struct{} - mu *sync.RWMutex -} - -func (l *loaded) Has(k string) bool { - l.mu.RLock() - defer l.mu.RUnlock() - _, ok := l.data[k] - return ok -} - -func (l *loaded) Add(k string) { - l.mu.Lock() - defer l.mu.Unlock() - l.data[k] = struct{}{} -} - -func (l *loaded) Remove(k string) { - l.mu.Lock() - defer l.mu.Unlock() - delete(l.data, k) -} - -func newLoaded() *loaded { - return &loaded{ - data: make(map[string]struct{}), - mu: new(sync.RWMutex), - } -} - -var trimRefererReg = regexp.MustCompile(`https?://([a-z-0-9.]+(:\d+)?)/.*`) - -var load = newLoaded() - -func getDB(ctx context.Context) *sql.DB { - return ctx.Value(dbKey).(*sql.DB) -} - -func UpdateStats(ctx context.Context, r *http.Request) error { - target := r.URL.Path - if strings.HasPrefix(target, "/static") || strings.HasPrefix(target, "/admin") { - return nil - } - ref := r.Header.Get("Referer") - if ref == "" { - return nil - } - subs := trimRefererReg.FindStringSubmatch(ref) - if len(subs) < 2 { - return nil - } - ref = subs[1] - if ref == ctx.Value(configKey).(*Config).Domain || ref == fmt.Sprintf("localhost:%d", 8000) { - ref = subs[0][strings.Index(subs[0], ref)+len(ref):] - if ref == target || strings.HasPrefix(ref, "/admin") || ref == "/favicon.ico" { - return nil - } - } - // using /assets/styles.css to detect if a page is loaded → majority of bots will not load this - if target == "/assets/styles.css" { - target = ref - ref = "?" - } - if load.Has(target) { - return nil - } - db := getDB(ctx) - rows, err := db.QueryContext(ctx, "SELECT id, visit FROM stats WHERE origin = ? AND target = ?", ref, target) - if err != nil { - return err - } - defer func() { - if err == nil { - slog.Debug("stats updated") - load.Add(target) - go func(target string) { - time.Sleep(5 * time.Second) - load.Remove(target) - }(target) - } - }() - if !rows.Next() { - _, err = db.ExecContext(ctx, "INSERT INTO stats (origin, target, visit) VALUES (?, ?, 1)", ref, target) - return err - } - var id uint - var nb uint - err = rows.Scan(&id, &nb) - if err != nil { - return err - } - err = rows.Close() - if err != nil { - return err - } - _, err = db.ExecContext(ctx, "UPDATE stats SET visit = ? WHERE id = ?", nb+1, id) - return err -} - -type statRow struct { - Origin string - Target string - Visit uint -} - -const statPerPage = 25 - -func GetStatRows(ctx context.Context, page uint) ([]statRow, error) { - rows, err := getDB(ctx).QueryContext( - ctx, - "SELECT origin, target, visit FROM stats ORDER BY visit DESC LIMIT ? OFFSET ?", - statPerPage, (page-1)*statPerPage, - ) - if err != nil { - return nil, err - } - defer rows.Close() - statRows := make([]statRow, statPerPage) - var i uint8 - for i = 0; rows.Next(); i++ { - var stat statRow - err = rows.Scan(&stat.Origin, &stat.Target, &stat.Visit) - if err != nil { - return nil, err - } - statRows[i] = stat - } - if i == 0 { - return nil, nil - } - return statRows[:i], nil -} - -func GetUnionStatRows(ctx context.Context) ([]statRow, error) { - rows, err := getDB(ctx).QueryContext(ctx, "SELECT target, visit FROM stats ORDER BY visit DESC") - if err != nil { - return nil, err - } - defer rows.Close() - data := make(map[string]uint) - for rows.Next() { - var stat statRow - err = rows.Scan(&stat.Target, &stat.Visit) - if err != nil { - return nil, err - } - if _, ok := data[stat.Target]; !ok { - data[stat.Target] = stat.Visit - } else { - data[stat.Target] += stat.Visit - } - } - var statRows []statRow - for k, v := range data { - statRows = append(statRows, statRow{ - Target: k, - Visit: v, - }) - } - slices.SortFunc(statRows, func(a, b statRow) int { - return int(b.Visit) - int(a.Visit) - }) - return statRows, nil -} - -type adminData struct { - *data - Visits []statRow - Rows []statRow - PagesNumber int - CurrentPage int -} - -func HandleAdmin(r *chi.Mux) { - r.Get("/admin", func(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - if !ctx.Value(loginKey).(bool) { - w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - d := new(adminData) - d.data = new(data) - rawPage := r.URL.Query().Get("page") - page := 1 - var err error - if rawPage != "" { - page, err = strconv.Atoi(rawPage) - if err != nil || page < 1 { - slog.Warn("invalid page number", "rawPage", rawPage) - http.Error(w, "Bad request", http.StatusBadRequest) - return - } - } - d.Rows, err = GetStatRows(ctx, uint(page)) - if err != nil { - panic(err) - } - d.Visits, err = GetUnionStatRows(ctx) - if err != nil { - panic(err) - } - d.PagesNumber = page + max(len(d.Rows)-statPerPage+1, 0) - d.CurrentPage = page - d.handleGeneric(w, r, "admin", d) - }) -} diff --git a/backend/storage/db.go b/backend/storage/db.go new file mode 100644 index 0000000..7a7c6e0 --- /dev/null +++ b/backend/storage/db.go @@ -0,0 +1,68 @@ +package storage + +import ( + "context" + "database/sql" + "embed" + "fmt" + "log/slog" + "regexp" + "slices" + "strconv" + + _ "github.com/mattn/go-sqlite3" +) + +//go:embed migrations +var migrations embed.FS + +var nameReg = regexp.MustCompile(`(\d{3})_[a-zA-Z_-]+.sql`) + +func ConnectDatabase(file string) *sql.DB { + db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared", file)) + if err != nil { + panic(err) + } + return db +} + +func RunMigration(ctx context.Context, db *sql.DB) error { + entries, err := migrations.ReadDir("migrations") + if err != nil { + return err + } + type runMig struct { + val string + n int + } + var toRun []runMig + for _, e := range entries { + rawId := nameReg.FindStringSubmatch(e.Name()) + id, err := strconv.Atoi(rawId[1]) + if err != nil { + return err + } + b, err := migrations.ReadFile("migrations/" + e.Name()) + if err != nil { + return err + } + slog.Debug("loading migration", "n", id, "file", e.Name(), "content", string(b)) + toRun = append(toRun, runMig{ + val: string(b), n: id, + }) + } + if len(toRun) == 0 { + return nil + } + slices.SortFunc(toRun, func(a, b runMig) int { + return a.n - b.n + }) + for _, m := range toRun { + slog.Info("migrating", "n", m.n) + _, err := db.ExecContext(ctx, m.val) + if err != nil { + return err + } + } + return nil +} diff --git a/backend/storage/migrations/000_init.sql b/backend/storage/migrations/000_init.sql new file mode 100644 index 0000000..6dd385c --- /dev/null +++ b/backend/storage/migrations/000_init.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS stats( + id INTEGER PRIMARY KEY, + visit INTEGER NOT NULL, + origin TEXT NOT NULL, + target TEXT NOT NULL +); diff --git a/backend/storage/stats.go b/backend/storage/stats.go new file mode 100644 index 0000000..757168d --- /dev/null +++ b/backend/storage/stats.go @@ -0,0 +1,182 @@ +package storage + +import ( + "context" + "database/sql" + "fmt" + "log/slog" + "net/http" + "regexp" + "slices" + "strings" + "sync" + "time" +) + +const DBKey = "db" + +type loaded struct { + data map[string]struct{} + mu *sync.RWMutex +} + +func (l *loaded) Has(k string) bool { + l.mu.RLock() + defer l.mu.RUnlock() + _, ok := l.data[k] + return ok +} + +func (l *loaded) Add(k string) { + l.mu.Lock() + defer l.mu.Unlock() + l.data[k] = struct{}{} +} + +func (l *loaded) Remove(k string) { + l.mu.Lock() + defer l.mu.Unlock() + delete(l.data, k) +} + +func newLoaded() *loaded { + return &loaded{ + data: make(map[string]struct{}), + mu: new(sync.RWMutex), + } +} + +var trimRefererReg = regexp.MustCompile(`https?://([a-z-0-9.]+(:\d+)?)/.*`) + +var load = newLoaded() + +func getDB(ctx context.Context) *sql.DB { + return ctx.Value(DBKey).(*sql.DB) +} + +func UpdateStats(ctx context.Context, r *http.Request, domain string) error { + target := r.URL.Path + if strings.HasPrefix(target, "/static") || strings.HasPrefix(target, "/admin") { + return nil + } + ref := r.Header.Get("Referer") + if ref == "" { + return nil + } + subs := trimRefererReg.FindStringSubmatch(ref) + if len(subs) < 2 { + return nil + } + ref = subs[1] + if ref == domain || ref == fmt.Sprintf("localhost:%d", 8000) { + ref = subs[0][strings.Index(subs[0], ref)+len(ref):] + if ref == target || strings.HasPrefix(ref, "/admin") || ref == "/favicon.ico" { + return nil + } + } + // using /assets/styles.css to detect if a page is loaded → majority of bots will not load this + if target == "/assets/styles.css" { + target = ref + ref = "?" + } + if load.Has(target) { + return nil + } + db := getDB(ctx) + rows, err := db.QueryContext(ctx, "SELECT id, visit FROM stats WHERE origin = ? AND target = ?", ref, target) + if err != nil { + return err + } + defer func() { + if err == nil { + slog.Debug("stats updated") + load.Add(target) + go func(target string) { + time.Sleep(5 * time.Second) + load.Remove(target) + }(target) + } + }() + if !rows.Next() { + _, err = db.ExecContext(ctx, "INSERT INTO stats (origin, target, visit) VALUES (?, ?, 1)", ref, target) + return err + } + var id uint + var nb uint + err = rows.Scan(&id, &nb) + if err != nil { + return err + } + err = rows.Close() + if err != nil { + return err + } + _, err = db.ExecContext(ctx, "UPDATE stats SET visit = ? WHERE id = ?", nb+1, id) + return err +} + +type StatsRow struct { + Origin string + Target string + Visit uint +} + +const StatsPerPage = 25 + +func GetStatsRows(ctx context.Context, page uint) ([]StatsRow, error) { + rows, err := getDB(ctx).QueryContext( + ctx, + "SELECT origin, target, visit FROM stats ORDER BY visit DESC LIMIT ? OFFSET ?", + StatsPerPage, (page-1)*StatsPerPage, + ) + if err != nil { + return nil, err + } + defer rows.Close() + statRows := make([]StatsRow, StatsPerPage) + var i uint8 + for i = 0; rows.Next(); i++ { + var stat StatsRow + err = rows.Scan(&stat.Origin, &stat.Target, &stat.Visit) + if err != nil { + return nil, err + } + statRows[i] = stat + } + if i == 0 { + return nil, nil + } + return statRows[:i], nil +} + +func GetUnionStatsRows(ctx context.Context) ([]StatsRow, error) { + rows, err := getDB(ctx).QueryContext(ctx, "SELECT target, visit FROM stats ORDER BY visit DESC") + if err != nil { + return nil, err + } + defer rows.Close() + data := make(map[string]uint) + for rows.Next() { + var stat StatsRow + err = rows.Scan(&stat.Target, &stat.Visit) + if err != nil { + return nil, err + } + if _, ok := data[stat.Target]; !ok { + data[stat.Target] = stat.Visit + } else { + data[stat.Target] += stat.Visit + } + } + var statRows []StatsRow + for k, v := range data { + statRows = append(statRows, StatsRow{ + Target: k, + Visit: v, + }) + } + slices.SortFunc(statRows, func(a, b StatsRow) int { + return int(b.Visit) - int(a.Visit) + }) + return statRows, nil +} -- cgit v1.2.3 From 9ad7b0e67c90697893f188323a32590fbecd5a65 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Mon, 22 Dec 2025 18:51:42 +0100 Subject: fix(storage): does not store stats if request fails --- backend/data.go | 5 +++++ backend/router.go | 43 +++++++++++++++++++++++-------------------- backend/storage/db.go | 6 ++++++ backend/storage/stats.go | 7 ------- 4 files changed, 34 insertions(+), 27 deletions(-) (limited to 'backend') diff --git a/backend/data.go b/backend/data.go index 5bc5391..2f2e06d 100644 --- a/backend/data.go +++ b/backend/data.go @@ -111,6 +111,11 @@ func (d *data) handleGeneric(w http.ResponseWriter, r *http.Request, name string if err != nil { panic(err) } + if name == "404" { + w.WriteHeader(http.StatusNotFound) + return + } + UpdateStats(r) } func (d *data) handleRSS(w http.ResponseWriter, r *http.Request, custom dataUsable) { diff --git a/backend/router.go b/backend/router.go index 161433d..1a2f024 100644 --- a/backend/router.go +++ b/backend/router.go @@ -91,28 +91,11 @@ func NewRouter(debug bool, cfg *Config, db *sql.DB, assets fs.FS) *chi.Mux { }) }) // context - setContext := func(ctx context.Context) context.Context { - ctx = context.WithValue(ctx, configKey, cfg) - ctx = context.WithValue(ctx, assetsFSKey, assets) - ctx = context.WithValue(ctx, debugKey, debug) - return context.WithValue(ctx, storage.DBKey, db) - } - r.Use(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - next.ServeHTTP(w, r.WithContext(setContext(r.Context()))) - }) - }) - // stats r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - go func(r *http.Request) { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - if err := storage.UpdateStats(setContext(ctx), r, cfg.Domain); err != nil { - slog.Error("updating stats", "error", err) - } - }(r) - next.ServeHTTP(w, r) + next.ServeHTTP(w, r.WithContext( + setContext(r.Context(), cfg, debug, db, assets), + )) }) }) // login @@ -179,3 +162,23 @@ func HandleStaticFiles(r *chi.Mux, path string, root fs.FS) { http.StripPrefix(pathPrefix, http.FileServerFS(root)).ServeHTTP(w, req) }) } + +func UpdateStats(r *http.Request) { + ctx := r.Context() + cfg := ctx.Value(configKey).(*Config) + debug := ctx.Value(debugKey).(bool) + db := ctx.Value(storage.DBKey).(*sql.DB) + assets := ctx.Value(assetsFSKey).(fs.FS) + + ctx2, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + if err := storage.UpdateStats(setContext(ctx2, cfg, debug, db, assets), r, cfg.Domain); err != nil { + slog.Error("updating stats", "error", err) + } +} +func setContext(ctx context.Context, cfg *Config, debug bool, db *sql.DB, assets fs.FS) context.Context { + ctx = context.WithValue(ctx, configKey, cfg) + ctx = context.WithValue(ctx, assetsFSKey, assets) + ctx = context.WithValue(ctx, debugKey, debug) + return context.WithValue(ctx, storage.DBKey, db) +} diff --git a/backend/storage/db.go b/backend/storage/db.go index 7a7c6e0..0ee50da 100644 --- a/backend/storage/db.go +++ b/backend/storage/db.go @@ -13,6 +13,8 @@ import ( _ "github.com/mattn/go-sqlite3" ) +const DBKey = "db" + //go:embed migrations var migrations embed.FS @@ -66,3 +68,7 @@ func RunMigration(ctx context.Context, db *sql.DB) error { } return nil } + +func getDB(ctx context.Context) *sql.DB { + return ctx.Value(DBKey).(*sql.DB) +} diff --git a/backend/storage/stats.go b/backend/storage/stats.go index 757168d..d95387d 100644 --- a/backend/storage/stats.go +++ b/backend/storage/stats.go @@ -2,7 +2,6 @@ package storage import ( "context" - "database/sql" "fmt" "log/slog" "net/http" @@ -13,8 +12,6 @@ import ( "time" ) -const DBKey = "db" - type loaded struct { data map[string]struct{} mu *sync.RWMutex @@ -50,10 +47,6 @@ var trimRefererReg = regexp.MustCompile(`https?://([a-z-0-9.]+(:\d+)?)/.*`) var load = newLoaded() -func getDB(ctx context.Context) *sql.DB { - return ctx.Value(DBKey).(*sql.DB) -} - func UpdateStats(ctx context.Context, r *http.Request, domain string) error { target := r.URL.Path if strings.HasPrefix(target, "/static") || strings.HasPrefix(target, "/admin") { -- cgit v1.2.3 From c66a5cc0809875df0299ebb80be423436c195416 Mon Sep 17 00:00:00 2001 From: Anhgelus Morhtuuzh Date: Mon, 22 Dec 2025 19:17:59 +0100 Subject: fix(storage): remove query from ref --- backend/storage/stats.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'backend') diff --git a/backend/storage/stats.go b/backend/storage/stats.go index d95387d..b30cb8c 100644 --- a/backend/storage/stats.go +++ b/backend/storage/stats.go @@ -5,7 +5,7 @@ import ( "fmt" "log/slog" "net/http" - "regexp" + "net/url" "slices" "strings" "sync" @@ -43,8 +43,6 @@ func newLoaded() *loaded { } } -var trimRefererReg = regexp.MustCompile(`https?://([a-z-0-9.]+(:\d+)?)/.*`) - var load = newLoaded() func UpdateStats(ctx context.Context, r *http.Request, domain string) error { @@ -56,13 +54,13 @@ func UpdateStats(ctx context.Context, r *http.Request, domain string) error { if ref == "" { return nil } - subs := trimRefererReg.FindStringSubmatch(ref) - if len(subs) < 2 { + refUrl, err := url.Parse(ref) + if err != nil { return nil } - ref = subs[1] + ref = refUrl.Host if ref == domain || ref == fmt.Sprintf("localhost:%d", 8000) { - ref = subs[0][strings.Index(subs[0], ref)+len(ref):] + ref = refUrl.Path if ref == target || strings.HasPrefix(ref, "/admin") || ref == "/favicon.ico" { return nil } -- cgit v1.2.3