From 700b056cd2edf578866b1cdf1e7a0307804f58fe Mon Sep 17 00:00:00 2001 From: Gabi Date: Wed, 4 Sep 2024 12:16:50 -0700 Subject: [PATCH] fix(ui): make internet resource off by default (#6518) With this PR we made internet resource disabled by default. Since no other resource is disalable and internet resource behavior is particular we remove all client code to make non internet resource disalable. Also, since the portal never makes the internet resource that can't be disabled we remove the whole code path to handle that. Additionally, some other smaller refactors across the UI wrt internet resource Fix #6509 --------- Signed-off-by: conectado Co-authored-by: conectado Co-authored-by: Thomas Eizinger --- .../firezone/android/core/data/Repository.kt | 47 +++++++++-- .../session/ui/ResourceDetailsBottomSheet.kt | 36 ++++----- .../features/session/ui/ResourceViewModel.kt | 44 +++++++++++ .../features/session/ui/ResourcesAdapter.kt | 24 +++--- .../features/session/ui/SessionActivity.kt | 21 +++-- .../features/session/ui/SessionViewModel.kt | 19 ++++- .../features/session/ui/ViewResource.kt | 37 --------- .../firezone/android/tunnel/TunnelService.kt | 48 +++++++----- .../firezone/android/tunnel/model/Resource.kt | 5 +- rust/connlib/shared/src/callbacks.rs | 13 ---- rust/connlib/shared/src/messages/client.rs | 8 +- rust/connlib/tunnel/src/proptest.rs | 11 +-- rust/gui-client/src-tauri/src/client/gui.rs | 40 ++++++---- .../src-tauri/src/client/gui/system_tray.rs | 77 +++++++++++++------ .../src/client/gui/system_tray/builder.rs | 8 +- .../src-tauri/src/client/settings.rs | 12 ++- .../FirezoneKit/Managers/TunnelManager.swift | 42 +++++----- .../Sources/FirezoneKit/Models/Resource.swift | 9 ++- .../Sources/FirezoneKit/Models/Settings.swift | 20 +++-- .../Sources/FirezoneKit/Stores/Store.swift | 10 +-- .../Sources/FirezoneKit/Views/MenuBar.swift | 70 +++++++++++------ .../FirezoneKit/Views/ResourceView.swift | 24 +++--- .../FirezoneKit/Views/SessionView.swift | 20 ++++- .../FirezoneNetworkExtension/Adapter.swift | 22 +++--- .../PacketTunnelProvider.swift | 12 +-- 25 files changed, 409 insertions(+), 270 deletions(-) create mode 100644 kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourceViewModel.kt delete mode 100644 kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ViewResource.kt diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/Repository.kt b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/Repository.kt index fb8aca0b3..8100fb855 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/core/data/Repository.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/core/data/Repository.kt @@ -4,6 +4,7 @@ package dev.firezone.android.core.data import android.content.Context import android.content.SharedPreferences import com.google.gson.Gson +import com.google.gson.annotations.SerializedName import com.google.gson.reflect.TypeToken import dev.firezone.android.BuildConfig import dev.firezone.android.core.data.model.Config @@ -14,6 +15,40 @@ import kotlinx.coroutines.flow.flowOn import java.security.MessageDigest import javax.inject.Inject +const val ON_SYMBOL: String = "<->" +const val OFF_SYMBOL: String = " — " + +enum class ResourceState { + @SerializedName("enabled") + ENABLED, + + @SerializedName("disabled") + DISABLED, + + @SerializedName("unset") + UNSET, +} + +fun ResourceState.isEnabled(): Boolean { + return this == ResourceState.ENABLED +} + +fun ResourceState.stateSymbol(): String { + return if (this.isEnabled()) { + ON_SYMBOL + } else { + OFF_SYMBOL + } +} + +fun ResourceState.toggle(): ResourceState { + return if (this.isEnabled()) { + ResourceState.DISABLED + } else { + ResourceState.ENABLED + } +} + internal class Repository @Inject constructor( @@ -94,14 +129,14 @@ internal class Repository .putString(DEVICE_ID_KEY, value) .apply() - fun getDisabledResourcesSync(): Set { - val jsonString = sharedPreferences.getString(DISABLED_RESOURCES_KEY, null) ?: return hashSetOf() - val type = object : TypeToken>() {}.type + fun getInternetResourceStateSync(): ResourceState { + val jsonString = sharedPreferences.getString(ENABLED_INTERNET_RESOURCE_KEY, null) ?: return ResourceState.UNSET + val type = object : TypeToken() {}.type return Gson().fromJson(jsonString, type) } - fun saveDisabledResourcesSync(value: Set): Unit = - sharedPreferences.edit().putString(DISABLED_RESOURCES_KEY, Gson().toJson(value)) + fun saveInternetResourceStateSync(value: ResourceState): Unit = + sharedPreferences.edit().putString(ENABLED_INTERNET_RESOURCE_KEY, Gson().toJson(value)) .apply() fun saveNonce(value: String): Flow = @@ -183,6 +218,6 @@ internal class Repository private const val NONCE_KEY = "nonce" private const val STATE_KEY = "state" private const val DEVICE_ID_KEY = "deviceId" - private const val DISABLED_RESOURCES_KEY = "disabledResources" + private const val ENABLED_INTERNET_RESOURCE_KEY = "enabledInternetResource" } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourceDetailsBottomSheet.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourceDetailsBottomSheet.kt index 74182a31d..9ed4c1de9 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourceDetailsBottomSheet.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourceDetailsBottomSheet.kt @@ -21,9 +21,14 @@ import androidx.fragment.app.activityViewModels import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.button.MaterialButton import dev.firezone.android.R +import dev.firezone.android.core.data.ResourceState +import dev.firezone.android.core.data.isEnabled import dev.firezone.android.tunnel.model.StatusEnum -class ResourceDetailsBottomSheet(private val resource: ViewResource, private val activity: SessionActivity) : BottomSheetDialogFragment() { +class ResourceDetailsBottomSheet( + private val resource: ResourceViewModel, + private val internetResourceToggle: () -> ResourceState, +) : BottomSheetDialogFragment() { private lateinit var view: View private val viewModel: SessionViewModel by activityViewModels() @@ -51,8 +56,6 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource, private val resourceHeader() - refreshDisableToggleButton() - if (!resource.sites.isNullOrEmpty()) { val site = resource.sites.first() siteNameTextView.text = site.name @@ -89,11 +92,11 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource, private val } } - private fun resourceToggleText(): String { - if (resource.enabled) { - return "Disable this resource" + private fun resourceToggleText(resource: ResourceViewModel): String { + return if (resource.state.isEnabled()) { + "Disable this resource" } else { - return "Enable this resource" + "Enable this resource" } } @@ -122,6 +125,8 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource, private val resourceDescriptionLayout.visibility = View.VISIBLE resourceAddressDescriptionTextView.text = "All network traffic" + + refreshDisableToggleButton() } private fun nonInternetResourceHeader() { @@ -173,17 +178,12 @@ class ResourceDetailsBottomSheet(private val resource: ViewResource, private val } private fun refreshDisableToggleButton() { - if (resource.canBeDisabled) { - val toggleResourceEnabled: MaterialButton = view.findViewById(R.id.toggleResourceEnabled) - toggleResourceEnabled.visibility = View.VISIBLE - toggleResourceEnabled.text = resourceToggleText() - toggleResourceEnabled.setOnClickListener { - resource.enabled = !resource.enabled - - activity.onViewResourceToggled(resource) - - refreshDisableToggleButton() - } + val toggleResourceEnabled: MaterialButton = view.findViewById(R.id.toggleResourceEnabled) + toggleResourceEnabled.visibility = View.VISIBLE + toggleResourceEnabled.text = resourceToggleText(resource) + toggleResourceEnabled.setOnClickListener { + resource.state = internetResourceToggle() + refreshDisableToggleButton() } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourceViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourceViewModel.kt new file mode 100644 index 000000000..47b5ab13a --- /dev/null +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourceViewModel.kt @@ -0,0 +1,44 @@ +/* Licensed under Apache 2.0 (C) 2024 Firezone, Inc. */ +package dev.firezone.android.features.session.ui + +import dev.firezone.android.core.data.ResourceState +import dev.firezone.android.core.data.stateSymbol +import dev.firezone.android.tunnel.model.Resource +import dev.firezone.android.tunnel.model.ResourceType +import dev.firezone.android.tunnel.model.Site +import dev.firezone.android.tunnel.model.StatusEnum +import dev.firezone.android.tunnel.model.isInternetResource + +class ResourceViewModel(resource: Resource, resourceState: ResourceState) { + val id: String = resource.id + val type: ResourceType = resource.type + val address: String? = resource.address + val addressDescription: String? = resource.addressDescription + val sites: List? = resource.sites + val displayName: String = displayName(resource, resourceState) + val name: String = resource.name + val status: StatusEnum = resource.status + var state: ResourceState = resourceState +} + +fun displayName( + resource: Resource, + state: ResourceState, +): String { + return if (resource.isInternetResource()) { + internetResourceDisplayName(resource, state) + } else { + resource.name + } +} + +fun internetResourceDisplayName( + resource: Resource, + state: ResourceState, +): String { + return "${state.stateSymbol()} ${resource.name}" +} + +fun ResourceViewModel.isInternetResource(): Boolean { + return this.type == ResourceType.Internet +} diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourcesAdapter.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourcesAdapter.kt index 3f087ec3d..cd750d88f 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourcesAdapter.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ResourcesAdapter.kt @@ -8,11 +8,13 @@ import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import dev.firezone.android.core.data.ResourceState import dev.firezone.android.databinding.ListItemResourceBinding -internal class ResourcesAdapter(private val activity: SessionActivity) : ListAdapter( - ResourceDiffCallback(), -) { +internal class ResourcesAdapter(private val internetResourceToggle: () -> ResourceState) : + ListAdapter( + ResourceDiffCallback(), + ) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, @@ -31,14 +33,14 @@ internal class ResourcesAdapter(private val activity: SessionActivity) : ListAda // Show bottom sheet val fragmentManager = (holder.itemView.context as AppCompatActivity).supportFragmentManager - val bottomSheet = ResourceDetailsBottomSheet(resource, activity) + val bottomSheet = ResourceDetailsBottomSheet(resource, internetResourceToggle) bottomSheet.show(fragmentManager, "ResourceDetailsBottomSheet") } } class ViewHolder(private val binding: ListItemResourceBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(resource: ViewResource) { - binding.resourceNameText.text = resource.name + fun bind(resource: ResourceViewModel) { + binding.resourceNameText.text = resource.displayName if (resource.isInternetResource()) { binding.addressText.visibility = View.GONE } else { @@ -47,17 +49,17 @@ internal class ResourcesAdapter(private val activity: SessionActivity) : ListAda } } - class ResourceDiffCallback : DiffUtil.ItemCallback() { + class ResourceDiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame( - oldItem: ViewResource, - newItem: ViewResource, + oldItem: ResourceViewModel, + newItem: ResourceViewModel, ): Boolean { return oldItem.id == newItem.id } override fun areContentsTheSame( - oldItem: ViewResource, - newItem: ViewResource, + oldItem: ResourceViewModel, + newItem: ResourceViewModel, ): Boolean { return oldItem == newItem } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt index 785947966..322e22321 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionActivity.kt @@ -15,6 +15,8 @@ import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.tabs.TabLayout import dagger.hilt.android.AndroidEntryPoint +import dev.firezone.android.core.data.ResourceState +import dev.firezone.android.core.data.toggle import dev.firezone.android.databinding.ActivitySessionBinding import dev.firezone.android.features.settings.ui.SettingsActivity import dev.firezone.android.tunnel.TunnelService @@ -47,7 +49,7 @@ class SessionActivity : AppCompatActivity() { } } - private val resourcesAdapter = ResourcesAdapter(this) + private val resourcesAdapter = ResourcesAdapter { this.onInternetResourceToggled() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -72,9 +74,18 @@ class SessionActivity : AppCompatActivity() { super.onDestroy() } - fun onViewResourceToggled(resourceToggled: ViewResource) { - Log.d(TAG, "Resource toggled $resourceToggled") - tunnelService?.resourceToggled(resourceToggled) + fun internetState(): ResourceState { + return tunnelService?.internetState() ?: ResourceState.UNSET + } + + private fun onInternetResourceToggled(): ResourceState { + tunnelService?.let { + it.internetResourceToggled(internetState().toggle()) + refreshList() + Log.d(TAG, "Internet resource toggled ${internetState()}") + } + + return internetState() } private fun setupViews() { @@ -150,7 +161,7 @@ class SessionActivity : AppCompatActivity() { View.GONE } - resourcesAdapter.submitList(viewModel.resourcesList()) { + resourcesAdapter.submitList(viewModel.resourcesList(internetState())) { afterLoad() } } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionViewModel.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionViewModel.kt index 344006e18..00e9a0fba 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionViewModel.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/SessionViewModel.kt @@ -5,7 +5,10 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel import dev.firezone.android.core.data.Repository +import dev.firezone.android.core.data.ResourceState import dev.firezone.android.tunnel.TunnelService.Companion.State +import dev.firezone.android.tunnel.model.Resource +import dev.firezone.android.tunnel.model.isInternetResource import javax.inject.Inject @HiltViewModel @@ -16,14 +19,14 @@ internal class SessionViewModel internal lateinit var repo: Repository private val _favoriteResourcesLiveData = MutableLiveData>(HashSet()) private val _serviceStatusLiveData = MutableLiveData() - private val _resourcesLiveData = MutableLiveData>(emptyList()) + private val _resourcesLiveData = MutableLiveData>(emptyList()) private var showOnlyFavorites: Boolean = false val favoriteResourcesLiveData: MutableLiveData> get() = _favoriteResourcesLiveData val serviceStatusLiveData: MutableLiveData get() = _serviceStatusLiveData - val resourcesLiveData: MutableLiveData> + val resourcesLiveData: MutableLiveData> get() = _resourcesLiveData private val favoriteResources: HashSet @@ -56,8 +59,16 @@ internal class SessionViewModel fun clearToken() = repo.clearToken() // The subset of Resources to actually render - fun resourcesList(): List { - val resources = resourcesLiveData.value!! + fun resourcesList(isInternetResourceEnabled: ResourceState): List { + val resources = + resourcesLiveData.value!!.map { + if (it.isInternetResource()) { + ResourceViewModel(it, isInternetResourceEnabled) + } else { + ResourceViewModel(it, ResourceState.ENABLED) + } + } + return if (favoriteResources.isEmpty()) { resources } else if (showOnlyFavorites) { diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ViewResource.kt b/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ViewResource.kt deleted file mode 100644 index 75c0831c8..000000000 --- a/kotlin/android/app/src/main/java/dev/firezone/android/features/session/ui/ViewResource.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* Licensed under Apache 2.0 (C) 2024 Firezone, Inc. */ -package dev.firezone.android.features.session.ui - -import dev.firezone.android.tunnel.model.Resource -import dev.firezone.android.tunnel.model.ResourceType -import dev.firezone.android.tunnel.model.Site -import dev.firezone.android.tunnel.model.StatusEnum - -data class ViewResource( - val id: String, - val type: ResourceType, - val address: String?, - val addressDescription: String?, - val sites: List?, - val name: String, - val status: StatusEnum, - var enabled: Boolean, - var canBeDisabled: Boolean = true, -) - -fun Resource.toViewResource(enabled: Boolean): ViewResource { - return ViewResource( - id = this.id, - type = this.type, - address = this.address, - addressDescription = this.addressDescription, - sites = this.sites, - name = this.name, - status = this.status, - enabled = enabled, - canBeDisabled = this.canBeDisabled, - ) -} - -fun ViewResource.isInternetResource(): Boolean { - return this.type == ResourceType.Internet -} diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt index ba6f2f5d9..7b9bb498b 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/TunnelService.kt @@ -22,11 +22,12 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import dagger.hilt.android.AndroidEntryPoint import dev.firezone.android.core.data.Repository -import dev.firezone.android.features.session.ui.ViewResource -import dev.firezone.android.features.session.ui.toViewResource +import dev.firezone.android.core.data.ResourceState +import dev.firezone.android.core.data.isEnabled import dev.firezone.android.tunnel.callback.ConnlibCallback import dev.firezone.android.tunnel.model.Cidr import dev.firezone.android.tunnel.model.Resource +import dev.firezone.android.tunnel.model.isInternetResource import java.nio.file.Files import java.nio.file.Paths import java.util.UUID @@ -49,9 +50,9 @@ class TunnelService : VpnService() { var tunnelIpv6Address: String? = null private var tunnelDnsAddresses: MutableList = mutableListOf() private var tunnelRoutes: MutableList = mutableListOf() - private var _tunnelResources: List = emptyList() + private var _tunnelResources: List = emptyList() private var _tunnelState: State = State.DOWN - private var disabledResources: MutableSet = mutableSetOf() + var resourceState: ResourceState = ResourceState.UNSET // For reacting to changes to the network private var networkCallback: NetworkMonitor? = null @@ -67,7 +68,7 @@ class TunnelService : VpnService() { var startedByUser: Boolean = false var connlibSessionPtr: Long? = null - var tunnelResources: List + var tunnelResources: List get() = _tunnelResources set(value) { _tunnelResources = value @@ -82,7 +83,7 @@ class TunnelService : VpnService() { // Used to update the UI when the SessionActivity is bound to this service private var serviceStateLiveData: MutableLiveData? = null - private var resourcesLiveData: MutableLiveData>? = null + private var resourcesLiveData: MutableLiveData>? = null // For binding the SessionActivity view to this service private val binder = LocalBinder() @@ -99,7 +100,7 @@ class TunnelService : VpnService() { object : ConnlibCallback { override fun onUpdateResources(resourceListJSON: String) { moshi.adapter>().fromJson(resourceListJSON)?.let { - tunnelResources = it.map { resource -> resource.toViewResource(!disabledResources.contains(resource.id)) } + tunnelResources = it resourcesUpdated() } } @@ -257,24 +258,33 @@ class TunnelService : VpnService() { super.onRevoke() } + fun internetState(): ResourceState { + return resourceState + } + + fun internetResource(): Resource? { + return tunnelResources.firstOrNull { it.isInternetResource() } + } + // UI updates for resources fun resourcesUpdated() { - val newResources = tunnelResources.associateBy { it.id } - val currentlyDisabled = disabledResources.filter { newResources[it]?.canBeDisabled ?: false } + val currentlyDisabled = + if (internetResource() != null && !resourceState.isEnabled()) { + setOf(internetResource()!!.id) + } else { + emptySet() + } connlibSessionPtr?.let { ConnlibSession.setDisabledResources(it, Gson().toJson(currentlyDisabled)) } } - fun resourceToggled(resource: ViewResource) { - if (!resource.enabled) { - disabledResources.add(resource.id) - } else { - disabledResources.remove(resource.id) - } + fun internetResourceToggled(state: ResourceState) { + resourceState = state + + repo.saveInternetResourceStateSync(resourceState) - repo.saveDisabledResourcesSync(disabledResources) resourcesUpdated() } @@ -304,7 +314,7 @@ class TunnelService : VpnService() { private fun connect() { val token = appRestrictions.getString("token") ?: repo.getTokenSync() val config = repo.getConfigSync() - disabledResources = repo.getDisabledResourcesSync().toMutableSet() + resourceState = repo.getInternetResourceStateSync() if (!token.isNullOrBlank()) { tunnelState = State.CONNECTING @@ -377,7 +387,7 @@ class TunnelService : VpnService() { serviceStateLiveData?.postValue(tunnelState) } - fun setResourcesLiveData(liveData: MutableLiveData>) { + fun setResourcesLiveData(liveData: MutableLiveData>) { resourcesLiveData = liveData // Update the newly bound SessionActivity with our current resources @@ -388,7 +398,7 @@ class TunnelService : VpnService() { serviceStateLiveData?.postValue(state) } - private fun updateResourcesLiveData(resources: List) { + private fun updateResourcesLiveData(resources: List) { resourcesLiveData?.postValue(resources) } diff --git a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Resource.kt b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Resource.kt index 339ecacc3..14411b0c3 100644 --- a/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Resource.kt +++ b/kotlin/android/app/src/main/java/dev/firezone/android/tunnel/model/Resource.kt @@ -16,9 +16,12 @@ data class Resource( val sites: List?, val name: String, val status: StatusEnum, - @Json(name = "can_be_disabled") val canBeDisabled: Boolean, ) : Parcelable +fun Resource.isInternetResource(): Boolean { + return this.type == ResourceType.Internet +} + enum class ResourceType { @Json(name = "dns") DNS, diff --git a/rust/connlib/shared/src/callbacks.rs b/rust/connlib/shared/src/callbacks.rs index f63056ce4..5c0b917fd 100644 --- a/rust/connlib/shared/src/callbacks.rs +++ b/rust/connlib/shared/src/callbacks.rs @@ -71,14 +71,6 @@ impl ResourceDescription { } } - pub fn can_be_disabled(&self) -> bool { - match self { - ResourceDescription::Dns(r) => r.can_be_disabled, - ResourceDescription::Cidr(r) => r.can_be_disabled, - ResourceDescription::Internet(r) => r.can_be_disabled, - } - } - pub fn is_internet_resource(&self) -> bool { matches!(self, ResourceDescription::Internet(_)) } @@ -99,7 +91,6 @@ pub struct ResourceDescriptionDns { pub sites: Vec, pub status: Status, - pub can_be_disabled: bool, } /// Description of a resource that maps to a CIDR. @@ -118,7 +109,6 @@ pub struct ResourceDescriptionCidr { pub sites: Vec, pub status: Status, - pub can_be_disabled: bool, } /// Description of an Internet resource @@ -131,7 +121,6 @@ pub struct ResourceDescriptionInternet { pub sites: Vec, pub status: Status, - pub can_be_disabled: bool, } impl PartialOrd for ResourceDescription { @@ -176,7 +165,6 @@ mod tests { id: "99ba0c1e-5189-4cfc-a4db-fd6cb1c937fd".parse().unwrap(), }], status: Status::Online, - can_be_disabled: false, }) } @@ -189,7 +177,6 @@ mod tests { id: "99ba0c1e-5189-4cfc-a4db-fd6cb1c937fd".parse().unwrap(), }], status: Status::Offline, - can_be_disabled: true, }) } diff --git a/rust/connlib/shared/src/messages/client.rs b/rust/connlib/shared/src/messages/client.rs index 3443e960e..3c7c29623 100644 --- a/rust/connlib/shared/src/messages/client.rs +++ b/rust/connlib/shared/src/messages/client.rs @@ -36,7 +36,6 @@ impl ResourceDescriptionDns { name: self.name, address_description: self.address_description, sites: self.sites, - can_be_disabled: false, status, } } @@ -67,14 +66,13 @@ impl ResourceDescriptionCidr { name: self.name, address_description: self.address_description, sites: self.sites, - can_be_disabled: false, status, } } } fn internet_resource_name() -> String { - "<-> Internet Resource".to_string() + "Internet Resource".to_string() } /// Description of an internet resource. @@ -90,9 +88,6 @@ pub struct ResourceDescriptionInternet { /// Sites for the internet resource #[serde(rename = "gateway_groups")] pub sites: Vec, - /// Whether or not resource can be disabled from UI - #[serde(default)] - pub can_be_disabled: bool, } impl ResourceDescriptionInternet { @@ -101,7 +96,6 @@ impl ResourceDescriptionInternet { name: self.name, id: self.id, sites: self.sites, - can_be_disabled: self.can_be_disabled, status, } } diff --git a/rust/connlib/tunnel/src/proptest.rs b/rust/connlib/tunnel/src/proptest.rs index e8b05b7e5..9ee594caf 100644 --- a/rust/connlib/tunnel/src/proptest.rs +++ b/rust/connlib/tunnel/src/proptest.rs @@ -78,13 +78,10 @@ pub fn cidr_resource( pub fn internet_resource( sites: impl Strategy>, ) -> impl Strategy { - (resource_id(), sites, any::()).prop_map(move |(id, sites, can_be_disabled)| { - ResourceDescriptionInternet { - name: "Internet Resource".to_string(), - id, - sites, - can_be_disabled, - } + (resource_id(), sites).prop_map(move |(id, sites)| ResourceDescriptionInternet { + name: "Internet Resource".to_string(), + id, + sites, }) } diff --git a/rust/gui-client/src-tauri/src/client/gui.rs b/rust/gui-client/src-tauri/src/client/gui.rs index 6155c5e7c..1e9af17cb 100644 --- a/rust/gui-client/src-tauri/src/client/gui.rs +++ b/rust/gui-client/src-tauri/src/client/gui.rs @@ -17,6 +17,7 @@ use firezone_headless_client::{ }; use secrecy::{ExposeSecret, SecretString}; use std::{ + collections::BTreeSet, path::PathBuf, str::FromStr, time::{Duration, Instant}, @@ -441,6 +442,16 @@ impl Status { | Status::WaitingForTunnel { .. } => true, } } + + fn internet_resource(&self) -> Option { + #[allow(clippy::wildcard_enum_match_arm)] + match self { + Status::TunnelReady { resources } => { + resources.iter().find(|r| r.is_internet_resource()).cloned() + } + _ => None, + } + } } struct Controller { @@ -629,12 +640,12 @@ impl Controller { self.refresh_favorite_resources().await?; } Req::SystemTrayMenu(TrayMenuEvent::RetryPortalConnection) => self.try_retry_connection().await?, - Req::SystemTrayMenu(TrayMenuEvent::EnableResource(resource_id)) => { - self.advanced_settings.disabled_resources.remove(&resource_id); + Req::SystemTrayMenu(TrayMenuEvent::EnableInternetResource) => { + self.advanced_settings.internet_resource_enabled = Some(true); self.update_disabled_resources().await?; } - Req::SystemTrayMenu(TrayMenuEvent::DisableResource(resource_id)) => { - self.advanced_settings.disabled_resources.insert(resource_id); + Req::SystemTrayMenu(TrayMenuEvent::DisableInternetResource) => { + self.advanced_settings.internet_resource_enabled = Some(false); self.update_disabled_resources().await?; } Req::SystemTrayMenu(TrayMenuEvent::ShowWindow(window)) => { @@ -792,15 +803,16 @@ impl Controller { async fn update_disabled_resources(&mut self) -> Result<()> { settings::save(&self.advanced_settings).await?; - let Status::TunnelReady { resources } = &self.status else { - bail!("Tunnel is not ready"); - }; + let internet_resource = self + .status + .internet_resource() + .context("Tunnel not ready")?; - let disabled_resources = resources - .iter() - .filter_map(|r| r.can_be_disabled().then_some(r.id())) - .filter(|id| self.advanced_settings.disabled_resources.contains(id)) - .collect(); + let mut disabled_resources = BTreeSet::new(); + + if !self.advanced_settings.internet_resource_enabled() { + disabled_resources.insert(internet_resource.id()); + } self.ipc_client .send_msg(&SetDisabledResources(disabled_resources)) @@ -832,7 +844,9 @@ impl Controller { system_tray::ConnlibState::SignedIn(system_tray::SignedIn { actor_name: &auth_session.actor_name, favorite_resources: &self.advanced_settings.favorite_resources, - disabled_resources: &self.advanced_settings.disabled_resources, + internet_resource_enabled: &self + .advanced_settings + .internet_resource_enabled, resources, }) } diff --git a/rust/gui-client/src-tauri/src/client/gui/system_tray.rs b/rust/gui-client/src-tauri/src/client/gui/system_tray.rs index fe975dea8..7f5e3905e 100644 --- a/rust/gui-client/src-tauri/src/client/gui/system_tray.rs +++ b/rust/gui-client/src-tauri/src/client/gui/system_tray.rs @@ -35,6 +35,9 @@ const NO_ACTIVITY: &str = "[-] No activity"; const GATEWAY_CONNECTED: &str = "[O] Gateway connected"; const ALL_GATEWAYS_OFFLINE: &str = "[X] All Gateways offline"; +const ENABLED_SYMBOL: &str = "<->"; +const DISABLED_SYMBOL: &str = "—"; + const ADD_FAVORITE: &str = "Add to favorites"; const REMOVE_FAVORITE: &str = "Remove from favorites"; const FAVORITE_RESOURCES: &str = "Favorite Resources"; @@ -82,7 +85,7 @@ pub(crate) struct SignedIn<'a> { pub(crate) actor_name: &'a str, pub(crate) favorite_resources: &'a HashSet, pub(crate) resources: &'a [ResourceDescription], - pub(crate) disabled_resources: &'a HashSet, + pub(crate) internet_resource_enabled: &'a Option, } impl<'a> SignedIn<'a> { @@ -103,17 +106,17 @@ impl<'a> SignedIn<'a> { fn resource_submenu(&self, res: &ResourceDescription) -> Menu { let mut submenu = Menu::default().resource_description(res); - if !res.is_internet_resource() { - self.add_favorite_toggle(&mut submenu, res.id()); + if res.is_internet_resource() { + submenu.add_separator(); + if self.is_internet_resource_enabled() { + submenu.add_item(item(Event::DisableInternetResource, DISABLE)); + } else { + submenu.add_item(item(Event::EnableInternetResource, ENABLE)); + } } - if res.can_be_disabled() { - submenu.add_separator(); - if self.is_enabled(res) { - submenu.add_item(item(Event::DisableResource(res.id()), DISABLE)); - } else { - submenu.add_item(item(Event::EnableResource(res.id()), ENABLE)); - } + if !res.is_internet_resource() { + self.add_favorite_toggle(&mut submenu, res.id()); } if let Some(site) = res.sites().first() { @@ -134,8 +137,8 @@ impl<'a> SignedIn<'a> { } } - fn is_enabled(&self, res: &ResourceDescription) -> bool { - !self.disabled_resources.contains(&res.id()) + fn is_internet_resource_enabled(&self) -> bool { + self.internet_resource_enabled.unwrap_or_default() } } @@ -259,11 +262,22 @@ impl<'a> AppState<'a> { } } +fn append_status(name: &str, enabled: bool) -> String { + let symbol = if enabled { + ENABLED_SYMBOL + } else { + DISABLED_SYMBOL + }; + + format!("{symbol} {name}") +} + fn signed_in(signed_in: &SignedIn) -> Menu { let SignedIn { actor_name, favorite_resources, resources, // Make sure these are presented in the order we receive them + internet_resource_enabled, .. } = signed_in; @@ -288,7 +302,12 @@ fn signed_in(signed_in: &SignedIn) -> Menu { .iter() .filter(|res| favorite_resources.contains(&res.id()) || res.is_internet_resource()) { - menu = menu.add_submenu(res.name(), signed_in.resource_submenu(res)); + let mut name = res.name().to_string(); + if res.is_internet_resource() { + name = append_status(&name, internet_resource_enabled.unwrap_or_default()); + } + + menu = menu.add_submenu(name, signed_in.resource_submenu(res)); } } else { // No favorites, show every Resource normally, just like before @@ -296,7 +315,12 @@ fn signed_in(signed_in: &SignedIn) -> Menu { // Always show Resources in the original order menu = menu.disabled(RESOURCES); for res in *resources { - menu = menu.add_submenu(res.name(), signed_in.resource_submenu(res)); + let mut name = res.name().to_string(); + if res.is_internet_resource() { + name = append_status(&name, internet_resource_enabled.unwrap_or_default()); + } + + menu = menu.add_submenu(name, signed_in.resource_submenu(res)); } } @@ -387,14 +411,14 @@ mod tests { fn signed_in<'a>( resources: &'a [ResourceDescription], favorite_resources: &'a HashSet, - disabled_resources: &'a HashSet, + internet_resource_enabled: &'a Option, ) -> AppState<'a> { AppState { connlib: ConnlibState::SignedIn(SignedIn { actor_name: "Jane Doe", favorite_resources, resources, - disabled_resources, + internet_resource_enabled, }), release: None, } @@ -409,8 +433,7 @@ mod tests { "address": "172.172.0.0/16", "address_description": "cidr resource", "sites": [{"name": "test", "id": "bf56f32d-7b2c-4f5d-a784-788977d014a4"}], - "status": "Unknown", - "can_be_disabled": false + "status": "Unknown" }, { "id": "03000143-e25e-45c7-aafb-144990e57dcd", @@ -419,8 +442,7 @@ mod tests { "address": "gitlab.mycorp.com", "address_description": "https://gitlab.mycorp.com", "sites": [{"name": "test", "id": "bf56f32d-7b2c-4f5d-a784-788977d014a4"}], - "status": "Online", - "can_be_disabled": false + "status": "Online" }, { "id": "1106047c-cd5d-4151-b679-96b93da7383b", @@ -428,8 +450,7 @@ mod tests { "name": "Internet Resource", "address": "All internet addresses", "sites": [{"name": "test", "id": "eb94482a-94f4-47cb-8127-14fb3afa5516"}], - "status": "Offline", - "can_be_disabled": false + "status": "Offline" } ]"#; @@ -534,10 +555,12 @@ mod tests { .copyable(GATEWAY_CONNECTED), ) .add_submenu( - "Internet Resource", + "— Internet Resource", Menu::default() .disabled(INTERNET_RESOURCE_DESCRIPTION) .separator() + .item(Event::EnableInternetResource, ENABLE) + .separator() .disabled("Site") .copyable("test") .copyable(ALL_GATEWAYS_OFFLINE), @@ -588,10 +611,12 @@ mod tests { .copyable(GATEWAY_CONNECTED), ) .add_submenu( - "Internet Resource", + "— Internet Resource", Menu::default() .disabled(INTERNET_RESOURCE_DESCRIPTION) .separator() + .item(Event::EnableInternetResource, ENABLE) + .separator() .disabled("Site") .copyable("test") .copyable(ALL_GATEWAYS_OFFLINE), @@ -687,10 +712,12 @@ mod tests { .copyable(GATEWAY_CONNECTED), ) .add_submenu( - "Internet Resource", + "— Internet Resource", Menu::default() .disabled(INTERNET_RESOURCE_DESCRIPTION) .separator() + .item(Event::EnableInternetResource, ENABLE) + .separator() .disabled("Site") .copyable("test") .copyable(ALL_GATEWAYS_OFFLINE), diff --git a/rust/gui-client/src-tauri/src/client/gui/system_tray/builder.rs b/rust/gui-client/src-tauri/src/client/gui/system_tray/builder.rs index 6948de95c..d2f8169a5 100644 --- a/rust/gui-client/src-tauri/src/client/gui/system_tray/builder.rs +++ b/rust/gui-client/src-tauri/src/client/gui/system_tray/builder.rs @@ -67,10 +67,10 @@ pub(crate) enum Event { Url(Url), /// Quits the app, without signing the user out Quit, - /// A resource was enabled in the UI - EnableResource(ResourceId), - /// A resource was disabled in the UI - DisableResource(ResourceId), + /// The internet resource was enabled + EnableInternetResource, + /// The internet resource was disabled + DisableInternetResource, } #[derive(Debug, Deserialize, PartialEq, Serialize)] diff --git a/rust/gui-client/src-tauri/src/client/settings.rs b/rust/gui-client/src-tauri/src/client/settings.rs index ff0e4140d..328ab36e7 100644 --- a/rust/gui-client/src-tauri/src/client/settings.rs +++ b/rust/gui-client/src-tauri/src/client/settings.rs @@ -18,7 +18,7 @@ pub(crate) struct AdvancedSettings { #[serde(default)] pub favorite_resources: HashSet, #[serde(default)] - pub disabled_resources: HashSet, + pub internet_resource_enabled: Option, pub log_filter: String, } @@ -29,7 +29,7 @@ impl Default for AdvancedSettings { auth_base_url: Url::parse("https://app.firez.one").unwrap(), api_url: Url::parse("wss://api.firez.one").unwrap(), favorite_resources: Default::default(), - disabled_resources: Default::default(), + internet_resource_enabled: Default::default(), log_filter: "firezone_gui_client=debug,info".to_string(), } } @@ -42,12 +42,18 @@ impl Default for AdvancedSettings { auth_base_url: Url::parse("https://app.firezone.dev").unwrap(), api_url: Url::parse("wss://api.firezone.dev").unwrap(), favorite_resources: Default::default(), - disabled_resources: Default::default(), + internet_resource_enabled: Default::default(), log_filter: "info".to_string(), } } } +impl AdvancedSettings { + pub fn internet_resource_enabled(&self) -> bool { + self.internet_resource_enabled.is_some_and(|v| v) + } +} + pub(crate) fn advanced_settings_path() -> Result { Ok(known_dirs::settings() .context("`known_dirs::settings` failed")? diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/TunnelManager.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/TunnelManager.swift index cb2814804..c9be6aa2a 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/TunnelManager.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Managers/TunnelManager.swift @@ -20,13 +20,13 @@ public enum TunnelManagerKeys { static let authBaseURL = "authBaseURL" static let apiURL = "apiURL" public static let logFilter = "logFilter" - public static let disabledResources = "disabledResources" + public static let internetResourceEnabled = "internetResourceEnabled" } public enum TunnelMessage: Codable { case getResourceList(Data) case signOut - case setDisabledResources(Set) + case internetResourceEnabled(Bool) enum CodingKeys: String, CodingKey { case type @@ -36,16 +36,16 @@ public enum TunnelMessage: Codable { enum MessageType: String, Codable { case getResourceList case signOut - case setDisabledResources + case internetResourceEnabled } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let type = try container.decode(MessageType.self, forKey: .type) switch type { - case .setDisabledResources: - let value = try container.decode(Set.self, forKey: .value) - self = .setDisabledResources(value) + case .internetResourceEnabled: + let value = try container.decode(Bool.self, forKey: .value) + self = .internetResourceEnabled(value) case .getResourceList: let value = try container.decode(Data.self, forKey: .value) self = .getResourceList(value) @@ -56,9 +56,9 @@ public enum TunnelMessage: Codable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) switch self { - case .setDisabledResources(let value): - try container.encode(MessageType.setDisabledResources, forKey: .type) - try container.encode(value, forKey: .value) + case .internetResourceEnabled(let value): + try container.encode(MessageType.internetResourceEnabled, forKey: .type) + try container.encode(value, forKey: .value) case .getResourceList(let value): try container.encode(MessageType.getResourceList, forKey: .type) try container.encode(value, forKey: .value) @@ -87,8 +87,8 @@ public class TunnelManager { // Persists our tunnel settings private var manager: NETunnelProviderManager? - // Resources that are currently disabled and will not be used - public var disabledResources: Set = [] + // Indicates if the internet resource is currently enabled + public var internetResourceEnabled: Bool = false // Encoder used to send messages to the tunnel private let encoder = PropertyListEncoder() @@ -154,8 +154,9 @@ public class TunnelManager { // Found it let settings = Settings.fromProviderConfiguration(providerConfiguration) let actorName = providerConfiguration[TunnelManagerKeys.actorName] - if let disabledResourcesData = providerConfiguration[TunnelManagerKeys.disabledResources]?.data(using: .utf8) { - self.disabledResources = (try? JSONDecoder().decode(Set.self, from: disabledResourcesData)) ?? Set() + if let internetResourceEnabled = providerConfiguration[TunnelManagerKeys.internetResourceEnabled]?.data(using: .utf8) { + + self.internetResourceEnabled = (try? JSONDecoder().decode(Bool.self, from: internetResourceEnabled)) ?? false } let status = manager.connection.status @@ -256,20 +257,15 @@ public class TunnelManager { } } - func updateDisabledResources() { + func updateInternetResourceState() { guard session().status == .connected else { return } - try? session().sendProviderMessage(encoder.encode(TunnelMessage.setDisabledResources(disabledResources))) { _ in } + try? session().sendProviderMessage(encoder.encode(TunnelMessage.internetResourceEnabled(internetResourceEnabled))) { _ in } } - func toggleResourceDisabled(resource: String, enabled: Bool) { - if enabled { - disabledResources.remove(resource) - } else { - disabledResources.insert(resource) - } - - updateDisabledResources() + func toggleInternetResource(enabled: Bool) { + internetResourceEnabled = enabled + updateInternetResourceState() } func fetchResources(callback: @escaping (ResourceList) -> Void) { diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift index ca8a49788..531428cf8 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Resource.swift @@ -8,6 +8,11 @@ import Foundation +class StatusSymbol { + static var on: String = "<->" + static var off: String = "—" +} + public enum ResourceList { case loading case loaded([Resource]) @@ -30,9 +35,8 @@ public struct Resource: Decodable, Identifiable, Equatable { public var status: ResourceStatus public var sites: [Site] public var type: ResourceType - public var canBeDisabled: Bool - public init(id: String, name: String, address: String?, addressDescription: String?, status: ResourceStatus, sites: [Site], type: ResourceType, canBeDisabled: Bool) { + public init(id: String, name: String, address: String?, addressDescription: String?, status: ResourceStatus, sites: [Site], type: ResourceType) { self.id = id self.name = name self.address = address @@ -40,7 +44,6 @@ public struct Resource: Decodable, Identifiable, Equatable { self.status = status self.sites = sites self.type = type - self.canBeDisabled = canBeDisabled } public func isInternetResource() -> Bool { diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Settings.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Settings.swift index 58951a1fb..179ee5ef8 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Settings.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Models/Settings.swift @@ -10,7 +10,7 @@ struct Settings: Equatable { var authBaseURL: String var apiURL: String var logFilter: String - var disabledResources: Set + var internetResourceEnabled: Bool? var isValid: Bool { let authBaseURL = URL(string: authBaseURL) @@ -38,19 +38,17 @@ struct Settings: Equatable { ?? Settings.defaultValue.apiURL, logFilter: providerConfiguration[TunnelManagerKeys.logFilter] ?? Settings.defaultValue.logFilter, - disabledResources: getDisabledResources(disabledResources: providerConfiguration[TunnelManagerKeys.disabledResources]) + internetResourceEnabled: getInternetResourceEnabled(internetResourceEnabled: providerConfiguration[TunnelManagerKeys.internetResourceEnabled]) ) } else { return Settings.defaultValue } } - static private func getDisabledResources(disabledResources: String?) -> Set { - guard let disabledResourcesJSON = disabledResources, let disabledResourcesData = disabledResourcesJSON.data(using: .utf8) else{ - return Set() - } - return (try? JSONDecoder().decode(Set.self, from: disabledResourcesData)) - ?? Settings.defaultValue.disabledResources + static private func getInternetResourceEnabled(internetResourceEnabled: String?) -> Bool? { + guard let internetResourceEnabled = internetResourceEnabled, let jsonData = internetResourceEnabled.data(using: .utf8) else { return nil } + + return try? JSONDecoder().decode(Bool?.self, from: jsonData) } // Used for initializing a new providerConfiguration from Settings @@ -59,7 +57,7 @@ struct Settings: Equatable { TunnelManagerKeys.authBaseURL: authBaseURL, TunnelManagerKeys.apiURL: apiURL, TunnelManagerKeys.logFilter: logFilter, - TunnelManagerKeys.disabledResources: String(data: try! JSONEncoder().encode(disabledResources), encoding: .utf8) ?? "", + TunnelManagerKeys.internetResourceEnabled: String(data: try! JSONEncoder().encode(internetResourceEnabled) , encoding: .utf8)!, ] } @@ -72,14 +70,14 @@ struct Settings: Equatable { apiURL: "wss://api.firez.one", logFilter: "firezone_tunnel=debug,phoenix_channel=debug,connlib_shared=debug,connlib_client_shared=debug,snownet=debug,str0m=info,warn", - disabledResources: Set() + internetResourceEnabled: nil ) #else Settings( authBaseURL: "https://app.firezone.dev", apiURL: "wss://api.firezone.dev", logFilter: "str0m=warn,info", - disabledResources: Set() + internetResourceEnabled: nil ) #endif }() diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift index a7dabf8f5..afeb20bf2 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Stores/Store.swift @@ -45,8 +45,8 @@ public final class Store: ObservableObject { initTunnelManager() } - public func isResourceEnabled(_ id: String) -> Bool { - !tunnelManager.disabledResources.contains(id) + public func internetResourceEnabled() -> Bool { + tunnelManager.internetResourceEnabled } private func initNotifications() { @@ -171,10 +171,10 @@ public final class Store: ObservableObject { } } - func toggleResourceDisabled(resource: String, enabled: Bool) { - tunnelManager.toggleResourceDisabled(resource: resource, enabled: enabled) + func toggleInternetResource(enabled: Bool) { + tunnelManager.toggleInternetResource(enabled: enabled) var newSettings = settings - newSettings.disabledResources = tunnelManager.disabledResources + newSettings.internetResourceEnabled = tunnelManager.internetResourceEnabled Task { try await save(newSettings) } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift index 307ae7ed5..c27a09cf7 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/MenuBar.swift @@ -22,6 +22,9 @@ public final class MenuBar: NSObject, ObservableObject { // Wish these could be `[String]` but diffing between different types is tricky private var lastShownFavorites: [Resource] = [] private var lastShownOthers: [Resource] = [] + + private var wasInternetResourceEnabled: Bool = false + private var cancellables: Set = [] @ObservedObject var model: SessionViewModel @@ -432,12 +435,21 @@ public final class MenuBar: NSObject, ObservableObject { populateOtherResourcesMenu(newOthers) } + private func displayNameChanged(_ resource: Resource) -> Bool { + if !resource.isInternetResource() { + return false + } + + return wasInternetResourceEnabled != model.store.internetResourceEnabled() + } + private func populateFavoriteResourcesMenu(_ newFavorites: [Resource]) { // Update the menu in place so everything won't vanish if it's open when it updates let diff = (newFavorites).difference( from: lastShownFavorites, - by: { $0 == $1 } + by: { $0 == $1 && !displayNameChanged($0) } ) + let index = menu.index(of: resourcesTitleMenuItem) + 1 for change in diff { switch change { @@ -449,6 +461,7 @@ public final class MenuBar: NSObject, ObservableObject { } } lastShownFavorites = newFavorites + wasInternetResourceEnabled = model.store.internetResourceEnabled() } private func populateOtherResourcesMenu(_ newOthers: [Resource]) { @@ -464,7 +477,7 @@ public final class MenuBar: NSObject, ObservableObject { // Update the menu in place so everything won't vanish if it's open when it updates let diff = (newOthers).difference( from: lastShownOthers, - by: { $0 == $1 } + by: { $0 == $1 && !displayNameChanged($0) } ) for change in diff { switch change { @@ -476,6 +489,8 @@ public final class MenuBar: NSObject, ObservableObject { } } lastShownOthers = newOthers + wasInternetResourceEnabled = model.store.internetResourceEnabled() + } private func addItemToMenu(menu: NSMenu, item: NSMenuItem, at: Int) { @@ -498,8 +513,22 @@ public final class MenuBar: NSObject, ObservableObject { menu.removeItem(item) } + private func internetResourceTitle(resource: Resource) -> String { + let status = model.store.internetResourceEnabled() ? StatusSymbol.on : StatusSymbol.off + + return status + " " + resource.name + } + + private func resourceTitle(resource: Resource) -> String { + if resource.isInternetResource() { + return internetResourceTitle(resource: resource) + } + + return resource.name + } + private func createResourceMenuItem(resource: Resource) -> NSMenuItem { - let item = NSMenuItem(title: resource.name, action: nil, keyEquivalent: "") + let item = NSMenuItem(title: resourceTitle(resource: resource), action: nil, keyEquivalent: "") item.isHidden = false item.submenu = createSubMenu(resource: resource) @@ -507,8 +536,8 @@ public final class MenuBar: NSObject, ObservableObject { return item } - private func resourceTitle(_ id: String) -> String { - model.isResourceEnabled(id) ? "Disable this resource" : "Enable this resource" + private func internetResourceToggleTitle() -> String { + model.isInternetResourceEnabled() ? "Disable this resource" : "Enable this resource" } private func nonInternetResourceHeader(resource: Resource) -> NSMenu { @@ -595,6 +624,16 @@ public final class MenuBar: NSObject, ObservableObject { subMenu.addItem(description) + // Resource enable / disable toggle + subMenu.addItem(NSMenuItem.separator()) + let enableToggle = NSMenuItem() + enableToggle.action = #selector(internetResourceToggle(_:)) + enableToggle.title = internetResourceToggleTitle() + enableToggle.toolTip = "Enable or disable resource" + enableToggle.isEnabled = true + enableToggle.target = self + subMenu.addItem(enableToggle) + return subMenu } @@ -610,22 +649,9 @@ public final class MenuBar: NSObject, ObservableObject { let siteSectionItem = NSMenuItem() let siteNameItem = NSMenuItem() let siteStatusItem = NSMenuItem() - let enableToggle = NSMenuItem() let subMenu = resourceHeader(resource: resource) - // Resource enable / disable toggle - if resource.canBeDisabled { - subMenu.addItem(NSMenuItem.separator()) - enableToggle.action = #selector(resourceToggle(_:)) - enableToggle.title = resourceTitle(resource.id) - enableToggle.toolTip = "Enable or disable resource" - enableToggle.isEnabled = true - enableToggle.target = self - enableToggle.representedObject = resource.id - subMenu.addItem(enableToggle) - } - // Site details if let site = resource.sites.first { subMenu.addItem(NSMenuItem.separator()) @@ -668,11 +694,9 @@ public final class MenuBar: NSObject, ObservableObject { } } - @objc private func resourceToggle(_ sender: NSMenuItem) { - let id = sender.representedObject as! String - - self.model.store.toggleResourceDisabled(resource: id, enabled: !model.isResourceEnabled(id)) - sender.title = resourceTitle(id) + @objc private func internetResourceToggle(_ sender: NSMenuItem) { + self.model.store.toggleInternetResource(enabled: !model.store.internetResourceEnabled()) + sender.title = internetResourceToggleTitle() } @objc private func resourceURLTapped(_ sender: AnyObject?) { diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift index 40fadfc0e..66b7274bd 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/ResourceView.swift @@ -173,9 +173,6 @@ struct NonInternetResourceHeader: View { } } } - - ToggleResourceEnabledButton(resource: resource, model: model) - } } } @@ -204,17 +201,18 @@ struct InternetResourceHeader: View { Text("All network traffic") } - ToggleResourceEnabledButton(resource: resource, model: model) + + ToggleInternetResourceButton(resource: resource, model: model) } } } -struct ToggleResourceEnabledButton: View { +struct ToggleInternetResourceButton: View { var resource: Resource @ObservedObject var model: SessionViewModel private func toggleResourceEnabledText() -> String { - if model.isResourceEnabled(resource.id) { + if model.isInternetResourceEnabled() { "Disable this resource" } else { "Enable this resource" @@ -222,14 +220,12 @@ struct ToggleResourceEnabledButton: View { } var body: some View { - if resource.canBeDisabled { - Button(action: { - model.store.toggleResourceDisabled(resource: resource.id, enabled: !model.isResourceEnabled(resource.id)) - }) { - HStack { - Text(toggleResourceEnabledText()) - Spacer() - } + Button(action: { + model.store.toggleInternetResource(enabled: !model.isInternetResourceEnabled()) + }) { + HStack { + Text(toggleResourceEnabledText()) + Spacer() } } } diff --git a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift index 989f761d3..7746408c4 100644 --- a/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift +++ b/swift/apple/FirezoneKit/Sources/FirezoneKit/Views/SessionView.swift @@ -60,8 +60,8 @@ public final class SessionViewModel: ObservableObject { .store(in: &cancellables) } - public func isResourceEnabled(_ resource: String) -> Bool { - store.isResourceEnabled(resource) + public func isInternetResourceEnabled() -> Bool { + store.internetResourceEnabled() } } @@ -127,12 +127,26 @@ struct ResourceSection: View { let resources: [Resource] @ObservedObject var model: SessionViewModel + private func internetResourceTitle(resource: Resource) -> String { + let status = model.store.internetResourceEnabled() ? StatusSymbol.on : StatusSymbol.off + + return status + " " + resource.name + } + + private func resourceTitle(resource: Resource) -> String { + if resource.isInternetResource() { + return internetResourceTitle(resource: resource) + } + + return resource.name + } + var body: some View { ForEach(resources) { resource in HStack { NavigationLink { ResourceView(model: model, resource: resource) } label: { - Text(resource.name) + Text(resourceTitle(resource: resource)) } } .navigationTitle("All Resources") diff --git a/swift/apple/FirezoneNetworkExtension/Adapter.swift b/swift/apple/FirezoneNetworkExtension/Adapter.swift index 91f22b46a..30993325a 100644 --- a/swift/apple/FirezoneNetworkExtension/Adapter.swift +++ b/swift/apple/FirezoneNetworkExtension/Adapter.swift @@ -61,10 +61,10 @@ class Adapter { private let workQueue = DispatchQueue(label: "FirezoneAdapterWorkQueue") /// Currently disabled resources - private var disabledResources: Set = [] + private var internetResourceEnabled: Bool = false - /// Cache of resources that can be disabled - private var canBeDisabled: Set = [] + /// Cache of internet resource + private var internetResource: Resource? /// Adapter state. private var state: AdapterState { @@ -86,7 +86,7 @@ class Adapter { apiURL: String, token: String, logFilter: String, - disabledResources: Set, + internetResourceEnabled: Bool, packetTunnelProvider: PacketTunnelProvider ) { self.apiURL = apiURL @@ -97,7 +97,7 @@ class Adapter { self.logFilter = logFilter self.connlibLogFolderPath = SharedAccess.connlibLogFolderURL?.path ?? "" self.networkSettings = nil - self.disabledResources = disabledResources + self.internetResourceEnabled = internetResourceEnabled } // Could happen abruptly if the process is killed. @@ -204,10 +204,10 @@ class Adapter { return (try? decoder.decode([Resource].self, from: resourceList.data(using: .utf8)!)) ?? [] } - public func setDisabledResources(newDisabledResources: Set) { + public func setInternetResourceEnabled(_ enabled: Bool) { workQueue.async { [weak self] in guard let self = self else { return } - self.disabledResources = newDisabledResources + self.internetResourceEnabled = enabled self.resourcesUpdated() } } @@ -218,9 +218,13 @@ class Adapter { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase - canBeDisabled = Set(resources().filter({ $0.canBeDisabled }).map({ $0.id })) + internetResource = resources().filter{ $0.isInternetResource() }.first + + var disablingResources: Set = [] + if let internetResource = internetResource, !internetResourceEnabled { + disablingResources.insert(internetResource.id) + } - let disablingResources = disabledResources.filter({ canBeDisabled.contains($0) }) let currentlyDisabled = try! JSONEncoder().encode(disablingResources) session.setDisabledResources(String(data: currentlyDisabled, encoding: .utf8)!) diff --git a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift index 266647713..9262098c4 100644 --- a/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift +++ b/swift/apple/FirezoneNetworkExtension/PacketTunnelProvider.swift @@ -78,14 +78,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider { return } - let disabledResources: Set = if let disabledResourcesJSON = providerConfiguration[TunnelManagerKeys.disabledResources]?.data(using: .utf8) { - (try? JSONDecoder().decode(Set.self, from: disabledResourcesJSON )) ?? Set() + let internetResourceEnabled: Bool = if let internetResourceEnabledJSON = providerConfiguration[TunnelManagerKeys.internetResourceEnabled]?.data(using: .utf8) { + (try? JSONDecoder().decode(Bool.self, from: internetResourceEnabledJSON )) ?? false } else { - Set() + false } let adapter = Adapter( - apiURL: apiURL, token: token, logFilter: logFilter, disabledResources: disabledResources, packetTunnelProvider: self) + apiURL: apiURL, token: token, logFilter: logFilter, internetResourceEnabled: internetResourceEnabled, packetTunnelProvider: self) self.adapter = adapter @@ -139,8 +139,8 @@ class PacketTunnelProvider: NEPacketTunnelProvider { guard let tunnelMessage = try? PropertyListDecoder().decode(TunnelMessage.self, from: message) else { return } switch tunnelMessage { - case .setDisabledResources(let value): - adapter?.setDisabledResources(newDisabledResources: value) + case .internetResourceEnabled(let value): + adapter?.setInternetResourceEnabled(value) case .signOut: Task { await clearToken()