mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
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:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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")?
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)!)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user