Commit 2180a51f authored by Daniel Wolf's avatar Daniel Wolf
Browse files

Merge branch 'master' of...

Merge branch 'master' of https://git.frostnerd.com/PublicAndroidApps/smokescreen into 185-request-advanced-backup-and-restore
parents 9f2c6766 22924ff9
......@@ -71,6 +71,8 @@ class NewHostSourceDialog(
view.whitelist.isChecked = hostSource.whitelistSource
}
view.url.addTextChangedListener(object: TextWatcher {
private var previousText:String = ""
override fun afterTextChanged(s: Editable?) {
val alteredString = if(s.isNullOrBlank()) "" else if(s.contains("://")) s.toString() else "https://$s"
if(URLUtil.isValidUrl(alteredString)) {
......@@ -90,12 +92,17 @@ class NewHostSourceDialog(
}
view.name.setText(domain)
} else if(URLUtil.isContentUrl(alteredString)) {
val text = context.contentResolver.query(Uri.parse(alteredString), null, null, null, null).let {
if(it?.moveToFirst() == false) null to it
else it?.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) to it
}.let {
it.second?.close()
it.first
val text = try {
context.contentResolver.query(Uri.parse(alteredString), null, null, null, null).let {
if(it?.moveToFirst() == false) null to it
else it?.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) to it
}.let {
it.second?.close()
it.first
}
} catch (e: SecurityException) {
view.url.setText(previousText)
null
}
if(text != null) view.name.setText(text)
}
......@@ -104,6 +111,7 @@ class NewHostSourceDialog(
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
previousText = s.toString()
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
......
......@@ -24,6 +24,17 @@ import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import kotlinx.android.synthetic.main.dialog_new_server.*
import kotlinx.android.synthetic.main.dialog_new_server.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.OkHttpClient
import org.minidns.dnsmessage.DnsMessage
import org.minidns.dnsmessage.Question
import org.minidns.record.Record
import java.lang.Exception
import java.util.concurrent.TimeUnit
/*
* Copyright (C) 2019 Daniel Wolf (Ch4t4r)
......@@ -50,23 +61,44 @@ class NewServerDialog(
onServerAdded: (serverInfo: DnsServerInformation<*>) -> Unit,
server: UserServerConfiguration? = null
) : BaseDialog(context, context.getPreferences().theme.dialogStyle) {
private var validationRegex = SERVER_URL_REGEX
companion object {
// Hostpart has to begin with a character or number
// Then has to either:
// - Consist of numbers, characters and dashes AND ends with a character
// - End with a character or number
// => Host part has to be at least 2 characters long
// Host can optionally end with a dot
// - If there is a dot there has to be either a number or a char after it
private val dohAddressPart = "(?:[a-z0-9](?:(?:[a-z0-9-]*[a-z]*[a-z0-9-]*[a-z0-9])|[a-z0-9])(?:.(?=[a-z0-9])|))*"
val SERVER_URL_REGEX =
Regex(
"^\\s*(?:https://)?((?:$dohAddressPart)|(?:\\[[a-z0-9:]+]))(?::[1-9][0-9]{0,4})?(/[a-z0-9-.]+)*(/)?\\s*$",
RegexOption.IGNORE_CASE
)
val TLS_REGEX = Regex("^\\s*($dohAddressPart)(?::[1-9][0-9]{0,4})?\\s*$", RegexOption.IGNORE_CASE)
fun isValidDoH(s:String): Boolean {
// It should contain at least one period (or colon for IPv6), either because it's an IP Address or a domain (like test.com). This explicitly prevents local-only domains (like localhost or raspberry)
return s.trim().let {
it.toHttpUrlOrNull() ?: "https://$it".toHttpUrlOrNull()
}?.host?.contains(Regex("[.:]")) ?: false
}
fun isValidDot(s:String):Boolean {
return parseDotAddress(s.trim()) != null
}
fun parseDotAddress(s:String):TLSUpstreamAddress? {
return s.takeIf {
!it.startsWith("http", ignoreCase = true) && it.isNotBlank() // It isn't http(s)
}?.let {
"https://$it".toHttpUrlOrNull() // But it has valid format for a URL
}?.takeIf {
it.pathSegments.size == 1 && it.pathSegments[0] == "" // But isn't an URL (no path in string)
}?.takeIf {
it.host.contains(Regex("[.:]")) // It should contain at least one period (or colon for IPv6), either because it's an IP Address or a domain (like test.com). This explicitly prevents local-only domains (like localhost or raspberry)
}?.let { url ->
val port = if(s.indexOf(":").let {
it == -1 || s.toCharArray().getOrNull(it + 1)?.isDigit() != true
}) 853 else url.port // Default would treat it as https (port 443)
// Try to find a matching server in the default list as it'd contain more information than the user provides
AbstractTLSDnsHandle.waitUntilKnownServersArePopulated { allServer ->
allServer.values.filter {
it.servers.any { server ->
server.address.host == url.host && server.address.port == url.port
}
}
}.firstOrNull()?.servers?.firstOrNull {
it.address.host == url.host
}?.address ?: TLSUpstreamAddress(url.host, port)
}
}
}
init {
......@@ -105,9 +137,8 @@ class NewServerDialog(
var secondary =
if (secondaryServer.text.isNullOrBlank()) null else secondaryServer.text.toString().trim()
if (primary.startsWith("https")) primary = primary.replace("https://", "")
if (secondary != null && secondary.startsWith("https")) secondary =
secondary.replace("https://", "")
if (dnsOverHttps && !primary.startsWith("http", ignoreCase = true)) primary = "https://$primary"
if (dnsOverHttps && secondary != null && !secondary.startsWith("http", ignoreCase = true)) secondary = "https://$secondary"
invokeCallback(name, primary, secondary, onServerAdded)
dismiss()
} else {
......@@ -115,7 +146,7 @@ class NewServerDialog(
vibrator.vibrate(250)
}
}
val spinnerAdapter = ArrayAdapter<String>(
val spinnerAdapter = ArrayAdapter(
context, android.R.layout.simple_spinner_item,
arrayListOf(
context.getString(R.string.dialog_serverconfiguration_https),
......@@ -128,7 +159,6 @@ class NewServerDialog(
serverType.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onNothingSelected(parent: AdapterView<*>?) {}
override fun onItemSelected(parent: AdapterView<*>?, v: View?, position: Int, id: Long) {
validationRegex = if (position == 0) SERVER_URL_REGEX else TLS_REGEX
dnsOverHttps = position == 0
setHintAndTitle(view, dnsOverHttps, title)
primaryServer.text = primaryServer.text
......@@ -169,40 +199,12 @@ class NewServerDialog(
onServerAdded: (DnsServerInformation<*>) -> Unit
) {
if (dnsOverHttps) {
val requestType = mapOf(RequestType.WIREFORMAT_POST to ResponseType.WIREFORMAT)
val serverInfo = mutableListOf<HttpsDnsServerConfiguration>()
serverInfo.add(
HttpsDnsServerConfiguration(
address = createHttpsUpstreamAddress(primary),
requestTypes = requestType,
experimental = false
)
)
if (!secondary.isNullOrBlank()) serverInfo.add(
HttpsDnsServerConfiguration(
address = createHttpsUpstreamAddress(
secondary
), requestTypes = requestType, experimental = false
)
)
onServerAdded.invoke(
HttpsDnsServerInformation(
name,
HttpsDnsServerSpecification(
Decision.UNKNOWN,
Decision.UNKNOWN,
Decision.UNKNOWN,
Decision.UNKNOWN
),
serverInfo,
emptyList()
)
)
detectDohServerTypes(name, primary, secondary, onServerAdded)
} else {
val serverInfo = mutableListOf<DnsServerConfiguration<TLSUpstreamAddress>>()
serverInfo.add(
DnsServerConfiguration(
address = createTlsUpstreamAddress(primary),
address = parseDotAddress(primary)!!,
experimental = false,
supportedProtocols = listOf(TLS),
preferredProtocol = TLS
......@@ -210,9 +212,7 @@ class NewServerDialog(
)
if (!secondary.isNullOrBlank()) serverInfo.add(
DnsServerConfiguration(
address = createTlsUpstreamAddress(
secondary
), experimental = false, supportedProtocols = listOf(TLS), preferredProtocol = TLS
address = parseDotAddress(secondary)!!, experimental = false, supportedProtocols = listOf(TLS), preferredProtocol = TLS
)
)
onServerAdded.invoke(
......@@ -231,69 +231,110 @@ class NewServerDialog(
}
}
private fun detectDohServerTypes(
name: String,
primary: String,
secondary: String?,
onServerAdded: (DnsServerInformation<*>) -> Unit
) {
val defaultRequestType = RequestType.WIREFORMAT_POST to ResponseType.WIREFORMAT
val addresses = mutableListOf<HttpsUpstreamAddress>()
addresses.add(createHttpsUpstreamAddress(primary))
if (!secondary.isNullOrBlank()) addresses.add(createHttpsUpstreamAddress(secondary))
val dialog = LoadingDialog(
context,
R.string.dialog_doh_detect_type_title,
R.string.dialog_doh_detect_type_message
)
val httpClient = OkHttpClient.Builder()
.connectTimeout(1250, TimeUnit.MILLISECONDS)
.readTimeout(1250, TimeUnit.MILLISECONDS)
.build()
dialog.show()
GlobalScope.launch {
val availableTypes = mapOf(
defaultRequestType,
RequestType.WIREFORMAT to ResponseType.WIREFORMAT,
RequestType.PARAMETER_REQUEST to ResponseType.JSON
)
val serverInfo = mutableListOf<HttpsDnsServerConfiguration>()
for (address in addresses) {
val detectedTypes = mutableListOf<Pair<RequestType, ResponseType>>()
for (availableType in availableTypes) {
try {
val response = ServerConfiguration.createSimpleServerConfig(address, availableType.key, availableType.value).query(client = httpClient,
question = Question("example.com", Record.TYPE.A))
if(response != null && response.responseCode == DnsMessage.RESPONSE_CODE.NO_ERROR) {
detectedTypes.add(availableType.toPair())
}
} catch (ignored:Exception) {
}
}
if(detectedTypes.isEmpty()) detectedTypes.add(defaultRequestType)
serverInfo.add(HttpsDnsServerConfiguration(
address = address,
experimental = false,
requestTypes = detectedTypes.toMap()
))
}
GlobalScope.launch(Dispatchers.Main) {
dialog.dismiss()
onServerAdded.invoke(
HttpsDnsServerInformation(
name,
HttpsDnsServerSpecification(
Decision.UNKNOWN,
Decision.UNKNOWN,
Decision.UNKNOWN,
Decision.UNKNOWN
),
serverInfo,
emptyList()
)
)
}
}
}
private fun createHttpsUpstreamAddress(url: String): HttpsUpstreamAddress {
context.log("Creating HttpsUpstreamAddress for `$url`")
var host = ""
var port: Int? = null
var path: String? = null
if (url.contains(":")) {
host = url.split(":")[0]
port = url.split(":")[1].split("/")[0].toInt()
if (port > 65535) port = null
}
if (url.contains("/")) {
path = url.split("/").let { it.subList(1, it.size).joinToString(separator = "/") }
if (host == "") host = url.split("/")[0]
}
if (host == "") host = url
val parsedUrl = url.toHttpUrl()
val host = parsedUrl.host
val port = parsedUrl.port
val path = parsedUrl.pathSegments.takeIf {
it.isNotEmpty() && (it.size > 1 || !it.contains("")) // Non-empty AND contains something other than "" (empty string used when there is no path)
}?.joinToString(separator = "/")
return AbstractHttpsDNSHandle.waitUntilKnownServersArePopulated { allServer ->
if(path != null) emptyList()
else allServer.values.filter {
it.servers.any { server ->
server.address.host == host && (port == null || server.address.port == port)
server.address.host == host && (server.address.port == port)
}
}
}.firstOrNull()?.servers?.firstOrNull {
it.address.host == host
}?.address ?: if (port != null && path != null) HttpsUpstreamAddress(host, port, path)
else if (port != null) HttpsUpstreamAddress(host, port)
else if (path != null) HttpsUpstreamAddress(host, urlPath = path)
else HttpsUpstreamAddress(host)
}
private fun createTlsUpstreamAddress(host: String): TLSUpstreamAddress {
context.log("Creating TLSUpstreamAddress for `$host`")
val parsedHost:String
var port: Int? = null
if (host.contains(":")) {
parsedHost = host.split(":")[0]
port = host.split(":")[1].split("/")[0].toInt()
if (port > 65535) port = null
} else parsedHost = host
return AbstractTLSDnsHandle.waitUntilKnownServersArePopulated { allServer ->
allServer.values.filter {
it.servers.any { server ->
server.address.host == parsedHost && (port == null || server.address.port == port)
}
}
}.firstOrNull()?.servers?.firstOrNull {
it.address.host == parsedHost
}?.address ?: if (port != null) TLSUpstreamAddress(parsedHost, port)
else TLSUpstreamAddress(parsedHost)
}?.address ?: if (path != null) HttpsUpstreamAddress(host, port, path)
else HttpsUpstreamAddress(host, port)
}
private fun addUrlTextWatcher(input: TextInputLayout, editText: TextInputEditText, emptyAllowed: Boolean) {
editText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable) {
val valid =
(emptyAllowed && s.isBlank()) || (dnsOverHttps && SERVER_URL_REGEX.matches(s.toString())) || (!dnsOverHttps && TLS_REGEX.matches(
s.toString()
))
var valid = (emptyAllowed && s.isBlank())
valid = valid || (!s.isBlank() && dnsOverHttps && isValidDoH(s.toString()))
valid = valid || (!s.isBlank() && !dnsOverHttps && isValidDot(s.toString()))
input.error = if (valid) {
null
} else context.getString(R.string.error_invalid_url)
input.error = when {
valid -> {
null
}
dnsOverHttps -> context.getString(R.string.error_invalid_url)
else -> context.getString(R.string.error_invalid_host)
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
......
@file:Suppress("BlockingMethodInNonBlockingContext")
package com.frostnerd.smokescreen.dialog
import android.content.Context
......@@ -13,10 +15,7 @@ import com.frostnerd.smokescreen.log
import com.frostnerd.smokescreen.service.DnsVpnService
import kotlinx.android.synthetic.main.dialog_query_generator.*
import kotlinx.android.synthetic.main.dialog_query_generator.view.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
......@@ -346,6 +345,9 @@ class QueryGeneratorDialog(context: Context):AlertDialog(context, context.getPre
"https://www.bild.de/video/clip/elefant/elefant-haengt-ueber-zaun-viralpress-61819274.bild.html"
)
private var job: Job? = null
private val generatorScope:CoroutineScope by lazy {
CoroutineScope(newSingleThreadContext("query-generator"))
}
private var loadingDialog:AlertDialog? = null
init {
......@@ -355,7 +357,6 @@ class QueryGeneratorDialog(context: Context):AlertDialog(context, context.getPre
val callDomains = view.findViewById<CheckBox>(R.id.baseDomains)
val callDeepurls = view.findViewById<CheckBox>(R.id.deepurls)
val useChrome = view.findViewById<CheckBox>(R.id.useChrome)
val useRandomDelay = view.findViewById<CheckBox>(R.id.randomTimeout)
val delay = view.findViewById<EditText>(R.id.delay)
setView(view)
......@@ -369,7 +370,7 @@ class QueryGeneratorDialog(context: Context):AlertDialog(context, context.getPre
}
val restartVpn = view.restartVpn.isChecked
val runCount = iterations.text.toString().toIntOrNull() ?: 1
job = GlobalScope.launch {
job = generatorScope.launch {
context.log("Generating queries for ${urlsToUse.size} urls $runCount times", "[QueryGenerator]")
val logFileWriter = BufferedWriter(FileWriter(File(context.filesDir, "querygenlog.txt"), true))
val callWithChrome = useChrome.isChecked
......@@ -392,6 +393,7 @@ class QueryGeneratorDialog(context: Context):AlertDialog(context, context.getPre
}
}
job = null
generatorScope.cancel()
loadingDialog?.cancel()
}
showLoadingDialog()
......@@ -410,7 +412,7 @@ class QueryGeneratorDialog(context: Context):AlertDialog(context, context.getPre
}
private fun showLoadingDialog() {
loadingDialog = AlertDialog.Builder(context, context.getPreferences().theme.dialogStyle)
loadingDialog = Builder(context, context.getPreferences().theme.dialogStyle)
.setTitle("Generating queries")
.setCancelable(false)
.setNegativeButton("Stop") { dialog, _ ->
......
......@@ -15,7 +15,7 @@ import com.frostnerd.encrypteddnstunnelproxy.tls.AbstractTLSDnsHandle
import com.frostnerd.lifecyclemanagement.BaseDialog
import com.frostnerd.smokescreen.*
import com.frostnerd.smokescreen.util.preferences.UserServerConfiguration
import kotlinx.android.synthetic.main.dialog_server_configuration.*
import kotlinx.android.synthetic.main.dialog_server_configuration.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
......@@ -46,6 +46,9 @@ class ServerChoosalDialog(
onEntrySelected: (config: DnsServerInformation<*>) -> Unit
) :
BaseDialog(context, context.getPreferences().theme.dialogStyle) {
private var layout:View =
layoutInflater.inflate(R.layout.dialog_server_configuration, null, false)
private var populationJob: Job? = null
private var currentSelectedServer: DnsServerInformation<*>?
private lateinit var defaultConfig: List<DnsServerInformation<*>>
......@@ -55,9 +58,8 @@ class ServerChoosalDialog(
onEntrySelected: (config: DnsServerInformation<*>) -> Unit):this(context, context.getPreferences().dnsServerConfig, onEntrySelected=onEntrySelected)
init {
val view = layoutInflater.inflate(R.layout.dialog_server_configuration, null, false)
setTitle(R.string.dialog_serverconfiguration_title)
setView(view)
setView(layout)
currentSelectedServer = selectedServer
......@@ -71,7 +73,7 @@ class ServerChoosalDialog(
}
loadServerData(showTls)
val spinnerAdapter = ArrayAdapter<String>(
val spinnerAdapter = ArrayAdapter(
context, android.R.layout.simple_spinner_item,
arrayListOf(
context.getString(R.string.dialog_serverconfiguration_https),
......@@ -79,11 +81,11 @@ class ServerChoosalDialog(
)
)
spinnerAdapter.setDropDownViewResource(R.layout.item_tasker_action_spinner_dropdown_item)
val spinner = view.findViewById<Spinner>(R.id.spinner)
val spinner = layout.findViewById<Spinner>(R.id.spinner)
spinner.adapter = spinnerAdapter
if(showTls) spinner.setSelection(1)
view.findViewById<RadioGroup>(R.id.knownServersGroup).setOnCheckedChangeListener { group, _ ->
val button = view.findViewById(group.checkedRadioButtonId) as RadioButton
layout.findViewById<RadioGroup>(R.id.knownServersGroup).setOnCheckedChangeListener { group, _ ->
val button = layout.findViewById(group.checkedRadioButtonId) as RadioButton
val payload = button.tag
currentSelectedServer = if (payload is UserServerConfiguration) {
......@@ -92,14 +94,14 @@ class ServerChoosalDialog(
payload as DnsServerInformation<*>
}
}
view.findViewById<Button>(R.id.addServer).setOnClickListener {
layout.findViewById<Button>(R.id.addServer).setOnClickListener {
NewServerDialog(context, title = null, dnsOverHttps = spinner.selectedItemPosition == 0, server = null, onServerAdded = { info ->
val config = createButtonForUserConfiguration(
context.getPreferences().addUserServerConfiguration(
info
)
)
if (info.hasTlsServer() == defaultConfig.any { it.hasTlsServer() }) knownServersGroup.addView(
if (info.hasTlsServer() == defaultConfig.any { it.hasTlsServer() }) layout.knownServersGroup.addView(
config
)
}).show()
......@@ -109,7 +111,7 @@ class ServerChoosalDialog(
override fun onNothingSelected(parent: AdapterView<*>?) {}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
loadServerData(position == 1)
knownServersGroup.removeAllViews()
layout.knownServersGroup.removeAllViews()
addKnownServers()
}
}
......@@ -166,9 +168,9 @@ class ServerChoosalDialog(
buttons.add(createButtonForUserConfiguration(it))
}
launch(Dispatchers.Main) {
progress.visibility = View.GONE
layout.progress.visibility = View.GONE
for (button in buttons) {
knownServersGroup.addView(button)
layout.knownServersGroup.addView(button)
}
markCurrentSelectedServer()
populationJob = null
......@@ -179,8 +181,8 @@ class ServerChoosalDialog(
private fun markCurrentSelectedServer() {
val currentSelectedServer = this.currentSelectedServer ?: return
for (id in 0 until knownServersGroup.childCount) {
val child = knownServersGroup.getChildAt(id) as RadioButton
for (id in 0 until layout.knownServersGroup.childCount) {
val child = layout.knownServersGroup.getChildAt(id) as RadioButton
val payload = child.tag
val info =
if (payload is UserServerConfiguration) {
......@@ -272,7 +274,7 @@ class ServerChoosalDialog(
button.tag = userConfiguration
button.setOnLongClickListener {
AlertDialog.Builder(context, context.getPreferences().theme.dialogStyle)
Builder(context, context.getPreferences().theme.dialogStyle)
.setTitle(R.string.dialog_editdelete_title)
.setPositiveButton(R.string.dialog_editdelete_edit) { dialog, _ ->
showUserConfigEditDialog(userConfiguration, button)
......@@ -288,7 +290,7 @@ class ServerChoosalDialog(
}
private fun showUserConfigDeleteDialog(userConfiguration: UserServerConfiguration, button: RadioButton) {
AlertDialog.Builder(context, context.getPreferences().theme.dialogStyle)
Builder(context, context.getPreferences().theme.dialogStyle)
.setTitle(R.string.dialog_deleteconfig_title)
.setMessage(
context.getString(
......@@ -306,7 +308,7 @@ class ServerChoosalDialog(
markCurrentSelectedServer()
context.getPreferences().dnsServerConfig = currentSelectedServer!!
}
knownServersGroup.removeView(button)
layout.knownServersGroup.removeView(button)
}.show()
}
......@@ -321,14 +323,14 @@ class ServerChoosalDialog(
currentSelectedServer = newConfig.serverInformation
context.getPreferences().dnsServerConfig = newConfig.serverInformation
}
loadServerData(spinner.selectedItemPosition == 1)
knownServersGroup.removeAllViews()
loadServerData(layout.spinner.selectedItemPosition == 1)
layout.knownServersGroup.removeAllViews()
addKnownServers()
}).show()
}
private fun showDefaultConfigDeleteDialog(config:DnsServerInformation<*>, button: RadioButton) {
AlertDialog.Builder(context, context.getPreferences().theme.dialogStyle)
Builder(context, context.getPreferences().theme.dialogStyle)
.setTitle(R.string.dialog_deleteconfig_title)
.setMessage(
context.getString(
......@@ -338,7 +340,7 @@ class ServerChoosalDialog(
)
.setNegativeButton(R.string.all_no) { _, _ -> }
.setPositiveButton(R.string.all_yes) { _, _ ->
val isHttps = spinner.selectedItemPosition == 0
val isHttps = layout.spinner.selectedItemPosition == 0