package obs
import (
"fmt"
"hash/fnv"
"net/http"
"net/netip"
"sort"
"strings"
)
// PrefixOf is an address's network range: the /24 of an IPv4 address,
// the /48 of an IPv6 one, "" for anything that does not parse. A /48 is
// what one site is given; a /24 is the smallest block routed alone.
func PrefixOf(addr string) string {
a, err := netip.ParseAddr(addr)
if err != nil {
return ""
}
a = a.Unmap()
bits := 24
if a.Is6() {
bits = 48
}
p, err := a.Prefix(bits)
if err != nil {
return ""
}
return p.String()
}
// browserHints are the headers a current browser sends with a page
// load, by the short name the fingerprint gives each. A script that
// borrows a browser's user agent rarely sends them, or sends them in a
// combination no browser does.
var browserHints = []struct{ header, short string }{
{"Sec-Ch-Ua", "ch"},
{"Sec-Ch-Ua-Mobile", "chm"},
{"Sec-Ch-Ua-Platform", "chp"},
{"Sec-Fetch-Site", "fs"},
{"Sec-Fetch-Mode", "fm"},
{"Sec-Fetch-Dest", "fd"},
{"Sec-Fetch-User", "fu"},
{"Upgrade-Insecure-Requests", "uir"},
{"Accept-Language", "al"},
{"Accept-Encoding", "ae"},
{"Cookie", "ck"},
{"Priority", "pri"},
}
// edgeHeaders are added by what stands in front of the forge, so they
// say nothing about the client.
func edgeHeader(name string) bool {
n := strings.ToLower(name)
return strings.HasPrefix(n, "cf-") || strings.HasPrefix(n, "x-forwarded-") || n == "x-real-ip" ||
n == "cdn-loop" || n == "true-client-ip" || n == "via"
}
// Fingerprint describes how a request presented itself. hints names the
// browser headers present, in a fixed order, with Sec-Fetch-Mode's value
// (navigate, cors...) since a page load says navigate; lang is the first
// language asked for; headset is a digest of the set of header names,
// the edge's own taken out.
func Fingerprint(h http.Header) (hints, lang, headset string) {
var on []string
for _, b := range browserHints {
if _, ok := h[b.header]; ok {
if b.short == "fm" {
on = append(on, "fm="+strings.ToLower(h.Get(b.header)))
continue
}
on = append(on, b.short)
}
}
hints = strings.Join(on, " ")
if al := h.Get("Accept-Language"); al != "" {
lang, _, _ = strings.Cut(al, ",")
lang, _, _ = strings.Cut(lang, ";")
lang = strings.TrimSpace(lang)
if len(lang) > 16 {
lang = lang[:16]
}
}
names := make([]string, 0, len(h))
for k := range h {
if !edgeHeader(k) {
names = append(names, strings.ToLower(k))
}
}
sort.Strings(names)
f := fnv.New64a()
f.Write([]byte(strings.Join(names, ",")))
return hints, lang, fmt.Sprintf("%016x", f.Sum64())
}