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 <gabrielalejandro7@gmail.com>
Co-authored-by: conectado <conectado@conectados-MacBook-Air.local>
Co-authored-by: Thomas Eizinger <thomas@eizinger.io>
This commit is contained in:
Gabi
2024-09-04 12:16:50 -07:00
committed by GitHub
parent 112be91cae
commit 700b056cd2
25 changed files with 409 additions and 270 deletions

View File

@@ -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<String> {
val jsonString = sharedPreferences.getString(DISABLED_RESOURCES_KEY, null) ?: return hashSetOf()
val type = object : TypeToken<HashSet<String>>() {}.type
fun getInternetResourceStateSync(): ResourceState {
val jsonString = sharedPreferences.getString(ENABLED_INTERNET_RESOURCE_KEY, null) ?: return ResourceState.UNSET
val type = object : TypeToken<ResourceState>() {}.type
return Gson().fromJson(jsonString, type)
}
fun saveDisabledResourcesSync(value: Set<String>): 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<Unit> =
@@ -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"
}
}

View File

@@ -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()
}
}

View File

@@ -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<Site>? = 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
}

View File

@@ -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<ViewResource, ResourcesAdapter.ViewHolder>(
ResourceDiffCallback(),
) {
internal class ResourcesAdapter(private val internetResourceToggle: () -> ResourceState) :
ListAdapter<ResourceViewModel, ResourcesAdapter.ViewHolder>(
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<ViewResource>() {
class ResourceDiffCallback : DiffUtil.ItemCallback<ResourceViewModel>() {
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
}

View File

@@ -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()
}
}

View File

@@ -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<String>>(HashSet())
private val _serviceStatusLiveData = MutableLiveData<State>()
private val _resourcesLiveData = MutableLiveData<List<ViewResource>>(emptyList())
private val _resourcesLiveData = MutableLiveData<List<Resource>>(emptyList())
private var showOnlyFavorites: Boolean = false
val favoriteResourcesLiveData: MutableLiveData<HashSet<String>>
get() = _favoriteResourcesLiveData
val serviceStatusLiveData: MutableLiveData<State>
get() = _serviceStatusLiveData
val resourcesLiveData: MutableLiveData<List<ViewResource>>
val resourcesLiveData: MutableLiveData<List<Resource>>
get() = _resourcesLiveData
private val favoriteResources: HashSet<String>
@@ -56,8 +59,16 @@ internal class SessionViewModel
fun clearToken() = repo.clearToken()
// The subset of Resources to actually render
fun resourcesList(): List<ViewResource> {
val resources = resourcesLiveData.value!!
fun resourcesList(isInternetResourceEnabled: ResourceState): List<ResourceViewModel> {
val resources =
resourcesLiveData.value!!.map {
if (it.isInternetResource()) {
ResourceViewModel(it, isInternetResourceEnabled)
} else {
ResourceViewModel(it, ResourceState.ENABLED)
}
}
return if (favoriteResources.isEmpty()) {
resources
} else if (showOnlyFavorites) {

View File

@@ -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<Site>?,
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
}

View File

@@ -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<String> = mutableListOf()
private var tunnelRoutes: MutableList<Cidr> = mutableListOf()
private var _tunnelResources: List<ViewResource> = emptyList()
private var _tunnelResources: List<Resource> = emptyList()
private var _tunnelState: State = State.DOWN
private var disabledResources: MutableSet<String> = 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<ViewResource>
var tunnelResources: List<Resource>
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<State>? = null
private var resourcesLiveData: MutableLiveData<List<ViewResource>>? = null
private var resourcesLiveData: MutableLiveData<List<Resource>>? = 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<List<Resource>>().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<List<ViewResource>>) {
fun setResourcesLiveData(liveData: MutableLiveData<List<Resource>>) {
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<ViewResource>) {
private fun updateResourcesLiveData(resources: List<Resource>) {
resourcesLiveData?.postValue(resources)
}

View File

@@ -16,9 +16,12 @@ data class Resource(
val sites: List<Site>?,
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,

View File

@@ -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<Site>,
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<Site>,
pub status: Status,
pub can_be_disabled: bool,
}
/// Description of an Internet resource
@@ -131,7 +121,6 @@ pub struct ResourceDescriptionInternet {
pub sites: Vec<Site>,
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,
})
}

View File

@@ -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<Site>,
/// 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,
}
}

View File

@@ -78,13 +78,10 @@ pub fn cidr_resource(
pub fn internet_resource(
sites: impl Strategy<Value = Vec<Site>>,
) -> impl Strategy<Value = ResourceDescriptionInternet> {
(resource_id(), sites, any::<bool>()).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,
})
}

View File

@@ -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<ResourceDescription> {
#[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,
})
}

View File

@@ -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<ResourceId>,
pub(crate) resources: &'a [ResourceDescription],
pub(crate) disabled_resources: &'a HashSet<ResourceId>,
pub(crate) internet_resource_enabled: &'a Option<bool>,
}
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<ResourceId>,
disabled_resources: &'a HashSet<ResourceId>,
internet_resource_enabled: &'a Option<bool>,
) -> 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),

View File

@@ -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)]

View File

@@ -18,7 +18,7 @@ pub(crate) struct AdvancedSettings {
#[serde(default)]
pub favorite_resources: HashSet<ResourceId>,
#[serde(default)]
pub disabled_resources: HashSet<ResourceId>,
pub internet_resource_enabled: Option<bool>,
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<PathBuf> {
Ok(known_dirs::settings()
.context("`known_dirs::settings` failed")?

View File

@@ -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<String>)
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<String>.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<String> = []
// 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<String>.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) {

View File

@@ -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 {

View File

@@ -10,7 +10,7 @@ struct Settings: Equatable {
var authBaseURL: String
var apiURL: String
var logFilter: String
var disabledResources: Set<String>
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<String> {
guard let disabledResourcesJSON = disabledResources, let disabledResourcesData = disabledResourcesJSON.data(using: .utf8) else{
return Set()
}
return (try? JSONDecoder().decode(Set<String>.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
}()

View File

@@ -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)
}

View File

@@ -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<AnyCancellable> = []
@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?) {

View File

@@ -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()
}
}
}

View File

@@ -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")

View File

@@ -61,10 +61,10 @@ class Adapter {
private let workQueue = DispatchQueue(label: "FirezoneAdapterWorkQueue")
/// Currently disabled resources
private var disabledResources: Set<String> = []
private var internetResourceEnabled: Bool = false
/// Cache of resources that can be disabled
private var canBeDisabled: Set<String> = []
/// 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<String>,
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<String>) {
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<String> = []
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)!)

View File

@@ -78,14 +78,14 @@ class PacketTunnelProvider: NEPacketTunnelProvider {
return
}
let disabledResources: Set<String> = if let disabledResourcesJSON = providerConfiguration[TunnelManagerKeys.disabledResources]?.data(using: .utf8) {
(try? JSONDecoder().decode(Set<String>.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()