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
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ internal class DnsOverHttpsCall(
when (resourceRecord) {
is ResourceRecord.Https -> {
Dns.Record.ServiceMetadata(
hostname = request.hostname,
hostname = resourceRecord.targetName.takeIf { it != "" } ?: request.hostname,
alpnIds =
resourceRecord.alpnIds?.mapNotNull { alpnId ->
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ import okhttp3.dnsoverhttps.internal.Question
import okhttp3.dnsoverhttps.internal.ResourceRecord
import okhttp3.dnsoverhttps.internal.TYPE_A
import okhttp3.dnsoverhttps.internal.TYPE_AAAA
import okhttp3.internal.dns.DnsEvent
import okhttp3.internal.dns.EntryPoint
import okhttp3.internal.dns.invoke
import okhttp3.internal.dns.toEventsQueue
import okhttp3.testing.PlatformRule
import okio.Buffer
import okio.ByteString.Companion.decodeHex
Expand Down Expand Up @@ -343,6 +347,7 @@ class DnsOverHttpsTest(
ResourceRecord.Https(
name = "lysine.dev",
timeToLive = 5,
targetName = "cdn.lysine.dev",
alpnIds = listOf(Protocol.HTTP_2.toString()),
port = 8843,
ipAddressHints =
Expand All @@ -355,15 +360,15 @@ class DnsOverHttpsTest(
)

val call = dns.newCall(Dns.Request("lysine.dev"))
val dnsEvents = call.execute()
val dnsEvents = call.toEventsQueue()

assertThat(dnsEvents.take()).isEqualTo(
DnsEvent.Records(
last = false,
records =
listOf(
Dns.Record.ServiceMetadata(
hostname = "lysine.dev",
hostname = "cdn.lysine.dev",
alpnIds = listOf(Protocol.HTTP_2),
port = 8843,
ipAddressHints =
Expand Down Expand Up @@ -402,6 +407,55 @@ class DnsOverHttpsTest(
)
}

@Test
fun serviceMetadataEmptyTargetNameAliasesToRequestHostname() {
assumeTrue(entryPoint == EntryPoint.NewCall)

dns = buildLocalhost(bootstrapClient, includeIPv6 = true, includeHttps = true)
server["lysine.dev"] =
listOf(
ResourceRecord.IpAddress(
name = "lysine.dev",
timeToLive = 5,
address = InetAddress.getByName("10.20.30.40"),
),
ResourceRecord.Https(
name = "lysine.dev",
timeToLive = 5,
targetName = "",
alpnIds = listOf(Protocol.HTTP_2.toString()),
),
)

val call = dns.newCall(Dns.Request("lysine.dev"))
val dnsEvents = call.toEventsQueue()

assertThat(dnsEvents.take()).isEqualTo(
DnsEvent.Records(
last = false,
records =
listOf(
Dns.Record.ServiceMetadata(
hostname = "lysine.dev",
alpnIds = listOf(Protocol.HTTP_2),
),
),
),
)
assertThat(dnsEvents.take()).isEqualTo(
DnsEvent.Records(
last = true,
records =
listOf(
Dns.Record.IpAddress(
hostname = "lysine.dev",
address = InetAddress.getByName("10.20.30.40"),
),
),
),
)
}

/** An HTTPS error is received before the IPv4 results, but the error is delivered last. */
@Test
fun httpsFailureIsDeliveredAfterIpv6AndIpv4Records() {
Expand All @@ -426,7 +480,7 @@ class DnsOverHttpsTest(
)

val call = dns.newCall(Dns.Request("lysine.dev"))
val dnsEvents = call.execute()
val dnsEvents = call.toEventsQueue()

assertThat(dnsEvents.take()).isEqualTo(
DnsEvent.Records(
Expand Down Expand Up @@ -474,7 +528,7 @@ class DnsOverHttpsTest(
)

val call = dns.newCall(Dns.Request("lysine.dev"))
val dnsEvents = call.execute()
val dnsEvents = call.toEventsQueue()

assertThat(dnsEvents.take()).isEqualTo(
DnsEvent.Records(
Expand Down Expand Up @@ -507,7 +561,7 @@ class DnsOverHttpsTest(
)

val call = dns.newCall(Dns.Request("lysine.dev"))
val dnsEvents = call.execute()
val dnsEvents = call.toEventsQueue()

assertThat(dnsEvents.take()).isEqualTo(
DnsEvent.Records(
Expand All @@ -530,7 +584,7 @@ class DnsOverHttpsTest(
dns = buildLocalhost(bootstrapClient, includeIPv6 = true, includeHttps = true)

val call = dns.newCall(Dns.Request("lysine.dev"))
val dnsEvents = call.execute()
val dnsEvents = call.toEventsQueue()

assertThat(dnsEvents.take()).isEqualTo(
DnsEvent.Records(
Expand All @@ -549,7 +603,7 @@ class DnsOverHttpsTest(
val call = dns.newCall(Dns.Request("lysine.dev"))
call.cancel()
assertThat(call.isCanceled()).isTrue()
val dnsEvents = call.execute()
val dnsEvents = call.toEventsQueue()

assertThat(dnsEvents.take()).isInstanceOf<DnsEvent.Failure>()
}
Expand All @@ -568,7 +622,7 @@ class DnsOverHttpsTest(
chain.proceed(chain.request())
}

val dnsEvents = call.execute()
val dnsEvents = call.toEventsQueue()
assertThat(dnsEvents.take()).isInstanceOf<DnsEvent.Failure>()
}

Expand Down Expand Up @@ -601,7 +655,7 @@ class DnsOverHttpsTest(
}
}

val dnsEvents = call.execute()
val dnsEvents = call.toEventsQueue()
assertThat(dnsEvents.take()).isEqualTo(
DnsEvent.Records(
last = false,
Expand Down
106 changes: 105 additions & 1 deletion okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package okhttp3

import assertk.assertThat
import assertk.assertions.containsExactly
import java.io.IOException
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
Expand All @@ -36,13 +37,16 @@ import okhttp3.dnsoverhttps.internal.ResourceRecord
import okhttp3.dnsoverhttps.internal.TYPE_A
import okhttp3.dnsoverhttps.internal.TYPE_AAAA
import okhttp3.dnsoverhttps.internal.TYPE_HTTPS
import okhttp3.internal.concurrent.TaskRunner
import okio.Buffer
import okio.ByteString.Companion.decodeBase64

/**
* Handles DNS calls using in-memory records.
*/
class FakeDns : Dns {
class FakeDns(
private val taskRunner: TaskRunner = TaskRunner.INSTANCE,
) : Dns {
private val data = ConcurrentHashMap<String, List<ResourceRecord>>()

var extraHeaders: Headers = Headers.headersOf()
Expand Down Expand Up @@ -112,6 +116,8 @@ class FakeDns : Dns {
)
}

override fun newCall(request: Dns.Request): Dns.Call = FakeDnsCall(request)

@Throws(UnknownHostException::class)
override fun lookup(hostname: String): List<InetAddress> {
requests.put(Request.FunctionCall(hostname))
Expand Down Expand Up @@ -216,6 +222,104 @@ class FakeDns : Dns {
}
}

/**
* Deliver each kind of DNS record in a separate callback, to most accurately simulate what a real
* DNS server does.
*/
private inner class FakeDnsCall(
override val request: Dns.Request,
) : Dns.Call {
@Volatile private var canceled = false
private var executed = false
private val taskQueue = taskRunner.newQueue()

override fun cancel() {
canceled = true
}

override fun isCanceled() = canceled

override fun enqueue(callback: Dns.Callback) {
check(!executed)
executed = true

requests.put(Request.FunctionCall(request.hostname))

if (canceled) {
taskQueue.execute("${request.hostname} dns") {
callback.onFailure(
call = this,
e = IOException("canceled"),
)
}
return
}

val resourceRecords = data[request.hostname] ?: listOf()
if (resourceRecords.isEmpty()) {
taskQueue.execute("${request.hostname} dns") {
callback.onRecords(
call = this,
last = true,
records = listOf(),
)
}
return
}

val serviceMetadataRecords = mutableListOf<Dns.Record.ServiceMetadata>()
val ipv6Records = mutableListOf<Dns.Record.IpAddress>()
val ipv4Records = mutableListOf<Dns.Record.IpAddress>()
for (resourceRecord in resourceRecords) {
when (resourceRecord) {
is ResourceRecord.Https -> {
serviceMetadataRecords +=
Dns.Record.ServiceMetadata(
hostname = resourceRecord.targetName.takeIf { it != "" } ?: request.hostname,
port = resourceRecord.port,
alpnIds = resourceRecord.alpnIds?.map { Protocol.get(it) },
ipAddressHints = resourceRecord.ipAddressHints,
echConfigList = resourceRecord.echConfigList,
)
}

is ResourceRecord.IpAddress -> {
val ipAddressRecord = Dns.Record.IpAddress(request.hostname, resourceRecord.address)
when (resourceRecord.address) {
is Inet4Address -> ipv4Records += ipAddressRecord
is Inet6Address -> ipv6Records += ipAddressRecord
else -> error("unexpected address")
}
}
}
}

taskQueue.execute("${request.hostname} dns") {
if (serviceMetadataRecords.isNotEmpty()) {
callback.onRecords(
call = this,
last = ipv6Records.isEmpty() && ipv4Records.isEmpty(),
records = serviceMetadataRecords,
)
}
if (ipv6Records.isNotEmpty()) {
callback.onRecords(
call = this,
last = ipv4Records.isEmpty(),
records = ipv6Records,
)
}
if (ipv4Records.isNotEmpty()) {
callback.onRecords(
call = this,
last = true,
records = ipv4Records,
)
}
}
}
}

sealed interface Request {
val hostname: String

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package okhttp3.dnsoverhttps
package okhttp3.internal.dns

import java.net.InetAddress
import java.net.UnknownHostException
import java.util.concurrent.BlockingDeque
import java.util.concurrent.BlockingQueue
import java.util.concurrent.LinkedBlockingDeque
import okhttp3.Dns
import okhttp3.dnsoverhttps.DnsOverHttps
import okhttp3.internal.concurrent.TaskRunner
import okio.IOException

sealed interface DnsEvent {
Expand All @@ -33,7 +35,7 @@ sealed interface DnsEvent {
) : DnsEvent
}

internal fun Dns.Call.execute(): BlockingDeque<DnsEvent> {
fun Dns.Call.toEventsQueue(): BlockingQueue<DnsEvent> {
val result = LinkedBlockingDeque<DnsEvent>()

enqueue(
Expand Down Expand Up @@ -73,7 +75,7 @@ operator fun DnsOverHttps.invoke(

EntryPoint.NewCall -> {
buildList {
val dnsEvents = newCall(Dns.Request(hostname)).execute()
val dnsEvents = newCall(Dns.Request(hostname)).toEventsQueue()
while (true) {
when (val dnsEvent = dnsEvents.take()) {
is DnsEvent.Failure -> {
Expand All @@ -98,6 +100,33 @@ operator fun DnsOverHttps.invoke(
}
}

/**
* Force this instance to use the lookup API or ([LookupDnsCall]), or the call API (and definitely
* not [LookupDnsCall]). This is for tests that want to defeat OkHttp's implementation detection,
* so we can exercise all code paths.
*/
fun Dns.forceEntryPoint(entryPoint: EntryPoint): Dns {
return when (entryPoint) {
EntryPoint.Lookup -> {
object : Dns by this {
override fun newCall(request: Dns.Request) = LookupDnsCall(TaskRunner.INSTANCE, this, request)
}
}

EntryPoint.NewCall -> {
object : Dns by this {
override fun newCall(request: Dns.Request): Dns.Call {
val result = this@forceEntryPoint.newCall(request)
check(result !is LookupDnsCall) {
"unexpected call implementation on ${this@forceEntryPoint}"
}
return result
}
}
}
}
}

enum class EntryPoint {
Lookup,
NewCall,
Expand Down
2 changes: 1 addition & 1 deletion okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dns.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
*/
package okhttp3

import java.io.IOException
import java.net.InetAddress
import java.net.UnknownHostException
import okhttp3.internal.concurrent.TaskRunner
import okhttp3.internal.dns.LookupDnsCall
import okhttp3.internal.toCanonicalHost
import okio.ByteString
import okio.IOException

/**
* Loads IP addresses and service metadata for a hostname.
Expand Down
Loading
Loading