thin-hook v0.1.13-stack.8
thin-hook
Thin Hook Preprocessor (experimental)
Notes
- 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
class C {
add(a, b) {
return __hook__((a = 1, b = 2) => {
let plus = (...args) => __hook__((x, y) => x + y, this, args, 'examples/example2.js,C,add,plus');
return plus(a, b);
}, this, arguments, 'examples/example2.js,C,add');
}
}
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');
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>
...
</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><!--
<C!-- Hook Callback Function without hooking properties --C>
<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>
...
</html>-->
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.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
- 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.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
hook-property
:hookProperty
parameter.true
if property accessors are hooked. The value affects the default value of thehookProperty
parameter forhook()
hook-global
:hookGlobal
parameter.true
if global variables are hooked. The value affects the default value of thehookGlobal
parameter forhook()
hook-prefix
:hookPrefix
parameter. Prefix accessor names ofhook.global()._p_GlobalVariableName
with the value. Default:_p_
compact
:compact
parameter. Generate compact code iftrue
. The value affects the default value of thecompact
parameter forhook()
service-worker-ready
:true
if the entry HTML page is decoded;false
if encoded. This parameter must be at the end of the URL
<script src="script.js?no-hook=true"></script>
: skip hooking for the source script<script no-hook>...</script>
: skip hooking for the embedded script<script context-generator>
: register a custom context generator for both Service Worker and browser document<script context-generator no-hook>hook.contextGenerators.custom = function (astPath) {...}</script>
: embedded script<script context-generator src="custom-context-generator.js?no-hook=true"></script>
: with src URL- Valid only in the main entry document with
hook.min.js
for Service Worker - Must be runnable in both Service Worker and browser document
- Extensions other than context generators:
- Set Service Worker parameters:
hook.parameters.cors = [ 'cors_url_1', 'cors_url_2', ... ]
: specify CORS script URLshook.parameters.cors = [ (url) => url.match(/cors_url_pattern/), ... ]
: specify CORS script URL detector function(s)hook.parameters.opaque = [ 'opaque_url_1', 'opaque_url_2', ... ]
: specify authorized opaque URLshook.parameters.opaque = [ (url) => url.match(/opaque_url_pattern/), ... ]
: specify authorized opaque URL detector function(s)
- Set
no-hook
Authorization Tickets:hook.parameters.noHookAuthorization = { '{sha-256 hex hash for authorized no-hook script}': true, ... }
: Set keys fromhook.parameters.noHookAuthorizationPassed
in both Document and Service Worker threadshook.parameters.noHookAuthorization = { '*': true }
: learning mode to detect authorization tickets
- Specify URL patterns for
no-hook
scripts:hook.parameters.noHook = [ 'no_hook_url_1', 'no_hook_url_2', ... ]
: specifyno-hook
script URLshook.parameters.noHook = [ (url: URL) => !!url.href.match(/{no-hook URL pattern}/), ... ]
: specifyno-hook
script URL detector function(s)
- Specify URL patterns for source map target scripts:
hook.parameters.sourceMap = [ 'source_map_target_url_1', 'source_map_target_url_2', ... ]
: specify source map target script URLshook.parameters.sourceMap = [ (url: URL) => !!url.href.match(/{source map target URL pattern}/), ... ]
: specify source map target script URL detector function(s)
- Specify URL for hook worker script:
hook.parameters.hookWorker = 'hook-worker.js?no-hook=true'
: specify hook worker script URL
- Register Custom Event Handler:
if (typeof self === 'object' && self instanceof 'ServiceWorkerGlobalScope') { self.addEventListener('{event_type}', function handler(event) {...})}
- URL for the entry page
hook.parameters.baseURI
: Set indemo/bootstrap.js
- Empty Document URL
hook.parameters.emptyDocumentUrl = new URL('./empty-document.html', baseURI);
: Set indemo/bootstrap.js
.<iframe src="empty-document.html?url=https://host/path.html,iframe">
to specify context in iframe document
- Bootstrap Script Tag
hook.parameters.bootstrap = "<script>frameElement.dispatchEvent(new Event('srcdoc-load'))</script>";
: Set indemo/bootstrap.js
- Append to the hooked
srcdoc
to dispatchsrcdoc-load
event toonload
handler
- Onload Wrapper Script
hook.parameters.onloadWrapper = "event.target.addEventListener('srcdoc-load', () => { $onload$ })";
: Set indemo/bootstrap.js
- Receive
srcdoc-load
event and trigger the originalonload
script- Note:
addEventListener('load', handler)
is currently called BEFORE the document fromsrcdoc
is loaded andsrcdoc-load
event is fired.
- Note:
- Empty SVG to load the target URL
hook.parameters.emptySvg = '<?xml version="1.0"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="1px" height="1px"><script>location = "$location$";</script></svg>';
- Bootstrap Scripts for SVG
hook.parameters.bootstrapSvgScripts = '<script xlink:href="URL?params"></script>...'
- Check Request callback on Fetch at Service Worker
hook.parameters.checkRequest = function (event, response, cache) { /* check request */ return response ; }
:response
- cached response if exists; Seedemo/disable-devtools.js
- Root of Application Path
hook.parameters.appPathRoot = '/';
- The app assets are underlocation.origin + hook.parameters.appPathRoot
- Set Service Worker parameters:
- register as Service Worker
Service-Worker-Allowed
HTTP response header must have an appropriate scope for the target application
cors=true
parameter: CORS script, e.g.,<script src="https://cross.origin.host/path/script.js?cors=true"></script>
hook.serviceWorkerTransformers
:encodeHtml(html: string)
: encode HTML for Service WorkerdecodeHtml(html: string)
: decode encoded HTML for Service Worker
hook.hookWorkerHandler(event)
: onmessage handler for Hook Workers- Usage:
onmessage = hook.hookWorkerHandler
in Hook Worker script
- Usage:
hook.registerServiceWorker(fallbackUrl: string = './index-no-service-worker.html', reloadTimeout: number = 500, inactiveReloadTimeout: number = 1000)
:- Automatically called on loading
hook.min.js
on browsers fallbackUrl
: fallback URL for browsers without Service WorkerreloadTimeout
: default: 500 (ms). Timeout to reload the page when no Service Worker is detectedinactiveReloadTimeout
: default: 1000 (ms). Timeout to reload the page when inactive (waiting, installing) Service Worker is detected. When a state change of the Service Worker instance is detected, the page is reloaded immediately even before the timeout.
- Automatically called on loading
utils
: UtilitiescreateHash
: Synchronous SHA hash generator collections from sha.jsHTMLParser
: HTML parser from htmlparser2
TODOs
- Refine API
- Hook Coverage
- Hook Web Worker Scripts
- Hook Native APIs
- Consistent Contexts
- Track Asynchronous Calls
- Security Policies
- Framework for Access Control Policies
- Framework for Context Transition Policies
- Modularization of Policies
- Test Suites
- Demo
- Performance Optimization
License
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago