Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions FlowCrypt/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
31 changes: 20 additions & 11 deletions FlowCrypt/src/main/java/com/flowcrypt/email/api/wkd/WkdClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,30 @@
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
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) {
Expand All @@ -46,19 +49,23 @@ 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()))
val directDomain = parts[1].toLowerCase(Locale.ROOT)
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)
Expand All @@ -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
}
}

Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>): Boolean {
return emails.all { it.isValidEmail() }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
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

Expand Down Expand Up @@ -35,4 +39,23 @@ class WkdClientTest {
val keys = WkdClient.lookupEmail("doesnotexist@thisdomaindoesnotexist.test")
assertTrue("Key found for non-existing email", keys == null)
}

@Test
fun requestTimeoutTest() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FlowCryptMockWebServerRule is not available here since this is not "Android Test". However I've looked at it and it gives me idea to use okhttp MockWebServer.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant that it would be better to use a mock web server instead of sockets. It will be clearer.

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)
}
}
}