/*
* Created on 29-Dec-2004
* Created by Paul Gardner
* Copyright (C) 2004, 2005, 2006 Aelitis, All Rights Reserved.
*
* 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 2
* 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, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*
* AELITIS, SAS au capital de 46,603.30 euros
* 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
*
*/
package org.gudy.azureus2.pluginsimpl.local.clientid;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.*;
import org.gudy.azureus2.core3.logging.*;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.util.AEThread;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.ThreadPool;
import org.gudy.azureus2.core3.util.ThreadPoolTask;
import org.gudy.azureus2.plugins.PluginManager;
import org.gudy.azureus2.plugins.PluginManagerDefaults;
import org.gudy.azureus2.plugins.clientid.ClientIDException;
import org.gudy.azureus2.plugins.clientid.ClientIDGenerator;
import org.gudy.azureus2.plugins.clientid.ClientIDManager;
import org.gudy.azureus2.pluginsimpl.local.torrent.TorrentImpl;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;
/**
* @author parg
*
*/
public class
ClientIDManagerImpl
implements ClientIDManager
{
private static final LogIDs LOGID = LogIDs.PLUGIN;
protected static ClientIDManagerImpl singleton = new ClientIDManagerImpl();
protected static final char CR = '\015';
protected static final char FF = '\012';
protected static final String NL = "\015\012";
public static ClientIDManagerImpl
getSingleton()
{
return( singleton );
}
private ClientIDGenerator generator_user_accessor;
private boolean use_filter;
private boolean filter_override;
private ThreadPool thread_pool;
private int filter_port;
public void
setGenerator(
ClientIDGenerator _generator,
boolean _use_filter )
{
// I wanted to allow signed plugins the ability to do this but given that a malicious
// plugin can use reflection to get access to fields (such as the URL field of a
// URLClassLoader) I can't see a way to enforce this. That is, how can you verify
// that the class was loaded from a signed jar? you can get the jar that the URLClassLoader
// claims it was loaded from and verify that, but this jar location may have been changed
// by the plugin. you can look inside the signed jar and check that there's a class in
// there with the right name, implementing ClientIDGenerator, but this doesn't prove
// that the implementation passed to this method is the same as once an offical signed
// plugin is released that uses this feature (with, say, a class called a.b.c.X as the
// generator), a malicious plugin can simply also implement a class a.b.c.X, ship
// along with a copy of the official jar, hack the class-loader after loading to make
// the class-loader point to the official jar. The only things that can't be changed
// by reflection are static final fields which don't seem to help. We could modify
// our security manager to trap a checkAccess perm check but we don't have access to
// the thing being modified and this is used in various other places to work around bugs.
// So we only accept generators loaded by non-plugin loaders. Note that you can't
// change a class's class loader so this works.
// we might be able to fix things by using some native storage that can't be modified
// by a plugin, or by getting this code to load/instantiate the class, but you still
// have the problem that the plugin can directly modify the "generator" field. Another
// fix would be to enhance the security manager and provide methods to wrap the
// setAccessible operations so we can control which objects are accessible
checkGenerator( _generator );
generator_user_accessor = _generator;
use_filter = _use_filter;
// we override the filter parameter here if we have a local bind IP set as
// this is the only simple solution to enforcing the local bind (Sun's
// HTTPConnection doesn't allow the network interface to be bound)
if ( !use_filter ){
// another reason for NOT doing this is if the user has a defined proxy
// in this case the assumption is that they know what they're doing and
// the proxy will be bound correctly to ensure that things work...
String http_proxy = System.getProperty( "http.proxyHost" );
String socks_proxy = System.getProperty( "socksProxyHost" );
InetAddress bindIP = NetworkAdmin.getSingleton().getSingleHomedServiceBindAddress();
if ( ( http_proxy == null || http_proxy.trim().length() == 0 ) &&
( socks_proxy == null || socks_proxy.trim().length() == 0 ) &&
(bindIP != null && !bindIP.isAnyLocalAddress())
)
{
int ips = 0;
// seeing as this is a bit of a crappy way to enforce binding, add one more check to make
// sure that the machine has multiple ips before going ahead in case user has set it
// incorrectly
try{
Enumeration nis = NetworkInterface.getNetworkInterfaces();
while( nis.hasMoreElements()){
NetworkInterface ni = (NetworkInterface)nis.nextElement();
Enumeration addresses = ni.getInetAddresses();
while( addresses.hasMoreElements()){
InetAddress address = (InetAddress)addresses.nextElement();
if ( !address.isLoopbackAddress()){
ips++;
}
}
}
}catch( Throwable e ){
Logger.log(new LogEvent(LOGID, "", e));
}
if ( ips > 1 ){
filter_override = true;
use_filter = true;
if (Logger.isEnabled())
Logger.log(new LogEvent(LOGID,
"ClientIDManager: overriding filter "
+ "option to support local bind IP"));
}
}
}
if ( use_filter ){
try{
thread_pool = new ThreadPool( "ClientIDManager", 32 );
String connect_timeout = System.getProperty("sun.net.client.defaultConnectTimeout");
String read_timeout = System.getProperty("sun.net.client.defaultReadTimeout");
int timeout = Integer.parseInt( connect_timeout ) + Integer.parseInt( read_timeout );
thread_pool.setExecutionLimit( timeout );
final ServerSocket ss = new ServerSocket( 0, 1024, InetAddress.getByName("127.0.0.1"));
filter_port = ss.getLocalPort();
ss.setReuseAddress(true);
Thread accept_thread =
new AEThread("ClientIDManager::filterloop")
{
public void
runSupport()
{
long successfull_accepts = 0;
long failed_accepts = 0;
while(true){
try{
Socket socket = ss.accept();
successfull_accepts++;
thread_pool.run( new httpFilter( socket ));
}catch( Throwable e ){
failed_accepts++;
if (Logger.isEnabled())
Logger.log(new LogEvent(LOGID,
"ClientIDManager: listener failed on port "
+ filter_port, e ));
if ( failed_accepts > 100 && successfull_accepts == 0 ){
// looks like its not going to work...
// some kind of socket problem
Logger.logTextResource(new LogAlert(LogAlert.UNREPEATABLE,
LogAlert.AT_ERROR, "Network.alert.acceptfail"),
new String[] { "" + filter_port, "TCP" });
use_filter = false;
break;
}
}
}
}
};
accept_thread.setDaemon( true );
accept_thread.start();
if (Logger.isEnabled())
Logger.log(new LogEvent(LOGID,
"ClientIDManager: listener established on port " + filter_port));
}catch( Throwable e){
Logger.logTextResource(new LogAlert(LogAlert.UNREPEATABLE,
LogAlert.AT_ERROR, "Tracker.alert.listenfail"), new String[] { ""
+ filter_port });
if (Logger.isEnabled())
Logger.log(new LogEvent(LOGID,
"ClientIDManager: listener failed on port " + filter_port, e));
use_filter = false;
}
}
}
public ClientIDGenerator
getGenerator()
{
checkGenerator( generator_user_accessor );
return( generator_user_accessor );
}
protected void
checkGenerator(
ClientIDGenerator gen )
{
ClassLoader cl = gen.getClass().getClassLoader();
if ( cl != null && cl != ClientIDManager.class.getClassLoader()){
// if early in the day we can try to get the default one working here
PluginManager.getDefaults().setDefaultPluginEnabled( PluginManagerDefaults.PID_CLIENT_ID, true );
Debug.out( "Generator isn't trusted - " + gen );
throw( new RuntimeException( "Generator isn't trusted" ));
}
}
public byte[]
generatePeerID(
TOTorrent torrent,
boolean for_tracker )
throws ClientIDException
{
return( getGenerator().generatePeerID( new TorrentImpl( torrent ), for_tracker ));
}
public void
generateHTTPProperties(
Properties properties )
throws ClientIDException
{
if ( use_filter ){
// to support SSL here we would need to substitute the https url with an https one
// and then drive the SSL in the filter appropriately
URL url = (URL)properties.get( ClientIDGenerator.PR_URL );
if ( !url.getProtocol().toLowerCase().equals( "http" )){
Logger.log(new LogAlert(LogAlert.UNREPEATABLE, LogAlert.AT_ERROR,
"ClientIDManager only supports filtering of http, not https"));
return;
}
try{
String url_str = url.toString();
String target_host = url.getHost();
int target_port = url.getPort();
if ( target_port == -1 ){
target_port = url.getDefaultPort();
}
int host_pos = url_str.indexOf( target_host );
String new_url = url_str.substring(0,host_pos) + "127.0.0.1:" + filter_port;
String rem = url_str.substring( host_pos + target_host.length());
if ( rem.charAt(0) == ':' ){
rem = rem.substring( (""+ target_port ).length() + 1 );
}
int q_pos = rem.indexOf( '?' );
new_url += rem.substring(0,q_pos+1) + "cid=" + target_host + ":" + target_port + "&" + rem.substring(q_pos+1);
properties.put( ClientIDGenerator.PR_URL, new URL( new_url ));
}catch( Throwable e ){
Debug.printStackTrace(e);
}
}else{
getGenerator().generateHTTPProperties( properties );
}
}
protected class
httpFilter
extends ThreadPoolTask
{
private Socket socket;
protected
httpFilter(
Socket _socket )
{
socket = _socket;
}
public void
runSupport()
{
String report_error = null;
int written = 0;
try{
setTaskState( "reading header" );
InputStream is = socket.getInputStream();
byte[] buffer = new byte[1024];
String header = "";
while(true ){
int len = is.read(buffer);
if ( len == -1 ){
break;
}
header += new String( buffer, 0, len, Constants.BYTE_ENCODING );
if ( header.endsWith( NL+NL ) ||
header.indexOf( NL+NL ) != -1 ){
break;
}
}
List lines = new ArrayList();
int pos = 0;
while( true){
int p1 = header.indexOf( NL, pos );
String line;
if ( p1 == -1 ){
line = header.substring(pos);
}else{
line = header.substring( pos, p1 );
}
line = line.trim();
if ( line.length() > 0 ){
lines.add( line );
}
if ( p1 == -1 ){
break;
}
pos = p1+2;
}
String[] lines_in = new String[ lines.size()];
lines.toArray( lines_in );
String get = lines_in[0];
int p1 = get.indexOf( "?cid=" );
int p2 = get.indexOf( "&", p1 );
String cid = get.substring( p1+5, p2 );
int p3 = cid.indexOf( ":" );
String target_host = cid.substring( 0, p3 );
int target_port = Integer.parseInt( cid.substring(p3+1));
// fix up the Host: entry with the target details
for (int i=1;i<lines_in.length;i++){
String line = lines_in[i];
if ( line.toLowerCase().indexOf( "host:" ) != -1 ){
lines_in[i] = "Host: " + target_host + ":" + target_port;
break;
}
}
get = get.substring( 0, p1+1 ) + get.substring( p2+1 );
lines_in[0] = get;
String[] lines_out;
if ( filter_override ){
// bodge for ip override. we still need to take account of the correct
// user-agent
lines_out = lines_in;
Properties p = new Properties();
getGenerator().generateHTTPProperties( p );
String agent = p.getProperty( ClientIDGenerator.PR_USER_AGENT );
if ( agent != null ){
for (int i=0;i<lines_out.length;i++){
if ( lines_out[i].toLowerCase().startsWith( "user-agent" )){
lines_out[i] = "User-Agent: " + agent;
}
}
}
}else{
lines_out = getGenerator().filterHTTP( lines_in );
}
String header_out = "";
for (int i=0;i<lines_out.length;i++){
header_out += lines_out[i] + NL;
}
header_out += NL;
Socket target = new Socket();
InetAddress bindIP = NetworkAdmin.getSingleton().getSingleHomedServiceBindAddress();
if ( bindIP != null ){
target.bind( new InetSocketAddress( bindIP, 0 ) );
}
// System.out.println( "filtering " + target_host + ":" + target_port );
target.connect( new InetSocketAddress( target_host, target_port ));
target.getOutputStream().write( header_out.getBytes(Constants.BYTE_ENCODING ));
target.getOutputStream().flush();
InputStream target_is = target.getInputStream();
while( true ){
int len = target_is.read( buffer );
if ( len == -1 ){
break;
}
socket.getOutputStream().write( buffer, 0,len );
written += len;
}
}catch( ClientIDException e ){
report_error = e.getMessage();
}catch( UnknownHostException e ){
report_error = "Unknown host '" + e.getMessage() + "'";
}catch( Throwable e ){
// Debug.printStackTrace(e);
}finally{
if ( report_error != null && written == 0 ){
Map failure = new HashMap();
failure.put("failure reason", report_error );
try{
byte[] x = BEncoder.encode( failure );
socket.getOutputStream().write( x );
}catch( Throwable f ){
Debug.printStackTrace(f);
}
}
try{
socket.getOutputStream().flush();
socket.close();
}catch( Throwable f ){
}
}
}
public void
interruptTask()
{
try{
/*
if (Logger.isEnabled())
Logger.log(new LogEvent(LOGID, "ClientIDManager - interrupting "
+ "HTTP filter due to timeout"));
*/
socket.close();
}catch( Throwable e ){
}
}
}
}