Files
live-hub-py/core/huya/fingerprint/env.js
T

124 lines
9.4 KiB
JavaScript

// Android WebView 环境仿真 (默认 M2102J2SC; 可传设备覆盖参数实现一号一设备)
// overrides = {model, androidVer, vendor, appVer, screenWidth, screenHeight}
function makeEnv(overrides) {
const ov = overrides || {};
const MODEL = ov.model || 'M2102J2SC';
const ANDROID_VER = ov.androidVer || '11';
const VENDOR = ov.vendor || 'xiaomi';
const APP_VER = ov.appVer || '13.4.22';
const UA = ov.userAgent || ('Mozilla/5.0 (Linux; Android ' + ANDROID_VER + '; ' + MODEL +
' Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 ' +
'Chrome/149.0.7827.159 Mobile Safari/537.36 huya adr/' + APP_VER + '/' + VENDOR + '/30');
// 视口按物理分辨率/3 仿真 (393x851 ≈ 1080x2120 / 2.75)
const VW = ov.screenWidth ? Math.round(ov.screenWidth / 2.75) : 393;
const VH = ov.screenHeight ? Math.round(ov.screenHeight / 2.75) : 851;
const W = globalThis;
// Node 18/20 do not expose a browser navigator; create one before defining
// the properties consumed by hydevice. Newer Node versions already expose
// a Navigator instance, which is retained.
W.navigator = W.navigator || {};
W.screen = {width:VW, height:VH, availWidth:VW, availHeight:VH,
colorDepth:24, pixelDepth:24, availLeft:0, availTop:0, orientation:{type:'portrait-primary', angle:0}};
W.devicePixelRatio = 2.75;
W.innerWidth = VW; W.innerHeight = Math.max(VH - 114, 500); W.outerWidth = VW; W.outerHeight = VH;
W.location = {href:'https://aq.huya.com/p/safe_auth/pt_auth.html', protocol:'https:', host:'aq.huya.com',
hostname:'aq.huya.com', port:'', pathname:'/p/safe_auth/pt_auth.html', search:'', hash:'',
origin:'https://aq.huya.com', toString(){return this.href}};
W.history = {length:2, state:null, pushState(){}, replaceState(){}, back(){}, go(){}};
const navProps = {
userAgent: UA,
appName:'Netscape', appVersion: UA.replace(/^Mozilla\//, ''),
platform:'Linux armv8l', language:'zh-CN', languages:['zh-CN','zh','en'],
cookieEnabled:true, doNotTrack:null, maxTouchPoints:5, hardwareConcurrency:8,
deviceMemory:8, vendor:'Google Inc.', productSub:'20030107', webdriver:false,
pdfViewerEnabled:false, javaEnabled(){return false},
getBattery(){return Promise.resolve({charging:true, level:0.9})},
userAgentData: {brands:[{brand:'Chromium',version:'149'},{brand:'Google Chrome',version:'149'},{brand:'Not?A_Brand',version:'24'}],
mobile:true, platform:'Android',
getHighEntropyValues(){return Promise.resolve({architecture:'arm', bitness:'64', model:MODEL, platformVersion:ANDROID_VER+'.0.0', uaFullVersion:'149.0.7827.159'})}},
connection: {effectiveType:'4g', downlink:10, rtt:50, saveData:false},
plugins: {length:0, item(){return null}, namedItem(){return null}},
mimeTypes: {length:0},
};
for (const [k,v] of Object.entries(navProps)) {
try { Object.defineProperty(W.navigator, k, {value:v, configurable:true}); } catch(e){ try{W.navigator[k]=v}catch(_){} }
}
// canvas 2d/webgl 最小仿真
const glConsts = {RENDERER:0x1F01, VENDOR:0x1F00, VERSION:0x1F02, SHADING_LANGUAGE_VERSION:0x8B8C,
MAX_TEXTURE_SIZE:0x0D33, MAX_RENDERBUFFER_SIZE:0x84E8, MAX_VIEWPORT_DIMS:0x0D3A};
function fakeGL(){
return new Proxy({
getExtension(){ return {UNMASKED_VENDOR_WEBGL:0x9245, UNMASKED_RENDERER_WEBGL:0x9246}; },
getParameter(p){
if (p===0x9245) return 'Qualcomm';
if (p===0x9246) return 'Adreno (TM) 650';
if (p===glConsts.MAX_TEXTURE_SIZE) return 16384;
if (p===glConsts.MAX_VIEWPORT_DIMS) return new Int32Array([16384,16384]);
return 'WebKit WebGL';
},
getContextAttributes(){return {alpha:true, antialias:true, depth:true};},
createBuffer(){return {}}, bindBuffer(){}, bufferData(){}, createProgram(){return {}},
attachShader(){}, linkProgram(){}, useProgram(){}, createShader(){return {}},
shaderSource(){}, compileShader(){}, getShaderParameter(){return true},
getProgramParameter(){return true}, getSupportedExtensions(){return ['WEBGL_debug_renderer_info']},
readPixels(x,y,w,h,t,f,buf){ if(buf) buf.fill(128); },
}, { get(t,p){ if(p in t) return t[p]; if(typeof p==='string' && p.startsWith('on')) return null; return undefined; } });
}
const ctx2d = new Proxy({
fillRect(){}, clearRect(){}, getImageData(x,y,w,h){return {data:new Uint8ClampedArray(w*h*4).fill(128), width:w, height:h}},
putImageData(){}, createImageData(w,h){return {data:new Uint8ClampedArray(w*h*4), width:w, height:h}},
setTransform(){}, transform(){}, translate(){}, scale(){}, rotate(){}, save(){}, restore(){},
beginPath(){}, closePath(){}, moveTo(){}, lineTo(){}, stroke(){}, fill(){}, arc(){}, rect(){}, clip(){},
createLinearGradient(){return {addColorStop(){}}}, createRadialGradient(){return {addColorStop(){}}},
fillText(){}, strokeText(){}, measureText(t){return {width:String(t).length*7}},
isPointInPath(){return false}, drawImage(){}, arcTo(){}, bezierCurveTo(){}, quadraticCurveTo(){},
createPattern(){return {}}, setLineDash(){}, getLineDash(){return []},
}, { get(t,p){ if(p in t) return t[p]; if(p==='canvas') return {}; return undefined; } });
const canvasProto = {
getContext(type){
if (type === '2d') return ctx2d;
if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') return fakeGL();
return null;
},
toDataURL(){ return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=='; },
toBlob(cb){ cb && cb(null); },
addEventListener(){}, removeEventListener(){}, setAttribute(){}, getBoundingClientRect(){return {left:0,top:0,width:300,height:150}},
style:{}, width:300, height:150,
};
W.document = {
createElement(tag){ const el = Object.assign({}, canvasProto, {tagName:String(tag||'div').toUpperCase(), style:{}, children:[],
setAttribute(){}, getAttribute(){return null}, appendChild(c){this.children.push(c)}, removeChild(){}, addEventListener(){}, removeEventListener(){},
classList:{add(){},remove(){},contains(){return false}}, attachShadow(){return {appendChild(){}}}, getBoundingClientRect(){return {left:0,top:0,right:100,bottom:30,width:100,height:30}} });
return el; },
createEvent(){return {initEvent(){}}},
createElementNS(ns, tag){ return this.createElement(tag); },
createTextNode(t){return {textContent:t}},
getElementById(){return null}, querySelector(){return null}, querySelectorAll(){return []},
getElementsByTagName(t){ if(String(t).toLowerCase()==='head') return [this.head]; return []; },
documentElement:{setAttribute(){}, getAttribute(){return null}, style:{}},
head:{appendChild(){}}, body:{appendChild(){}},
cookie:'', referrer:'', title:'pt_auth', domain:'aq.huya.com', readyState:'complete', visibilityState:'visible',
addEventListener(){}, removeEventListener(){},
};
W.localStorage = (()=>{const m={}; return {getItem:k=>m[k]??null,setItem:(k,v)=>m[k]=String(v),removeItem:k=>delete m[k],clear:()=>{},key:i=>Object.keys(m)[i]??null,get length(){return Object.keys(m).length}}})();
W.sessionStorage = (()=>{const m={}; return {getItem:k=>m[k]??null,setItem:(k,v)=>m[k]=String(v),removeItem:k=>delete m[k],clear:()=>{},key:i=>null,get length(){return 0}}})();
W.indexedDB = {open(){return {onsuccess:null,onerror:null,onupgradeneeded:null,result:{objectStoreNames:{contains(){return false}},createObjectStore(){return {createIndex(){}}}},set onsuccess(f){setTimeout(()=>{},0)}}}};
W.performance = W.performance || {now:()=>Date.now(), timeOrigin:Date.now(), timing:{navigationStart:Date.now()}, getEntriesByType(){return []}, mark(){}, measure(){}};
W.AudioContext = class { constructor(){this.sampleRate=48000; this.destination={}} createOscillator(){return {type:'',frequency:{value:0,setValueAtTime(){}},connect(){},start(){},stop(){}}} createDynamicsCompressor(){return {threshold:{value:0},knee:{value:0},ratio:{value:0},attack:{value:0},release:{value:0},connect(){}}} createAnalyser(){return {frequencyBinCount:1024,getFloatFrequencyData(a){a&&a.fill(-100)}}} createGain(){return {gain:{value:1,setValueAtTime(){}},connect(){}}} createScriptProcessor(){return {connect(){},onaudioprocess:null}} decodeAudioData(){return Promise.resolve({getChannelData(){return new Float32Array(5000)}})} resume(){return Promise.resolve()} close(){return Promise.resolve()} };
W.webkitAudioContext = W.AudioContext;
W.OfflineAudioContext = W.AudioContext;
W.WebGLRenderingContext = {RENDERER:glConsts.RENDERER, VENDOR:glConsts.VENDOR, VERSION:glConsts.VERSION, SHADING_LANGUAGE_VERSION:glConsts.SHADING_LANGUAGE_VERSION, MAX_TEXTURE_SIZE:glConsts.MAX_TEXTURE_SIZE};
W.RTCPeerConnection = class { createDataChannel(){return {}} createOffer(){return Promise.resolve({})} setLocalDescription(){return Promise.resolve()} close(){} };
W.webkitRTCPeerConnection = W.RTCPeerConnection;
W.Notification = {permission:'default'};
W.permissions = {query(){return Promise.resolve({state:'prompt'})}};
W.matchMedia = () => ({matches:false, media:'', addListener(){}, removeListener(){}, addEventListener(){}, removeEventListener(){}});
W.getComputedStyle = () => new Proxy({getPropertyValue:()=>''}, {get(t,p){ if(p in t) return t[p]; return ()=>''; }});
W.requestAnimationFrame = cb => setTimeout(()=>cb(Date.now()), 16);
W.cancelAnimationFrame = clearTimeout;
W.crypto = require('crypto').webcrypto;
W.TextEncoder = require('util').TextEncoder;
W.TextDecoder = require('util').TextDecoder;
}
module.exports = { makeEnv };