/*
* Created on 14-Jun-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 com.aelitis.net.upnp.impl;
/**
* @author parg
*
*/
import java.util.*;
import java.net.*;
import java.io.*;
import com.aelitis.azureus.core.util.Java15Utils;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.ThreadPool;
import org.gudy.azureus2.core3.util.TorrentUtils;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloader;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderAdapter;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderException;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderFactory;
import org.gudy.azureus2.plugins.utils.xml.simpleparser.SimpleXMLParserDocument;
import org.gudy.azureus2.plugins.utils.xml.simpleparser.SimpleXMLParserDocumentException;
import com.aelitis.azureus.core.util.HTTPUtils;
import com.aelitis.net.upnp.*;
import com.aelitis.net.upnp.impl.device.*;
public class
UPnPImpl
extends ResourceDownloaderAdapter
implements UPnP, SSDPIGDListener
{
public static final String NL = "\r\n";
private static UPnPImpl singleton;
private static AEMonitor class_mon = new AEMonitor( "UPnP:class" );
public static UPnP
getSingleton(
UPnPAdapter adapter,
String[] selected_interfaces )
throws UPnPException
{
try{
class_mon.enter();
if ( singleton == null ){
singleton = new UPnPImpl( adapter, selected_interfaces );
}
return( singleton );
}finally{
class_mon.exit();
}
}
private UPnPAdapter adapter;
private SSDPIGD ssdp;
private Map<String,UPnPRootDeviceImpl> root_locations = new HashMap<String, UPnPRootDeviceImpl>();
private List log_listeners = new ArrayList();
private List log_history = new ArrayList();
private List log_alert_history = new ArrayList();
private List rd_listeners = new ArrayList();
private AEMonitor rd_listeners_mon = new AEMonitor( "UPnP:L" );
private int http_calls_ok = 0;
private int direct_calls_ok = 0;
private int trace_index = 0;
private ThreadPool device_dispatcher = new ThreadPool("UPnPDispatcher", 1, true );
private Set device_dispatcher_pending = new HashSet();
private Map<String,long[]> failed_urls = new HashMap<String,long[]>();
protected AEMonitor this_mon = new AEMonitor( "UPnP" );
protected
UPnPImpl(
UPnPAdapter _adapter,
String[] _selected_interfaces )
throws UPnPException
{
adapter = _adapter;
ssdp = SSDPIGDFactory.create( this, _selected_interfaces );
ssdp.addListener(this);
ssdp.start();
}
public UPnPSSDP
getSSDP()
{
return( ssdp.getSSDP());
}
public void
injectDiscoveryCache(
Map cache )
{
try{
String ni_s = new String((byte[])cache.get( "ni" ), "UTF-8" );
String la_s = new String((byte[])cache.get( "la" ), "UTF-8" );
String usn = new String((byte[])cache.get( "usn" ), "UTF-8" );
String loc_s = new String((byte[])cache.get( "loc" ), "UTF-8" );
NetworkInterface network_interface = NetworkInterface.getByName( ni_s );
if ( network_interface == null ){
return;
}
InetAddress local_address = InetAddress.getByName( la_s );
URL location = new URL( loc_s );
rootDiscovered( network_interface, local_address, usn, location );
}catch( Throwable e ){
Debug.out( e );
}
}
public void
rootDiscovered(
final NetworkInterface network_interface,
final InetAddress local_address,
final String usn,
final URL location )
{
// we need to take this operation off the main thread as it can take some time. This is a single
// concurrency queued thread pool so things get done serially in the right order
try{
rd_listeners_mon.enter();
if ( device_dispatcher_pending.contains( usn )){
// System.out.println( "UPnP: skipping discovery of " + usn + " as already pending (queue=" + device_dispatcher_pending.size() + ")" );
return;
}
if ( device_dispatcher_pending.size() > 512 ){
Debug.out( "Device dispatcher queue is full - dropping discovery of " + usn + "/" + location );
}
device_dispatcher_pending.add( usn );
}finally{
rd_listeners_mon.exit();
}
device_dispatcher.run(
new AERunnable()
{
public void
runSupport()
{
final UPnPRootDeviceImpl old_root_device;
try{
rd_listeners_mon.enter();
old_root_device = (UPnPRootDeviceImpl)root_locations.get( usn );
device_dispatcher_pending.remove( usn );
}finally{
rd_listeners_mon.exit();
}
if ( old_root_device != null ){
// we remember one route to the device - if the network interfaces change
// we do a full reset so we don't need to deal with that here
if ( !old_root_device.getNetworkInterface().getName().equals( network_interface.getName())){
return;
}
// check that the device's location is the same
if ( old_root_device.getLocation().equals( location )){
return;
}
}
if ( old_root_device != null ){
// something changed, resetablish everything
try{
// not the best "atomic" code here but it'll do as the code that adds roots (this)
// is single threaded via the dispatcher
rd_listeners_mon.enter();
root_locations.remove( usn );
}finally{
rd_listeners_mon.exit();
}
old_root_device.destroy( true );
}
List listeners;
try{
rd_listeners_mon.enter();
listeners = new ArrayList( rd_listeners );
}finally{
rd_listeners_mon.exit();
}
for (int i=0;i<listeners.size();i++){
try{
if ( !((UPnPListener)listeners.get(i)).deviceDiscovered( usn, location )){
return;
}
}catch( Throwable e ){
Debug.printStackTrace(e);
}
}
log( "UPnP: root discovered: usn=" + usn + ", location=" + location + ", ni=" + network_interface.getName() + ",local=" + local_address.toString() );
try{
UPnPRootDeviceImpl new_root_device = new UPnPRootDeviceImpl( UPnPImpl.this, network_interface, local_address, usn, location );
try{
rd_listeners_mon.enter();
root_locations.put( usn, new_root_device );
listeners = new ArrayList( rd_listeners );
}finally{
rd_listeners_mon.exit();
}
for (int i=0;i<listeners.size();i++){
try{
((UPnPListener)listeners.get(i)).rootDeviceFound( new_root_device );
}catch( Throwable e ){
Debug.printStackTrace(e);
}
}
}catch( UPnPException e ){
String message = e.getMessage();
String msg = message==null?Debug.getNestedExceptionMessageAndStack( e ):message;
adapter.log( msg );
}
}
});
}
public void
rootAlive(
String usn,
URL location )
{
UPnPRootDeviceImpl root_device = (UPnPRootDeviceImpl)root_locations.get( usn );
if ( root_device == null ){
ssdp.searchNow();
}
}
public void
rootLost(
final InetAddress local_address,
final String usn )
{
// we need to take this operation off the main thread as it can take some time
device_dispatcher.run(
new AERunnable()
{
public void
runSupport()
{
UPnPRootDeviceImpl root_device = null;
try{
rd_listeners_mon.enter();
root_device = (UPnPRootDeviceImpl)root_locations.remove( usn );
}finally{
rd_listeners_mon.exit();
}
if ( root_device == null ){
return;
}
log( "UPnP: root lost: usn=" + usn + ", location=" + root_device.getLocation() + ", ni=" + root_device.getNetworkInterface().getName() + ",local=" + root_device.getLocalAddress().toString());
root_device.destroy( false );
}
});
}
public void
interfaceChanged(
NetworkInterface network_interface )
{
reset();
}
public void
search()
{
ssdp.searchNow();
}
public void
search(
String[] STs )
{
ssdp.searchNow( STs );
}
public void
reset()
{
log( "UPnP: reset" );
List roots;
try{
rd_listeners_mon.enter();
roots = new ArrayList(root_locations.values());
root_locations.clear();
}finally{
rd_listeners_mon.exit();
}
for (int i=0;i<roots.size();i++){
((UPnPRootDeviceImpl)roots.get(i)).destroy( true );
}
ssdp.searchNow();
}
public SimpleXMLParserDocument
parseXML(
InputStream _is )
throws SimpleXMLParserDocumentException, IOException
{
// ASSUME UTF-8
ByteArrayOutputStream baos = null;
try{
baos = new ByteArrayOutputStream(1024);
byte[] buffer = new byte[8192];
while(true){
int len = _is.read( buffer );
if ( len <= 0 ){
break;
}
baos.write( buffer, 0, len );
}
}finally{
baos.close();
}
byte[] bytes_in = baos.toByteArray();
InputStream is = new ByteArrayInputStream( bytes_in );
// Gudy's router was returning trailing nulls which then stuffed up the
// XML parser. Hence this code to try and strip them
try{
StringBuffer data = new StringBuffer(1024);
LineNumberReader lnr = new LineNumberReader( new InputStreamReader( is, "UTF-8" ));
Set ignore_map = null;
while( true ){
String line = lnr.readLine();
if ( line == null ){
break;
}
// remove any obviously invalid characters - I've seen some routers generate stuff like
// 0x18 which stuffs the xml parser with "invalid unicode character"
for (int i=0;i<line.length();i++){
char c = line.charAt(i);
if ( c < 0x20 && c != '\r' && c != '\t' ){
data.append( ' ' );
if ( ignore_map == null ){
ignore_map = new HashSet();
}
Character cha = new Character(c);
if ( !ignore_map.contains( cha )){
ignore_map.add( cha );
adapter.trace( " ignoring character(s) " + (int)c + " in xml response" );
}
}else{
data.append( c );
}
}
data.append( "\n" );
}
String data_str = data.toString();
adapter.trace( "UPnP:Response:" + data_str );
return( adapter.parseXML( data_str ));
}catch( Throwable e ){
try{
FileOutputStream trace = new FileOutputStream( getTraceFile());
trace.write( bytes_in );
trace.close();
}catch( Throwable f ){
adapter.log(f);
}
if ( e instanceof SimpleXMLParserDocumentException ){
throw((SimpleXMLParserDocumentException)e);
}
throw( new SimpleXMLParserDocumentException(e ));
}
}
public SimpleXMLParserDocument
downloadXML(
UPnPRootDeviceImpl root,
URL url )
throws UPnPException
{
return( downloadXMLSupport( null, url ));
}
public SimpleXMLParserDocument
downloadXML(
UPnPDeviceImpl device,
URL url )
throws UPnPException
{
try{
// some devices have borked relative urls, work around
if ( device != null ){
device.restoreRelativeBaseURL();
}
return( downloadXMLSupport( device.getFriendlyName(), url ));
}catch( UPnPException e ){
if ( device != null ){
device.clearRelativeBaseURL();
return( downloadXMLSupport( device.getFriendlyName(), url ));
}
throw( e );
}
}
protected SimpleXMLParserDocument
downloadXMLSupport(
String friendly_name,
URL url )
throws UPnPException
{
String url_str = url.toExternalForm();
boolean record_failure = true;
try{
TorrentUtils.setTLSDescription( "UPnP Device" + ( friendly_name==null?"":( ": " + friendly_name )));
ResourceDownloaderFactory rdf = adapter.getResourceDownloaderFactory();
int retries;
synchronized( failed_urls ){
long[] fails = failed_urls.get( url_str );
if ( fails == null ){
retries = 3;
}else{
long consec_fails = fails[0];
long last_fail = fails[1];
long max_period = 10*60*1000;
long period = 60*1000;
for (int i=0;i<consec_fails;i++){
period <<= 1;
if ( period >= max_period ){
period = max_period;
break;
}
}
if ( SystemTime.getMonotonousTime() - last_fail < period ){
record_failure = false;
throw( new UPnPException( "Download failed too recently, ignoring" ));
}
retries = 1;
}
}
ResourceDownloader rd = rdf.getRetryDownloader( rdf.create( url, true ), retries );
rd.addListener( this );
InputStream data = rd.download();
try{
SimpleXMLParserDocument res = parseXML( data );
synchronized( failed_urls ){
failed_urls.remove( url_str );
}
return( res );
}finally{
data.close();
}
}catch( Throwable e ){
if ( record_failure ){
synchronized( failed_urls ){
if ( failed_urls.size() >= 64 ){
failed_urls.clear();
}
long[] fails = failed_urls.get( url_str );
if ( fails == null ){
fails = new long[2];
failed_urls.put( url_str, fails );
}
fails[0]++;
fails[1] = SystemTime.getMonotonousTime();
}
adapter.log( Debug.getNestedExceptionMessageAndStack(e));
}
if (e instanceof UPnPException ){
throw((UPnPException)e);
}
throw( new UPnPException( "Root device location '" + url + "' - data read failed", e ));
}finally{
TorrentUtils.setTLSDescription( null );
}
}
protected boolean
forceDirect()
{
String http_proxy = System.getProperty( "http.proxyHost" );
String socks_proxy = System.getProperty( "socksProxyHost" );
// extremely unlikely we want to proxy upnp requests
boolean force_direct = ( http_proxy != null && http_proxy.trim().length() > 0 ) ||
( socks_proxy != null && socks_proxy.trim().length() > 0 );
return( force_direct );
}
public SimpleXMLParserDocument
performSOAPRequest(
UPnPService service,
String soap_action,
String request )
throws SimpleXMLParserDocumentException, UPnPException, IOException
{
SimpleXMLParserDocument res;
if ( service.getDirectInvocations() || forceDirect()){
res = performSOAPRequest( service, soap_action, request, false );
}else{
try{
res = performSOAPRequest( service, soap_action, request, true );
http_calls_ok++;
}catch( IOException e ){
res = performSOAPRequest( service, soap_action, request, false );
direct_calls_ok++;
if ( direct_calls_ok == 1 ){
log( "Invocation via http connection failed (" + e.getMessage() + ") but socket connection succeeded" );
}
}
}
return( res );
}
/**
* The use_http_connection flag is set to false sometimes to avoid using
* the URLConnection library for some dopey UPnP routers.
*/
public SimpleXMLParserDocument
performSOAPRequest(
UPnPService service,
String soap_action,
String request,
boolean use_http_connection)
throws SimpleXMLParserDocumentException, UPnPException, IOException
{
//long start = SystemTime.getMonotonousTime();
URL control = service.getControlURL();
try{
adapter.trace( "UPnP:Request: -> " + control + "," + request );
if ( use_http_connection ){
try{
TorrentUtils.setTLSDescription( "UPnP Device: " + service.getDevice().getFriendlyName());
HttpURLConnection con1 = (HttpURLConnection)Java15Utils.openConnectionForceNoProxy(control);
con1.setRequestProperty( "SOAPAction", "\""+ soap_action + "\"");
con1.setRequestProperty( "Content-Type", "text/xml; charset=\"utf-8\"" );
con1.setRequestProperty( "User-Agent", "Azureus (UPnP/1.0)" );
con1.setRequestMethod( "POST" );
con1.setDoInput( true );
con1.setDoOutput( true );
OutputStream os = con1.getOutputStream();
PrintWriter pw = new PrintWriter( new OutputStreamWriter(os, "UTF-8" ));
pw.println( request );
pw.flush();
con1.connect();
if ( con1.getResponseCode() == 405 || con1.getResponseCode() == 500 ){
// gotta retry with M-POST method
try{
HttpURLConnection con2 = (HttpURLConnection)control.openConnection();
con2.setRequestProperty( "Content-Type", "text/xml; charset=\"utf-8\"" );
con2.setRequestMethod( "M-POST" );
con2.setRequestProperty( "MAN", "\"http://schemas.xmlsoap.org/soap/envelope/\"; ns=01" );
con2.setRequestProperty( "01-SOAPACTION", "\""+ soap_action + "\"");
con2.setDoInput( true );
con2.setDoOutput( true );
os = con2.getOutputStream();
pw = new PrintWriter( new OutputStreamWriter(os, "UTF-8" ));
pw.println( request );
pw.flush();
con2.connect();
return( parseXML(con2.getInputStream()));
}catch( Throwable e ){
}
InputStream es = con1.getErrorStream();
String info = null;
try{
info = FileUtil.readInputStreamAsString( es, 512 );
}catch( Throwable e ){
}
String error = "SOAP RPC failed: " + con1.getResponseCode() + " " + con1.getResponseMessage();
if ( info != null ){
error += " - " + info;
}
throw( new IOException ( error ));
}else{
return( parseXML(con1.getInputStream()));
}
}finally{
TorrentUtils.setTLSDescription( null );
}
}else{
final int CONNECT_TIMEOUT = 15*1000;
final int READ_TIMEOUT = 30*1000;
Socket socket = new Socket( Proxy.NO_PROXY );
socket.connect( new InetSocketAddress( control.getHost(), control.getPort()), CONNECT_TIMEOUT );
socket.setSoTimeout( READ_TIMEOUT );
try{
PrintWriter pw = new PrintWriter(new OutputStreamWriter( socket.getOutputStream(), "UTF8" ));
String url_target = control.toString();
int p1 = url_target.indexOf( "://" ) + 3;
p1 = url_target.indexOf( "/", p1 );
url_target = url_target.substring( p1 );
pw.print( "POST " + url_target + " HTTP/1.1" + NL );
pw.print( "Content-Type: text/xml; charset=\"utf-8\"" + NL );
pw.print( "SOAPAction: \"" + soap_action + "\"" + NL );
pw.print( "User-Agent: Azureus (UPnP/1.0)" + NL );
pw.print( "Host: " + control.getHost() + NL );
pw.print( "Content-Length: " + request.getBytes( "UTF8" ).length + NL );
pw.print( "Connection: Keep-Alive" + NL );
pw.print( "Pragma: no-cache" + NL + NL );
pw.print( request );
pw.flush();
InputStream is = HTTPUtils.decodeChunkedEncoding( socket );
return( parseXML( is ));
}finally{
try{
socket.close();
}catch( Throwable e ){
Debug.printStackTrace(e);
}
}
}
}finally{
//System.out.println( "UPnP: invocation of " + control + "/" + soap_action + " took " + ( SystemTime.getMonotonousTime() - start ));
}
}
protected File
getTraceFile()
{
try{
this_mon.enter();
trace_index++;
if ( trace_index == 6 ){
trace_index = 1;
}
return( new File( adapter.getTraceDir(), "upnp_trace" + trace_index + ".log" ));
}finally{
this_mon.exit();
}
}
public UPnPAdapter
getAdapter()
{
return( adapter );
}
public void
reportActivity(
ResourceDownloader downloader,
String activity )
{
log( activity );
}
public void
failed(
ResourceDownloader downloader,
ResourceDownloaderException e )
{
log( e );
}
public void
log(
Throwable e )
{
log( e.toString());
}
public void
log(
String str )
{
List old_listeners;
try{
this_mon.enter();
old_listeners = new ArrayList(log_listeners);
log_history.add( str );
if ( log_history.size() > 32 ){
log_history.remove(0);
}
}finally{
this_mon.exit();
}
for (int i=0;i<old_listeners.size();i++){
((UPnPLogListener)old_listeners.get(i)).log( str );
}
}
public void
logAlert(
String str,
boolean error,
int type )
{
List old_listeners;
try{
this_mon.enter();
old_listeners = new ArrayList(log_listeners);
log_alert_history.add(new Object[]{ str, new Boolean( error ), new Integer( type )});
if ( log_alert_history.size() > 32 ){
log_alert_history.remove(0);
}
}finally{
this_mon.exit();
}
for (int i=0;i<old_listeners.size();i++){
((UPnPLogListener)old_listeners.get(i)).logAlert( str, error, type );
}
}
public void
addLogListener(
UPnPLogListener l )
{
List old_logs;
List old_alerts;
try{
this_mon.enter();
old_logs = new ArrayList(log_history);
old_alerts = new ArrayList(log_alert_history);
log_listeners.add( l );
}finally{
this_mon.exit();
}
for (int i=0;i<old_logs.size();i++){
l.log((String)old_logs.get(i));
}
for (int i=0;i<old_alerts.size();i++){
Object[] entry = (Object[])old_alerts.get(i);
l.logAlert((String)entry[0], ((Boolean)entry[1]).booleanValue(), ((Integer)entry[2]).intValue());
}
}
public void
removeLogListener(
UPnPLogListener l )
{
log_listeners.remove( l );
}
public UPnPRootDevice[]
getRootDevices()
{
try{
this_mon.enter();
return( root_locations.values().toArray( new UPnPRootDevice[ root_locations.size()] ));
}finally{
this_mon.exit();
}
}
public void
addRootDeviceListener(
UPnPListener l )
{
List old_locations;
try{
this_mon.enter();
old_locations = new ArrayList(root_locations.values());
rd_listeners.add( l );
}finally{
this_mon.exit();
}
for (int i=0;i<old_locations.size();i++){
UPnPRootDevice device = (UPnPRootDevice)old_locations.get(i);
try{
if ( l.deviceDiscovered( device.getUSN(), device.getLocation())){
l.rootDeviceFound(device);
}
}catch( Throwable e ){
Debug.printStackTrace(e);
}
}
}
public void
removeRootDeviceListener(
UPnPListener l )
{
try{
this_mon.enter();
rd_listeners.remove( l );
}finally{
this_mon.exit();
}
}
/*
public static void
main(
String[] args )
{
try{
UPnP upnp = UPnPFactory.getSingleton(null,null); // won't work with null ....
upnp.addRootDeviceListener(
new UPnPListener()
{
public boolean
deviceDiscovered(
String USN,
URL location )
{
return( true );
}
public void
rootDeviceFound(
UPnPRootDevice device )
{
try{
processDevice( device.getDevice() );
}catch( Throwable e ){
e.printStackTrace();
}
}
});
upnp.addLogListener(
new UPnPLogListener()
{
public void
log(
String str )
{
System.out.println( str );
}
public void
logAlert(
String str,
boolean error,
int type )
{
System.out.println( str );
}
});
Thread.sleep(20000);
}catch( Throwable e ){
e.printStackTrace();
}
}
protected static void
processDevice(
UPnPDevice device )
throws UPnPException
{
if ( device.getDeviceType().equalsIgnoreCase("urn:schemas-upnp-org:device:WANConnectionDevice:1")){
System.out.println( "got device");
UPnPService[] services = device.getServices();
for (int i=0;i<services.length;i++){
UPnPService s = services[i];
if ( s.getServiceType().equalsIgnoreCase( "urn:schemas-upnp-org:service:WANIPConnection:1")){
System.out.println( "got service" );
UPnPAction[] actions = s.getActions();
for (int j=0;j<actions.length;j++){
System.out.println( actions[j].getName());
}
UPnPStateVariable[] vars = s.getStateVariables();
for (int j=0;j<vars.length;j++){
System.out.println( vars[j].getName());
}
UPnPStateVariable noe = s.getStateVariable("PortMappingNumberOfEntries");
System.out.println( "noe = " + noe.getValue());
UPnPWANIPConnection wan_ip = (UPnPWANIPConnection)s.getSpecificService();
UPnPWANConnectionPortMapping[] ports = wan_ip.getPortMappings();
wan_ip.addPortMapping( true, 7007, "Moo!" );
UPnPAction act = s.getAction( "GetGenericPortMappingEntry" );
UPnPActionInvocation inv = act.getInvocation();
inv.addArgument( "NewPortMappingIndex", "0" );
UPnPActionArgument[] outs = inv.invoke();
for (int j=0;j<outs.length;j++){
System.out.println( outs[j].getName() + " = " + outs[j].getValue());
}
}
}
}else{
UPnPDevice[] kids = device.getSubDevices();
for (int i=0;i<kids.length;i++){
processDevice( kids[i] );
}
}
}
*/
}