package com.frostnerd.smokescreen
import android.app.Application
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.core.app.NotificationCompat
import com.frostnerd.dnstunnelproxy.AddressCreator
import com.frostnerd.encrypteddnstunnelproxy.AbstractHttpsDNSHandle
import com.frostnerd.encrypteddnstunnelproxy.HttpsDnsServerInformation
import com.frostnerd.encrypteddnstunnelproxy.quic.AbstractQuicDnsHandle
import com.frostnerd.smokescreen.activity.ErrorDialogActivity
import com.frostnerd.smokescreen.activity.LoggingDialogActivity
import com.frostnerd.smokescreen.activity.PinActivity
import com.frostnerd.smokescreen.database.AppDatabase
import com.frostnerd.smokescreen.util.Notifications
import com.frostnerd.smokescreen.util.RequestCodes
import com.frostnerd.smokescreen.util.crashhelpers.DataSavingSentryEventProcessor
import com.frostnerd.smokescreen.util.preferences.AppSettings
import com.frostnerd.smokescreen.util.preferences.Crashreporting
import io.sentry.Integration
import io.sentry.Sentry
import io.sentry.SentryOptions
import io.sentry.android.core.*
import io.sentry.protocol.User
import okhttp3.*
import org.minidns.dnsmessage.DnsMessage
import org.minidns.dnsmessage.Question
import org.minidns.record.A
import org.minidns.record.AAAA
import org.minidns.record.Record
import java.io.IOException
import java.util.*
import kotlin.system.exitProcess
/*
* Copyright (C) 2019 Daniel Wolf (Ch4t4r)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*
* You can contact the developer at daniel.wolf@frostnerd.com.
*/
class SmokeScreen : Application() {
companion object {
var sentryReady:Boolean = false
private set
private fun setFallbackDns(to: HttpsDnsServerInformation?, context: Context) {
if (to == null) {
context.log("Using no fallback DNS server")
AddressCreator.globalResolve = AddressCreator.defaultResolver
}
else {
context.log("Using fallback server: $to")
val configs = to.serverConfigurations.values
AddressCreator.globalResolve = {
val responsesIpv4 = configs.random().query(Question(it, Record.TYPE.A))?.takeIf {
it.responseCode == DnsMessage.RESPONSE_CODE.NO_ERROR
}?.answerSection?.map {
it.payload as A
}?.map {
it.inetAddress
}
val responsesIpv6 = configs.random().query(Question(it, Record.TYPE.AAAA))?.takeIf {
it.responseCode == DnsMessage.RESPONSE_CODE.NO_ERROR
}?.answerSection?.map {
it.payload as AAAA
}?.map {
it.inetAddress
}
val responses = if(responsesIpv4 != null) {
if(responsesIpv6 != null) responsesIpv4 + responsesIpv6
else responsesIpv4
} else responsesIpv6
responses?.takeIf { it.isNotEmpty() }?.toTypedArray() ?: error("")
}
}
}
}
private var defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler? = null
val customUncaughtExceptionHandler = EnrichableUncaughtExceptionHandler()
private fun showCrashNotification() {
val notification =
NotificationCompat.Builder(this, Notifications.getHighPriorityChannelId(this))
.setSmallIcon(R.drawable.ic_cloud_warn)
.setOngoing(false)
.setAutoCancel(true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(
PendingIntent.getActivity(
this, RequestCodes.CRASH_NOTIFICATION,
PinActivity.openAppIntent(this), PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setContentTitle(getString(R.string.notification_appcrash_title))
if (getPreferences().loggingEnabled) {
val pendingIntent = PendingIntent.getActivity(
this,
RequestCodes.CRASH_NOTIFICATION_SEND_LOGS,
Intent(
this,
LoggingDialogActivity::class.java
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
PendingIntent.FLAG_CANCEL_CURRENT
)
notification.addAction(
R.drawable.ic_share,
getString(R.string.title_send_logs),
pendingIntent
)
}
notification.setStyle(NotificationCompat.BigTextStyle(notification).bigText(getString(R.string.notification_appcrash_message)))
(getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(
Notifications.ID_APP_CRASH,
notification.build()
)
}
override fun onCreate() {
initSentry()
defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler(customUncaughtExceptionHandler)
super.onCreate()
log("Application created.")
handleFallbackDns()
loadKnownDNSServers()
AbstractQuicDnsHandle.installProvider(this, {})
}
private fun handleFallbackDns() {
val preferences = getPreferences()
var runWithoutVPN = preferences.runWithoutVpn
var fallback = preferences.fallbackDns as HttpsDnsServerInformation?
preferences.listenForChanges(setOf("fallback_dns_server", "run_without_vpn"), preferences.preferenceChangeListener { changes ->
var newFallback = if("fallback_dns_server" in changes) changes["fallback_dns_server"]?.second as HttpsDnsServerInformation? else fallback
val newRunWithoutVPN = changes["run_without_vpn"]?.second as Boolean? ?: runWithoutVPN
runWithoutVPN = newRunWithoutVPN
fallback = newFallback
if(runWithoutVPN && newFallback == null) newFallback = AbstractHttpsDNSHandle.waitUntilKnownServersArePopulated {
it[0] // The IDs are stable and won't change. 0 == Cloudflare
}
setFallbackDns(newFallback, this@SmokeScreen)
})
if(preferences.runWithoutVpn && fallback == null) fallback = AbstractHttpsDNSHandle.waitUntilKnownServersArePopulated {
it[0] // The IDs are stable and won't change. 0 == Cloudflare
}
setFallbackDns(fallback, this)
}
fun closeSentry() {
Sentry.close()
sentryReady = false
}
fun initSentry(forceStatus: Status = Status.NONE) {
val enabledType = getPreferences().crashreportingType
if (forceStatus == Status.NONE &&
enabledType == Crashreporting.OFF) return
log("Maybe initializing Sentry")
getSentryDSN { resolvedDSN ->
log("Initializing Sentry.")
sentryReady = false
try {
if (sentryReady) {
log("Reinitializing Sentry with new DSN")
SentryAndroid.init(this@SmokeScreen) {
it.dsn = resolvedDSN
}
} else {
if (forceStatus != Status.DATASAVING && (enabledType == Crashreporting.FULL || forceStatus == Status.ENABLED)) {
// Enable Sentry in full mode
// This passes some device-related data, but nothing which allows user actions to be tracked across the app
// Info: Some data is attached by the AndroidEventBuilderHelper class, which is present by default
SentryAndroid.init(this@SmokeScreen) {
it.dsn = resolvedDSN
}
Sentry.setUser(User().apply {
this.username = getPreferences().crashReportingUUID
})
Sentry.setTag("user.language", Locale.getDefault().displayLanguage)
Sentry.setTag(
"app.database_version",
AppDatabase.currentVersion.toString()
)
Sentry.setTag(
"app.dns_server_name",
getPreferences().dnsServerConfig.name
)
Sentry.setTag(
"app.dns_server_primary",
getPreferences().dnsServerConfig.servers[0].address.formatToString()
)
Sentry.setTag(
"app.dns_server_secondary",
getPreferences().dnsServerConfig.servers.getOrNull(1)?.address?.formatToString()
?: ""
)
Sentry.setTag(
"app.installer_package",
packageManager.getInstallerPackageName(packageName) ?: ""
)
Sentry.setTag("richdata", "true")
Sentry.setTag("app.fromCi", BuildConfig.FROM_CI.toString())
Sentry.setTag("app.commit", BuildConfig.COMMIT_HASH)
sentryReady = true
} else if (enabledType == Crashreporting.MINIMAL || forceStatus == Status.DATASAVING) {
// Inits Sentry in datasaving mode
// Only data absolutely necessary is transmitted (Android version, app version).
// Only crashes will be reported, no regular events.
SentryAndroid.init(this@SmokeScreen) {
it.dsn = resolvedDSN
setupSentryForDatasaving(it)
}
Sentry.setUser(User().apply {
this.username = "anon-" + BuildConfig.VERSION_CODE
})
Sentry.setTag("richdata", "false")
Sentry.setTag("dist", BuildConfig.VERSION_CODE.toString())
Sentry.setTag("app.commit", BuildConfig.COMMIT_HASH)
Sentry.setTag("app.fromCi", BuildConfig.FROM_CI.toString())
sentryReady = true
}
}
log("Sentry ready.")
} catch (ex: Throwable) {
log("Creating Sentry errored")
log(ex)
}
}
}
private fun getSentryDSN(then:(dsn:String) -> Unit) {
if(BuildConfig.DEBUG) return
try {
val primaryDSN = BuildConfig.SENTRY_DSN
val configServer = BuildConfig.SENTRY_DSN_CONFIGSERVER
if(primaryDSN.contains("@")) then(primaryDSN)
else log("Primary Sentry DSN not set, maybe retrieving from server")
if(configServer.isNotBlank() && configServer.startsWith("http") && !configServer.contains("@")) {
log("Dynamically retrieving Sentry DSN from $configServer")
val request = Request.Builder().url(configServer).build()
OkHttpClient.Builder().build().newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
log("Sentry DSN retrieval failed with error: ${e.message}")
}
override fun onResponse(call: Call, response: Response) {
println("RESPONSE")
if (!response.isSuccessful) {
log("Sentry DSN retrieval failed with status ${response.code}")
}
val retrievedDSN = response.body?.string()?.trim()
if (retrievedDSN.isNullOrBlank() || !retrievedDSN.contains("@") || !retrievedDSN.startsWith(
"http"
)
) {
log("Sentry DSN retrieval returned invalid DSN '$retrievedDSN'.")
} else {
if (true || retrievedDSN != primaryDSN) {
log("Sentry DSN successfuly resolved to '$retrievedDSN'")
then(retrievedDSN)
} else log("Retrieved Sentry DSN is the same as configured DSN, not re-configuring")
}
}
})
}
} catch (ex:Throwable) {
log("Getting Sentry DSN errorerd")
log(ex)
}
}
private fun setupSentryForDatasaving(sentryOptions: SentryOptions) {
val remove = mutableListOf()
sentryOptions.integrations.forEach {
if (it is PhoneStateBreadcrumbsIntegration ||
it is SystemEventsBreadcrumbsIntegration ||
it is TempSensorBreadcrumbsIntegration ||
it is AppComponentsBreadcrumbsIntegration ||
it is SystemEventsBreadcrumbsIntegration ||
it is AppLifecycleIntegration
) remove.add(it)
}
remove.forEach { sentryOptions.integrations.remove(it) }
sentryOptions.eventProcessors.add(DataSavingSentryEventProcessor())
}
override fun onLowMemory() {
super.onLowMemory()
log("The system seems to have low memory")
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
log("Memory has been trimmed with level $level")
}
inner class EnrichableUncaughtExceptionHandler : Thread.UncaughtExceptionHandler {
private val extras = mutableMapOf()
override fun uncaughtException(t: Thread, e: Throwable) {
e.printStackTrace()
log("Caught an uncaught exception.")
log(e, extras)
extras.clear()
val isPrerelease = !AppSettings.isReleaseVersion
if (isPrerelease && getPreferences().loggingEnabled && getPreferences().crashreportingType == Crashreporting.OFF) {
startActivity(
Intent(
this@SmokeScreen,
ErrorDialogActivity::class.java
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
)
} else if (isPrerelease && getPreferences().crashreportingType != Crashreporting.OFF) {
showCrashNotification()
}
closeLogger()
defaultUncaughtExceptionHandler?.uncaughtException(t, e)
exitProcess(0)
}
fun addExtra(key: String, value: String) {
extras[key] = value
}
}
}
enum class Status {
ENABLED, DATASAVING, NONE
}