Java tutorial
/* * Twidere - Twitter client for Android * * Copyright (C) 2012-2014 Mariotaku Lee <mariotaku.lee@gmail.com> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.mariotaku.twidere.util.net; import android.content.Context; import android.content.SharedPreferences; import android.os.SystemClock; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.util.SimpleArrayMap; import android.util.Log; import android.util.LruCache; import android.util.TimingLogger; import com.squareup.okhttp.Dns; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.mariotaku.twidere.BuildConfig; import org.mariotaku.twidere.Constants; import org.mariotaku.twidere.util.SharedPreferencesWrapper; import org.xbill.DNS.AAAARecord; import org.xbill.DNS.ARecord; import org.xbill.DNS.CNAMERecord; import org.xbill.DNS.DClass; import org.xbill.DNS.Lookup; import org.xbill.DNS.Name; import org.xbill.DNS.Record; import org.xbill.DNS.Resolver; import org.xbill.DNS.SimpleResolver; import org.xbill.DNS.Type; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map.Entry; import java.util.concurrent.TimeUnit; import javax.inject.Singleton; @Singleton public class TwidereDns implements Constants, Dns { private static final String RESOLVER_LOGTAG = "TwidereDns"; private final SharedPreferences mHostMapping, mPreferences; private final HostCache mHostCache = new HostCache(512); private final SystemHosts mSystemHosts; private Resolver mResolver; private TimingLogger mLogger; public TwidereDns(final Context context) { mLogger = new TimingLogger(RESOLVER_LOGTAG, "resolve"); mHostMapping = SharedPreferencesWrapper.getInstance(context, HOST_MAPPING_PREFERENCES_NAME, Context.MODE_PRIVATE); mPreferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); mSystemHosts = new SystemHosts(); } private static boolean hostMatches(final String host, final String rule) { if (rule == null || host == null) return false; if (rule.startsWith(".")) return StringUtils.endsWithIgnoreCase(host, rule); return host.equalsIgnoreCase(rule); } private static boolean isValidIpAddress(final String address) { return InetAddressUtils.getInetAddressType(address) != 0; } @SuppressWarnings("unused") public synchronized void removeCachedHost(final String host) { mHostCache.remove(host); } @NonNull private InetAddress[] resolveInternal(String originalHost, String host, int depth) throws IOException { resetLog(originalHost); // Return if host is an address final InetAddress[] fromAddressString = fromAddressString(originalHost, host); if (fromAddressString != null) { if (BuildConfig.DEBUG) { addLogSplit(originalHost, host, "valid ip address", depth); dumpLog(); } return fromAddressString; } // Find from cache final InetAddress[] fromCache = getCached(host); if (fromCache != null) { if (BuildConfig.DEBUG) { addLogSplit(originalHost, host, "hit cache", depth); dumpLog(); } return fromCache; } // Load from custom mapping addLogSplit(originalHost, host, "start custom mapping resolve", depth); final InetAddress[] fromMapping = getFromMapping(host); addLogSplit(originalHost, host, "end custom mapping resolve", depth); if (fromMapping != null) { putCache(originalHost, fromMapping, -1, TimeUnit.SECONDS); if (BuildConfig.DEBUG) { dumpLog(); } return fromMapping; } // Load from /etc/hosts addLogSplit(originalHost, host, "start /etc/hosts resolve", depth); final InetAddress[] fromSystemHosts = fromSystemHosts(host); addLogSplit(originalHost, host, "end /etc/hosts resolve", depth); if (fromSystemHosts != null) { putCache(originalHost, fromSystemHosts, 60, TimeUnit.SECONDS); if (BuildConfig.DEBUG) { dumpLog(); } return fromSystemHosts; } // Use TCP DNS Query if enabled. addLogSplit(originalHost, host, "start resolver resolve", depth); final InetAddress[] fromResolver = fromResolver(originalHost, host, depth); addLogSplit(originalHost, host, "end resolver resolve", depth); if (fromResolver != null) { if (BuildConfig.DEBUG) { dumpLog(); } return fromResolver; } addLogSplit(originalHost, host, "start system default resolve", depth); final InetAddress[] fromDefault = InetAddress.getAllByName(host); addLogSplit(originalHost, host, "end system default resolve", depth); putCache(host, fromDefault, 0, TimeUnit.SECONDS); if (BuildConfig.DEBUG) { dumpLog(); } return fromDefault; } private void dumpLog() { mLogger.dumpToLog(); } private void resetLog(String originalHost) { mLogger.reset(RESOLVER_LOGTAG, originalHost); } private void addLogSplit(String originalHost, String host, String message, int depth) { final StringBuilder sb = new StringBuilder(); for (int i = 0; i < depth; i++) { sb.append(">"); } sb.append(" "); sb.append(host); sb.append(": "); sb.append(message); mLogger.addSplit(sb.toString()); } private InetAddress[] fromSystemHosts(String host) { try { return mSystemHosts.resolve(host); } catch (IOException e) { return null; } } private InetAddress[] getCached(String host) { return mHostCache.getValid(host); } private InetAddress[] fromResolver(String originalHost, String host, int depth) throws IOException { final Resolver dns = getResolver(); final Lookup lookup = new Lookup(new Name(host), Type.A, DClass.IN); final Record[] records; lookup.setResolver(dns); lookup.run(); final int result = lookup.getResult(); if (result != Lookup.SUCCESSFUL) { throw new UnknownHostException("Unable to resolve " + host + ", " + lookup.getErrorString()); } records = lookup.getAnswers(); final ArrayList<InetAddress> resolvedAddresses = new ArrayList<>(); // Test each IP address resolved. long ttl = -1; for (final Record record : records) { if (ttl == -1) { ttl = record.getTTL(); } if (record instanceof ARecord) { final InetAddress ipv4Addr = ((ARecord) record).getAddress(); resolvedAddresses.add(InetAddress.getByAddress(originalHost, ipv4Addr.getAddress())); } else if (record instanceof AAAARecord) { final InetAddress ipv6Addr = ((AAAARecord) record).getAddress(); resolvedAddresses.add(InetAddress.getByAddress(originalHost, ipv6Addr.getAddress())); } } if (!resolvedAddresses.isEmpty()) { final InetAddress[] hostAddr = resolvedAddresses.toArray(new InetAddress[resolvedAddresses.size()]); putCache(originalHost, hostAddr, ttl, TimeUnit.SECONDS); if (BuildConfig.DEBUG) { Log.v(RESOLVER_LOGTAG, "Resolved " + Arrays.toString(hostAddr)); } return hostAddr; } // No address is reachable, but I believe the IP is correct. for (final Record record : records) { if (record instanceof CNAMERecord) return resolveInternal(originalHost, ((CNAMERecord) record).getTarget().toString(), depth + 1); } return null; } private void putCache(String host, InetAddress[] addresses, long ttl, TimeUnit unit) { if (ArrayUtils.isEmpty(addresses) || ArrayUtils.contains(addresses, null)) return; mHostCache.put(host, addresses, ttl, unit); } private InetAddress[] fromAddressString(String host, String address) throws UnknownHostException { final InetAddress resolved = InetAddressUtils.getResolvedIPAddress(host, address); if (resolved == null) return null; return new InetAddress[] { resolved }; } @Nullable private InetAddress[] getFromMapping(final String host) { return getFromMappingInternal(host, host, false); } @Nullable private InetAddress[] getFromMappingInternal(String host, String origHost, boolean checkRecursive) { if (checkRecursive && hostMatches(host, origHost)) { // Recursive resolution, stop this call return null; } for (final Entry<String, ?> entry : mHostMapping.getAll().entrySet()) { if (hostMatches(host, entry.getKey())) { final String value = (String) entry.getValue(); final InetAddress resolved = InetAddressUtils.getResolvedIPAddress(origHost, value); if (resolved == null) { // Maybe another hostname return getFromMappingInternal(value, origHost, true); } return new InetAddress[] { resolved }; } } return null; } @NonNull private Resolver getResolver() throws IOException { if (mResolver != null) return mResolver; final boolean tcp = mPreferences.getBoolean(KEY_TCP_DNS_QUERY, false); final String address = mPreferences.getString(KEY_DNS_SERVER, null); final SimpleResolver resolver; if (isValidIpAddress(address)) { resolver = new SimpleResolver(address); } else { resolver = new SimpleResolver(); } resolver.setTCP(tcp); return mResolver = resolver; } public void reloadDnsSettings() { mResolver = null; } @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException { try { return Arrays.asList(resolveInternal(hostname, hostname, 0)); } catch (IOException e) { if (e instanceof UnknownHostException) throw (UnknownHostException) e; throw new UnknownHostException("Unable to resolve address " + e.getMessage()); } } private static class HostCache extends LruCache<String, InetAddress[]> { private SimpleArrayMap<String, Long> ttlMap = new SimpleArrayMap<>(); public HostCache(int maxSize) { super(maxSize); } public synchronized void put(String host, InetAddress[] addresses, long ttl, TimeUnit unit) { // Don't cache this entry if ttl == 0 if (ttl == 0) return; put(host, addresses); // ttl < 0 means permanent entry if (ttl > 0) { ttlMap.put(host, SystemClock.elapsedRealtime() + unit.toMillis(ttl)); } } public InetAddress[] getValid(String host) { cleanExpired(); return get(host); } private synchronized void cleanExpired() { for (int i = ttlMap.size() - 1; i >= 0; i--) { if (ttlMap.valueAt(i) < SystemClock.elapsedRealtime()) { remove(ttlMap.keyAt(i)); ttlMap.removeAt(i); } } } } }