Implement client limiting and MAC address filtering

This commit is contained in:
Mygod
2020-07-04 05:43:24 +08:00
parent 46d0544df2
commit 278f3f143b
6 changed files with 133 additions and 13 deletions

View File

@@ -1,7 +1,9 @@
package be.mygod.vpnhotspot.manage package be.mygod.vpnhotspot.manage
import android.annotation.TargetApi import android.annotation.TargetApi
import android.content.ClipData
import android.content.Intent import android.content.Intent
import android.net.MacAddress
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.view.View import android.view.View
@@ -161,6 +163,14 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
capability = maxSupportedClients to supportedFeatures capability = maxSupportedClients to supportedFeatures
data.notifyChange() data.notifyChange()
} }
override fun onBlockedClientConnecting(client: MacAddress, blockedReason: Int) {
SmartSnackbar.make(parent.getString(R.string.tethering_manage_wifi_client_blocked, client,
WifiApManager.clientBlockLookup(blockedReason, true))).apply {
action(R.string.tethering_manage_wifi_copy_mac) {
app.clipboard.setPrimaryClip(ClipData.newPlainText(null, client.toString()))
}
}.show()
}
override val title get() = parent.getString(R.string.tethering_manage_wifi) override val title get() = parent.getString(R.string.tethering_manage_wifi)
override val tetherType get() = TetherType.WIFI override val tetherType get() = TetherType.WIFI

View File

@@ -33,9 +33,9 @@ data class SoftApConfigurationCompat(
@TargetApi(30) @TargetApi(30)
var isClientControlByUserEnabled: Boolean = false, var isClientControlByUserEnabled: Boolean = false,
@RequiresApi(30) @RequiresApi(30)
var blockedClientList: List<MacAddress?> = emptyList(), var blockedClientList: List<MacAddress> = emptyList(),
@RequiresApi(30) @RequiresApi(30)
var allowedClientList: List<MacAddress?> = emptyList(), var allowedClientList: List<MacAddress> = emptyList(),
var underlying: Parcelable? = null) : Parcelable { var underlying: Parcelable? = null) : Parcelable {
companion object { companion object {
const val BAND_2GHZ = 1 const val BAND_2GHZ = 1
@@ -238,8 +238,8 @@ data class SoftApConfigurationCompat(
isAutoShutdownEnabled(this) as Boolean, isAutoShutdownEnabled(this) as Boolean,
getShutdownTimeoutMillis(this) as Long, getShutdownTimeoutMillis(this) as Long,
isClientControlByUserEnabled(this) as Boolean, isClientControlByUserEnabled(this) as Boolean,
getBlockedClientList(this) as List<MacAddress?>, getBlockedClientList(this) as List<MacAddress>,
getAllowedClientList(this) as List<MacAddress?>, getAllowedClientList(this) as List<MacAddress>,
this) this)
} }

View File

@@ -1,8 +1,10 @@
package be.mygod.vpnhotspot.net.wifi package be.mygod.vpnhotspot.net.wifi
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.ClipData import android.content.ClipData
import android.content.DialogInterface import android.content.DialogInterface
import android.net.MacAddress
import android.net.wifi.SoftApConfiguration import android.net.wifi.SoftApConfiguration
import android.os.Build import android.os.Build
import android.os.Parcelable import android.os.Parcelable
@@ -42,6 +44,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
Toolbar.OnMenuItemClickListener { Toolbar.OnMenuItemClickListener {
companion object { companion object {
private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP private const val BASE64_FLAGS = Base64.NO_PADDING or Base64.NO_WRAP
private val nonMacChars = "[^0-9a-fA-F:]+".toRegex()
private val channels by lazy { private val channels by lazy {
val list = ArrayList<BandOption.Channel>() val list = ArrayList<BandOption.Channel>()
for (chan in 1..14) list.add(BandOption.Channel(SoftApConfigurationCompat.BAND_2GHZ, chan)) for (chan in 1..14) list.add(BandOption.Channel(SoftApConfigurationCompat.BAND_2GHZ, chan))
@@ -109,7 +112,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
securityType = dialogView.security.selectedItemPosition securityType = dialogView.security.selectedItemPosition
isHiddenSsid = dialogView.hiddenSsid.isChecked isHiddenSsid = dialogView.hiddenSsid.isChecked
} }
if (full) { if (full) @TargetApi(28) {
isAutoShutdownEnabled = dialogView.autoShutdown.isChecked isAutoShutdownEnabled = dialogView.autoShutdown.isChecked
shutdownTimeoutMillis = dialogView.timeout.text.let { text -> shutdownTimeoutMillis = dialogView.timeout.text.let { text ->
if (text.isNullOrEmpty()) 0 else text.toString().toLong() if (text.isNullOrEmpty()) 0 else text.toString().toLong()
@@ -120,6 +123,14 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
bssid = if (dialogView.bssid.length() != 0) { bssid = if (dialogView.bssid.length() != 0) {
MacAddressCompat.fromString(dialogView.bssid.text.toString()) MacAddressCompat.fromString(dialogView.bssid.text.toString())
} else null } else null
maxNumberOfClients = dialogView.maxClient.text.let { text ->
if (text.isNullOrEmpty()) 0 else text.toString().toInt()
}
isClientControlByUserEnabled = dialogView.clientUserControl.isChecked
allowedClientList = (dialogView.allowedList.text ?: "").split(nonMacChars)
.filter { it.isNotEmpty() }.map { MacAddress.fromString(it) }
blockedClientList = (dialogView.blockedList.text ?: "").split(nonMacChars)
.filter { it.isNotEmpty() }.map { MacAddress.fromString(it) }
} }
} }
@@ -150,7 +161,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
if (arg.p2pMode || Build.VERSION.SDK_INT >= 30) { if (arg.p2pMode || Build.VERSION.SDK_INT >= 30) {
dialogView.timeoutWrapper.helperText = getString(R.string.wifi_hotspot_timeout_default, dialogView.timeoutWrapper.helperText = getString(R.string.wifi_hotspot_timeout_default,
TetherTimeoutMonitor.defaultTimeout) TetherTimeoutMonitor.defaultTimeout)
if (!arg.readOnly) dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment) dialogView.timeout.addTextChangedListener(this@WifiApDialogFragment)
} else dialogView.timeoutWrapper.isGone = true } else dialogView.timeoutWrapper.isGone = true
if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) dialogView.band.apply { if (Build.VERSION.SDK_INT >= 23 || arg.p2pMode) dialogView.band.apply {
bandOptions = mutableListOf<BandOption>().apply { bandOptions = mutableListOf<BandOption>().apply {
@@ -175,6 +186,16 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} else dialogView.bandWrapper.isGone = true } else dialogView.bandWrapper.isGone = true
dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment) dialogView.bssid.addTextChangedListener(this@WifiApDialogFragment)
if (arg.p2pMode) dialogView.hiddenSsid.isGone = true if (arg.p2pMode) dialogView.hiddenSsid.isGone = true
if (arg.p2pMode || Build.VERSION.SDK_INT < 30) {
dialogView.maxClientWrapper.isGone = true
dialogView.clientUserControl.isGone = true
dialogView.blockedListWrapper.isGone = true
dialogView.allowedListWrapper.isGone = true
} else {
dialogView.maxClient.addTextChangedListener(this@WifiApDialogFragment)
dialogView.blockedList.addTextChangedListener(this@WifiApDialogFragment)
dialogView.allowedList.addTextChangedListener(this@WifiApDialogFragment)
}
base = arg.configuration base = arg.configuration
populateFromConfiguration() populateFromConfiguration()
} }
@@ -193,18 +214,19 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
} }
dialogView.bssid.setText(base.bssid?.toString()) dialogView.bssid.setText(base.bssid?.toString())
dialogView.hiddenSsid.isChecked = base.isHiddenSsid dialogView.hiddenSsid.isChecked = base.isHiddenSsid
// TODO support more fields from SACC dialogView.maxClient.setText(base.maxNumberOfClients.let { if (it == 0) "" else it.toString() })
dialogView.clientUserControl.isChecked = base.isClientControlByUserEnabled
dialogView.blockedList.setText(base.blockedClientList.joinToString("\n"))
dialogView.allowedList.setText(base.allowedClientList.joinToString("\n"))
} }
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
started = true started = true
if (!arg.readOnly) validate() validate()
} }
/** @TargetApi(28)
* This function is reached only if not arg.readOnly.
*/
private fun validate() { private fun validate() {
if (!started) return if (!started) return
val ssidLength = dialogView.ssid.text.toString().toByteArray().size val ssidLength = dialogView.ssid.text.toString().toByteArray().size
@@ -239,9 +261,36 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
dialogView.bssidWrapper.error = e.readableMessage dialogView.bssidWrapper.error = e.readableMessage
false false
} }
val maxClientError = dialogView.maxClient.text.let { text ->
if (text.isNullOrEmpty()) null else try {
text.toString().toInt()
null
} catch (e: NumberFormatException) {
e.readableMessage
}
}
dialogView.maxClientWrapper.error = maxClientError
val blockedListError = try {
(dialogView.blockedList.text ?: "").split(nonMacChars)
.filter { it.isNotEmpty() }.forEach { MacAddress.fromString(it) }
null
} catch (e: IllegalArgumentException) {
e.readableMessage
}
dialogView.blockedListWrapper.error = blockedListError
val allowedListError = try {
(dialogView.allowedList.text ?: "").split(nonMacChars)
.filter { it.isNotEmpty() }.forEach { MacAddress.fromString(it) }
null
} catch (e: IllegalArgumentException) {
e.readableMessage
}
dialogView.allowedListWrapper.error = allowedListError
val canCopy = timeoutError == null && bssidValid && maxClientError == null && blockedListError == null &&
allowedListError == null
(dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled = (dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled =
ssidLength in 1..32 && passwordValid && timeoutError == null && bssidValid ssidLength in 1..32 && passwordValid && canCopy
dialogView.toolbar.menu.findItem(android.R.id.copy).isEnabled = timeoutError == null && bssidValid dialogView.toolbar.menu.findItem(android.R.id.copy).isEnabled = canCopy
} }
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { } override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { }

View File

@@ -97,6 +97,8 @@ object WifiApManager {
@RequiresApi(28) @RequiresApi(28)
val failureReasonLookup = ConstantLookup<WifiManager>("SAP_START_FAILURE_", val failureReasonLookup = ConstantLookup<WifiManager>("SAP_START_FAILURE_",
"SAP_START_FAILURE_GENERAL", "SAP_START_FAILURE_NO_CHANNEL") "SAP_START_FAILURE_GENERAL", "SAP_START_FAILURE_NO_CHANNEL")
@get:RequiresApi(30)
val clientBlockLookup by lazy { ConstantLookup<WifiManager>("SAP_CLIENT_") }
private val interfaceSoftApCallback by lazy { Class.forName("android.net.wifi.WifiManager\$SoftApCallback") } private val interfaceSoftApCallback by lazy { Class.forName("android.net.wifi.WifiManager\$SoftApCallback") }
private val registerSoftApCallback by lazy { private val registerSoftApCallback by lazy {

View File

@@ -143,6 +143,7 @@
android:layout_marginTop="8dip" android:layout_marginTop="8dip"
app:counterEnabled="true" app:counterEnabled="true"
app:counterMaxLength="17" app:counterMaxLength="17"
app:errorEnabled="true"
android:hint="@string/wifi_advanced_mac_address_title"> android:hint="@string/wifi_advanced_mac_address_title">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/bssid" android:id="@+id/bssid"
@@ -159,6 +160,58 @@
android:layout_marginTop="8dip" android:layout_marginTop="8dip"
style="@style/wifi_item_label" style="@style/wifi_item_label"
android:text="@string/wifi_hidden_network"/> android:text="@string/wifi_hidden_network"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/max_client_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
app:counterEnabled="true"
app:counterMaxLength="10"
app:errorEnabled="true"
android:hint="@string/wifi_max_clients">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/max_client"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
android:inputType="number"
android:maxLength="10" />
</com.google.android.material.textfield.TextInputLayout>
<Switch
android:id="@+id/client_user_control"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
style="@style/wifi_item_label"
android:text="@string/wifi_client_user_control"/>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/blocked_list_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:hint="@string/wifi_blocked_list"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/blocked_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
android:inputType="textMultiLine|textNoSuggestions" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/allowed_list_wrapper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dip"
android:hint="@string/wifi_allowed_list"
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/allowed_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/wifi_item_edit_content"
android:inputType="textMultiLine|textNoSuggestions" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
</LinearLayout> </LinearLayout>

View File

@@ -70,6 +70,8 @@
<string name="tethering_manage_wifi_info">%1$d MHz, channel %2$d, width %3$s</string> <string name="tethering_manage_wifi_info">%1$d MHz, channel %2$d, width %3$s</string>
<string name="tethering_manage_wifi_capabilities">%1$s/%2$d clients connected\nSupported features: %3$s</string> <string name="tethering_manage_wifi_capabilities">%1$s/%2$d clients connected\nSupported features: %3$s</string>
<string name="tethering_manage_wifi_no_features">None</string> <string name="tethering_manage_wifi_no_features">None</string>
<string name="tethering_manage_wifi_client_blocked">Blocked %1$s: %2$s</string>
<string name="tethering_manage_wifi_copy_mac">Copy MAC</string>
<string name="connected_state_incomplete">" (connecting)"</string> <string name="connected_state_incomplete">" (connecting)"</string>
<string name="connected_state_valid">" (reachable)"</string> <string name="connected_state_valid">" (reachable)"</string>
@@ -184,6 +186,10 @@
<string name="wifi_ap_choose_6G">6.0 GHz Band</string> <string name="wifi_ap_choose_6G">6.0 GHz Band</string>
<string name="wifi_advanced_mac_address_title">MAC address</string> <string name="wifi_advanced_mac_address_title">MAC address</string>
<string name="wifi_hidden_network">Hidden network</string> <string name="wifi_hidden_network">Hidden network</string>
<string name="wifi_max_clients">Maximum number of clients</string>
<string name="wifi_client_user_control">Control which client can use hotspot</string>
<string name="wifi_blocked_list">Blocked list of clients</string>
<string name="wifi_allowed_list">Allowed list of clients</string>
<string name="wifi_save">Save</string> <string name="wifi_save">Save</string>
<!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values/donations__strings.xml --> <!-- Based on: https://github.com/PrivacyApps/donations/blob/747d36a18433c7e9329691054122a8ad337a62d2/Donations/src/main/res/values/donations__strings.xml -->