李恒道 发表于 2024-6-27 16:00:27

onlyfans的OB解密及DRM过校验思路(六)

之前我们已经实现了python的DRM视频解密
但是我是nodejs,因为之前已经写了大量的爬虫代码
并且由于js目前没找到cdm的解密库
所以干脆考虑自己封一下
我决定把之前cdm解密的python代码抽象一下
并且引入flask,通过pyinstaller打包成exe
然后封装一个nodejs的库唤起,本地服务器如果一定时间没有心跳就自动销毁
首先封装一下python的flask代码
```go
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.cancel()
            except:
                pass

            caller = Timer(wait, call_it, )
            caller.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()
      sock.close()
    closeServer()
    app.run(host='0.0.0.0',port= args.port)

```
很简单,然后我们打包成exe,再写一下nodejs的库代码
```js
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)
页: [1]
查看完整版本: onlyfans的OB解密及DRM过校验思路(六)