webprofiler

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 27, 2026 License: MIT Imports: 18 Imported by: 0

README ΒΆ

web-profiler

Last Version License Go Version

δΈ­ζ–‡ζ–‡ζ‘£ | English

web-profiler is a lightweight request analysis middleware for net/http. It inspects incoming requests with bounded overhead, restores the request body for downstream handlers, and exposes structured results through context.Context.

It is designed as request-analysis infrastructure, not as a security decision engine.

🐲 Highlights

  • Native net/http middleware API with easy integration into Gin, Chi, Echo, and other net/http-based frameworks
  • One bounded body capture shared by all analyzers
  • Structured request profile exposed through FromContext
  • Per-request analysis duration with nanosecond precision
  • Safe degradation with warnings instead of failing the request
  • Built-in analyzers for entropy, fingerprint, complexity, and charset distribution

Installation

go get github.com/GoFurry/web-profiler

πŸš€ Quick Start

package main

import (
	"log"
	"net/http"

	webprofiler "github.com/GoFurry/web-profiler"
)

func main() {
	cfg := webprofiler.DefaultConfig()

	handler := webprofiler.Middleware(cfg)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		profile, ok := webprofiler.FromContext(r.Context())
		if ok && profile != nil {
			if profile.Entropy != nil {
				log.Printf("entropy=%.4f", profile.Entropy.Value)
			}
			if profile.Fingerprint != nil {
				log.Printf("fingerprint=%s", profile.Fingerprint.Hash)
			}
		}

		// The request body is still readable here.
		w.WriteHeader(http.StatusOK)
	}))

	log.Fatal(http.ListenAndServe(":8080", handler))
}

You can also use the convenience helper:

handler := webprofiler.Wrap(mux, webprofiler.DefaultConfig())

A runnable native net/http example lives at example/main.go.

Using Profile Data In Handlers

FromContext gives you the collected Profile. You can inspect metadata, module outputs, and warnings inside any downstream handler:

func inspectHandler(w http.ResponseWriter, r *http.Request) {
	profile, ok := webprofiler.FromContext(r.Context())
	if !ok || profile == nil {
		http.Error(w, "profile not found", http.StatusInternalServerError)
		return
	}

	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	log.Printf("path=%s content_type=%s", profile.Meta.Path, profile.Meta.ContentType)
	log.Printf("analysis_duration=%s", profile.Meta.AnalysisDuration)
	log.Printf("body=%s", string(body))

	if profile.Entropy != nil {
		log.Printf("entropy=%.4f sample_bytes=%d", profile.Entropy.Value, profile.Entropy.SampledBytes)
	}
	if profile.Fingerprint != nil {
		log.Printf("fingerprint=%s fields=%v", profile.Fingerprint.Hash, profile.Fingerprint.Fields)
	}
	if profile.Complexity != nil {
		log.Printf("complexity_score=%d depth=%d fields=%d", profile.Complexity.Score, profile.Complexity.Depth, profile.Complexity.FieldCount)
	}
	if profile.Charset != nil {
		log.Printf("non_ascii_ratio=%.2f suspicious=%v", profile.Charset.NonASCIIRatio, profile.Charset.SuspiciousFlags)
	}
	if len(profile.Warnings) > 0 {
		log.Printf("warnings=%v", profile.Warnings)
	}

	w.WriteHeader(http.StatusOK)
}

Gin Example

Gin uses net/http under the hood, so you can wrap its engine directly:

package main

import (
	"log"
	"net/http"

	"github.com/gin-gonic/gin"
	webprofiler "github.com/GoFurry/web-profiler"
)

func main() {
	cfg := webprofiler.DefaultConfig()
	engine := gin.New()

	engine.POST("/inspect", func(c *gin.Context) {
		profile, ok := webprofiler.FromContext(c.Request.Context())
		if !ok || profile == nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": "profile not found"})
			return
		}

		c.JSON(http.StatusOK, gin.H{
			"path":         profile.Meta.Path,
			"content_type": profile.Meta.ContentType,
			"analysis_ns":  profile.Meta.AnalysisDuration.Nanoseconds(),
			"entropy":      profile.Entropy,
			"fingerprint":  profile.Fingerprint,
			"complexity":   profile.Complexity,
			"charset":      profile.Charset,
			"warnings":     profile.Warnings,
		})
	})

	handler := webprofiler.Middleware(cfg)(engine)
	log.Fatal(http.ListenAndServe(":8080", handler))
}

Public API

func Middleware(cfg Config) func(http.Handler) http.Handler
func Wrap(next http.Handler, cfg Config) http.Handler
func DefaultConfig() Config
func FromContext(ctx context.Context) (*Profile, bool)

What Gets Collected

Entropy
  • Shannon entropy over the sampled request body bytes
  • Sample size and observed body size
  • Sampling strategy metadata
Fingerprint
  • Normalized request headers
  • Optional client IP and TLS metadata
  • Stable hashed fingerprint with versioning
Complexity
  • JSON depth and field counts
  • Object/array counts and max array length
  • URL-encoded form statistics
  • Interpretable score factors
Charset
  • ASCII, digit, whitespace, symbol, control, and non-ASCII ratios
  • Optional suspicious flags such as invalid UTF-8, zero-width characters, and mixed scripts

🧭 Configuration

DefaultConfig() returns a ready-to-use setup with bounded defaults:

  • BodyConfig limits read size, sample size, methods, and content types
  • FingerprintConfig controls headers, proxy trust, TLS metadata, and hash versioning
  • ComplexityConfig controls JSON depth, field limits, and supported content types
  • CharsetConfig controls text analysis size and suspicious-pattern detection

Typical customization:

cfg := webprofiler.DefaultConfig()
cfg.Body.MaxReadBytes = 64 << 10
cfg.Body.SampleBytes = 8 << 10
cfg.Fingerprint.IncludeIP = true
cfg.Fingerprint.TrustProxy = true
cfg.Complexity.MaxJSONDepth = 16

Performance Notes

  • MetaInfo.AnalysisDuration records middleware analysis time as time.Duration
  • The example exposes both analysis_duration and analysis_duration_ns so you can read it directly or aggregate it precisely
  • The SHA-256 fingerprint step hashes a very small normalized string built from a few headers and optional TLS/IP fields, so in most cases it is not the main cost
  • In practice, request-body capture, JSON parsing, and charset scanning are usually more expensive than the final SHA-256 call
  • If you run at very high QPS, benchmark with your own traffic and disable EnableFingerprint, IncludeIP, or IncludeTLS if you want an even cheaper profile

Example Response Fields

The native example at example/main.go returns a JSON payload like the one you posted. The following table maps each field to its meaning:

Field Meaning
path Request path seen by the middleware and handler.
body Request body re-read inside the handler, proving the middleware restored r.Body.
entropy.Value Shannon entropy of the sampled body bytes. Higher usually means more byte diversity.
entropy.SampledBytes Number of bytes used for entropy calculation.
entropy.TotalObservedBytes Number of body bytes observed by the middleware before sampling.
entropy.SampleStrategy Sampling mode currently used for body analysis.
fingerprint.Fields Normalized fields used to build the request fingerprint.
fingerprint.Hash Stable SHA-256 digest of the normalized fingerprint fields.
fingerprint.HashAlgorithm Fingerprint hash algorithm currently used.
fingerprint.HashVersion Fingerprint schema/version identifier.
complexity.ContentType Content type used for complexity analysis.
complexity.Depth Observed structural depth of the parsed body.
complexity.FieldCount Total number of parsed fields/values.
complexity.ObjectCount Number of JSON objects encountered.
complexity.ArrayCount Number of arrays encountered.
complexity.MaxArrayLength Longest array length seen in the body.
complexity.Score Aggregate complexity score.
complexity.ScoreFactors Breakdown of how the complexity score was calculated.
charset.TotalChars Total characters scanned in the sampled text body.
charset.ASCIIAlphaRatio Ratio of ASCII letters in the sampled body.
charset.DigitRatio Ratio of digits in the sampled body.
charset.WhitespaceRatio Ratio of whitespace characters in the sampled body.
charset.SymbolRatio Ratio of punctuation and symbol characters in the sampled body.
charset.ControlCharRatio Ratio of control characters or invalid byte sequences.
charset.NonASCIIRatio Ratio of non-ASCII characters in the sampled body.
charset.SuspiciousFlags Optional markers such as invalid UTF-8, zero-width characters, or mixed scripts.
content_type Normalized request Content-Type.
content_length Request body length reported by the incoming request.
sampled Whether the middleware analyzed only a subset of the observed body.
sample_bytes Number of sampled bytes actually used by body analyzers.
body_truncated Whether body observation stopped at MaxReadBytes.
analysis_duration Human-readable middleware analysis duration, for example 187.4Β΅s.
analysis_duration_ns Exact middleware analysis duration in nanoseconds, useful for metrics and aggregation.

🌟 Design Boundaries

This middleware:

  • analyzes requests, not responses
  • does not persist or export results
  • does not block traffic on analyzer failures
  • does not guarantee deep parsing for every content type
  • does not produce business risk decisions

Result Model

Each request produces a Profile:

type Profile struct {
	Meta        MetaInfo
	Entropy     *EntropyResult
	Fingerprint *FingerprintResult
	Complexity  *ComplexityResult
	Charset     *CharsetResult
	Warnings    []Warning
}

Analyzer results are optional pointers so handlers can distinguish between disabled, skipped, and populated modules.

Testing

go test ./...

The test suite covers middleware behavior, body replay, config normalization, analyzer outputs, and warning paths.

🐺 License

This project is open-sourced under the MIT License, which permits commercial use, modification, and distribution without requiring the original author's copyright notice to be retained.

Documentation ΒΆ

Index ΒΆ

Constants ΒΆ

This section is empty.

Variables ΒΆ

This section is empty.

Functions ΒΆ

func Middleware ΒΆ

func Middleware(cfg Config) func(http.Handler) http.Handler

func Wrap ΒΆ

func Wrap(next http.Handler, cfg Config) http.Handler

Types ΒΆ

type BodyConfig ΒΆ

type BodyConfig struct {
	MaxReadBytes        int64
	SampleBytes         int
	SampleStrategy      SampleStrategy
	AnalyzeMethods      []string
	AnalyzeContentTypes []string
}

type CharsetConfig ΒΆ

type CharsetConfig struct {
	MaxAnalyzeBytes         int
	EnableUnicodeScripts    bool
	EnableSuspiciousPattern bool
}

type CharsetResult ΒΆ

type CharsetResult struct {
	TotalChars       int
	ASCIIAlphaRatio  float64
	DigitRatio       float64
	WhitespaceRatio  float64
	SymbolRatio      float64
	ControlCharRatio float64
	NonASCIIRatio    float64
	SuspiciousFlags  []string
}

type ComplexityConfig ΒΆ

type ComplexityConfig struct {
	MaxJSONDepth          int
	MaxFields             int
	SupportedContentTypes []string
	EnableFormAnalysis    bool
	EnableMultipartMeta   bool
}

type ComplexityResult ΒΆ

type ComplexityResult struct {
	ContentType    string
	Depth          int
	FieldCount     int
	ObjectCount    int
	ArrayCount     int
	MaxArrayLength int
	Score          int
	ScoreFactors   []ScoreFactor
}

type Config ΒΆ

type Config struct {
	EnableEntropy     bool
	EnableFingerprint bool
	EnableComplexity  bool
	EnableCharset     bool

	Body        BodyConfig
	Fingerprint FingerprintConfig
	Complexity  ComplexityConfig
	Charset     CharsetConfig
}

func DefaultConfig ΒΆ

func DefaultConfig() Config

type EntropyResult ΒΆ

type EntropyResult struct {
	Value              float64
	SampledBytes       int
	TotalObservedBytes int64
	SampleStrategy     SampleStrategy
}

type FingerprintConfig ΒΆ

type FingerprintConfig struct {
	Headers       []string
	IncludeIP     bool
	IncludeTLS    bool
	TrustProxy    bool
	ProxyHeaders  []string
	HashAlgorithm string
	HashVersion   string
}

type FingerprintResult ΒΆ

type FingerprintResult struct {
	Fields        map[string]string
	Hash          string
	HashAlgorithm string
	HashVersion   string
}

type MetaInfo ΒΆ

type MetaInfo struct {
	Method           string
	Path             string
	ContentType      string
	ContentLength    int64
	Sampled          bool
	SampleBytes      int
	Truncated        bool
	AnalysisDuration time.Duration
}

type Profile ΒΆ

type Profile struct {
	Meta        MetaInfo
	Entropy     *EntropyResult
	Fingerprint *FingerprintResult
	Complexity  *ComplexityResult
	Charset     *CharsetResult
	Warnings    []Warning
}

func FromContext ΒΆ

func FromContext(ctx context.Context) (*Profile, bool)

type SampleStrategy ΒΆ

type SampleStrategy string
const (
	SampleStrategyHead SampleStrategy = "head"
)

type ScoreFactor ΒΆ

type ScoreFactor struct {
	Name  string
	Value int
}

type Warning ΒΆ

type Warning struct {
	Code    string
	Message string
}

Directories ΒΆ

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL