484 lines
13 KiB
Go
484 lines
13 KiB
Go
//go:build windows
|
|
|
|
package fingerprint
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows/registry"
|
|
)
|
|
|
|
// ── Win32 API (raw syscalls) ──────────────────────────────────────
|
|
|
|
var (
|
|
user32 = syscall.NewLazyDLL("user32.dll")
|
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
|
gdi32 = syscall.NewLazyDLL("gdi32.dll")
|
|
|
|
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
|
|
procSystemParametersInfo = user32.NewProc("SystemParametersInfoW")
|
|
procGetDC = user32.NewProc("GetDC")
|
|
procReleaseDC = user32.NewProc("ReleaseDC")
|
|
procGetDeviceCaps = gdi32.NewProc("GetDeviceCaps")
|
|
procGetActiveProcessorCount = kernel32.NewProc("GetActiveProcessorCount")
|
|
procGlobalMemoryStatusEx = kernel32.NewProc("GlobalMemoryStatusEx")
|
|
procGetUserDefaultLocaleName = kernel32.NewProc("GetUserDefaultLocaleName")
|
|
procGetTimeZoneInformation = kernel32.NewProc("GetTimeZoneInformation")
|
|
)
|
|
|
|
type memoryStatusEx struct {
|
|
Length uint32
|
|
MemoryLoad uint32
|
|
TotalPhys uint64
|
|
AvailPhys uint64
|
|
TotalPageFile uint64
|
|
AvailPageFile uint64
|
|
TotalVirtual uint64
|
|
AvailVirtual uint64
|
|
AvailExtendedVirtual uint64
|
|
}
|
|
|
|
type systemTime struct {
|
|
Year uint16
|
|
Month uint16
|
|
DayOfWeek uint16
|
|
Day uint16
|
|
Hour uint16
|
|
Minute uint16
|
|
Second uint16
|
|
Milliseconds uint16
|
|
}
|
|
|
|
type timeZoneInformation struct {
|
|
Bias int32
|
|
StandardName [32]uint16
|
|
StandardDate systemTime
|
|
StandardBias int32
|
|
DaylightName [32]uint16
|
|
DaylightDate systemTime
|
|
DaylightBias int32
|
|
}
|
|
|
|
type rect struct {
|
|
Left int32
|
|
Top int32
|
|
Right int32
|
|
Bottom int32
|
|
}
|
|
|
|
// ── Collect ───────────────────────────────────────────────────────
|
|
|
|
func Collect() *Result {
|
|
r := &Result{
|
|
Platform: "Win32",
|
|
OSArch: runtime.GOARCH,
|
|
}
|
|
|
|
r.OS = osProductName()
|
|
r.HardwareConcurrency = cpuCores()
|
|
r.DeviceMemory = deviceMemoryGB()
|
|
r.MaxTouchPoints = getSystemMetrics(95) // SM_MAXIMUMTOUCHES
|
|
r.ScreenWidth = getSystemMetrics(0) // SM_CXSCREEN
|
|
r.ScreenHeight = getSystemMetrics(1) // SM_CYSCREEN
|
|
|
|
var work rect
|
|
if systemParametersInfo(0x0030 /*SPI_GETWORKAREA*/, 0, unsafe.Pointer(&work), 0) {
|
|
r.AvailWidth = int(work.Right - work.Left)
|
|
r.AvailHeight = int(work.Bottom - work.Top)
|
|
}
|
|
|
|
hdc, _, _ := procGetDC.Call(0)
|
|
if hdc != 0 {
|
|
r.ColorDepth = getDeviceCaps(hdc, 12) // BITSPIXEL
|
|
if dpi := getDeviceCaps(hdc, 88); dpi > 0 { // LOGPIXELSX
|
|
r.DevicePixelRatio = float64(dpi) / 96.0
|
|
}
|
|
procReleaseDC.Call(0, hdc)
|
|
}
|
|
|
|
r.Timezone, r.TimezoneOffset = timezoneInfo()
|
|
r.Languages = languages()
|
|
r.Fonts = fonts()
|
|
r.GPU = gpuName()
|
|
r.Browsers = installedBrowsers()
|
|
r.UserAgent = userAgent(r.Browsers)
|
|
r.LocalIPs = localIPs()
|
|
|
|
return r
|
|
}
|
|
|
|
func getSystemMetrics(index int) int {
|
|
v, _, _ := procGetSystemMetrics.Call(uintptr(index))
|
|
return int(v)
|
|
}
|
|
|
|
func systemParametersInfo(uiAction, uiParam uint32, pvParam unsafe.Pointer, fWinIni uint32) bool {
|
|
r, _, _ := procSystemParametersInfo.Call(uintptr(uiAction), uintptr(uiParam), uintptr(pvParam), uintptr(fWinIni))
|
|
return r != 0
|
|
}
|
|
|
|
func getDeviceCaps(hdc uintptr, index int) int {
|
|
v, _, _ := procGetDeviceCaps.Call(hdc, uintptr(index))
|
|
return int(v)
|
|
}
|
|
|
|
func cpuCores() int {
|
|
v, _, _ := procGetActiveProcessorCount.Call(0xffff) // ALL_PROCESSOR_GROUPS
|
|
if v == 0 {
|
|
return runtime.NumCPU()
|
|
}
|
|
return int(v)
|
|
}
|
|
|
|
func deviceMemoryGB() int {
|
|
var ms memoryStatusEx
|
|
ms.Length = uint32(unsafe.Sizeof(ms))
|
|
r, _, _ := procGlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(&ms)))
|
|
if r == 0 {
|
|
return 0
|
|
}
|
|
gb := int(ms.TotalPhys / (1 << 30))
|
|
if gb > 8 {
|
|
gb = 8 // navigator.deviceMemory is clamped to 8
|
|
}
|
|
return gb
|
|
}
|
|
|
|
// ── OS ─────────────────────────────────────────────────────────────
|
|
|
|
func osProductName() string {
|
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
|
`SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE)
|
|
if err != nil {
|
|
return "Windows"
|
|
}
|
|
defer k.Close()
|
|
|
|
product, _, _ := k.GetStringValue("ProductName")
|
|
build, _, _ := k.GetStringValue("CurrentBuildNumber")
|
|
display, _, _ := k.GetStringValue("DisplayVersion")
|
|
ubr, _, _ := k.GetStringValue("UBR")
|
|
|
|
name := product
|
|
if name == "" {
|
|
name = "Windows"
|
|
}
|
|
ver := display
|
|
if ver == "" && build != "" {
|
|
ver = build
|
|
if ubr != "" {
|
|
ver = build + "." + ubr
|
|
}
|
|
}
|
|
if ver != "" {
|
|
return name + " " + ver
|
|
}
|
|
return name
|
|
}
|
|
|
|
// ── Timezone ──────────────────────────────────────────────────────
|
|
|
|
// windowsToIANA maps common Windows timezone names to IANA identifiers.
|
|
var windowsToIANA = map[string]string{
|
|
"Eastern Standard Time": "America/New_York",
|
|
"Central Standard Time": "America/Chicago",
|
|
"Mountain Standard Time": "America/Denver",
|
|
"Pacific Standard Time": "America/Los_Angeles",
|
|
"Alaskan Standard Time": "America/Anchorage",
|
|
"Hawaiian Standard Time": "Pacific/Honolulu",
|
|
"Atlantic Standard Time": "America/Halifax",
|
|
"Newfoundland Standard Time": "America/St_Johns",
|
|
"GMT Standard Time": "Europe/London",
|
|
"Greenwich Standard Time": "Atlantic/Reykjavik",
|
|
"W. Europe Standard Time": "Europe/Berlin",
|
|
"Central Europe Standard Time": "Europe/Budapest",
|
|
"Romance Standard Time": "Europe/Paris",
|
|
"Central European Standard Time": "Europe/Warsaw",
|
|
"E. Europe Standard Time": "Europe/Chisinau",
|
|
"Russian Standard Time": "Europe/Moscow",
|
|
"Israel Standard Time": "Asia/Jerusalem",
|
|
"China Standard Time": "Asia/Shanghai",
|
|
"Tokyo Standard Time": "Asia/Tokyo",
|
|
"Korea Standard Time": "Asia/Seoul",
|
|
"Singapore Standard Time": "Asia/Singapore",
|
|
"India Standard Time": "Asia/Kolkata",
|
|
"AUS Eastern Standard Time": "Australia/Sydney",
|
|
"New Zealand Standard Time": "Pacific/Auckland",
|
|
"SA Pacific Standard Time": "America/Bogota",
|
|
"Argentina Standard Time": "America/Argentina/Buenos_Aires",
|
|
"E. South America Standard Time": "America/Sao_Paulo",
|
|
}
|
|
|
|
func timezoneInfo() (string, int) {
|
|
var tzi timeZoneInformation
|
|
r, _, _ := procGetTimeZoneInformation.Call(uintptr(unsafe.Pointer(&tzi)))
|
|
if r == 0xFFFFFFFF {
|
|
return "", 0
|
|
}
|
|
windowsName := syscall.UTF16ToString(tzi.StandardName[:])
|
|
offset := -int(tzi.Bias)
|
|
|
|
iana := windowsToIANA[windowsName]
|
|
if iana == "" {
|
|
iana = windowsName
|
|
}
|
|
return iana, offset
|
|
}
|
|
|
|
// ── Languages ─────────────────────────────────────────────────────
|
|
|
|
func languages() []string {
|
|
var langs []string
|
|
var buf [85]uint16
|
|
r, _, _ := procGetUserDefaultLocaleName.Call(uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
|
|
if r > 0 && r <= uintptr(len(buf)) {
|
|
locale := syscall.UTF16ToString(buf[:r])
|
|
if locale != "" {
|
|
langs = append(langs, locale)
|
|
}
|
|
}
|
|
|
|
if prefs := chromeAcceptLanguages(); prefs != "" {
|
|
for _, l := range strings.Split(prefs, ",") {
|
|
l = strings.TrimSpace(l)
|
|
if l != "" && !containsStr(langs, l) {
|
|
langs = append(langs, l)
|
|
}
|
|
}
|
|
}
|
|
return langs
|
|
}
|
|
|
|
func chromeAcceptLanguages() string {
|
|
local := os.Getenv("LOCALAPPDATA")
|
|
if local == "" {
|
|
return ""
|
|
}
|
|
path := filepath.Join(local, `Google\Chrome\User Data\Default\Preferences`)
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
s := string(data)
|
|
idx := strings.Index(s, `"accept_languages"`)
|
|
if idx < 0 {
|
|
return ""
|
|
}
|
|
rest := s[idx:]
|
|
colon := strings.Index(rest, ":")
|
|
if colon < 0 {
|
|
return ""
|
|
}
|
|
rest = rest[colon+1:]
|
|
start := strings.Index(rest, `"`)
|
|
if start < 0 {
|
|
return ""
|
|
}
|
|
rest = rest[start+1:]
|
|
end := strings.Index(rest, `"`)
|
|
if end < 0 {
|
|
return ""
|
|
}
|
|
return rest[:end]
|
|
}
|
|
|
|
// ── Fonts ─────────────────────────────────────────────────────────
|
|
|
|
func fonts() []string {
|
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
|
`SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts`,
|
|
registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer k.Close()
|
|
|
|
names, err := k.ReadValueNames(-1)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var out []string
|
|
seen := map[string]bool{}
|
|
for _, name := range names {
|
|
f := strings.TrimSpace(name)
|
|
f = strings.TrimSuffix(f, " (TrueType)")
|
|
f = strings.TrimSuffix(f, " (OpenType)")
|
|
f = strings.TrimSuffix(f, " (All res)")
|
|
if f == "" || seen[f] {
|
|
continue
|
|
}
|
|
seen[f] = true
|
|
out = append(out, f)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// ── GPU ───────────────────────────────────────────────────────────
|
|
|
|
func gpuName() string {
|
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE,
|
|
`SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}`,
|
|
registry.ENUMERATE_SUB_KEYS|registry.QUERY_VALUE)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
defer k.Close()
|
|
|
|
subs, _ := k.ReadSubKeyNames(-1)
|
|
for _, sub := range subs {
|
|
if !strings.HasPrefix(sub, "0") {
|
|
continue
|
|
}
|
|
sk, err := registry.OpenKey(k, sub, registry.QUERY_VALUE)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
desc, _, _ := sk.GetStringValue("DriverDesc")
|
|
sk.Close()
|
|
desc = strings.TrimSpace(desc)
|
|
if desc == "" || strings.Contains(desc, "Microsoft Basic Display") ||
|
|
strings.Contains(desc, "Microsoft Remote Display") {
|
|
continue
|
|
}
|
|
return desc
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ── Installed browsers ────────────────────────────────────────────
|
|
|
|
var browserUpdateGUIDs = []struct {
|
|
name string
|
|
guid string
|
|
}{
|
|
{"Chrome", `{8A69D345-D564-463c-AFF1-A69D9E530F96}`},
|
|
{"Edge", `{56EB18F8-B008-4CBD-B6D2-8C97FE7E9062}`},
|
|
{"Brave", `{AFE6A462-C574-4B8A-AF43-4CC60DF4563B}`},
|
|
}
|
|
|
|
func installedBrowsers() []Browser {
|
|
var out []Browser
|
|
for _, b := range browserUpdateGUIDs {
|
|
ver := browserVersion(b.guid)
|
|
if ver == "" {
|
|
continue
|
|
}
|
|
out = append(out, Browser{Name: b.name, Version: ver, Path: browserExePath(b.name)})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func browserVersion(guid string) string {
|
|
for _, root := range []string{`SOFTWARE\Google\Update\Clients\`, `SOFTWARE\WOW6432Node\Google\Update\Clients\`} {
|
|
k, err := registry.OpenKey(registry.LOCAL_MACHINE, root+guid, registry.QUERY_VALUE)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
pv, _, err := k.GetStringValue("pv")
|
|
k.Close()
|
|
if err == nil && pv != "" {
|
|
return pv
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func browserExePath(name string) string {
|
|
var paths []string
|
|
pf := os.Getenv("ProgramFiles")
|
|
pf86 := os.Getenv("ProgramFiles(x86)")
|
|
switch name {
|
|
case "Chrome":
|
|
paths = []string{
|
|
filepath.Join(pf, `Google\Chrome\Application\chrome.exe`),
|
|
filepath.Join(pf86, `Google\Chrome\Application\chrome.exe`),
|
|
}
|
|
case "Edge":
|
|
paths = []string{
|
|
filepath.Join(pf, `Microsoft\Edge\Application\msedge.exe`),
|
|
filepath.Join(pf86, `Microsoft\Edge\Application\msedge.exe`),
|
|
}
|
|
case "Brave":
|
|
paths = []string{
|
|
filepath.Join(pf, `BraveSoftware\Brave-Browser\Application\brave.exe`),
|
|
filepath.Join(pf86, `BraveSoftware\Brave-Browser\Application\brave.exe`),
|
|
}
|
|
}
|
|
for _, p := range paths {
|
|
if _, err := os.Stat(p); err == nil {
|
|
return p
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ── User agent ────────────────────────────────────────────────────
|
|
|
|
func userAgent(browsers []Browser) string {
|
|
order := []string{"Chrome", "Edge", "Brave"}
|
|
version := ""
|
|
for _, want := range order {
|
|
for _, b := range browsers {
|
|
if b.Name == want && b.Version != "" {
|
|
version = b.Version
|
|
break
|
|
}
|
|
}
|
|
if version != "" {
|
|
break
|
|
}
|
|
}
|
|
if version == "" {
|
|
return ""
|
|
}
|
|
return fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s Safari/537.36", version)
|
|
}
|
|
|
|
// ── Local IPs ─────────────────────────────────────────────────────
|
|
|
|
func localIPs() []string {
|
|
ifaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
var ips []string
|
|
for _, iface := range ifaces {
|
|
addrs, err := iface.Addrs()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, addr := range addrs {
|
|
ip, _, err := net.ParseCIDR(addr.String())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if v4 := ip.To4(); v4 != nil && !v4.IsLoopback() {
|
|
s := v4.String()
|
|
if !containsStr(ips, s) {
|
|
ips = append(ips, s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ips
|
|
}
|
|
|
|
func containsStr(s []string, v string) bool {
|
|
for _, x := range s {
|
|
if x == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|