From d1866b7bb1d26667e89e5b0364a30b4dea1d3f96 Mon Sep 17 00:00:00 2001 From: Ivan Pizhenko Date: Tue, 3 Aug 2021 11:19:36 +0300 Subject: [PATCH 1/2] issue #1378 WKD client timeout test --- FlowCrypt/build.gradle | 1 + .../com/flowcrypt/email/api/wkd/WkdClient.kt | 31 ++++++++++++------- .../email/extensions/kotlin/StringExt.kt | 4 +++ .../email/util/BetterEmailAddress.kt | 5 +++ .../email/api/email/WkdClientTest.kt | 27 ++++++++++++++++ 5 files changed, 57 insertions(+), 11 deletions(-) diff --git a/FlowCrypt/build.gradle b/FlowCrypt/build.gradle index 5fe473b18d..68da2b0605 100644 --- a/FlowCrypt/build.gradle +++ b/FlowCrypt/build.gradle @@ -386,6 +386,7 @@ dependencies { androidTestImplementation "com.squareup.okhttp3:okhttp-tls:${rootProject.ext.okhttpVersion}" androidTestUtil 'androidx.test:orchestrator:1.4.0' + testImplementation "com.squareup.okhttp3:mockwebserver:${rootProject.ext.okhttpVersion}" testImplementation "junit:junit:${rootProject.ext.junitVersion}" testImplementation "androidx.room:room-testing:$roomVersion" testImplementation 'org.robolectric:robolectric:4.6.1' diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/wkd/WkdClient.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/wkd/WkdClient.kt index 2426ee5066..e3753453dc 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/wkd/WkdClient.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/wkd/WkdClient.kt @@ -7,6 +7,7 @@ package com.flowcrypt.email.api.wkd import com.flowcrypt.email.extensions.kotlin.isValidEmail +import com.flowcrypt.email.extensions.kotlin.isValidLocalhostEmail import com.flowcrypt.email.util.BetterInternetAddress import okhttp3.OkHttpClient import okhttp3.Request @@ -14,20 +15,22 @@ import org.apache.commons.codec.binary.ZBase32 import org.apache.commons.codec.digest.DigestUtils import org.bouncycastle.openpgp.PGPPublicKeyRingCollection import org.bouncycastle.openpgp.jcajce.JcaPGPPublicKeyRingCollection +import java.io.InterruptedIOException import java.net.URLEncoder import java.net.UnknownHostException import java.util.Locale import java.util.concurrent.TimeUnit object WkdClient { - private const val DEFAULT_REQUEST_TIMEOUT = 4 + const val DEFAULT_REQUEST_TIMEOUT = 4L fun lookupEmail( email: String, - timeout: Int = DEFAULT_REQUEST_TIMEOUT, - wkdPort: Int? = null + wkdPort: Int? = null, + useHttps: Boolean = true, + timeout: Long = DEFAULT_REQUEST_TIMEOUT ): PGPPublicKeyRingCollection? { - val keys = rawLookupEmail(email, timeout, wkdPort) ?: return null + val keys = rawLookupEmail(email, wkdPort, useHttps, timeout) ?: return null val lowerCaseEmail = email.toLowerCase(Locale.ROOT) val matchingKeys = keys.keyRings.asSequence().filter { for (userId in it.publicKey.userIDs) { @@ -46,10 +49,13 @@ object WkdClient { @Suppress("private") fun rawLookupEmail( email: String, - timeout: Int = DEFAULT_REQUEST_TIMEOUT, - wkdPort: Int? = null + wkdPort: Int? = null, + useHttps: Boolean = true, + timeout: Long = DEFAULT_REQUEST_TIMEOUT ): PGPPublicKeyRingCollection? { - if (!email.isValidEmail()) throw IllegalArgumentException("Invalid email address") + if (!email.isValidEmail() && !email.isValidLocalhostEmail()) { + throw IllegalArgumentException("Invalid email address") + } val parts = email.split('@') val user = parts[0].toLowerCase(Locale.ROOT) val hu = ZBase32().encodeAsString(DigestUtils.sha1(user.toByteArray())) @@ -57,8 +63,9 @@ object WkdClient { val advancedDomainPrefix = if (directDomain == "localhost") "" else "openpgpkey." val directHost = if (wkdPort == null) directDomain else "${directDomain}:${wkdPort}" val advancedHost = "$advancedDomainPrefix$directHost" - val advancedUrl = "https://${advancedHost}/.well-known/openpgpkey/${directDomain}" - val directUrl = "https://${directHost}/.well-known/openpgpkey" + val protocol = if (useHttps) "https" else "http" + val advancedUrl = "$protocol://${advancedHost}/.well-known/openpgpkey/${directDomain}" + val directUrl = "$protocol://${directHost}/.well-known/openpgpkey" val userPart = "hu/$hu?l=${URLEncoder.encode(user, "UTF-8")}" try { val result = urlLookup(advancedUrl, userPart, timeout) @@ -71,6 +78,8 @@ object WkdClient { urlLookup(directUrl, userPart, timeout).keys } catch (ex: UnknownHostException) { null + } catch (ex: InterruptedIOException) { + if (ex.message == "timeout") null else throw ex } } @@ -79,8 +88,8 @@ object WkdClient { val keys: PGPPublicKeyRingCollection? = null ) - private fun urlLookup(methodUrlBase: String, userPart: String, timeout: Int): UrlLookupResult { - val httpClient = OkHttpClient.Builder().callTimeout(timeout.toLong(), TimeUnit.SECONDS).build() + private fun urlLookup(methodUrlBase: String, userPart: String, timeout: Long): UrlLookupResult { + val httpClient = OkHttpClient.Builder().callTimeout(timeout, TimeUnit.SECONDS).build() val policyRequest = Request.Builder().url("$methodUrlBase/policy").build() httpClient.newCall(policyRequest).execute().use { policyResponse -> if (policyResponse.code != 200) return UrlLookupResult() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/kotlin/StringExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/kotlin/StringExt.kt index f8a53d3655..e4967f145e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/kotlin/StringExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/kotlin/StringExt.kt @@ -162,3 +162,7 @@ fun String.stripTrailing(ch: Char): String { fun String.isValidEmail(): Boolean { return BetterInternetAddress.isValidEmail(this) } + +fun String.isValidLocalhostEmail(): Boolean { + return BetterInternetAddress.isValidLocalhostEmail(this) +} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/util/BetterEmailAddress.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/util/BetterEmailAddress.kt index e24e153229..83264ee9ee 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/util/BetterEmailAddress.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/util/BetterEmailAddress.kt @@ -29,11 +29,16 @@ class BetterInternetAddress(str: String, verifySpecialCharacters: Boolean = true private val containsSpecialCharacterRegex = ".*[()<>\\[\\]:;@\\\\,.\"].*".toRegex() // double quotes at ends only private val doubleQuotedTextRegex = "\"[^\"]*\"".toRegex() + private val validLocalhostEmailRegex = Regex("([a-zA-z])([a-zA-z0-9])+@localhost") fun isValidEmail(email: String): Boolean { return validEmailRegex.matchEntire(email) != null } + fun isValidLocalhostEmail(email: String): Boolean { + return validLocalhostEmailRegex.matchEntire(email) != null + } + fun areValidEmails(emails: Iterable): Boolean { return emails.all { it.isValidEmail() } } diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/WkdClientTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/WkdClientTest.kt index 79bd41c3f0..6b05f35b43 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/WkdClientTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/WkdClientTest.kt @@ -7,8 +7,16 @@ package com.flowcrypt.email.api.email import com.flowcrypt.email.api.wkd.WkdClient +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest import org.junit.Assert.assertTrue import org.junit.Test +import java.net.ServerSocket +import java.net.Socket +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.concurrent.thread class WkdClientTest { @Test @@ -35,4 +43,23 @@ class WkdClientTest { val keys = WkdClient.lookupEmail("doesnotexist@thisdomaindoesnotexist.test") assertTrue("Key found for non-existing email", keys == null) } + + @Test + fun requestTimeoutTest() { + val mockWebServer = MockWebServer() + mockWebServer.dispatcher = object: Dispatcher() { + private val sleepTimeout = (WkdClient.DEFAULT_REQUEST_TIMEOUT + 2) * 1000 + override fun dispatch(request: RecordedRequest): MockResponse { + Thread.sleep(sleepTimeout) + return MockResponse().setResponseCode(200) + } + } + mockWebServer.start() + val port = mockWebServer.port + mockWebServer.use { + val keys = WkdClient.lookupEmail(email = "user@localhost", wkdPort = port, useHttps = false) + assertTrue("Key found for non-existing email", keys == null) + } + } } + From ab28bee849a34b7bc7f9f0765d33ba238b5d1dbf Mon Sep 17 00:00:00 2001 From: Ivan Pizhenko Date: Tue, 3 Aug 2021 11:22:35 +0300 Subject: [PATCH 2/2] Removed unused imports --- .../test/java/com/flowcrypt/email/api/email/WkdClientTest.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/WkdClientTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/WkdClientTest.kt index 6b05f35b43..6dc7056410 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/WkdClientTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/WkdClientTest.kt @@ -13,10 +13,6 @@ import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.RecordedRequest import org.junit.Assert.assertTrue import org.junit.Test -import java.net.ServerSocket -import java.net.Socket -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.concurrent.thread class WkdClientTest { @Test