File size: 7,611 Bytes
2dc7238
bbbe047
 
3b1b98d
2dc7238
 
 
 
 
bbbe047
3b1b98d
5bea81c
3b1b98d
2dc7238
 
 
 
 
3b1b98d
2dc7238
 
 
3b1b98d
 
 
2dc7238
 
5bea81c
2dc7238
 
3b1b98d
 
 
 
 
 
 
2dc7238
 
 
 
3b1b98d
2dc7238
 
3b1b98d
 
 
 
 
 
 
 
2dc7238
 
bbbe047
3b1b98d
2dc7238
 
bbbe047
 
2dc7238
3b1b98d
5bea81c
3b1b98d
 
 
 
 
 
 
 
 
 
 
5bea81c
2dc7238
bbbe047
 
3b1b98d
 
 
 
 
bbbe047
3b1b98d
 
 
bbbe047
3b1b98d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bbbe047
 
3b1b98d
bbbe047
 
3b1b98d
 
bbbe047
 
 
3b1b98d
bbbe047
3b1b98d
 
bbbe047
 
2dc7238
bbbe047
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2dc7238
bbbe047
 
 
 
 
 
2dc7238
bbbe047
2dc7238
3b1b98d
 
 
 
 
 
2dc7238
5bea81c
2dc7238
 
5bea81c
3b1b98d
2dc7238
 
5bea81c
2dc7238
 
bbbe047
 
 
 
2dc7238
5bea81c
2dc7238
 
bbbe047
 
2dc7238
 
bbbe047
 
 
 
 
 
 
 
2dc7238
 
bbbe047
3b1b98d
 
bbbe047
 
 
3b1b98d
 
bbbe047
 
 
2dc7238
 
 
 
bbbe047
2dc7238
bbbe047
2dc7238
 
 
bbbe047
2dc7238
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
"""
Proxy Manager for Browser Portals
---------------------------------
Supports custom IP proxy configuration with optional authentication.
"""

import asyncio
import aiohttp
import logging
from typing import Optional, Dict
from dataclasses import dataclass, field
from datetime import datetime
from urllib.parse import urlparse

logger = logging.getLogger("kai_api.proxy_manager")

@dataclass
class Proxy:
    """Represents a proxy server with optional authentication."""
    ip: str
    port: int
    protocol: str = "http"
    username: Optional[str] = None
    password: Optional[str] = None
    country: str = "Custom"
    last_tested: Optional[datetime] = None
    is_working: bool = False
    response_time: float = 999.0
    
    def __str__(self):
        """Return proxy URL string."""
        if self.username and self.password:
            return f"{self.protocol}://{self.username}:{self.password}@{self.ip}:{self.port}"
        return f"{self.protocol}://{self.ip}:{self.port}"
    
    def to_display_string(self) -> str:
        """Return proxy URL without credentials for display."""
        return f"{self.protocol}://{self.ip}:{self.port}"
    
    def to_playwright_format(self) -> Dict:
        """Convert to Playwright proxy format."""
        proxy_dict = {
            "server": f"{self.protocol}://{self.ip}:{self.port}",
        }
        
        # Add authentication if present
        if self.username:
            proxy_dict["username"] = self.username
        if self.password:
            proxy_dict["password"] = self.password
            
        return proxy_dict


class ProxyManager:
    """Manages proxy configuration with optional authentication."""
    
    def __init__(self):
        self.custom_proxy: Optional[Proxy] = None
        self._proxy_str: Optional[str] = None
        
    def set_custom_proxy(self, proxy_str: str, username: Optional[str] = None, password: Optional[str] = None) -> bool:
        """
        Set a custom proxy from string format.
        Supports formats:
        - ip:port
        - protocol://ip:port
        - protocol://username:password@ip:port
        - ip:port (with separate username/password params)
        
        Examples: 
        - 192.168.1.1:8080
        - http://proxy.example.com:3128
        - http://user:pass@proxy.example.com:3128
        """
        try:
            proxy_str = proxy_str.strip()
            
            # Check if credentials are embedded in URL
            parsed = urlparse(proxy_str)
            
            # Parse protocol
            protocol = parsed.scheme or "http"
            
            # Get host and port
            host = parsed.hostname
            port = parsed.port
            
            # If no host parsed, try simple ip:port format
            if not host:
                if ":" not in proxy_str:
                    raise ValueError("Proxy must include port (e.g., ip:port)")
                
                # Remove protocol if present
                if "://" in proxy_str:
                    protocol, proxy_str = proxy_str.split("://", 1)
                
                parts = proxy_str.rsplit(":", 1)
                host = parts[0]
                port = int(parts[1])
            
            # Extract credentials from URL if present
            url_username = parsed.username
            url_password = parsed.password
            
            # Use provided credentials or extracted ones
            final_username = username or url_username
            final_password = password or url_password
            
            self.custom_proxy = Proxy(
                ip=host,
                port=port,
                protocol=protocol,
                username=final_username,
                password=final_password,
                is_working=True,  # Assume working until tested
                last_tested=datetime.now()
            )
            self._proxy_str = str(self.custom_proxy)
            
            auth_info = f" with auth" if final_username else ""
            logger.info(f"✅ Custom proxy set: {self.custom_proxy.to_display_string()}{auth_info}")
            return True
            
        except Exception as e:
            logger.error(f"❌ Failed to set custom proxy: {e}")
            return False
    
    def clear_proxy(self):
        """Clear the custom proxy."""
        self.custom_proxy = None
        self._proxy_str = None
        logger.info("🗑️ Custom proxy cleared")
    
    def get_current_proxy(self) -> Optional[Proxy]:
        """Get the current custom proxy."""
        return self.custom_proxy
    
    def get_proxy_string(self) -> Optional[str]:
        """Get the proxy string for environment variables."""
        return self._proxy_str
    
    async def test_proxy(self, proxy: Optional[Proxy] = None) -> bool:
        """Test if a proxy is working."""
        test_proxy = proxy or self.custom_proxy
        if not test_proxy:
            return False
        
        try:
            timeout = aiohttp.ClientTimeout(total=10)
            
            # Build proxy URL with auth if present
            if test_proxy.username and test_proxy.password:
                proxy_url = f"{test_proxy.protocol}://{test_proxy.username}:{test_proxy.password}@{test_proxy.ip}:{test_proxy.port}"
            else:
                proxy_url = f"{test_proxy.protocol}://{test_proxy.ip}:{test_proxy.port}"
            
            async with aiohttp.ClientSession(timeout=timeout) as session:
                start = asyncio.get_event_loop().time()
                
                async with session.get(
                    "http://httpbin.org/ip",
                    proxy=proxy_url,
                    ssl=False
                ) as response:
                    elapsed = asyncio.get_event_loop().time() - start
                    
                    if response.status == 200:
                        test_proxy.is_working = True
                        test_proxy.response_time = elapsed
                        test_proxy.last_tested = datetime.now()
                        logger.info(f"✅ Proxy test passed: {elapsed:.2f}s")
                        return True
                    return False
                        
        except Exception as e:
            logger.warning(f"❌ Proxy test failed: {e}")
            test_proxy.is_working = False
            return False
    
    def get_status(self) -> Dict:
        """Get proxy status."""
        if not self.custom_proxy:
            return {
                "enabled": False,
                "proxy": None,
                "message": "No custom proxy configured"
            }
        
        return {
            "enabled": True,
            "proxy": self.custom_proxy.to_display_string(),
            "full_url": str(self.custom_proxy),
            "protocol": self.custom_proxy.protocol,
            "ip": self.custom_proxy.ip,
            "port": self.custom_proxy.port,
            "has_auth": bool(self.custom_proxy.username),
            "username": self.custom_proxy.username,
            "is_working": self.custom_proxy.is_working,
            "response_time": f"{self.custom_proxy.response_time:.2f}s",
            "last_tested": self.custom_proxy.last_tested.isoformat() if self.custom_proxy.last_tested else None
        }


# Global proxy manager instance
_proxy_manager: Optional[ProxyManager] = None

def get_proxy_manager() -> ProxyManager:
    """Get the global proxy manager instance."""
    global _proxy_manager
    if _proxy_manager is None:
        _proxy_manager = ProxyManager()
    return _proxy_manager