Support hex-encoded SSID
This commit is contained in:
@@ -145,10 +145,9 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
||||
content.target.bssid?.let { MacAddress.fromString(it) }
|
||||
}
|
||||
|
||||
suspend fun update(ssid: String, psk: String, bssid: MacAddress?) {
|
||||
suspend fun update(ssid: WifiSsidCompat, psk: String, bssid: MacAddress?) {
|
||||
val (lines, block, persistentMacLine, legacy) = content
|
||||
block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray()
|
||||
.joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') }
|
||||
block[block.ssidLine!!] = "\tssid=${ssid.hex}"
|
||||
block[block.pskLine!!] = "\tpsk=\"$psk\"" // no control chars or weird stuff
|
||||
if (bssid != null) {
|
||||
persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid }
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.annotation.TargetApi
|
||||
import android.net.MacAddress
|
||||
import android.net.wifi.ScanResult
|
||||
import android.net.wifi.SoftApConfiguration
|
||||
import android.net.wifi.WifiSsid
|
||||
import android.os.Build
|
||||
import android.os.Parcelable
|
||||
import android.util.SparseIntArray
|
||||
@@ -12,6 +13,7 @@ import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
|
||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.requireSingleBand
|
||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.setChannel
|
||||
import be.mygod.vpnhotspot.net.wifi.WifiSsidCompat.Companion.toCompat
|
||||
import be.mygod.vpnhotspot.util.ConstantLookup
|
||||
import be.mygod.vpnhotspot.util.UnblockCentral
|
||||
import kotlinx.parcelize.Parcelize
|
||||
@@ -20,7 +22,7 @@ import java.lang.reflect.InvocationTargetException
|
||||
|
||||
@Parcelize
|
||||
data class SoftApConfigurationCompat(
|
||||
var ssid: String? = null,
|
||||
var ssid: WifiSsidCompat? = null,
|
||||
var bssid: MacAddress? = null,
|
||||
var passphrase: String? = null,
|
||||
var isHiddenSsid: Boolean = false,
|
||||
@@ -116,8 +118,6 @@ data class SoftApConfigurationCompat(
|
||||
"WPA3-OWE",
|
||||
)
|
||||
|
||||
private val qrSanitizer = Regex("([\\\\\":;,])")
|
||||
|
||||
/**
|
||||
* Based on:
|
||||
* https://elixir.bootlin.com/linux/v5.12.8/source/net/wireless/util.c#L75
|
||||
@@ -344,11 +344,15 @@ data class SoftApConfigurationCompat(
|
||||
private val setVendorElements by lazy @TargetApi(33) {
|
||||
classBuilder.getDeclaredMethod("setVendorElements", java.util.List::class.java)
|
||||
}
|
||||
@get:RequiresApi(33)
|
||||
private val setWifiSsid by lazy @TargetApi(33) {
|
||||
classBuilder.getDeclaredMethod("setWifiSsid", WifiSsid::class.java)
|
||||
}
|
||||
|
||||
@Deprecated("Class deprecated in framework")
|
||||
@Suppress("DEPRECATION")
|
||||
fun android.net.wifi.WifiConfiguration.toCompat() = SoftApConfigurationCompat(
|
||||
SSID,
|
||||
WifiSsidCompat.fromUtf8Text(SSID),
|
||||
BSSID?.let { MacAddress.fromString(it) },
|
||||
preSharedKey,
|
||||
hiddenSSID,
|
||||
@@ -388,7 +392,9 @@ data class SoftApConfigurationCompat(
|
||||
@RequiresApi(30)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun SoftApConfiguration.toCompat() = SoftApConfigurationCompat(
|
||||
ssid,
|
||||
if (Build.VERSION.SDK_INT >= 33) wifiSsid?.toCompat() else @Suppress("DEPRECATION") {
|
||||
WifiSsidCompat.fromUtf8Text(ssid)
|
||||
},
|
||||
bssid,
|
||||
passphrase,
|
||||
isHiddenSsid,
|
||||
@@ -483,7 +489,7 @@ data class SoftApConfigurationCompat(
|
||||
val wc = underlying as? android.net.wifi.WifiConfiguration
|
||||
val result = if (wc == null) android.net.wifi.WifiConfiguration() else android.net.wifi.WifiConfiguration(wc)
|
||||
val original = wc?.toCompat()
|
||||
result.SSID = ssid
|
||||
result.SSID = ssid?.toString()
|
||||
result.preSharedKey = passphrase
|
||||
result.hiddenSSID = isHiddenSsid
|
||||
apBand.setInt(result, when (band) {
|
||||
@@ -520,7 +526,9 @@ data class SoftApConfigurationCompat(
|
||||
fun toPlatform(): SoftApConfiguration {
|
||||
val sac = underlying as? SoftApConfiguration
|
||||
val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac)
|
||||
setSsid(builder, ssid)
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
setWifiSsid(builder, ssid?.toPlatform())
|
||||
} else setSsid(builder, ssid?.toString())
|
||||
setPassphrase(builder, when (securityType) {
|
||||
SoftApConfiguration.SECURITY_TYPE_OPEN,
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
|
||||
@@ -581,7 +589,6 @@ data class SoftApConfigurationCompat(
|
||||
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/4a5ff58/src/com/android/settings/wifi/dpp/WifiNetworkConfig.java#161
|
||||
*/
|
||||
fun toQrCode() = StringBuilder("WIFI:").apply {
|
||||
fun String.sanitize() = qrSanitizer.replace(this) { "\\${it.groupValues[1]}" }
|
||||
when (securityType) {
|
||||
SoftApConfiguration.SECURITY_TYPE_OPEN, SoftApConfiguration.SECURITY_TYPE_WPA3_OWE_TRANSITION,
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA3_OWE -> { }
|
||||
@@ -592,11 +599,11 @@ data class SoftApConfigurationCompat(
|
||||
else -> throw IllegalArgumentException("Unsupported authentication type")
|
||||
}
|
||||
append("S:")
|
||||
append(ssid!!.sanitize())
|
||||
append(ssid!!.toMeCard())
|
||||
append(';')
|
||||
passphrase?.let { passphrase ->
|
||||
append("P:")
|
||||
append(passphrase.sanitize())
|
||||
append(WifiSsidCompat.toMeCard(passphrase))
|
||||
append(';')
|
||||
}
|
||||
if (isHiddenSsid) append("H:true;")
|
||||
|
||||
@@ -36,6 +36,7 @@ import be.mygod.vpnhotspot.util.QRCodeDialog
|
||||
import be.mygod.vpnhotspot.util.RangeInput
|
||||
import be.mygod.vpnhotspot.util.readableMessage
|
||||
import be.mygod.vpnhotspot.util.showAllowingStateLoss
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.text.DecimalFormat
|
||||
@@ -129,6 +130,14 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
)
|
||||
}
|
||||
override val ret get() = Arg(generateConfig())
|
||||
private val hexToggleable get() = if (arg.p2pMode) !RepeaterService.safeMode else Build.VERSION.SDK_INT >= 33
|
||||
private var hexSsid = false
|
||||
set(value) {
|
||||
field = value
|
||||
dialogView.ssidWrapper.setEndIconActivated(value)
|
||||
}
|
||||
private val ssid get() =
|
||||
if (hexSsid) WifiSsidCompat.fromHex(dialogView.ssid.text) else WifiSsidCompat.fromUtf8Text(dialogView.ssid.text)
|
||||
|
||||
private fun generateChannels() = SparseIntArray(2).apply {
|
||||
if (!arg.p2pMode && Build.VERSION.SDK_INT >= 31) {
|
||||
@@ -137,7 +146,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
(dialogView.bandPrimary.selectedItem as ChannelOption).apply { put(band, channel) }
|
||||
}
|
||||
private fun generateConfig(full: Boolean = true) = base.copy(
|
||||
ssid = dialogView.ssid.text.toString(),
|
||||
ssid = ssid,
|
||||
passphrase = if (dialogView.password.length() != 0) dialogView.password.text.toString() else null).apply {
|
||||
if (!arg.p2pMode) {
|
||||
securityType = dialogView.security.selectedItemPosition
|
||||
@@ -189,7 +198,31 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
setNegativeButton(R.string.donations__button_close, null)
|
||||
dialogView.toolbar.inflateMenu(R.menu.toolbar_configuration)
|
||||
dialogView.toolbar.setOnMenuItemClickListener(this@WifiApDialogFragment)
|
||||
dialogView.ssidWrapper.setLengthCounter { it.toString().toByteArray().size }
|
||||
dialogView.ssidWrapper.setLengthCounter {
|
||||
try {
|
||||
ssid?.bytes?.size ?: 0
|
||||
} catch (_: IllegalArgumentException) {
|
||||
0
|
||||
}
|
||||
}
|
||||
if (hexToggleable) dialogView.ssidWrapper.apply {
|
||||
endIconMode = TextInputLayout.END_ICON_CUSTOM
|
||||
setEndIconOnClickListener {
|
||||
val ssid = try {
|
||||
ssid
|
||||
} catch (_: IllegalArgumentException) {
|
||||
return@setEndIconOnClickListener
|
||||
}
|
||||
val newText = if (hexSsid) ssid?.run {
|
||||
decode().also { if (it == null) return@setEndIconOnClickListener }
|
||||
} else ssid?.hex
|
||||
hexSsid = !hexSsid
|
||||
dialogView.ssid.setText(newText)
|
||||
}
|
||||
findViewById<View>(com.google.android.material.R.id.text_input_end_icon).apply {
|
||||
tooltipText = contentDescription
|
||||
}
|
||||
}
|
||||
if (!arg.readOnly) dialogView.ssid.addTextChangedListener(this@WifiApDialogFragment)
|
||||
if (arg.p2pMode) dialogView.securityWrapper.isGone = true else dialogView.security.apply {
|
||||
adapter = ArrayAdapter(activity, android.R.layout.simple_spinner_item, 0,
|
||||
@@ -293,7 +326,14 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
} else selection
|
||||
}
|
||||
private fun populateFromConfiguration() {
|
||||
dialogView.ssid.setText(base.ssid)
|
||||
dialogView.ssid.setText(base.ssid.let { ssid ->
|
||||
when {
|
||||
ssid == null -> null
|
||||
hexSsid -> ssid.hex
|
||||
hexToggleable -> ssid.decode() ?: ssid.hex.also { hexSsid = true }
|
||||
else -> ssid.toString()
|
||||
}
|
||||
})
|
||||
if (!arg.p2pMode) dialogView.security.setSelection(base.securityType)
|
||||
dialogView.password.setText(base.passphrase)
|
||||
dialogView.autoShutdown.isChecked = base.isAutoShutdownEnabled
|
||||
@@ -334,11 +374,18 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
|
||||
private fun validate() {
|
||||
if (!started) return
|
||||
val ssidLength = dialogView.ssid.text.toString().toByteArray().size
|
||||
val ssidLengthOk = ssidLength in 1..32
|
||||
dialogView.ssidWrapper.error = if (arg.p2pMode && RepeaterService.safeMode && ssidLength < 9) {
|
||||
requireContext().getString(R.string.settings_service_repeater_safe_mode_warning)
|
||||
} else if (ssidLengthOk) null else " "
|
||||
val (ssidOk, ssidError) = 0.let {
|
||||
val ssid = try {
|
||||
ssid
|
||||
} catch (e: IllegalArgumentException) {
|
||||
return@let false to e.readableMessage
|
||||
}
|
||||
val ssidLength = ssid?.bytes?.size ?: 0
|
||||
if (ssidLength in 1..32) true to if (arg.p2pMode && RepeaterService.safeMode && ssidLength < 9) {
|
||||
requireContext().getString(R.string.settings_service_repeater_safe_mode_warning)
|
||||
} else null else false to " "
|
||||
}
|
||||
dialogView.ssidWrapper.error = ssidError
|
||||
val selectedSecurity = if (arg.p2pMode) {
|
||||
SoftApConfiguration.SECURITY_TYPE_WPA2_PSK
|
||||
} else dialogView.security.selectedItemPosition
|
||||
@@ -466,7 +513,7 @@ class WifiApDialogFragment : AlertDialogFragment<WifiApDialogFragment.Arg, WifiA
|
||||
bridgedTimeoutError == null && vendorElementsError == null && persistentRandomizedMacValid &&
|
||||
acsNoError && bandwidthError == null
|
||||
(dialog as? AlertDialog)?.getButton(DialogInterface.BUTTON_POSITIVE)?.isEnabled =
|
||||
ssidLengthOk && passwordValid && bandError == null && canCopy
|
||||
ssidOk && passwordValid && bandError == null && canCopy
|
||||
dialogView.toolbar.menu.findItem(android.R.id.copy).isEnabled = canCopy
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.net.wifi.WifiSsid
|
||||
import android.os.Parcelable
|
||||
import androidx.annotation.RequiresApi
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.jetbrains.annotations.Contract
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.CharBuffer
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.CodingErrorAction
|
||||
|
||||
@Parcelize
|
||||
data class WifiSsidCompat(val bytes: ByteArray) : Parcelable {
|
||||
companion object {
|
||||
private val hexTester = Regex("^(?:[0-9a-f]{2})*$", RegexOption.IGNORE_CASE)
|
||||
private val qrSanitizer = Regex("([\\\\\":;,])")
|
||||
|
||||
fun fromHex(hex: CharSequence?) = hex?.run {
|
||||
require(length % 2 == 0) { "Input should be hex: $hex" }
|
||||
WifiSsidCompat((0 until length / 2).map {
|
||||
Integer.parseInt(substring(it * 2, it * 2 + 2), 16).toByte()
|
||||
}.toByteArray())
|
||||
}
|
||||
|
||||
@Contract("null -> null; !null -> !null")
|
||||
fun fromUtf8Text(text: CharSequence?) = text?.toString()?.toByteArray()?.let { WifiSsidCompat(it) }
|
||||
|
||||
fun toMeCard(text: String) = qrSanitizer.replace(text) { "\\${it.groupValues[1]}" }
|
||||
|
||||
@RequiresApi(33)
|
||||
fun WifiSsid.toCompat() = WifiSsidCompat(bytes)
|
||||
}
|
||||
|
||||
init {
|
||||
require(bytes.size <= 32)
|
||||
}
|
||||
|
||||
@RequiresApi(31)
|
||||
fun toPlatform() = WifiSsid.fromBytes(bytes)
|
||||
|
||||
fun decode(charset: Charset = Charsets.UTF_8) = CharBuffer.allocate(32).run {
|
||||
val result = charset.newDecoder().apply {
|
||||
onMalformedInput(CodingErrorAction.REPORT)
|
||||
onUnmappableCharacter(CodingErrorAction.REPORT)
|
||||
}.decode(ByteBuffer.wrap(bytes), this, true)
|
||||
if (result.isError) null else flip().toString()
|
||||
}
|
||||
val hex get() = bytes.joinToString("") { "%02x".format(it.toUByte().toInt()) }
|
||||
|
||||
fun toMeCard(): String {
|
||||
val utf8 = decode() ?: return hex
|
||||
return if (hexTester.matches(utf8)) "\"$utf8\"" else toMeCard(utf8)
|
||||
}
|
||||
|
||||
override fun toString() = String(bytes)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
other as WifiSsidCompat
|
||||
if (!bytes.contentEquals(other.bytes)) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return bytes.contentHashCode()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user