本博客日IP超过2000,PV 3000 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog2,之前的微信号好友位已满,备注:返现
受密码保护的文章请关注“业余草”公众号,回复关键字“0”获得密码
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
腾讯云】1核2G5M轻量应用服务器50元首年,高性价比,助您轻松上云
这已经是去年的事情了,这件事要从被同事打脸开始说起…
去年我和同事开玩笑,说只要网上你能看到的东西,我就能下载下来让他保存在电脑上。同事摇了摇头,一脸的不相信,然后拿住我的鼠标,打开了众所周知的慕课网,说:“我要这个视频,你能把文件给我吗?”。
我点开浏览器控制台,发现网络请求中一堆的ts文件,之前我又不是没有下载过这样的视频,M3U8而已,怎么能难得住我?我满口就答应了同事,说:“5分钟后给你。”,事后我发现人们总是会低估一件事情的难度,从业者都低估自己所面对的需求,更何况是外行人呢。不过最后我还是用实际行动证明了我的观点,凡是浏览器能播放的东西,都可以保存在自己的电脑上,不管多么的复杂。
虽然我看到了一堆的ts文件,可是我苦苦寻找,并没有找到M3U8文件,那么这传说中的M3U8文件到底去了哪里了呢?
然后我从一套大家都见过的课程开始入手了,慢慢探索这其中的奥秘。
视频的播放地址是:
https://www.imooc.com/video/1430
是个学编程的人都知道后面的1430是个id,那么这个id用来做什么了呢?
看了看网络请求,发现了一个非常有趣的链接:
https://www.imooc.com/course/playlist/1430?t=m3u8&_id=5848c001b3fee30a6c8b51bd&cdn=aliyun1
t=m3u8,嗯?看到了我熟悉的m3u8,那么t可能代表的意思是type喽,后面的_id参数又是什么?毫不犹豫我按下了ctrl + F,搜索一下5848c001b3fee30a6c8b51bd,在搜索结果中我看了了HTML页面上居然有这个东西:
OP_CONFIG.mongo_id="5848c001b3fee30a6c8b51bd";
OP_CONFIG.page="video2.4";
没有问题,就是他了,后面的cdn=aliyun1这个不用想就知道他是用了阿里云的cdn。
链接中的参数都找全了,但是有一个问题,这个链接真的是返回的m3u8文件吗?
看了一眼响应数据,我又傻眼了:
{
"result":1,
"data":{
"info":"MkkqXqhmamoxAUB1PQ...",
"cdn":["aliyun","aliyun1","letv"]
},
"msg":""
}
info中一堆乱七八糟的东西,这堆东西到底是啥??? 明明这个链接看上去好像是要返回M3U8来着,但是为什么返回了这么一堆东西,一定是某种加密方式,不探索出这种加密方式,还怎么往下面进行。可是啊,加密方式的探索,哪里有那么容易。
我想了一会儿,这个链接是哪里发送出来的呢?我就可以从哪里找到他的加密方式啊。我又开始了一顿搜索,在一个js文件中看到了这个链接。
摘录一小部分:
...
"https" === h && (v = "https://www.imooc.com/course/playlist/" + pageInfo.mid + "?t=m3u8&_id=" + OP_CONFIG.mongo_id),
window.thePlayer = mocoplayer($("#video-box"), {
url: v,
title: videoTitle,
...
我们可以看到,拼接好了链接字符串后,又调用了mocoplayer方法,那么我一定可以根据这个mocoplayer方法来一步步跟进,得知他的加密算法的。又一顿搜索,得知了,mocoplayer原来在mocoplayer.js中。不错不错。可是难题来了,这个文件好大啊,使用webpack打包的文件,让我怎么看?
苦想了很久,应该用什么思路去面对这么大的文件呢?突然想到一招,我看看上面那个链接,到底是js的哪一行请求的不就行了?
很快我就到了发起链接请求的地方,可是这也太难找了吧,发起了一个链接请求而已,距离解密可能还早着呢!!!
没有办法,既然到这里了,那我就必须要打着断点往后看,毕竟这个链接一旦访问成功,离成功就不远了啊,过了好长一段时间,我看到了代码中有data.info这几个字符,立刻集中了精神,马上要到了,紧接着我就看到了下面的代码:
这个被圈起来的部分长得好眼熟,那么就意味着这个destm_1.default(mediadata.data.info);就是解密方法了。
深深的呼吸了一口气,给这个方法打上断点,然后进去看看,果不其然,最终确定这个文件的21919行到22066行是我想要的解密算法。
给这个字符串解密后,我发现,并不是我想的那么简单!!!
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=512000, RESOLUTION=1280x720
https://www.imooc.com/video/5848c001b3fee30a6c8b51bd/medium.m3u8?cdn=aliyun1...
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=384000, RESOLUTION=1280x720
https://www.imooc.com/video/5848c001b3fee30a6c8b51bd/medium.m3u8?cdn=aliyun1...
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=256000, RESOLUTION=720x480
https://www.imooc.com/video/5848c001b3fee30a6c8b51bd/low.m3u8?cdn=aliyun1...
看上去像是M3U8文件,可是文件里面的链接却又是m3u8格式的???
我尝试着访问了这些m3u8地址,于是又看到了熟悉的东西:
{
"code":200,
"data":{
"info":"Gm1kQ7hmVBYXVGoxVGdUVGA9VBwcKnAyF1RmJD1UY1QHVGRrVBxu..."
},
"msg":"OK"
}
现实总是那么的出乎意料,这一定又是一个加密后的字符串,我敢确定,一定和上一次用了同样的加密方法,我再次试了试:
丝毫没有问题,解密出来了,这就是我想要的m3u8,但是真的有那么简单吗???
我把这些ts文件直接下载下来,可是加着密呢呀!!!
上面M3U8字符串中有一串METHOD=AES-128引起了我的注意,这明显是告诉我,加密方式是用了什么!!! 后面的那一串链接一定是密钥了。
可是我打开链接后,居然与普通的密钥完全不一样,他长这样:
{
"code":200,
"data":{
"info":"I2RqhmmlX1UTHvYGIi4cQKqgARoiHh1rIjIo7WRlwhoT9h1nIg==t01UNGJIRvfj"
},
"msg":"OK"
}
历史总是那么惊人的相似,又解了一下密:
看上去,并不是那么回事,后面的参数改成了true,它长成了这样:
看起来像是那么回事了,正好16个字节,正好符合AES-128也就是传说中的AES/CBC/PKCS5PADDING,好了,java解密代码安排起来:
/**
* AES-CBC加解密,该类用来对每一段视频进行解密
*
* @author CY
*/
public class AESCBCDecrypt {
private static final String AES = "AES";
private static final String AES_CBC = "AES/CBC/PKCS5PADDING";
/**
* 使用AES加密或解密无编码的原始字节数组, 返回无编码的字节数组结果.
*
* @param input 原始字节数组
* @param key 符合AES要求的密钥
* @param iv 初始向量
* @param mode Cipher.ENCRYPT_MODE 或 Cipher.DECRYPT_MODE
*/
public static byte[] aes(byte[] input, byte[] key, byte[] iv, int mode) {
try {
Cipher cipher = Cipher.getInstance(AES_CBC);
cipher.init(mode, new SecretKeySpec(key, AES), new IvParameterSpec(iv));
cipher.update(input);
return cipher.doFinal(input);
} catch (GeneralSecurityException e) {
throw new RuntimeException("加解密出现了异常,当前的key:" + Arrays.toString(key) + ",IV:" + Arrays.toString(iv));
}
}
}
可是啊,需要的iv呢,他去哪里了,问题又出来了,我得看看,是哪里发起的ts文件请求,发起请求的地方应该会有iv吧!
又如我所料,找到了。
我跟着断点走了好几个ts文件,发现iv前面15位永远都是0,只是第16位会跟随ts文件的变化而发生变化,会逐渐的加1,我也可以模拟这样的变化,用来创建视频的iv值。
经过了一会,我把上面的东西全部梳理了一遍,解密算法是用js写的,而我使用了java,那么我有两种选择:
- 把它用java代码重新构建一遍(效率高)
- 用java来执行js文件(效率低)
java执行js的代码如下:
/**
* 解码的备用方法,运行JavaScript脚本,速度较慢
*
* @author CY
*/
public class ImoocDecoderBackup {
/**
* JavaScript解码算法执行器
*/
private static Invocable invocable;
/* 读取JS文件,并构造执行器 */
static {
InputStream inputStream = ImoocDecoderBackup.class.getClassLoader().getResourceAsStream("attachment/decode.js");
if (inputStream != null) {
try {
String javaScript = new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.joining("\n"));
ScriptEngine engine = new ScriptEngineManager().getEngineByName("Nashorn");
engine.eval(javaScript);
invocable = (Invocable) engine;
} catch (ScriptException ignored) {
}
}
}
/**
* 解码成字符串
*
* @param crypto 待解码的字符串
* @return 解码后的字符串
*/
public static String decoderString(String crypto) {
return invoke("decode2String", crypto);
}
/**
* 获取慕课网视频的密匙key
*
* @param crypto 待解码的字符串
* @return 字节数组
*/
public static byte[] decoderKey(String crypto) {
return new Gson().fromJson(invoke("decode2Bytes", crypto), byte[].class);
}
/**
* 调用JavaScript方法
*
* @param methodName 方法名
* @param encodeString 待解码的字符串
* @return 解码后的结果
*/
private static String invoke(String methodName, String encodeString) {
try {
return invocable.invokeFunction(methodName, encodeString).toString();
} catch (ScriptException | NoSuchMethodException ignored) {
}
return null;
}
}
解密出来之后生成一个m3u8文件,那么我就需要解析这个m3u8了,解析它就要符合HLS协议的规范,我尝试写了一下,并且做了测试,但是我发现,我写的并不那么理想,适用范围较小,因为毕竟HLS协议也不是那么简单的规定,他除了普通的视频流,还有直播流的,我找了找同性交友网站,看见了一个工具open-m3u8,不管它好不好用,反正我不用,既然自己写了一部分,就用自己的。
上面的一切工作准备就绪了,现在已经拥有了,解密,解析,那么接下来就是普普通通的下载了,边下载边进行AES-128解密就可以了,可是下载下来的文件零零散散,我还需要合并的,刚开始的时候使用Java的输出流拼接的方式进行的合并,可是没多久就发现了这个方式并不是那么的理想。因为视频也有头和尾,直接硬生生的拼接在一起,相当于玩了一场人体蜈蚣,播放的时候会造成一定的卡顿现象,于是我就选择了使用FFMpeg的方式进行合并:
./ffmpeg.exe -i "concat:ts文件1|ts文件2" -c copy -bsf:a aac_adtstoasc "输出文件"
最终解密出来了一堆完整的视频,放一张成果图片:
推荐阅读:https://github.com/iheartradio/open-m3u8 和 https://halo.cyblogs.top/archives/decrypt-imooc-video-download.html
最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加作者微信号:xttblog2。备注:“1”,添加博主微信拉你进微信群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作也可添加作者微信进行联系!