之前我们已经实现了python的DRM视频解密
但是我是nodejs,因为之前已经写了大量的爬虫代码
并且由于js目前没找到cdm的解密库
所以干脆考虑自己封一下
我决定把之前cdm解密的python代码抽象一下
并且引入flask,通过pyinstaller打包成exe
然后封装一个nodejs的库唤起,本地服务器如果一定时间没有心跳就自动销毁
首先封装一下python的flask代码
from flask import Flask
from flask import request
from flask import jsonify
from threading import Timer
from inspect import signature
import threading
from pywidevine.cdm import Cdm
from pywidevine.device import Device
from pywidevine.pssh import PSSH
import argparse
import time
import os
import socket
import signal
import requests
parser = argparse.ArgumentParser(description='command', formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('--autoClose', '-c', help='是否自动关闭,默认为300s,设置为0则不自动关闭',default='300')
parser.add_argument('--port', '-p', help='设置端口号')
args = parser.parse_args()
args.autoClose=int(args.autoClose)
cdmInstance=None
app = Flask(__name__)
PID = os.getpid()
@app.route("/ping",methods=["GET"])
def ping():
print('run ping')
closeServer()
return jsonify(status="success")
@app.route("/close",methods=["GET"])
def close():
shutdown()
return jsonify(status="success")
def debounce(wait):
def decorator(fn):
sig = signature(fn)
caller = {}
def debounced(*args, **kwargs):
nonlocal caller
try:
bound_args = sig.bind(*args, **kwargs)
bound_args.apply_defaults()
called_args = fn.__name__ + str(dict(bound_args.arguments))
except:
called_args = ''
t_ = time.time()
def call_it(key):
try:
# always remove on call
caller.pop(key)
except:
pass
fn(*args, **kwargs)
try:
# Always try to cancel timer
caller[called_args].cancel()
except:
pass
caller[called_args] = Timer(wait, call_it, [called_args])
caller[called_args].start()
return debounced
return decorator
@app.route("/loadDevice",methods=["POST"])
def loadDevice():
global cdmInstance
form = request.form
device=None
print(form.get("path"))
try:
device = Device.load(form.get("path"))
except:
return jsonify(status="error")
cdmInstance = Cdm.from_device(device)
return jsonify(status="success")
@app.route("/getKeys",methods=["POST"])
def getKeys():
form = request.form
license_url = form.get("url")
headers= form.get("headers")
pssh= form.get("pssh")
pssh_value = PSSH(pssh)
cdm_session_id = cdmInstance.open()
challenge = cdmInstance.get_license_challenge(cdm_session_id, pssh_value)
licence = requests.post(
license_url, data=challenge
)
licence.raise_for_status()
cdmInstance.parse_license(cdm_session_id, licence.content)
keys = []
for key in cdmInstance.get_keys(cdm_session_id):
if "CONTENT" in key.type:
keys.append({
"kid":key.kid.hex,
"key":key.key.hex()
})
cdmInstance.close(cdm_session_id)
return jsonify(status="success",data=keys)
def shutdown():
if args.autoClose==0:
return
print('自动销毁')
os._exit(1)
@debounce(args.autoClose)
def closeServer():
shutdown()
@app.errorhandler(Exception)
def framework_error(e):
print(e)
return jsonify(status="error")
if __name__ == '__main__':
if args.port==None:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('localhost', 0))
args.port = sock.getsockname()[1]
sock.close()
closeServer()
app.run(host='0.0.0.0',port= args.port)
很简单,然后我们打包成exe,再写一下nodejs的库代码
const { default: axios } = require("axios");
const { spawn } = require("child_process");
const net = require("net");
const path = require('path')
const { exec } = require('child_process');
const querystring = require('querystring');
function sleep(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, time)
})
}
exports.openCDMServer = async function openCDMServer(option) {
let port = option.port
const wvdPath = option.wvdFullPath
if (port === undefined) {
port = await getPortFree()
}
const portOccupyStatus = await checkPortOccupy(port)
if (!portOccupyStatus) {
//no use!
exec(path.join(__filename, '../cdmServer.exe')+' --port '+port, (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return;
}
console.log(`stdout: ${stdout}`);
console.error(`stderr: ${stderr}`);
});
}
let serverOpen = false
const serverAddr = 'http://127.0.0.1:' + port
for (let index = 0; index < 60; index++) {
try {
const { data } = await axios.get(serverAddr + '/ping')
if (data?.status === 'success') {
serverOpen = true;
break;
}
} catch (error) {
await sleep(1000)
continue;
}
await sleep(1000)
}
if (!serverOpen) {
return {
status: 'error',
content: "server open failed!"
}
}
const timer = setInterval(async () => {
try {
const { data } = await axios.get(serverAddr + '/ping')
if (data?.status === 'success') {
serverOpen = true;
}
} catch (error) {
console.log('heart:the cdm server is loss')
}
}, 60 * 1000)
const closeFunc = () => {
clearInterval(timer)
axios.get(serverAddr + '/close')
}
let loadWvdStatus = false
try {
const { data } = await axios.post(serverAddr + '/loadDevice', querystring.stringify({
path: wvdPath
}))
if (data?.status === 'success') {
loadWvdStatus = true;
}
} catch (error) {
console.log('loadWvd Post Error')
}
if (!loadWvdStatus) {
closeFunc()
return {
content: "wvd load Error",
status: "error"
}
}
async function getKeys(url, pssh, headers) {
return axios.post(serverAddr + '/getKeys', querystring.stringify({
url,
pssh,
headers
}))
}
return {
close: closeFunc,
port: port,
status: "success",
getKeys: getKeys
}
}
function checkPortOccupy(port) {
return new Promise((resolve, reject) => {
const server = net.createConnection({ port });
server.on('connect', () => {
server.end();
resolve(true);
});
server.on('error', () => {
resolve(false);
});
});
}
async function getPortFree() {
return new Promise(res => {
const srv = net.createServer();
srv.listen(0, () => {
const port = srv.address().port
srv.close((err) => res(port))
});
})
}
由于我们还没上传到npm,这个时候需要通过本地软连接测试,修改package.json中的name属性
然后在库项目输入npm link
紧接着在测试的项目中输入 npm link 项目名
即可实现本地导入
接下来我们写一下测试代码,其中node-widevine-decrypt就是我软连接库的名字
const { openCDMServer } = require('node-widevine-decrypt')
const path = require('path')
async function main() {
const { getKeys, port, status } = await openCDMServer({
wvdFullPath: path.join(__filename, '../aosp.wvd')
})
if (status === 'success') {
const {data} =await getKeys("URL地址","pssh数据")
console.log(data)
} else {
console.log('server error')
}
}
main()
跑一下看看,发现成功解密~
![图片.png](data/attachment/forum/202406/27/160025iipa0q3r9a3gu96g.png)