/*
This file is part of leafdigital leafChat.
leafChat 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.
leafChat 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 leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2011 Samuel Marshall.
*/
package com.leafdigital.net;
import java.io.IOException;
import java.net.*;
import javax.net.ssl.*;
import org.w3c.dom.Document;
import util.xml.XML;
import net.sbbi.upnp.impls.InternetGatewayDevice;
import net.sbbi.upnp.messages.UPNPResponseException;
import com.leafdigital.net.api.Network;
import com.leafdigital.prefs.api.*;
import com.leafdigital.prefsui.api.PreferencesUI;
import leafchat.core.api.*;
/** Provides support for network connections via proxies etc */
public class NetPlugin implements Plugin
{
private static final String URL_IPCHECK="https://live.leafdigital.com/leafchat-remote/ip.jsp";
private static final String HOST_LOCALCHECK="live.leafdigital.com";
private final static String PREF_CONNECTIONTYPE="connection-type";
private final static String PREFVALUE_CONNECTIONTYPE_UPNP="upnp";
private final static String PREFVALUE_CONNECTIONTYPE_MANUAL="manual";
private final static String PREFVALUE_CONNECTIONTYPE_SOCKS5="socks5";
final static int CONNECTION_UPNP=1,CONNECTION_MANUAL=2,CONNECTION_SOCKS5=3;
private final static String PREF_SOCKSHOST="socks-host";
private final static String PREF_SOCKSPORT="socks-port";
private final static String PREFDEFAULT_SOCKSPORT="1080";
private final static String PREF_SOCKSUSERNAME="socks-username";
private final static String PREF_SOCKSPASSWORD="socks-password";
// IP address details
private static final String PREF_PUBLICADDRESS="public-address";
private static final String PREF_REPORTEDVERSION="reported-version";
private static final String
PREF_LISTENPORTMIN="listen-port-min",
PREFDEFAULT_LISTENPORTMIN="50631";
private static final String
PREF_LISTENPORTMAX="listen-port-max",
PREFDEFAULT_LISTENPORTMAX="50639";
private PluginContext context;
private InternetGatewayDevice gateway=null;
private boolean checkedGateway=false;
private Preferences prefs;
private PreferencesGroup group;
private InetAddress reportedAddress;
@Override
public void init(PluginContext context, PluginLoadReporter status) throws GeneralException
{
this.context=context;
prefs=context.getSingle(Preferences.class);
group=prefs.getGroup(prefs.getPluginOwner(context.getPlugin()));
context.registerSingleton(Network.class,new NetworkSingleton(context));
PreferencesUI pui=context.getSingle(PreferencesUI.class);
pui.registerPage(this,(new ConnectionPage(context)).getPage());
// If UPnP is not configured or is set to yes, see if we have UPnP device
int type=getConnectionType();
if(type==0 || type==CONNECTION_UPNP)
{
(new Thread(new Runnable()
{
@Override
public void run()
{
if(getUPnPGateway()!=null)
group.set(PREF_CONNECTIONTYPE,PREFVALUE_CONNECTIONTYPE_UPNP);
else
group.set(PREF_CONNECTIONTYPE,PREFVALUE_CONNECTIONTYPE_MANUAL);
}
},"leafChat UPnP check thread")).start();
}
// Once per version, report the version alongside checking the public IP
// address.
String currentVersion=SystemVersion.getBuildVersion();
String reportedVersion=group.get(PREF_REPORTEDVERSION,"");
if(!reportedVersion.equals(currentVersion))
{
status.reportProgress("Checking public IP address");
context.log("Checking public IP address");
String publicAddress=getPublicAddress(currentVersion);
context.log("Public IP: "+(publicAddress==null ? "<unknown>" : publicAddress));
if(publicAddress!=null)
{
// OK, now we can check if we're behind a router
status.reportProgress("Checking local IP address");
context.log("Checking local IP address");
if(checkBehindRouter(publicAddress)) {
group.set(PREF_PUBLICADDRESS,publicAddress);
}
}
group.set(PREF_REPORTEDVERSION,currentVersion);
}
}
/** @return SOCKS server address */
String getSOCKSHost()
{
return group.get(PREF_SOCKSHOST,"");
}
/** @return SOCKS server port */
int getSOCKSPort()
{
return Integer.parseInt(group.get(PREF_SOCKSPORT,PREFDEFAULT_SOCKSPORT));
}
/** @return SOCKS username */
String getSOCKSUsername()
{
return group.get(PREF_SOCKSUSERNAME,"");
}
/** @return SOCKS password */
String getSOCKSPassword()
{
return group.get(PREF_SOCKSPASSWORD,"");
}
/** @param host Host of SOCKS server */
void setSOCKSHost(String host)
{
group.set(PREF_SOCKSHOST,host,"");
}
/** @param port Port of SOCKS server */
void setSOCKSPort(int port)
{
group.set(PREF_SOCKSPORT,port+"",PREFDEFAULT_SOCKSPORT);
}
/** @param username Username */
void setSOCKSUsername(String username)
{
group.set(PREF_SOCKSUSERNAME,username,"");
}
/** @param password Password */
void setSOCKSPassword(String password)
{
group.set(PREF_SOCKSPASSWORD,password,"");
}
/** @return The connection type as CONNECTION_xx constant */
int getConnectionType()
{
String type=group.get(PREF_CONNECTIONTYPE,"");
if(type.equals(PREFVALUE_CONNECTIONTYPE_UPNP))
return CONNECTION_UPNP;
if(type.equals(PREFVALUE_CONNECTIONTYPE_MANUAL))
return CONNECTION_MANUAL;
if(type.equals(PREFVALUE_CONNECTIONTYPE_SOCKS5))
return CONNECTION_SOCKS5;
return 0;
}
/**
* Sets connection type and stores in preferences.
* @param type CONNECTION_xx constant
*/
void setConnectionType(int type)
{
switch(type)
{
case CONNECTION_UPNP:
group.set(PREF_CONNECTIONTYPE,PREFVALUE_CONNECTIONTYPE_UPNP);
break;
case CONNECTION_MANUAL:
group.set(PREF_CONNECTIONTYPE,PREFVALUE_CONNECTIONTYPE_MANUAL);
break;
case CONNECTION_SOCKS5:
group.set(PREF_CONNECTIONTYPE,PREFVALUE_CONNECTIONTYPE_SOCKS5);
break;
default:
throw new IllegalArgumentException();
}
}
/**
* Attempts to enable UPnP and sets the preference to the relevant value.
*/
private synchronized void findGateway()
{
checkedGateway=true;
try
{
long start=System.currentTimeMillis();
InternetGatewayDevice[] IGDs = InternetGatewayDevice.getDevices(-1);
long done=System.currentTimeMillis();
String time=" (discovery took "+(done-start)+" ms)";
if(IGDs!=null)
{
// Yay, got one
gateway=IGDs[0];
context.log("Found UPnP gateway: "+gateway.getIGDRootDevice().getModelName()+time);
try
{
setReportedAddress(InetAddress.getByName(gateway.getExternalIPAddress()),true);
}
catch(IOException e)
{
}
catch(UPNPResponseException e)
{
}
return;
}
else
{
context.log("No UPnP gateway"+time);
}
}
catch (IOException e)
{
context.log("Error searching for UPnP gateway",e);
}
gateway=null;
}
/** @return UPnP device if available, otherwise null */
synchronized InternetGatewayDevice getUPnPGateway()
{
if(!checkedGateway) findGateway();
return gateway;
}
/**
* Makes a connection to the Web server on live.leafdigital.com and checks
* whether the local source address is the same as the public reported one.
* @param expectedAddress Known public address
* @return True if user is behind router
*/
private boolean checkBehindRouter(String expectedAddress)
{
try
{
Socket s=new Socket();
s.setSoTimeout(10000);
s.connect(new InetSocketAddress(HOST_LOCALCHECK,80));
String gotAddress=s.getLocalAddress().getHostAddress();
boolean behindRouter=!gotAddress.equals(expectedAddress);
if(!behindRouter) setReportedAddress(s.getLocalAddress(),false);
return behindRouter;
}
catch(Exception e)
{
return false; // Er, don't know
}
}
/**
* Obtains public IP address of this computer via a Web service on
* live.leafdigital.com.
* @param currentVersion Software version. Added to end of URL.
* @return IP address as string
*/
private String getPublicAddress(String currentVersion)
{
try
{
// This uses HTTPS because it's likely to get a direct connection
// and none of this proxy bull.
URL u=new URL(URL_IPCHECK+"?version="+currentVersion);
TrustManager[] trustAllCerts = new TrustManager[]
{
new X509TrustManager()
{
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers()
{
return null;
}
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType)
{
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType)
{
}
}
};
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
SSLSocketFactory before=HttpsURLConnection.getDefaultSSLSocketFactory();
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection connection=(HttpsURLConnection)u.openConnection();
HttpsURLConnection.setDefaultSSLSocketFactory(before);
Document d=XML.parse(connection.getInputStream());
return XML.getChildText(d.getDocumentElement(),"address");
}
catch(Exception e)
{
return null;
}
}
/** @return The address user has set in preferences */
String getManualPublicAddress()
{
return group.get(PREF_PUBLICADDRESS,null);
}
/**
* Set public address manually.
* @param address New address or null for none
*/
void setManualPublicAddress(String address)
{
if(address==null)
group.unset(PREF_PUBLICADDRESS);
else
group.set(PREF_PUBLICADDRESS,address);
}
/**
* Sets reported address. The address will only actually be set if the
* parameter is a valid public address.
* @param ia Address as local end of any connection
* @param replace If true, replaces any existing address
*/
void setReportedAddress(InetAddress ia,boolean replace)
{
if((reportedAddress==null || replace) && isValidAddress(ia)) reportedAddress=ia;
}
/**
* @param ia Internet address being checked
* @return True if address is a valid public address
*/
static boolean isValidAddress(InetAddress ia)
{
return !(ia.isLoopbackAddress() || ia.isLinkLocalAddress() || ia.isSiteLocalAddress());
}
/**
* @return The address that has been reported as a local end to connections
* from this system, if any
*/
InetAddress getReportedAddress()
{
return reportedAddress;
}
int getListenPortMin()
{
return Integer.parseInt(group.get(PREF_LISTENPORTMIN,PREFDEFAULT_LISTENPORTMIN));
}
int getListenPortMax()
{
return Integer.parseInt(group.get(PREF_LISTENPORTMAX,PREFDEFAULT_LISTENPORTMAX));
}
void setListenPorts(int min,int max)
{
group.set(PREF_LISTENPORTMIN,min+"");
group.set(PREF_LISTENPORTMAX,max+"");
}
@Override
public void close() throws GeneralException
{
}
@Override
public String toString()
{
return "Network plugin";
}
}