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
|
||||
* 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<InetAddress> dnsServers;
|
||||
|
||||
BootstrapDns(String dnsHostname, List<InetAddress> dnsServers) {
|
||||
this.dnsHostname = dnsHostname;
|
||||
this.dnsServers = dnsServers;
|
||||
}
|
||||
|
||||
@Override public List<InetAddress> 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<InetAddress>
|
||||
) : Dns {
|
||||
@Throws(UnknownHostException::class)
|
||||
override fun lookup(hostname: String): List<InetAddress> {
|
||||
if (this.dnsHostname != hostname) {
|
||||
throw 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