/* * 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.getlantern.firetweet.util.net; import android.content.Context; import android.content.SharedPreferences; import android.support.annotation.NonNull; import android.util.Log; import org.apache.http.conn.util.InetAddressUtils; import org.getlantern.firetweet.Constants; import org.getlantern.firetweet.util.HostsFileParser; import org.getlantern.firetweet.util.Utils; 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.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map.Entry; import java.util.regex.Pattern; import twitter4j.http.HostAddressResolver; import static android.text.TextUtils.isEmpty; public class TwidereHostAddressResolver implements Constants, HostAddressResolver { private static final String RESOLVER_LOGTAG = "Twidere.Host"; private static final String DEFAULT_DNS_SERVER_ADDRESS = "8.8.8.8"; private final SharedPreferences mHostMapping, mPreferences; private final HostsFileParser mSystemHosts = new HostsFileParser(); private final HostCache mHostCache = new HostCache(512); private final boolean mLocalMappingOnly; private final String mDnsAddress; private Resolver mDns; public TwidereHostAddressResolver(final Context context) { this(context, false); } public TwidereHostAddressResolver(final Context context, final boolean localOnly) { mHostMapping = context.getSharedPreferences(HOST_MAPPING_PREFERENCES_NAME, Context.MODE_PRIVATE); mPreferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); final String address = mPreferences.getString(KEY_DNS_SERVER, DEFAULT_DNS_SERVER_ADDRESS); mDnsAddress = isValidIpAddress(address) ? address : DEFAULT_DNS_SERVER_ADDRESS; mLocalMappingOnly = localOnly; } @SuppressWarnings("unused") public synchronized void removeCachedHost(final String host) { mHostCache.remove(host); } @NonNull @Override public InetAddress[] resolve(@NonNull final String host) throws IOException { return resolveInternal(host, host); } private InetAddress[] resolveInternal(String originalHost, String host) throws IOException { if (isValidIpAddress(host)) return fromAddressString(originalHost, host); // First, I'll try to load address cached. if (mHostCache.containsKey(host)) { final InetAddress[] hostAddr = mHostCache.get(host); if (Utils.isDebugBuild()) { Log.d(RESOLVER_LOGTAG, "Got cached " + Arrays.toString(hostAddr)); } return hostAddr; } // Then I'll try to load from custom host mapping. // Stupid way to find top domain, but really fast. if (mHostMapping.contains(host)) { final String mappedAddr = mHostMapping.getString(host, null); if (mappedAddr != null) { final InetAddress[] hostAddr = fromAddressString(originalHost, mappedAddr); mHostCache.put(originalHost, hostAddr); if (Utils.isDebugBuild()) { Log.d(RESOLVER_LOGTAG, "Got mapped " + Arrays.toString(hostAddr)); } return hostAddr; } } mSystemHosts.reloadIfNeeded(); if (mSystemHosts.contains(host)) { final InetAddress[] hostAddr = fromAddressString(originalHost, mSystemHosts.getAddress(host)); mHostCache.put(originalHost, hostAddr); if (Utils.isDebugBuild()) { Log.d(RESOLVER_LOGTAG, "Got hosts " + Arrays.toString(hostAddr)); } return hostAddr; } final String customMappedHost = findHost(host); if (customMappedHost != null) { final InetAddress[] hostAddr = fromAddressString(originalHost, customMappedHost); mHostCache.put(originalHost, hostAddr); if (Utils.isDebugBuild()) { Log.d(RESOLVER_LOGTAG, "Got mapped address " + customMappedHost + " for host " + host); } return hostAddr; } // Use TCP DNS Query if enabled. final Resolver dns = getResolver(); if (dns != null && mPreferences.getBoolean(KEY_TCP_DNS_QUERY, false)) { 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. for (final Record record : records) { 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()]); mHostCache.put(originalHost, hostAddr); if (Utils.isDebugBuild()) { Log.d(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()); } } if (Utils.isDebugBuild()) { Log.w(RESOLVER_LOGTAG, "Resolve address " + host + " failed, using original host"); } final InetAddress[] defaultAddresses = InetAddress.getAllByName(host); mHostCache.put(host, defaultAddresses); return defaultAddresses; } private InetAddress[] fromAddressString(String host, String address) throws UnknownHostException { InetAddress inetAddress = InetAddress.getByName(address); if (inetAddress instanceof Inet4Address) { return new InetAddress[]{Inet4Address.getByAddress(host, inetAddress.getAddress())}; } else if (inetAddress instanceof Inet6Address) { return new InetAddress[]{Inet6Address.getByAddress(host, inetAddress.getAddress())}; } throw new UnknownHostException("Bad address " + host + " = " + address); } private String findHost(final String host) { for (final Entry<String, ?> entry : mHostMapping.getAll().entrySet()) { if (hostMatches(host, entry.getKey())) return (String) entry.getValue(); } return null; } private Resolver getResolver() throws IOException { if (mDns != null) return mDns; mDns = new SimpleResolver(mDnsAddress); mDns.setTCP(true); return mDns; } private static boolean hostMatches(final String host, final String rule) { if (rule == null || host == null) return false; if (rule.startsWith(".")) return host.matches("(?i).*" + Pattern.quote(rule)); return host.equalsIgnoreCase(rule); } private static boolean isValidIpAddress(final String address) { if (isEmpty(address)) return false; return InetAddressUtils.isIPv4Address(address) || InetAddressUtils.isIPv6Address(address); } private static class HostCache extends LinkedHashMap<String, InetAddress[]> { private static final long serialVersionUID = -9216545511009449147L; HostCache(final int initialCapacity) { super(initialCapacity); } @Override public InetAddress[] put(final String key, final InetAddress... value) { if (value == null) return null; return super.put(key, value); } } }