/* * This software copyright by various authors including the RPTools.net * development team, and licensed under the LGPL Version 3 or, at your * option, any later version. * * Portions of this software were originally covered under the Apache * Software License, Version 1.1 or Version 2.0. * * See the file LICENSE elsewhere in this distribution for license details. */ 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 net.sbbi.upnp.messages.UPNPResponseException; import net.sbbi.upnp.services.ServiceStateVariable; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; /** * 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 final DocumentBuilderFactory builder = DocumentBuilderFactory.newInstance(); private HttpPostRequest() { builder.setNamespaceAware(true); } public static HttpRequestHandler getInstance() { return instance; } public String service(Set<UPNPMBeanDevice> 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<UPNPMBeanDevice> i = devices.iterator(); i.hasNext();) { device = i.next(); if (device.getUuid().equals(uuid)) { // found service = device.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")) { // TODO 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 = 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) { // TODO 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; } }