SmokeScreen.kt 15.4 KB
Newer Older
1
2
3
package com.frostnerd.smokescreen

import android.app.Application
4
5
6
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
7
import android.content.Intent
8
import androidx.core.app.NotificationCompat
9
import com.frostnerd.dnstunnelproxy.AddressCreator
10
import com.frostnerd.encrypteddnstunnelproxy.AbstractHttpsDNSHandle
11
import com.frostnerd.encrypteddnstunnelproxy.HttpsDnsServerInformation
Daniel Wolf's avatar
Daniel Wolf committed
12
import com.frostnerd.encrypteddnstunnelproxy.quic.AbstractQuicDnsHandle
13
import com.frostnerd.smokescreen.activity.ErrorDialogActivity
14
import com.frostnerd.smokescreen.activity.LoggingDialogActivity
15
import com.frostnerd.smokescreen.activity.PinActivity
16
import com.frostnerd.smokescreen.database.AppDatabase
17
import com.frostnerd.smokescreen.util.Notifications
18
import com.frostnerd.smokescreen.util.RequestCodes
Daniel Wolf's avatar
Daniel Wolf committed
19
import com.frostnerd.smokescreen.util.crashhelpers.DataSavingSentryEventProcessor
Daniel Wolf's avatar
Daniel Wolf committed
20
import com.frostnerd.smokescreen.util.preferences.AppSettings
21
import com.frostnerd.smokescreen.util.preferences.Crashreporting
Daniel Wolf's avatar
Daniel Wolf committed
22
23
24
import io.sentry.Integration
import io.sentry.Sentry
import io.sentry.SentryOptions
Daniel Wolf's avatar
Daniel Wolf committed
25
import io.sentry.android.core.*
Daniel Wolf's avatar
Daniel Wolf committed
26
import io.sentry.protocol.User
27
import okhttp3.*
28
29
30
31
32
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
33
import java.io.IOException
34
import java.util.*
35
36
import kotlin.system.exitProcess

Daniel Wolf's avatar
Daniel Wolf committed
37
38
/*
 * Copyright (C) 2019 Daniel Wolf (Ch4t4r)
39
 *
Daniel Wolf's avatar
Daniel Wolf committed
40
41
42
43
44
45
46
47
48
49
50
51
52
53
 * 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 <http://www.gnu.org/licenses/>.
 *
 * You can contact the developer at daniel.wolf@frostnerd.com.
54
55
 */
class SmokeScreen : Application() {
56
57
58
    companion object {
        var sentryReady:Boolean = false
            private set
59

Daniel Wolf's avatar
Daniel Wolf committed
60
61
62
63
64
        private fun setFallbackDns(to: HttpsDnsServerInformation?, context: Context) {
            if (to == null) {
                context.log("Using no fallback DNS server")
                AddressCreator.globalResolve = AddressCreator.defaultResolver
            }
65
            else {
Daniel Wolf's avatar
Daniel Wolf committed
66
                context.log("Using fallback server: $to")
67
68
                val configs = to.serverConfigurations.values
                AddressCreator.globalResolve = {
69
                    val responsesIpv4 = configs.random().query(Question(it, Record.TYPE.A))?.takeIf {
70
71
72
73
74
75
                        it.responseCode == DnsMessage.RESPONSE_CODE.NO_ERROR
                    }?.answerSection?.map {
                        it.payload as A
                    }?.map {
                        it.inetAddress
                    }
76
                    val responsesIpv6 = configs.random().query(Question(it, Record.TYPE.AAAA))?.takeIf {
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
                        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("")
                }
            }
        }
93
    }
94
    private var defaultUncaughtExceptionHandler: Thread.UncaughtExceptionHandler? = null
95
    val customUncaughtExceptionHandler = EnrichableUncaughtExceptionHandler()
96
    private fun showCrashNotification() {
97
        val notification =
98
            NotificationCompat.Builder(this, Notifications.getHighPriorityChannelId(this))
99
100
101
                .setSmallIcon(R.drawable.ic_cloud_warn)
                .setOngoing(false)
                .setAutoCancel(true)
102
                .setPriority(NotificationCompat.PRIORITY_HIGH)
103
104
                .setContentIntent(
                    PendingIntent.getActivity(
105
                        this, RequestCodes.CRASH_NOTIFICATION,
106
                        PinActivity.openAppIntent(this), PendingIntent.FLAG_UPDATE_CURRENT
107
                    )
108
                )
109
                .setContentTitle(getString(R.string.notification_appcrash_title))
110
111
112
        if (getPreferences().loggingEnabled) {
            val pendingIntent = PendingIntent.getActivity(
                this,
113
                RequestCodes.CRASH_NOTIFICATION_SEND_LOGS,
114
115
116
117
                Intent(
                    this,
                    LoggingDialogActivity::class.java
                ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
118
119
                PendingIntent.FLAG_CANCEL_CURRENT
            )
120
121
122
123
124
            notification.addAction(
                R.drawable.ic_share,
                getString(R.string.title_send_logs),
                pendingIntent
            )
125
126
        }

127
128
        notification.setStyle(NotificationCompat.BigTextStyle(notification).bigText(getString(R.string.notification_appcrash_message)))
        (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(
Daniel Wolf's avatar
Daniel Wolf committed
129
            Notifications.ID_APP_CRASH,
130
131
132
133
            notification.build()
        )
    }

134
    override fun onCreate() {
135
        initSentry()
136
137
138
        defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
        Thread.setDefaultUncaughtExceptionHandler(customUncaughtExceptionHandler)
        super.onCreate()
139
        log("Application created.")
140
        handleFallbackDns()
Daniel Wolf's avatar
Daniel Wolf committed
141
142
        loadKnownDNSServers()
        AbstractQuicDnsHandle.installProvider(this, {})
143
144
145
    }

    private fun handleFallbackDns() {
146
147
148
149
150
151
152
153
154
155
156
157
158
        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)
159
        })
160
161
162
163
164

        if(preferences.runWithoutVpn && fallback == null) fallback = AbstractHttpsDNSHandle.waitUntilKnownServersArePopulated {
            it[0] // The IDs are stable and won't change. 0 == Cloudflare
        }
        setFallbackDns(fallback, this)
165
166
    }

167
168
169
170
171
    fun closeSentry() {
        Sentry.close()
        sentryReady = false
    }

172
    fun initSentry(forceStatus: Status = Status.NONE) {
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
        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)
231
                        }
232
233
234
235
236
237
238
239
                        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
240
                    }
241
                }
Daniel Wolf's avatar
Daniel Wolf committed
242
                log("Sentry ready.")
243
244
245
246
247
248
249
250
            } catch (ex: Throwable) {
                log("Creating Sentry errored")
                log(ex)
            }
        }
    }

    private fun getSentryDSN(then:(dsn:String) -> Unit) {
Daniel Wolf's avatar
Daniel Wolf committed
251
        if(BuildConfig.DEBUG) return
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
        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")
                        }
                    }
                })
285
            }
286
287
288
        } catch (ex:Throwable) {
            log("Getting Sentry DSN errorerd")
            log(ex)
289
        }
290
291
    }

Daniel Wolf's avatar
Daniel Wolf committed
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
    private fun setupSentryForDatasaving(sentryOptions: SentryOptions) {
        val remove = mutableListOf<Integration>()
        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())
    }

307
308
309
310
311
312
313
314
315
    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")
    }
316

317
    inner class EnrichableUncaughtExceptionHandler : Thread.UncaughtExceptionHandler {
318
319
320
321
        private val extras = mutableMapOf<String, String>()

        override fun uncaughtException(t: Thread, e: Throwable) {
            e.printStackTrace()
Daniel Wolf's avatar
Daniel Wolf committed
322
            log("Caught an uncaught exception.")
323
324
            log(e, extras)
            extras.clear()
Daniel Wolf's avatar
Daniel Wolf committed
325
            val isPrerelease = !AppSettings.isReleaseVersion
326
            if (isPrerelease && getPreferences().loggingEnabled && getPreferences().crashreportingType == Crashreporting.OFF) {
327
328
329
330
331
332
                startActivity(
                    Intent(
                        this@SmokeScreen,
                        ErrorDialogActivity::class.java
                    ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                )
333
            } else if (isPrerelease && getPreferences().crashreportingType != Crashreporting.OFF) {
334
335
336
337
338
339
340
                showCrashNotification()
            }
            closeLogger()
            defaultUncaughtExceptionHandler?.uncaughtException(t, e)
            exitProcess(0)
        }

341
        fun addExtra(key: String, value: String) {
342
343
344
345
            extras[key] = value
        }

    }
346
347
348
349
}

enum class Status {
    ENABLED, DATASAVING, NONE
350
}