thin-hook
v0.4.0-alpha.61
Published
Thin Hook Preprocessor
Downloads
84
Readme
thin-hook
Thin Hook Preprocessor (experimental)
Notes
- [Vulnerability Fix] Since 0.4.0-alpha.45 with Fix #398 Unchain policy objects to
Object.prototype
, access policy objects are immune to contaminatedObject.prototype
properties whentargetConfig.policy.unchainAcl = true
is configured (which is disabled by default for compatibility). Prior to this version, writableObject.prototype
properties can contaminate access policy objects. - [Vulnerability Fix] Since 0.4.0-alpha.45 with Fix #399 Recognize acl for
Object.prototype
object properly,acl.Object[S_PROTOTYPE]
policies forObject.prototype
properties are properly applied whentargetConfig.policy.unchainAcl = true
is configured (which is disabled by default for compatibility). Prior to this version,acl.Object
policies are incorrectly applied forObject.prototype
properties in some cases. - [Enhancement] Since 0.4.0-alpha.28 with Issue #376 Support ES modules, ACL policies are applied to ES modules by hooking ES module objects. This feature is optional and can be disabled by
hook.parameters.importMapper = null
indemo/bootstrap.js
- [Vulnerability Fix] Since 0.4.0-alpha.25 with Fix #369 Block DOM intrusion by Browser Extensions, the application hangs up with an alert message on DOM intrusion by Browser Extensions. Prior to this version, Browser Extensions can intrude into DOM and manipulate contents.
- [Vulnerability Fix] Since 0.4.0-alpha.24 with Fix #368 Check Service Worker cache integrity, integrity of Service Worker cache contents is verified with HMAC keys. Prior to this version, corrupted Service Worker cache contents can intrude into the application.
- [Vulnerability Fix] Since 0.4.0-alpha.22 with Fix #363 Block blob URLs, blob URLs are blocked except for
<a download="filename" href="blob:...">Download Link</a>
. Prior to this version, documents with blob URLs bypass Service Worker. - [Vulnerability Fix] Since 0.4.0-alpha.22 with Fix #362 Option to block
<embed>
and<object>
elements, the application hangs up on<embed>
and<object>
activities withhook.parameters.hangUpOnEmbedAndObjectElement = true
. Prior to this version,<embed>
and<object>
documents can bypass Service Worker with Chrome Canary 86. - [Vulnerability Fix] Since 0.4.0-alpha.21 with Fix #355 Treat proxy objects as alias objects in ACL, ACL is properly applied for proxy objects created via
new Proxy(target, handler)
andProxy.revocable(target, handler)
as with their originaltarget
objects. Prior to this version, ACL for thetarget
objects are not applied to proxy objects. - [Vulnerability Fix] Since 0.4.0-alpha.20 with Fix #350 Append the target value to hooked arguments and pick it up in
Policy.defaultAcl()
, ACL is properly applied forwith
-scoped values in function calls and constructor calls. Prior to this version, calls towith
-scoped functions can skip ACLs for their reference values. - [Vulnerability Fix] Since 0.4.0-alpha.20 with Fix #349 Hook local function calls in
with
clause, local function calls inwith
clause are property hooked. Prior to this version, local function calls inwith
clause are not hooked. This is a regression issue from the fix for Fix #339 Local variables in a with block are mistreated as global variables in ACL #339. - [Vulnerability Fix] Since 0.4.0-alpha.20 with Fix #348 Hook tagged template literals as function calls, tag functions of tagged template literals are hooked as function calls. Prior to this version, calls to tag functions of tagged template literals are not hooked.
- [Vulnerability Fix] Since 0.4.0-alpha.19 with Fix #347 Apply ACL for properties of primitive values,
acl.type[S_PROTOTYPE][S_INSTANCE]
ACLs for primitive type classesString
,Number
,Boolean
,Symbol
, andBigInt
are applied to properties of primitive values. Prior to this version, ACL is not applied to properties of primitive values. - [Vulnerability Fix] Since 0.4.0-alpha.17 with Fix #344 Normalize
with
namespace objects before bound function detection, bound function calls in awith
clause is properly detected. Prior to this version, ACL for unbound original function is not applied for bound function calls in awith
clause. - [Vulnerability Fix] Since 0.4.0-alpha.16 with Fix #342 Chain
acl[S_DEFAULT][S_PROTOTYPE][S_INSTANCE]
toacl.Object[S_PROTOTYPE][S_INSTANCE]
,acl.Object[S_PROTOTYPE][S_INSTANCE]
is applied for anonymous object properties. Prior to this version,acl.Object[S_PROTOTYPE][S_INSTANCE]
is not applied anonymous object properties. - [Vulnerability Fix] Since 0.4.0-alpha.15 with Fix #341 Apply ACL to all sources of Object.assign(), ACL is applied for all sources of
Object.assign()
even they contain falsy values. Prior to this version, ACL is not applied for sources ofObject.assign()
if the first source is not an object likeundefined
. - [Vulnerability Fix] Since 0.4.0-alpha.13 with Fix #336 Apply ACL for super classes/objects of global classes/objects with no dedicated ACLs, ACL is applied for super classes/objects of global classes/objects and instances of global classes even without dedicated ACLs. Prior to this version, ACL is not applied for super classes/objects of global classes/objects and instances of global classes without dedicated ACLs.
- [Vulnerability Fix] Since 0.4.0-alpha.9 with Fix #336 Apply ACL for super classes/objects, ACL is applied for super classes/objects of non-global classes/objects and instances of non-global classes. Prior to this version, ACL is not applied for super classes/objects of non-global classes/objects and instances of non-global classes.
- [Vulnerability Fix] Since 0.4.0-alpha.8 with Fix #334 Apply ACL for reading the target property of
receiver
inReflect.get()
, ACL is applied forreceiver
inReflect.get()
to read the target property. Prior to this version, ACL is not applied forreceiver
inReflect.get()
. - [Vulnerability Fix] Since 0.4.0-alpha.7 with Fix #333 Check constructors of Object.assign() source objects, ACL is applied for class instances as source objects in
Object.assign()
by checking their constructors. Prior to this version, ACL for class instances as source objects inObject.assign()
is not applied while ACL for global objects is applied properly. This fix is to supplement the fix for Fix #324 Apply ACL for S_TARGETED normalized properties with S_ALL normalized property. - [Vulnerability Fix] Since 0.4.0-alpha.6 with Fix #331 Check for all property access of destructured argument objects, destructured argument objects of functions are checked against all property access when called. Prior to this version, all properties of destructured argument objects of functions can be read without S_ALL access.
- [Vulnerability Fix] Since 0.4.0-alpha.5 with Fix #327 Hook RHS values of ObjectPattern and ArrayPattern for checking all property access, RHS values of ObjectPattern and ArrayPattern are hooked for checking all property access of the target values. Prior to this version, all properties of RHS values in ObjectPattern and ArrayPattern can be iterated without S_ALL access.
- [Vulnerability Fix] Since 0.4.0-alpha.4 with Fix #325 Track global objects set via defineProperty(), etc., global objects set via
Object.defineProperty()
, etc. are properly tracked for ACL. Prior to this version, ACL is skipped for global objects set viaObject.defineProperty()
, etc. - [Vulnerability Fix] Since 0.4.0-alpha.3 with Fix #324 Apply ACL for S_TARGETED normalized properties with S_ALL normalized property, source objects in
Object.assign()
are checked against ACL for reading all properties. Prior to this version, ACL is skipped for source objects inObject.assign()
and any enumerable properties in the source objects can be assigned to the target object. - [Vulnerability Fix] Since 0.4.0-alpha.2 with Fix #310 Hang up with an infinite loop if a hacked Service Worker tries to replace the entry page, the entry page hangs up if a hacked Service Worker tries to navigate the entry page. Prior to this version, a hacked Service Worker from an MITM attacker can replace the entry page with an arbitrary URL.
- [Vulnerability Fix] Since 0.3.7 with Fix #319 Check HTML/SVG extensions and content-type case-insensitively, extensions and content-type for HTML and SVG are checked case-insensitively. Prior to this version, extensions and content-type for HTML and SVG are check case-sensitively and some contents with capital letters such as *.HTML with content-type TEXT/HTML are not detected as HTML and can bypass hooking.
- [Vulnerability Fix] Since 0.3.6 with Fix #318 Check Worker extensions, extensions for Worker URLs are checked against .m?js. Prior to this version, extensions for Worker URLs are not checked and workers with irregular extensions can bypass hooking.
- [Vulnerability Fix] Since 0.3.5 with Fix #316 Redirect top SVG to about:blank, top SVG document is redirected to about:blank. Prior to this version, top SVG document can invalidate disable-devtools.js and DevTools is unexpectedly enabled.
- [Vulnerability Fix] Since 0.3.4 with Fix #314 Check content-type for HTML and SVG as well as extensions, content-type is checked for HTML/SVG detection as well as extensions. Prior to this version, HTML/SVG responses with irregular extensions are not detected as HTML/SVG and thus not hooked.
- [Vulnerability Fix] Since 0.3.3 with Fix #313 GET errorReport.json with 307 about:blank response, 307 redirect to
about:blank
is responded for GET errorReport.json request. Prior to this version, 404 Not Found is responded for GET errorReport.json, whose HTML contents in iframe can be accessed bypassing access policies. - [Feature Enhancements] Since 0.3.0 with Fix #284 Additional Content-Types in cache-bundle.json, extended metadata are supported in
cache-bundle.json
to add additional cacheable content types. This README document is updated to describe the new features and their configurations. - [Feature Enhancements] Since 0.2.0 with Fix #266 Block access via automation like puppeteer, there are many significant changes on global object access and hooking. ACL is basically compatible with prior versions but extra configurations for new features are required. This README document is updated to describe the new features and their configurations.
- [Vulnerability Fix] Since 0.1.11 with Fix #265 Attach context to wrapper property name for global object access, the correct contexts are used for global object access in self-assignment. Prior to this version, the context for the RHS value in self-assignment is incorrectly used for the access to the object.
- [Vulnerability Fix] Since 0.1.9 with Fix #263 Use the current context for global object access, the correct contexts are used for global object read/write/call access. Prior to this version, the context for the first access to the target global object is incorrectly used for the following access to the object.
- [Configuration] Since 0.0.250 with Fix #252 Block direct access to source codes and Fix #254 Block direct access to source codes even after the app shutdown, direct access to source codes are blocked.
hook.parameters.appPathRoot = '/';
indemo/disable-devtools.js
can be configured to set the root of the application assets. Prior to this version, direct access to source codes are allowed. - [Vulnerability Fix] Since 0.0.243 with Fix #250 Hook scripts in SVG and block data:/blob: URLs for SVG, scripts in SVG are hooked and
blob:
anddata:
URLs are blocked for SVG. Prior to this version, scripts in SVG are not hoooked andblob:
anddata:
URLs are allowed for SVG.<object data="inline-script.svg"></object>
,<embed src="inline-script.svg">
,<iframe src="inline-script.svg"></iframe>
- [Vulnerability Fix] Since 0.0.239 with Fix #249 Block blob: URLs for Worker,
blob:
anddata:
URLs are blocked forWorker
andSharedWorker
. Prior to this version,blob:
anddata:
URLs are allowed forWorker
andSharedWorker
. - [Vulnerability Fix] Since 0.0.236 with Fix #247 Hook script.text property, script.text property is properly hooked. Prior to this version, script.text property is not hooked.
- [Vulnerability Fix] Since 0.0.236 with Fix #246 Handle non-http protocols in iframe.src, script.src properly, non-http protocols in iframe src and script src are handled properly. Prior to this version, non-http protocols in iframe src and script src are not handled properly.
- [Vulnerability Fix] Since 0.0.235 with Fix #245 no-hook-authorization parameter is missing in sub documents, unauthorized no hook scripts are blocked in sub documents. Prior to this version, unauthorized no hook scripts in sub documents are not blocked.
- [Vulnerability Fix] Since 0.0.233 with Fix #242 Hook iframe.srcdoc,
iframe.srcdoc
is hooked asonload
attribute. Prior to this version,iframe.srcdoc
is not hooked. - [Vulnerability Fix] Since 0.0.232 with Fix #241 AsyncFunction() is not hooked,
AsyncFunction('script')
is properly hooked. Prior to this version,AsyncFunction('script')
is not hooked.AsyncFunction = (async function() {}).constructor
- [Vulnerability Fix] Since 0.0.231 with Fix #240 object.Function() is not hooked,
object.Function('script')
is properly hooked. Prior to this version,object.Function('script')
is not hooked. - [Vulnerability Fix] Since 0.0.230 with Fix #239 Full ACLs for iframe.contentWindow, full ACLs for iframe.contentWindow are properly applied. Prior to this version, only partial ACLs for iframe.contentWindow are applied.
- [Vulnerability Fix] Since 0.0.229 with Fix #238 No ACLs for iframe.contentWindow, global object ACLs for iframe.contentWindow are properly applied. Prior to this version, global object ACLs for iframe.contentWindow are not applied.
- [Vulnerability Fix] Since 0.0.228 with Fix #234 Global ACLs are not applied in web workers, ACLs for global objects in web workers are properly applied. Prior to this version, ACLs for global objects in web workers are not applied.
- [Performance Optimization]
__hook__acl
indemo/hook-callback.js
should be used as it is much faster than__hook__
as described in Fix #230. Modification:Object.defineProperty(_global, '__hook__', { configurable: false, enumerable: false, writable: false, value: hookCallbacks.__hook__acl });
- [ACL Compatibility] Since 0.0.225 with Fix #229 Exclude Multiple ACLs for global object properties, ACLs for the global object properties (
top
,parent
,frames
,global
,_global
, etc.) other than the main global object property (window
in the main document,self
in workers) are applied only for access likewindow.top
. In 0.0.224, all the ACLs for the global object properties are applied for every global object access, which is redundant. - [Vulnerability Fix] Since 0.0.225 with Fix #227 Private API registered in strict mode, ACLs for private APIs registered to the global object in strict mode are properly applied. Prior to this version, ACLs for private APIs registered to the global object in strict mode are not applied.
- [ACL Compatibility] Since 0.0.225 with Fix #226 Multiple ACLs,
_globalObjects
is aSetMap
object defined inhook-callback.js
and_globalObjects.get(obj)
return aSet
object containingstring
s. All the ACLs for the set ofstring
s are applied for the object. Prior to this version,_globalObjects
is aMap
object and_globalObjects.get(obj)
returns astring
. - [ACL Compatibility] Since 0.0.225 with Fix #226 Multiple ACLs,
_blacklistObjects
is deprecated. - [ACL Compatibility] Since 0.0.216 with Fix #217,
delete
operations require'W'
permission as they can delete properties with customized descriptors. Prior to this version,delete
operations require'w'
permission. - [ACL Compatibility] Since 0.0.214 with Fix #215,
'R'
and'W'
opTypes are introduced for getting/setting property descriptors, i.e., contexts to access descriptors must have explicit'R'
and/or'W'
permissions for the target properties. Prior to 0.0.213, property descriptors can be accessed by mere'r'
and/or'w'
permissions. - [Vulnerability Fix] Since 0.0.211 with Fix #211, bypassing of ACL for global objects by dummy custom element definition is avoided. Prior to this version, ACL can be skipped by defining dummy custom elements by standard elements as constructor classes.
- [Vulnerability Fix] Since 0.0.209 with Fix #210, bypassing of ACL for global objects by cloing them to other global objects is avoided. Prior to this version, ACL can be skipped by cloing global objects.
- [Vulnerability Fix] Since 0.0.205 with Fix #208, scripts via
document.writeln()
are hooked as indocument.write()
. Prior to this version, scripts viadocument.writeln()
are not hooked. - [Vulnerability Fix] Since 0.0.203 with Fix #207,
textContent
ofscript
elements are always treated as JavaScript scripts regardless of their configured MIME types (type
property/attribute). Prior to this version,textContent
ofscript
elements containing__hook__
as strings can be mistaken as HOOKED scripts and run without hooking. - [Context Generator Compatibility] Since 0.0.148 with #144, the old context generator
"method"
is renamed as"oldMethod"
and the"cachedMethod"
is renamed as"method"
and become the new default context generator. The"cachedMethod"
remains as an alias for the new"method"
context generator. There are slight changes in the new"method"
context generator. A warning message is shown on the debug console to notify the change.
| old name | new name | feature |
|:-----:|:-----:|:-----|
| method
| oldMethod
| script.js,Class,method
|
| cachedMethod
| method
| script.js,Class,method
including computed property names |
- [Hook Callback Compatibility] Since 0.0.149 with #123, the hook callback function has to support new operators for hooking in strict mode. See below for the updated hook callback function
hook.__hook__
.hook.hookCallbackCompatibilityTest()
can detect if the target hook callback function is compatible or not. - [Opaque URL Authorization] Since 0.0.178 with #178, all opaque content URLs must be authorized via
hook.parameters.opaque = [ 'opaque_url', ..., (url) => url.match(/opaque_url_pattern/), ... ]
configuration.
Native API Access Graph generated via hook callback function (view2 of thin-hook/demo/)
Demo on GitHub Pages
Input
class C {
add(a = 1, b = 2) {
let plus = (x, y) => x + y;
return plus(a, b);
}
}
Hooked Output
const __context_mapper__ = $hook$.$(__hook__, [
'examples/example2.js,C',
'_p_C;examples/example2.js,C',
'examples/example2.js,C,add',
'examples/example2.js,C,add,plus'
]);
$hook$.global(__hook__, __context_mapper__[0], 'C', 'class')[__context_mapper__[1]] = class C {
add(a, b) {
return __hook__((a = 1, b = 2) => {
let plus = (...args) => __hook__((x, y) => x + y, null, args, __context_mapper__[3]);
return __hook__(plus, null, [
a,
b
], __context_mapper__[2], 0);
}, null, arguments, __context_mapper__[2]);
}
};
Preprocess
const hook = require('thin-hook/hook.js');
let code = fs.readFileSync('src/target.js', 'UTF-8');
let initialContext = [['src/target.js', {}]];
let gen = hook(code, '__hook__', initialContext, 'hash');
fs.writeFileSync('hooked/target.js', gen);
fs.writeFileSync('hooked/target.js.contexts.json', JSON.stringify(contexts, null, 2));
Context Generator Function (customizable)
// Built-in Context Generator Function
hook.contextGenerators.method = function generateMethodContext(astPath) {
return astPath.map(([ path, node ], index) => node && node.type
? (node.id && node.id.name ? node.id.name : (node.key && node.key.name
? (node.kind === 'get' || node.kind === 'set' ? node.kind + ' ' : node.static ? 'static ' : '') + node.key.name : ''))
: index === 0 ? path : '').filter(p => p).join(',');
}
// Example Custom Context Generator Function with Hashing
const hashSalt = '__hash_salt__';
let contexts = {};
hook.contextGenerators.hash = function generateHashContext(astPath) {
const hash = hook.utils.createHash('sha256');
let hashedInitialContext = astPath[0][0];
astPath[0][0] = contexts[hashedInitialContext] || astPath[0][0];
let methodContext = hook.contextGenerators.method(astPath);
astPath[0][0] = hashedInitialContext;
hash.update(hashSalt + methodContext);
let hashContext = hash.digest('hex');
contexts[hashContext] = methodContext;
return hashContext;
}
{
// Authorization Tickets for no-hook scripts
// Ticket for this script itself is specified in URL of script tag as
// hook.min.js?no-hook-authorization={ticket}
// Note: no-hook-authorization must not exist in learning mode
let noHookAuthorization = {
// '*' is for learning mode to detect authorization tickets in
// hook.parameters.noHookAuthorizationPassed,
// hook.parameters.noHookAuthorizationFailed
// JSONs are output to console in the learning mode
//'*': true,
"35ae97a3305b863af7eb0ac75c8679233a2a7550e4c3046507fc9ea182c03615": true,
"16afd3d5aa90cbd026eabcc4f09b1e4207a7042bc1e9be3b36d94415513683ed": true,
"ae11a06c0ddec9f5b75de82a40745d6d1f92aea1459e8680171c405a5497d1c8": true,
"5b7ebf7b0b2977d44f47ffa4b19907abbc443feb31c343a6cbbbb033c8deb01a": true,
"c714633723320be54f106de0c50933c0aeda8ac3fba7c41c97a815ed0e71594c": true,
"2f43d927664bdfcbcb2cc4e3743652c7eb070057efe7eaf43910426c6eae7e45": true,
"b397e7c81cca74075d2934070cbbe58f345d3c00ff0bc04dc30b5c67715a572f": true,
"02c107ea633ed697acc12e1b3de1bcf2f0ef7cafe4f048e29553c224656ecd7a": true,
"aebb23ce36eb6f7d597d37727b4e6ee5a57aafc564af2d65309a9597bfd86625": true
};
let hidden;
const passcode = 'XX02c107ea633ed697acc12e1b3de1bcf2f0ef7cafe4f048e29553c224656ecd7a';
if (typeof self === 'object' && self.constructor.name === 'ServiceWorkerGlobalScope') {
// Service Worker
let reconfigure = false;
if (hook.parameters.noHookAuthorization) {
if (Object.getOwnPropertyDescriptor(hook.parameters, 'noHookAuthorization').configurable) {
reconfigure = true;
}
}
else {
reconfigure = true;
}
if (reconfigure) {
Object.defineProperty(hook.parameters, 'noHookAuthorization', {
configurable: false,
enumerable: true,
get() {
return hidden;
},
set(value) {
if (value && value.passcode === passcode) {
delete value.passcode;
Object.freeze(value);
hidden = value;
}
}
});
}
noHookAuthorization.passcode = passcode;
hook.parameters.noHookAuthorization = noHookAuthorization;
}
else {
// Browser Document
Object.defineProperty(hook.parameters, 'noHookAuthorization', {
configurable: false,
enumerable: true,
writable: false,
value: Object.freeze(noHookAuthorization)
});
}
if (!noHookAuthorization['*']) {
Object.seal(hook.parameters.noHookAuthorizationPassed);
}
}
{
// source map target filters
hook.parameters.sourceMap = [
url => location.origin === url.origin && url.pathname.match(/^\/components\/thin-hook\/demo\//)
];
// hook worker script URL
hook.parameters.hookWorker = `hook-worker.js?no-hook=true`;
}
// Hook worker script (demo/hook-worker.js)
//
// Configuration:
// hook.parameters.hookWorker = `hook-worker.js?no-hook=true`;
importScripts('../hook.min.js?no-hook=true', 'context-generator.js?no-hook=true', 'bootstrap.js?no-hook=true');
onmessage = hook.hookWorkerHandler;
<!-- Example Custom Context Generator for Service Worker and Browser Document -->
<script src="bower_components/thin-hook/hook.min.js?version=1&no-hook=true&hook-name=__hook__&context-generator-name=method2&fallback-page=index-fb.html&service-worker-ready=true"></script>
<script context-generator src="custom-context-generator.js?no-hook=true"></script>
<script context-generator no-hook>
{
hook.contextGenerators.method2 = function generateMethodContext2(astPath) {
return astPath.map(([ path, node ], index) => node && node.type
? (node.id && node.id.name ? node.id.name : (node.key && node.key.name
? (node.kind === 'get' || node.kind === 'set' ? node.kind + ' ' : node.static ? 'static ' : '') + node.key.name : ''))
: index === 0 ? path : '').filter(p => p).join(',') +
(astPath[astPath.length - 1][1].range ? ':' + astPath[astPath.length - 1][1].range[0] + '-' + astPath[astPath.length - 1][1].range[1] : '');
}
Object.freeze(hook.contextGenerators);
// CORS script list
hook.parameters.cors = [
'https://raw.githubusercontent.com/t2ym/thin-hook/master/examples/example1.js',
(url) => { let _url = new URL(url); return _url.hostname !== location.hostname && ['www.gstatic.com'].indexOf(_url.hostname) < 0; }
];
// Authorized opaque URL list
hook.parameters.opaque = [
'https://www.gstatic.com/charts/loader.js',
(url) => {
let _url = new URL(url);
return _url.hostname !== location.hostname &&
_url.href.match(/^(https:\/\/www.gstatic.com|https:\/\/apis.google.com\/js\/api.js|https:\/\/apis.google.com\/_\/)/);
}
];
}
</script>
Hook Callback Function (customizable)
// Built-in Minimal Hook Callback Function without hooking properties (hook-property=false)
hook.__hook_except_properties__ = function __hook_except_properties__(f, thisArg, args, context, newTarget) {
return newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
// the global object
const _global = (new Function('return this'))();
// helper for strict mode
class StrictModeWrapper {
static ['#.'](o, p) { return o[p]; }
static ['#[]'](o, p) { return o[p]; }
static ['#*'](o) { return o; }
static ['#in'](o, p) { return p in o; }
static ['#()'](o, p, a) { return o[p](...a); }
static ['#p++'](o, p) { return o[p]++; }
static ['#++p'](o, p) { return ++o[p]; }
static ['#p--'](o, p) { return o[p]--; }
static ['#--p'](o, p) { return --o[p]; }
static ['#delete'](o, p) { return delete o[p]; }
static ['#='](o, p, v) { return o[p] = v; }
static ['#+='](o, p, v) { return o[p] += v; }
static ['#-='](o, p, v) { return o[p] -= v; }
static ['#*='](o, p, v) { return o[p] *= v; }
static ['#/='](o, p, v) { return o[p] /= v; }
static ['#%='](o, p, v) { return o[p] %= v; }
static ['#**='](o, p, v) { return o[p] **= v; }
static ['#<<='](o, p, v) { return o[p] <<= v; }
static ['#>>='](o, p, v) { return o[p] >>= v; }
static ['#>>>='](o, p, v) { return o[p] >>>= v; }
static ['#&='](o, p, v) { return o[p] &= v; }
static ['#^='](o, p, v) { return o[p] ^= v; }
static ['#|='](o, p, v) { return o[p] |= v; }
static ['#.='](o, p) { return { set ['='](v) { o[p] = v; }, get ['=']() { return o[p]; } }; }
}
// Built-in Minimal Hook Callback Function with hooking properties (hook-property=true) - default
function __hook__(f, thisArg, args, context, newTarget) {
let normalizedThisArg = thisArg;
if (newTarget === false) { // resolve the scope in 'with' statement body
let varName = args[0];
let __with__ = thisArg;
let scope = _global;
let _scope;
let i;
for (i = 0; i < __with__.length; i++) {
_scope = __with__[i];
if (Reflect.has(_scope, varName)) {
if (_scope[Symbol.unscopables] && _scope[Symbol.unscopables][varName]) {
continue;
}
else {
scope = _scope;
break;
}
}
}
thisArg = normalizedThisArg = scope;
}
let result;
let args1 = args[1]; // for '()'
function * gen() {}
let GeneratorFunction = gen.constructor;
switch (f) {
case Function:
args = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args);
break;
case GeneratorFunction:
args = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args, true);
break;
case '()':
case '#()':
switch (thisArg) {
case Reflect:
switch (args[0]) {
case 'construct':
if (args[1]) {
switch (args[1][0]) {
case Function:
args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1])];
if (args[1][2]) {
args1.push(args[1][2]);
}
break;
default:
if (args[1][0].prototype instanceof Function) {
args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1], args[1][0].prototype instanceof GeneratorFunction)];
if (args[1][2]) {
args1.push(args[1][2]);
}
}
break;
}
}
break;
case 'apply':
if (args[1]) {
switch (args[1][0]) {
case Function:
args1 = [args[1][0], args[1][1], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][2])];
break;
case GeneratorFunction:
args1 = [args[1][0], args[1][1], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][2], true)];
break;
default:
if (args[1][0].prototype instanceof Function) {
args1 = [args[1][0], args[1][1], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][2], args[1][0].prototype instanceof GeneratorFunction)];
}
break;
}
}
break;
default:
break;
}
break;
case Function:
switch (args[0]) {
case 'apply':
args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1])];
break;
case 'call':
args1 = [args[1][0], ...hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1].slice(1))];
break;
default:
break;
}
break;
case GeneratorFunction:
switch (args[0]) {
case 'apply':
args1 = [args[1][0], hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1][1], true)];
break;
case 'call':
args1 = [args[1][0], ...hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1].slice(1), true)];
break;
default:
break;
}
break;
default:
if (thisArg instanceof GeneratorFunction && args[0] === 'constructor') {
args1 = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1], true);
}
else if (thisArg instanceof Function && args[0] === 'constructor') {
args1 = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args[1]);
}
break;
}
break;
default:
if (typeof f === 'function') {
if (f.prototype instanceof Function && newTarget) {
args = hook.FunctionArguments('__hook__', [[context, {}]], 'method', args, f.prototype instanceof GeneratorFunction);
}
else if (newTarget === '') {
if (args[0] && Object.getPrototypeOf(args[0]) === Function) {
args = [ args[0], ...hook.FunctionArguments('__hook__', [[context, {}]], 'method', args.slice(1)) ];
}
}
}
break;
}
if (typeof f !== 'string') {
result = newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
else {
// property access
switch (f) {
// getter
case '.':
case '[]':
result = thisArg[args[0]];
break;
// enumeration
case '*':
result = thisArg;
break;
// property existence
case 'in':
result = args[0] in thisArg;
break;
// funcation call
case '()':
result = thisArg[args[0]](...args1);
break;
// unary operators
case 'p++':
result = thisArg[args[0]]++;
break;
case '++p':
result = ++thisArg[args[0]];
break;
case 'p--':
result = thisArg[args[0]]--;
break;
case '--p':
result = --thisArg[args[0]];
break;
case 'delete':
result = delete thisArg[args[0]];
break;
// assignment operators
case '=':
result = thisArg[args[0]] = args[1];
break;
case '+=':
result = thisArg[args[0]] += args[1];
break;
case '-=':
result = thisArg[args[0]] -= args[1];
break;
case '*=':
result = thisArg[args[0]] *= args[1];
break;
case '/=':
result = thisArg[args[0]] /= args[1];
break;
case '%=':
result = thisArg[args[0]] %= args[1];
break;
case '**=':
result = thisArg[args[0]] **= args[1];
break;
case '<<=':
result = thisArg[args[0]] <<= args[1];
break;
case '>>=':
result = thisArg[args[0]] >>= args[1];
break;
case '>>>=':
result = thisArg[args[0]] >>>= args[1];
break;
case '&=':
result = thisArg[args[0]] &= args[1];
break;
case '^=':
result = thisArg[args[0]] ^= args[1];
break;
case '|=':
result = thisArg[args[0]] |= args[1];
break;
// LHS property access
case '.=':
result = { set ['='](v) { thisArg[args[0]] = v; }, get ['=']() { return thisArg[args[0]]; } };
break;
// strict mode operators prefixed with '#'
// getter
case '#.':
case '#[]':
result = StrictModeWrapper['#.'](thisArg, args[0]);
break;
// enumeration
case '#*':
result = StrictModeWrapper['#*'](thisArg);
break;
// property existence
case '#in':
result = StrictModeWrapper['#in'](thisArg, args[0]);
break;
// funcation call
case '#()':
result = StrictModeWrapper['#()'](thisArg, args[0], args1);
break;
// unary operators
case '#p++':
result = StrictModeWrapper['#p++'](thisArg, args[0]);
break;
case '#++p':
result = StrictModeWrapper['#++p'](thisArg, args[0]);
break;
case '#p--':
result = StrictModeWrapper['#p--'](thisArg, args[0]);
break;
case '#--p':
result = StrictModeWrapper['#--p'](thisArg, args[0]);
break;
case '#delete':
result = StrictModeWrapper['#delete'](thisArg, args[0]);
break;
// assignment operators
case '#=':
result = StrictModeWrapper['#='](thisArg, args[0], args[1]);
break;
case '#+=':
result = StrictModeWrapper['#+='](thisArg, args[0], args[1]);
break;
case '#-=':
result = StrictModeWrapper['#-='](thisArg, args[0], args[1]);
break;
case '#*=':
result = StrictModeWrapper['#*='](thisArg, args[0], args[1]);
break;
case '#/=':
result = StrictModeWrapper['#/='](thisArg, args[0], args[1]);
break;
case '#%=':
result = StrictModeWrapper['#%='](thisArg, args[0], args[1]);
break;
case '#**=':
result = StrictModeWrapper['#**='](thisArg, args[0], args[1]);
break;
case '#<<=':
result = StrictModeWrapper['#<<='](thisArg, args[0], args[1]);
break;
case '#>>=':
result = StrictModeWrapper['#>>='](thisArg, args[0], args[1]);
break;
case '#>>>=':
result = StrictModeWrapper['#>>>='](thisArg, args[0], args[1]);
break;
case '#&=':
result = StrictModeWrapper['#&='](thisArg, args[0], args[1]);
break;
case '#^=':
result = StrictModeWrapper['#^='](thisArg, args[0], args[1]);
break;
case '#|=':
result = StrictModeWrapper['#|='](thisArg, args[0], args[1]);
break;
// LHS property access
case '#.=':
result = StrictModeWrapper['#.='](thisArg, args[0]);
break;
// getter for super
case 's.':
case 's[]':
result = args[1](args[0]);
break;
// super method call
case 's()':
result = args[2](args[0]).apply(thisArg, args[1]);
break;
// unary operators for super
case 's++':
case '++s':
case 's--':
case '--s':
result = args[1].apply(thisArg, args);
break;
// assignment operators for super
case 's=':
case 's+=':
case 's-=':
case 's*=':
case 's/=':
case 's%=':
case 's**=':
case 's<<=':
case 's>>=':
case 's>>>=':
case 's&=':
case 's^=':
case 's|=':
result = args[2].apply(thisArg, args);
break;
// getter in 'with' statement body
case 'w.':
case 'w[]':
result = args[1]();
break;
// function call in 'with' statement body
case 'w()':
result = args[2](...args[1]);
break;
// constructor call in 'with' statement body
case 'wnew':
result = args[2](...args[1]);
break;
// unary operators in 'with' statement body
case 'w++':
case '++w':
case 'w--':
case '--w':
result = args[1]();
break;
// unary operators in 'with' statement body
case 'wtypeof':
case 'wdelete':
result = args[1]();
break;
// LHS value in 'with' statement body (__hook__('w.=', __with__, ['p', { set ['='](v) { p = v } } ], 'context', false)['='])
case 'w.=':
result = args[1];
break;
// assignment operators in 'with' statement body
case 'w=':
case 'w+=':
case 'w-=':
case 'w*=':
case 'w/=':
case 'w%=':
case 'w**=':
case 'w<<=':
case 'w>>=':
case 'w>>>=':
case 'w&=':
case 'w^=':
case 'w|=':
result = args[2](args[1]);
break;
// default (invalid operator)
default:
f(); // throw TypeError: f is not a function
result = null;
break;
}
}
return result;
}
// Example Hook Callback Function with Primitive Access Control
hashContext = { 'hash': 'context', ... }; // Generated from hook.preprocess initialContext[0][1]
trustedContext = { 'context': /trustedModules/, ... }; // Access Policies
window.__hook__ = function __hook__(f, thisArg, args, context, newTarget) {
console.log('hook:', context, args);
if (!hashContext[context] ||
!trustedContext[hashContext[context]] ||
!(new Error('').stack.match(trustedContext[hashContext[context]]))) {
// plus check thisArg, args, etc.
throw new Error('Permission Denied');
}
return newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
Entry HTML with Service Worker
If hooking is performed run-time in Service Worker, the entry HTML page must be loaded via Service Worker so that no hook-targeted scripts are evaluated without hooking.
To achieve this, the static entry HTML has to be Encoded at build time by hook.serviceWorkerTransformers.encodeHTML(html)
.
Hook CLI to encode the entry HTML
# encode src/index.html to dist/index.html
hook --out dist/index.html src/index.html
Decoded/Original HTML (source code)
<html>
<head>
<script src="../thin-hook/hook.min.js?version=1&no-hook=true&hook-name=__hook__&fallback-page=index-no-sw.html&hook-property=false&service-worker-ready=true"></script>
<!-- Hook Callback Function witout hooking properties -->
<script no-hook>
window.__hook__ = function __hook__(f, thisArg, args, context, newTarget) {
...
return newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
</script><!-- end of mandatory no-hook scripts -->
<!-- comment --->
<script src="..."></script>
...
</html>
Encoded HTML (Service Worker converts it to Decoded HTML)
<html>
<head>
<script src="../thin-hook/hook.min.js?version=1&no-hook=true&hook-name=__hook__&fallback-page=index-no-sw.html&hook-property=false&service-worker-ready=false"></script></head></html>
<!-- Hook Callback Function without hooking properties -->
<script no-hook>
window.__hook__ = function __hook__(f, thisArg, args, context, newTarget) {
...
return newTarget
? Reflect.construct(f, args)
: thisArg
? f.apply(thisArg, args)
: f(...args);
}
</script><!--<C!-- end of mandatory no-hook scripts --C>
<C!-- comment --C>
<script src="..."></script>
...
</html>-->
</head></html>
is inserted between the firsthook.min.js
script and the second no-hook script, which looks strange but is required for correct execution of no-hook scripts.- If
</head></html>
is inserted at the end of mandatory no-hook scripts according to the normal HTML format, the page encounters the unexpected "hook is not defined" error, whose root cause is under investigation.
- If
Supported Syntax
- Functions
- Object Shorthand Methods (
{ m() {} }
) - ES6 Classes (
constructor
,super
,this
,new
) - ES6 Modules (
import
,export
); - Expressions in Template Literals(
`${(v => v * v)(x)}`
) - Generator Functions (
function *g() { yield X }
) - Arrow Functions (
a => a
,a => { return a; }
,a => ({ p: a })
) - Async Functions (
async function f() {}
,async method() {}
,async () => {}
) - Default Parameters for Functions/Methods/Arrow Functions
- Default Parameters with Destructuring (
function f([ a = 1 ], { b = 2, x: c = 3 }) {}
) - Property Accessors (
o.p
,o['p']
,o.p()
)
Install
Browsers
bower install --save thin-hook
NodeJS
npm install --save thin-hook
Import
Browsers
<!-- browserified along with espree and escodegen; minified -->
<script src="path/to/bower_components/thin-hook/hook.min.js"></script>
NodeJS
const hook = require('thin-hook/hook.js');
API (Tentative)
hook(code: string, hookName: string = '__hook__', initialContext: Array = [], contextGeneratorName: string = 'method', metaHooking: boolean = true, hookProperty: boolean = true, sourceMap: object = null, asynchronous: boolean = false, compact: boolean = false, hookGlobal: boolean = true, hookPrefix: string = '_p_', initialScope: object = null)
code
: input JavaScript as stringhookName
: name of hook callback functioninitialContext
: typically[ ['script.js', {}] ]
contextGeneratorName
: function property name inhook.contextGenerators
- argument
astPath = [ ['script.js', {}], ['root', rootAst], ['body', bodyAst], ..., [0, FunctionExpressionAst] ]
- argument
metaHooking
: Enable meta hooking (run-time hooking of metaprogramming) if truehookProperty
: Enable hooking of object property accessors and new operators if truesourceMap
: Source map parameter in an object.{ pathname: 'path/to/script_source.js'}
Default: nullasynchronous
: Return a Promise if true. Default: falsecompact
: Generate compact code if true. Default: false- Note:
sourceMap
is disabled whencompact
is true
- Note:
hookGlobal
: Hook global variable access. Must be enabled withhookProperty
. Default: truehookPrefix
: Prefix forhook.global()._p_GlobalVariable
proxy accessors. Default:_p_
- Note:
hook.global()
return the global object withget/set
accessors for the prefixed name
- Note:
initialScope
: Initial scope object ({ vname: true, ... }
) for hooked eval scripts. Default: null
$hook$
:$hook$ === hook
. Alias ofhook
in hooked scriptshook.hookHtml(html: string, hookName, url, cors, contextGenerator, contextGeneratorScripts, isDecoded, metaHooking = true, scriptOffset = 0, _hookProperty = true, asynchronous = false)
hook.__hook__(f: function or string, thisArg: object, args: Array, context: string, newTarget: new.target meta property)
- minimal hook callback function with property hooking
f
:function
: target function to hookstring
: property operation to hook.
: get property (o.prop
)*
: iterate over (for (p in o)
,for (p of o)
)in
: property existence ('p' in o
)()
: function call (o.func()
)=
,+=
, ...: assignment operation (o.prop = value
)p++
,++p
,p--
,--p
: postfixed/prefixed increment/decrement operation (o.prop++
)delete
: delete operation (delete o.prop
)s.
: get property of super (super.prop
)s()
: call super method (super.method()
)s=
,s+=
, ...: assignment operation for super (super.prop = value
)s++
,++s
,s--
,--s
: postfixed/prefixed increment/decrement operation for super (super.prop++
)w.
,w=
,w()
,w++
, ...: operations on variables in withinwith
statements
thisArg
:this
object for the function or the operationargs
:- arguments for the function
[ property ]
for property access operations[ property, value ]
for property assignment operations[ property, [...args] ]
for function call operations
context
: context in the scriptnewTarget
:new.target
meta property for constructor calls;true
for new calls- Falsy values for non-
new
operations for faster detection of the operationsfalse
forwith
statement calls0
for function callsundefined
for other calls
hook.__hook_except_properties__(f, thisArg, args, context, newTarget)
- minimal hook callback function without property hooking
hook.hookCallbackCompatibilityTest(__hook__ = window[hookName], throwError = true, checkTypeError = true)
- run-time test suite for hook callback function
- Usage:
window.__hook__ = function __hook__ (...) {}; hook.hookCallbackCompatibilityTest();
- An error is thrown on compatibility test failure.
false
is returned on a test failure ifthrowError = false
- tests on non-callable object's function call are skipped if
checkTypeError = false
hook.contextGenerators
: object. Context Generator Functionsnull()
: context as''
astPath(astPath: Array)
: context as'script.js,[root]Program,body,astType,...'
method(astPath: Array)
: context as'script.js,Class,Method'
with caching, including computed method variable namecachedMethod(astPath: Array)
: alias formethod
cachedMethodDebug(astPath: Array)
: context as'script.js,Class,Method'
, comparing contexts with those by "oldMethod" in console.warn() messagesoldMethod(astPath: Array)
: context as'script.js,Class,Method'
for compatibility- custom context generator function has to be added to this object with its unique contextGeneratorName
hook.$(symbolToContext = __hook__, contexts)
: context symbol generator function used in hooked scripts to generate symbols corresponding to given contexts- Example call inserted at the beginning of a hooked script:
const __context_mapper__ = $hook$.$(__hook__, [ 'examples/example2.js,C', ... ]);
__context_mapper__
:Array
of symbol contexts- In a hooked script,
__context_mapper__
is actually__ + hex(sha256(topContextOfScript + code)) + __
- Note: Due to this specification, the same script in the same URL cannot be loaded to a single document multiple times
__context_mapper__[N]
: the symbol context corresponding to the string contextcontexts[N]
__hook__[__context_mapper__[N]]
is set ascontexts[N]
so that__hook__
can convert symbol contexts to their corresponding string contexts
- In a hooked script,
- Example call inserted at the beginning of a hooked script:
- Hooked Native APIs: Automatically applied in
hook()
preprocessinghook.global(hookCallback: function = hookName, context: string, name: string, type: string)._p_name
: hooked global variable accessor whenhookGlobal
is truetype
: one of'var', 'function', 'let', 'const', 'class', 'get', 'set', 'delete', 'typeof'
hook.Function(hookName, initialContext: Array = [['Function', {}]], contextGeneratorName)
: hooked Function constructor for use in hook callback function__hook__
- Usage:
(new (hook.Function('__hook__', [['window,Function', {}]], 'method'))('return function f() {}'))()
- Notes:
- Avoid replacing the native API
window.Function
for better transparency (now commented out in thedemo/hook-native-api.js
) - NOT automatically applied in the hooking
- Applied in the hook callback function (
__hook__
) instead
- Avoid replacing the native API
- Usage:
hook.FunctionArguments(hookName, initialContext: Array = [['Function', {}]], contextGeneratorName = 'method', args, isGenerator = false)
: generate hooked Function arguments to hand to Function constructor for use in hook callback function__hook__
- Usage:
hook.FunctionArguments('__hook__', [['window,Function', {}]], 'method', ['return function f() {}'])
- Returns hooked
args
in a clonedArray
- Usage:
hook.eval(hookName, initialContext: Array = [['eval', {}]], contextGeneratorName)
: hooked eval function- Usage:
hook.eval('__hook__', [['eval', {}]], 'method'))('1 + 2', (script, eval) => eval(script))
- Note: In no-hook scripts with the hooked global
eval
function viahook.hook(hook.eval(...))
, the evaluation is bound to the global scope unless the wrapper arrow function(script, eval) => eval(script)
is defined in the local scope and specifed as the second argument of eacheval()
call
- Usage:
hook.setTimeout(hookName, initialContext: Array = [['setTimeout', {}]], contextGeneratorName)
: hooked setTimeout function- Note: Not automatically applied if the first argument is an (arrow) function expression
hook.setInterval(hookName, initialContext: Array = [['setInterval', {}]], contextGeneratorName)
: hooked setInterval function- Note: Not automatically applied if the first argument is an (arrow) function expression
hook.Node(hookName, initialContext: Array = [['Node', {}]], contextGeneratorName)
: hooktextContent
propertyset textContent
: hooked with context 'ClassName,set textContent'
hook.Element(hookName, initialContext: Array = [['Element', {}]], contextGeneratorName)
: hooksetAttribute
functionsetAttribute('onXX', '{script in attribute}')
: Script in onXX handler attribute is hookedsetAttribute('href', 'javascript:{script in URL}')
: Script in URL"javascript:{script in URL}"
is hooked
hook.HTMLScriptElement(hookName, initialContext: Array = [['HTMLScriptElement', {}]], contextGeneratorName)
: HTMLScriptElement with hooked properties- Note: Applied only at run time. Not applied in preprocessing.
HTMLScriptElement
class is the same object as the native one.hook.Node
andhook.Element
are called internally. set textContent
: Script intextContent
is hooked iftype
is a JavaScript MIME type.Node.textContent
is hooked as well.- Note: Scripts set by
innerHTML
/outerHTML
/text
properties are NOT executed, whiletext
should be executed according to the standards.
- Note: Scripts set by
set type
: Script inthis.textContent
is hooked iftype
is a JavaScript MIME type.setAttribute('type', mimeType)
: Script inthis.textContent
is hooked ifmimeType
is a JavaScript MIME type.Element.setAttribute
is hooked as well.
- Note: Applied only at run time. Not applied in preprocessing.
hook.HTMLAnchorElement(hookName, initialContext: Array = [['HTMLAnchorElement', {}]]), contextGeneratorName)
: HTMLAnchorElement with hooked href propertyset href
: Script in URL"javascript:{script in URL}"
is hooked
hook.HTMLAreaElement(hookName, initialContext: Array = [['HTMLAreaElement', {}]]), contextGeneratorName)
: HTMLAreaElement with hooked href propertyset href
: Script in URL"javascript:{script in URL}"
is hooked
hook.Document(hookName, initialContext: Array = [['Document', {}]], contextGeneratorName)
: hookwrite
functionwrite('<sc' + 'ript>{script in string}</sc' + 'ript>')
: Script in HTML fragment is hooked
hook.with(scope: Object, ...scopes: Array of Object)
: Hookwith
statement scope objectwith (hook.with(obj, { v1: true, v2: true, ...})) {}
hook.importScripts()
: return hookedimportScripts
function for Workers, invalidating extensions other than.js
and.mjs
- Note: No arguments to pass
hook.hook(target: Class, ...)
: hook platform global object withtarget
- Usage:
['Function','setTimeout','setInterval',...].forEach(name => hook.hook(hook.Function('__hook__', [[name, {}]], 'method'))
- Usage:
hook.serviceWorkerHandlers
: Service Worker event handlersinstall
: 'install' event handler. Set version from theversion
parameteractivate
: 'activate' event handler. Clear caches of old versions.message
: 'message' event handler.- INTERNAL
'channel'
message: Transfer MessageChannel port objects for hook workers from the main document to the Service Worker at initialization - INTERNAL
'unload'
message: Trigger unloading of hook workers - INTERNAL
'coverage'
message: Transfer__coverage__
instanbul coverage object for the Service Worker to the main document to collect code coverage intest/hook.min.js
['plugin', 'pluginId', ...params ]
message: Transfer a message to the target plugin identified by'pluginId'
. The target plugin must add its own event listener to handle the message.['plugin', 'pluginId:enqueue', ...params ]
: When thepluginId
ends with:enqueue
, events with posted messages are enqueued tohook.parameters.messageQueues['pluginId:enqueue'] = []
even before plugins are loaded into the Service Worker- Each enqueued message is immediately responded via
event.ports[0].postMessage()
with a dummy response message generated by cloning the posted message and appending':enqueued'
such as['plugin', 'pluginId:enqueue', ...params, ':enqueued' ]
- The target plugin must dequeue the enqueued events and append
':dequeued'
to the queue to stop further enqueueing. For example, the queue[]
changes as follows:- An event is enqueued:
[ event1 ]
- The plugin append
':dequeued'
:[ event1, ':dequeued' ]
- The plugin dequeues and processes the event(s):
[ ':dequeued' ]
- An event is enqueued:
- Enqueued messages are likely to be one-way messages as the main document is about to reload itself
hook.parameters.messageQueues['pluginId:enqueue']
may NOT exist when the plugin is loaded. So the plugin must create its own queue if it has not been created.
- Each enqueued message is immediately responded via
- INTERNAL
fetch
: 'fetch' event handler. Cache hooked JavaScripts and HTMLs except for the main page loadinghook.min.js
<script src="thin-hook/hook.min.js?version=1&sw-root=/&no-hook=true&hook-name=__hook__&discard-hook-errors=true&fallback-page=index-no-sw.html&hook-property=true&service-worker-ready=true"></script>
: arguments from the pageversion
: default1
. Service Worker cache version. Old caches are flushed when the version is changed in the main page and reloaded. Service Worker is updated when the controlled page is detached after the reloading.sw-root
: optional. Set Service Worker scopehook-name
: default__hook__
. hook callback function namecontext-generator-name
: defaultmethod
. context generator callback function namediscard-hook-errors
:true
if errors in hooking are ignored and the original contents are provided. Default:true
fallback-page
: fallback page to land if Service Worker is not available in the browserno-hook-authorization
: Optional. CSV of no-hook authorization tickets for no-hook scripts. Typically for ticket of no-hook authorization script itself.- The values are stored in
hook.parameters.noHookAuthorizationPreValidated
object in Service Worker - Add the value
log-no-hook-authorization
to log authorization in console - Note:
no-hook-authorization
must not exist in learning mode withhook.parameters.noHookAuthorization['*'] === true
- Steps to update authorized no-hook scripts:
- Let no-hook be "learning mode" by truthy
hook.parameters.noHookAuthorization['*']
- Let no-hook be "learning mode" by truthy
- Remove (or temporarily rename)
no-hook-authorization
parameter from hook.min.js
- Remove (or temporarily rename)
- Update no-hook script(s)
- Clear Service Worker(s)
- Update
version
parameter for hook.min.js
- Update
- Check "Preserve Logs" option in debugger console
- Reload the page(s) with no-hook script(s)
- Copy and Paste values of hook.parameters.noHookAuthorizationPassed from both browser document and Service Worker to no-hook authorization script
- Disable "learning mode"
- Enable (or revive)
no-hook-authorization
parameter for hook.min.js with a dummy value
- Enable (or revive)
- Clear Service Worker(s)
- Update
version
parameter for hook.min.js
- Update
- Reload the page(s) with no-hook scripts(s)
- Copy and Paste the ticket for the no-hook authorization script into the
no-hook-authorization
parameter
- Copy and Paste the ticket for the no-hook authorization script into the
- Update
version
parameter for hook.min.js
- Update
- Clear Service Worker(s)
- Reload the page(s) with no-hook script(s)
- Check if there are no unauthorized no-hook scripts
- Steps to update authorized no-hook scripts:
- The values are stored in