cursorflow 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cursorflow/__init__.py +78 -0
- cursorflow/auto_updater.py +244 -0
- cursorflow/cli.py +408 -0
- cursorflow/core/agent.py +272 -0
- cursorflow/core/auth_handler.py +433 -0
- cursorflow/core/browser_controller.py +534 -0
- cursorflow/core/browser_engine.py +386 -0
- cursorflow/core/css_iterator.py +397 -0
- cursorflow/core/cursor_integration.py +744 -0
- cursorflow/core/cursorflow.py +649 -0
- cursorflow/core/error_correlator.py +322 -0
- cursorflow/core/event_correlator.py +182 -0
- cursorflow/core/file_change_monitor.py +548 -0
- cursorflow/core/log_collector.py +410 -0
- cursorflow/core/log_monitor.py +179 -0
- cursorflow/core/persistent_session.py +910 -0
- cursorflow/core/report_generator.py +282 -0
- cursorflow/log_sources/local_file.py +198 -0
- cursorflow/log_sources/ssh_remote.py +210 -0
- cursorflow/updater.py +512 -0
- cursorflow-1.2.0.dist-info/METADATA +444 -0
- cursorflow-1.2.0.dist-info/RECORD +25 -0
- cursorflow-1.2.0.dist-info/WHEEL +5 -0
- cursorflow-1.2.0.dist-info/entry_points.txt +2 -0
- cursorflow-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,433 @@
|
|
1
|
+
"""
|
2
|
+
Authentication Handler
|
3
|
+
|
4
|
+
Universal authentication support with session persistence.
|
5
|
+
Handles form-based login, cookie auth, header auth without framework complexity.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
import json
|
10
|
+
import time
|
11
|
+
from typing import Dict, List, Optional, Any
|
12
|
+
from pathlib import Path
|
13
|
+
import logging
|
14
|
+
|
15
|
+
|
16
|
+
class AuthHandler:
|
17
|
+
"""
|
18
|
+
Universal authentication handler - works with any web technology
|
19
|
+
|
20
|
+
Supports multiple auth methods with session persistence for faster testing.
|
21
|
+
NO FRAMEWORK ASSUMPTIONS - pure universal patterns.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self, auth_config: Dict):
|
25
|
+
"""
|
26
|
+
Initialize authentication handler
|
27
|
+
|
28
|
+
Args:
|
29
|
+
auth_config: {
|
30
|
+
"method": "form|cookies|headers",
|
31
|
+
"username_selector": "#username",
|
32
|
+
"password_selector": "#password",
|
33
|
+
"submit_selector": "#login-button",
|
34
|
+
"username": "test_user",
|
35
|
+
"password": "test_pass",
|
36
|
+
"session_storage": "sessions/"
|
37
|
+
}
|
38
|
+
"""
|
39
|
+
self.config = auth_config
|
40
|
+
self.method = auth_config.get("method", "form")
|
41
|
+
self.logger = logging.getLogger(__name__)
|
42
|
+
|
43
|
+
# Create session storage in user's project under .cursorflow
|
44
|
+
session_dir = auth_config.get("session_storage", ".cursorflow/sessions/")
|
45
|
+
if not Path(session_dir).is_absolute():
|
46
|
+
session_dir = Path.cwd() / session_dir
|
47
|
+
session_dir = Path(session_dir)
|
48
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
49
|
+
self.session_dir = session_dir
|
50
|
+
|
51
|
+
async def authenticate(
|
52
|
+
self,
|
53
|
+
page,
|
54
|
+
session_options: Optional[Dict] = None
|
55
|
+
) -> bool:
|
56
|
+
"""
|
57
|
+
Authenticate user with session management
|
58
|
+
|
59
|
+
Args:
|
60
|
+
page: Playwright page object
|
61
|
+
session_options: {
|
62
|
+
"reuse_session": True,
|
63
|
+
"save_session": True,
|
64
|
+
"fresh_session": False,
|
65
|
+
"session_name": "test_session"
|
66
|
+
}
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
True if authentication successful, False otherwise
|
70
|
+
"""
|
71
|
+
session_options = session_options or {}
|
72
|
+
session_name = session_options.get("session_name", "default")
|
73
|
+
|
74
|
+
try:
|
75
|
+
# Try to reuse existing session if requested
|
76
|
+
if (session_options.get("reuse_session", True) and
|
77
|
+
not session_options.get("fresh_session", False)):
|
78
|
+
|
79
|
+
if await self._restore_session(page, session_name):
|
80
|
+
self.logger.info("✅ Reused existing authentication session")
|
81
|
+
return True
|
82
|
+
|
83
|
+
# Perform fresh authentication
|
84
|
+
self.logger.info("🔐 Performing fresh authentication...")
|
85
|
+
success = await self._perform_authentication(page)
|
86
|
+
|
87
|
+
# Save session if requested and successful
|
88
|
+
if success and session_options.get("save_session", True):
|
89
|
+
await self._save_session(page, session_name)
|
90
|
+
|
91
|
+
return success
|
92
|
+
|
93
|
+
except Exception as e:
|
94
|
+
self.logger.error(f"Authentication failed: {e}")
|
95
|
+
return False
|
96
|
+
|
97
|
+
async def _restore_session(self, page, session_name: str) -> bool:
|
98
|
+
"""Try to restore a saved session"""
|
99
|
+
try:
|
100
|
+
session_file = self.session_dir / f"{session_name}_session.json"
|
101
|
+
|
102
|
+
if not session_file.exists():
|
103
|
+
return False
|
104
|
+
|
105
|
+
with open(session_file, 'r') as f:
|
106
|
+
session_data = json.load(f)
|
107
|
+
|
108
|
+
# Restore cookies
|
109
|
+
if "cookies" in session_data:
|
110
|
+
await page.context.add_cookies(session_data["cookies"])
|
111
|
+
|
112
|
+
# Restore local storage
|
113
|
+
if "localStorage" in session_data:
|
114
|
+
await page.evaluate(f"""
|
115
|
+
Object.entries({json.dumps(session_data["localStorage"])}).forEach(([key, value]) => {{
|
116
|
+
localStorage.setItem(key, value);
|
117
|
+
}});
|
118
|
+
""")
|
119
|
+
|
120
|
+
# Restore session storage
|
121
|
+
if "sessionStorage" in session_data:
|
122
|
+
await page.evaluate(f"""
|
123
|
+
Object.entries({json.dumps(session_data["sessionStorage"])}).forEach(([key, value]) => {{
|
124
|
+
sessionStorage.setItem(key, value);
|
125
|
+
}});
|
126
|
+
""")
|
127
|
+
|
128
|
+
# Test if session is still valid by checking for auth indicators
|
129
|
+
is_valid = await self._validate_session(page)
|
130
|
+
|
131
|
+
if is_valid:
|
132
|
+
self.logger.info(f"Successfully restored session: {session_name}")
|
133
|
+
return True
|
134
|
+
else:
|
135
|
+
self.logger.info(f"Restored session {session_name} is no longer valid")
|
136
|
+
# Clean up invalid session
|
137
|
+
session_file.unlink(missing_ok=True)
|
138
|
+
return False
|
139
|
+
|
140
|
+
except Exception as e:
|
141
|
+
self.logger.warning(f"Session restoration failed: {e}")
|
142
|
+
return False
|
143
|
+
|
144
|
+
async def _save_session(self, page, session_name: str):
|
145
|
+
"""Save current session for reuse"""
|
146
|
+
try:
|
147
|
+
session_file = self.session_dir / f"{session_name}_session.json"
|
148
|
+
|
149
|
+
# Get browser storage state
|
150
|
+
storage_state = await page.context.storage_state()
|
151
|
+
|
152
|
+
# Get local storage
|
153
|
+
local_storage = await page.evaluate("""
|
154
|
+
() => {
|
155
|
+
const storage = {};
|
156
|
+
for (let i = 0; i < localStorage.length; i++) {
|
157
|
+
const key = localStorage.key(i);
|
158
|
+
storage[key] = localStorage.getItem(key);
|
159
|
+
}
|
160
|
+
return storage;
|
161
|
+
}
|
162
|
+
""")
|
163
|
+
|
164
|
+
# Get session storage
|
165
|
+
session_storage = await page.evaluate("""
|
166
|
+
() => {
|
167
|
+
const storage = {};
|
168
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
169
|
+
const key = sessionStorage.key(i);
|
170
|
+
storage[key] = sessionStorage.getItem(key);
|
171
|
+
}
|
172
|
+
return storage;
|
173
|
+
}
|
174
|
+
""")
|
175
|
+
|
176
|
+
session_data = {
|
177
|
+
"timestamp": time.time(),
|
178
|
+
"method": self.method,
|
179
|
+
"cookies": storage_state.get("cookies", []),
|
180
|
+
"localStorage": local_storage,
|
181
|
+
"sessionStorage": session_storage,
|
182
|
+
"url": page.url
|
183
|
+
}
|
184
|
+
|
185
|
+
with open(session_file, 'w') as f:
|
186
|
+
json.dump(session_data, f, indent=2)
|
187
|
+
|
188
|
+
self.logger.info(f"💾 Session saved: {session_name}")
|
189
|
+
|
190
|
+
except Exception as e:
|
191
|
+
self.logger.warning(f"Session save failed: {e}")
|
192
|
+
|
193
|
+
async def _perform_authentication(self, page) -> bool:
|
194
|
+
"""Perform authentication based on configured method"""
|
195
|
+
|
196
|
+
if self.method == "form":
|
197
|
+
return await self._form_authentication(page)
|
198
|
+
elif self.method == "cookies":
|
199
|
+
return await self._cookie_authentication(page)
|
200
|
+
elif self.method == "headers":
|
201
|
+
return await self._header_authentication(page)
|
202
|
+
else:
|
203
|
+
self.logger.error(f"Unsupported authentication method: {self.method}")
|
204
|
+
return False
|
205
|
+
|
206
|
+
async def _form_authentication(self, page) -> bool:
|
207
|
+
"""Perform form-based authentication"""
|
208
|
+
try:
|
209
|
+
username = self.config.get("username")
|
210
|
+
password = self.config.get("password")
|
211
|
+
username_selector = self.config.get("username_selector", "#username")
|
212
|
+
password_selector = self.config.get("password_selector", "#password")
|
213
|
+
submit_selector = self.config.get("submit_selector", "#login-button")
|
214
|
+
|
215
|
+
if not username or not password:
|
216
|
+
self.logger.error("Username and password required for form authentication")
|
217
|
+
return False
|
218
|
+
|
219
|
+
# Wait for login form
|
220
|
+
await page.wait_for_selector(username_selector, timeout=10000)
|
221
|
+
|
222
|
+
# Fill username
|
223
|
+
await page.fill(username_selector, username)
|
224
|
+
self.logger.debug(f"Filled username: {username_selector}")
|
225
|
+
|
226
|
+
# Fill password
|
227
|
+
await page.fill(password_selector, password)
|
228
|
+
self.logger.debug(f"Filled password: {password_selector}")
|
229
|
+
|
230
|
+
# Submit form
|
231
|
+
await page.click(submit_selector)
|
232
|
+
self.logger.debug(f"Clicked submit: {submit_selector}")
|
233
|
+
|
234
|
+
# Wait for navigation or success indicator
|
235
|
+
try:
|
236
|
+
await page.wait_for_load_state("networkidle", timeout=10000)
|
237
|
+
except:
|
238
|
+
# If no navigation, wait a bit for any AJAX auth
|
239
|
+
await page.wait_for_timeout(3000)
|
240
|
+
|
241
|
+
# Validate authentication success
|
242
|
+
is_authenticated = await self._validate_authentication(page)
|
243
|
+
|
244
|
+
if is_authenticated:
|
245
|
+
self.logger.info("✅ Form authentication successful")
|
246
|
+
return True
|
247
|
+
else:
|
248
|
+
self.logger.warning("❌ Form authentication failed")
|
249
|
+
return False
|
250
|
+
|
251
|
+
except Exception as e:
|
252
|
+
self.logger.error(f"Form authentication error: {e}")
|
253
|
+
return False
|
254
|
+
|
255
|
+
async def _cookie_authentication(self, page) -> bool:
|
256
|
+
"""Perform cookie-based authentication"""
|
257
|
+
try:
|
258
|
+
cookies = self.config.get("cookies", [])
|
259
|
+
|
260
|
+
if not cookies:
|
261
|
+
self.logger.error("No cookies provided for cookie authentication")
|
262
|
+
return False
|
263
|
+
|
264
|
+
# Add cookies to context
|
265
|
+
await page.context.add_cookies(cookies)
|
266
|
+
self.logger.info(f"Added {len(cookies)} authentication cookies")
|
267
|
+
|
268
|
+
# Refresh page to apply cookies
|
269
|
+
await page.reload()
|
270
|
+
await page.wait_for_load_state("networkidle")
|
271
|
+
|
272
|
+
# Validate authentication
|
273
|
+
is_authenticated = await self._validate_authentication(page)
|
274
|
+
|
275
|
+
if is_authenticated:
|
276
|
+
self.logger.info("✅ Cookie authentication successful")
|
277
|
+
return True
|
278
|
+
else:
|
279
|
+
self.logger.warning("❌ Cookie authentication failed")
|
280
|
+
return False
|
281
|
+
|
282
|
+
except Exception as e:
|
283
|
+
self.logger.error(f"Cookie authentication error: {e}")
|
284
|
+
return False
|
285
|
+
|
286
|
+
async def _header_authentication(self, page) -> bool:
|
287
|
+
"""Perform header-based authentication"""
|
288
|
+
try:
|
289
|
+
headers = self.config.get("headers", {})
|
290
|
+
|
291
|
+
if not headers:
|
292
|
+
self.logger.error("No headers provided for header authentication")
|
293
|
+
return False
|
294
|
+
|
295
|
+
# Set extra HTTP headers
|
296
|
+
await page.set_extra_http_headers(headers)
|
297
|
+
self.logger.info(f"Set {len(headers)} authentication headers")
|
298
|
+
|
299
|
+
# Refresh page to apply headers
|
300
|
+
await page.reload()
|
301
|
+
await page.wait_for_load_state("networkidle")
|
302
|
+
|
303
|
+
# Validate authentication
|
304
|
+
is_authenticated = await self._validate_authentication(page)
|
305
|
+
|
306
|
+
if is_authenticated:
|
307
|
+
self.logger.info("✅ Header authentication successful")
|
308
|
+
return True
|
309
|
+
else:
|
310
|
+
self.logger.warning("❌ Header authentication failed")
|
311
|
+
return False
|
312
|
+
|
313
|
+
except Exception as e:
|
314
|
+
self.logger.error(f"Header authentication error: {e}")
|
315
|
+
return False
|
316
|
+
|
317
|
+
async def _validate_authentication(self, page) -> bool:
|
318
|
+
"""Validate that authentication was successful"""
|
319
|
+
try:
|
320
|
+
# Universal authentication validation strategies
|
321
|
+
|
322
|
+
# Strategy 1: Check for common auth failure indicators
|
323
|
+
auth_failure_selectors = [
|
324
|
+
".error", ".alert-danger", ".login-error",
|
325
|
+
"#error", "#login-error", "[data-testid='error']"
|
326
|
+
]
|
327
|
+
|
328
|
+
for selector in auth_failure_selectors:
|
329
|
+
try:
|
330
|
+
error_element = await page.wait_for_selector(selector, timeout=1000)
|
331
|
+
if error_element:
|
332
|
+
error_text = await error_element.text_content()
|
333
|
+
if error_text and any(word in error_text.lower() for word in ["error", "invalid", "failed", "incorrect"]):
|
334
|
+
self.logger.warning(f"Authentication error detected: {error_text}")
|
335
|
+
return False
|
336
|
+
except:
|
337
|
+
continue
|
338
|
+
|
339
|
+
# Strategy 2: Check for common success indicators
|
340
|
+
success_indicators = self.config.get("success_indicators", [
|
341
|
+
"dashboard", "profile", "logout", "welcome", "user", "account"
|
342
|
+
])
|
343
|
+
|
344
|
+
page_content = await page.content()
|
345
|
+
page_content_lower = page_content.lower()
|
346
|
+
|
347
|
+
success_count = sum(1 for indicator in success_indicators if indicator in page_content_lower)
|
348
|
+
|
349
|
+
# Strategy 3: Check URL changes
|
350
|
+
current_url = page.url
|
351
|
+
login_urls = ["/login", "/signin", "/auth"]
|
352
|
+
|
353
|
+
url_indicates_success = not any(login_url in current_url.lower() for login_url in login_urls)
|
354
|
+
|
355
|
+
# Strategy 4: Check for auth-specific elements
|
356
|
+
auth_selectors = self.config.get("auth_check_selectors", [])
|
357
|
+
auth_elements_found = 0
|
358
|
+
|
359
|
+
for selector in auth_selectors:
|
360
|
+
try:
|
361
|
+
element = await page.wait_for_selector(selector, timeout=1000)
|
362
|
+
if element:
|
363
|
+
auth_elements_found += 1
|
364
|
+
except:
|
365
|
+
continue
|
366
|
+
|
367
|
+
# Determine authentication success
|
368
|
+
is_authenticated = (
|
369
|
+
success_count >= 2 or # Multiple success indicators
|
370
|
+
url_indicates_success or # URL changed from login page
|
371
|
+
auth_elements_found > 0 # Found auth-specific elements
|
372
|
+
)
|
373
|
+
|
374
|
+
self.logger.debug(f"Auth validation - success_count: {success_count}, url_ok: {url_indicates_success}, auth_elements: {auth_elements_found}")
|
375
|
+
|
376
|
+
return is_authenticated
|
377
|
+
|
378
|
+
except Exception as e:
|
379
|
+
self.logger.error(f"Authentication validation error: {e}")
|
380
|
+
return False
|
381
|
+
|
382
|
+
async def _validate_session(self, page) -> bool:
|
383
|
+
"""Validate that a restored session is still valid"""
|
384
|
+
# Use the same validation logic as fresh authentication
|
385
|
+
return await self._validate_authentication(page)
|
386
|
+
|
387
|
+
def get_session_info(self, session_name: str = "default") -> Optional[Dict]:
|
388
|
+
"""Get information about a saved session"""
|
389
|
+
try:
|
390
|
+
session_file = self.session_dir / f"{session_name}_session.json"
|
391
|
+
|
392
|
+
if not session_file.exists():
|
393
|
+
return None
|
394
|
+
|
395
|
+
with open(session_file, 'r') as f:
|
396
|
+
session_data = json.load(f)
|
397
|
+
|
398
|
+
return {
|
399
|
+
"session_name": session_name,
|
400
|
+
"timestamp": session_data.get("timestamp"),
|
401
|
+
"method": session_data.get("method"),
|
402
|
+
"url": session_data.get("url"),
|
403
|
+
"cookies_count": len(session_data.get("cookies", [])),
|
404
|
+
"has_local_storage": bool(session_data.get("localStorage")),
|
405
|
+
"has_session_storage": bool(session_data.get("sessionStorage"))
|
406
|
+
}
|
407
|
+
|
408
|
+
except Exception as e:
|
409
|
+
self.logger.error(f"Session info retrieval failed: {e}")
|
410
|
+
return None
|
411
|
+
|
412
|
+
def clear_session(self, session_name: str = "default"):
|
413
|
+
"""Clear a saved session"""
|
414
|
+
try:
|
415
|
+
session_file = self.session_dir / f"{session_name}_session.json"
|
416
|
+
session_file.unlink(missing_ok=True)
|
417
|
+
self.logger.info(f"Cleared session: {session_name}")
|
418
|
+
|
419
|
+
except Exception as e:
|
420
|
+
self.logger.error(f"Session clearing failed: {e}")
|
421
|
+
|
422
|
+
def list_sessions(self) -> List[str]:
|
423
|
+
"""List all saved sessions"""
|
424
|
+
try:
|
425
|
+
sessions = []
|
426
|
+
for session_file in self.session_dir.glob("*_session.json"):
|
427
|
+
session_name = session_file.stem.replace("_session", "")
|
428
|
+
sessions.append(session_name)
|
429
|
+
return sessions
|
430
|
+
|
431
|
+
except Exception as e:
|
432
|
+
self.logger.error(f"Session listing failed: {e}")
|
433
|
+
return []
|