diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt b/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt index ba43f833d8..96f3d5e036 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt @@ -36,6 +36,7 @@ enum class Methods(val method: String) { Stop("stopVPN"), ConnectToServer("connectToServer"), IsVpnConnected("isVPNConnected"), + IsTagAvailable("isTagAvailable"), //Payment methods StripeSubscription("stripeSubscription"), @@ -195,6 +196,23 @@ class MethodHandler : FlutterPlugin, } } + Methods.IsTagAvailable.method -> { + scope.launch { + try { + val tag = call.arguments as? String + ?: throw IllegalArgumentException("Missing or invalid tag") + val available = Mobile.isTagAvailable(tag) + withContext(Dispatchers.Main) { + result.success(available) + } + } catch (e: Throwable) { + withContext(Dispatchers.Main) { + result.error("tag_check_failed", e.localizedMessage ?: "Error", e) + } + } + } + } + Methods.IsVpnConnected.method -> { scope.launch { runCatching { diff --git a/ios/Runner/Handlers/MethodHandler.swift b/ios/Runner/Handlers/MethodHandler.swift index f5cf3b982f..dbfe80d157 100644 --- a/ios/Runner/Handlers/MethodHandler.swift +++ b/ios/Runner/Handlers/MethodHandler.swift @@ -32,6 +32,12 @@ class MethodHandler { case "startVPN": self.startVPN(result: result) + case "isTagAvailable": + guard let tag: String = self.decodeValue(from: call.arguments, result: result) else { + return + } + self.isTagAvailable(result: result, tag: tag) + case "connectToServer": guard let data = self.decodeDict(from: call.arguments, result: result) else { return } self.connectToServer(result: result, data: data) @@ -301,6 +307,11 @@ class MethodHandler { } } + private func isTagAvailable(result: @escaping FlutterResult, tag: String) { + let available = MobileIsTagAvailable(tag) + result(available) + } + private func connectToServer(result: @escaping FlutterResult, data: [String: Any]) { Task { do { diff --git a/lantern-core/ffi/ffi.go b/lantern-core/ffi/ffi.go index b88bdb169a..9bf94982ba 100644 --- a/lantern-core/ffi/ffi.go +++ b/lantern-core/ffi/ffi.go @@ -277,6 +277,30 @@ func getAutoLocation() *C.char { return C.CString(string(jsonBytes)) } +// isTagAvailable checks if a server with the given tag exists in the server list. +// Returns "true" if found, "false" if not found, or "true" when the check cannot be +// performed (fail-open: allows connection attempts to proceed normally). +// +//export isTagAvailable +func isTagAvailable(_tag *C.char) *C.char { + tag := C.GoString(_tag) + c, errStr := requireCore() + if errStr != nil { + slog.Warn("Unable to check tag availability (core not ready), assuming available", "tag", tag) + C.free(unsafe.Pointer(errStr)) + return C.CString("true") + } + _, found, err := c.GetServerByTagJSON(tag) + if err != nil { + slog.Warn("Error checking tag availability, assuming available", "tag", tag, "error", err) + return C.CString("true") + } + if found { + return C.CString("true") + } + return C.CString("false") +} + // startAutoLocationListener starts the auto location listener. // //export startAutoLocationListener diff --git a/lantern-core/mobile/mobile.go b/lantern-core/mobile/mobile.go index 7d817fb88e..e5391a57a4 100644 --- a/lantern-core/mobile/mobile.go +++ b/lantern-core/mobile/mobile.go @@ -180,6 +180,21 @@ func CloseIPC() error { return vpn_tunnel.CloseIPC() } +// IsTagAvailable checks if a server with the given tag exists in the server list. +// Returns true if the tag is found. Returns true when the check cannot be performed +// (fail-open: allows connection attempts to proceed normally). +func IsTagAvailable(tag string) bool { + found, err := withCoreR(func(c lanterncore.Core) (bool, error) { + _, ok, err := c.GetServerByTagJSON(tag) + return ok, err + }) + if err != nil { + slog.Warn("Unable to check tag availability, assuming available", "tag", tag, "error", err) + return true + } + return found +} + // ConnectToServer connects to a server using the provided location type and tag. // It works with private servers and lantern location servers. func ConnectToServer(locationType, tag string, platIfce utils.PlatformInterface, options *utils.Opts) error { diff --git a/lantern-core/vpn_tunnel/vpn_tunnel.go b/lantern-core/vpn_tunnel/vpn_tunnel.go index 0388e996c4..ed10e090a9 100644 --- a/lantern-core/vpn_tunnel/vpn_tunnel.go +++ b/lantern-core/vpn_tunnel/vpn_tunnel.go @@ -35,7 +35,7 @@ func StartVPN(platform rvpn.PlatformInterface, opts *utils.Opts) error { } // it should use InternalTagLantern so it will connect to best lantern server by default. // if you want to connect to user server, use ConnectToServer with InternalTagUser - err := vpn.QuickConnect("", platform) + err := vpn.AutoConnect("") if err != nil { return fmt.Errorf("failed to start VPN: %w", err) } diff --git a/lib/features/vpn/provider/vpn_notifier.dart b/lib/features/vpn/provider/vpn_notifier.dart index 2514a2faf3..d19c77162f 100644 --- a/lib/features/vpn/provider/vpn_notifier.dart +++ b/lib/features/vpn/provider/vpn_notifier.dart @@ -18,45 +18,42 @@ class VpnNotifier extends _$VpnNotifier { @override VPNStatus build() { ref.read(lanternServiceProvider).isVPNConnected(); - ref.listen( - vPNStatusProvider, - (previous, next) { - final previousStatus = previous?.value?.status; - final nextStatus = next.value!.status; + ref.listen(vPNStatusProvider, (previous, next) { + final previousStatus = previous?.value?.status; + final nextStatus = next.value!.status; - if (previous != null && - previous.value != null && - previousStatus != nextStatus) { - if (previousStatus != VPNStatus.connecting && - nextStatus == VPNStatus.disconnected) { - sl().showNotification( - id: NotificationEvent.vpnDisconnected.id, - title: 'app_name'.i18n, - body: 'vpn_disconnected'.i18n, - ); - } else if (nextStatus == VPNStatus.connected) { - if (PlatformUtils.isMobile) { - HapticFeedback.mediumImpact(); - } + if (previous != null && + previous.value != null && + previousStatus != nextStatus) { + if (previousStatus != VPNStatus.connecting && + nextStatus == VPNStatus.disconnected) { + sl().showNotification( + id: NotificationEvent.vpnDisconnected.id, + title: 'app_name'.i18n, + body: 'vpn_disconnected'.i18n, + ); + } else if (nextStatus == VPNStatus.connected) { + if (PlatformUtils.isMobile) { + HapticFeedback.mediumImpact(); + } - /// Mark successful connection in app settings - ref.read(appSettingProvider.notifier).setSuccessfulConnection(true); + /// Mark successful connection in app settings + ref.read(appSettingProvider.notifier).setSuccessfulConnection(true); - // Server location is updated via the "server-location" push event - // from the Go side (handled by AppEventNotifier), not by polling - // getAutoServerLocation here. This avoids a race where the NE - // reports "connected" before the Go tunnel is fully ready. + // Server location is updated via the "server-location" push event + // from the Go side (handled by AppEventNotifier), not by polling + // getAutoServerLocation here. This avoids a race where the NE + // reports "connected" before the Go tunnel is fully ready. - sl().showNotification( - id: NotificationEvent.vpnConnected.id, - title: 'app_name'.i18n, - body: 'vpn_connected'.i18n, - ); - } + sl().showNotification( + id: NotificationEvent.vpnConnected.id, + title: 'app_name'.i18n, + body: 'vpn_connected'.i18n, + ); } - state = nextStatus; - }, - ); + } + state = nextStatus; + }); return VPNStatus.disconnected; } @@ -81,14 +78,17 @@ class VpnNotifier extends _$VpnNotifier { final type = serverLocation.serverType.toServerLocationType; if (type == ServerLocationType.auto || force) { appLogger.debug( - 'Got server location with type auto or force is true, starting VPN with auto'); + 'Got server location with type auto or force is true, starting VPN with auto', + ); return lantern.startVPN(); } final tag = serverLocation.serverName; final tagAvailable = await lantern.isTagAvailable(tag); if (!tagAvailable) { - appLogger.debug('Server tag "$tag" not available, falling back to auto VPN'); + appLogger.debug( + 'Server tag "$tag" not available, falling back to auto VPN', + ); return lantern.startVPN(); } return connectToServer(type, tag); @@ -97,7 +97,9 @@ class VpnNotifier extends _$VpnNotifier { /// Connects to a specific server location. /// it supports lantern locations and private servers. Future> connectToServer( - ServerLocationType location, String tag) async { + ServerLocationType location, + String tag, + ) async { appLogger.debug("Connecting to server: $location with tag: $tag"); final result = await ref .read(lanternServiceProvider) diff --git a/lib/lantern/lantern_ffi_service.dart b/lib/lantern/lantern_ffi_service.dart index 151b945267..ae7895a430 100644 --- a/lib/lantern/lantern_ffi_service.dart +++ b/lib/lantern/lantern_ffi_service.dart @@ -192,7 +192,8 @@ class LanternFFIService implements LanternCoreService { final dataDir = await AppStorageUtils.getAppDirectory(); final logDir = await AppStorageUtils.getAppLogDirectory(); appLogger.info( - "Radiance configuration - env: $env, dataDir: ${dataDir.path}, logDir: $logDir, telemetryConsent: $consent"); + "Radiance configuration - env: $env, dataDir: ${dataDir.path}, logDir: $logDir, telemetryConsent: $consent", + ); final dataDirPtr = dataDir.path.toCharPtr; final logDirPtr = logDir.toCharPtr; @@ -218,10 +219,7 @@ class LanternFFIService implements LanternCoreService { checkAPIError(result); if (result != 'ok' && result != 'true') { - throw PlatformException( - code: 'radiance_setup_failed', - message: result, - ); + throw PlatformException(code: 'radiance_setup_failed', message: result); } return right(unit); } catch (e, st) { @@ -340,8 +338,9 @@ class LanternFFIService implements LanternCoreService { }); checkAPIError(enabledJson); - final enabledKeys = - (jsonDecode(enabledJson) as List).cast().toSet(); + final enabledKeys = (jsonDecode(enabledJson) as List) + .cast() + .toSet(); final decoded = jsonDecode(jsonApps) as List; final rawApps = decoded.cast>(); @@ -429,7 +428,8 @@ class LanternFFIService implements LanternCoreService { return left( Failure( error: result['error'] ?? 'Unknown error', - localizedErrorMessage: result['localizedErrorMessage'] ?? + localizedErrorMessage: + result['localizedErrorMessage'] ?? result['error'] ?? 'Unknown error', ), @@ -525,8 +525,9 @@ class LanternFFIService implements LanternCoreService { return left( Failure( error: e.toString(), - localizedErrorMessage: - (e is Exception) ? e.localizedDescription : e.toString(), + localizedErrorMessage: (e is Exception) + ? e.localizedDescription + : e.toString(), ), ); } finally { @@ -623,11 +624,28 @@ class LanternFFIService implements LanternCoreService { Future isTagAvailable(String tag) async { try { final result = await runInBackground(() async { - return _ffiService.isTagAvailable(tag.toCharPtr).toDartString(); + final tagPtr = tag.toCharPtr; + try { + final resultPtr = _ffiService.isTagAvailable(tagPtr); + if (resultPtr == nullptr) { + return 'true'; + } + try { + return resultPtr.toDartString(); + } finally { + _ffiService.freeCString(resultPtr); + } + } finally { + malloc.free(tagPtr); + } }); return result == 'true'; } catch (e, st) { - appLogger.error('Error checking tag availability, assuming available', e, st); + appLogger.error( + 'Error checking tag availability, assuming available', + e, + st, + ); return true; } } @@ -1253,19 +1271,21 @@ class LanternFFIService implements LanternCoreService { } @override - Future> addServerBasedOnURLs( - {required String urls, - required bool skipCertVerification, - required String serverName}) async { + Future> addServerBasedOnURLs({ + required String urls, + required bool skipCertVerification, + required String serverName, + }) async { try { - final result = await runInBackground( - () async { - return _ffiService - .addServerBasedOnURLs(urls.toCharPtr, - skipCertVerification ? 1 : 0, serverName.toCharPtr) - .toDartString(); - }, - ); + final result = await runInBackground(() async { + return _ffiService + .addServerBasedOnURLs( + urls.toCharPtr, + skipCertVerification ? 1 : 0, + serverName.toCharPtr, + ) + .toDartString(); + }); checkAPIError(result); return Right(unit); } catch (e, stackTrace) { @@ -1275,11 +1295,12 @@ class LanternFFIService implements LanternCoreService { } @override - Future> inviteToServerManagerInstance( - {required String ip, - required String port, - required String accessToken, - required String inviteName}) async { + Future> inviteToServerManagerInstance({ + required String ip, + required String port, + required String accessToken, + required String inviteName, + }) async { try { final result = await runInBackground(() async { return _ffiService diff --git a/lib/lantern/lantern_generated_bindings.dart b/lib/lantern/lantern_generated_bindings.dart index b2caf2cb30..597c62d6d8 100644 --- a/lib/lantern/lantern_generated_bindings.dart +++ b/lib/lantern/lantern_generated_bindings.dart @@ -2602,6 +2602,19 @@ class LanternBindings { late final _getAutoLocation = _getAutoLocationPtr.asFunction Function()>(); + ffi.Pointer isTagAvailable( + ffi.Pointer _tag, + ) { + return _isTagAvailable(_tag); + } + + late final _isTagAvailablePtr = _lookup< + ffi.NativeFunction< + ffi.Pointer Function( + ffi.Pointer)>>('isTagAvailable'); + late final _isTagAvailable = _isTagAvailablePtr + .asFunction Function(ffi.Pointer)>(); + ffi.Pointer startAutoLocationListener() { return _startAutoLocationListener(); } diff --git a/lib/lantern/lantern_platform_service.dart b/lib/lantern/lantern_platform_service.dart index f5ad2d8f20..47c095cc99 100644 --- a/lib/lantern/lantern_platform_service.dart +++ b/lib/lantern/lantern_platform_service.dart @@ -1107,6 +1107,17 @@ class LanternPlatformService implements LanternCoreService { } } + @override + Future isTagAvailable(String tag) async { + try { + final result = + await _methodChannel.invokeMethod('isTagAvailable', tag); + return result ?? true; + } catch (e) { + return true; + } + } + @override Future> connectToServer( String location, String tag) async { diff --git a/macos/Runner/Handlers/MethodHandler.swift b/macos/Runner/Handlers/MethodHandler.swift index 17259c5162..3854bcb1d1 100644 --- a/macos/Runner/Handlers/MethodHandler.swift +++ b/macos/Runner/Handlers/MethodHandler.swift @@ -31,6 +31,12 @@ class MethodHandler { case "startVPN": self.startVPN(result: result) + case "isTagAvailable": + guard let tag: String = self.decodeValue(from: call.arguments, result: result) else { + return + } + self.isTagAvailable(result: result, tag: tag) + case "connectToServer": guard let data = self.decodeDict(from: call.arguments, result: result) else { return } self.connectToServer(result: result, data: data) @@ -327,6 +333,11 @@ class MethodHandler { } } + private func isTagAvailable(result: @escaping FlutterResult, tag: String) { + let available = MobileIsTagAvailable(tag) + result(available) + } + private func connectToServer(result: @escaping FlutterResult, data: [String: Any]) { Task { do {