/**
 * @author Coco
 * @QQ:308695699
 * @name httphijack 1.0.0
 * @update : 2016-08-10
 * @description 使用Javascript实现前端防御http劫持及防御XSS攻击，并且对可疑攻击进行上报
 * -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
 *
1、使用方法：调用 httphijack.init()

2、建立自己的黑白名单、上报系统及接收后端

3、防范范围：
   1）所有内联事件执行的代码
   2）href 属性 javascript: 内嵌的代码
   3）静态脚本文件内容
   4）动态添加的脚本文件内容
   5）document-write添加的内容
   6）iframe嵌套
 *
 */
(function(window, undifined) {

  var httphijack = function() {},
    // 记录内联事件是否被扫描过的 hash map
    mCheckMap = {},
    // 记录内联事件是否被扫描过的id
    mCheckID = 0;

  // 建立白名单
  var whiteList = [
    'test.iamberry.com',
    'w.iamberry.com',
    'res.wx.qq.com',
    'static.iamberry.com',
    'h5.iamberry.com'
  ];
  // 建立黑名单
  var blackList = [
    '192.168.1.0'
  ];

  // 建立关键词黑名单
  var keywordBlackList = [
    'xss',
    'embed_v3',
    '_embed_v3',
    '_embed_v3_dc',
    '_embed_v3_hd_l',
    '_embed_v3_hd_c',
    '_embed_v3_hd_r',
    '_embed_v3_frmc',
    '_embed_v3_main',
    '_embed_v3_ft',
    'BAIDU_SSP__wrapper',
    'BAIDU_DSPUI_FLOWBAR',
    '_embed_v3_hd'
  ];
  
  // 触发内联事件拦截
  function triggerIIE() {
    var i = 0,
      obj = null;

    for (obj in document) {
      if (/^on./.test(obj)) {
        interceptionInlineEvent(obj, i++);
      }
    }
  }

  /**
   * 内联事件拦截
   * @param  {[String]} eventName [内联事件名]
   * @param  {[Number]} eventID   [内联事件id]
   * @return {[type]}             [description]
   */
  function interceptionInlineEvent(eventName, eventID) {
    var isClick = (eventName == 'onclick');

    document.addEventListener(eventName.substr(2), function(e) {
      scanElement(e.target, isClick, eventName, eventID);
    }, true);
  }

  /**
   * 扫描元素是否存在内联事件
   * @param  {[DOM]} elem [DOM元素]
   * @param  {[Boolean]} isClick [是否是内联点击事件]
   * @param  {[String]} eventName [内联 on* 事件名]
   * @param  {[Number]} eventID [给每个内联 on* 事件一个id]
   */
  function scanElement(elem, isClick, eventName, eventID) {
    var
      flag = elem['isScan'],
      // 扫描内联代码
      code = "",
      hash = 0;

    // 跳过已扫描的事件
    if (!flag) {
      flag = elem['isScan'] = ++mCheckID;
    }

    hash = (flag << 8) | eventID;

    if (hash in mCheckMap) {
      return;
    }

    mCheckMap[hash] = true;

    // 非元素节点
    if (elem.nodeType != Node.ELEMENT_NODE) {
      return;
    }

    if (elem[eventName]) {
      code = elem.getAttribute(eventName);
      if (code && blackListMatch(keywordBlackList, code)) {
        // 注销事件
        elem[eventName] = null;
        console.log('拦截可疑内联事件:' + code);
        hijackReport('拦截可疑内联事件', code);
      }
    }

    // 扫描 <a href="javascript:"> 的脚本
    if (isClick && elem.tagName == 'A' && elem.protocol == 'javascript:') {
      var code = elem.href.substr(11);
      if (blackListMatch(keywordBlackList, string)) {
        // 注销代码
        elem.href = 'javascript:void(0)';
        console.log('拦截可疑事件:' + code);
        hijackReport('拦截可疑javascript:代码', code);
      }
    }

    // 递归扫描上级元素
    scanElement(elem.parentNode);
  }

  // 主动防御 MutationEvent
  /**
   * 使用 MutationObserver 进行静态脚本拦截
   * @return {[type]} [description]
   */
  function interceptionStaticScript() {
    // MutationObserver 的不同兼容性写法
    var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;

    // 该构造函数用来实例化一个新的 Mutation 观察者对象
    // Mutation 观察者对象能监听在某个范围内的 DOM 树变化
    var observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        // 返回被添加的节点,或者为null.
        var nodes = mutation.addedNodes;

        // 逐个遍历
        for (var i = 0; i < nodes.length; i++) {
          var node = nodes[i];

          // 扫描 script 与 iframe
          if (node.tagName === 'SCRIPT' || node.tagName === 'IFRAME') {
            // 拦截到可疑iframe
            if (node.tagName === 'IFRAME' && node.srcdoc) {
              node.parentNode.removeChild(node);
              console.log('拦截到可疑iframe', node.srcdoc);
              hijackReport('拦截可疑静态脚本', node.srcdoc);

            } else if (node.src) {
              // 只放行白名单
              if (!whileListMatch(whiteList, node.src)) {
                node.parentNode.removeChild(node);
                // 上报
                console.log('拦截可疑静态脚本:', node.src);
                hijackReport('拦截可疑静态脚本', node.src);
              }
            }
          }
        }
      });
    });

    // 传入目标节点和观察选项
    // 如果 target 为 document 或者 document.documentElement
    // 则当前文档中所有的节点添加与删除操作都会被观察到d
    observer.observe(document, {
      subtree: true,
      childList: true
    });
  }

  /**
   * 使用 DOMNodeInserted  进行动态脚本拦截监
   * 此处无法拦截，只能监测
   * @return {[type]} [description]
   */
  function interceptionDynamicScript() {
    // DOMNodeInserted 的执行时机早于 MutationObserver
    document.addEventListener('DOMNodeInserted', function(e) {
      var node = e.target;
      if (/xss/i.test(node.src) || /xss/i.test(node.innerHTML)) {
        node.parentNode.removeChild(node);
        console.log('拦截可疑动态脚本:', node);
        hijackReport('拦截可疑动态脚本', node.src);
      }
    }, true);
  }

  // 重写 createElement
  function resetCreateElement() {}

  /**
   * 重写单个 window 窗口的 document.write 属性
   * @param  {[BOM]} window [浏览器window对象]
   * @return {[type]}       [description]
   */
  function resetDocumentWrite(window) {
    var old_write = window.document.write;

    window.document.write = function(string) {
      if (blackListMatch(keywordBlackList, string)) {
        console.log('拦截可疑模块:', string);
        hijackReport('拦截可疑document-write', string);
        return;
      }

      // 调用原始接口
      old_write.apply(document, arguments);
    }
  }

  /**
   * 重写单个 window 窗口的 setAttribute 属性
   * @param  {[BOM]} window [浏览器window对象]
   * @return {[type]} [description]
   */
  function resetSetAttribute(window) {
    // 保存原有接口
    var old_setAttribute = window.Element.prototype.setAttribute;

    // 重写 setAttribute 接口
    window.Element.prototype.setAttribute = function(name, value) {

      // 额外细节实现
      if (this.tagName == 'SCRIPT' && /^src$/i.test(name)) {

        if (!whileListMatch(whiteList, value)) {
          console.log('拦截可疑模块:', value);
          hijackReport('拦截可疑setAttribute', value);
          return;
        }
      }

      // 调用原始接口
      old_setAttribute.apply(this, arguments);
    };
  }

  /**
   * 使用 MutationObserver 对生成的 iframe 页面进行监控，
   * 防止调用内部原生 setAttribute 及 document.write
   * @return {[type]} [description]
   */
  function defenseIframe() {
    // 先保护当前页面
    installHook(window);
  }

  /**
   * 实现单个 window 窗口的 setAttribute保护
   * @param  {[BOM]} window [浏览器window对象]
   * @return {[type]}       [description]
   */
  function installHook(window) {
    // 重写单个 window 窗口的 setAttribute 属性
    resetSetAttribute(window);
    // 重写单个 window 窗口的 document.Write 属性
    resetDocumentWrite(window);

    // MutationObserver 的不同兼容性写法
    var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;

    // 该构造函数用来实例化一个新的 Mutation 观察者对象
    // Mutation 观察者对象能监听在某个范围内的 DOM 树变化
    var observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        // 返回被添加的节点,或者为null.
        var nodes = mutation.addedNodes;

        // 逐个遍历
        for (var i = 0; i < nodes.length; i++) {
          var node = nodes[i];

          // 给生成的 iframe 里环境也装上重写的钩子
          if (node.tagName == 'IFRAME') {
            installHook(node.contentWindow);
          }
        }
      });
    });

    observer.observe(document, {
      subtree: true,
      childList: true
    });
  }

  /**
   * 使用 Object.defineProperty，锁住call和apply，使之无法被重写
   * @return {[type]} [description]
   */
  function lockCallAndApply() {
    // 锁住 call
    Object.defineProperty(Function.prototype, 'call', {
      value: Function.prototype.call,
      // 当且仅当仅当该属性的 writable 为 true 时，该属性才能被赋值运算符改变
      writable: false,
      // 当且仅当该属性的 configurable 为 true 时，该属性才能够被改变，也能够被删除
      configurable: false,
      enumerable: true
    });
    // 锁住 apply
    Object.defineProperty(Function.prototype, 'apply', {
      value: Function.prototype.apply,
      writable: false,
      configurable: false,
      enumerable: true
    });
  }

  /**
   * 重定向iframe hijack（页面被iframe包裹）
   */
  function redirectionIframeHijack() {
    var flag = 'iframe_hijack_redirected';
    // 当前页面存在于一个 iframe 中
    // 此处需要建立一个白名单匹配规则，白名单默认放行
    if (self != top) {
      var
      // 使用 document.referrer 可以拿到跨域 iframe 父页面的 URL
        parentUrl = document.referrer,
        length = whiteList.length,
        i = 0;

      for (; i < length; i++) {
        // 建立白名单正则
        var reg = new RegExp(whiteList[i], 'i');

        // 存在白名单中，放行
        if (reg.test(parentUrl)) {
          return;
        }
      }

      var url = location.href;
      var parts = url.split('#');
      if (location.search) {
        parts[0] += '&' + flag + '=1';
      } else {
        parts[0] += '?' + flag + '=1';
      }
      try {
        console.log('页面被嵌入iframe中:', parentUrl);
        hijackReport('页面被嵌入iframe中', parentUrl);
        top.location.href = parts.join('#');
      } catch (e) {}
    }
  }

  /**
   * 自定义上报 -- 替换页面中的 console.log()
   * @param  {[String]} name  [拦截类型]
   * @param  {[String]} value [拦截值]
   * @return {[type]}   [description]
   */
  function hijackReport(name, value) {
    var img = document.createElement('img'),
      hijackName = name,
      hijackValue = value.toString(),
      curDate = new Date().getTime();

    // 上报
    img.src = 'http://www.reportServer.com/report/?msg=' + hijackName + '&value=' + hijackValue + '&time=' + curDate;
  }

  /**
   * [白名单匹配]
   * @param  {[Array]} whileList [白名单]
   * @param  {[String]} value    [需要验证的字符串]
   * @return {[Boolean]}         [false -- 验证不通过，true -- 验证通过]
   */
  function whileListMatch(whileList, value) {
    var length = whileList.length,
      i = 0;

    for (; i < length; i++) {
      // 建立白名单正则
      var reg = new RegExp(whiteList[i], 'i');

      // 存在白名单中，放行
      if (reg.test(value)) {
        return true;
      }
    }
    return false;
  }

  /**
   * [黑名单匹配]
   * @param  {[Array]} blackList [黑名单]
   * @param  {[String]} value    [需要验证的字符串]
   * @return {[Boolean]}         [false -- 验证不通过，true -- 验证通过]
   */
  function blackListMatch(blackList, value) {
    var length = blackList.length,
      i = 0;

    for (; i < length; i++) {
      // 建立黑名单正则
      var reg = new RegExp(blackList[i], 'i');

      // 存在黑名单中，拦截
      if (reg.test(value)) {
        return true;
      }
    }
    return false;
  }

  // 待完成：
  // 建立黑白名单列表
  // 对正则匹配精细化

  // 初始化方法
  httphijack.init = function() {
    // 触发内联事件拦截
    triggerIIE();
    // 进行静态脚本拦截
    interceptionStaticScript();
    // 进行动态脚本拦截
    // interceptionDynamicScript();
    // 锁住 apply 和 call
    lockCallAndApply();
    // 对当前窗口及多重内嵌 iframe 进行 setAttribute | document.write 重写
    defenseIframe();
    // 对iframe劫持进行重定向
    redirectionIframeHijack();
  }

  window.httphijack = httphijack;
})(window);
