package uc; import helpers.GH; import helpers.Observable; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.NetworkInterface; import java.net.Proxy; import java.net.Socket; import java.net.URL; import java.net.URLConnection; import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import logger.LoggerFactory; import net.sbbi.upnp.impls.InternetGatewayDevice; import net.sbbi.upnp.messages.UPNPResponseException; import org.apache.log4j.Logger; import uc.Identity.FilteredChangedAttributeListener; import uc.crypto.HashValue; import uc.files.downloadqueue.AbstractDownloadQueueEntry; import uc.files.search.FileSearch; import uc.files.search.SearchType; import uc.protocols.AbstractConnection; import uc.protocols.IProtocolCommand; import uc.protocols.Socks; import uc.protocols.hub.Hub; /** * Class for determining public IP * also helps with UPnP.. so everything needed to get our client active. * To make this easier this does also some self diagnostic and tries to find out * what works wrong * * @author Quicksilver * */ public class ConnectionDeterminator extends Observable<String> implements IConnectionDeterminator { private static Logger logger = LoggerFactory.make(); private final Set<FavHub> hubsPresentingLocalIP = new HashSet<FavHub>(); private PortMapping udpActive = null; private static final int MAXConnections = 3; /** * used to check on IP correct.. */ private volatile int connectionsFailedInARow = 0; //TCP state.. private final TCPState tcp = new TCPState(PI.inPort); private final TCPState tls = new TCPState(PI.tlsPort); private static class TCPState { private final String setting; public TCPState(String settingPort) { this.setting = settingPort; } private volatile boolean tcpTested = false; /** * working if 0 or 1 * so rather the higher this is -> the less it works.. */ private volatile int connectionsWorking = 0; private PortMapping tcpActive = null; public int getPort() { return PI.getInt(setting); } public boolean isConnectionWorking() { return connectionsWorking <= 1; } } //UDP state private volatile boolean udpReceived = false; private volatile boolean udpTested = false; //UPnP private static final int LEASETIME = 3600; private volatile boolean upnpTried = false; private volatile int upnpMappingCreated = 0; private volatile boolean upnpDeviceFound = false; private volatile String upnpErrorDescription = null; // null means none.. private volatile boolean natPresent = false; private final DCClient dcc; private final Identity identity; private ScheduledFuture<?> portMapper; private final FilteredChangedAttributeListener fcal; private volatile Inet4Address current; private volatile Inet6Address ip6FoundandWorking; /** * * @return an IPv6Address .. if its tested and working.. * null otherwise.. */ public Inet6Address getIp6FoundandWorking() { return ip6FoundandWorking; } public ConnectionDeterminator(DCClient dcc,Identity identity) { this.dcc = dcc; this.identity = identity; fcal = new FilteredChangedAttributeListener(PI.inPort,PI.passive,PI.udpPort,PI.tlsPort) { @Override protected void preferenceChanged(String key, String oldValue,String newValue) { if (key.equals(PI.inPort) || key.equals(PI.tlsPort)) { TCPState s = key.equals(PI.inPort) ? tcp:tls; s.tcpTested = false; connectionsFailedInARow = 0 ; notifyObservers(); } else if(key.equals(PI.passive)) { notifyObservers(); } else if (key.equals(PI.udpPort)) { udpReceived = false; udpTested = false; notifyObservers(); } } }; } /* (non-Javadoc) * @see uc.IConnectionDeterminator#init() */ public void start() { identity.addObserver(fcal); //init settings so url connections don'Tt block too long... if (identity.isDefault()) { System.getProperties().setProperty("sun.net.client.defaultConnectTimeout", "10000") ; System.getProperties().setProperty("sun.net.client.defaultReadTimeout", "10000" ) ; } try { current = getLocalAddress(); } catch(IOException ioe) { logger.debug(ioe,ioe); } try { natPresent = (current == null) || GH.isLocaladdress(current); if (natPresent) { current = getIP(); } } catch(IOException ioe) { logger.debug(ioe,ioe); } if (current == null) { dcc.logEvent(LanguageKeys.IPDetectionFailed); try { current = (Inet4Address)InetAddress.getByName("127.0.0.1"); } catch(IOException ioe) { logger.debug(ioe,ioe); } } // if (identity.getBoolean(PI.allowIPV6)) { determineIPv6Working(); // } notifyObservers(); //periodical checking if active port mappings are still active.. portMapper = dcc.getSchedulerDir().scheduleAtFixedRate(new Runnable() { public void run() { if (udpActive != null && !udpActive.isValid() && identity.isDefault()) { createPortmapping(false, PI.getInt(PI.udpPort),null); } if (tcp.tcpActive != null && !tcp.tcpActive.isValid()) { createPortmapping(true, tcp.getPort(),tcp); } if (tls.tcpActive != null && !tls.tcpActive.isValid()) { createPortmapping(true, tls.getPort(),tls); } } } , LEASETIME , 60, TimeUnit.SECONDS); } public void stop() { identity.deleteObserver(fcal); if (portMapper!= null) { portMapper.cancel(false); portMapper = null; } } /* (non-Javadoc) * @see uc.IConnectionDeterminator#isUdpReceived() */ public boolean isUdpReceived() { return udpReceived; } /* (non-Javadoc) * @see uc.IConnectionDeterminator#isSearchStarted() */ public boolean isSearchStarted() { return udpTested; } /* (non-Javadoc) * @see uc.IConnectionDeterminator#isNATPresent() */ public boolean isNATPresent() { return natPresent; } /* (non-Javadoc) * @see uc.IConnectionDeterminator#getState() */ public CDState getState() { Boolean tcpWorking = tcp.tcpTested ? tcp.isConnectionWorking() : null ; Boolean tlsTCPWorking = tls.tcpTested ? tls.isConnectionWorking(): null; Boolean udpWorking = udpTested || udpReceived ? udpReceived : null ; Boolean upnpWorking = upnpTried ? upnpMappingCreated != 0: null; return new CDState(natPresent,tcpWorking,tlsTCPWorking,udpWorking,upnpWorking,upnpDeviceFound,upnpErrorDescription); } /* (non-Javadoc) * @see uc.IConnectionDeterminator#connectionReceived(boolean) */ public void connectionReceived(boolean encrypted) { TCPState s = encrypted ? tls: tcp; if (connectionsFailedInARow != 0 || s.connectionsWorking != 0 ) { connectionsFailedInARow = 0; s.connectionsWorking = 0; notifyObservers(); } if (!s.tcpTested) { s.tcpTested = true; notifyObservers(); } } /* (non-Javadoc) * @see uc.IConnectionDeterminator#connectionTimedOut(uc.IUser, boolean) */ public void connectionTimedOut(IUser usr,boolean encryption) { TCPState s = encryption ? tls: tcp; s.tcpTested = true; connectionsFailedInARow++; logger.debug("connections failed: "+connectionsFailedInARow); if (connectionsFailedInARow % MAXConnections == 0) { requestUserIP(); s.connectionsWorking++; notifyObservers(); if (s.connectionsWorking == 2 ) { //Try UPnP if connections are not working.. createPortmapping(true, s.getPort() ,s); } } } /* (non-Javadoc) * @see uc.IConnectionDeterminator#searchStarted(uc.files.search.FileSearch) */ public void searchStarted(FileSearch search) { if (search.getSearchType().equals(SearchType.TTH) && HashValue.isHash(search.getSearchString())) { HashValue hash = HashValue.createHash(search.getSearchString()) ; AbstractDownloadQueueEntry adqe = dcc.getDownloadQueue().get(hash); if (adqe != null && adqe.getNrOfOnlineUsers() > 0 ) { udpTested = true; if (!udpReceived) { dcc.getSchedulerDir().schedule(new Runnable() { public void run() { notifyObservers(); if (!udpReceived && identity.isDefault()) { //only default identity has separate TCP createPortmapping(false, PI.getInt(PI.udpPort),null); } } },10,TimeUnit.SECONDS); } } } } /* (non-Javadoc) * @see uc.IConnectionDeterminator#udpPacketReceived(java.net.InetSocketAddress) */ public void udpPacketReceived(InetSocketAddress from) { connectionsFailedInARow = 0; if (!udpReceived) { udpReceived = true; notifyObservers(); } } /* (non-Javadoc) * @see uc.IConnectionDeterminator#userIPReceived(java.net.InetAddress, uc.FavHub) */ public synchronized void userIPReceived(InetAddress ourIPAddress, FavHub whoTold) { if (!GH.isLocaladdress(ourIPAddress) && ourIPAddress instanceof Inet4Address) { if (!ourIPAddress.equals(current)) { current = (Inet4Address)ourIPAddress; dcc.logEvent(String.format(LanguageKeys.NewPublicIP,current.getHostAddress())); connectionsFailedInARow = 0; notifyObservers(); } hubsPresentingLocalIP.remove(whoTold); } else { hubsPresentingLocalIP.add(whoTold); } } private synchronized void requestUserIP() { List<Hub> hubs = new ArrayList<Hub>(dcc.getHubs().values()); Collections.shuffle(hubs); boolean ipRequested = false; for (Hub h: hubs) { if (h.supportsUserIP() && !hubsPresentingLocalIP.contains(h.getFavHub())) { h.requestUserIP(); ipRequested = true; break; } } if (!ipRequested) { requestIPOverWeb(); } } /* (non-Javadoc) * @see uc.IConnectionDeterminator#getPublicIP() */ public Inet4Address getPublicIP() { String ip = identity.get(PI.externalIp); if (GH.isEmpty(ip)) { return getDetectedIP(); } else { try { InetAddress ia = InetAddress.getByName(ip); if (ia instanceof Inet4Address) { return (Inet4Address)ia; } } catch(UnknownHostException uhe) { logger.warn(uhe.toString() + " : "+ip,uhe); } return getDetectedIP(); } } /* (non-Javadoc) * @see uc.IConnectionDeterminator#getDetectedIP() */ public Inet4Address getDetectedIP() { return current; } private void requestIPOverWeb() { try { InetAddress ia = getIP(); userIPReceived(ia,null); } catch (IOException ioe) { dcc.logEvent(String.format(LanguageKeys.IPWebDetectionFailed,ioe)); } } /** * * @return IP as determined by the web... * @throws IOException */ private Inet4Address getIP() throws IOException { List<String> urls = new ArrayList<String>( Arrays.asList(PI.get(PI.failOverDetection).split(Pattern.quote(";")) )); Collections.shuffle(urls); urls.add(0, PI.get(PI.defaultIPDetection)); Pattern p = Pattern.compile("("+IProtocolCommand.IPv4+")"); for (String url:urls) { logger.debug("trying: "+url); BufferedReader br = null; try { URL u = new URL(url); URLConnection uc; InetAddress ia = AbstractConnection.getBindAddress(identity.get(PI.bindAddress)); if (!Socks.isEnabled() && ia != null) { //if no proxy is enabled use this trick to bind the address.. Proxy proxy = new Proxy(Proxy.Type.DIRECT, new InetSocketAddress( ia, 0)); uc = u.openConnection(proxy); } else { uc = u.openConnection(); } br = new BufferedReader(new InputStreamReader(uc.getInputStream())); String line; while ((line = br.readLine()) != null) { Matcher m = p.matcher(line); if (m.find()) { String ip = m.group(1); dcc.logEvent(String.format(LanguageKeys.CDDeterminedIPOverWeb,ip)); Inet4Address ia4=(Inet4Address)InetAddress.getByName(ip); return ia4; } } logger.debug("no ip found for url: "+url); } catch(IOException ioe) { logger.debug("failed on url: "+url); } finally { GH.close(br); } } throw new IOException("No Detection Service available"); } private void determineIPv6Working() { Socket s = null; try { String address = "ipv6.google.com"; // sue google to check for ip6.. s = new Socket(); s.connect(new InetSocketAddress(address, 80), 250); InetAddress ia = s.getLocalAddress(); if (ia instanceof Inet6Address) { this.ip6FoundandWorking = (Inet6Address)ia; dcc.logEvent(String.format(LanguageKeys.IPv6PublicIPFound, ia)); // "IPv6 public IP used: "+ia); } s.close(); } catch(UnknownHostException uhe) { dcc.logEvent(LanguageKeys.IPv6DetectionFailed); } catch (IOException e) { dcc.logEvent(LanguageKeys.IPv6DetectionFailed); } finally { GH.close(s); } } /** * mechanism for retrieving own public IF .. if the computer has * an interface with WAN access * @return The first public IPv4! address that can be found * or a local if none * * @throws IOException */ private static Inet4Address getLocalAddress() throws IOException { InetAddress addy = null; for (Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();e.hasMoreElements();) { NetworkInterface next = e.nextElement(); for (Enumeration<InetAddress> x = next.getInetAddresses(); x.hasMoreElements();) { addy = x.nextElement(); if (!GH.isLocaladdress(addy) && addy instanceof Inet4Address) { return (Inet4Address)addy; } } } addy = InetAddress.getLocalHost(); if (addy instanceof Inet4Address) { return (Inet4Address)addy; } return null; } public boolean isExternalIPSetByHand() { return !GH.isEmpty(identity.get(PI.externalIp)); } /** * little token that hold information on the connection * * @author Quicksilver * */ public class CDState { private final boolean natPresent; /** * ternary logic null == unknown */ private final Boolean tcpWorking; private final Boolean tlsTcpWorking; /** * ternary logic null == unknown */ private final Boolean udpWorking; /** * ternary logic null == unknown */ private final Boolean uPnPWorking; private final boolean upnpDeviceFound; private final String upnpErrorDescription; public CDState(boolean natPresent, Boolean tcpWorking,Boolean tlsTcpWorking,Boolean udpWorking,Boolean uPnPWorking, boolean upnpDeviceFound,String upnpErrorDescription) { this.tlsTcpWorking = tlsTcpWorking; this.natPresent = natPresent; this.tcpWorking = tcpWorking; this.udpWorking = udpWorking; this.uPnPWorking = uPnPWorking; this.upnpDeviceFound = upnpDeviceFound; this.upnpErrorDescription = upnpErrorDescription; } /** * * @return all is working - 1 some components working - 2-3 nothing working */ public int getWarningState() { return (Boolean.FALSE.equals(getTcpWorking()) ? 1 : 0) + (Boolean.FALSE.equals(getTLSWorking()) ? 1 : 0) + (Boolean.FALSE.equals(getUdpWorking()) ? 1 : 0); } /** * gives a description for a user * that should help if something is wrong * and tell what he can do or not do * will be empty if no problem exists. */ public String getProblemSolution() { if (Boolean.FALSE.equals(tcpWorking) || Boolean.FALSE.equals(udpWorking)|| Boolean.FALSE.equals(tlsTcpWorking) ) { String s = ""; if (natPresent) { String ports = ""; boolean beforeExists = false; if (Boolean.FALSE.equals(tcpWorking)) { ports += String.format(LanguageKeys.CDCheckTCP,identity.getInt(PI.inPort)); beforeExists = true; } if (Boolean.FALSE.equals(tcpWorking) && Boolean.FALSE.equals(udpWorking)) { ports += " & "; } if (Boolean.FALSE.equals(udpWorking)) { ports += String.format(LanguageKeys.CDCheckUDP,PI.getInt(PI.udpPort)); beforeExists = true; } if (beforeExists && Boolean.FALSE.equals(tlsTcpWorking) ) { ports += " & "; } if (Boolean.FALSE.equals(tlsTcpWorking)) { ports += String.format(LanguageKeys.CDCheckTCP,PI.getInt(PI.tlsPort)); } s = String.format(LanguageKeys.CDCheckForwardedPorts,ports); if (Boolean.FALSE.equals(uPnPWorking)) { if (!GH.isNullOrEmpty(upnpErrorDescription)) { s += "\n"+upnpErrorDescription; } else if (upnpDeviceFound) { s += LanguageKeys.CDUPnPNotWorking; } else { s += LanguageKeys.CDUPnPNotPresent; } } } s += LanguageKeys.CDCheckFirewall; return s; } return ""; } public Boolean getTcpWorking() { return tcpWorking; } public Boolean getTLSWorking() { return tlsTcpWorking; } public Boolean getUdpWorking() { return udpWorking; } public boolean isNatPresent() { return natPresent; } public Boolean getUPnPWorking() { return uPnPWorking; } } /** * creates Portmapping if allowed/possible * * @param tcp * @param port * @param state */ private void createPortmapping(final boolean tcp,final int port,final TCPState state) { if (tcp? (state.tcpActive == null || !state.tcpActive.isValid()) : (udpActive == null || !udpActive.isValid())) { if (identity.getBoolean(PI.allowUPnP) && natPresent && identity.isActive()) { new Thread(new Runnable() { public void run() { createPortmapping(port , tcp,state); } },"Create Port Mapping").start(); } } } /** * try to map a port to the current computer * @param portnumber - which port to map * @param tcp - true means TCP/false a UDP mapping * @return true if the mapping was created successful */ private boolean createPortmapping(int portnumber, boolean tcp,TCPState state) { upnpTried = true; boolean mapped = false; int discoveryTimeout = 1000; // 5 secs to receive a response from devices try { logger.debug( "checking devices"); InternetGatewayDevice[] IGDs = InternetGatewayDevice.getDevices( discoveryTimeout ); if ( IGDs != null && IGDs.length > 0 ) { upnpDeviceFound = true; InternetGatewayDevice igd = IGDs[0]; logger.debug( "Found device " + igd.getIGDRootDevice().getModelName() ); // now let's open the port String localHostIP = InetAddress.getLocalHost().getHostAddress(); logger.debug("localhost: "+localHostIP); final Thread t = Thread.currentThread(); ScheduledFuture<?> fut = dcc.getSchedulerDir().schedule(new Runnable() { @SuppressWarnings("deprecation") public void run() { logger.debug("interrupting"); t.stop(new IllegalStateException()); // sadly no other chance to do this.. } },40,TimeUnit.SECONDS); logger.debug("Cancelled Portmapping: "+cancelPortmapping(tcp,state)); try { mapped = igd.addPortMapping( "jucy port forward "+portnumber, null, portnumber, portnumber, localHostIP, LEASETIME, getTCPStr(tcp )); } catch(IllegalStateException ie) { mapped = false; } fut.cancel(false); if (mapped) { setActive(tcp,new PortMapping(portnumber,tcp,igd),state); upnpMappingCreated++; dcc.logEvent(LanguageKeys.CreatedPortmapping); upnpErrorDescription = null; } else { dcc.logEvent(LanguageKeys.CreatingPortmappingFailed); } logger.debug("mapped: "+mapped+" "); } else { upnpDeviceFound = false; } } catch ( IOException ex ) { String err = LanguageKeys.CreatingPortmappingFailed+": "+ (ex.getMessage() != null?ex.getMessage():ex.toString()); logger.debug(err , ex); upnpErrorDescription = err; // some IO Exception occurred during communication with device } catch( UPNPResponseException respEx ) { String err = LanguageKeys.CreatingPortmappingFailed+": "+ (respEx.getMessage() != null?respEx.getMessage():respEx.toString()); logger.debug(err, respEx); upnpErrorDescription = err; } notifyObservers(); return mapped; } private static String getTCPStr(boolean tcp) { return tcp ? "TCP" : "UDP"; } private void setActive(boolean tcp,PortMapping pm,TCPState state) { if (tcp) { state.tcpActive = pm; } else { udpActive = pm; } } private PortMapping getPM(boolean tcp,TCPState ts) { return tcp? ts.tcpActive : udpActive; } private boolean cancelPortmapping(boolean tcp,TCPState ts) throws UPNPResponseException , IOException{ PortMapping pm = getPM(tcp,ts); if (pm != null) { boolean cancel = pm.cancel(); if (cancel) { setActive(tcp, null,ts); } return cancel; } return false; } private static class PortMapping { private final int portnumber; private final boolean tcp; private final InternetGatewayDevice igd; private final long creationTime; public PortMapping(int portnumber, boolean tcp,InternetGatewayDevice igd) { this.portnumber = portnumber; this.tcp = tcp; this.igd = igd; this.creationTime = System.currentTimeMillis(); } public boolean cancel() throws UPNPResponseException , IOException{ boolean unmapped = igd.deletePortMapping( null, portnumber, getTCPStr(tcp)); return unmapped; } private boolean isValid() { long timedif = System.currentTimeMillis() - creationTime; timedif /= 1000; return timedif < LEASETIME ; } } }