import 'dart:io'; import 'dart:async'; import 'dart:developer' show log; /// A lightweight HTTP server for handling OAuth callbacks on desktop platforms. /// This provides a standard localhost callback URL that's compatible with most OAuth providers. class OAuthCallbackServer { HttpServer? _server; late int _port; String? _path; final Completer _completer = Completer(); Timer? _timeoutTimer; Timer? _heartbeatTimer; DateTime? _lastHeartbeat; bool _browserTabActive = false; /// Starts the HTTP server and returns the callback URL. /// /// [path] - Optional path for the callback endpoint (defaults to '/callback') /// Returns the full callback URL (e.g., 'http://localhost:8080/callback') Future start({String path = '/callback'}) async { _path = path; // Try to bind to a random available port starting from 8080 for (int port = 8080; port <= 8090; port++) { try { _server = await HttpServer.bind(InternetAddress.loopbackIPv4, port); _port = port; break; } catch (e) { // Port is busy, try next one if (port == 8090) { throw Exception( 'Unable to find available port for OAuth callback server', ); } } } if (_server == null) { throw Exception('Failed to start OAuth callback server'); } _server!.listen(_handleRequest); final callbackUrl = 'http://localhost:$_port$_path'; log('OAuth callback server started at: $callbackUrl'); return callbackUrl; } /// Waits for the OAuth callback and returns the full callback URL with query parameters. /// /// [timeout] - Optional timeout duration (defaults to 3 minutes) /// Throws [TimeoutException] if no callback is received within the timeout period. /// Throws [Exception] if the browser tab is closed without completing authorization. Future waitForCallback({ Duration timeout = const Duration(minutes: 3), }) async { // Set up timeout timer _timeoutTimer = Timer(timeout, () { if (!_completer.isCompleted) { _completer.completeError( TimeoutException( 'OAuth callback timeout: No response received within ${timeout.inMinutes} minutes. ' 'The user may have closed the browser tab without completing authorization.', timeout, ), ); // Automatically stop the server on timeout _stopServerOnError( 'OAuth flow timed out after ${timeout.inMinutes} minutes', ); } }); // Set up heartbeat monitoring to detect if browser tab is closed _startHeartbeatMonitoring(); try { return await _completer.future; } finally { _timeoutTimer?.cancel(); _timeoutTimer = null; _stopHeartbeatMonitoring(); } } /// Stops the HTTP server and cleans up resources. Future stop() async { if (_server == null) { log('OAuth callback server already stopped'); return; } _timeoutTimer?.cancel(); _timeoutTimer = null; _stopHeartbeatMonitoring(); try { await _server?.close(); log('OAuth callback server stopped gracefully'); } catch (e) { log('Error during graceful server stop: $e'); } finally { _server = null; } } /// Cancels the waiting callback operation. /// This is useful when the user wants to cancel the OAuth flow manually. void cancel([String reason = 'OAuth flow cancelled by user']) { _timeoutTimer?.cancel(); _timeoutTimer = null; _stopHeartbeatMonitoring(); if (!_completer.isCompleted) { _completer.completeError(Exception('OAuth callback cancelled: $reason')); } // Automatically stop the server when cancelled _stopServerOnError(reason); } /// Starts heartbeat monitoring to detect browser tab closure void _startHeartbeatMonitoring() { _lastHeartbeat = DateTime.now(); _browserTabActive = true; // Check for heartbeat every 5 seconds _heartbeatTimer = Timer.periodic(const Duration(seconds: 5), (timer) { final now = DateTime.now(); // If no heartbeat received for 5 seconds and we had an active tab before if (_browserTabActive && _lastHeartbeat != null && now.difference(_lastHeartbeat!).inSeconds > 5) { log( 'Browser tab appears to be closed (no heartbeat for ${now.difference(_lastHeartbeat!).inSeconds}s)', ); if (!_completer.isCompleted) { _completer.completeError( Exception( 'OAuth authorization cancelled: Browser tab was closed without completing the authorization process. ' 'Please try again and complete the authorization in your browser.', ), ); } timer.cancel(); // Automatically stop the server when browser tab is closed _stopServerOnError( 'Browser tab closed without completing authorization', ); } }); } /// Stops heartbeat monitoring void _stopHeartbeatMonitoring() { _heartbeatTimer?.cancel(); _heartbeatTimer = null; _lastHeartbeat = null; _browserTabActive = false; } /// Stops the server immediately due to an error condition /// This is used for automatic cleanup when errors occur void _stopServerOnError(String reason) { if (_server == null) { log('OAuth callback server already stopped, skipping error stop'); return; } log('Stopping OAuth callback server due to error: $reason'); // Cancel any active timers _timeoutTimer?.cancel(); _timeoutTimer = null; _stopHeartbeatMonitoring(); // Close the server without waiting _server ?.close(force: true) .then((_) { log('OAuth callback server forcefully stopped due to error'); }) .catchError((error) { log('Error while force-stopping server: $error'); }); _server = null; } void _handleRequest(HttpRequest request) { log('OAuth request received: ${request.uri}'); try { // Handle heartbeat requests if (request.uri.path == '/heartbeat') { _lastHeartbeat = DateTime.now(); _browserTabActive = true; request.response ..statusCode = HttpStatus.ok ..headers.add('Access-Control-Allow-Origin', '*') ..headers.add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') ..headers.add('Access-Control-Allow-Headers', 'Content-Type') ..write('ok') ..close(); return; } // Handle OPTIONS preflight requests for CORS if (request.method == 'OPTIONS') { request.response ..statusCode = HttpStatus.ok ..headers.add('Access-Control-Allow-Origin', '*') ..headers.add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') ..headers.add('Access-Control-Allow-Headers', 'Content-Type') ..close(); return; } // Check if this is an authorization callback (has 'code' or 'error' parameters) final uri = request.uri; final hasCode = uri.queryParameters.containsKey('code'); final hasError = uri.queryParameters.containsKey('error'); String responseHtml; if (hasError) { final error = uri.queryParameters['error'] ?? 'unknown_error'; final errorDescription = uri.queryParameters['error_description'] ?? 'No description provided'; responseHtml = _generateErrorHtml(error, errorDescription); // Complete the future with error and stop server if (!_completer.isCompleted) { _completer.completeError( Exception('OAuth authorization failed: $error - $errorDescription'), ); } // Send response to browser first, then stop server after a delay request.response ..statusCode = HttpStatus.ok ..headers.contentType = ContentType.html ..write(responseHtml) ..close(); // Stop server after sending response (with a small delay) Timer(const Duration(seconds: 1), () { _stopServerOnError('OAuth authorization error: $error'); }); return; } else if (hasCode) { responseHtml = _generateSuccessHtml(); } else { responseHtml = _generateInfoHtml(); } // Send response to the browser request.response ..statusCode = HttpStatus.ok ..headers.contentType = ContentType.html ..write(responseHtml) ..close(); // Complete the future with the full callback URL if (!_completer.isCompleted) { _completer.complete(request.uri.toString()); } // For successful authorization, schedule server stop after response if (hasCode) { Timer(const Duration(seconds: 6), () { _stopServerOnError('OAuth flow completed successfully'); }); } } catch (e) { log('Error handling OAuth callback request: $e'); // Complete with error if not already completed if (!_completer.isCompleted) { _completer.completeError( Exception('OAuth callback request handling failed: $e'), ); } // Try to send an error response try { request.response ..statusCode = HttpStatus.internalServerError ..write('Internal server error occurred during OAuth callback') ..close(); } catch (responseError) { log('Failed to send error response: $responseError'); } // Stop server due to error _stopServerOnError('Request handling error: $e'); } } String _generateSuccessHtml() { return ''' OAuth Authentication Successful
Authentication Successful!
Your OAuth authorization was completed successfully. You can now close this window and return to API Dash.
This window will close automatically in 5 seconds...
'''; } String _generateErrorHtml(String error, String errorDescription) { return ''' OAuth Authentication Failed
Authentication Failed
The OAuth authorization was not completed successfully. Please try again from API Dash.
Error: $error
Description: $errorDescription
This window will close automatically in 10 seconds...
'''; } String _generateInfoHtml() { return ''' OAuth Callback Server
OAuth Callback Server
This is the OAuth callback endpoint for API Dash. If you're seeing this page, the callback server is running correctly. Please return to API Dash to complete the OAuth flow.
'''; } }