diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsMessage.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessage.kt similarity index 74% rename from okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsMessage.kt rename to okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessage.kt index 9530c89643c1..11a36272efa9 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsMessage.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessage.kt @@ -13,16 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:Suppress("ktlint:standard:filename") + package okhttp3.dnsoverhttps import java.net.InetAddress +import okhttp3.RequestBody +import okhttp3.dnsoverhttps.DnsOverHttps.Companion.DNS_MESSAGE +import okio.Buffer +import okio.BufferedSink import okio.ByteString internal data class DnsMessage( val id: Short, val flags: Int, val questions: List, - val answers: List, + val answers: List = listOf(), val authorityRecords: List = listOf(), val additionalRecords: List = listOf(), ) { @@ -40,12 +46,36 @@ internal data class DnsMessage( result = 31 * result + additionalRecords.hashCode() return result } + + companion object { + fun query( + hostname: String, + type: Int, + ): DnsMessage { + // QR = 0 (Query) + // RD = 1 (Recursion Desired) + // OPCODE = 0 (standard query) + // QR OPCODE AA TC RD RA Z RCODE + val flags = 0b0___0000__0__0__1__0_000__0000 + return DnsMessage( + id = 0, + flags = flags, + questions = + listOf( + Question( + name = hostname, + type = type, + ), + ), + ) + } + } } internal data class Question( val name: String, - val type: Short, - val `class`: Short, + val type: Int, + val `class`: Int = CLASS_IN, ) { // Avoid Short.hashCode(short) which isn't available on Android 5. override fun hashCode(): Int { @@ -82,9 +112,9 @@ internal sealed interface ResourceRecord { val priority: Int, val targetName: String, val alpnIds: List? = null, - var port: Int = -1, + var port: Int = 443, val ipAddressHints: List = listOf(), - var echConfigList: ByteString? = null, + val echConfigList: ByteString? = null, ) : ResourceRecord { // Avoid Int.hashCode(int) which isn't available on Android 5. override fun hashCode(): Int { @@ -122,3 +152,20 @@ internal const val SERVICE_PARAMETER_PORT = 3 internal const val SERVICE_PARAMETER_IPV4_HINT = 4 internal const val SERVICE_PARAMETER_ECH = 5 internal const val SERVICE_PARAMETER_IPV6_HINT = 6 + +internal fun DnsMessage.asQueryParameter(): String { + val buffer = Buffer() + DnsMessageWriter(buffer).write(this@asQueryParameter) + return buffer.readByteString().base64Url().replace("=", "") +} + +internal class QueryRequestBody( + private val query: DnsMessage, +) : RequestBody() { + override fun contentType() = DNS_MESSAGE + + override fun writeTo(sink: BufferedSink) { + DnsMessageWriter(sink.buffer).write(query) + sink.emitCompleteSegments() + } +} diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt index 987b9c4071e2..e93b2a4f06a3 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageReader.kt @@ -90,8 +90,8 @@ internal class DnsMessageReader( private fun BufferedSource.readQuestion(): Question { val name = readName() - val type = readShort() - val `class` = readShort() + val type = readShort().toInt() + val `class` = readShort().toInt() return Question( name = name, type = type, @@ -186,7 +186,7 @@ internal class DnsMessageReader( var lastKey = -1 var alpnIds: MutableList? = null var noDefaultAlpn = false - var port = -1 + var port = 443 var ipAddressHints: MutableList? = null var echConfigList: ByteString? = null diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageWriter.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageWriter.kt new file mode 100644 index 000000000000..3dc248f7bf5a --- /dev/null +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/-DnsMessageWriter.kt @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("ktlint:standard:filename") + +package okhttp3.dnsoverhttps + +import java.net.Inet4Address +import java.net.Inet6Address +import okhttp3.Protocol +import okio.Buffer +import okio.ByteString +import okio.ByteString.Companion.encodeUtf8 +import okio.utf8Size + +/** + * Encode DNS messages. + * + * https://datatracker.ietf.org/doc/html/rfc1035 + */ +internal class DnsMessageWriter( + private val sink: Buffer, +) { + /** + * When compressing name labels, this is the offset into [sink] of the label. + * + * Because we write [ResourceRecord.Https] values to an intermediate buffer, the overall offset + * must be predicted when putting in this map. + */ + private val labelToOffset = mutableMapOf() + private val messageStart = sink.size + + fun write(message: DnsMessage) { + sink.writeShort(message.id.toInt()) + sink.writeShort(message.flags) + sink.writeShort(message.questions.size) + sink.writeShort(message.answers.size) + sink.writeShort(message.authorityRecords.size) + sink.writeShort(message.additionalRecords.size) + + for (question in message.questions) { + writeQuestion(question) + } + for (resourceRecord in message.answers) { + writeResourceRecord(resourceRecord) + } + for (resourceRecord in message.authorityRecords) { + writeResourceRecord(resourceRecord) + } + for (resourceRecord in message.additionalRecords) { + writeResourceRecord(resourceRecord) + } + } + + private fun writeQuestion(question: Question) { + sink.writeName(question.name.encodeUtf8()) + sink.writeShort(question.type.toInt()) + sink.writeShort(question.`class`.toInt()) + } + + private tailrec fun Buffer.writeName( + name: ByteString, + receiverOffset: Long = 0L, + ) { + // Write an empty name. + if (name.size == 0) { + writeByte(0) + return + } + + // Write a back reference to a previous name. + val compressedAt = labelToOffset[name] + if (compressedAt != null) { + writeShort(compressedAt.toInt() or 0b1100_0000_0000_0000) + return + } + + // We're about to write a new name. Remember its offset for future compression. + labelToOffset[name] = receiverOffset + size - messageStart + + // This is the last label. Write it followed by an empty string. + val dot = name.indexOf(LABEL_SEPARATOR) + if (dot == -1) { + check(name.size < 64) { "label too long" } + writeByte(name.size) + write(name) + writeByte(0) + return + } + + // Write one label, then recurse. + check(dot < 64) { "label too long" } + writeByte(dot) + write(name, offset = 0, byteCount = dot) + writeName(name.substring(dot + 1), receiverOffset) + } + + private fun writeResourceRecord(resourceRecord: ResourceRecord) { + when (resourceRecord) { + is ResourceRecord.Https -> writeHttps(resourceRecord) + is ResourceRecord.IpAddress -> writeIpAddress(resourceRecord) + } + } + + private fun writeIpAddress(record: ResourceRecord.IpAddress) { + val address = record.address.address + sink.writeName(record.name.encodeUtf8()) + sink.writeShort( + when (address.size) { + 4 -> TYPE_A + 16 -> TYPE_AAAA + else -> error("unexpected address") + }, + ) + sink.writeShort(CLASS_IN) + sink.writeInt(record.timeToLive) + sink.writeShort(address.size) + sink.write(address) + } + + /** https://datatracker.ietf.org/doc/rfc9460/ */ + private fun writeHttps(record: ResourceRecord.Https) { + sink.writeName(record.name.encodeUtf8()) + sink.writeShort(TYPE_HTTPS) + sink.writeShort(CLASS_IN) + sink.writeInt(record.timeToLive) + + val content = Buffer() + content.writeShort(record.priority) + content.writeName( + name = record.targetName.encodeUtf8(), + receiverOffset = sink.size + 2, + ) + + if (record.alpnIds != null) { + content.writeShort(SERVICE_PARAMETER_ALPN) + val nonDefaultAlpnIds = record.alpnIds.filter { it != Protocol.HTTP_1_1.toString() } + content.writeShort(nonDefaultAlpnIds.sumOf { 1 + it.utf8Size() }.toInt()) + for (alpnId in nonDefaultAlpnIds) { + content.writeByte(alpnId.utf8Size().toInt()) + content.writeUtf8(alpnId) + } + if (Protocol.HTTP_1_1.toString() !in record.alpnIds) { + content.writeShort(SERVICE_PARAMETER_NO_DEFAULT_ALPN) + content.writeShort(0) + } + } + + if (record.port != 443) { + content.writeShort(SERVICE_PARAMETER_PORT) + content.writeShort(2) + content.writeShort(record.port) + } + + val ipv4Hints = record.ipAddressHints.filterIsInstance() + if (ipv4Hints.isNotEmpty()) { + content.writeShort(SERVICE_PARAMETER_IPV4_HINT) + content.writeShort(ipv4Hints.size * 4) + for (address in ipv4Hints) { + content.write(address.address) + } + } + + // Note that ECH (5) sits between IPV4 (4) hints and IPv6 (6) hints because parameters must be + // encoded in strict order, and somebody thought it was cute to use 4 for IPv4 and 6 for IPv6. + if (record.echConfigList != null) { + content.writeShort(SERVICE_PARAMETER_ECH) + content.writeShort(record.echConfigList.size) + content.write(record.echConfigList) + } + + val ipv6Hints = record.ipAddressHints.filterIsInstance() + if (ipv6Hints.isNotEmpty()) { + content.writeShort(SERVICE_PARAMETER_IPV6_HINT) + content.writeShort(ipv6Hints.size * 16) + for (address in ipv6Hints) { + content.write(address.address) + } + } + + check(content.size < UShort.MAX_VALUE.toLong()) { "record too long" } + sink.writeShort(content.size.toInt()) + sink.writeAll(content) + } +} + +private val LABEL_SEPARATOR = ".".encodeUtf8() diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt index 36459e9fcef6..634653f65983 100644 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt +++ b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsOverHttps.kt @@ -29,7 +29,6 @@ import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.Response import okhttp3.internal.platform.Platform import okhttp3.internal.publicsuffix.PublicSuffixDatabase @@ -74,14 +73,14 @@ class DnsOverHttps internal constructor( private fun lookupHttps(hostname: String): List { val networkRequests = buildList { - add(client.newCall(buildRequest(hostname, DnsRecordCodec.TYPE_A))) + add(client.newCall(buildRequest(hostname, TYPE_A))) if (includeHttps) { add(client.newCall(buildRequest(hostname, TYPE_HTTPS))) } if (includeIPv6) { - add(client.newCall(buildRequest(hostname, DnsRecordCodec.TYPE_AAAA))) + add(client.newCall(buildRequest(hostname, TYPE_AAAA))) } } @@ -223,8 +222,6 @@ class DnsOverHttps internal constructor( .Builder() .header("Accept", DNS_MESSAGE.toString()) .apply { - val query = DnsRecordCodec.encodeQuery(hostname, type) - val dnsUrl: HttpUrl = this@DnsOverHttps.url if (post) { url(dnsUrl) @@ -233,11 +230,10 @@ class DnsOverHttps internal constructor( .newBuilder() .addQueryParameter("hostname", hostname) .build(), - ).post(query.toRequestBody(DNS_MESSAGE)) + ).post(QueryRequestBody(DnsMessage.query(hostname, type))) } else { - val encoded = query.base64Url().replace("=", "") - val requestUrl = dnsUrl.newBuilder().addQueryParameter("dns", encoded).build() - + val queryParameter = DnsMessage.query(hostname, type).asQueryParameter() + val requestUrl = dnsUrl.newBuilder().addQueryParameter("dns", queryParameter).build() url(requestUrl) } }.build() diff --git a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsRecordCodec.kt b/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsRecordCodec.kt deleted file mode 100644 index 0eeeef3782fc..000000000000 --- a/okhttp-dnsoverhttps/src/main/kotlin/okhttp3/dnsoverhttps/DnsRecordCodec.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright 2016 The Netty Project - * - * The Netty Project licenses this file to you under the Apache License, version 2.0 (the - * "License"); you may not use this file except in compliance with the License. You may obtain a - * copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License - * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express - * or implied. See the License for the specific language governing permissions and limitations under - * the License. - */ - -package okhttp3.dnsoverhttps - -import java.io.EOFException -import java.net.InetAddress -import java.net.UnknownHostException -import okio.Buffer -import okio.ByteString -import okio.utf8Size - -/** - * Trivial Dns Encoder/Decoder, basically ripped from Netty full implementation. - */ -internal object DnsRecordCodec { - private const val SERVFAIL = 2 - private const val NXDOMAIN = 3 - const val TYPE_A = 0x0001 - const val TYPE_AAAA = 0x001c - private const val TYPE_PTR = 0x000c - private val ASCII = Charsets.US_ASCII - - fun encodeQuery( - host: String, - type: Int, - ): ByteString = - Buffer() - .apply { - writeShort(0) // query id - writeShort(256) // flags with recursion - writeShort(1) // question count - writeShort(0) // answerCount - writeShort(0) // authorityResourceCount - writeShort(0) // additional - - val nameBuf = Buffer() - val labels = host.split('.').dropLastWhile { it.isEmpty() } - for (label in labels) { - val utf8ByteCount = label.utf8Size() - require(utf8ByteCount == label.length.toLong()) { "non-ascii hostname: $host" } - nameBuf.writeByte(utf8ByteCount.toInt()) - nameBuf.writeUtf8(label) - } - nameBuf.writeByte(0) // end - - nameBuf.copyTo(this, 0, nameBuf.size) - writeShort(type) - writeShort(1) // CLASS_IN - }.readByteString() - - @Throws(Exception::class) - fun decodeAnswers( - hostname: String, - byteString: ByteString, - ): List { - val result = mutableListOf() - - val buf = Buffer() - buf.write(byteString) - buf.readShort() // query id - - val flags = buf.readShort().toInt() and 0xffff - require(flags shr 15 != 0) { "not a response" } - - val responseCode = flags and 0xf - - if (responseCode == NXDOMAIN) { - throw UnknownHostException("$hostname: NXDOMAIN") - } else if (responseCode == SERVFAIL) { - throw UnknownHostException("$hostname: SERVFAIL") - } - - val questionCount = buf.readShort().toInt() and 0xffff - val answerCount = buf.readShort().toInt() and 0xffff - buf.readShort() // authority record count - buf.readShort() // additional record count - - for (i in 0 until questionCount) { - skipName(buf) // name - buf.readShort() // type - buf.readShort() // class - } - - for (i in 0 until answerCount) { - skipName(buf) // name - - val type = buf.readShort().toInt() and 0xffff - buf.readShort() // class - @Suppress("UNUSED_VARIABLE") - val ttl = buf.readInt().toLong() and 0xffffffffL // ttl - val length = buf.readShort().toInt() and 0xffff - - if (type == TYPE_A || type == TYPE_AAAA) { - val bytes = ByteArray(length) - buf.read(bytes) - result.add(InetAddress.getByAddress(bytes)) - } else { - buf.skip(length.toLong()) - } - } - - return result - } - - @Throws(EOFException::class) - private fun skipName(source: Buffer) { - // 0 - 63 bytes - var length = source.readByte().toInt() - - if (length < 0) { - // compressed name pointer, first two bits are 1 - // drop second byte of compression offset - source.skip(1) - } else { - while (length > 0) { - // skip each part of the domain name - source.skip(length.toLong()) - length = source.readByte().toInt() - } - } - } -} diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderRecordedValuesTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderRecordedValuesTest.kt index 421357a9b93a..f057584370e3 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderRecordedValuesTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderRecordedValuesTest.kt @@ -50,8 +50,7 @@ class DnsMessageReaderRecordedValuesTest { listOf( Question( name = "google.com", - type = 28, - `class` = 1, + type = TYPE_AAAA, ), ), answers = @@ -96,8 +95,7 @@ class DnsMessageReaderRecordedValuesTest { listOf( Question( name = "google.com", - type = 1, - `class` = 1, + type = TYPE_A, ), ), answers = @@ -127,8 +125,7 @@ class DnsMessageReaderRecordedValuesTest { listOf( Question( name = "graph.facebook.com", - type = 1, - `class` = 1, + type = TYPE_A, ), ), answers = @@ -159,12 +156,9 @@ class DnsMessageReaderRecordedValuesTest { listOf( Question( name = "sdflkhfsdlkjdf.ee", - type = 28, - `class` = 1, + type = TYPE_AAAA, ), ), - answers = - listOf(), ), ) } @@ -185,8 +179,7 @@ class DnsMessageReaderRecordedValuesTest { listOf( Question( name = "google.com", - type = 1, - `class` = 1, + type = TYPE_A, ), ), answers = @@ -237,7 +230,6 @@ class DnsMessageReaderRecordedValuesTest { priority = 1, targetName = "", alpnIds = listOf("h2", "http/1.1"), - port = 443, ), ) } @@ -416,7 +408,6 @@ class DnsMessageReaderRecordedValuesTest { priority = 1, targetName = "dnsr1.planaltonet.net.br", alpnIds = listOf("h2", "h3", "http/1.1"), - port = 443, ipAddressHints = listOf( InetAddress.getByName("190.109.80.251"), diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderWriterTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderWriterTest.kt new file mode 100644 index 000000000000..de80efeddcea --- /dev/null +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsMessageReaderWriterTest.kt @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3.dnsoverhttps + +import assertk.assertThat +import assertk.assertions.isEqualTo +import java.net.InetAddress +import kotlin.test.Test +import okio.Buffer +import okio.ByteString.Companion.decodeHex + +class DnsMessageReaderWriterTest { + @Test + fun `ipv4 round trip`() { + assertRoundTrip( + DnsMessage( + id = 65432.toShort(), + flags = -32384, + questions = + listOf( + Question( + name = "lysine.dev", + type = TYPE_A, + ), + ), + answers = + listOf( + ResourceRecord.IpAddress( + name = "lysine.dev", + timeToLive = 0, + address = InetAddress.getByName("172.217.23.238"), + ), + ResourceRecord.IpAddress( + name = "lysine.dev", + timeToLive = 86_400, + address = InetAddress.getByName("31.13.80.8"), + ), + ), + ), + ) + } + + @Test + fun `ipv6 round trip`() { + assertRoundTrip( + DnsMessage( + id = 65432.toShort(), + flags = -32384, + questions = + listOf( + Question( + name = "lysine.dev", + type = TYPE_AAAA, + ), + ), + answers = + listOf( + ResourceRecord.IpAddress( + name = "lysine.dev", + timeToLive = 60, + address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:71"), + ), + ResourceRecord.IpAddress( + name = "lysine.dev", + timeToLive = 300, + address = InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:8b"), + ), + ), + ), + ) + } + + /** + * This also confirms that name compression accepts elements from [ResourceRecord.Https] records + * and uses those records. + */ + @Test + fun `https round trip`() { + assertRoundTrip( + DnsMessage( + id = 65432.toShort(), + flags = -32384, + questions = + listOf( + Question( + name = "lysine.dev", + type = TYPE_HTTPS, + ), + ), + answers = + listOf( + ResourceRecord.Https( + name = "lysine.dev", + timeToLive = 60, + priority = 2, + targetName = "ca-west.cdn.lysine.dev", + alpnIds = listOf("h2", "http/1.1"), + port = 8443, + ipAddressHints = + listOf( + InetAddress.getByName("172.217.23.238"), + InetAddress.getByName("31.13.80.8"), + InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:71"), + InetAddress.getByName("2607:f8b0:4023:1807:0:0:0:8b"), + ), + echConfigList = + ( + "003dfe0d0039aa00200020a4a7bb34b77c43336c3a2931dd28c87d008218a99b44f1f" + + "0aa8a82537d487d43000400010001000a676f6f676c652e636f6d0000" + ).decodeHex(), + ), + ResourceRecord.Https( + name = "lysine.dev", + timeToLive = 300, + priority = 1, + targetName = "ca-east.cdn.lysine.dev", + alpnIds = listOf("h2"), + ), + ), + ), + ) + } + + private fun assertRoundTrip(message: DnsMessage) { + val buffer = Buffer() + DnsMessageWriter(buffer).write(message) + assertThat(DnsMessageReader(buffer).read()).isEqualTo(message) + } +} diff --git a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt index 9fa9272e1711..0a9c70651b7b 100644 --- a/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt +++ b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DnsRecordCodecTest.kt @@ -22,9 +22,8 @@ import assertk.assertions.isEqualTo import java.net.InetAddress import java.net.UnknownHostException import kotlin.test.assertFailsWith -import okhttp3.dnsoverhttps.DnsRecordCodec.TYPE_A -import okhttp3.dnsoverhttps.DnsRecordCodec.TYPE_AAAA -import okhttp3.dnsoverhttps.DnsRecordCodec.decodeAnswers +import okio.Buffer +import okio.ByteString import okio.ByteString.Companion.decodeHex import org.junit.jupiter.api.Test @@ -38,7 +37,7 @@ class DnsRecordCodecTest { private fun encodeQuery( host: String, type: Int, - ): String = DnsRecordCodec.encodeQuery(host, type).base64Url().replace("=", "") + ): String = DnsMessage.query(host, type).asQueryParameter() @Test fun testGoogleDotComEncodingWithIPv6() { @@ -50,7 +49,6 @@ class DnsRecordCodecTest { fun testGoogleDotComDecodingFromCloudflare() { val encoded = decodeAnswers( - hostname = "test.com", byteString = ( "00008180000100010000000006676f6f676c6503636f6d0000010001c00c0001000100000043" + @@ -64,7 +62,6 @@ class DnsRecordCodecTest { fun testGoogleDotComDecodingFromGoogle() { val decoded = decodeAnswers( - hostname = "test.com", byteString = ( "0000818000010003000000000567726170680866616365626f6f6b03636f6d0000010001c00c" + @@ -79,7 +76,6 @@ class DnsRecordCodecTest { fun testGoogleDotComDecodingFromGoogleIPv6() { val decoded = decodeAnswers( - hostname = "test.com", byteString = ( "0000818000010003000000000567726170680866616365626f6f6b03636f6d00001c0001c00c" + @@ -95,7 +91,6 @@ class DnsRecordCodecTest { fun testGoogleDotComDecodingNxdomainFailure() { assertFailsWith { decodeAnswers( - hostname = "sdflkhfsdlkjdf.ee", byteString = ( "0000818300010000000100000e7364666c6b686673646c6b6a64660265650000010001c01b" + @@ -103,8 +98,17 @@ class DnsRecordCodecTest { "74c01b5adb12c100000e10000003840012750000000e10" ).decodeHex(), ) - }.also { expected -> - assertThat(expected.message).isEqualTo("sdflkhfsdlkjdf.ee: NXDOMAIN") } } + + private fun decodeAnswers(byteString: ByteString): List { + val reader = DnsMessageReader(Buffer().write(byteString)) + val dnsMessage = reader.read() + if (dnsMessage.responseCode != RESPONSE_CODE_SUCCESS) { + throw UnknownHostException() + } + return dnsMessage.answers + .filterIsInstance() + .map { it.address } + } }