Rename to hkt.sh

This commit is contained in:
mango
2026-03-21 01:10:53 +08:00
parent 76a263d0f9
commit 8f1171fe99
6676 changed files with 1724268 additions and 0 deletions

View File

@@ -0,0 +1,254 @@
'use strict'
const { PuppeteerExtraPlugin } = require('puppeteer-extra-plugin')
const withUtils = require('../_utils/withUtils')
const STATIC_DATA = require('./staticData.json')
/**
* Mock the `chrome.runtime` object if not available (e.g. when running headless) and on a secure site.
*/
class Plugin extends PuppeteerExtraPlugin {
constructor(opts = {}) {
super(opts)
}
get name() {
return 'stealth/evasions/chrome.runtime'
}
get defaults() {
return { runOnInsecureOrigins: false } // Override for testing
}
async onPageCreated(page) {
await withUtils(page).evaluateOnNewDocument(
(utils, { opts, STATIC_DATA }) => {
if (!window.chrome) {
// Use the exact property descriptor found in headful Chrome
// fetch it via `Object.getOwnPropertyDescriptor(window, 'chrome')`
Object.defineProperty(window, 'chrome', {
writable: true,
enumerable: true,
configurable: false, // note!
value: {} // We'll extend that later
})
}
// That means we're running headful and don't need to mock anything
const existsAlready = 'runtime' in window.chrome
// `chrome.runtime` is only exposed on secure origins
const isNotSecure = !window.location.protocol.startsWith('https')
if (existsAlready || (isNotSecure && !opts.runOnInsecureOrigins)) {
return // Nothing to do here
}
window.chrome.runtime = {
// There's a bunch of static data in that property which doesn't seem to change,
// we should periodically check for updates: `JSON.stringify(window.chrome.runtime, null, 2)`
...STATIC_DATA,
// `chrome.runtime.id` is extension related and returns undefined in Chrome
get id() {
return undefined
},
// These two require more sophisticated mocks
connect: null,
sendMessage: null
}
const makeCustomRuntimeErrors = (preamble, method, extensionId) => ({
NoMatchingSignature: new TypeError(
preamble + `No matching signature.`
),
MustSpecifyExtensionID: new TypeError(
preamble +
`${method} called from a webpage must specify an Extension ID (string) for its first argument.`
),
InvalidExtensionID: new TypeError(
preamble + `Invalid extension id: '${extensionId}'`
)
})
// Valid Extension IDs are 32 characters in length and use the letter `a` to `p`:
// https://source.chromium.org/chromium/chromium/src/+/master:components/crx_file/id_util.cc;drc=14a055ccb17e8c8d5d437fe080faba4c6f07beac;l=90
const isValidExtensionID = str =>
str.length === 32 && str.toLowerCase().match(/^[a-p]+$/)
/** Mock `chrome.runtime.sendMessage` */
const sendMessageHandler = {
apply: function(target, ctx, args) {
const [extensionId, options, responseCallback] = args || []
// Define custom errors
const errorPreamble = `Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback): `
const Errors = makeCustomRuntimeErrors(
errorPreamble,
`chrome.runtime.sendMessage()`,
extensionId
)
// Check if the call signature looks ok
const noArguments = args.length === 0
const tooManyArguments = args.length > 4
const incorrectOptions = options && typeof options !== 'object'
const incorrectResponseCallback =
responseCallback && typeof responseCallback !== 'function'
if (
noArguments ||
tooManyArguments ||
incorrectOptions ||
incorrectResponseCallback
) {
throw Errors.NoMatchingSignature
}
// At least 2 arguments are required before we even validate the extension ID
if (args.length < 2) {
throw Errors.MustSpecifyExtensionID
}
// Now let's make sure we got a string as extension ID
if (typeof extensionId !== 'string') {
throw Errors.NoMatchingSignature
}
if (!isValidExtensionID(extensionId)) {
throw Errors.InvalidExtensionID
}
return undefined // Normal behavior
}
}
utils.mockWithProxy(
window.chrome.runtime,
'sendMessage',
function sendMessage() {},
sendMessageHandler
)
/**
* Mock `chrome.runtime.connect`
*
* @see https://developer.chrome.com/apps/runtime#method-connect
*/
const connectHandler = {
apply: function(target, ctx, args) {
const [extensionId, connectInfo] = args || []
// Define custom errors
const errorPreamble = `Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): `
const Errors = makeCustomRuntimeErrors(
errorPreamble,
`chrome.runtime.connect()`,
extensionId
)
// Behavior differs a bit from sendMessage:
const noArguments = args.length === 0
const emptyStringArgument = args.length === 1 && extensionId === ''
if (noArguments || emptyStringArgument) {
throw Errors.MustSpecifyExtensionID
}
const tooManyArguments = args.length > 2
const incorrectConnectInfoType =
connectInfo && typeof connectInfo !== 'object'
if (tooManyArguments || incorrectConnectInfoType) {
throw Errors.NoMatchingSignature
}
const extensionIdIsString = typeof extensionId === 'string'
if (extensionIdIsString && extensionId === '') {
throw Errors.MustSpecifyExtensionID
}
if (extensionIdIsString && !isValidExtensionID(extensionId)) {
throw Errors.InvalidExtensionID
}
// There's another edge-case here: extensionId is optional so we might find a connectInfo object as first param, which we need to validate
const validateConnectInfo = ci => {
// More than a first param connectInfo as been provided
if (args.length > 1) {
throw Errors.NoMatchingSignature
}
// An empty connectInfo has been provided
if (Object.keys(ci).length === 0) {
throw Errors.MustSpecifyExtensionID
}
// Loop over all connectInfo props an check them
Object.entries(ci).forEach(([k, v]) => {
const isExpected = ['name', 'includeTlsChannelId'].includes(k)
if (!isExpected) {
throw new TypeError(
errorPreamble + `Unexpected property: '${k}'.`
)
}
const MismatchError = (propName, expected, found) =>
TypeError(
errorPreamble +
`Error at property '${propName}': Invalid type: expected ${expected}, found ${found}.`
)
if (k === 'name' && typeof v !== 'string') {
throw MismatchError(k, 'string', typeof v)
}
if (k === 'includeTlsChannelId' && typeof v !== 'boolean') {
throw MismatchError(k, 'boolean', typeof v)
}
})
}
if (typeof extensionId === 'object') {
validateConnectInfo(extensionId)
throw Errors.MustSpecifyExtensionID
}
// Unfortunately even when the connect fails Chrome will return an object with methods we need to mock as well
return utils.patchToStringNested(makeConnectResponse())
}
}
utils.mockWithProxy(
window.chrome.runtime,
'connect',
function connect() {},
connectHandler
)
function makeConnectResponse() {
const onSomething = () => ({
addListener: function addListener() {},
dispatch: function dispatch() {},
hasListener: function hasListener() {},
hasListeners: function hasListeners() {
return false
},
removeListener: function removeListener() {}
})
const response = {
name: '',
sender: undefined,
disconnect: function disconnect() {},
onDisconnect: onSomething(),
onMessage: onSomething(),
postMessage: function postMessage() {
if (!arguments.length) {
throw new TypeError(`Insufficient number of arguments.`)
}
throw new Error(`Attempting to use a disconnected port object`)
}
}
return response
}
},
{
opts: this.opts,
STATIC_DATA
}
)
}
}
module.exports = function(pluginConfig) {
return new Plugin(pluginConfig)
}

View File

@@ -0,0 +1,286 @@
const test = require('ava')
const {
getVanillaFingerPrint,
getStealthFingerPrint
} = require('../../test/util')
const { vanillaPuppeteer, addExtra } = require('../../test/util')
const Plugin = require('.')
const STATIC_DATA = require('./staticData.json')
/* global chrome */
test('vanilla: is chrome false', async t => {
const pageFn = async page => await page.evaluate(() => window.chrome) // eslint-disable-line
const { pageFnResult: chrome, hasChrome } = await getVanillaFingerPrint(
pageFn
)
t.is(hasChrome, false)
t.false(chrome instanceof Object)
t.is(chrome, undefined)
})
test('stealth: is chrome true', async t => {
const pageFn = async page => await page.evaluate(() => window.chrome) // eslint-disable-line
const { pageFnResult: chrome, hasChrome } = await getStealthFingerPrint(
Plugin,
pageFn
)
t.is(hasChrome, true)
t.true(chrome instanceof Object)
})
test('stealth: will add convincing chrome.runtime object', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(
Plugin({
runOnInsecureOrigins: true // for testing
})
)
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
//
const results = await page.evaluate(() => {
const catchErr = (fn, ...args) => {
try {
return fn.apply(this, args)
} catch (err) {
return err.toString()
}
}
return {
runtime: {
exists: window.chrome && 'runtime' in window.chrome,
toString: chrome.runtime.toString()
},
staticData: {
OnInstalledReason: chrome.runtime.OnInstalledReason,
OnRestartRequiredReason: chrome.runtime.OnRestartRequiredReason,
PlatformArch: chrome.runtime.PlatformArch,
PlatformNaclArch: chrome.runtime.PlatformNaclArch,
PlatformOs: chrome.runtime.PlatformOs,
RequestUpdateCheckStatus: chrome.runtime.RequestUpdateCheckStatus
},
id: {
exists: 'id' in chrome.runtime,
undefined: chrome.runtime.id === undefined
},
sendMessage: {
exists: 'sendMessage' in chrome.runtime,
name: chrome.runtime.sendMessage.name,
toString1: chrome.runtime.sendMessage + '',
toString2: chrome.runtime.sendMessage.toString(),
validIdWorks:
chrome.runtime.sendMessage('nckgahadagoaajjgafhacjanaoiihapd', '') ===
undefined
},
sendMessageErrors: {
noArg: catchErr(chrome.runtime.sendMessage),
singleArg: catchErr(chrome.runtime.sendMessage, ''),
tooManyArg: catchErr(
chrome.runtime.sendMessage,
'',
'',
'',
'',
'',
''
),
incorrectArg: catchErr(chrome.runtime.sendMessage, '', '', {}, ''),
noValidID: catchErr(chrome.runtime.sendMessage, 'foo', '')
}
}
})
const bla = `TypeError: Error in invocation of runtime.sendMessage(optional string extensionId, any message, optional object options, optional function responseCallback)`
t.deepEqual(results, {
runtime: {
exists: true,
toString: '[object Object]'
},
staticData: STATIC_DATA,
id: {
exists: true,
undefined: true
},
sendMessage: {
exists: true,
name: 'sendMessage',
toString1: 'function sendMessage() { [native code] }',
toString2: 'function sendMessage() { [native code] }',
validIdWorks: true
},
sendMessageErrors: {
noArg: `${bla}: No matching signature.`,
singleArg: `${bla}: chrome.runtime.sendMessage() called from a webpage must specify an Extension ID (string) for its first argument.`,
tooManyArg: `${bla}: No matching signature.`,
incorrectArg: `${bla}: No matching signature.`,
noValidID: `${bla}: Invalid extension id: 'foo'`
}
})
})
test('stealth: will add convincing chrome.runtime.connect', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(
Plugin({
runOnInsecureOrigins: true // for testing
})
)
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const results = await page.evaluate(() => {
const catchErr = (fn, ...args) => {
try {
return fn.apply(this, args)
} catch (err) {
return err.toString()
}
}
return {
connect: {
exists: 'connect' in chrome.runtime,
name: chrome.runtime.connect.name,
toString1: chrome.runtime.connect + '',
toString2: chrome.runtime.connect.toString(),
validIdWorks:
chrome.runtime.connect('nckgahadagoaajjgafhacjanaoiihapd') !==
undefined
},
connectErrors: {
noArg: catchErr(chrome.runtime.connect),
singleArg: catchErr(chrome.runtime.connect, ''),
tooManyArg: catchErr(chrome.runtime.connect, '', '', '', '', '', ''),
incorrectArg: catchErr(chrome.runtime.connect, '', '', {}, ''),
noValidID: catchErr(chrome.runtime.connect, 'foo', ''),
connectInfoFirst: {
emptyObject: catchErr(chrome.runtime.connect, {}),
tooManyArg: catchErr(chrome.runtime.connect, {}, {}),
unexpectedProp: catchErr(chrome.runtime.connect, { wtf: true }),
invalidName: catchErr(chrome.runtime.connect, { name: 666 }),
invalidTLS: catchErr(chrome.runtime.connect, {
includeTlsChannelId: 777
}),
invalidBoth: catchErr(chrome.runtime.connect, {
name: 666,
includeTlsChannelId: 777
}),
validName: catchErr(chrome.runtime.connect, { name: 'foo' }),
missingExtensionId: catchErr(chrome.runtime.connect, {
name: 'bob',
includeTlsChannelId: false
})
}
}
}
})
const bla = `TypeError: Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo)`
t.deepEqual(results, {
connect: {
exists: true,
name: 'connect',
toString1: 'function connect() { [native code] }',
toString2: 'function connect() { [native code] }',
validIdWorks: true
},
connectErrors: {
noArg: `${bla}: chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.`,
singleArg: `${bla}: chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.`,
tooManyArg: `${bla}: No matching signature.`,
incorrectArg: `${bla}: No matching signature.`,
noValidID: `${bla}: Invalid extension id: 'foo'`,
connectInfoFirst: {
emptyObject: `${bla}: chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.`,
tooManyArg: `${bla}: No matching signature.`,
unexpectedProp: `${bla}: Unexpected property: 'wtf'.`,
invalidName: `${bla}: Error at property 'name': Invalid type: expected string, found number.`,
invalidTLS: `${bla}: Error at property 'includeTlsChannelId': Invalid type: expected boolean, found number.`,
invalidBoth: `${bla}: Error at property 'name': Invalid type: expected string, found number.`,
validName: `${bla}: chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.`,
missingExtensionId: `${bla}: chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.`
}
}
})
})
test('stealth: will add convincing chrome.runtime.connect response', async t => {
const puppeteer = addExtra(vanillaPuppeteer).use(
Plugin({
runOnInsecureOrigins: true // for testing
})
)
const browser = await puppeteer.launch({ headless: true })
const page = await browser.newPage()
const results = await page.evaluate(() => {
const connectResponse = chrome.runtime.connect(
'nckgahadagoaajjgafhacjanaoiihapd'
)
return {
connectResponse: {
exists: !!connectResponse,
toString1: connectResponse + '',
toString2: connectResponse.toString(),
nestedToString: connectResponse.onDisconnect.addListener + ''
},
disconnect: {
toString: connectResponse.disconnect + '',
noReturn: connectResponse.disconnect() === undefined
}
}
})
t.deepEqual(results, {
connectResponse: {
exists: true,
toString1: '[object Object]',
toString2: '[object Object]',
nestedToString: `function addListener() { [native code] }`
},
disconnect: {
toString: `function disconnect() { [native code] }`,
noReturn: true
}
})
})
// FIXME: This changed in more recent chrome versions
// test('stealth: error stack is fine', async t => {
// const puppeteer = addExtra(vanillaPuppeteer).use(
// Plugin({
// runOnInsecureOrigins: true // for testing
// })
// )
// const browser = await puppeteer.launch({ headless: true })
// const page = await browser.newPage()
// const result = await page.evaluate(() => {
// const catchErr = (fn, ...args) => {
// try {
// return fn.apply(this, args)
// } catch ({ name, message, stack }) {
// return {
// name,
// message,
// stack
// }
// }
// }
// return catchErr(chrome.runtime.connect, '').stack
// })
// /**
// * OK:
// TypeError: Error in invocation of runtime.connect(optional string extensionId, optional object connectInfo): chrome.runtime.connect() called from a webpage must specify an Extension ID (string) for its first argument.␊
// - at catchErr (__puppeteer_evaluation_script__:4:19)␊
// - at __puppeteer_evaluation_script__:18:12
// */
// t.is(result.split('\n').length, 3)
// })

View File

@@ -0,0 +1,4 @@
{
"private": true,
"main": "index.js"
}

View File

@@ -0,0 +1,33 @@
## API
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
#### Table of Contents
- [class: Plugin](#class-plugin)
- [sendMessageHandler()](#sendmessagehandler)
- [connectHandler()](#connecthandler)
### class: [Plugin](https://github.com/berstend/puppeteer-extra/blob/e6133619b051febed630ada35241664eba59b9fa/packages/puppeteer-extra-plugin-stealth/evasions/chrome.runtime/index.js#L13-L251)
- `opts` (optional, default `{}`)
**Extends: PuppeteerExtraPlugin**
Mock the `chrome.runtime` object if not available (e.g. when running headless) and on a secure site.
---
### [sendMessageHandler()](https://github.com/berstend/puppeteer-extra/blob/e6133619b051febed630ada35241664eba59b9fa/packages/puppeteer-extra-plugin-stealth/evasions/chrome.runtime/index.js#L80-L123)
Mock `chrome.runtime.sendMessage`
---
### [connectHandler()](https://github.com/berstend/puppeteer-extra/blob/e6133619b051febed630ada35241664eba59b9fa/packages/puppeteer-extra-plugin-stealth/evasions/chrome.runtime/index.js#L136-L210)
Mock `chrome.runtime.connect`
- **See: <https://developer.chrome.com/apps/runtime#method-connect>**
---

View File

@@ -0,0 +1,41 @@
{
"OnInstalledReason": {
"CHROME_UPDATE": "chrome_update",
"INSTALL": "install",
"SHARED_MODULE_UPDATE": "shared_module_update",
"UPDATE": "update"
},
"OnRestartRequiredReason": {
"APP_UPDATE": "app_update",
"OS_UPDATE": "os_update",
"PERIODIC": "periodic"
},
"PlatformArch": {
"ARM": "arm",
"ARM64": "arm64",
"MIPS": "mips",
"MIPS64": "mips64",
"X86_32": "x86-32",
"X86_64": "x86-64"
},
"PlatformNaclArch": {
"ARM": "arm",
"MIPS": "mips",
"MIPS64": "mips64",
"X86_32": "x86-32",
"X86_64": "x86-64"
},
"PlatformOs": {
"ANDROID": "android",
"CROS": "cros",
"LINUX": "linux",
"MAC": "mac",
"OPENBSD": "openbsd",
"WIN": "win"
},
"RequestUpdateCheckStatus": {
"NO_UPDATE": "no_update",
"THROTTLED": "throttled",
"UPDATE_AVAILABLE": "update_available"
}
}