/* * ============================================================================ * The Apache Software License, Version 1.1 * ============================================================================ * * Copyright (C) 2002 The Apache Software Foundation. All rights reserved. * * Redistribution and use in source and binary forms, with or without modifica- * tion, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * 3. The end-user documentation included with the redistribution, if any, must * include the following acknowledgment: "This product includes software * developed by SuperBonBon Industries (http://www.sbbi.net/)." * Alternately, this acknowledgment may appear in the software itself, if * and wherever such third-party acknowledgments normally appear. * * 4. The names "UPNPLib" and "SuperBonBon Industries" must not be * used to endorse or promote products derived from this software without * prior written permission. For written permission, please contact * info@sbbi.net. * * 5. Products derived from this software may not be called * "SuperBonBon Industries", nor may "SBBI" appear in their name, * without prior written permission of SuperBonBon Industries. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT,INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU- * DING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * This software consists of voluntary contributions made by many individuals * on behalf of SuperBonBon Industries. For more information on * SuperBonBon Industries, please see <http://www.sbbi.net/>. */ package net.sbbi.upnp; import java.util.*; import java.io.*; import java.net.*; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import net.sbbi.upnp.services.UPNPService; import org.apache.commons.logging.*; import org.xml.sax.InputSource; /** * This class can be used with the ServiceEventHandler interface * to recieve notifications about state variables changes on * a given UPNP service. * @author <a href="mailto:superbonbon@sbbi.net">SuperBonBon</a> * @version 1.0 */ public class ServicesEventing implements Runnable { private final static Log log = LogFactory.getLog( ServicesEventing.class ); private final static ServicesEventing singleton = new ServicesEventing(); private boolean inService = false; private boolean daemon = true; private int daemonPort = 9999; private ServerSocket server = null; private List registered = new ArrayList(); private ServicesEventing() { } public final static ServicesEventing getInstance() { return singleton; } /** * Set the listeniner thread as a daemon, default to true. * Only works when no more objects are registered. * @param daemon the new thread type. */ public void setDaemon( boolean daemon ) { this.daemon = daemon; } /** * Sets the listener thread port, default to 9999. * Only works when no more objects are registered. * @param daemonPort the new listening port */ public void setDaemonPort( int daemonPort ) { this.daemonPort = daemonPort; } /** * Register state variable events notification for a device service * @param service the service to register with * @param handler the registrant object * @param subscriptionDuration subscription time in seconds, -1 for infinite time * @return the subscription duration returned by the device, 0 for an infinite duration or -1 if no subscription done * @throws IOException if some IOException error happens during coms with the device */ public int register( UPNPService service, ServiceEventHandler handler, int subscriptionDuration ) throws IOException { ServiceEventSubscription sub = registerEvent( service, handler, subscriptionDuration ); if ( sub != null ) { return sub.getDurationTime(); } return -1; } /** * Register state variable events notification for a device service * @param service the service to register with * @param handler the registrant object * @param subscriptionDuration subscription time in seconds, -1 for infinite time * @return an ServiceEventSubscription object instance containing all the required info or null if no subscription done * @throws IOException if some IOException error happens during coms with the device */ public ServiceEventSubscription registerEvent( UPNPService service, ServiceEventHandler handler, int subscriptionDuration ) throws IOException { URL eventingLoc = service.getEventSubURL(); if ( eventingLoc != null ) { if ( !inService ) startServicesEventingThread(); String duration = Integer.toString( subscriptionDuration ); if ( subscriptionDuration == -1 ) { duration = "infinite"; } Subscription sub = lookupSubscriber( service, handler ); if ( sub != null ) { // allready registered let's try to unregister it unRegister( service, handler ); } StringBuffer packet = new StringBuffer( 64 ); packet.append( "SUBSCRIBE " ).append( eventingLoc.getFile() ).append( " HTTP/1.1\r\n" ); packet.append( "HOST: " ).append( eventingLoc.getHost() ).append( ":" ).append( eventingLoc.getPort() ).append( "\r\n" ); packet.append( "CALLBACK: <http://" ).append( InetAddress.getLocalHost().getHostAddress() ).append( ":" ).append( daemonPort ).append( "" ).append( eventingLoc.getFile() ).append( ">\r\n" ); packet.append( "NT: upnp:event\r\n" ); packet.append( "Connection: close\r\n" ); packet.append( "TIMEOUT: Second-" ).append( duration ).append( "\r\n\r\n" ); Socket skt = new Socket( eventingLoc.getHost(), eventingLoc.getPort() ); skt.setSoTimeout( 30000 ); // 30 secs timeout according to the specs if ( log.isDebugEnabled() ) log.debug( packet ); OutputStream out = skt.getOutputStream(); out.write( packet.toString().getBytes() ); out.flush(); InputStream in = skt.getInputStream(); StringBuffer data = new StringBuffer(); int readen = 0; byte[] buffer = new byte[256]; while ( ( readen = in.read( buffer ) ) != -1 ) { data.append( new String( buffer, 0, readen ) ); } in.close(); out.close(); skt.close(); if ( log.isDebugEnabled() ) log.debug( data.toString() ); if ( data.toString().trim().length() > 0 ) { HttpResponse resp = new HttpResponse( data.toString() ); if ( resp.getHeader().startsWith( "HTTP/1.1 200 OK" ) ) { String sid = resp.getHTTPHeaderField( "SID" ); String actualTimeout = resp.getHTTPHeaderField( "TIMEOUT" ); int durationTime = 0; // actualTimeout = Second-xxx or Second-infinite if ( !actualTimeout.equalsIgnoreCase( "Second-infinite" ) ) { durationTime = Integer.parseInt( actualTimeout.substring( 7 ) ); } sub = new Subscription(); sub.handler = handler; sub.sub = new ServiceEventSubscription( service.getServiceType(), service.getServiceId(), service.getEventSubURL(), sid, skt.getInetAddress(), durationTime ); synchronized( registered ) { registered.add( sub ); } return sub.sub; } } } return null; } private Subscription lookupSubscriber( UPNPService service, ServiceEventHandler handler ) { synchronized( registered ) { for ( Iterator i = registered.iterator(); i.hasNext(); ) { Subscription sub = (Subscription)i.next(); if ( sub.handler == handler && sub.sub.getServiceId().hashCode() == service.getServiceId().hashCode() && sub.sub.getServiceType().hashCode() == service.getServiceType().hashCode() && sub.sub.getServiceURL().equals( service.getEventSubURL() ) ) { return sub; } } } return null; } private Subscription lookupSubscriber( String sid, InetAddress deviceIp ) { synchronized( registered ) { for ( Iterator i = registered.iterator(); i.hasNext(); ) { Subscription sub = (Subscription)i.next(); if ( sub.sub.getSID().equals( sid ) && sub.sub.getDeviceIp().equals( deviceIp ) ) { return sub; } } } return null; } private Subscription lookupSubscriber( String sid ) { synchronized( registered ) { for ( Iterator i = registered.iterator(); i.hasNext(); ) { Subscription sub = (Subscription)i.next(); if ( sub.sub.getSID().equals( sid ) ) { return sub; } } } return null; } /** * Unregisters events notifications from a service * @param service the service that need to be unregistered * @param handler the handler that registered for this service * @return true if unregistered false otherwise ( the given handler never registred for the given service ) * @throws IOException if some IOException error happens during coms with the device */ public boolean unRegister( UPNPService service, ServiceEventHandler handler ) throws IOException { URL eventingLoc = service.getEventSubURL(); if ( eventingLoc != null ) { Subscription sub = lookupSubscriber( service, handler ); if ( sub != null ) { synchronized( registered ) { registered.remove( sub ); } if ( registered.size() == 0 ) { stopServicesEventingThread(); } StringBuffer packet = new StringBuffer( 64 ); packet.append( "UNSUBSCRIBE " ).append( eventingLoc.getFile() ).append( " HTTP/1.1\r\n" ); packet.append( "HOST: " ).append( eventingLoc.getHost() ).append( ":" ).append( eventingLoc.getPort() ).append( "\r\n" ); packet.append( "SID: " ).append( sub.sub.getSID() ).append( "\r\n\r\n" ); Socket skt = new Socket( eventingLoc.getHost(), eventingLoc.getPort() ); skt.setSoTimeout( 30000 ); // 30 secs timeout according to the specs if ( log.isDebugEnabled() ) log.debug( packet ); OutputStream out = skt.getOutputStream(); out.write( packet.toString().getBytes() ); out.flush(); InputStream in = skt.getInputStream(); StringBuffer data = new StringBuffer(); int readen = 0; byte[] buffer = new byte[256]; while ( ( readen = in.read( buffer ) ) != -1 ) { data.append( new String( buffer, 0, readen ) ); } in.close(); out.close(); skt.close(); if ( log.isDebugEnabled() ) log.debug( data.toString() ); if ( data.toString().trim().length() > 0 ) { HttpResponse resp = new HttpResponse( data.toString() ); if ( resp.getHeader().startsWith( "HTTP/1.1 200 OK" ) ) { return true; } } } } return false; } private void startServicesEventingThread() { synchronized( singleton ) { if ( !inService ) { Thread deamon = new Thread( singleton, "ServicesEventing daemon" ); deamon.setDaemon( daemon ); inService = true; deamon.start(); } } } private void stopServicesEventingThread() { synchronized( singleton ) { inService = false; try { server.close(); } catch ( IOException ex ) { // should not happen } } } public void run() { // only the deamon thread is allowed to call such method if ( !Thread.currentThread().getName().equals( "ServicesEventing daemon" ) ) return; try { server = new ServerSocket( daemonPort ); } catch ( IOException ex ) { log.error( "Error during daemon server socket on port " + daemonPort + " creation", ex ); return; } while ( inService ) { try { Socket skt = server.accept(); new Thread( new RequestProcessor( skt ) ).start(); } catch ( IOException ioEx ) { if ( inService ) { log.error( "IO Exception during UPNP messages listening thread", ioEx ); } } } } private class Subscription { private ServiceEventSubscription sub = null; private ServiceEventHandler handler = null; } private class RequestProcessor implements Runnable { private Socket client; private RequestProcessor( Socket client ) { this.client = client; } public void run() { try { client.setSoTimeout( 30000 ); InputStream in = client.getInputStream(); OutputStream out = client.getOutputStream(); int readen = 0; StringBuffer data = new StringBuffer(); byte[] buffer = new byte[256]; boolean EOF = false; while ( !EOF && ( readen = in.read( buffer ) ) != -1 ) { data.append( new String( buffer, 0, readen ) ); // avoid a strange behaviour with some impls.. the -1 is never reached and a sockettimeout occurs // and a 0 byte is sent as the last byte if ( data.charAt( data.length()-1 ) == (char)0 ) { EOF = true; } } String packet = data.toString(); if ( packet.trim().length() > 0 ) { if ( packet.indexOf( (char)0 ) != -1 ) packet = packet.replace( (char)0, ' ' ); HttpResponse resp = new HttpResponse( packet ); if ( resp.getHeader().startsWith( "NOTIFY" ) ) { String sid = resp.getHTTPHeaderField( "SID" ); InetAddress deviceIp = client.getInetAddress(); String postURL = resp.getHTTPHeaderField( "SID" ); Subscription subscription = null; if ( sid != null && postURL != null ) { subscription = lookupSubscriber( sid, deviceIp ); if ( subscription == null ) { // not found maybe that the IP is not the same subscription = lookupSubscriber( sid ); } } if ( subscription != null ) { // respond ok out.write( "HTTP/1.1 200 OK\r\n".getBytes() ); } else { // unknown sid respond ko out.write( "HTTP/1.1 412 Precondition Failed\r\n".getBytes() ); } out.flush(); in.close(); out.close(); client.close(); if ( subscription != null ) { // let's parse it SAXParserFactory saxParFact = SAXParserFactory.newInstance(); saxParFact.setValidating( false ); saxParFact.setNamespaceAware( true ); SAXParser parser = saxParFact.newSAXParser(); ServiceEventMessageParser msgParser = new ServiceEventMessageParser(); StringReader stringReader = new StringReader( resp.getBody() ); InputSource src = new InputSource( stringReader ); parser.parse( src, msgParser ); Map changedStateVars = msgParser.getChangedStateVars(); for ( Iterator i = changedStateVars.keySet().iterator(); i.hasNext(); ) { String stateVarName = (String)i.next(); String stateVarNewVal = (String)changedStateVars.get( stateVarName ); subscription.handler.handleStateVariableEvent( stateVarName, stateVarNewVal ); } } } } } catch ( IOException ioEx ) { log.error( "IO Exception during client processing thread", ioEx ); } catch( Exception ex ) { log.error( "Unexpected error during client processing thread", ex ); } } } }