Skip to main content

Overview

The fingerprint middleware generates unique fingerprints for incoming requests based on headers, IP addresses, and other request attributes. Useful for bot detection, analytics, and rate limiting.

Installation

import "github.com/go-mizu/mizu/middlewares/fingerprint"

Quick Start

app := mizu.New()
app.Use(fingerprint.New())

app.Get("/", func(c *mizu.Ctx) error {
    hash := fingerprint.Hash(c)
    return c.JSON(200, map[string]string{"fingerprint": hash})
})

Configuration

OptionTypeDefaultDescription
Headers[]stringCommon headersHeaders to include
IncludeIPbooltrueInclude client IP
IncludeMethodboolfalseInclude HTTP method
IncludePathboolfalseInclude request path
Customfunc(*mizu.Ctx) map[string]string-Custom components

Default Headers

[]string{
    "User-Agent",
    "Accept",
    "Accept-Language",
    "Accept-Encoding",
    "Connection",
    "Sec-Ch-Ua",
    "Sec-Ch-Ua-Mobile",
    "Sec-Ch-Ua-Platform",
}

Examples

Basic Usage

app.Use(fingerprint.New())

app.Get("/", func(c *mizu.Ctx) error {
    info := fingerprint.Get(c)
    return c.JSON(200, map[string]any{
        "hash":       info.Hash,
        "components": info.Components,
    })
})

With IP Address

app.Use(fingerprint.WithIP())

// Fingerprint includes client IP

Headers Only

app.Use(fingerprint.HeadersOnly(
    "User-Agent",
    "Accept-Language",
))

Full Fingerprint

app.Use(fingerprint.Full())
// Includes IP, method, path, and all default headers

Custom Headers

app.Use(fingerprint.WithOptions(fingerprint.Options{
    Headers: []string{
        "User-Agent",
        "Accept",
        "Accept-Language",
        "Accept-Encoding",
        "X-Custom-Header",
    },
}))

Include Request Path

app.Use(fingerprint.WithOptions(fingerprint.Options{
    IncludeIP:     true,
    IncludeMethod: true,
    IncludePath:   true,
}))

Custom Components

app.Use(fingerprint.WithOptions(fingerprint.Options{
    Custom: func(c *mizu.Ctx) map[string]string {
        return map[string]string{
            "session_id": c.Cookie("session_id"),
            "user_id":    getUserID(c),
        }
    },
}))

Rate Limiting by Fingerprint

app.Use(fingerprint.New())

var requestCounts = make(map[string]int)
var mu sync.Mutex

app.Use(func(next mizu.Handler) mizu.Handler {
    return func(c *mizu.Ctx) error {
        hash := fingerprint.Hash(c)

        mu.Lock()
        requestCounts[hash]++
        count := requestCounts[hash]
        mu.Unlock()

        if count > 100 {
            return c.Text(429, "Rate limit exceeded")
        }

        return next(c)
    }
})

Bot Detection

app.Use(fingerprint.New())

// Known bot fingerprints
var knownBots = map[string]bool{
    "abc123...": true,
    "def456...": true,
}

app.Use(func(next mizu.Handler) mizu.Handler {
    return func(c *mizu.Ctx) error {
        hash := fingerprint.Hash(c)

        if knownBots[hash] {
            return c.Text(403, "Bot detected")
        }

        return next(c)
    }
})

Analytics Tracking

app.Use(fingerprint.New())

app.Use(func(next mizu.Handler) mizu.Handler {
    return func(c *mizu.Ctx) error {
        info := fingerprint.Get(c)

        // Track unique visitors
        analytics.TrackVisitor(info.Hash, map[string]any{
            "path":       c.Request().URL.Path,
            "components": info.Components,
        })

        return next(c)
    }
})

Session Correlation

app.Use(fingerprint.WithOptions(fingerprint.Options{
    IncludeIP: true,
    Custom: func(c *mizu.Ctx) map[string]string {
        return map[string]string{
            "session": c.Cookie("session_id"),
        }
    },
}))

app.Get("/", func(c *mizu.Ctx) error {
    hash := fingerprint.Hash(c)

    // Correlate with existing sessions
    existingSession := findSession(hash)
    if existingSession != nil {
        return c.JSON(200, existingSession)
    }

    // Create new session
    return c.JSON(200, createSession(hash))
})

Debugging Fingerprint

app.Get("/debug/fingerprint", func(c *mizu.Ctx) error {
    info := fingerprint.Get(c)

    return c.JSON(200, map[string]any{
        "hash":       info.Hash,
        "components": info.Components,
    })
})
Response:
{
    "hash": "a1b2c3d4e5f6...",
    "components": {
        "User-Agent": "Mozilla/5.0...",
        "Accept": "text/html,application/xhtml+xml...",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept-Encoding": "gzip, deflate, br",
        "IP": "192.168.1.1"
    }
}

Fraud Detection

app.Use(fingerprint.Full())

app.Post("/checkout", func(c *mizu.Ctx) error {
    hash := fingerprint.Hash(c)
    userID := c.Get("user_id").(string)

    // Check if fingerprint is associated with fraud
    if isFraudulent(hash) {
        return c.JSON(403, map[string]string{
            "error": "Transaction blocked",
        })
    }

    // Check for fingerprint/account mismatch
    if !matchesUser(hash, userID) {
        flagForReview(userID, hash)
    }

    return processCheckout(c)
})

Info Structure

type Info struct {
    Hash       string            // SHA256 hash of components
    Components map[string]string // Individual fingerprint components
}

API Reference

func New() mizu.Middleware
func WithOptions(opts Options) mizu.Middleware
func HeadersOnly(headers ...string) mizu.Middleware
func WithIP() mizu.Middleware
func Full() mizu.Middleware
func Get(c *mizu.Ctx) *Info
func Hash(c *mizu.Ctx) string

Hash Algorithm

The fingerprint hash is generated by:
  1. Collecting all components (headers, IP, etc.)
  2. Sorting component keys alphabetically
  3. Concatenating as key:value|key:value|...
  4. Computing SHA256 hash
  5. Returning hex-encoded string

Technical Details

Implementation Architecture

The fingerprint middleware uses a context-based storage mechanism to preserve fingerprint information throughout the request lifecycle. The implementation follows these key design principles: Context Storage
  • Uses a private contextKey struct type to avoid collisions with other middleware
  • Stores fingerprint information in the request context as an *Info pointer
  • Information persists across the entire middleware chain
Hash Generation Algorithm
  • Deterministic hashing through alphabetical key sorting
  • Uses SHA256 cryptographic hash function
  • Produces 64-character hexadecimal strings
  • Format: key1:value1|key2:value2|...|keyN:valueN
Component Collection The middleware collects fingerprint components in the following order:
  1. Headers: Iterates through configured header list
  2. IP Address: Extracted via getClientIP() helper
  3. HTTP Method: Request method if enabled
  4. Request Path: URL path if enabled
  5. Custom Components: User-defined function results
IP Address Detection The getClientIP() function implements a priority-based IP extraction:
  1. First checks X-Forwarded-For header (takes first IP in comma-separated list)
  2. Falls back to X-Real-IP header
  3. Finally uses RemoteAddr from the request

Performance Considerations

  • Minimal memory allocation through pre-sized maps
  • String builder for efficient concatenation
  • Single-pass component collection
  • Lazy evaluation: hash only computed once during middleware execution

Thread Safety

The middleware is thread-safe as each request receives its own context and Info struct. No shared state exists between requests.

Best Practices

  • Use minimal headers for privacy compliance
  • Don’t rely solely on fingerprints for authentication
  • Store fingerprints hashed, not raw
  • Combine with other signals for bot detection
  • Consider GDPR implications when storing fingerprints
  • Use for analytics and fraud detection, not tracking

Testing

The fingerprint middleware includes comprehensive test coverage for all configuration options and edge cases:
Test CaseDescriptionExpected Behavior
TestNewDefault middleware initializationGenerates fingerprint hash from default headers (User-Agent, Accept, etc.)
TestWithOptions_IncludeIPIP address inclusionCaptures client IP from RemoteAddr and includes in components
TestWithOptions_IncludeMethodHTTP method inclusionIncludes HTTP method (GET, POST, etc.) in fingerprint components
TestWithOptions_IncludePathRequest path inclusionCaptures and includes the full request path in components
TestWithOptions_CustomCustom component functionExecutes custom function and merges returned map into components
TestHashHash function retrievalReturns 64-character SHA256 hex string via Hash() helper
TestConsistentHashHash consistencyIdentical requests produce identical hash values
TestHeadersOnlyCustom header filteringOnly includes specified headers, excludes others (e.g., User-Agent)
TestWithIPWithIP() helper functionEnables IP inclusion using convenience function
TestFullFull() comprehensive modeIncludes IP, Method, Path, and all default headers
TestXForwardedForX-Forwarded-For header parsingExtracts first IP from comma-separated list in X-Forwarded-For header

Test Coverage

All test cases validate:
  • Correct component extraction and storage
  • Proper context propagation
  • Hash generation and consistency
  • Configuration option handling
  • Edge cases (empty headers, missing values, etc.)