KManager / registration /oauth_callback_server.py
StarrySkyWorld's picture
Initial commit
494c89b
"""
OAuth Callback Server
Локальный HTTP сервер для приёма OAuth callback от AWS/Google/Github.
Используется в WebView стратегии для получения authorization code.
"""
from http.server import HTTPServer, BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import threading
import time
from typing import Optional, Tuple, Callable
import logging
logger = logging.getLogger(__name__)
class OAuthCallbackHandler(BaseHTTPRequestHandler):
"""HTTP handler для OAuth callback"""
def log_message(self, format, *args):
"""Отключаем стандартное логирование"""
pass
def do_GET(self):
"""Обработка GET запроса с OAuth callback"""
try:
parsed = urlparse(self.path)
query = parse_qs(parsed.query)
# Проверяем наличие code
if 'code' in query:
code = query['code'][0]
state = query.get('state', [None])[0]
# Сохраняем в сервер
self.server.auth_code = code
self.server.auth_state = state
self.server.callback_received = True
logger.info(f"[OAuth] Callback received: code={code[:20]}..., state={state[:20] if state else None}...")
# Вызываем callback если есть
if hasattr(self.server, 'on_callback') and self.server.on_callback:
try:
self.server.on_callback(code, state)
except Exception as e:
logger.error(f"[OAuth] Callback handler error: {e}")
# Отправляем success page
self.send_response(200)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Authorization Successful</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
text-align: center;
max-width: 400px;
}
.success-icon {
font-size: 64px;
color: #4CAF50;
margin-bottom: 20px;
}
h1 {
color: #333;
margin: 0 0 10px 0;
font-size: 24px;
}
p {
color: #666;
margin: 0 0 20px 0;
}
.info {
background: #f5f5f5;
padding: 15px;
border-radius: 5px;
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<div class="success-icon">✓</div>
<h1>Authorization Successful!</h1>
<p>You can close this window and return to the application.</p>
<div class="info">
The authorization code has been received.<br>
Token exchange is in progress...
</div>
</div>
<script>
// Auto-close after 3 seconds
setTimeout(function() {
window.close();
}, 3000);
</script>
</body>
</html>
"""
self.wfile.write(html.encode('utf-8'))
elif 'error' in query:
# OAuth error
error = query['error'][0]
error_description = query.get('error_description', ['Unknown error'])[0]
self.server.auth_error = error
self.server.auth_error_description = error_description
self.server.callback_received = True
logger.error(f"[OAuth] Error callback: {error} - {error_description}")
# Отправляем error page
self.send_response(400)
self.send_header('Content-type', 'text/html; charset=utf-8')
self.end_headers()
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Authorization Failed</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}}
.container {{
background: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
text-align: center;
max-width: 400px;
}}
.error-icon {{
font-size: 64px;
color: #f44336;
margin-bottom: 20px;
}}
h1 {{
color: #333;
margin: 0 0 10px 0;
font-size: 24px;
}}
p {{
color: #666;
margin: 0 0 20px 0;
}}
.error-details {{
background: #ffebee;
padding: 15px;
border-radius: 5px;
font-size: 14px;
color: #c62828;
word-break: break-word;
}}
</style>
</head>
<body>
<div class="container">
<div class="error-icon">✗</div>
<h1>Authorization Failed</h1>
<p>An error occurred during authorization.</p>
<div class="error-details">
<strong>{error}</strong><br>
{error_description}
</div>
</div>
</body>
</html>
"""
self.wfile.write(html.encode('utf-8'))
else:
# Неизвестный запрос
self.send_response(400)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(b'Invalid OAuth callback')
except Exception as e:
logger.error(f"[OAuth] Handler error: {e}")
self.send_response(500)
self.send_header('Content-type', 'text/plain')
self.end_headers()
self.wfile.write(f'Server error: {str(e)}'.encode('utf-8'))
class OAuthCallbackServer:
"""
OAuth callback server
Запускает локальный HTTP сервер для приёма OAuth callback.
Используется в WebView стратегии.
"""
def __init__(self, port: int = 43210,
on_callback: Optional[Callable[[str, Optional[str]], None]] = None):
"""
Args:
port: Порт для сервера (по умолчанию 43210)
on_callback: Callback функция, вызывается при получении code
Сигнатура: (code: str, state: Optional[str]) -> None
"""
self.port = port
self.on_callback = on_callback
self.server: Optional[HTTPServer] = None
self.thread: Optional[threading.Thread] = None
# Результаты callback
self.auth_code: Optional[str] = None
self.auth_state: Optional[str] = None
self.auth_error: Optional[str] = None
self.auth_error_description: Optional[str] = None
self.callback_received = False
def start(self):
"""Запустить сервер в отдельном потоке"""
if self.server:
logger.warning("[OAuth] Server already running")
return
try:
self.server = HTTPServer(('127.0.0.1', self.port), OAuthCallbackHandler)
# Передаём callback в сервер
self.server.auth_code = None
self.server.auth_state = None
self.server.auth_error = None
self.server.auth_error_description = None
self.server.callback_received = False
self.server.on_callback = self.on_callback
# Запускаем в отдельном потоке
self.thread = threading.Thread(target=self._run, daemon=True)
self.thread.start()
logger.info(f"[OAuth] Callback server started on http://127.0.0.1:{self.port}")
except OSError as e:
if 'Address already in use' in str(e):
raise RuntimeError(f"Port {self.port} already in use. Another OAuth server running?")
raise
def _run(self):
"""Запустить сервер (вызывается в отдельном потоке)"""
try:
# Обрабатываем только один запрос
self.server.handle_request()
# Копируем результаты из сервера
self.auth_code = self.server.auth_code
self.auth_state = self.server.auth_state
self.auth_error = self.server.auth_error
self.auth_error_description = self.server.auth_error_description
self.callback_received = self.server.callback_received
except Exception as e:
logger.error(f"[OAuth] Server error: {e}")
def wait_for_callback(self, timeout: int = 300) -> Tuple[Optional[str], Optional[str]]:
"""
Ждать OAuth callback
Args:
timeout: Таймаут в секундах (по умолчанию 5 минут)
Returns:
Tuple (code, state) или (None, None) при ошибке/таймауте
"""
start_time = time.time()
while time.time() - start_time < timeout:
if self.callback_received:
if self.auth_code:
logger.info(f"[OAuth] Callback received successfully")
return self.auth_code, self.auth_state
elif self.auth_error:
logger.error(f"[OAuth] Error: {self.auth_error} - {self.auth_error_description}")
return None, None
time.sleep(0.5)
logger.error(f"[OAuth] Callback timeout after {timeout}s")
return None, None
def stop(self):
"""Остановить сервер"""
if self.server:
try:
self.server.shutdown()
self.server.server_close()
except Exception as e:
logger.error(f"[OAuth] Error stopping server: {e}")
finally:
self.server = None
if self.thread and self.thread.is_alive():
self.thread.join(timeout=2)
self.thread = None
logger.info("[OAuth] Callback server stopped")
def get_redirect_uri(self) -> str:
"""Получить redirect URI для OAuth"""
return f"http://127.0.0.1:{self.port}/oauth/callback"
def __enter__(self):
self.start()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.stop()