From c2fa8ca193dd917caaa528188f51101ab1df139f Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sun, 24 Mar 2019 12:24:40 +0000 Subject: [PATCH] Refactor DOH classes to Kotlin (#4768) --- .../{BootstrapDns.java => BootstrapDns.kt} | 34 +- .../okhttp3/dnsoverhttps/DnsOverHttps.java | 359 ------------------ .../java/okhttp3/dnsoverhttps/DnsOverHttps.kt | 327 ++++++++++++++++ .../okhttp3/dnsoverhttps/DnsRecordCodec.java | 139 ------- .../okhttp3/dnsoverhttps/DnsRecordCodec.kt | 134 +++++++ .../okhttp3/dnsoverhttps/package-info.java | 3 - .../java/okhttp3/dnsoverhttps/package-info.kt | 2 + 7 files changed, 478 insertions(+), 520 deletions(-) rename okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/{BootstrapDns.java => BootstrapDns.kt} (53%) delete mode 100644 okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsOverHttps.java create mode 100644 okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsOverHttps.kt delete mode 100644 okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsRecordCodec.java create mode 100644 okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsRecordCodec.kt delete mode 100644 okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/package-info.java create mode 100644 okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/package-info.kt diff --git a/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/BootstrapDns.java b/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/BootstrapDns.kt similarity index 53% rename from okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/BootstrapDns.java rename to okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/BootstrapDns.kt index 2cd474d30..552348783 100644 --- a/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/BootstrapDns.java +++ b/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/BootstrapDns.kt @@ -13,33 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package okhttp3.dnsoverhttps; +package okhttp3.dnsoverhttps -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.List; -import okhttp3.Dns; +import okhttp3.Dns +import java.net.InetAddress +import java.net.UnknownHostException /** * Internal Bootstrap DNS implementation for handling initial connection to DNS over HTTPS server. * * Returns hardcoded results for the known host. */ -final class BootstrapDns implements Dns { - private final String dnsHostname; - private final List dnsServers; - - BootstrapDns(String dnsHostname, List dnsServers) { - this.dnsHostname = dnsHostname; - this.dnsServers = dnsServers; - } - - @Override public List lookup(String hostname) throws UnknownHostException { - if (!this.dnsHostname.equals(hostname)) { - throw new UnknownHostException( - "BootstrapDns called for " + hostname + " instead of " + dnsHostname); +internal class BootstrapDns( + private val dnsHostname: String, + private val dnsServers: List +) : Dns { + @Throws(UnknownHostException::class) + override fun lookup(hostname: String): List { + if (this.dnsHostname != hostname) { + throw UnknownHostException( + "BootstrapDns called for $hostname instead of $dnsHostname" + ) } - return dnsServers; + return dnsServers } } diff --git a/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsOverHttps.java b/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsOverHttps.java deleted file mode 100644 index fb6901fe3..000000000 --- a/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsOverHttps.java +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright (C) 2018 Square, Inc. - * - * 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 java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import javax.annotation.Nullable; -import okhttp3.CacheControl; -import okhttp3.Call; -import okhttp3.Callback; -import okhttp3.Dns; -import okhttp3.HttpUrl; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Protocol; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; -import okhttp3.ResponseBody; -import okhttp3.internal.platform.Platform; -import okhttp3.internal.publicsuffix.PublicSuffixDatabase; -import okio.ByteString; - -import static java.util.Arrays.asList; - -/** - * DNS over HTTPS implementation. - * - * Implementation of https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-13 - * - *
A DNS API client encodes a single DNS query into an HTTP request - * using either the HTTP GET or POST method and the other requirements - * of this section. The DNS API server defines the URI used by the - * request through the use of a URI Template.
- * - *

Warning: This is a non-final API.

- * - *

As of OkHttp 3.11, this feature is an unstable preview: the API is subject to change, - * and the implementation is incomplete. We expect that OkHttp 3.12 or 3.13 will finalize this API. - * Until then, expect API and behavior changes when you update your OkHttp dependency. - */ -public class DnsOverHttps implements Dns { - public static final MediaType DNS_MESSAGE = MediaType.get("application/dns-message"); - public static final int MAX_RESPONSE_SIZE = 64 * 1024; - private final OkHttpClient client; - private final HttpUrl url; - private final boolean includeIPv6; - private final boolean post; - private final boolean resolvePrivateAddresses; - private final boolean resolvePublicAddresses; - - DnsOverHttps(Builder builder) { - if (builder.client == null) { - throw new NullPointerException("client not set"); - } - if (builder.url == null) { - throw new NullPointerException("url not set"); - } - - this.url = builder.url; - this.includeIPv6 = builder.includeIPv6; - this.post = builder.post; - this.resolvePrivateAddresses = builder.resolvePrivateAddresses; - this.resolvePublicAddresses = builder.resolvePublicAddresses; - this.client = builder.client.newBuilder().dns(buildBootstrapClient(builder)).build(); - } - - private static Dns buildBootstrapClient(Builder builder) { - List hosts = builder.bootstrapDnsHosts; - - if (hosts != null) { - return new BootstrapDns(builder.url.host(), hosts); - } else { - return builder.systemDns; - } - } - - public HttpUrl url() { - return url; - } - - public boolean post() { - return post; - } - - public boolean includeIPv6() { - return includeIPv6; - } - - public OkHttpClient client() { - return client; - } - - public boolean resolvePrivateAddresses() { - return resolvePrivateAddresses; - } - - public boolean resolvePublicAddresses() { - return resolvePublicAddresses; - } - - @Override public List lookup(String hostname) throws UnknownHostException { - if (!resolvePrivateAddresses || !resolvePublicAddresses) { - boolean privateHost = isPrivateHost(hostname); - - if (privateHost && !resolvePrivateAddresses) { - throw new UnknownHostException("private hosts not resolved"); - } - - if (!privateHost && !resolvePublicAddresses) { - throw new UnknownHostException("public hosts not resolved"); - } - } - - return lookupHttps(hostname); - } - - private List lookupHttps(String hostname) throws UnknownHostException { - List networkRequests = new ArrayList<>(2); - List failures = new ArrayList<>(2); - List results = new ArrayList<>(5); - - buildRequest(hostname, networkRequests, results, failures, DnsRecordCodec.TYPE_A); - - if (includeIPv6) { - buildRequest(hostname, networkRequests, results, failures, DnsRecordCodec.TYPE_AAAA); - } - - executeRequests(hostname, networkRequests, results, failures); - - if (!results.isEmpty()) { - return results; - } - - return throwBestFailure(hostname, failures); - } - - private void buildRequest(String hostname, List networkRequests, List results, - List failures, int type) { - Request request = buildRequest(hostname, type); - Response response = getCacheOnlyResponse(request); - - if (response != null) { - processResponse(response, hostname, results, failures); - } else { - networkRequests.add(client.newCall(request)); - } - } - - private void executeRequests(final String hostname, List networkRequests, - final List responses, final List failures) { - final CountDownLatch latch = new CountDownLatch(networkRequests.size()); - - for (Call call : networkRequests) { - call.enqueue(new Callback() { - @Override public void onFailure(Call call, IOException e) { - synchronized (failures) { - failures.add(e); - } - latch.countDown(); - } - - @Override public void onResponse(Call call, Response response) { - processResponse(response, hostname, responses, failures); - latch.countDown(); - } - }); - } - - try { - latch.await(); - } catch (InterruptedException e) { - failures.add(e); - } - } - - private void processResponse(Response response, String hostname, List results, - List failures) { - try { - List addresses = readResponse(hostname, response); - synchronized (results) { - results.addAll(addresses); - } - } catch (Exception e) { - synchronized (failures) { - failures.add(e); - } - } - } - - private List throwBestFailure(String hostname, List failures) - throws UnknownHostException { - if (failures.size() == 0) { - throw new UnknownHostException(hostname); - } - - Exception failure = failures.get(0); - - if (failure instanceof UnknownHostException) { - throw (UnknownHostException) failure; - } - - UnknownHostException unknownHostException = new UnknownHostException(hostname); - unknownHostException.initCause(failure); - - for (int i = 1; i < failures.size(); i++) { - unknownHostException.addSuppressed(failures.get(i)); - } - - throw unknownHostException; - } - - private @Nullable Response getCacheOnlyResponse(Request request) { - if (!post && client.cache() != null) { - try { - Request cacheRequest = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build(); - - Response cacheResponse = client.newCall(cacheRequest).execute(); - - if (cacheResponse.code() != 504) { - return cacheResponse; - } - } catch (IOException ioe) { - // Failures are ignored as we can fallback to the network - // and hopefully repopulate the cache. - } - } - - return null; - } - - private List readResponse(String hostname, Response response) throws Exception { - if (response.cacheResponse() == null && response.protocol() != Protocol.HTTP_2) { - Platform.get().log(Platform.WARN, "Incorrect protocol: " + response.protocol(), null); - } - - try { - if (!response.isSuccessful()) { - throw new IOException("response: " + response.code() + " " + response.message()); - } - - ResponseBody body = response.body(); - - if (body.contentLength() > MAX_RESPONSE_SIZE) { - throw new IOException("response size exceeds limit (" - + MAX_RESPONSE_SIZE - + " bytes): " - + body.contentLength() - + " bytes"); - } - - ByteString responseBytes = body.source().readByteString(); - - return DnsRecordCodec.decodeAnswers(hostname, responseBytes); - } finally { - response.close(); - } - } - - private Request buildRequest(String hostname, int type) { - Request.Builder requestBuilder = new Request.Builder().header("Accept", DNS_MESSAGE.toString()); - - ByteString query = DnsRecordCodec.encodeQuery(hostname, type); - - if (post) { - requestBuilder = requestBuilder.url(url).post(RequestBody.create(DNS_MESSAGE, query)); - } else { - String encoded = query.base64Url().replace("=", ""); - HttpUrl requestUrl = url.newBuilder().addQueryParameter("dns", encoded).build(); - - requestBuilder = requestBuilder.url(requestUrl); - } - - return requestBuilder.build(); - } - - static boolean isPrivateHost(String host) { - return PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) == null; - } - - public static final class Builder { - @Nullable OkHttpClient client = null; - @Nullable HttpUrl url = null; - boolean includeIPv6 = true; - boolean post = false; - Dns systemDns = Dns.SYSTEM; - @Nullable List bootstrapDnsHosts = null; - boolean resolvePrivateAddresses = false; - boolean resolvePublicAddresses = true; - - public Builder() { - } - - public DnsOverHttps build() { - return new DnsOverHttps(this); - } - - public Builder client(OkHttpClient client) { - this.client = client; - return this; - } - - public Builder url(HttpUrl url) { - this.url = url; - return this; - } - - public Builder includeIPv6(boolean includeIPv6) { - this.includeIPv6 = includeIPv6; - return this; - } - - public Builder post(boolean post) { - this.post = post; - return this; - } - - public Builder resolvePrivateAddresses(boolean resolvePrivateAddresses) { - this.resolvePrivateAddresses = resolvePrivateAddresses; - return this; - } - - public Builder resolvePublicAddresses(boolean resolvePublicAddresses) { - this.resolvePublicAddresses = resolvePublicAddresses; - return this; - } - - public Builder bootstrapDnsHosts(@Nullable List bootstrapDnsHosts) { - this.bootstrapDnsHosts = bootstrapDnsHosts; - return this; - } - - public Builder bootstrapDnsHosts(InetAddress... bootstrapDnsHosts) { - return bootstrapDnsHosts(asList(bootstrapDnsHosts)); - } - - public Builder systemDns(Dns systemDns) { - this.systemDns = systemDns; - return this; - } - } -} diff --git a/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsOverHttps.kt b/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsOverHttps.kt new file mode 100644 index 000000000..c2f1313c0 --- /dev/null +++ b/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsOverHttps.kt @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * 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 okhttp3.CacheControl +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Dns +import okhttp3.HttpUrl +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.RequestBody +import okhttp3.Response +import okhttp3.internal.platform.Platform +import okhttp3.internal.publicsuffix.PublicSuffixDatabase +import java.io.IOException +import java.net.InetAddress +import java.net.UnknownHostException +import java.util.ArrayList +import java.util.Arrays.asList +import java.util.concurrent.CountDownLatch + +/** + * DNS over HTTPS implementation. + * + * Implementation of https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-13 + * + *

A DNS API client encodes a single DNS query into an HTTP request + * using either the HTTP GET or POST method and the other requirements + * of this section. The DNS API server defines the URI used by the + * request through the use of a URI Template.
+ * + *

Warning: This is a non-final API.

+ * + * + * As of OkHttp 3.14, this feature is an unstable preview: the API is subject to change, + * and the implementation is incomplete. We expect that OkHttp 4.0 or 4.1 will finalize this API. + * Until then, expect API and behavior changes when you update your OkHttp dependency.** + */ +class DnsOverHttps internal constructor(builder: Builder) : Dns { + private val client: OkHttpClient = + builder.client?.newBuilder()?.dns(buildBootstrapClient(builder))?.build() + ?: throw NullPointerException("client not set") + private val url: HttpUrl = builder.url ?: throw NullPointerException("url not set") + private val includeIPv6: Boolean = builder.includeIPv6 + private val post: Boolean = builder.post + private val resolvePrivateAddresses: Boolean = builder.resolvePrivateAddresses + private val resolvePublicAddresses: Boolean = builder.resolvePublicAddresses + + fun url(): HttpUrl = url + + fun post(): Boolean = post + + fun includeIPv6(): Boolean = includeIPv6 + + fun client(): OkHttpClient = client + + fun resolvePrivateAddresses(): Boolean = resolvePrivateAddresses + + fun resolvePublicAddresses(): Boolean = resolvePublicAddresses + + @Throws(UnknownHostException::class) + override fun lookup(hostname: String): List { + if (!resolvePrivateAddresses || !resolvePublicAddresses) { + val privateHost = isPrivateHost(hostname) + + if (privateHost && !resolvePrivateAddresses) { + throw UnknownHostException("private hosts not resolved") + } + + if (!privateHost && !resolvePublicAddresses) { + throw UnknownHostException("public hosts not resolved") + } + } + + return lookupHttps(hostname) + } + + @Throws(UnknownHostException::class) + private fun lookupHttps(hostname: String): List { + val networkRequests = ArrayList(2) + val failures = ArrayList(2) + val results = ArrayList(5) + + buildRequest(hostname, networkRequests, results, failures, DnsRecordCodec.TYPE_A) + + if (includeIPv6) { + buildRequest(hostname, networkRequests, results, failures, DnsRecordCodec.TYPE_AAAA) + } + + executeRequests(hostname, networkRequests, results, failures) + + return if (!results.isEmpty()) { + results + } else { + throwBestFailure(hostname, failures) + } + } + + private fun buildRequest( + hostname: String, networkRequests: MutableList, results: MutableList, + failures: MutableList, type: Int + ) { + val request = buildRequest(hostname, type) + val response = getCacheOnlyResponse(request) + + response?.let { processResponse(it, hostname, results, failures) } ?: networkRequests.add( + client.newCall(request)) + } + + private fun executeRequests( + hostname: String, networkRequests: List, + responses: MutableList, failures: MutableList + ) { + val latch = CountDownLatch(networkRequests.size) + + for (call in networkRequests) { + call.enqueue(object : Callback { + override fun onFailure(call: Call, e: IOException) { + synchronized(failures) { + failures.add(e) + } + latch.countDown() + } + + override fun onResponse(call: Call, response: Response) { + processResponse(response, hostname, responses, failures) + latch.countDown() + } + }) + } + + try { + latch.await() + } catch (e: InterruptedException) { + failures.add(e) + } + } + + private fun processResponse( + response: Response, hostname: String, results: MutableList, + failures: MutableList + ) { + try { + val addresses = readResponse(hostname, response) + synchronized(results) { + results.addAll(addresses) + } + } catch (e: Exception) { + synchronized(failures) { + failures.add(e) + } + } + } + + @Throws(UnknownHostException::class) + private fun throwBestFailure(hostname: String, failures: List): List { + if (failures.isEmpty()) { + throw UnknownHostException(hostname) + } + + val failure = failures[0] + + if (failure is UnknownHostException) { + throw failure + } + + val unknownHostException = UnknownHostException(hostname) + unknownHostException.initCause(failure) + + for (i in 1 until failures.size) { + unknownHostException.addSuppressed(failures[i]) + } + + throw unknownHostException + } + + private fun getCacheOnlyResponse(request: Request): Response? { + if (!post && client.cache() != null) { + try { + val cacheRequest = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build() + + val cacheResponse = client.newCall(cacheRequest).execute() + + if (cacheResponse.code() != 504) { + return cacheResponse + } + } catch (ioe: IOException) { + // Failures are ignored as we can fallback to the network + // and hopefully repopulate the cache. + } + } + + return null + } + + @Throws(Exception::class) + private fun readResponse(hostname: String, response: Response): List { + if (response.cacheResponse() == null && response.protocol() !== Protocol.HTTP_2) { + Platform.get().log(Platform.WARN, "Incorrect protocol: ${response.protocol()}", null) + } + + response.use { + if (!response.isSuccessful) { + throw IOException("response: " + response.code() + " " + response.message()) + } + + val body = response.body() + + if (body!!.contentLength() > MAX_RESPONSE_SIZE) { + throw IOException( + "response size exceeds limit ($MAX_RESPONSE_SIZE bytes): ${body.contentLength()} bytes" + ) + } + + val responseBytes = body.source().readByteString() + + return DnsRecordCodec.decodeAnswers(hostname, responseBytes) + } + } + + private fun buildRequest(hostname: String, type: Int): Request = + Request.Builder().header("Accept", DNS_MESSAGE.toString()).apply { + val query = DnsRecordCodec.encodeQuery(hostname, type) + + if (post) { + url(url).post(RequestBody.create(DNS_MESSAGE, query)) + } else { + val encoded = query.base64Url().replace("=", "") + val requestUrl = url.newBuilder().addQueryParameter("dns", encoded).build() + + url(requestUrl) + } + + }.build() + + class Builder { + internal var client: OkHttpClient? = null + internal var url: HttpUrl? = null + internal var includeIPv6 = true + internal var post = false + internal var systemDns = Dns.SYSTEM + internal var bootstrapDnsHosts: List? = null + internal var resolvePrivateAddresses = false + internal var resolvePublicAddresses = true + + fun build(): DnsOverHttps = DnsOverHttps(this) + + fun client(client: OkHttpClient): Builder { + this.client = client + return this + } + + fun url(url: HttpUrl): Builder { + this.url = url + return this + } + + fun includeIPv6(includeIPv6: Boolean): Builder { + this.includeIPv6 = includeIPv6 + return this + } + + fun post(post: Boolean): Builder { + this.post = post + return this + } + + fun resolvePrivateAddresses(resolvePrivateAddresses: Boolean): Builder { + this.resolvePrivateAddresses = resolvePrivateAddresses + return this + } + + fun resolvePublicAddresses(resolvePublicAddresses: Boolean): Builder { + this.resolvePublicAddresses = resolvePublicAddresses + return this + } + + fun bootstrapDnsHosts(bootstrapDnsHosts: List?): Builder { + this.bootstrapDnsHosts = bootstrapDnsHosts + return this + } + + fun bootstrapDnsHosts(vararg bootstrapDnsHosts: InetAddress): Builder { + return bootstrapDnsHosts(asList(*bootstrapDnsHosts)) + } + + fun systemDns(systemDns: Dns): Builder { + this.systemDns = systemDns + return this + } + } + + companion object { + val DNS_MESSAGE: MediaType = MediaType.get("application/dns-message") + const val MAX_RESPONSE_SIZE = 64 * 1024 + + private fun buildBootstrapClient(builder: Builder): Dns { + val hosts = builder.bootstrapDnsHosts + + return if (hosts != null) { + BootstrapDns(builder.url!!.host(), hosts) + } else { + builder.systemDns + } + } + + internal fun isPrivateHost(host: String): Boolean { + return PublicSuffixDatabase.get().getEffectiveTldPlusOne(host) == null + } + } +} diff --git a/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsRecordCodec.java b/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsRecordCodec.java deleted file mode 100644 index 4c1cfc481..000000000 --- a/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsRecordCodec.java +++ /dev/null @@ -1,139 +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 java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.List; -import okio.Buffer; -import okio.ByteString; -import okio.Utf8; - -/** - * Trivial Dns Encoder/Decoder, basically ripped from Netty full implementation. - */ -class DnsRecordCodec { - private static final byte SERVFAIL = 2; - private static final byte NXDOMAIN = 3; - public static final int TYPE_A = 0x0001; - public static final int TYPE_AAAA = 0x001c; - private static final int TYPE_PTR = 0x000c; - private static final Charset ASCII = Charset.forName("ASCII"); - - private DnsRecordCodec() { - } - - public static ByteString encodeQuery(String host, int type) { - Buffer buf = new Buffer(); - - buf.writeShort(0); // query id - buf.writeShort(256); // flags with recursion - buf.writeShort(1); // question count - buf.writeShort(0); // answerCount - buf.writeShort(0); // authorityResourceCount - buf.writeShort(0); // additional - - Buffer nameBuf = new Buffer(); - final String[] labels = host.split("\\."); - for (String label : labels) { - long utf8ByteCount = Utf8.size(label); - if (utf8ByteCount != label.length()) { - throw new IllegalArgumentException("non-ascii hostname: " + host); - } - nameBuf.writeByte((byte) utf8ByteCount); - nameBuf.writeUtf8(label); - } - nameBuf.writeByte(0); // end - - nameBuf.copyTo(buf, 0, nameBuf.size()); - buf.writeShort(type); - buf.writeShort(1); // CLASS_IN - - return buf.readByteString(); - } - - public static List decodeAnswers(String hostname, ByteString byteString) - throws Exception { - List result = new ArrayList<>(); - - Buffer buf = new Buffer(); - buf.write(byteString); - buf.readShort(); // query id - - final int flags = buf.readShort() & 0xffff; - if (flags >> 15 == 0) { - throw new IllegalArgumentException("not a response"); - } - - byte responseCode = (byte) (flags & 0xf); - - if (responseCode == NXDOMAIN) { - throw new UnknownHostException(hostname + ": NXDOMAIN"); - } else if (responseCode == SERVFAIL) { - throw new UnknownHostException(hostname + ": SERVFAIL"); - } - - final int questionCount = buf.readShort() & 0xffff; - final int answerCount = buf.readShort() & 0xffff; - buf.readShort(); // authority record count - buf.readShort(); // additional record count - - for (int i = 0; i < questionCount; i++) { - skipName(buf); // name - buf.readShort(); // type - buf.readShort(); // class - } - - for (int i = 0; i < answerCount; i++) { - skipName(buf); // name - - int type = buf.readShort() & 0xffff; - buf.readShort(); // class - final long ttl = buf.readInt() & 0xffffffffL; // ttl - final int length = buf.readShort() & 0xffff; - - if (type == TYPE_A || type == TYPE_AAAA) { - byte[] bytes = new byte[length]; - buf.read(bytes); - result.add(InetAddress.getByAddress(bytes)); - } else { - buf.skip(length); - } - } - - return result; - } - - private static void skipName(Buffer in) throws EOFException { - // 0 - 63 bytes - int length = in.readByte(); - - if (length < 0) { - // compressed name pointer, first two bits are 1 - // drop second byte of compression offset - in.skip(1); - } else { - while (length > 0) { - // skip each part of the domain name - in.skip(length); - length = in.readByte(); - } - } - } -} diff --git a/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsRecordCodec.kt b/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsRecordCodec.kt new file mode 100644 index 000000000..cb26297f4 --- /dev/null +++ b/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/DnsRecordCodec.kt @@ -0,0 +1,134 @@ +/* + * 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 okio.Buffer +import okio.ByteString +import okio.Utf8 +import java.io.EOFException +import java.net.InetAddress +import java.net.UnknownHostException +import java.nio.charset.StandardCharsets +import java.util.ArrayList + +/** + * Trivial Dns Encoder/Decoder, basically ripped from Netty full implementation. + */ +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 = StandardCharsets.US_ASCII + + @JvmStatic + 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() }.toTypedArray() + for (label in labels) { + val utf8ByteCount = Utf8.size(label) + if (utf8ByteCount != label.length.toLong()) { + throw IllegalArgumentException("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) + @JvmStatic + fun decodeAnswers(hostname: String, byteString: ByteString): List { + val result = ArrayList() + + val buf = Buffer() + buf.write(byteString) + buf.readShort() // query id + + val flags = buf.readShort().toInt() and 0xffff + if (flags shr 15 == 0) { + throw IllegalArgumentException("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 + 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/main/java/okhttp3/dnsoverhttps/package-info.java b/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/package-info.java deleted file mode 100644 index 54da565bc..000000000 --- a/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/package-info.java +++ /dev/null @@ -1,3 +0,0 @@ -/** A DNS over HTTPS implementation for OkHttp. */ -@okhttp3.internal.annotations.EverythingIsNonNull -package okhttp3.dnsoverhttps; diff --git a/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/package-info.kt b/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/package-info.kt new file mode 100644 index 000000000..f0f89d115 --- /dev/null +++ b/okhttp-dnsoverhttps/src/main/java/okhttp3/dnsoverhttps/package-info.kt @@ -0,0 +1,2 @@ +/** A DNS over HTTPS implementation for OkHttp. */ +package okhttp3.dnsoverhttps