Skip to content

Commit

Permalink
Adds support for probing a TCP tunnel
Browse files Browse the repository at this point in the history
Helps for #510
  • Loading branch information
hufman committed Sep 4, 2023
1 parent d46ec46 commit 9499150
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ interface AppSettings {
HIDDEN_MUSIC_APPS("Hidden_Music_Apps", "com.android.bluetooth,com.clearchannel.iheartradio.connect,com.google.android.googlequicksearchbox,com.google.android.youtube,com.vanced.android.youtube", "List of music apps to hide from the app list"),
FORCE_SPOTIFY_LAYOUT("Force_Spotify_Layout", "false", "Use Spotify UI Resources"),
FORCE_AUDIOPLAYER_LAYOUT("Force_Audioplayer_Layout", "false", "Use legacy music screen even with Spotify resources"),
CONNECTION_PROBE_IPS("Connection_Probe_IPs", "", "Extra IP(s) to test while probe for the car"),
DONATION_DAYS_COUNT("Donation_Days_Count", "0", "Number of days that the user has used the app, counting towards the donation request threshold"),
DONATION_LAST_DAY("Donation_Last_Day", "2000-01-01", "The last day that the user used the app"),
SPOTIFY_CONTROL_SUCCESS("Spotify_Control_Success", "false", "Whether Spotify Control api worked previously"),
Expand Down
69 changes: 49 additions & 20 deletions app/src/main/java/me/hufman/androidautoidrive/CarProber.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import io.bimmergestalt.idriveconnectkit.android.CertMangling
import io.bimmergestalt.idriveconnectkit.android.IDriveConnectionStatus
import io.bimmergestalt.idriveconnectkit.android.security.SecurityAccess
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Socket

/**
* Tries to connect to a car
*/
class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val miniCert: ByteArray?, val j29Cert: ByteArray?): HandlerThread("CarProber") {
class CarProber(val securityAccess: SecurityAccess, val settings: AppSettings, val bmwCert: ByteArray?, val miniCert: ByteArray?, val j29Cert: ByteArray?): HandlerThread("CarProber") {
companion object {
val PORTS = listOf(4004, 4005, 4006, 4007, 4008)
val TAG = "CarProber"
Expand All @@ -25,16 +26,26 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val
var carConnection: BMWRemotingServer? = null

val ProberTask = Runnable {
for (port in PORTS) {
if (probePort(port)) {
// we found a car proxy! probably
// let's try connecting to it
Log.i(TAG, "Found open socket at $port, detecting car brand")
probeCar(port)
// successful connection!
schedule(5000)
val configuredIps = settings[AppSettings.KEYS.CONNECTION_PROBE_IPS]
if (configuredIps.isNotEmpty()) {
for (configuredIp in configuredIps.split(',')) {
val parts = configuredIp.split(':')
val host = parts[0].trim()
val port = parts.getOrNull(1)?.trim()?.toIntOrNull()
if (port != null) { // the user gave us a valid port
if (probePort(host, port)) {
// let's try connecting to it
Log.i(TAG, "Found open socket at $host:$port, detecting car brand")
probeCar(host, port)
}
} else {
// scan the car ports on this host
probeHost(host)
}
}
}
// scan the car ports from local BCL tunnel
probeHost("127.0.0.1")
schedule(2000)
if (IDriveConnectionStatus.isConnected && carConnection == null) {
// weird state, assert that we really have no connection
Expand All @@ -58,8 +69,12 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val
schedule(1000)
}

private fun isConnected(): Boolean {
return IDriveConnectionStatus.isConnected && carConnection != null
}

fun schedule(delay: Long) {
if (!IDriveConnectionStatus.isConnected || carConnection == null) {
if (!isConnected()) {
handler?.removeCallbacks(ProberTask)
handler?.postDelayed(ProberTask, delay)
} else {
Expand All @@ -68,12 +83,26 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val
}
}

/** Tries connecting to a car at this host */
private fun probeHost(host: String) {
for (port in PORTS) {
if (!isConnected() && probePort(host, port)) {
// we found a car proxy! probably
// let's try connecting to it
Log.i(TAG, "Found open socket at $port, detecting car brand")
probeCar(host, port)
}
}
}

/**
* Detects whether a port is open
*/
private fun probePort(port: Int): Boolean {
private fun probePort(host: String, port: Int): Boolean {
try {
val socket = Socket("127.0.0.1", port)
// Log.d(TAG, "Probing $host:$port")
val socket = Socket()
socket.connect(InetSocketAddress(host, port), 100)
if (socket.isConnected) {
socket.close()
return true
Expand All @@ -88,7 +117,7 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val
/**
* Attempts to detect the car brand
*/
private fun probeCar(port: Int) {
private fun probeCar(host: String, port: Int) {
if (!securityAccess.isConnected()) {
// try again later after the security service is ready
schedule(2000)
Expand All @@ -106,7 +135,7 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val
else -> j29Cert
} ?: continue // j29Cert is optional cdsBaseApp from MyBMW
val signedCert = CertMangling.mergeBMWCert(cert, securityAccess.fetchBMWCerts(brandHint = brand))
val conn = IDriveConnection.getEtchConnection("127.0.0.1", port, BaseBMWRemotingClient())
val conn = IDriveConnection.getEtchConnection(host, port, BaseBMWRemotingClient())
val sas_challenge = conn.sas_certificate(signedCert)
val sas_login = securityAccess.signChallenge(challenge = sas_challenge)
conn.sas_login(sas_login)
Expand All @@ -119,18 +148,18 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val
Log.i(TAG, "Probing detected a HMI type $hmiType")
if (hmiType?.startsWith("BMW") == true) {
// BMW brand
setConnectedState(port, "bmw")
setConnectedState(host, port, "bmw")
success = true
break
}
if (hmiType?.startsWith("MINI") == true) {
// MINI connected
setConnectedState(port, "mini")
setConnectedState(host, port, "mini")
success = true
break
}
if (brand == "j29") {
setConnectedState(port, "j29")
setConnectedState(host, port, "j29")
success = true
break
}
Expand All @@ -156,8 +185,8 @@ class CarProber(val securityAccess: SecurityAccess, val bmwCert: ByteArray?, val
}
}

private fun setConnectedState(port: Int, brand: String) {
Log.i(TAG, "Successfully detected $brand connection at port $port")
IDriveConnectionStatus.setConnection(brand, "127.0.0.1", port)
private fun setConnectedState(host: String, port: Int, brand: String) {
Log.i(TAG, "Successfully detected $brand connection at port $host:$port")
IDriveConnectionStatus.setConnection(brand, host, port)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ class MainService: Service() {

private fun startCarProber() {
if (carProberThread?.isAlive != true) {
carProberThread = CarProber(securityAccess,
carProberThread = CarProber(securityAccess, appSettings,
CarAppAssetResources(applicationContext, "smartthings").getAppCertificateRaw("bmw")?.readBytes(),
CarAppAssetResources(applicationContext, "smartthings").getAppCertificateRaw("mini")?.readBytes(),
CarAppAssetResources(applicationContext, "cdsbaseapp").getAppCertificateRaw("")?.readBytes()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ class MusicAppMode(val iDriveConnectionStatus: IDriveConnectionStatus, val capab
}

fun isBTConnection(): Boolean {
return TRANSPORT_PORTS.fromPort(iDriveConnectionStatus.port) == TRANSPORT_PORTS.BT
return (iDriveConnectionStatus.host == "127.0.0.1" || iDriveConnectionStatus.host == "::1") &&
TRANSPORT_PORTS.fromPort(iDriveConnectionStatus.port) == TRANSPORT_PORTS.BT
}
fun supportsUsbAudio(): Boolean {
return appSettings[AppSettings.KEYS.AUDIO_SUPPORTS_USB].toBoolean()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import me.hufman.androidautoidrive.AppSettings
import me.hufman.androidautoidrive.BooleanLiveSetting
import me.hufman.androidautoidrive.StringLiveSetting

class MusicAdvancedSettingsModel(appContext: Context): ViewModel() {
class Factory(val appContext: Context): ViewModelProvider.Factory {
Expand All @@ -15,6 +16,7 @@ class MusicAdvancedSettingsModel(appContext: Context): ViewModel() {
}

val showAdvanced = BooleanLiveSetting(appContext, AppSettings.KEYS.SHOW_ADVANCED_SETTINGS)
val connectionProbeIps = StringLiveSetting(appContext, AppSettings.KEYS.CONNECTION_PROBE_IPS)
val audioContext = BooleanLiveSetting(appContext, AppSettings.KEYS.AUDIO_FORCE_CONTEXT)
val spotifyLayout = BooleanLiveSetting(appContext, AppSettings.KEYS.FORCE_SPOTIFY_LAYOUT)
val audioplayerLayout = BooleanLiveSetting(appContext, AppSettings.KEYS.FORCE_AUDIOPLAYER_LAYOUT)
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/res/layout/fragment_music_advancedsettings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/lbl_connection_probe_ips"
android:labelFor="@id/txtConnectionProbeIps"
android:layout_marginBottom="@dimen/settings_vertical_margin" />

<EditText
android:id="@+id/txtConnectionProbeIps"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:text="@={settings.connectionProbeIps}"
android:layout_marginBottom="@dimen/settings_vertical_margin"
android:autofillHints="connection_ip" />

<androidx.appcompat.widget.SwitchCompat
android:id="@+id/swAudioContext"
android:checked="@={settings.audioContext}"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@
<string name="txt_cds_view">Car Data Report:</string>
<string name="txt_car_capabilities">Detailed Car Capabilities:</string>
<string name="lbl_advanced_settings">Show Advanced Settings</string>
<string name="lbl_connection_probe_ips">Try connecting to a car by TCP tunnel</string>

<string name="lbl_assistant">Voice Assistant (Beta)</string>
<string name="lbl_assistant_empty">None found</string>
Expand Down

0 comments on commit 9499150

Please sign in to comment.