QueryLogDetailFragment.kt 8.55 KB
Newer Older
1
2
package com.frostnerd.smokescreen.fragment.querylogfragment

3
import android.os.Build
4
5
6
7
8
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
9
import com.frostnerd.dnstunnelproxy.QueryListener
10
11
import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.database.entities.DnsQuery
12
13
14
15
import com.frostnerd.smokescreen.database.entities.DnsRule
import com.frostnerd.smokescreen.database.getDatabase
import com.frostnerd.smokescreen.dialog.DnsRuleDialog
import com.google.android.material.snackbar.Snackbar
16
import kotlinx.android.synthetic.main.fragment_querylog_detail.*
17
18
19
20
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
21
import org.minidns.record.Record
22
23
import java.text.DateFormat
import java.util.*
24
25


Daniel Wolf's avatar
Daniel Wolf committed
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
 * 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 <http://www.gnu.org/licenses/>.
 *
 * You can contact the developer at daniel.wolf@frostnerd.com.
43
44
45
46
 */
class QueryLogDetailFragment : Fragment() {
    var currentQuery: DnsQuery? = null
        private set
47
48
    private lateinit var timeFormatSameDay: DateFormat
    private lateinit var timeFormatDifferentDay: DateFormat
Daniel Wolf's avatar
Daniel Wolf committed
49
    private fun formatTimeStamp(timestamp:Long): String {
50
51
52
        return if(isTimeStampToday(timestamp)) timeFormatSameDay.format(timestamp)
        else timeFormatDifferentDay.format(timestamp)
    }
53
    private var hostSourceFetchJob: Job? = null
54
55
56
57
58
59
60
61
62
63
64
65
66

    private fun isTimeStampToday(timestamp:Long):Boolean {
        return timestamp >= getStartOfDay()
    }

    private fun getStartOfDay():Long {
        val calendar = Calendar.getInstance()
        calendar.set(Calendar.HOUR_OF_DAY, 0)
        calendar.set(Calendar.MINUTE, 0)
        calendar.set(Calendar.SECOND, 0)
        calendar.set(Calendar.MILLISECOND, 0)
        return calendar.timeInMillis
    }
Daniel Wolf's avatar
Daniel Wolf committed
67

68
69
70
71
72
73
74
75
    private fun getLocale(): Locale {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
            resources.configuration.locales.get(0)!!
        } else{
            @Suppress("DEPRECATION")
            resources.configuration.locale!!
        }
    }
Daniel Wolf's avatar
Daniel Wolf committed
76

77
78
79
80
81
    private fun setupTimeFormat() {
        val locale = getLocale()
        timeFormatSameDay = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale)
        timeFormatDifferentDay = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale)
    }
82
83
84
    private var viewCreated = false

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
Daniel Wolf's avatar
Daniel Wolf committed
85
        setupTimeFormat()
86
87
88
89
90
91
        return inflater.inflate(R.layout.fragment_querylog_detail, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        viewCreated = true
        updateUi()
92
93
94
        createDnsRule.setOnClickListener {
            val query = currentQuery
            if(query != null) {
Daniel Wolf's avatar
Daniel Wolf committed
95
                val answerIP = query.parsedResponses.firstOrNull {
96
97
                    it.type == query.type
                }?.payload?.toString() ?: if(query.type == Record.TYPE.A) "0.0.0.0" else "::1"
98
                DnsRuleDialog(requireContext(), DnsRule(query.type, query.name, target = answerIP), onRuleCreated = { newRule ->
99
100
101
102
103
                    val id = if (newRule.isWhitelistRule()) {
                        getDatabase().dnsRuleDao().insertWhitelist(newRule)
                    } else getDatabase().dnsRuleDao().insertIgnore(newRule)
                    if (id != -1L) {
                        Snackbar.make(
104
                            requireActivity().findViewById(android.R.id.content),
105
106
107
108
109
                            R.string.windows_querylogging_dnsrule_created,
                            Snackbar.LENGTH_LONG
                        ).show()
                    } else {
                        Snackbar.make(
110
                            requireActivity().findViewById(android.R.id.content),
111
112
113
114
115
116
117
                            R.string.window_dnsrules_hostalreadyexists,
                            Snackbar.LENGTH_LONG
                        ).show()
                    }
                }).show()
            }
        }
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
    }

    fun isShowingQuery(): Boolean {
        return currentQuery != null
    }

    fun showQuery(query: DnsQuery) {
        val wasUpdated = query != currentQuery
        currentQuery = query

        if (wasUpdated) {
            updateUi()
        }
    }

    private fun updateUi() {
        val query = currentQuery
        if(query != null && viewCreated) {
136
            scrollView.scrollTo(0, 0)
137
            hostSourceFetchJob?.cancel()
138
            queryTime.text = formatTimeStamp(query.questionTime)
139
140
141
142
143
144
145
            if(query.responseTime >= query.questionTime) {
                latency.text = (query.responseTime - query.questionTime).toString() + " ms"
            } else {
                latency.text = "-"
            }
            longName.text = query.name
            type.text = query.type.name
146
            protocol.text = when {
147
148
149
                query.responseSource == QueryListener.Source.CACHE -> getString(R.string.windows_querylogging_usedserver_cache)
                query.responseSource == QueryListener.Source.LOCALRESOLVER -> getString(R.string.windows_querylogging_usedserver_dnsrules)
                query.askedServer == null -> "-"
150
151
152
                query.askedServer!!.startsWith("https") -> getString(R.string.fragment_querydetail_mode_doh)
                else -> getString(R.string.fragment_querydetail_mode_dot)
            }
Daniel Wolf's avatar
Daniel Wolf committed
153
154
155
            resolvedBy.text = when (query.responseSource) {
                QueryListener.Source.CACHE -> getString(R.string.windows_querylogging_usedserver_cache)
                QueryListener.Source.LOCALRESOLVER -> getString(R.string.windows_querylogging_usedserver_dnsrules)
156
                else -> query.askedServer?.replace("tls::", "")?.replace("https::", "") ?: "-"
157
            }
Daniel Wolf's avatar
Daniel Wolf committed
158
            responses.text = query.parsedResponses.joinToString(separator = "\n") {
159
                val payload = it.payload
160
                "$payload (${it.type.name}, TTL: ${it.ttl})"
161
162
163
164
            }.let {
                if(it.isBlank()) "-"
                else it
            }
165
166
167

            if(query.responseSource == QueryListener.Source.LOCALRESOLVER) {
                hostSourceWrap.visibility = View.VISIBLE
168
                showRuleSource(query)
169
            } else hostSourceWrap.visibility = View.GONE
170
171
172
        }
    }

173
174
175
    private fun showRuleSource(query:DnsQuery) {
        hostSourceFetchJob = GlobalScope.launch(Dispatchers.IO) {
            val sourceRule = if(query.name.startsWith("www", ignoreCase = true)) {
Daniel Wolf's avatar
Daniel Wolf committed
176
                getDatabase().dnsRuleDao().findRuleTargetEntity(query.name.replaceFirst("www.", "", ignoreCase = true), query.type, true)
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
                    ?: getDatabase().dnsRuleDao().findRuleTargetEntity(query.name, query.type, true)
            } else {
                getDatabase().dnsRuleDao().findRuleTargetEntity(query.name, query.type, true)
            } ?: getDatabase().dnsRuleDao().findPossibleWildcardRuleTarget(
                        query.name, query.type,
                        useUserRules = true,
                        includeWhitelistEntries = false,
                        includeNonWhitelistEntries = true
                    ).firstOrNull {
                        DnsRuleDialog.databaseHostToMatcher(it.host).reset(query.name).matches()
                    }
            val text = if (sourceRule != null) {
                if (sourceRule.importedFrom == null) {
                    getString(R.string.windows_querylogging_hostsource__user)
                } else {
                    getDatabase().hostSourceDao().findById(sourceRule.importedFrom)?.name
                        ?: getString(R.string.windows_querylogging_hostsource__unknown)
                }
            } else {
                getString(R.string.windows_querylogging_hostsource__unknown)
            }
            if (hostSourceFetchJob?.isCancelled == false) launch(Dispatchers.Main) {
                hostSource.text = text
            }
        }
    }
203
}