video.js 劫持方式分析
本文的诞生是源于某星的技术对抗,相对其他文章可能较为极限,并且在对抗的过程中不断升级方法,所以如果没有能力可以暂且先跳过
正文
video.js 常见的劫持方式是对 window.videojs 进行 hook 来进行劫持,例如
unsafeWindow.savevideojs = undefined;
Object.defineProperty(unsafeWindow, "videojs", {
get() {
let result = unsafeWindow.savevideojs;
return result;
},
set(obj) {
unsafeWindow.savevideojs = obj;
},
});
但是很对网站为了对抗 videojs 劫持,会习惯性将 videojs 通过 Object.defineProperty 设置 writable 为 flase,从而导致无法赋值,所以我们的目标是不仅仅局限于 videojs 的入口劫持,而从源码的角度进行分析和处理。
通过 hook 绕过
针对 Object.defineProperty 设置 writable 的问题,我们可以通过对针对 Object.definproperty 也进行劫持来处理这个问题
let defineHook = Object.defineProperty;
Object.defineProperty = function (...args) {
if (args.length >= 2) {
if (args[1] === "videojs" && args[2] !== undefined) {
args[2].writable = true;
}
}
return defineHook.call(this, ...args);
};
源码分析
我们也可以尝试阅读源码来解决,video.js 源码
查看源码
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.videojs = factory());
}(this, (function () { 'use strict';
umd 格式,直接拖底部看一下找到了return videojs;
,发现传出一个函数,我们查找 videojs,是一个
function videojs(id, options, ready)
大致阅读一下代码
var player = videojs.getPlayer(id);
if (player) {
if (options) {
log$1.warn(
'Player "' + id + '" is already initialised. Options will not be applied.'
);
}
if (ready) {
player.ready(ready);
}
return player;
}
首先获取 id,判断是否存在,如果存在则不初始化
这里我一开始试图对 Getplayer 进行劫持,后来发现少了一些参数,所以决定放弃
var el = typeof id === "string" ? $("#" + normalizeId(id)) : id;
if (!isEl(el)) {
throw new TypeError("The element or ID supplied is not valid. (videojs)");
} // document.body.contains(el) will only check if el is contained within that one document.
// This causes problems for elements in iframes.
// Instead, use the element's ownerDocument instead of the global document.
// This will make sure that the element is indeed in the dom of that document.
// Additionally, check that the document in question has a default view.
// If the document is no longer attached to the dom, the defaultView of the document will be null.
if (!el.ownerDocument.defaultView || !el.ownerDocument.body.contains(el)) {
log$1.warn("The element supplied is not included in the DOM");
}
options = options || {};
hooks("beforesetup").forEach(function (hookFunction) {
var opts = hookFunction(el, mergeOptions$3(options));
if (!isObject$1(opts) || Array.isArray(opts)) {
log$1.error("please return an object in beforesetup hooks");
return;
}
options = mergeOptions$3(options, opts);
}); // We get the current "Player" component here in case an integration has
// replaced it with a custom player.
兼容化处理+options 合并,可以直接跳过,继续往下看
var PlayerComponent = Component$1.getComponent("Player");
player = new PlayerComponent(el, options, ready);
hooks("setup").forEach(function (hookFunction) {
return hookFunction(player);
});
return player;
这里发现通过了 Component.GetComponent 读了 Player 函数
然后对其构造,最后调用了 Hooks 回调钩子,传入了一个函数,然后循环遍历
等等,嗯?hooks 钩?往前翻再看看
hooks("beforesetup").forEach(function (hookFunction) {
var opts = hookFunction(el, mergeOptions$3(options));
if (!isObject$1(opts) || Array.isArray(opts)) {
log$1.error("please return an object in beforesetup hooks");
return;
}
options = mergeOptions$3(options, opts);
});
hooks("setup").forEach(function (hookFunction) {
return hookFunction(player);
});
经典的 beforeCreated 钩和 Created 钩,查阅一下video.js 手册
videojs.hook("beforesetup", function (videoEl, options) {
// videoEl will be the video element with id="some-id" since that
// gets passed to videojs() below. On subsequent calls, it will be
// different.
videoEl.className += " some-super-class";
// autoplay will be true here, since we passed it as such.
if (options.autoplay) {
options.autoplay = false;
}
// Options that are returned here will be merged with old options.
//
// In this example options will now be:
// {autoplay: false, controls: true}
//
// This has the practical effect of always disabling autoplay no matter
// what options are passed to videojs().
return options;
});
videojs.hook("setup", function (player) {
// Initialize the foo plugin after any player is created.
player.foo();
});
所以我们可以直接通过videojs.hook
来曲线获得 video.js 创建出来的实例。例如:
videojs.hook("beforesetup", function (videoEl, options) {
//修改选项
return options;
});
videojs.hook("setup", function (player) {
//控制实例
});
具体的 hook 使用方式大家可以参考实战篇的相关内容
hook 钩被屏蔽下的解决方案
由于某星屏蔽了 hook 钩,所以我们继续探索其他方案
本质上学习观察源码的过程也是提升自我的一个过程
只要忍过第一个阵痛期,后面其实能看懂源码很爽的
很多时候你比只会看文档的程序员更了解执行原理!
之前的 hooks 已经失效了,那我们在初始化阶段几乎没有什么可以插手的地方了
hooks("beforesetup").forEach(function (hookFunction) {
var opts = hookFunction(el, mergeOptions$3(options));
if (!isObject$1(opts) || Array.isArray(opts)) {
log$1.error("please return an object in beforesetup hooks");
return;
}
options = mergeOptions$3(options, opts);
}); // We get the current "Player" component here in case an integration has
// replaced it with a custom player.
var PlayerComponent = Component$1.getComponent("Player");
player = new PlayerComponent(el, options, ready);
hooks("setup").forEach(function (hookFunction) {
return hookFunction(player);
});
hooks 钩的地方可以全部排除了
那我们唯一能下手的地方就是更核心的地方
也就是 Component$1.getComponent 函数
我们先观察一下他的源码
Component.getComponent = function getComponent(name) {
if (!name || !Component.components_) {
return;
}
return Component.components_[name];
};
这里判断是否为空,或者保存组件的位置是否为空,如果都不为空,则返回对应的名字
也就是说 Player 保存在Component.components_['Player']
中
那我们需要设置的,就是对components_
进行设置
全局搜索Component.components_
以及查阅官方文档
可以找到这里
查阅官方文档可以发现,这里是注册组件的地方
那么思路来了,我们可不可以注册 Player 函数?
阅读源码开始!
if (typeof name !== "string" || !name) {
throw new Error(
'Illegal component name, "' + name + '"; must be a non-empty string.'
);
}