Commit 4fa62d5e authored by Daniel Wolf's avatar Daniel Wolf
Browse files

Merge branch '94-overhaul-the-query-log-view' into 'master'

Resolve "Overhaul the query log view"

Closes #94

See merge request PublicAndroidApps/smokescreen!38
parents cabf80ee 531ab0ee
{
"formatVersion": 1,
"database": {
"version": 11,
"identityHash": "6593a50157ce88fb018043b5c151353a",
"entities": [
{
"tableName": "CachedResponse",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`dnsName` TEXT NOT NULL, `type` INTEGER NOT NULL, `records` TEXT NOT NULL, PRIMARY KEY(`dnsName`, `type`))",
"fields": [
{
"fieldPath": "dnsName",
"columnName": "dnsName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "records",
"columnName": "records",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"dnsName",
"type"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DnsQuery",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `askedServer` TEXT, `responseSource` TEXT NOT NULL, `questionTime` INTEGER NOT NULL, `responseTime` INTEGER NOT NULL, `responses` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "askedServer",
"columnName": "askedServer",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "responseSource",
"columnName": "responseSource",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "questionTime",
"columnName": "questionTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "responseTime",
"columnName": "responseTime",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "responses",
"columnName": "responses",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "DnsRule",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `stagingType` INTEGER NOT NULL, `type` INTEGER NOT NULL, `host` TEXT NOT NULL, `target` TEXT NOT NULL, `ipv6Target` TEXT, `importedFrom` INTEGER, `isWildcard` INTEGER NOT NULL, FOREIGN KEY(`importedFrom`) REFERENCES `HostSource`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "stagingType",
"columnName": "stagingType",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "host",
"columnName": "host",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "target",
"columnName": "target",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "ipv6Target",
"columnName": "ipv6Target",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "importedFrom",
"columnName": "importedFrom",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "isWildcard",
"columnName": "isWildcard",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_DnsRule_importedFrom",
"unique": false,
"columnNames": [
"importedFrom"
],
"createSql": "CREATE INDEX `index_DnsRule_importedFrom` ON `${TABLE_NAME}` (`importedFrom`)"
},
{
"name": "index_DnsRule_host_type_stagingType",
"unique": true,
"columnNames": [
"host",
"type",
"stagingType"
],
"createSql": "CREATE UNIQUE INDEX `index_DnsRule_host_type_stagingType` ON `${TABLE_NAME}` (`host`, `type`, `stagingType`)"
}
],
"foreignKeys": [
{
"table": "HostSource",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"importedFrom"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "HostSource",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `enabled` INTEGER NOT NULL, `ruleCount` INTEGER, `checksum` TEXT, `name` TEXT NOT NULL, `source` TEXT NOT NULL, `whitelistSource` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "enabled",
"columnName": "enabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ruleCount",
"columnName": "ruleCount",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "checksum",
"columnName": "checksum",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "whitelistSource",
"columnName": "whitelistSource",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6593a50157ce88fb018043b5c151353a')"
]
}
}
\ No newline at end of file
...@@ -2,6 +2,7 @@ package com.frostnerd.smokescreen.activity ...@@ -2,6 +2,7 @@ package com.frostnerd.smokescreen.activity
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.frostnerd.dnstunnelproxy.QueryListener
import com.frostnerd.smokescreen.R import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.database.converters.StringListConverter import com.frostnerd.smokescreen.database.converters.StringListConverter
import com.frostnerd.smokescreen.database.entities.DnsQuery import com.frostnerd.smokescreen.database.entities.DnsQuery
...@@ -55,11 +56,17 @@ class QueryImportActivity: AppCompatActivity() { ...@@ -55,11 +56,17 @@ class QueryImportActivity: AppCompatActivity() {
val converter = StringListConverter() val converter = StringListConverter()
for(line in iterator) { for(line in iterator) {
val split = line.split(",") val split = line.split(",")
val source = if(split[5].equals("false", true) || split[5].equals("true", true)) {
if (split[5].toBoolean()) QueryListener.Source.CACHE
else QueryListener.Source.UPSTREAM
} else QueryListener.Source.values().find {
it.name.equals(split[5], true)
} ?: QueryListener.Source.UPSTREAM
queries.add(DnsQuery( queries.add(DnsQuery(
name=split[0], name=split[0],
type = Record.TYPE.getType(split[3].toInt()), type = Record.TYPE.getType(split[3].toInt()),
askedServer = split[4], askedServer = split[4],
fromCache = split[5].toBoolean(), responseSource = source,
questionTime = split[6].toLong(), questionTime = split[6].toLong(),
responseTime = split[7].toLong(), responseTime = split[7].toLong(),
responses = converter.stringToList(split[8].replaceFirst("\"", "").replace(Regex("\"$"), "")) responses = converter.stringToList(split[8].replaceFirst("\"", "").replace(Regex("\"$"), ""))
......
...@@ -37,7 +37,7 @@ import com.frostnerd.smokescreen.database.repository.HostSourceRepository ...@@ -37,7 +37,7 @@ import com.frostnerd.smokescreen.database.repository.HostSourceRepository
@Database(entities = [CachedResponse::class, DnsQuery::class, DnsRule::class, HostSource::class], version = AppDatabase.currentVersion) @Database(entities = [CachedResponse::class, DnsQuery::class, DnsRule::class, HostSource::class], version = AppDatabase.currentVersion)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val currentVersion:Int = 10 const val currentVersion:Int = 11
} }
abstract fun cachedResponseDao(): CachedResponseDao abstract fun cachedResponseDao(): CachedResponseDao
......
...@@ -97,12 +97,21 @@ val MIGRATION_9_10 = migration(9, 10) { ...@@ -97,12 +97,21 @@ val MIGRATION_9_10 = migration(9, 10) {
Logger.logIfOpen("DB_MIGRATION", "Migration from 9 to 10 completed") Logger.logIfOpen("DB_MIGRATION", "Migration from 9 to 10 completed")
} }
val MIGRATION_10_11 = migration(10, 11) {
Logger.logIfOpen("DB_MIGRATION", "Migrating from 10 to 11")
it.execSQL("CREATE TABLE `DnsQuery_tmp` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `askedServer` TEXT, `responseSource` TEXT NOT NULL, `questionTime` INTEGER NOT NULL, `responseTime` INTEGER NOT NULL, `responses` TEXT NOT NULL)")
it.execSQL("INSERT INTO `DnsQuery_tmp`(id, type, name, askedServer, questionTime, responseTime, responses, responseSource) SELECT id, type, name, askedServer, questionTime, responseTime, responses, CASE WHEN fromCache=1 THEN 'CACHE' else 'UPSTREAM' END as `responseSource` FROM DnsQuery")
it.execSQL("DROP TABLE `DnsQuery`")
it.execSQL("ALTER TABLE `DnsQuery_tmp` RENAME TO `DnsQuery`")
Logger.logIfOpen("DB_MIGRATION", "Migration from 10 to 11 completed")
}
fun Context.getDatabase(): AppDatabase { fun Context.getDatabase(): AppDatabase {
if (INSTANCE == null) { if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "data") INSTANCE = Room.databaseBuilder(applicationContext, AppDatabase::class.java, "data")
.allowMainThreadQueries() .allowMainThreadQueries()
.addMigrations(MIGRATION_2_X, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10) .addMigrations(MIGRATION_2_X, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10, MIGRATION_10_11)
.build() .build()
} }
return INSTANCE!! return INSTANCE!!
......
package com.frostnerd.smokescreen.database.converters
import androidx.room.TypeConverter
import com.frostnerd.dnstunnelproxy.QueryListener
/*
* 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.
*/
class QuerySourceConverter {
@TypeConverter
fun sourceFromString(value: String): QueryListener.Source? {
return QueryListener.Source.values().find {
it.name.equals(value, true)
}
}
@TypeConverter
fun sourceToString(source:QueryListener.Source?): String {
return source?.name ?: ""
}
}
\ No newline at end of file
...@@ -5,7 +5,9 @@ import androidx.room.Entity ...@@ -5,7 +5,9 @@ import androidx.room.Entity
import androidx.room.Ignore import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import androidx.room.TypeConverters import androidx.room.TypeConverters
import com.frostnerd.dnstunnelproxy.QueryListener
import com.frostnerd.smokescreen.database.converters.DnsTypeConverter import com.frostnerd.smokescreen.database.converters.DnsTypeConverter
import com.frostnerd.smokescreen.database.converters.QuerySourceConverter
import com.frostnerd.smokescreen.database.converters.StringListConverter import com.frostnerd.smokescreen.database.converters.StringListConverter
import com.frostnerd.smokescreen.database.recordFromBase64 import com.frostnerd.smokescreen.database.recordFromBase64
import org.minidns.record.Record import org.minidns.record.Record
...@@ -29,13 +31,13 @@ import org.minidns.record.Record ...@@ -29,13 +31,13 @@ import org.minidns.record.Record
* You can contact the developer at daniel.wolf@frostnerd.com. * You can contact the developer at daniel.wolf@frostnerd.com.
*/ */
@Entity(tableName = "DnsQuery") @Entity(tableName = "DnsQuery")
@TypeConverters(DnsTypeConverter::class, StringListConverter::class) @TypeConverters(DnsTypeConverter::class, StringListConverter::class, QuerySourceConverter::class)
data class DnsQuery( data class DnsQuery(
@PrimaryKey(autoGenerate = true) var id: Long = 0, @PrimaryKey(autoGenerate = true) var id: Long = 0,
val type: Record.TYPE, val type: Record.TYPE,
val name: String, val name: String,
var askedServer: String?, var askedServer: String?,
var fromCache:Boolean, var responseSource:QueryListener.Source,
val questionTime: Long, val questionTime: Long,
var responseTime: Long = 0, var responseTime: Long = 0,
var responses: MutableList<String> var responses: MutableList<String>
......
...@@ -86,7 +86,7 @@ class DnsQueryRepository(private val dnsQueryDao: DnsQueryDao) { ...@@ -86,7 +86,7 @@ class DnsQueryRepository(private val dnsQueryDao: DnsQueryDao) {
builder.append(query.type.name).append(",") builder.append(query.type.name).append(",")
builder.append(query.type.value).append(",") builder.append(query.type.value).append(",")
builder.append(query.askedServer).append(",") builder.append(query.askedServer).append(",")
builder.append(query.fromCache).append(",") builder.append(query.responseSource?.name ?: "").append(",")
builder.append(query.questionTime).append(",") builder.append(query.questionTime).append(",")
builder.append(query.responseTime).append(",") builder.append(query.responseTime).append(",")
builder.append("\"").append(responseConverter.someObjectListToString(query.responses).replace(",", ";").replace("\"", "'")).append("\"") builder.append("\"").append(responseConverter.someObjectListToString(query.responses).replace(",", ";").replace("\"", "'")).append("\"")
......
package com.frostnerd.smokescreen.fragment.querylogfragment package com.frostnerd.smokescreen.fragment.querylogfragment
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.frostnerd.dnstunnelproxy.QueryListener
import com.frostnerd.smokescreen.R import com.frostnerd.smokescreen.R
import com.frostnerd.smokescreen.database.entities.DnsQuery import com.frostnerd.smokescreen.database.entities.DnsQuery
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
import kotlinx.android.synthetic.main.activity_dns_rules.*
import kotlinx.android.synthetic.main.fragment_querylog_detail.* import kotlinx.android.synthetic.main.fragment_querylog_detail.*
import org.minidns.record.A
import org.minidns.record.Record
import java.text.DateFormat
import java.util.*
/* /*
...@@ -31,17 +42,76 @@ import kotlinx.android.synthetic.main.fragment_querylog_detail.* ...@@ -31,17 +42,76 @@ import kotlinx.android.synthetic.main.fragment_querylog_detail.*
class QueryLogDetailFragment : Fragment() { class QueryLogDetailFragment : Fragment() {
var currentQuery: DnsQuery? = null var currentQuery: DnsQuery? = null
private set private set
private lateinit var timeFormatSameDay: DateFormat
private lateinit var timeFormatDifferentDay: DateFormat
private fun formatTimeStamp(timestamp:Long): String {
return if(isTimeStampToday(timestamp)) timeFormatSameDay.format(timestamp)
else timeFormatDifferentDay.format(timestamp)
}
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
}
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!!
}
}
private fun setupTimeFormat() {
val locale = getLocale()
timeFormatSameDay = DateFormat.getTimeInstance(DateFormat.MEDIUM, locale)
timeFormatDifferentDay = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, locale)
}
private var viewCreated = false private var viewCreated = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setupTimeFormat()
return inflater.inflate(R.layout.fragment_querylog_detail, container, false) return inflater.inflate(R.layout.fragment_querylog_detail, container, false)
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewCreated = true viewCreated = true
updateUi() updateUi()
createDnsRule.setOnClickListener {
val query = currentQuery
if(query != null) {
val answerIP = query.getParsedResponses().firstOrNull {
it.type == query.type
}?.payload?.toString() ?: if(query.type == Record.TYPE.A) "0.0.0.0" else "::1"
DnsRuleDialog(context!!, DnsRule(query.type, query.name, target = answerIP), onRuleCreated = { newRule ->
val id = if (newRule.isWhitelistRule()) {
getDatabase().dnsRuleDao().insertWhitelist(newRule)
} else getDatabase().dnsRuleDao().insertIgnore(newRule)
if (id != -1L) {
Snackbar.make(
activity!!.findViewById(android.R.id.content),
R.string.windows_querylogging_dnsrule_created,
Snackbar.LENGTH_LONG
).show()
} else {
Snackbar.make(
activity!!.findViewById(android.R.id.content),
R.string.window_dnsrules_hostalreadyexists,
Snackbar.LENGTH_LONG
).show()
}
}).show()
}
}
} }
fun isShowingQuery(): Boolean { fun isShowingQuery(): Boolean {
...@@ -60,7 +130,7 @@ class QueryLogDetailFragment : Fragment() { ...@@ -60,7 +130,7 @@ class QueryLogDetailFragment : Fragment() {