# Umami.Net en Bot Detektie

# Inleiding

Dus I've [posted a LOT](/blog/category/Umami) in het verleden over het gebruik van Umami voor analyses in een zelfgehoste omgeving, en zelfs de publicatie van [Umami.Net Nuget pacakge](https://www.nuget.org/packages/Umami.Net/). Ik had echter een probleem waarbij ik de gebruikers van mijn RSS-feed wilde volgen.

[TOC]

<!--category-- ASP.NET, Umami -->
<datetime class="hidden">2024-09-12T14:50</datetime>

# 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:

```plaintext
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](user-agents.png?width=600&format=webp&quality=30)

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:

```csharp
   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:

```csharp
    [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

```csharp
    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:

```csharp
    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.

```csharp
    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:

<details>
<summary>Response Decoder</summary>

```csharp
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
        };
    }
}
```

</details>
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

```csharp
    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.