| 1 | # ============================================================================== |
| 2 | # Copyright (C) 2021 Evil0ctal |
| 3 | # |
| 4 | # This file is part of the Douyin_TikTok_Download_API project. |
| 5 | # |
| 6 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 7 | # you may not use this file except in compliance with the License. |
| 8 | # You may obtain a copy of the License at |
| 9 | # |
| 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | # |
| 12 | # Unless required by applicable law or agreed to in writing, software |
| 13 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 15 | # See the License for the specific language governing permissions and |
| 16 | # limitations under the License. |
| 17 | # ============================================================================== |
| 18 | |
| 19 | import base64 |
| 20 | import hashlib |
| 21 | import time |
| 22 | from typing import List, Optional, Tuple, Union |
| 23 | |
| 24 | |
| 25 | class XBogus: |
| 26 | def __init__(self, user_agent: Optional[str] = None) -> None: |
| 27 | # fmt: off |
| 28 | self._array = [ |
| 29 | None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, |
| 30 | None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, |
| 31 | None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, |
| 32 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, None, None, None, None, None, None, None, None, None, None, None, |
| 33 | None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, |
| 34 | None, None, None, None, None, None, None, None, None, None, None, None, 10, 11, 12, 13, 14, 15 |
| 35 | ] |
| 36 | self._character = "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe=" |
| 37 | # fmt: on |
| 38 | self._ua_key = b"\x00\x01\x0c" |
| 39 | self._user_agent = ( |
| 40 | user_agent |
| 41 | if user_agent |
| 42 | else ( |
| 43 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " |
| 44 | "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36" |
| 45 | ) |
| 46 | ) |
| 47 | |
| 48 | @property |
| 49 | def user_agent(self) -> str: |
| 50 | return self._user_agent |
| 51 | |
| 52 | def _md5_str_to_array(self, md5_str: str) -> List[int]: |
| 53 | if isinstance(md5_str, str) and len(md5_str) > 32: |
| 54 | return [ord(char) for char in md5_str] |
| 55 | |
| 56 | array: List[int] = [] |
| 57 | idx = 0 |
| 58 | while idx < len(md5_str): |
| 59 | array.append((self._array[ord(md5_str[idx])] << 4) | self._array[ord(md5_str[idx + 1])]) |
| 60 | idx += 2 |
| 61 | return array |
| 62 | |
| 63 | def _md5(self, input_data: Union[str, List[int]]) -> str: |
| 64 | if isinstance(input_data, str): |
| 65 | data = self._md5_str_to_array(input_data) |
| 66 | else: |
| 67 | data = input_data |
| 68 | md5_hash = hashlib.md5() |
| 69 | md5_hash.update(bytes(data)) |
| 70 | return md5_hash.hexdigest() |
| 71 | |
| 72 | def _md5_encrypt(self, url_path: str) -> List[int]: |
| 73 | hashed = self._md5(self._md5_str_to_array(self._md5(url_path))) |
| 74 | return self._md5_str_to_array(hashed) |
| 75 | |
| 76 | def _encoding_conversion( |
| 77 | self, |
| 78 | a, |
| 79 | b, |
| 80 | c, |
| 81 | e, |
| 82 | d, |
| 83 | t, |
| 84 | f, |
| 85 | r, |
| 86 | n, |
| 87 | o, |
| 88 | i, |
| 89 | _, |
| 90 | x, |
| 91 | u, |
| 92 | s, |
| 93 | l, # noqa: E741 — single-letter params mirror the upstream JS xbogus impl |
| 94 | v, |
| 95 | h, |
| 96 | p, |
| 97 | ) -> str: |
| 98 | payload = [a] |
| 99 | payload.append(int(i)) |
| 100 | payload.extend([b, _, c, x, e, u, d, s, t, l, f, v, r, h, n, p, o]) |
| 101 | return bytes(payload).decode("ISO-8859-1") |
| 102 | |
| 103 | def _encoding_conversion2(self, a: int, b: int, c: str) -> str: |
| 104 | return chr(a) + chr(b) + c |
| 105 | |
| 106 | @staticmethod |
| 107 | def _rc4_encrypt(key: bytes, data: bytes) -> bytearray: |
| 108 | s = list(range(256)) |
| 109 | j = 0 |
| 110 | encrypted = bytearray() |
| 111 | |
| 112 | for i in range(256): |
| 113 | j = (j + s[i] + key[i % len(key)]) % 256 |
| 114 | s[i], s[j] = s[j], s[i] |
| 115 | |
| 116 | i = j = 0 |
| 117 | for byte in data: |
| 118 | i = (i + 1) % 256 |
| 119 | j = (j + s[i]) % 256 |
| 120 | s[i], s[j] = s[j], s[i] |
| 121 | encrypted.append(byte ^ s[(s[i] + s[j]) % 256]) |
| 122 | |
| 123 | return encrypted |
| 124 | |
| 125 | def _calculation(self, a1: int, a2: int, a3: int) -> str: |
| 126 | x3 = ((a1 & 255) << 16) | ((a2 & 255) << 8) | (a3 & 255) |
| 127 | return ( |
| 128 | self._character[(x3 & 16515072) >> 18] |
| 129 | + self._character[(x3 & 258048) >> 12] |
| 130 | + self._character[(x3 & 4032) >> 6] |
| 131 | + self._character[x3 & 63] |
| 132 | ) |
| 133 | |
| 134 | def build(self, url: str) -> Tuple[str, str, str]: |
| 135 | ua_md5_array = self._md5_str_to_array( |
| 136 | self._md5( |
| 137 | base64.b64encode( |
| 138 | self._rc4_encrypt(self._ua_key, self._user_agent.encode("ISO-8859-1")) |
| 139 | ).decode("ISO-8859-1") |
| 140 | ) |
| 141 | ) |
| 142 | |
| 143 | empty_md5_array = self._md5_str_to_array( |
| 144 | self._md5(self._md5_str_to_array("d41d8cd98f00b204e9800998ecf8427e")) |
| 145 | ) |
| 146 | url_md5_array = self._md5_encrypt(url) |
| 147 | |
| 148 | timer = int(time.time()) |
| 149 | ct = 536919696 |
| 150 | |
| 151 | new_array = [ |
| 152 | 64, |
| 153 | 0.00390625, |
| 154 | 1, |
| 155 | 12, |
| 156 | url_md5_array[14], |
| 157 | url_md5_array[15], |
| 158 | empty_md5_array[14], |
| 159 | empty_md5_array[15], |
| 160 | ua_md5_array[14], |
| 161 | ua_md5_array[15], |
| 162 | timer >> 24 & 255, |
| 163 | timer >> 16 & 255, |
| 164 | timer >> 8 & 255, |
| 165 | timer & 255, |
| 166 | ct >> 24 & 255, |
| 167 | ct >> 16 & 255, |
| 168 | ct >> 8 & 255, |
| 169 | ct & 255, |
| 170 | ] |
| 171 | |
| 172 | xor_result = new_array[0] |
| 173 | for value in new_array[1:]: |
| 174 | if isinstance(value, float): |
| 175 | value = int(value) |
| 176 | xor_result ^= value |
| 177 | new_array.append(xor_result) |
| 178 | |
| 179 | array3: list[int] = [] |
| 180 | array4: list[int] = [] |
| 181 | idx = 0 |
| 182 | while idx < len(new_array): |
| 183 | value = new_array[idx] |
| 184 | array3.append(value) |
| 185 | if idx + 1 < len(new_array): |
| 186 | array4.append(new_array[idx + 1]) |
| 187 | idx += 2 |
| 188 | |
| 189 | merged = array3 + array4 |
| 190 | |
| 191 | garbled = self._encoding_conversion2( |
| 192 | 2, |
| 193 | 255, |
| 194 | self._rc4_encrypt( |
| 195 | "ÿ".encode("ISO-8859-1"), |
| 196 | self._encoding_conversion(*merged).encode("ISO-8859-1"), |
| 197 | ).decode("ISO-8859-1"), |
| 198 | ) |
| 199 | |
| 200 | xb = "" |
| 201 | idx = 0 |
| 202 | while idx < len(garbled): |
| 203 | xb += self._calculation( |
| 204 | ord(garbled[idx]), |
| 205 | ord(garbled[idx + 1]), |
| 206 | ord(garbled[idx + 2]), |
| 207 | ) |
| 208 | idx += 3 |
| 209 | |
| 210 | signed_url = f"{url}&X-Bogus={xb}" |
| 211 | return signed_url, xb, self._user_agent |
| 212 | |
| 213 | |
| 214 | def generate_x_bogus(url: str, user_agent: Optional[str] = None) -> Tuple[str, str, str]: |
| 215 | signer = XBogus(user_agent=user_agent) |
| 216 | return signer.build(url) |
| 217 |