mirror of
https://github.com/square/okhttp.git
synced 2025-07-31 05:04:26 +03:00
Refactor DOH classes to Kotlin (#4768)
This commit is contained in:
@ -13,33 +13,29 @@
|
|||||||
* See the License for the specific language governing permissions and
|
* See the License for the specific language governing permissions and
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
package okhttp3.dnsoverhttps;
|
package okhttp3.dnsoverhttps
|
||||||
|
|
||||||
import java.net.InetAddress;
|
import okhttp3.Dns
|
||||||
import java.net.UnknownHostException;
|
import java.net.InetAddress
|
||||||
import java.util.List;
|
import java.net.UnknownHostException
|
||||||
import okhttp3.Dns;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal Bootstrap DNS implementation for handling initial connection to DNS over HTTPS server.
|
* Internal Bootstrap DNS implementation for handling initial connection to DNS over HTTPS server.
|
||||||
*
|
*
|
||||||
* Returns hardcoded results for the known host.
|
* Returns hardcoded results for the known host.
|
||||||
*/
|
*/
|
||||||
final class BootstrapDns implements Dns {
|
internal class BootstrapDns(
|
||||||
private final String dnsHostname;
|
private val dnsHostname: String,
|
||||||
private final List<InetAddress> dnsServers;
|
private val dnsServers: List<InetAddress>
|
||||||
|
) : Dns {
|
||||||
BootstrapDns(String dnsHostname, List<InetAddress> dnsServers) {
|
@Throws(UnknownHostException::class)
|
||||||
this.dnsHostname = dnsHostname;
|
override fun lookup(hostname: String): List<InetAddress> {
|
||||||
this.dnsServers = dnsServers;
|
if (this.dnsHostname != hostname) {
|
||||||
}
|
throw UnknownHostException(
|
||||||
|
"BootstrapDns called for $hostname instead of $dnsHostname"
|
||||||
@Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
|
)
|
||||||
if (!this.dnsHostname.equals(hostname)) {
|
|
||||||
throw new UnknownHostException(
|
|
||||||
"BootstrapDns called for " + hostname + " instead of " + dnsHostname);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return dnsServers;
|
return dnsServers
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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
|
|
||||||
*
|
|
||||||
* <blockquote>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.</blockquote>
|
|
||||||
*
|
|
||||||
* <h3>Warning: This is a non-final API.</h3>
|
|
||||||
*
|
|
||||||
* <p><strong>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.</strong>
|
|
||||||
*/
|
|
||||||
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<InetAddress> 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<InetAddress> 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<InetAddress> lookupHttps(String hostname) throws UnknownHostException {
|
|
||||||
List<Call> networkRequests = new ArrayList<>(2);
|
|
||||||
List<Exception> failures = new ArrayList<>(2);
|
|
||||||
List<InetAddress> 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<Call> networkRequests, List<InetAddress> results,
|
|
||||||
List<Exception> 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<Call> networkRequests,
|
|
||||||
final List<InetAddress> responses, final List<Exception> 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<InetAddress> results,
|
|
||||||
List<Exception> failures) {
|
|
||||||
try {
|
|
||||||
List<InetAddress> addresses = readResponse(hostname, response);
|
|
||||||
synchronized (results) {
|
|
||||||
results.addAll(addresses);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
synchronized (failures) {
|
|
||||||
failures.add(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<InetAddress> throwBestFailure(String hostname, List<Exception> 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<InetAddress> 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<InetAddress> 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<InetAddress> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
||||||
|
*
|
||||||
|
* <blockquote>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.</blockquote>
|
||||||
|
*
|
||||||
|
* <h3>Warning: This is a non-final API.</h3>
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* 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<InetAddress> {
|
||||||
|
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<InetAddress> {
|
||||||
|
val networkRequests = ArrayList<Call>(2)
|
||||||
|
val failures = ArrayList<Exception>(2)
|
||||||
|
val results = ArrayList<InetAddress>(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<Call>, results: MutableList<InetAddress>,
|
||||||
|
failures: MutableList<Exception>, 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<Call>,
|
||||||
|
responses: MutableList<InetAddress>, failures: MutableList<Exception>
|
||||||
|
) {
|
||||||
|
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<InetAddress>,
|
||||||
|
failures: MutableList<Exception>
|
||||||
|
) {
|
||||||
|
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<Exception>): List<InetAddress> {
|
||||||
|
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<InetAddress> {
|
||||||
|
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<InetAddress>? = 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<InetAddress>?): 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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<InetAddress> decodeAnswers(String hostname, ByteString byteString)
|
|
||||||
throws Exception {
|
|
||||||
List<InetAddress> 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<InetAddress> {
|
||||||
|
val result = ArrayList<InetAddress>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
/** A DNS over HTTPS implementation for OkHttp. */
|
|
||||||
@okhttp3.internal.annotations.EverythingIsNonNull
|
|
||||||
package okhttp3.dnsoverhttps;
|
|
@ -0,0 +1,2 @@
|
|||||||
|
/** A DNS over HTTPS implementation for OkHttp. */
|
||||||
|
package okhttp3.dnsoverhttps
|
Reference in New Issue
Block a user