t2ym / thin-hook

Thin Hook Preprocessor
Other
4 stars 1 forks source link
access-control-list es6 hooks mandatory-access-control service-worker

npm version Bower version

thin-hook

Thin Hook Preprocessor (experimental)

Notes

old name new name feature
method oldMethod script.js,Class,method
cachedMethod method script.js,Class,method including computed property names

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="https://github.com/t2ym/thin-hook/raw/master/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="https://github.com/t2ym/thin-hook/raw/master/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="https://github.com/t2ym/thin-hook/raw/master/./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="https://github.com/t2ym/thin-hook/raw/master/.."></script>
    ...
</html>

Encoded HTML (Service Worker converts it to Decoded HTML)

<html>
  <head>
    <script src="https://github.com/t2ym/thin-hook/raw/master/./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="https://github.com/t2ym/thin-hook/raw/master/.."></script>
    ...
</html>-->

Supported Syntax

Install

Browsers

  bower install --save thin-hook

NodeJS

  npm install --save thin-hook

Import

Browsers

  <!-- browserified along with espree and escodegen; minified -->
  <script src="https://github.com/t2ym/thin-hook/raw/master/path/to/bower_components/thin-hook/hook.min.js"></script>

NodeJS

  const hook = require('thin-hook/hook.js');

API (Tentative)

Plugins

<script context-generator src="https://github.com/t2ym/thin-hook/raw/master/no-hook-authorization.js?no-hook=true"></script>

<script context-generator src="https://github.com/t2ym/thin-hook/raw/master/integrity.js?no-hook=true"></script>

<script context-generator src="https://github.com/t2ym/thin-hook/raw/master/disable-devtools.js?no-hook=true"></script>

<script context-generator src="https://github.com/t2ym/thin-hook/raw/master/context-generator.js?no-hook=true"></script>

<script context-generator src='bootstrap.js?no-hook=true'></script>

<script context-generator no-hook>hook.parameters.* ...</script>

<script context-generator src="https://github.com/t2ym/thin-hook/raw/master/cache-bundle.js?no-hook=true&authorization=..."></script>

<script src="https://github.com/t2ym/thin-hook/raw/master/hook-callback.js?no-hook=true"></script>

<script context-generator src="https://github.com/t2ym/thin-hook/raw/master/script-hashes.js?no-hook=true&service-worker-ready=false"></script>

<script src="https://github.com/t2ym/thin-hook/raw/master/content-loader.js?no-hook=true"></script>

<script src="https://github.com/t2ym/thin-hook/raw/master/wrap-globals.js?no-hook=true"></script>

Notes on Performance Overheads on Global Object Access

<script src="https://github.com/t2ym/thin-hook/raw/master/mark-parsed.js?no-hook=true"></script>

Server-side Components

Server-side scripts and components configured for the demo but fully customizable for the target application

demo-backend/demoServer.js

Back-end server for the demo. TBD

demo-backend/errorReportService.js

Handler for demo/errorReport.json POST requests

demo-backend/cacheBundleGeneration.js

Used at build time to automate generation of cache-bundle.json via puppeteer

demo-backend/cacheBundleUploadService.js

Formerly used at build time to automate uploading of cache-bundle.json via a POST request

demo-backend/postHtml.js

Express middleware for demoServer.js to handle demo/postHtml. This should be unnecessary and should not be used except for verification of HTML via a POST request.

demo-backend/integrityService.js

Express middleware for demoServer.js to provide integrity and double encryption of body data

demo-backend/integrity-service-helpers/build/release/native.node

Node addon package compiled from the C++ source binding.cpp to provide the following functions

demo-backend/validationService.js

When invoked as a CLI script, it provides the validation server for ClientIntegrity.browserHash. TBD

When imported as a package, it provides the client API for the validation server. TBD

demo-backend/validation-console/dist/

Validation Console GUI served by demo-backend/validationService.js. TBD

demo-keys/generate_cert.sh

Script to generate certificates in demo-keys/demoCA/

demo-keys/keys.json

Key pairs and secret keys are stored for the application version.

{
  "version": "version_668", // application version
  "rsa-private-key.pem": "RSA PRIVATE KEY in PEM",
  "rsa-public-key.pem": "RSA PUBLIC KEY in PEM",
  "ecdsa-private-key.pem": "ECDSA PRIVATE KEY in PEM",
  "ecdsa-public-key.pem": "ECDSA PUBLIC KEY in PEM",
  "session-id-aes-key": "base64(random(32 bytes))",
  "session-id-aes-iv": "base64(random(12 bytes))",
  "scriptsHashHex": "hex(ClientIntegrity.scriptsHash)",
  "htmlHashHex": "hex(ClientIntegrity.htmlHash)"
}

TBD

NPM scripts

{
  "scripts": {
    "test": "wct",
    "build": "gulp",
    "demo": "run-p -l demoServer errorReportService validationService",
    "debug": "run-p -l debugServer errorReportService validationService",
    "https": "run-p -l httpsServer errorReportService validationService",
    "upload": "run-p -l buildServer cacheBundleUploadService",
    "cache-bundle": "run-p -r -l buildServer cacheBundleUploadService cacheBundleGeneration",
    "updateHtmlHash": "run-p -r -l buildServer cacheBundleUploadService loadOnly",
    "buildServer": "node demo-backend/demoServer.js -p 8080 -m build -P https -H \"localhost:8080\"",
    "demoServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -H \"${SERVER_HOST}\"",
    "httpsServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -P https -H \"${SERVER_HOST}:8080\"",
    "debugServer": "node --inspect-brk=0.0.0.0:9229 demo-backend/demoServer.js -p 8080 -m debug -c 1 -H \"${SERVER_HOST}\"",
    "postHtml": "run-p -l postHtmlServer errorReportService",
    "postHtmlServer": "node demo-backend/demoServer.js -p 8080 -m server -c 4 -H \"${SERVER_HOST}\" --middleware ./postHtml.js",
    "errorReportService": "node demo-backend/errorReportService.js -p 8081",
    "validationService": "node demo-backend/validationService.js -p 8082 -m server -H \"${VALIDATION_HOST}\"",
    "integrity-service-helpers": "cd demo-backend/integrity-service-helpers && npm install",
    "validation-console": "cd demo-backend/validation-console && npm ci && npm run build",
    "demo-certificates": "cd demo-keys && ./generate_cert.sh ",
    "clean-demo-certificates": "cd demo-keys && rm -riv demoCA",
    "cacheBundleUploadService": "node demo-backend/cacheBundleUploadService.js",
    "cacheBundleGeneration": "node demo-backend/cacheBundleGeneration.js",
    "loadOnly": "node demo-backend/cacheBundleGeneration.js loadOnly",
    "test:attack": "run-p -r -l buildServer cacheBundleUploadService puppeteerAttackTest",
    "puppeteerAttackTest": "node test/puppeteerAttackTest.js",
    "demo-frontend-modules": "cd demo/ && npm install",
    "demo-frontend-modules-locked": "cd demo/ && npm ci"
  }
}

${SERVER_HOST} environment variable

HTTPS server host name for the application. Defaults to localhost

${VALIDATION_HOST} environment variable

HTTPS server host name at port 8082 for Validation Console and Validation Service API. Defaults to localhost

npm test

Run hook tests

npm run build

Build hook.min.js and the demo via gulp

npm run demo

Serve the demo from demo-frontend/ at https://${SERVER_HOST}/components/thin-hook/demo/ via nginx proxing to http://localhost:8080

npm run debug

Serve the demo from demo/ at https://${SERVER_HOST}/components/thin-hook/demo/ via nginx proxying to http://localhost:8080

npm run https

Serve the demo from demo-frontend/ at https://${SERVER_HOST}:8080/components/thin-hook/demo/ with the key pair demo-keys/demoCA/${SERVER_HOST}.{key|crt}

npm run upload

Formerly used to upload cache-bundle.json via demo-backend/cacheBundleUploadService.js at build time

npm run cache-bundle

Called from gulp cache-bundle-automation task to automate building of cache-bundle.json at build time

npm run updateHtmlHash

Called from gulp update-html-hash task to update demo-keys/keys.json for "htmlHashHex" of the entry page HTML after the integrity attribute of <script src="https://github.com/t2ym/thin-hook/raw/master/script-hashes.js"> is updated in gulp script-hashes-integrity task

npm run buildServer

Called from npm run cache-bundle to tonvoke demoServer.js in build mode at build time

npm run demoServer

Called from npm run demo to invoke demoServer.js in server mode without TLS

npm run httpsServer

Called from npm run https to invoke demoServer.js in server mode with TLS

npm run debugServer

Called from npm run debug to invoke demoServer.js in debug mode attached by Node.js debugger

npm run errorReportService

Called from npm run {demo|https|debug} to invoke errorReportService.js at port 8081

npm run validationService

Called from npm run {demo|https|debug} to invoke validationService.js at port 8082

npm run integrity-service-helpers

Build demo-backend/integrity-service-helpers/ as Node addon API

npm run validation-console

Build demo-backend/validation-console/ Validation Console GUI, which is served via validationService.js

npm run demo-certificates

Generate certificates for the demo

npm run clean-demo-certificates

Clean up certificates in demo-keys/demoCA/. Each removal must be confirmed via rm -rvi

npm run cacheBundleUploadService

Called from npm run cache-bundle to invoke cacheBundleUploadService.js

npm run cacheBundleGeneration

Called from npm run cache-bundle to invoke cacheBundleGeneration.js

npm run loadOnly

Called from npm run updateHtmlHash to invoke cacheBundleGeneration.js in loadOnly mode

npm run demo-frontend-modules

Install demo/node_modules based on demo/package.json for frontend modules for the demo. demo/package-lock.json is updated.

npm run demo-frontend-modules-locked

Called from gulp demo-frontend-modules-locked to install demo/node_modules based on demo/package-lock.json

Gulp Tasks

gulp.task('default',
  gulp.series(
    'build',        // build hook.min.js
    'build:test',   // build test/hook.min.js
    'examples',     // hook examples/*
    'demo'          // build demo
  )
);

gulp.task('examples',
  gulp.series(
    'script-examples',                      // hook non-module script examples
    'module-examples',                      // hook module examples
    'module-examples-dependencies'          // save hook.parameters.moduleDependencies at examples/moduleDependencies.json
  )
);

gulp.task('demo',
  gulp.series(
    'integrity-service-helpers',            // build demo-backend/integrity-service-helpers/
    'validation-console',                   // build demo-backend/validation-console/
    'clean-gzip',                           // clean demo/*.gz
    'get-version',                          // get version from the entry page demo/original-index.html
    'demo-certificates',                    // generate certificates in demo-keys/demoCA/ if they are missing
    'demo-keys',                            // generate key pairs and secret keys in demo-keys/keys.json
    'import-maps',                          // generate import maps for demo at demo/modules.importmap
    'browserify-commonjs',                  // build demo/browserify-commonjs.js
    'webpack-es6-module',                   // build demo/webpack-es6-module.js
    'webpack-commonjs',                     // build demo/webpack-commonjs.js
    'rollup-es-modules',                    // build demo/rollup-module1.js and demo/rollup-es6-module.js
    'policy',                               // configure demo/hook-callback.js
    'disable-devtools',                     // configure demo/disable-devtools.js
    'update-integrity-js',                  // update demo/integrity.js for the generated public keys in base64
    'update-no-hook-authorization',         // update demo/no-hook-authorization.js
    'update-no-hook-authorization-in-html', // update hook.min.js?no-hook-authorization=* in HTMLs
    'encode-demo-html',                     // generate demo/index.html from demo/original-index.html
    'cache-bundle',                         // generate demo/cache-bundle.json via puppeteer
    'integrity-json',                       // generate demo/integrity.json
    'gzip',                                 // gzip demo/cache-bundle.json and demo/integrity.json
    'demo-frontend',                        // refresh and generate `demo-frontend/`
  )
);

gulp.task('import-maps', 
  gulp.series(
    'demo-frontend-modules-locked',  // install demo/node_modules based on demo/package.json and demo/package-lock.json
    'generate-import-maps',          // generate import maps for demo frontend at demo/modules.importmap based on demo/node_modules/* and demo/modules-private.importmap
    'embed-import-maps',             // embed the generated import maps JSON into demo/bootstrap.js
  )
);

gulp.task('cache-bundle',
  gulp.series(
    'get-version',                   // get version
    'dummy-integrity',               // generate dummy demo/integrity.json for build
    'cache-bundle-automation-json',  // generate dummy demo/cache-bundle.json for build
    'cache-bundle-automation',       // generate demo/cache-bundle.json via npm run cache-bundle
    'script-hashes',                 // add script hashes to demo/cache-bundle.json
    'script-hashes-integrity',       // update integrity attributes of script-hashes.js script element in the entry page
    'update-html-hash'               // update "htmlHashHex" in demo-keys/keys.json via npm run updateHtmlHash
  )
);

TODOs

License

BSD-2-Clause