// Copyright (c) 2025 Nikolai Papin // // This file is part of the Auto Attendance app that looks for // self-attend QR-codes during lectures and opens their URLs in your // browser. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See // the GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package linkvalidator import ( "net/url" "strings" "sync" "git.weirdcat.su/weirdcat/auto-attendance/internal/config" "git.weirdcat.su/weirdcat/auto-attendance/internal/logger" ) type LinkValidator interface { ValidateLink(string) (token string, ok bool) ResetSeenLinks() GetSeenCount() int } type linkValidatorImpl struct { config *config.Config log *logger.Logger seenMu sync.RWMutex seenLinks map[string]bool } // ValidateLink implements LinkValidator. func (v *linkValidatorImpl) ValidateLink(rawURL string) (token string, ok bool) { if rawURL == "" { v.log.Debug("Empty URL provided for validation") return "", false } parsedURL, err := url.Parse(rawURL) if err != nil { v.log.Debug("Failed to parse URL", "url", rawURL, "error", err) return "", false } if v.config.Communication.QrUrl != "" { expectedURL, err := url.Parse(v.config.Communication.QrUrl) if err == nil { if parsedURL.Scheme != expectedURL.Scheme || parsedURL.Host != expectedURL.Host || parsedURL.Path != expectedURL.Path { v.log.Debug("URL doesn't match configured QR URL pattern", "url", rawURL, "expected", v.config.Communication.QrUrl) return "", false } } } if v.config.Communication.ApiSelfApproveMethod != "" { if !strings.Contains(parsedURL.Path, v.config.Communication.ApiSelfApproveMethod) { v.log.Debug("URL doesn't contain expected API method", "url", parsedURL.Path, "expected", v.config.Communication.ApiSelfApproveMethod) return "", false } } token = parsedURL.Query().Get("token") if token == "" { v.log.Debug("URL missing token parameter", "url", rawURL) return "", false } // Check if we've already seen this token v.seenMu.RLock() alreadySeen := v.seenLinks[token] v.seenMu.RUnlock() if alreadySeen { v.log.Debug("URL token already processed, skipping", "token", token, "url", rawURL) return token, false } // Mark this token as seen v.seenMu.Lock() v.seenLinks[token] = true v.seenMu.Unlock() v.log.Debug("URL validation successful", "url", rawURL, "token", token) return token, true } // ResetSeenLinks clears the cache of seen links func (v *linkValidatorImpl) ResetSeenLinks() { v.seenMu.Lock() defer v.seenMu.Unlock() oldCount := len(v.seenLinks) v.seenLinks = make(map[string]bool) v.log.Debug("Reset seen links cache", "previous_count", oldCount) } // GetSeenCount returns the number of unique links seen func (v *linkValidatorImpl) GetSeenCount() int { v.seenMu.RLock() defer v.seenMu.RUnlock() return len(v.seenLinks) } func NewLinkValidator(config *config.Config, log *logger.Logger) LinkValidator { return &linkValidatorImpl{ config: config, log: log, seenLinks: make(map[string]bool), } }