/*
* Copyright (C) 2015 SoftIndex LLC.
*
* 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 io.datakernel.dns;
import io.datakernel.async.ConcurrentResultCallback;
import io.datakernel.async.IgnoreResultCallback;
import io.datakernel.async.ResultCallback;
import io.datakernel.dns.DnsCache.DnsCacheQueryResult;
import io.datakernel.eventloop.AsyncUdpSocketImpl;
import io.datakernel.eventloop.Eventloop;
import io.datakernel.http.HttpUtils;
import io.datakernel.jmx.EventloopJmxMBean;
import io.datakernel.jmx.JmxAttribute;
import io.datakernel.jmx.JmxOperation;
import io.datakernel.net.DatagramSocketSettings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.channels.DatagramChannel;
import java.util.ArrayList;
import java.util.List;
import static io.datakernel.dns.DnsCache.DnsCacheQueryResult.*;
import static io.datakernel.eventloop.Eventloop.createDatagramChannel;
import static io.datakernel.http.HttpUtils.inetAddress;
import static io.datakernel.util.Preconditions.checkArgument;
import static java.util.Arrays.asList;
/**
* An implementation of DNS client which resolves IP addresses.
* <p>
* Instance of this class is capable to cache resolved addresses and able to
* resolve IPv6 and IPv4 addresses.
*/
public final class AsyncDnsClient implements IAsyncDnsClient, EventloopJmxMBean {
private final Logger logger = LoggerFactory.getLogger(AsyncDnsClient.class);
private static final int DNS_SERVER_PORT = 53;
private static final long ONE_MINUTE_MILLIS = 60 * 1000L;
public static final DatagramSocketSettings DEFAULT_DATAGRAM_SOCKET_SETTINGS = DatagramSocketSettings.create();
public static final InetSocketAddress GOOGLE_PUBLIC_DNS = new InetSocketAddress(inetAddress("8.8.8.8"), DNS_SERVER_PORT);
public static final InetSocketAddress LOCAL_DNS = new InetSocketAddress(inetAddress("192.168.0.1"), DNS_SERVER_PORT);
public static final long DEFAULT_TIMEOUT = 3_000L;
private final Eventloop eventloop;
private DnsClientConnection connection;
private final DatagramSocketSettings datagramSocketSettings;
private InetSocketAddress dnsServerAddress;
private final DnsCache cache;
private final long timeout;
// region builders
public static AsyncDnsClient create(Eventloop eventloop) {
DnsCache cache = DnsCache.create(eventloop, ONE_MINUTE_MILLIS, ONE_MINUTE_MILLIS);
return new AsyncDnsClient(eventloop, DEFAULT_DATAGRAM_SOCKET_SETTINGS,
DEFAULT_TIMEOUT, GOOGLE_PUBLIC_DNS, cache);
}
public AsyncDnsClient withDatagramSocketSetting(DatagramSocketSettings setting) {
return new AsyncDnsClient(eventloop, setting, timeout, dnsServerAddress, cache);
}
/**
* Creates a client which waits for result for specified timeout
*
* @param timeout time which this resolver will wait result
* @return a client, waiting for response for specified timeout
*/
public AsyncDnsClient withTimeout(long timeout) {
return new AsyncDnsClient(eventloop, datagramSocketSettings, timeout, dnsServerAddress, cache);
}
/**
* Creates a client with an address of server responsible for resolving
* domains names
*
* @param address address of DNS server which will resolve domain names
* @return a client with specified DNS server address
*/
public AsyncDnsClient withDnsServerAddress(InetSocketAddress address) {
return new AsyncDnsClient(eventloop, datagramSocketSettings, timeout, address, cache);
}
/**
* Creates a client with an address of server responsible for resolving
* domains names
*
* @param address address of DNS server which will resolve domain names
* @return a client with specified DNS server address
*/
public AsyncDnsClient withDnsServerAddress(InetAddress address) {
return new AsyncDnsClient(eventloop, datagramSocketSettings, timeout,
new InetSocketAddress(address, DNS_SERVER_PORT), cache);
}
public AsyncDnsClient withExpiration(long errorCacheExpirationMillis, long hardExpirationDeltaMillis) {
DnsCache cache = DnsCache.create(eventloop, errorCacheExpirationMillis, hardExpirationDeltaMillis);
return new AsyncDnsClient(eventloop, datagramSocketSettings, timeout, dnsServerAddress, cache);
}
// endregion
private AsyncDnsClient(Eventloop eventloop, DatagramSocketSettings datagramSocketSettings, long timeout, InetSocketAddress dnsServerAddress,
long errorCacheExpirationMillis, long hardExpirationDeltaMillis) {
this.eventloop = eventloop;
this.datagramSocketSettings = datagramSocketSettings;
this.timeout = timeout;
this.dnsServerAddress = dnsServerAddress;
this.cache = DnsCache.create(eventloop, errorCacheExpirationMillis, hardExpirationDeltaMillis);
}
private AsyncDnsClient(Eventloop eventloop, DatagramSocketSettings datagramSocketSettings, long timeout, InetSocketAddress dnsServerAddress,
DnsCache cache) {
this.eventloop = eventloop;
this.datagramSocketSettings = datagramSocketSettings;
this.timeout = timeout;
this.dnsServerAddress = dnsServerAddress;
this.cache = cache;
}
/**
* Returns the DNS adapted client which will run in other eventloop using the same DNS cache
*
* @param eventloop eventloop in which DnsClient will be ran
* @return DNS client which will run in other eventloop
*/
public IAsyncDnsClient adaptToAnotherEventloop(final Eventloop eventloop) {
if (eventloop == this.eventloop)
return this;
return new IAsyncDnsClient() {
@Override
public void resolve4(final String domainName, final ResultCallback<InetAddress[]> callback) {
resolve(domainName, false, callback);
}
@Override
public void resolve6(final String domainName, final ResultCallback<InetAddress[]> callback) {
resolve(domainName, true, callback);
}
private void resolve(final String domainName, final boolean ipv6, final ResultCallback<InetAddress[]> callback) {
checkArgument(domainName != null && !domainName.isEmpty(), "Domain name cannot be null or empty");
if (HttpUtils.isInetAddress(domainName)) {
callback.setResult(new InetAddress[]{inetAddress(domainName)});
return;
}
DnsCacheQueryResult cacheQueryResult = cache.tryToResolve(domainName, ipv6, callback);
if (cacheQueryResult == RESOLVED)
return;
if (cacheQueryResult == RESOLVED_NEEDS_REFRESHING) {
AsyncDnsClient.this.eventloop.execute(new Runnable() {
@Override
public void run() {
AsyncDnsClient.this.resolve(domainName, ipv6, IgnoreResultCallback.<InetAddress[]>create());
}
});
return;
}
if (cacheQueryResult == NOT_RESOLVED) {
AsyncDnsClient.this.eventloop.execute(new Runnable() {
@Override
public void run() {
AsyncDnsClient.this.resolve(domainName, ipv6, ConcurrentResultCallback.create(eventloop, callback));
}
});
}
}
};
}
/**
* Resolves the IP for the IPv4 addresses and handles it with callback
*
* @param domainName domain name for searching IP
* @param callback result callback
*/
@Override
public void resolve4(final String domainName, ResultCallback<InetAddress[]> callback) {
resolve(domainName, false, callback);
}
/**
* Resolves the IP for the IPv6 addresses and handles it with callback
*
* @param domainName domain name for searching IP
* @param callback result callback
*/
@Override
public void resolve6(String domainName, ResultCallback<InetAddress[]> callback) {
resolve(domainName, true, callback);
}
private void resolve(final String domainName, final boolean ipv6, final ResultCallback<InetAddress[]> callback) {
checkArgument(domainName != null && !domainName.isEmpty(), "Domain name cannot be null or empty");
if (HttpUtils.isInetAddress(domainName)) {
callback.setResult(new InetAddress[]{HttpUtils.inetAddress(domainName)});
return;
}
DnsCacheQueryResult cacheQueryResult = cache.tryToResolve(domainName, ipv6, callback);
if (cacheQueryResult == RESOLVED) {
cache.performCleanup();
return;
}
final boolean resolvedFromCache = cacheQueryResult == RESOLVED_NEEDS_REFRESHING;
logger.trace("Resolving {} with DNS server.", domainName);
final ResultCallback<DnsQueryResult> queryCachingCallback = new ResultCallback<DnsQueryResult>() {
@Override
protected void onResult(DnsQueryResult result) {
if (callback != null && !resolvedFromCache) {
callback.setResult(result.getIps());
}
cache.add(result);
closeConnectionIfDone();
}
@Override
protected void onException(Exception exception) {
if (exception instanceof DnsException) {
DnsException dnsException = (DnsException) exception;
cache.add(dnsException);
}
if (callback != null && !resolvedFromCache) {
callback.setException(exception);
}
closeConnectionIfDone();
}
private void closeConnectionIfDone() {
if (connection != null && connection.allRequestsCompleted()) {
connection.close();
connection = null;
}
}
};
eventloop.post(new Runnable() {
@Override
public void run() {
if (connection == null) {
try {
registerConnection();
} catch (IOException e) {
if (logger.isErrorEnabled()) logger.error("DnsClientConnection registration failed.", e);
queryCachingCallback.setException(e);
return;
}
}
if (ipv6) {
connection.resolve6(domainName, dnsServerAddress, timeout, queryCachingCallback);
} else {
connection.resolve4(domainName, dnsServerAddress, timeout, queryCachingCallback);
}
}
});
cache.performCleanup();
}
private void registerConnection() throws IOException {
DatagramChannel datagramChannel = createDatagramChannel(datagramSocketSettings, null, dnsServerAddress);
AsyncUdpSocketImpl udpSocket = AsyncUdpSocketImpl.create(eventloop, datagramChannel);
connection = DnsClientConnection.create(eventloop, udpSocket);
udpSocket.setEventHandler(connection);
udpSocket.register();
}
@Override
public Eventloop getEventloop() {
return eventloop;
}
DnsCache getCache() {
return cache;
}
// jmx
@JmxAttribute
public int getNumberOfCachedDomainNames() {
return cache.getNumberOfCachedDomainNames();
}
@JmxAttribute
public int getNumberOfCachedExceptions() {
return cache.getNumberOfCachedExceptions();
}
@JmxAttribute
public int getNumberOfQueriesInProgress() {
if (connection == null) {
return 0;
} else {
return connection.getNumberOfRequestsInProgress();
}
}
@JmxAttribute
public List<String> getDomainNamesBeingResolved() {
if (connection == null) {
return new ArrayList<>();
} else {
return asList(connection.getDomainNamesBeingResolved());
}
}
@JmxAttribute
public List<String> getAllCacheEntries() {
return asList(cache.getAllCacheEntries());
}
@JmxAttribute
public List<String> getSuccessfullyResolvedDomainNames() {
return asList(cache.getSuccessfullyResolvedDomainNames());
}
@JmxAttribute
public List<String> getDomainNamesOfFailedRequests() {
return asList(cache.getDomainNamesOfFailedRequests());
}
@JmxAttribute(description = "max time to live for cache entry (resolved ip address for domain)")
public long getMaxTtlSeconds() {
return cache.getMaxTtlSeconds();
}
@JmxAttribute
public void setMaxTtlSeconds(long maxTtlSeconds) {
cache.setMaxTtlSeconds(maxTtlSeconds);
}
@JmxOperation
public void emptyCache() {
cache.clear();
}
}