UNPKG

remoteview

Version:

Remote isolated browser platform by Dosyago

442 lines (396 loc) 12.5 kB
// to evade some basic bot detection pertaining to detecting signatures of headless // initial evasions from @sangaline // https://intoli.com/blog/not-possible-to-block-chrome-headless/ // https://intoli.com/blog/not-possible-to-block-chrome-headless/test-headless-final.js // also // https://infosimples.github.io/detect-headless/ { /* eslint-disable no-inner-declarations */ // temp location pointer targeting diagnois code /** // state and config const Events = ` click mousemove mouseover mouseout mousedown pointerdown pointerup pointermove `.split(/\s+/g).filter(s => s.length); const Styles = { position: 'fixed', pointerEvents: 'none', transform: 'translate(-50%, -50%)', zIndex:999999999, display: 'block', width: '1rem', height: '1rem', background: 'transparent', border: 'medium solid aqua', left: '0px', top: '0px' } const positionMarkers = []; install(); function install() { for( const eventName of Events ) { document.addEventListener(eventName, showPosition); } document.defaultView.addEventListener('resize', () => { const div = document.createElement('div'); Object.assign(div.style, Styles); Object.assign(div.style, { left: window.innerWidth/2+'px', top: (window.innerHeight - window.innerHeight*0.85)+10+'px', width: '66px', borderColor: 'red', background: 'lime', textAlign: 'center' }) div.style.position = 'fixed'; div.innerText = 'RESIZE'; setTimeout(() => { div.remove(); }, 3000); document.body.insertAdjacentElement('beforeend', div); }); } // display a marker function showPosition({clientX,clientY}) { const div = document.createElement('div'); const timestamp = Date.now(); Object.assign(div.style, Styles); Object.assign(div.style, { left:clientX+'px', top:clientY+'px' }); document.body.insertAdjacentElement('afterbegin', div); positionMarkers.push({ timestamp, div, remove(ms) { setTimeout(() => div.remove(), ms); } }); } // remove markers older than 2 seconds setInterval( () => { const Now = Date.now(); let spliceCount = 0; for( const {timestamp, remove} of positionMarkers) { const age = Now - timestamp; if ( age > 500 ) { remove(Math.max(0, 1000 - age)); spliceCount++; } } positionMarkers.splice(0, spliceCount); }, 1000); **/ /* eslint-enable no-inner-declarations */ } { /* eslint-disable no-inner-declarations */ // temp location for file input related code in page context const isFileInput = el => el.localName == 'input' && el.type == 'file'; self.zombieDosyLastClicked = {}; self.addEventListener('click', click => { const {target} = click; const {clientX,clientY} = click; self.zombieDosyLastClicked = {target}; const stack = expandShadowRoots(Array.from(document.elementsFromPoint(clientX,clientY)), clientX, clientY); if ( isFileInput(target) && target ) { self.zombieDosyLastClicked.fileInput = target; } else { const fileInputFromStack = stack.find(isFileInput); self.zombieDosyLastClicked.fileInput = fileInputFromStack; } //console.log(JSON.stringify({zombieDosyLastClicked:self.zombieDosyLastClicked})); }, {capture:true}); function expandShadowRoots(els, x,y ) { const result = []; for( const el of els ) { if ( el.shadowRoot ) { result.push(...el.shadowRoot.elementsFromPoint(x,y)); } else { result.push(el); } } return result; } console.log(JSON.stringify({install:"Installed zombieDosyLastClicked with isFileInput support"})); /* eslint-enable no-inner-declarations */ } { /* eslint-disable no-inner-declarations */ // Pass the Webdriver Test. const newProto = navigator.__proto__; delete newProto.webdriver; navigator.__proto__ = newProto; // Pass the Chrome Test. // We can mock this in as much depth as we need for the test. window.chrome = { runtime: {} }; // Pass the Permissions Test. const originalQuery = window.navigator.permissions.query; window.navigator.permissions.__proto__.query = parameters => parameters.name === 'notifications' ? Promise.resolve({state: Notification.permission}) : originalQuery(parameters); // Inspired by: https://github.com/ikarienator/phantomjs_hide_and_seek/blob/master/5.spoofFunctionBind.js const oldCall = Function.prototype.call; function call() { return oldCall.apply(this, arguments); } Function.prototype.call = call; const nativeToStringFunctionString = Error.toString().replace(/Error/g, "toString"); const oldToString = Function.prototype.toString; function functionToString() { if (this === window.navigator.permissions.query) { return "function query() { [native code] }"; } if (this === functionToString) { return nativeToStringFunctionString; } return oldCall.call(oldToString, this); } Function.prototype.toString = functionToString; // Pass the Plugins Length Test. // Overwrite the `plugins` property to use a custom getter. const PLUGINS = [ { name: "Chrome PDF Plugin", description: "Portable Document Format", filename: "internal-pdf-viewer", mimetypes: [ { type: "application/x-google-chrome-pdf", suffixes: "pdf", description: "Portable Document Format", } ] }, { name: "Chrome PDF Viewer", description: "", filename: "mhjfbmdgcfjbbpaeojofohoefgiehjai", mimetypes: [ { type: "application/pdf", suffixes: "pdf", description: "", } ] }, { name: "Native Client", description: "", filename: "internal-nacl-plugin", mimetypes: [ { description: "Native Client Executable", suffixes: "", type: "application/x-nacl" }, { description: "Portable Native Client Executable", suffixes: "", type: "application/x-pnacl" } ] } ]; const MIMETYPES = [ { type: "application/x-google-chrome-pdf", suffixes: "pdf", description: "Portable Document Format", }, { type: "application/pdf", suffixes: "pdf", description: "", }, { description: "Native Client Executable", suffixes: "", type: "application/x-nacl" }, { description: "Portable Native Client Executable", suffixes: "", type: "application/x-pnacl" } ]; const {PluginArray, MimeTypeArray} = getPluginsClasses(); try { delete newProto.plugins; newProto.plugins = PluginArray; } catch(e) { console.log(JSON.stringify({msg:`navigator.plugins needs workaround.`})); } try { delete newProto.mimeTypes; newProto.mimeTypes = MimeTypeArray; } catch(e) { console.log(JSON.stringify({msg:`navigator.mimeTypes needs workaround.`})); } // Classes function getPluginsClasses() { class Plugin { constructor({ name, description, filename, mimetypes }) { Object.assign(this, { name, description, filename }); mimetypes.forEach((mt, index) => { this[index] = mt; Object.defineProperty(mt, 'enabledPlugin', { get: () => this }); }); this.item = this.namedItem = i => this[i]; this.length = mimetypes.length; Object.freeze(this); } toString() { return '[object Plugin]'; } } class MimeType { constructor({ type, suffixes, description }) { let plugin; Object.assign(this, { type, suffixes, description }); Object.defineProperty(this, 'enabledPlugin', { set: p => { plugin = plugin || p; }, get: () => plugin }); Object.freeze(this); } toString() { return '[object MimeType]'; } } self.Plugin = Plugin; self.MimeType = MimeType; let Z = {}; if ( navigator.plugins.__proto__ ) { Z = Object.create(navigator.plugins.__proto__); } let Z2 = {}; if ( navigator.mimeTypes.__proto__ ) { Z2 = Object.create(navigator.mimeTypes.__proto__); } MimeTypeArray.call(Z2,{mimeTypes:MIMETYPES}); PluginArray.call(Z,{plugins:PLUGINS}); return {PluginArray: Z, MimeTypeArray: Z2, Plugin, MimeType}; function PluginArray({plugins}) { Object.defineProperty(this, 'length', { value: plugins.length }); plugins.forEach((p, i) => { const plugin = new Plugin(p); this[i] = plugin; this[p.name] = plugin; }); Object.defineProperty(this, Symbol.iterator, { get: () => () => plugins.values() }); Object.defineProperties(this, { toString: { value: '[object PluginArray]' }, item: { value: i => this[i] }, namedItem: { value: n => this[n] }, refresh: { value: () => undefined } }); Object.freeze(this); } function MimeTypeArray({mimeTypes}) { Object.defineProperty(this, 'length', { value: mimeTypes.length }); mimeTypes.forEach((m, i) => { const mime = new MimeType(m); this[i] = mime; this[m.name] = mime; }); Object.defineProperty(this, Symbol.iterator, { get: () => () => mimeTypes.values() }); Object.defineProperties(this, { toString: { value: '[object MimeTypeArray]' }, item: { value: i => this[i] }, namedItem: { value: n => this[n] }, refresh: { value: () => undefined } }); Object.freeze(this); } } // Pass the Languages Test. try { Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] }); } catch(e) { console.log(JSON.stringify({msg:`navigator.languages needs workaround.`})); } // [caution] Pass the iframe Test /** careful with this, it might break something (overriding contentWindow on an iframe? what if a page relies on an actual value for this ?? **/ /** /** Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', { get: function() { return window; } }); **/ // [caution] Pass toString test, though it breaks console.debug() from working window.console.debug = () => { return null; }; // Pass the connection rtt test delete newProto.connection; newProto.connection = { rtt:50, type:"unknown", effectiveType:"4g", downlinkMax:Infinity, downlink:(Math.random()*15).toFixed(1), saveData: false, onchange: null, ontypechange: null, }; // Pass the MouseEvent movementX/Y test const mouseProto = MouseEvent.prototype; delete mouseProto.movementX; delete mouseProto.movementY; mouseProto.movementX = 10; mouseProto.movementY = 10; document.addEventListener('mousemove',e => { e.__proto__ = mouseProto; //const {clientX,clientY,movementX,movementY} = e; //console.log(JSON.stringify({mm:{clientX,clientY,movementX,movementY}})); }, {capture:true}); /* eslint-enable no-inner-declarations */ }