diff --git a/app/src/main/java/tech/httptoolkit/android/HttpToolkitApplication.kt b/app/src/main/java/tech/httptoolkit/android/HttpToolkitApplication.kt index 3a0bcce..c454519 100644 --- a/app/src/main/java/tech/httptoolkit/android/HttpToolkitApplication.kt +++ b/app/src/main/java/tech/httptoolkit/android/HttpToolkitApplication.kt @@ -1,21 +1,38 @@ package tech.httptoolkit.android import android.app.Application +import android.content.SharedPreferences import com.google.android.gms.analytics.GoogleAnalytics import com.google.android.gms.analytics.HitBuilders import com.google.android.gms.analytics.Tracker import io.sentry.Sentry import io.sentry.android.AndroidSentryClientFactory + @Suppress("unused") class HttpToolkitApplication : Application() { private var analytics: GoogleAnalytics? = null private var ga: Tracker? = null + private var prefs: SharedPreferences? = null + + private var _isFirstRun: Boolean? = null + private val isFirstRun: Boolean + get() = this._isFirstRun != false override fun onCreate() { super.onCreate() + prefs = getSharedPreferences("tech.httptoolkit.android", MODE_PRIVATE) + + // TODO: Similar to this, but not this: want to get the install referrer and use it *once* + // Need a verb for 'get and use up': useUpInstallReferrer + // Here: if there is an install referrer, and none saved(null not false), save it. + // After it's used, set to false + + _isFirstRun = prefs!!.getBoolean("is-first-run", true) + prefs!!.edit().putBoolean("is-first-run", false) + if (BuildConfig.SENTRY_DSN != null) { Sentry.init(BuildConfig.SENTRY_DSN, AndroidSentryClientFactory(this)) } @@ -58,4 +75,8 @@ class HttpToolkitApplication : Application() { analytics?.setLocalDispatchPeriod(120) // Set dispatching back to Android default } + fun isFirstRun() { + + } + } \ No newline at end of file diff --git a/app/src/main/java/tech/httptoolkit/android/MainActivity.kt b/app/src/main/java/tech/httptoolkit/android/MainActivity.kt index b95b868..b046da2 100644 --- a/app/src/main/java/tech/httptoolkit/android/MainActivity.kt +++ b/app/src/main/java/tech/httptoolkit/android/MainActivity.kt @@ -1,9 +1,7 @@ package tech.httptoolkit.android -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter +import android.app.AlertDialog +import android.content.* import android.net.Uri import android.net.VpnService import androidx.appcompat.app.AppCompatActivity @@ -42,7 +40,7 @@ enum class MainState { DISCONNECTING } -private fun getCerticateFingerprint(cert: X509Certificate): String { +private fun getCertificateFingerprint(cert: X509Certificate): String { val md = MessageDigest.getInstance("SHA-256") md.update(cert.publicKey.encoded) val fingerprint = md.digest() @@ -88,6 +86,38 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() { addAction(VPN_STOPPED_BROADCAST) }) app = this.application as HttpToolkitApplication + + // TODO: Listen for referrer data from app installs too + + // Check if first run + // Check if packageManager.getPackageInfo(packageName, 0).firstInstallTime is recent + // ^ TODO: Test if this is reset on reinstall + // If it's recent (1 hour), get the referrer + // If set & valid (android...), use it. + + if (intent != null && intent.action == Intent.ACTION_VIEW) { + + // TODO: Save global state, remembering the last proxy we connected to. + // Only show this message if there has been at least one. + + // If we were started from an intent (e.g. another barcode scanner/link), confirm + // interception, then start the VPN. + AlertDialog.Builder(this) + .setTitle("Enable Interception") + .setMessage( + "Do you want to share all this device's HTTP traffic with HTTP Toolkit?" + + "\n\n" + + "Only accept this if you trust the source." + ) + // Specifying a listener allows you to take an action before dismissing the dialog. + // The dialog is automatically dismissed when a dialog button is clicked. + .setPositiveButton("Enable"){ _, _ -> + launch { connectToVpnFromUrl(intent.data!!) } + } + .setNegativeButton("Cancel", null) + .setIcon(android.R.drawable.ic_dialog_alert) + .show() + } } override fun onResume() { @@ -212,7 +242,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() { val foundCert = certFactory.generateCertificate( ByteArrayInputStream(certString.toByteArray(Charsets.UTF_8)) ) as X509Certificate - val foundCertHash = getCerticateFingerprint(foundCert) + val foundCertHash = getCertificateFingerprint(foundCert) if (proxyInfo.certificateHash == foundCertHash) { validatedCertificate = foundCert @@ -244,13 +274,20 @@ class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() { } } - private fun ensureCertificateTrusted(proxyConfig: ProxyConfig) { + private fun isVpnConfigured(): Boolean { + return VpnService.prepare(this) == null + } + + private fun isCertTrusted(proxyConfig: ProxyConfig): Boolean { val keyStore = KeyStore.getInstance("AndroidCAStore") keyStore.load(null, null) val certificateAlias = keyStore.getCertificateAlias(proxyConfig.certificate) + return certificateAlias != null + } - if (certificateAlias == null) { + private fun ensureCertificateTrusted(proxyConfig: ProxyConfig) { + if (isCertTrusted(proxyConfig)) { app!!.trackEvent("Setup", "installing-cert") Log.i(TAG, "Certificate not trusted, prompting to install") val certInstallIntent = KeyChain.createInstallIntent() diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..e5e5cd4 --- /dev/null +++ b/plan.md @@ -0,0 +1,158 @@ +There should be no 'Toggle VPN' button: what does it mean most of the time? Very unclear + +# States: + +## Never connected / Reset + +Shows: +* Instructions + * Make sure you have HTK running ((i) -> Don't have it? Install it from httptoolkit.tech) + * Select the 'Android device' option and scan the barcode shown. + * Link to docs +* Scan button -> Scan +* Manually connect button -> Manually connect + +## Scan activity + +Shows the camera +When a valid barcode is found: +* Return to main activity with the result + +## Manually connect activity + +Shows: ip, port, connect button + +Enter the ip & port +Connect: +* Return to main activity with the result + +## Connect to / loading: + +Separate state of the main activity. + +Shows a spinner whilst we connect + +Jumps to error if we fail +Jumps to connected if we succeed + +## Connected: + +Separate state of the main activity. + +Shows the current HTTP Toolkit connection state & details + +> +> Connected +> +> to 10.0.0.1 on port 8000 + +Shows a button to disconnect +* Disconnects, shows start UI when complete + +--- + +# Logic: + +Initial connection data: +List of ips, port, optional certificate hash + +For each ip: + - Send an *http* request to android.httptoolkit.tech/certificate via the proxy to get the cert + - If it fails or the cert doesn't match the hash, try the next ip +If one worked, that's our ip, port & cert +If not, show an error like: +> Could not connect to HTTP Toolkit. Is it connected to the same network as this device? +> More info: +> Tried port with addresses: +> - 172.20.10.13: ECONNECT +> - 10.0.0.1: Certificate mismatch + +--- + +Last connection data: +Single ip, port & certificate + +Connection test: connect to amiusing.httptoolkit.tech via proxy, examine the result. + +--- + + +-------------------- + + H T + T P + + Start HTTP Toolkit, + select 'Android' on + the Intercept page, + and then tap below to + scan the code shown. + + -------------- +---[ Scan Code ]--- + -------------- + + [Connect manually] + + [Read the docs] + +-------------------- + +-------------------- + + H T + T P + + *Connected* + + to 10.0.0.1 + on port 8000 + + -------------- +---[ Disconnect ]--- + -------------- + + [Read the docs] + +-------------------- + +-------------------- + + H T + T P + + + Connecting... + + + ----------------- +-[ Please wait... ]- + ----------------- + + [Read the docs] + +-------------------- + +-------------------- + + H T + T P + + Start HTTP Toolkit, + select 'Android' on + the Intercept page, + and then tap below to + scan the code shown. + + -------------- +---[ Scan Code ]--- + -------------- + + [ Reconnect ] + [ to 10.0.0.1 ] + + [Connect manually] + + [Read the docs] + +-------------------- \ No newline at end of file