import sys import re import json import time import random import struct import requests from base64 import b64encode, b64decode from urllib.parse import urlencode from urllib3 import disable_warnings from base.spider import Spider disable_warnings() class Spider(Spider): # ==================== 加密常量 ==================== DELTA = 0x9e3779b9 ROUNDS = 16 LOG_ROUNDS = 4 SALT_LEN = 2 ZERO_LEN = 7 TEA_CKEY = bytes.fromhex('59b2f7cf725ef43c34fdd7c123411ed3') GUARD_TEA_KEY = bytes.fromhex('110DBEC10C23E7D2E56A1CAD6914EF1B') XOR_KEY = [0x84, 0x2E, 0xED, 0x08, 0xF0, 0x66, 0xE6, 0xEA, 0x48, 0xB4, 0xCA, 0xA9, 0x91, 0xED, 0x6F, 0xF3] GUARD_XOR_KEY = [0xB3, 0xC9, 0x53, 0xA0, 0x69, 0x13, 0xAD, 0x4D] STANDARD_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' CUSTOM_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-=' # ==================== 频道图标 ==================== # 使用 wanglindl/TVlogo 小尺寸台标源(jsdelivr CDN,已验证可用) # 图标尺寸较小,适合TVBox/直播列表显示 LOGOS = { 'cctv1': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV1.png', 'cctv2': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV2.png', 'cctv3': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV3.png', 'cctv4': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV4.png', 'cctv5': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV5.png', 'cctv5p': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV5plus.png', 'cctv6': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV6.png', 'cctv7': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV7.png', 'cctv8': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV8.png', 'cctv9': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV9.png', 'cctv10': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV10.png', 'cctv11': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV11.png', 'cctv12': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV12.png', 'cctv13': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV13.png', 'cctv14': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV14.png', 'cctv15': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV15.png', 'cctv16': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV16.png', 'cctv164k': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV16.png', 'cctv17': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV17.png', 'cctv4k': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV4K.png', 'cctv8k': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTV8K.png', 'cgtn': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTN.png', 'cgtnfy': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNfy.png', 'cgtney': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNey.png', 'cgtnalby': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNalby.png', 'cgtnxby': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNxbyy.png', 'cgtnwyjl': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CGTNjilu.png', 'cctvfyjc': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVfyjc.png', 'cctvdyjc': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVdyjc.png', 'cctvhjjc': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVhjjc.png', 'cctvsjdl': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVsjdl.png', 'cctvfyyy': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVfyyy.png', 'cctvbqkj': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVbqkj.png', 'cctvfyzq': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVfyzq.png', 'cctvgeqwq': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVgefwq.png', 'cctvnxss': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVnxss.png', 'cctvyswhjp': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVyswhjp.png', 'cctvystq': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVystq.png', 'cctvdszn': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVdszn.png', 'cctvwsjk': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CCTVwsjk.png', 'bjws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Beijing.png', 'jsws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Jiangsu.png', 'dfws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Dongfang.png', 'zjws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Zhejiang.png', 'hnws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Hunan.png', 'hbws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Hubei.png', 'gdws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Guangdong.png', 'gxws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Guangxi.png', 'hljws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Heilongjiang.png', 'hnws2': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Hainan.png', 'cqws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Chongqing.png', 'szws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Shenzhen.png', 'scws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Sichuan.png', 'henanws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Henan.png', 'fjdnhz': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Dongnan.png', 'gzhws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Guizhou.png', 'jxws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Jiangxi.png', 'lnws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Liaoning.png', 'ahws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Anhui.png', 'hbws2': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Hebei.png', 'sdws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Shandong.png', 'tjws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Tianjin.png', 'jlws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Jilin.png', 'shanxiws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Shanxi.png', 'nxws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Ningxia.png', 'nmgws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Neimeng.png', 'ynws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Yunnan.png', 'shanxiws2': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Shanxi_.png', 'qhws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Qinghai.png', 'xzws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Xizang.png', 'xjws': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/Xinjiang.png', 'cetv1': 'https://cdn.jsdelivr.net/gh/wanglindl/TVlogo@main/img/CETV1.png', 'gxpd': '', } # ==================== 频道数据 ==================== # {key: [cnlid, livepid, defn, display_name]} CHANNELS = { 'cctv1': ['2024078201', '600001859', 'fhd', 'CCTV-1'], 'cctv2': ['2024075401', '600001800', 'fhd', 'CCTV-2'], 'cctv3': ['2024068501', '600001801', 'fhd', 'CCTV-3'], 'cctv4': ['2029797101', '600001814', 'fhd', 'CCTV-4'], 'cctv5': ['2024078401', '600001818', 'fhd', 'CCTV-5'], 'cctv5p': ['2024078001', '600001817', 'fhd', 'CCTV-5+'], 'cctv6': ['2013693901', '600108442', 'fhd', 'CCTV-6'], 'cctv7': ['2024072001', '600004092', 'fhd', 'CCTV-7'], 'cctv8': ['2029793001', '600001803', 'fhd', 'CCTV-8'], 'cctv9': ['2024078601', '600004078', 'fhd', 'CCTV-9'], 'cctv10': ['2024078701', '600001805', 'fhd', 'CCTV-10'], 'cctv11': ['2027248701', '600001806', 'fhd', 'CCTV-11'], 'cctv12': ['2027248801', '600001807', 'fhd', 'CCTV-12'], 'cctv13': ['2029797201', '600001811', 'fhd', 'CCTV-13'], 'cctv14': ['2027248901', '600001809', 'fhd', 'CCTV-14'], 'cctv15': ['2027249001', '600001815', 'fhd', 'CCTV-15'], 'cctv16': ['2027249101', '600098637', 'fhd', 'CCTV-16'], 'cctv164k': ['2027249301', '600099502', 'fhd', 'CCTV-16(4K)'], 'cctv17': ['2027249401', '600001810', 'fhd', 'CCTV-17'], 'cctv4k': ['2029810301', '600002264', 'fhd', 'CCTV-4K'], 'cctv8k': ['2026774101', '600156816', 'fhd', 'CCTV-8K'], 'cgtn': ['2024181701', '600014550', 'fhd', 'CGTN'], 'cgtnfy': ['2024181801', '600084704', 'fhd', 'CGTN法语'], 'cgtney': ['2024181901', '600084758', 'fhd', 'CGTN俄语'], 'cgtnalby': ['2024182001', '600084782', 'fhd', 'CGTN阿拉伯语'], 'cgtnxby': ['2024182101', '600084744', 'fhd', 'CGTN西班牙语'], 'cgtnwyjl': ['2024182301', '600084781', 'fhd', 'CGTN外语纪录'], 'cctvfyjc': ['2025637103', '600099658', 'shd', '风云剧场'], 'cctvdyjc': ['2026874203', '600099655', 'shd', '第一剧场'], 'cctvhjjc': ['2026874303', '600099620', 'shd', '怀旧剧场'], 'cctvsjdl': ['2026874403', '600099637', 'shd', '世界地理'], 'cctvfyyy': ['2026874503', '600099660', 'shd', '风云音乐'], 'cctvbqkj': ['2026874603', '600099649', 'shd', '兵器科技'], 'cctvfyzq': ['2026966203', '600099636', 'shd', '风云足球'], 'cctvgeqwq': ['2026874703', '600099659', 'shd', '高尔夫·网球'], 'cctvnxss': ['2026874803', '600099650', 'shd', '女性时尚'], 'cctvyswhjp': ['2026874903', '600099653', 'shd', '央视文化精品'], 'cctvystq': ['2026875003', '600099652', 'shd', '央视台球'], 'cctvdszn': ['2026875103', '600099656', 'shd', '电视指南'], 'cctvwsjk': ['2025637003', '600099651', 'shd', '卫生健康'], 'bjws': ['2024052703', '600002309', 'fhd', '北京卫视'], 'jsws': ['2024171103', '600002521', 'fhd', '江苏卫视'], 'dfws': ['2024054503', '600002483', 'fhd', '东方卫视'], 'zjws': ['2024054703', '600002520', 'fhd', '浙江卫视'], 'hnws': ['2024054803', '600002475', 'fhd', '湖南卫视'], 'hbws': ['2024171203', '600002508', 'fhd', '湖北卫视'], 'gdws': ['2024060903', '600002485', 'fhd', '广东卫视'], 'gxws': ['2024060703', '600002509', 'fhd', '广西卫视'], 'hljws': ['2029797003', '600002498', 'fhd', '黑龙江卫视'], 'hnws2': ['2024055603', '600002506', 'fhd', '海南卫视'], 'cqws': ['2024061103', '600002531', 'fhd', '重庆卫视'], 'szws': ['2024061303', '600002481', 'fhd', '深圳卫视'], 'scws': ['2024061403', '600002516', 'fhd', '四川卫视'], 'henanws': ['2029797303', '600002525', 'fhd', '河南卫视'], 'fjdnhz': ['2024061503', '600002484', 'fhd', '福建东南卫视'], 'gzhws': ['2024061603', '600002490', 'fhd', '贵州卫视'], 'jxws': ['2024061703', '600002503', 'fhd', '江西卫视'], 'lnws': ['2024171303', '600002505', 'fhd', '辽宁卫视'], 'ahws': ['2024171403', '600002532', 'fhd', '安徽卫视'], 'hbws2': ['2024171503', '600002493', 'fhd', '河北卫视'], 'sdws': ['2029787903', '600002513', 'fhd', '山东卫视'], 'tjws': ['2019927003', '600152137', 'fhd', '天津卫视'], 'jlws': ['2025561503', '600190405', 'fhd', '吉林卫视'], 'shanxiws': ['2029795103', '600190400', 'fhd', '陕西卫视'], 'nxws': ['2025608503', '600190737', 'fhd', '宁夏卫视'], 'nmgws': ['2025561203', '600190401', 'fhd', '内蒙古卫视'], 'ynws': ['2025561303', '600190402', 'fhd', '云南卫视'], 'shanxiws2': ['2025560803', '600190407', 'fhd', '山西卫视'], 'qhws': ['2025559103', '600190406', 'fhd', '青海卫视'], 'xzws': ['2025558003', '600190403', 'fhd', '西藏卫视'], 'xjws': ['2019927403', '600152138', 'fhd', '新疆卫视'], 'cetv1': ['2022823801', '600171827', 'fhd', '中国教育电视台'], 'gxpd': ['2029360403', '600213139', 'fhd', '国学频道'], } # ==================== 分类定义 ==================== CATS = [ { 'name': '央视', 'ids': [ 'cctv1', 'cctv2', 'cctv3', 'cctv4', 'cctv5', 'cctv5p', 'cctv6', 'cctv7', 'cctv8', 'cctv9', 'cctv10', 'cctv11', 'cctv12', 'cctv13', 'cctv14', 'cctv15', 'cctv16', 'cctv164k', 'cctv17', 'cctv4k', 'cctv8k', ] }, { 'name': 'CGTN', 'ids': ['cgtn', 'cgtnfy', 'cgtney', 'cgtnalby', 'cgtnxby', 'cgtnwyjl'] }, { 'name': '央视付费', 'ids': [ 'cctvfyjc', 'cctvdyjc', 'cctvhjjc', 'cctvsjdl', 'cctvfyyy', 'cctvbqkj', 'cctvfyzq', 'cctvgeqwq', 'cctvnxss', 'cctvyswhjp', 'cctvystq', 'cctvdszn', 'cctvwsjk', ] }, { 'name': '卫视', 'ids': [ 'bjws', 'jsws', 'dfws', 'zjws', 'hnws', 'hbws', 'gdws', 'gxws', 'hljws', 'hnws2', 'cqws', 'szws', 'scws', 'henanws', 'fjdnhz', 'gzhws', 'jxws', 'lnws', 'ahws', 'hbws2', 'sdws', 'tjws', 'jlws', 'shanxiws', 'nxws', 'nmgws', 'ynws', 'shanxiws2', 'qhws', 'xzws', 'xjws', ] }, { 'name': '其他', 'ids': ['cetv1', 'gxpd'] }, ] # ==================== 初始化 ==================== def init(self, extend=""): self.guid = self._gen_guid() self.session = requests.Session() self.session.verify = False self.session.headers.update({ 'User-Agent': 'qqlive', 'Connection': 'Keep-Alive', 'Accept': 'application/json', }) def getName(self): return "央视频" # ==================== 蜘蛛壳接口 ==================== def homeContent(self, filter): classes = [ {'type_id': str(i), 'type_name': c['name']} for i, c in enumerate(self.CATS) ] return {'class': classes} def categoryContent(self, tid, pg, filter, extend): cat = self.CATS[int(tid)] vod_list = [] for cid in cat['ids']: ch = self.CHANNELS.get(cid) if ch: vod_list.append({ 'vod_id': cid, 'vod_name': ch[3], 'vod_pic': self.LOGOS.get(cid, ''), 'vod_remarks': ch[2].upper(), }) return { 'page': 1, 'pagecount': 1, 'limit': len(vod_list), 'total': len(vod_list), 'list': vod_list, } def detailContent(self, ids): cid = ids[0] ch = self.CHANNELS.get(cid) if not ch: return {'list': []} vod = { 'vod_id': cid, 'vod_name': ch[3], 'vod_pic': self.LOGOS.get(cid, ''), 'vod_play_from': '央视频', 'vod_play_url': f'直播${cid}', 'vod_content': f'{ch[3]} 高清直播', } return {'list': [vod]} def playerContent(self, flag, id, vipFlags): ch = self.CHANNELS.get(id) if not ch: return {'parse': 0, 'url': '', 'header': ''} play_url = self._get_play_url(ch[0], ch[1], ch[2]) if play_url: return {'parse': 0, 'url': play_url, 'header': ''} return {'parse': 0, 'url': '', 'header': ''} # ==================== 播放地址获取 ==================== def _get_play_url(self, cnlid, livepid, defn): try: self.guid = self._gen_guid() ck = self._gen_ckey(cnlid) flowid = self._gen_flowid() params = { "atime": "120", "livepid": livepid, "cnlid": cnlid, "appVer": "V8.22.1035.3031", "app_version": "300090", "caplv": "1", "cmd": "2", "defn": defn, "device": "iPhone", "encryptVer": "4.2", "getpreviewinfo": "0", "hevclv": "33", "lang": "zh-Hans_JP", "livequeue": "0", "logintype": "1", "nettype": "1", "newnettype": "1", "newplatform": "4330403", "platform": "4330403", "sdtfrom": "v3021", "spacode": "23", "spaudio": "1", "spdemuxer": "6", "spdrm": "2", "spdynamicrange": "7", "spflv": "1", "spflvaudio": "1", "sphdrfps": "60", "sphttps": "0", "spvcode": "MSgzMDoyMTYwLDYwOjIxNjB8MzA6MjE2MCw2MDoyMTYwKTsyKDMwOjIxNjAsNjA6MjE2MHwzMDoyMTYwLDYwOjIxNjAp", "spvideo": "4", "stream": "1", "system": "1", "sysver": "ios18.2.1", "uhd_flag": "4", "cKey": ck['ckey'], "guid": self.guid, "fntick": ck['params']['Timestamp'], "flowid": flowid, "playbacktime": "0", } resp = self.session.get( "https://bkliveinfo.ysp.cctv.cn", params=params, timeout=15, ) data = resp.json() if data.get('iretcode') == 0 and data.get('playurl'): return data['playurl'] except Exception as e: print(f"[央视频] 获取播放地址失败: {e}") return None # ==================== GUID / FlowID ==================== def _gen_guid(self): return '{:08x}{:04x}{:04x}{:04x}{:012x}'.format( random.randint(0, 0xffffffff), random.randint(0, 0xffff), random.randint(0, 0xffff), random.randint(0, 0xffff), random.randint(0, 0xffffffffffff), ) def _gen_flowid(self): p = [ random.randint(0, 0xffff), random.randint(0, 0xffff), random.randint(0, 0xffff), random.randint(0, 0x0fff) | 0x4000, random.randint(0, 0x3fff) | 0x8000, random.randint(0, 0xffff), random.randint(0, 0xffff), random.randint(0, 0xffff), ] return '{:04X}{:04X}-{:04X}-{:04X}-{:04X}-{:04X}{:04X}{:04X}_4330403'.format(*p) # ==================== 签名 ==================== def _calc_sig(self, buf): s = 0 for b in buf: s = (0x83 * s + (b & 0xFF)) & 0x7FFFFFFF return s # ==================== 自定义 Base64 ==================== def _b64enc(self, data): enc = b64encode(data).decode() return enc.translate(str.maketrans(self.STANDARD_ALPHABET, self.CUSTOM_ALPHABET)).rstrip('=') def _b64dec(self, text): text = text.rstrip('=') if len(text) % 4: text += '=' * (4 - len(text) % 4) return b64decode(text.translate(str.maketrans(self.CUSTOM_ALPHABET, self.STANDARD_ALPHABET))) # ==================== XOR ==================== def _xor(self, arr): return [arr[i] ^ self.XOR_KEY[i & 0xF] for i in range(len(arr))] # ==================== TEA ==================== def _tea_enc(self, data, key): if len(data) < 8: data = data.ljust(8, b'\0') y, z = struct.unpack('>II', data[:8]) k = struct.unpack('>4I', key[:16]) s = 0 for _ in range(self.ROUNDS): s = (s + self.DELTA) & 0xFFFFFFFF y = (y + (((z << 4) + k[0]) ^ (z + s) ^ ((z >> 5) + k[1]))) & 0xFFFFFFFF z = (z + (((y << 4) + k[2]) ^ (y + s) ^ ((y >> 5) + k[3]))) & 0xFFFFFFFF return struct.pack('>II', y, z) def _tea_dec(self, data, key): y, z = struct.unpack('>II', data[:8]) k = struct.unpack('>4I', key[:16]) s = (self.DELTA << self.LOG_ROUNDS) & 0xFFFFFFFF for _ in range(self.ROUNDS): z = (z - (((y << 4) + k[2]) ^ (y + s) ^ ((y >> 5) + k[3]))) & 0xFFFFFFFF y = (y - (((z << 4) + k[0]) ^ (z + s) ^ ((z >> 5) + k[1]))) & 0xFFFFFFFF s = (s - self.DELTA) & 0xFFFFFFFF return struct.pack('>II', y, z) # ==================== CBC 加密 ==================== def _cbc_enc(self, p_in, n_len, p_key): pad_salt_zero = n_len + 1 + self.SALT_LEN + self.ZERO_LEN n_pad = pad_salt_zero % 8 if n_pad: n_pad = 8 - n_pad out = b'' src = [0] * 8 src[0] = (random.randint(0, 255) & 0xF8) | n_pad si = 1 while n_pad: src[si] = random.randint(0, 255) si += 1 n_pad -= 1 iv_p = [0] * 8 iv_c = [0] * 8 # salt i = 0 while i < self.SALT_LEN: if si < 8: src[si] = random.randint(0, 255) si += 1 i += 1 if si == 8: for j in range(8): src[j] ^= iv_c[j] tb = list(self._tea_enc(bytes(src), p_key)) for j in range(8): tb[j] ^= iv_p[j] iv_p = list(src) iv_c = list(tb) out += bytes(tb) si = 0 # body pi = 0 while n_len: if si < 8: src[si] = p_in[pi] pi += 1 si += 1 n_len -= 1 if si == 8: for j in range(8): src[j] ^= iv_c[j] tb = list(self._tea_enc(bytes(src), p_key)) for j in range(8): tb[j] ^= iv_p[j] iv_p = list(src) iv_c = list(tb) out += bytes(tb) si = 0 # zero i = 0 while i < self.ZERO_LEN: if si < 8: src[si] = 0 si += 1 i += 1 if si == 8: for j in range(8): src[j] ^= iv_c[j] tb = list(self._tea_enc(bytes(src), p_key)) for j in range(8): tb[j] ^= iv_p[j] iv_p = list(src) iv_c = list(tb) out += bytes(tb) si = 0 # last if si > 0: for j in range(si, 8): src[j] = 0 for j in range(8): src[j] ^= iv_c[j] tb = list(self._tea_enc(bytes(src), p_key)) for j in range(8): tb[j] ^= iv_p[j] out += bytes(tb) return out # ==================== CBC 解密 ==================== def _cbc_dec(self, p_in, n_len, p_key): if n_len % 8 != 0 or n_len < 16: return None dest = list(self._tea_dec(p_in[:8], p_key)) n_pad = dest[0] & 0x07 out_len = n_len - 1 - n_pad - self.SALT_LEN - self.ZERO_LEN if out_len < 0: return None iv_pre = [0] * 8 iv_cur = list(p_in[:8]) off = 8 di = 1 + n_pad # skip salt sc = 1 while sc <= self.SALT_LEN: if di < 8: di += 1 sc += 1 elif di == 8: iv_pre = list(iv_cur) if off + 8 > n_len: return None iv_cur = list(p_in[off:off + 8]) for j in range(8): dest[j] ^= iv_cur[j] dest = list(self._tea_dec(bytes(dest), p_key)) off += 8 di = 0 # copy plain plain = [] rem = out_len while rem > 0: if di < 8: plain.append(dest[di] ^ iv_pre[di]) di += 1 rem -= 1 elif di == 8: iv_pre = list(iv_cur) if off + 8 > n_len: return None iv_cur = list(p_in[off:off + 8]) for j in range(8): dest[j] ^= iv_cur[j] dest = list(self._tea_dec(bytes(dest), p_key)) off += 8 di = 0 return bytes(plain) # ==================== Guard Time ==================== def _last5(self, v): v = str(v) return v[-5:] if len(v) >= 5 else '' def _gen_guard_time(self, ts, guid): body = struct.pack('>I', ts) for part in [self._last5(guid), self._last5('null'), self._last5('null'), '-1']: pb = part.encode() body += struct.pack('>H', len(pb)) + pb plain = struct.pack('>H', len(body)) + body chk = self._calc_sig(list(plain)) enc = self._cbc_enc(plain, len(plain), self.GUARD_TEA_KEY) + struct.pack('>I', chk) el = list(enc) for i in range(len(el)): el[i] ^= self.GUARD_XOR_KEY[i & 7] return bytes(el).hex().upper() # ==================== CKey ==================== def _encrypt_ckey(self, data): chk = self._calc_sig(list(data)) enc = self._cbc_enc(data, len(data), self.TEA_CKEY) + struct.pack('>I', chk) return '--01' + self._b64enc(bytes(self._xor(list(enc)))) def _build_pkt(self, params): d = b'' d += bytes.fromhex('0000004200000004000004d2') # 12-byte header d += struct.pack('>I', params['Platform']) d += struct.pack('>I', 0) # sig placeholder d += struct.pack('>I', params['Timestamp']) for k in ['Sdtfrom', 'randFlag', 'appVer', 'vid', 'guid']: v = params[k].encode() d += struct.pack('>H', len(v)) + v d += struct.pack('>I', 1) # part1 d += struct.pack('>I', 1) # isDlna for v in [b'2622783A', b'nil']: d += struct.pack('>H', len(v)) + v uuid4 = params['uuid4'].encode() d += struct.pack('>H', len(uuid4)) + uuid4 d += struct.pack('>H', 3) + b'nil' # bundleID1 for v in [b'v0.1.000', b'com.cctv.yangshipin.app.iphone', b'4330403', b'ex_json_bus', b'ex_json_vs']: d += struct.pack('>H', len(v)) + v cgt = params['ck_guard_time'].encode() d += struct.pack('>H', len(cgt)) + cgt buf = struct.pack('>H', len(d)) + d sig = self._calc_sig(list(buf)) return buf[:18] + struct.pack('>I', sig) + buf[22:] def _gen_ckey(self, cnlid): ts = int(time.time()) cgt = self._gen_guard_time(ts, self.guid) params = { 'Platform': 4330403, 'Timestamp': ts, 'Sdtfrom': 'dcgh', 'vid': cnlid, 'guid': self.guid, 'appVer': 'V8.22.1035.3031', 'randFlag': '_zj1A5Gh6QYcxWjIUGos2w==', 'uuid4': '57eab0c4-2c58-44c6-8ae9-dd2757525dc5', 'ck_guard_time': cgt, } pkt = self._build_pkt(params) return {'ckey': self._encrypt_ckey(pkt), 'params': params}