Back to "Umami.Net en Bot Detektie"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

ASP.NET Umami

Umami.Net en Bot Detektie

Thursday, 12 September 2024

Inleiding

Dus I've posted a LOT in het verleden over het gebruik van Umami voor analyses in een zelfgehoste omgeving, en zelfs de publicatie van Umami.Net Nuget pacakge. Ik had echter een probleem waarbij ik de gebruikers van mijn RSS-feed wilde volgen.

Het probleem

Het probleem is dat RSS-feed-lesers proberen te doorgeven. nuttig. Gebruikeragenten wanneer je feed vraagt. Dit laat toe Aanpasbaar. providers om het aantal gebruikers en de soort gebruikers te volgen die de stroom verbruiken. Dit betekent echter ook dat Umami deze verzoeken zal identificeren als Bot requests. Dit is een probleem voor mijn gebruik omdat het resultaat is dat de vraag wordt geignoreerd en niet gevolgd.

De Feedbin-gebruikeragent ziet er zo uit:

Feedbin feed-id:1234 - 21 subscribers

Dus vrij nuttig goed, het geeft een paar nuttige details over wat je feed-id is , de aantallen gebruikers en de gebruikeragenten. Maar dit is ook een probleem, want het betekent dat Umami de vraag zal negeren. {"beep": "boop"} Dat betekent dat dit geïdentificeerd wordt als een botvraag. Dit is vervelend, want ik kan het niet door middel van normale foutenbewerking aanpakken.

Als we na deze verandering naar het Umami-dashboard kijken, zie ik nu de oorspronkelijke UserAgents die de feed hadden gevraagd:

Umami-dashboard

We zien nu een groep die de bot-detectie zou hebben geactiveerd, maar we kunnen deze zien en de verzoeken volgen.

De oplossing

Dus wat is de oplossing voor dit?? Ik kan' niet al deze verzoeken manueel analyseren en detecteren of Umami ze als een bot zal detecteren.; Het gebruikt IsBot. Dus moet ik de vraag intercepteren voordat ze naar Umami komt en de Gebruikeragent veranderen in iets dat Umami accepteert voor specifieke verzoeken.

Nu heb ik wat extra parameters bijgevoegd aan mijn volgmethoden in Umami.Net. Dit laat je toe om de nieuwe M SK2Default User Agent ' te spesifiseren die naar Umami zal worden gestuurd in plaats van de oorspronkelijke User Agent

De methoden

Op mijn UmamiBackgroundSender Ik voegde de volgende toe:

   public async Task TrackPageView(string url, string title, UmamiPayload? payload = null,
        UmamiEventData? eventData = null, bool useDefaultUserAgent = false)

Dit bestaat op alle volgmethoden en stelt alleen maar een parameter op. UmamiPayload object.

Op UmamiClient Deze kunnen worden opgesteld als volgt:

    [Fact]
    public async Task TrackPageView_WithUrl()
    {
        var handler = JwtResponseHandler.Create();
        var umamiClient = GetServices(handler);
        var response = await umamiClient.TrackPageViewAndDecode("https://example.com", "Example Page",
            new UmamiPayload { UseDefaultUserAgent = true });
        Assert.NotNull(response);
        Assert.Equal(UmamiDataResponse.ResponseStatus.Success, response.Status);
    }

In deze test gebruik ik de nieuwe. TrackPageViewAndDecode methode die een terugbrengt. UmamiDataResponse object. Dit object bevat ontcijferd JWT-token ( dat ongeldig is als het een bot is, dus het is bruikbaar om te controleren.

PayloadService

Dit is allemaal behandeld in de Payload De dienst die verantwoordelijk is voor de populatie van het payload object. Dit is waar de UseDefaultUserAgent is set.

De payload wordt per verstek populaeerd. HttpContext Dus krijg je deze set meestal goed; I' Ik zal later laten zien waar dit teruggetrokken is van UmamiM SK2

    private UmamiPayload GetPayload(string? url = null, UmamiEventData? data = null)
    {
        var httpContext = httpContextAccessor.HttpContext;
        var request = httpContext?.Request;

        var payload = new UmamiPayload
        {
            Website = settings.WebsiteId,
            Data = data,
            Url = url ?? httpContext?.Request?.Path.Value,
            IpAddress = httpContext?.Connection?.RemoteIpAddress?.ToString(),
            UserAgent = request?.Headers["User-Agent"].FirstOrDefault(),
            Referrer = request?.Headers["Referer"].FirstOrDefault(),
            Hostname = request?.Host.Host
        };

        return payload;
    }

Dan heb ik een stukje code, genaamd 'Sleutelcode'. PopulateFromPayload Dat is waar het verzoek object het krijgt's dataset up:

    public static string DefaultUserAgent =>
        $"Mozilla/5.0 (Windows 11)  Umami.Net/{Assembly.GetAssembly(typeof(UmamiClient))!.GetName().Version}";

    public UmamiPayload PopulateFromPayload(UmamiPayload? payload, UmamiEventData? data)
    {
        var newPayload = GetPayload(data: data);
        ...
        
        newPayload.UserAgent = payload.UserAgent ?? DefaultUserAgent;

        if (payload.UseDefaultUserAgent)
        {
            var userData = newPayload.Data ?? new UmamiEventData();
            userData.TryAdd("OriginalUserAgent", newPayload.UserAgent ?? "");
            newPayload.UserAgent = DefaultUserAgent;
            newPayload.Data = userData;
        }


        logger.LogInformation("Using UserAgent: {UserAgent}", newPayload.UserAgent);
     }        
        

Je zult zien dat dit een nieuwe Gebruikeragent definieert aan de top van de file (die ik bevestigd heb als 't Op dit moment Het wordt als een bot ontdekt.). Dan detecteert het in de methode of de UserAgent null is. (die niet zou moeten gebeuren, tenzij ' uit code wordt gebeld zonder HttpContext. UseDefaultUserAgent is ingesteld. Als dat zo is, stelt het de UserAgent als standaard en voegt de oorspronkelijke UserAgentie toe aan het dataobject.

Dit is dan gelogd zodat je kunt zien wat UserAgent gebruikt wordt.

Decodering van de reactie.

In Umami.Net 0.3.0 voegde ik een aantal nieuwe methoden toe, die een UmamiDataResponse object. Dit object bevat de ontcijferde JWT-token.

    public async Task<UmamiDataResponse?> TrackPageViewAndDecode(
        string? url = "",
        string? title = "",
        UmamiPayload? payload = null,
        UmamiEventData? eventData = null)
    {
        var response = await TrackPageView(url, title, payload, eventData);
        return await DecodeResponse(response);
    }
    
        private async Task<UmamiDataResponse?> DecodeResponse(HttpResponseMessage responseMessage)
    {
        var responseString = await responseMessage.Content.ReadAsStringAsync();

        switch (responseMessage.IsSuccessStatusCode)
        {
            case false:
                return new UmamiDataResponse(UmamiDataResponse.ResponseStatus.Failed);
            case true when responseString.Contains("beep") && responseString.Contains("boop"):
                logger.LogWarning("Bot detected data not stored in Umami");
                return new UmamiDataResponse(UmamiDataResponse.ResponseStatus.BotDetected);

            case true:
                var decoded = await jwtDecoder.DecodeResponse(responseString);
                if (decoded == null)
                {
                    logger.LogError("Failed to decode response from Umami");
                    return null;
                }

                var payload = UmamiDataResponse.Decode(decoded);

                return payload;
        }
    }

Je kunt zien dat dit een normale roeping is. TrackPageView De methode roept dan een methode op. DecodeResponse Die controleert de reactie op de beep en boop Stringen ( voor bot-detectie). Als het ze vindt, registreert hij een waarschuwing en geeft een terug. BotDetected Status. Als het ze niet vindt, ontcijfert het JWT-token en geeft de payload terug.

Het JWT-token zelf is slechts een Base64 gecodeerd string dat de gegevens bevat die Umami heeft opgeslagen. Dit wordt ontcijferd en teruggestuurd als een UmamiDataResponse object.

De volledige bron hierfür is onder:

Response Decoder
using System.IdentityModel.Tokens.Jwt;

namespace Umami.Net.Models;

public class UmamiDataResponse
{
    public enum ResponseStatus
    {
        Failed,
        BotDetected,
        Success
    }

    public UmamiDataResponse(ResponseStatus status)
    {
        Status = status;
    }

    public ResponseStatus Status { get; set; }

    public Guid Id { get; set; }
    public Guid WebsiteId { get; set; }
    public string? Hostname { get; set; }
    public string? Browser { get; set; }
    public string? Os { get; set; }
    public string? Device { get; set; }
    public string? Screen { get; set; }
    public string? Language { get; set; }
    public string? Country { get; set; }
    public string? Subdivision1 { get; set; }
    public string? Subdivision2 { get; set; }
    public string? City { get; set; }
    public DateTime CreatedAt { get; set; }
    public Guid VisitId { get; set; }
    public long Iat { get; set; }

    public static UmamiDataResponse Decode(JwtPayload? payload)
    {
        if (payload == null) return new UmamiDataResponse(ResponseStatus.Failed);
        payload.TryGetValue("visitId", out var visitIdObj);
        payload.TryGetValue("iat", out var iatObj);
        //This should only happen then the payload is dummy.
        if (payload.Count == 2)
        {
            var visitId = visitIdObj != null ? Guid.Parse(visitIdObj.ToString()!) : Guid.Empty;
            var iat = iatObj != null ? long.Parse(iatObj.ToString()!) : 0;

            return new UmamiDataResponse(ResponseStatus.Success)
            {
                VisitId = visitId,
                Iat = iat
            };
        }

        payload.TryGetValue("id", out var idObj);
        payload.TryGetValue("websiteId", out var websiteIdObj);
        payload.TryGetValue("hostname", out var hostnameObj);
        payload.TryGetValue("browser", out var browserObj);
        payload.TryGetValue("os", out var osObj);
        payload.TryGetValue("device", out var deviceObj);
        payload.TryGetValue("screen", out var screenObj);
        payload.TryGetValue("language", out var languageObj);
        payload.TryGetValue("country", out var countryObj);
        payload.TryGetValue("subdivision1", out var subdivision1Obj);
        payload.TryGetValue("subdivision2", out var subdivision2Obj);
        payload.TryGetValue("city", out var cityObj);
        payload.TryGetValue("createdAt", out var createdAtObj);

        return new UmamiDataResponse(ResponseStatus.Success)
        {
            Id = idObj != null ? Guid.Parse(idObj.ToString()!) : Guid.Empty,
            WebsiteId = websiteIdObj != null ? Guid.Parse(websiteIdObj.ToString()!) : Guid.Empty,
            Hostname = hostnameObj?.ToString(),
            Browser = browserObj?.ToString(),
            Os = osObj?.ToString(),
            Device = deviceObj?.ToString(),
            Screen = screenObj?.ToString(),
            Language = languageObj?.ToString(),
            Country = countryObj?.ToString(),
            Subdivision1 = subdivision1Obj?.ToString(),
            Subdivision2 = subdivision2Obj?.ToString(),
            City = cityObj?.ToString(),
            CreatedAt = createdAtObj != null ? DateTime.Parse(createdAtObj.ToString()!) : DateTime.MinValue,
            VisitId = visitIdObj != null ? Guid.Parse(visitIdObj.ToString()!) : Guid.Empty,
            Iat = iatObj != null ? long.Parse(iatObj.ToString()!) : 0
        };
    }
}
Je kunt zien dat dit een hoop nuttige informatie bevat over de verzoek die Umami heeft opgeslagen. Als je bijvoorbeeld verschillende inhoud zou willen laten zien op basis van locale, taal , browser etc., dan laat dit het je doen
    public Guid Id { get; set; }
    public Guid WebsiteId { get; set; }
    public string? Hostname { get; set; }
    public string? Browser { get; set; }
    public string? Os { get; set; }
    public string? Device { get; set; }
    public string? Screen { get; set; }
    public string? Language { get; set; }
    public string? Country { get; set; }
    public string? Subdivision1 { get; set; }
    public string? Subdivision2 { get; set; }
    public string? City { get; set; }
    public DateTime CreatedAt { get; set; }
    public Guid VisitId { get; set; }
    public long Iat { get; set; }

In slotting:

Dus een korte post over enkele nieuwe functionaliteit in Umami.Net 0.4.0 waarmee je een standaard Gebruikeragent voor specifieke verzoeken kan bepalenM SK2 Dit is bruikbaar om verzoeken te volgen die Umami anders zou negeren.

logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.