File size: 12,409 Bytes
494c89b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
"""
CDP Collector - глубокий перехват через Chrome DevTools Protocol

Использует CDP Network domain для надёжного перехвата ВСЕХ запросов,
включая те что происходят при навигации (JS interceptor их теряет).
"""

import json
import time
import threading
from typing import Dict, List, Any, Optional
from .base import BaseCollector


class CDPCollector(BaseCollector):
    """
    Использует CDP для глубокого перехвата:
    - Все сетевые запросы через Network domain
    - WebSocket сообщения
    - Навигация
    - JavaScript exceptions
    - Console messages
    """
    
    name = "cdp"
    
    def __init__(self, session):
        super().__init__(session)
        self._requests: Dict[str, Dict] = {}  # requestId -> request data
        self._completed_requests: List[Dict] = []
        self._navigation_events: List[Dict] = []
        self._js_exceptions: List[Dict] = []
        self._websocket_messages: List[Dict] = []
        self._console_messages: List[Dict] = []
        self._polling = False
        self._poll_thread: Optional[threading.Thread] = None
    
    def inject(self):
        """Включает CDP domains и запускает polling"""
        if not self.page:
            return
        
        try:
            # Включаем Network domain с большим буфером
            self.page.run_cdp('Network.enable', 
                maxTotalBufferSize=100*1024*1024,
                maxResourceBufferSize=10*1024*1024
            )
            
            # Включаем Page domain для навигации
            self.page.run_cdp('Page.enable')
            
            # Включаем Runtime для JS exceptions
            self.page.run_cdp('Runtime.enable')
            
            # Включаем Log domain для console
            try:
                self.page.run_cdp('Log.enable')
            except:
                pass
            
            self.log("CDP domains enabled (Network, Page, Runtime, Log)")
            
            # Инжектим JS для сбора ошибок
            self._inject_error_collector()
            
        except Exception as e:
            self.log(f"CDP enable failed: {e}")
    
    def _inject_error_collector(self):
        """Инжектит JS для сбора ошибок и console"""
        js = '''
(function() {
    if (window.__cdpErrorCollector) return;
    window.__cdpErrorCollector = true;
    window.__jsErrors = [];
    window.__consoleLog = [];
    
    // Перехват ошибок
    window.addEventListener('error', function(e) {
        window.__jsErrors.push({
            type: 'error',
            message: e.message,
            filename: e.filename,
            lineno: e.lineno,
            colno: e.colno,
            timestamp: Date.now()
        });
    });
    
    window.addEventListener('unhandledrejection', function(e) {
        window.__jsErrors.push({
            type: 'unhandledrejection',
            message: String(e.reason),
            timestamp: Date.now()
        });
    });
    
    // Перехват console
    ['log', 'warn', 'error', 'info', 'debug'].forEach(function(level) {
        const orig = console[level];
        console[level] = function() {
            window.__consoleLog.push({
                level: level,
                args: Array.from(arguments).map(a => {
                    try { return JSON.stringify(a); } 
                    catch(e) { return String(a); }
                }),
                timestamp: Date.now()
            });
            return orig.apply(console, arguments);
        };
    });
})();
'''
        try:
            self.page.run_js(js)
            self.page.run_cdp('Page.addScriptToEvaluateOnNewDocument', source=js)
        except:
            pass
    
    def collect(self) -> List[Dict]:
        """Собирает данные через CDP и Performance API"""
        if not self.page:
            return []
        
        collected = []
        
        # 1. Собираем через Performance API (resource timing)
        try:
            perf_entries = self.page.run_js('''
                const entries = performance.getEntriesByType('resource');
                const result = entries.map(e => ({
                    name: e.name,
                    type: e.initiatorType,
                    startTime: e.startTime,
                    duration: e.duration,
                    transferSize: e.transferSize || 0,
                    encodedBodySize: e.encodedBodySize || 0,
                    decodedBodySize: e.decodedBodySize || 0,
                    responseStatus: e.responseStatus || 0,
                    nextHopProtocol: e.nextHopProtocol || '',
                    serverTiming: e.serverTiming ? e.serverTiming.map(s => ({name: s.name, duration: s.duration})) : []
                }));
                performance.clearResourceTimings();
                return result;
            ''') or []
            
            for entry in perf_entries:
                url = entry.get('name', '')
                req_data = {
                    'source': 'performance',
                    'url': url,
                    'type': entry.get('type', ''),
                    'duration': round(entry.get('duration', 0), 2),
                    'size': entry.get('transferSize', 0),
                    'status': entry.get('responseStatus', 0),
                    'protocol': entry.get('nextHopProtocol', ''),
                    'timestamp': self.session._elapsed(),
                }
                collected.append(req_data)
                
                # Добавляем в общий список сессии
                self.session.all_requests.append(req_data)
                
                # Логируем важные запросы
                if any(x in url for x in ['api/', 'signin', 'oauth', 'token', 'fingerprint', 'fwcim', 'shortbread']):
                    self.log(f"[PERF] {url[:60]}... ({entry.get('duration', 0):.0f}ms)")
            
        except Exception as e:
            pass  # Performance API может быть недоступен
        
        # 2. Собираем navigation timing
        try:
            nav_timing = self.page.run_js('''
                const t = performance.timing;
                if (!t.navigationStart) return null;
                return {
                    navigationStart: t.navigationStart,
                    domContentLoaded: t.domContentLoadedEventEnd - t.navigationStart,
                    loadComplete: t.loadEventEnd - t.navigationStart,
                    domInteractive: t.domInteractive - t.navigationStart,
                    responseEnd: t.responseEnd - t.navigationStart,
                    redirectTime: t.redirectEnd - t.redirectStart,
                    dnsTime: t.domainLookupEnd - t.domainLookupStart,
                    connectTime: t.connectEnd - t.connectStart,
                    ttfb: t.responseStart - t.navigationStart,
                };
            ''')
            
            if nav_timing and nav_timing.get('navigationStart'):
                self._navigation_events.append({
                    'timestamp': self.session._elapsed(),
                    'url': self.page.url,
                    'timing': nav_timing,
                })
                
        except:
            pass
        
        # 3. Собираем JS errors
        try:
            result = self.page.run_js('''
                const errors = window.__jsErrors || [];
                window.__jsErrors = [];
                return errors;
            ''') or []
            
            for err in result:
                self._js_exceptions.append(err)
                msg = err.get('message', '')[:80]
                self.log(f"[JS ERROR] {err.get('type')}: {msg}")
                
        except:
            pass
        
        # 4. Собираем console messages
        try:
            console_msgs = self.page.run_js('''
                const logs = window.__consoleLog || [];
                window.__consoleLog = [];
                return logs;
            ''') or []
            
            for msg in console_msgs:
                self._console_messages.append(msg)
                # Логируем только warnings и errors
                if msg.get('level') in ('warn', 'error'):
                    args = ' '.join(msg.get('args', []))[:60]
                    self.log(f"[CONSOLE {msg.get('level').upper()}] {args}")
                    
        except:
            pass
        
        return collected
    
    def get_all_cookies_cdp(self) -> List[Dict]:
        """Получает все cookies через CDP (более полно чем JS)"""
        if not self.page:
            return []
        
        try:
            result = self.page.run_cdp('Network.getAllCookies')
            return result.get('cookies', [])
        except:
            return []
    
    def get_request_body(self, request_id: str) -> str:
        """Получает body запроса через CDP"""
        if not self.page:
            return ""
        
        try:
            result = self.page.run_cdp('Network.getRequestPostData', requestId=request_id)
            return result.get('postData', '')
        except:
            return ""
    
    def get_response_body(self, request_id: str) -> str:
        """Получает body ответа через CDP"""
        if not self.page:
            return ""
        
        try:
            result = self.page.run_cdp('Network.getResponseBody', requestId=request_id)
            body = result.get('body', '')
            if result.get('base64Encoded'):
                import base64
                body = base64.b64decode(body).decode('utf-8', errors='ignore')
            return body
        except:
            return ""
    
    def capture_screenshot(self, filename: str = None) -> str:
        """Делает скриншот через CDP"""
        if not self.page:
            return ""
        
        try:
            result = self.page.run_cdp('Page.captureScreenshot', format='png')
            data = result.get('data', '')
            
            if data and filename:
                import base64
                from pathlib import Path
                
                path = self.session.session_dir / filename
                path.write_bytes(base64.b64decode(data))
                self.log(f"Screenshot: {filename}")
                return str(path)
            
            return data
            
        except Exception as e:
            self.log(f"Screenshot failed: {e}")
            return ""
    
    def get_dom_snapshot(self) -> Dict:
        """Получает snapshot DOM через CDP"""
        if not self.page:
            return {}
        
        try:
            result = self.page.run_cdp('DOMSnapshot.captureSnapshot',
                computedStyles=[],
                includeDOMRects=False,
                includePaintOrder=False
            )
            return result
        except:
            return {}
    
    def get_js_errors(self) -> List[Dict]:
        """Возвращает собранные JS ошибки"""
        return self._js_exceptions.copy()
    
    def get_console_messages(self) -> List[Dict]:
        """Возвращает собранные console messages"""
        return self._console_messages.copy()
    
    def get_navigation_events(self) -> List[Dict]:
        """Возвращает события навигации"""
        return self._navigation_events.copy()
    
    def on_step_end(self, step):
        """Добавляем CDP данные к шагу и делаем скриншот"""
        # Делаем скриншот в конце каждого шага
        self.capture_screenshot(f"step_{step.name}.png")
        
        # Добавляем JS ошибки к шагу
        if hasattr(step, 'js_errors'):
            step.js_errors = self._js_exceptions.copy()
        
        # Добавляем console к шагу  
        if hasattr(step, 'console_logs'):
            step.console_logs = self._console_messages.copy()