/*
* ============================================================================
* 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.jmx;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.util.Iterator;
import java.util.Set;
import javax.management.AttributeNotFoundException;
import javax.management.MBeanInfo;
import javax.management.MBeanOperationInfo;
import javax.management.MBeanParameterInfo;
import javax.management.ReflectionException;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import net.sbbi.upnp.messages.UPNPResponseException;
import net.sbbi.upnp.services.ServiceStateVariable;
/**
* Class to handle HTTP POST requests on UPNPMBeanDevices
* @author <a href="mailto:superbonbon@sbbi.net">SuperBonBon</a>
* @version 1.0
*/
public class HttpPostRequest implements HttpRequestHandler {
private final static HttpPostRequest instance = new HttpPostRequest();
private static final String STATE_VAR_ACTION_URN = "urn:schemas-upnp-org:control-1-0#QueryStateVariable";
private DocumentBuilderFactory builder = DocumentBuilderFactory.newInstance();
private HttpPostRequest() {
builder.setNamespaceAware( true );
}
public static HttpRequestHandler getInstance() {
return instance;
}
public String service( Set devices, HttpRequest request ) {
String rtr = null;
String filePath = request.getHttpCommandArg();
// Request are : /uuid/serviceId/control || /uuid/serviceId/events
boolean validPostUrl = ( filePath.startsWith( "/" ) && filePath.endsWith( "/control" ) ) ||
( filePath.startsWith( "/" ) && filePath.endsWith( "/events" ) );
if ( validPostUrl ) {
String uuid = null;
String serviceUuid = null;
int lastSlash = filePath.lastIndexOf( '/' );
if ( lastSlash != -1 ) {
serviceUuid = filePath.substring( 1, lastSlash );
// check if this is an uuid/desc.xml or uuid/serviceId/scpd.xml request type
int slashIndex = serviceUuid.indexOf( "/" );
if ( slashIndex != -1 ) {
uuid = serviceUuid.substring( 0, slashIndex );
}
}
if ( uuid != null ) {
// search now the bean within the set
UPNPMBeanDevice device = null;
UPNPMBeanService service = null;
synchronized( devices ) {
for ( Iterator i = devices.iterator(); i.hasNext(); ) {
UPNPMBeanDevice dv = (UPNPMBeanDevice)i.next();
if ( dv.getUuid().equals( uuid ) ) {
// found
service = dv.getUPNPMBeanService( serviceUuid );
break;
}
}
}
if ( service != null ) {
try {
if ( filePath.endsWith( "/control" ) ) {
String soapAction = request.getHTTPHeaderField( "SOAPACTION" );
String soapRequest = request.getBody();
if ( soapRequest == null || soapRequest.trim().length() == 0 ) {
throw new IllegalArgumentException( "No SOAP request provided" );
}
if ( soapAction.indexOf( STATE_VAR_ACTION_URN ) != -1 ) {
String requestedVar = getQueryStateVariableVarName( soapRequest );
if ( requestedVar == null || requestedVar.trim().length() == 0 ) {
throw new IllegalArgumentException( "No varname content provided" );
}
Object result = null;
// if the state variable has been created for an operation (input or output argument)
// then we cannot call the method on the mBean since this is not a callable jmx attribute.
if ( service.getOperationsStateVariables().get( requestedVar ) == null ) {
try {
result = service.getAttribute( requestedVar );
} catch ( AttributeNotFoundException ex ) {
throw new UPNPResponseException( 404, "State varibale " + requestedVar + " unknown" );
}
}
rtr = getQueryStateVariableResult( result );
} else {
// this is an operation that is called
if ( soapAction == null || soapAction.trim().length() == 0 ) {
throw new IllegalArgumentException( "Missing SOAPACTION HTTP header" );
}
if ( !soapAction.startsWith( "\"" ) ||
!soapAction.endsWith( "\"" ) ||
soapAction.indexOf( "#" ) == -1 ) {
throw new IllegalArgumentException( "Invalid SOAPACTION HTTP header (" + soapAction + ") check your specs" );
}
// ok this is an action. first let's parse the XML message
String actionName = getActionName( soapAction, service.getServiceType() );
if ( actionName == null ) {
throw new UPNPResponseException( 401, "Provided SOAPACTION (" + soapAction + ") wrongly " +
"formatted or does not match target device type (" + device.getDeviceType() + ")" );
}
String[] providedParams = getActionParams( soapRequest, actionName, service.getServiceType() );
Object result = null;
String[] signature = null;
Object[] parameters = null;
Object[] signatureAndVals = getSignatureAndParamsVals( providedParams, actionName, service );
if ( signatureAndVals != null ) {
signature = (String[])signatureAndVals[0];
parameters = (Object[])signatureAndVals[1];
}
try {
result = service.invoke( actionName, parameters, signature );
} catch ( ReflectionException ex ) {
throw ex.getTargetException();
}
rtr = getActionResult( result, service.getServiceType(), actionName );
}
} else if ( filePath.endsWith( "/events" ) ) {
// implement eventing
throw new Exception( "Not yet implemented :o(, try again with the next software version" );
}
} catch ( Exception ex ) {
rtr = createSOAPError( ex );
}
}
}
}
return rtr;
}
private MBeanOperationInfo getOperationInfo( MBeanInfo beanInfo, String methodName ) {
MBeanOperationInfo[] ops = beanInfo.getOperations();
for ( int i = 0; i < ops.length; i++ ) {
if ( ops[i].getName().equals( methodName ) ) {
return ops[i];
}
}
return null;
}
private Object[] getParameterInfo( MBeanOperationInfo opInfo, String paramName ) {
MBeanParameterInfo[] args = opInfo.getSignature();
for ( int i = 0; i < args.length; i++ ) {
if ( args[i].getName().equals( paramName ) ) {
return new Object[]{ args[i], new Integer( i )};
}
}
return null;
}
private Object[] getSignatureAndParamsVals( String[] providedParams, String methodName, UPNPMBeanService service ) throws UPNPResponseException {
MBeanOperationInfo opInfo = getOperationInfo( service.getMBeanInfo(), methodName );
if ( opInfo == null ) {
// OUPS have a serious problem here !
throw new RuntimeException( "Unexpected null MBeanOperationInfo for operation " + methodName );
}
int providedParamsCount = 0;
if ( providedParams != null ) {
providedParamsCount = providedParams.length / 2;
}
if( opInfo.getSignature().length != providedParamsCount ) {
throw new UPNPResponseException( 402, "Invalid provided parameter(s) count (" + providedParamsCount + ") for action " +
methodName + ", " + opInfo.getSignature().length + " parameter(s) are needed" );
}
// checks done, no params provided, returning null
if ( providedParamsCount == 0 ) {
return null;
}
Object[] rtrVal = new Object[2];
rtrVal[0] = new String[providedParamsCount];
rtrVal[1] = new Object[providedParamsCount];
for ( int i = 0; i < providedParams.length; i += 2 ) {
String paramName = providedParams[i];
String paramValue = providedParams[i+1];
String paramType = (String)service.getOperationsStateVariables().get( paramName );
if ( paramType == null ) {
throw new UPNPResponseException( 402, "Unknown action " + methodName + " parameter " + paramName );
}
Object[] beanParamInfos = getParameterInfo( opInfo, paramName );
if ( beanParamInfos == null ) {
// should never happen
throw new UPNPResponseException( 402, "Unknown action " + methodName + " parameter " + paramName );
}
MBeanParameterInfo beanParamInfo = (MBeanParameterInfo)beanParamInfos[0];
int paramSignaturePos = ((Integer)beanParamInfos[1]).intValue();
Object value = null;
try {
// TODO what about null or empty params ?
value = ServiceStateVariable.UPNPToJavaObject( paramType, paramValue );
} catch ( Throwable t ) {
throw new UPNPResponseException( 501, "Error occured during parameter " + paramName + "(" + paramType + ") value " + paramValue + " parsing:" + t.getMessage() );
}
String[] sign = (String[])rtrVal[0];
sign[paramSignaturePos] = beanParamInfo.getType();
Object[] vals = (Object[])rtrVal[1];
vals[paramSignaturePos] = value;
}
return rtrVal;
}
private String getActionName( String SOAPAction, String serviceType ) {
int index = SOAPAction.indexOf( serviceType );
if ( index != -1 ) {
try {
return SOAPAction.substring( serviceType.length() + 2, SOAPAction.length() -1 );
} catch ( Throwable t ) {
// probable wrongly formatted
}
}
return null;
}
private String[] getActionParams( String xmlRequest, String actionName, String serviceType ) throws Exception {
String[] rtrVal = null;
ByteArrayInputStream in = new ByteArrayInputStream( xmlRequest.getBytes() );
InputSource src = new InputSource( in );
Document doc = null;
synchronized( builder ) {
doc = builder.newDocumentBuilder().parse( src );
}
Element root = doc.getDocumentElement();
Element body = (Element)root.getElementsByTagNameNS( "http://schemas.xmlsoap.org/soap/envelope/", "Body" ).item( 0 );
if ( body == null ) {
throw new IllegalArgumentException( "Missing body tag" );
}
Element action = (Element)body.getElementsByTagNameNS( serviceType, actionName ).item( 0 );
if ( action == null ) {
throw new IllegalArgumentException( "Missing action tag " + actionName );
}
NodeList params = action.getChildNodes();
int length = 0;
for ( int i = 0; i < params.getLength(); i++ ) {
if ( params.item( i ) instanceof Element ) {
length++;
}
}
if ( length > 0 ) {
rtrVal = new String[length * 2];
int j = 0;
for ( int i = 0; i < params.getLength(); i++ ) {
if ( params.item( i ) instanceof Element ) {
Element arg = (Element)params.item( i );
rtrVal[j] = arg.getNodeName();
rtrVal[j + 1] = arg.getFirstChild().getNodeValue();
j+=2;
}
}
}
return rtrVal;
}
private String getQueryStateVariableVarName( String xmlRequest ) throws Exception {
ByteArrayInputStream in = new ByteArrayInputStream( xmlRequest.getBytes() );
InputSource src = new InputSource( in );
Document doc = null;
synchronized( builder ) {
doc = builder.newDocumentBuilder().parse( src );
}
Element root = doc.getDocumentElement();
Element body = (Element)root.getElementsByTagNameNS( "http://schemas.xmlsoap.org/soap/envelope/", "Body" ).item( 0 );
if ( body == null ) {
throw new IllegalArgumentException( "Missing body tag" );
}
Element query = (Element)body.getElementsByTagNameNS( "urn:schemas-upnp-org:control-1-0", "QueryStateVariable" ).item( 0 );
if ( query == null ) {
throw new IllegalArgumentException( "Missing query tag" );
}
Element varName = (Element)query.getElementsByTagNameNS( "urn:schemas-upnp-org:control-1-0", "varName" ).item( 0 );
if ( varName == null ) {
throw new IllegalArgumentException( "Missing varName tag" );
}
return varName.getFirstChild().getNodeValue();
}
private String createSOAPError( Throwable ex ) {
StringBuffer rtr = new StringBuffer();
int errorCode;
String errorDescription = null;
if ( ex instanceof UPNPResponseException ) {
UPNPResponseException upnpEx = (UPNPResponseException)ex;
errorCode = upnpEx.getDetailErrorCode();
errorDescription = upnpEx.getDetailErrorDescription();
if ( upnpEx.getCause() != null ) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter( out );
upnpEx.getCause().printStackTrace( writer );
writer.flush();
writer.close();
errorDescription += "\nAttached stack trace:\n" + new String( out.toByteArray() );
}
} else {
errorCode = 501;
ByteArrayOutputStream out = new ByteArrayOutputStream();
PrintWriter writer = new PrintWriter( out );
ex.printStackTrace( writer );
writer.flush();
writer.close();
errorDescription = new String( out.toByteArray() );
}
StringBuffer xml = new StringBuffer();
xml.append( "<?xml version=\"1.0\"?>\r\n" )
.append( "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " )
.append( "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" )
.append( "<s:Body>\r\n<s:Fault>\r\n" )
.append( "<faultcode>s:Client</faultcode>\r\n" )
.append( "<faultstring>UPnPError</faultstring>\r\n" )
.append( "<detail>\r\n" )
.append( "<UPnPError xmlns=\"urn:schemas-upnp-org:control-1-0\">\r\n" )
.append( "<errorCode>" ).append( errorCode ).append( "</errorCode>\r\n" )
.append( "<errorDescription>" ).append( errorDescription ).append( "</errorDescription>\r\n" )
.append( "</UPnPError>\r\n" )
.append( "</detail>\r\n" )
.append( "</s:Fault></s:Body>\r\n" )
.append( "</s:Envelope>" );
rtr.append( "HTTP/1.1 500 Server error\r\n" )
.append( "CONTENT-LENGTH: " ).append( xml.length() ).append( "\r\n" )
.append( "CONTENT-TYPE: text/xml\r\n\r\n" )
.append( xml );
return rtr.toString();
}
private String getActionResult( Object result, String serviceType, String actionName ) {
StringBuffer xml = new StringBuffer();
xml.append( "<?xml version=\"1.0\"?>\r\n" )
.append( "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " )
.append( "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" )
.append( "<s:Body>\r\n" )
.append( "<u:" ).append( actionName ).append( "Response xmlns:u=\"" ).append( serviceType ).append( "\">\r\n" )
.append( "<" ).append( actionName ).append( "_out>" ).append( getResultAsString( result ) )
.append( "</" ).append( actionName ).append( "_out>\r\n")
.append( "</u:" ).append( actionName ).append( "Response>\r\n" )
.append( "</s:Body>\r\n" )
.append( "</s:Envelope>" );
StringBuffer rtr = new StringBuffer();
rtr.append( "HTTP/1.1 200 OK\r\n" )
.append( "CONTENT-LENGTH: " ).append( xml.length() ).append( "\r\n" )
.append( "CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n" )
.append( "EXT:\r\n" )
.append( "SERVER: " ).append( UPNPMBeanDevice.IMPL_NAME ).append( "\r\n\r\n" )
.append( xml );
return rtr.toString();
}
private String getQueryStateVariableResult( Object result ) {
StringBuffer xml = new StringBuffer();
xml.append( "<?xml version=\"1.0\"?>\r\n" )
.append( "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " )
.append( "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" )
.append( "<s:Body>\r\n" )
.append( "<u:QueryStateVariableResponse xmlns:u=\"urn:schemas-upnp-org:control-1-0\">\r\n" )
.append( "<return>" ).append( getResultAsString( result ) )
.append( "</return>\r\n")
.append( "</u:QueryStateVariableResponse>\r\n" )
.append( "</s:Body>\r\n" )
.append( "</s:Envelope>" );
StringBuffer rtr = new StringBuffer();
rtr.append( "HTTP/1.1 200 OK\r\n" )
.append( "CONTENT-LENGTH: " ).append( xml.length() ).append( "\r\n" )
.append( "CONTENT-TYPE: text/xml; charset=\"utf-8\"\r\n" )
.append( "EXT:\r\n" )
.append( "SERVER: " ).append( UPNPMBeanDevice.IMPL_NAME ).append( "\r\n\r\n" )
.append( xml );
return rtr.toString();
}
private String getResultAsString( Object result ) {
// result to utf-8
if ( result == null ) return "";
String rtrVal = null;
if ( result instanceof Object[] ) {
StringBuffer tmp = new StringBuffer();
Object[] array = (Object[])result;
for ( int i = 0; i < array.length; i++ ) {
Object val = array[i];
if ( val != null ) {
tmp.append( val.toString() );
} else {
tmp.append( "null" );
}
if ( i < array.length ) tmp.append( "\n" );
}
rtrVal = tmp.toString();
} else if ( result instanceof long[] ) {
StringBuffer tmp = new StringBuffer();
long[] array = (long[])result;
for ( int i = 0; i < array.length; i++ ) {
tmp.append( array[i] );
if ( i < array.length ) tmp.append( "\n" );
}
rtrVal = tmp.toString();
} else if ( result instanceof double[] ) {
StringBuffer tmp = new StringBuffer();
double[] array = (double[])result;
for ( int i = 0; i < array.length; i++ ) {
tmp.append( array[i] );
if ( i < array.length ) tmp.append( "\n" );
}
rtrVal = tmp.toString();
} else if ( result instanceof float[] ) {
StringBuffer tmp = new StringBuffer();
float[] array = (float[])result;
for ( int i = 0; i < array.length; i++ ) {
tmp.append( array[i] );
if ( i < array.length ) tmp.append( "\n" );
}
rtrVal = tmp.toString();
} else if ( result instanceof short[] ) {
StringBuffer tmp = new StringBuffer();
short[] array = (short[])result;
for ( int i = 0; i < array.length; i++ ) {
tmp.append( array[i] );
if ( i < array.length ) tmp.append( "\n" );
}
rtrVal = tmp.toString();
} else if ( result instanceof int[] ) {
StringBuffer tmp = new StringBuffer();
int[] array = (int[])result;
for ( int i = 0; i < array.length; i++ ) {
tmp.append( array[i] );
if ( i < array.length ) tmp.append( "\n" );
}
rtrVal = tmp.toString();
} else if ( result instanceof byte[] ) {
StringBuffer tmp = new StringBuffer();
byte[] array = (byte[])result;
for ( int i = 0; i < array.length; i++ ) {
tmp.append( array[i] );
if ( i < array.length ) tmp.append( "\n" );
}
rtrVal = tmp.toString();
} else if ( result instanceof char[] ) {
rtrVal = new String( (char[])result );
} else {
rtrVal = result.toString();
}
if ( rtrVal.indexOf( "<" ) != -1 || rtrVal.indexOf( ">" ) != -1 ) {
rtrVal = "<![CDATA[" + rtrVal + "]]>";
}
return rtrVal;
}
}