Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ android {
applicationId "tech.httptoolkit.android.v1"
minSdkVersion 21
targetSdkVersion 33
versionCode 33
versionName "1.4.0"
versionCode 34
versionName "1.4.1"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

Expand Down
9 changes: 8 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:node="remove" />

<uses-feature android:name="android.software.leanback"
android:required="false" />
<uses-feature android:name="android.hardware.touchscreen"
android:required="false" />

<application
android:name=".HttpToolkitApplication"
android:allowBackup="true"
Expand All @@ -28,7 +33,8 @@
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:targetApi="m" android:usesCleartextTraffic="true"
android:largeHeap="true">
android:largeHeap="true"
android:banner="@drawable/ic_tv_banner">
<service
android:name=".ProxyVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
Expand All @@ -46,6 +52,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>

<intent-filter
Expand Down
107 changes: 61 additions & 46 deletions app/src/main/java/tech/httptoolkit/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -237,11 +237,18 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
when (mainState) {
MainState.DISCONNECTED -> {
statusText.setText(R.string.disconnected_status)
buttonContainer.visibility = View.VISIBLE

detailContainer.addView(detailText(R.string.disconnected_details))
val hasCamera = this.packageManager
.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)

buttonContainer.visibility = View.VISIBLE
buttonContainer.addView(primaryButton(R.string.scan_button, ::scanCode))
if (hasCamera) {
detailContainer.addView(detailText(R.string.disconnected_details))
val scanQrButton = primaryButton(R.string.scan_button, ::scanCode)
buttonContainer.addView(scanQrButton)
} else {
detailContainer.addView(detailText(R.string.disconnected_no_camera_details))
}

val lastProxy = app.lastProxy
if (lastProxy != null) {
Expand Down Expand Up @@ -335,41 +342,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
Log.i(TAG, if (vpnIntent != null) "got intent" else "no intent")
val vpnNotConfigured = vpnIntent != null

if (whereIsCertTrusted(config) == null && PROMPTED_CERT_SETUP_SUPPORTED) {
// The cert isn't trusted, and the VPN may need setup, so there'll be a series of prompts
// here. Explain them beforehand, so users understand what's going on.
withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(this@MainActivity)
.setTitle("Enable interception")
.setIcon(R.drawable.ic_info_circle)
.setMessage(
"To intercept traffic from this device, you need to " +
(if (vpnNotConfigured) "activate HTTP Toolkit's VPN and " else "") +
"trust your HTTP Toolkit's certificate authority. " +
"\n\n" +
"Please accept the following prompts to allow this." +
if (!isDeviceSecured(applicationContext))
"\n\n" +
"Due to Android security requirements, trusting the certificate will " +
"require you to set a PIN, password or pattern for this device."
else " To trust the certificate, your device PIN will be required."
)
.setPositiveButton("Ok") { _, _ ->
if (vpnNotConfigured) {
startActivityForResult(vpnIntent, START_VPN_REQUEST)
} else {
onActivityResult(START_VPN_REQUEST, RESULT_OK, null)
}
}
.show()
}
} else if (vpnNotConfigured) {
// In this case the VPN needs setup, but the cert is trusted already, so it's
// a single confirmation. Pretty clear, no need to explain. This happens if the
// VPN/app was removed from the device in the past, or when using injected system certs.
if (vpnNotConfigured) {
// Show the 'Enable the VPN' prompt
startActivityForResult(vpnIntent, START_VPN_REQUEST)
} else {
// VPN is trusted & cert setup already, lets get to it.
// VPN is trusted already, continue
onActivityResult(START_VPN_REQUEST, RESULT_OK, null)
}

Expand Down Expand Up @@ -637,26 +614,56 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
if (existingTrust == null) {
Log.i(TAG, "Certificate not trusted, prompting to install")

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
// Android 11+, with no trusted cert: we need to download the cert to Downloads and
// then tell the user how to install it manually:
launch { promptToManuallyInstallCert(proxyConfig.certificate) }
} else {
if (PROMPTED_CERT_SETUP_SUPPORTED) {
// Up until Android 11, we can prompt the user to install the CA cert into the user
// CA store. Notably, if the cert is already installed as a system cert but
// disabled, this will get triggered, and will enable the cert, rather than adding
// a normal user cert.
val certInstallIntent = KeyChain.createInstallIntent()
certInstallIntent.putExtra(EXTRA_NAME, "HTTP Toolkit CA")
certInstallIntent.putExtra(EXTRA_CERTIFICATE, proxyConfig.certificate.encoded)
startActivityForResult(certInstallIntent, INSTALL_CERT_REQUEST)
launch { promptToAutoInstallCert(proxyConfig.certificate) }
} else {
// Android 11+, with no trusted cert: we need to download the cert to Downloads and
// then tell the user how to install it manually:
launch { promptToManuallyInstallCert(proxyConfig.certificate) }
}
} else {
Log.i(TAG, "Certificate already trusted, continuing")
onActivityResult(INSTALL_CERT_REQUEST, RESULT_OK, null)
}
}

private suspend fun promptToAutoInstallCert(certificate: Certificate) {
withContext(Dispatchers.Main) {
MaterialAlertDialogBuilder(this@MainActivity)
.setTitle("Enable HTTPS interception")
.setIcon(R.drawable.ic_info_circle)
.setMessage(
"To intercept HTTPS traffic from this device, you need to " +
"trust your HTTP Toolkit's certificate authority. " +
"\n\n" +
"Please accept the following prompts to allow this." +
if (!isDeviceSecured(applicationContext))
"\n\n" +
"Due to Android security requirements, trusting the certificate will " +
"require you to set a PIN, password or pattern for this device."
else " To trust the certificate, your device PIN will be required."
)
.setPositiveButton("Install") { _, _ ->
val certInstallIntent = KeyChain.createInstallIntent()
certInstallIntent.putExtra(EXTRA_NAME, "HTTP Toolkit CA")
certInstallIntent.putExtra(EXTRA_CERTIFICATE, certificate.encoded)
startActivityForResult(certInstallIntent, INSTALL_CERT_REQUEST)
}
.setNeutralButton("Skip") { _, _ ->
onActivityResult(INSTALL_CERT_REQUEST, RESULT_OK, null)
}
.setNegativeButton("Cancel") { _, _ ->
disconnect()
}
.setCancelable(false)
.show()
}
}

@RequiresApi(Build.VERSION_CODES.Q)
private suspend fun promptToManuallyInstallCert(cert: Certificate, repeatPrompt: Boolean = false) {
if (!repeatPrompt) {
Expand Down Expand Up @@ -694,7 +701,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
Html.fromHtml(
"""
<p>
Android ${Build.VERSION.RELEASE} doesn't allow automatic certificate setup.
${
if (PROMPTED_CERT_SETUP_SUPPORTED)
"Automatic certificate installation failed, so it must be done manually."
else
"Android ${Build.VERSION.RELEASE} doesn't allow automatic certificate setup."
}
</p>
<p>
To allow HTTP Toolkit to intercept HTTPS traffic:
Expand All @@ -721,6 +733,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
.setPositiveButton("Open security settings") { _, _ ->
startActivityForResult(Intent(Settings.ACTION_SECURITY_SETTINGS), INSTALL_CERT_REQUEST)
}
.setNeutralButton("Skip") { _, _ ->
onActivityResult(INSTALL_CERT_REQUEST, RESULT_OK, null)
}
.setNegativeButton("Cancel") { _, _ ->
disconnect()
}
Expand Down
Binary file added app/src/main/res/drawable/ic_tv_banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
<string name="connected_status">Connected</string>
<string name="failed_status">Oh no!</string>

<string name="disconnected_details">To intercept this device, start HTTP Toolkit on your computer, and activate Android interception there via QR code or using ADB.
</string>
<string name="disconnected_details">To intercept this device, start HTTP Toolkit on your computer, and activate Android interception there via QR code or using ADB.</string>
<string name="disconnected_no_camera_details">To intercept this device, start HTTP Toolkit on your computer, and activate Android interception there using the ADB or Frida options.</string>

<string name="connected_details">to %s on port %d</string>
<string name="connected_tunnel_details">via ADB tunnel</string>
Expand Down