package org.imsglobal.pox; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.Reader; import java.net.URLDecoder; import java.net.URLEncoder; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.util.Date; import java.util.Map; import java.util.Properties; import java.util.logging.Logger; import javax.servlet.http.HttpServletRequest; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathFactory; import net.oauth.OAuthAccessor; import net.oauth.OAuthConsumer; import net.oauth.OAuthMessage; import net.oauth.OAuthValidator; import net.oauth.SimpleOAuthValidator; import net.oauth.server.OAuthServlet; import net.oauth.signature.OAuthSignatureMethod; import oauth.signpost.commonshttp.CommonsHttpOAuthConsumer; import oauth.signpost.exception.OAuthException; import oauth.signpost.http.HttpParameters; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.client.HttpResponseException; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.imsglobal.lti.XMLMap; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; public class IMSPOXRequest { private final static Logger Log = Logger.getLogger(IMSPOXRequest.class .getName()); public final static String MAJOR_SUCCESS = "success"; public final static String MAJOR_FAILURE = "failure"; public final static String MAJOR_UNSUPPORTED = "unsupported"; public final static String MAJOR_PROCESSING = "processing"; public final static String [] validMajor = { MAJOR_SUCCESS, MAJOR_FAILURE, MAJOR_UNSUPPORTED, MAJOR_PROCESSING }; public final static String SEVERITY_ERROR = "error"; public final static String SEVERITY_WARNING = "warning"; public final static String SEVERITY_STATUS = "status"; public final static String [] validSeverity = { SEVERITY_ERROR, SEVERITY_WARNING, SEVERITY_STATUS }; public final static String MINOR_FULLSUCCESS ="fullsuccess"; public final static String MINOR_NOSOURCEDIDS = "nosourcedids"; public final static String MINOR_IDALLOC = "idalloc"; public final static String MINOR_OVERFLOWFAIL = "overflowfail"; public final static String MINOR_IDALLOCINUSEFAIL = "idallocinusefail"; public final static String MINOR_INVALIDDATAFAIL = "invaliddata"; public final static String MINOR_INCOMPLETEDATA = "incompletedata"; public final static String MINOR_PARTIALSTORAGE = "partialdatastorage"; public final static String MINOR_UNKNOWNOBJECT = "unknownobject"; public final static String MINOR_DELETEFAILURE = "deletefailure"; public final static String MINOR_TARGETREADFAILURE = "targetreadfailure"; public final static String MINOR_SAVEPOINTERROR = "savepointerror"; public final static String MINOR_SAVEPOINTSYNCERROR = "savepointsyncerror"; public final static String MINOR_UNKNOWNQUERY = "unknownquery"; public final static String MINOR_UNKNOWNVOCAB = "unknownvocab"; public final static String MINOR_TARGETISBUSY = "targetisbusy"; public final static String MINOR_UNKNOWNEXTENSION = "unknownextension"; public final static String MINOR_UNAUTHORIZEDREQUEST = "unauthorizedrequest"; public final static String MINOR_LINKFAILURE = "linkfailure"; public final static String MINOR_UNSUPPORTED = "unsupported"; public final static String [] validMinor = { MINOR_FULLSUCCESS, MINOR_NOSOURCEDIDS, MINOR_IDALLOC, MINOR_OVERFLOWFAIL, MINOR_IDALLOCINUSEFAIL, MINOR_INVALIDDATAFAIL, MINOR_INCOMPLETEDATA, MINOR_PARTIALSTORAGE, MINOR_UNKNOWNOBJECT, MINOR_DELETEFAILURE, MINOR_TARGETREADFAILURE, MINOR_SAVEPOINTERROR, MINOR_SAVEPOINTSYNCERROR, MINOR_UNKNOWNQUERY, MINOR_UNKNOWNVOCAB, MINOR_TARGETISBUSY, MINOR_UNKNOWNEXTENSION, MINOR_UNAUTHORIZEDREQUEST, MINOR_LINKFAILURE, MINOR_UNSUPPORTED } ; public Document postDom = null; public Element bodyElement = null; public Element headerElement = null; public String postBody = null; private String header = null; private String oauth_body_hash = null; private String oauth_consumer_key = null; public boolean valid = false; private String operation = null; public String errorMessage = null; public String base_string = null; private Map<String,String> bodyMap = null; private Map<String,String> headerMap = null; public String getOperation() { return operation; } public String getOAuthConsumerKey() { return oauth_consumer_key; } public String getHeaderVersion() { return getHeaderItem("/imsx_version"); } public String getHeaderMessageIdentifier() { return getHeaderItem("/imsx_messageIdentifier"); } public String getHeaderItem(String path) { if ( getHeaderMap() == null ) return null; return headerMap.get(path); } public Map<String,String> getHeaderMap() { if ( headerMap != null ) return headerMap; if ( headerElement == null ) return null; headerMap = XMLMap.getMap(headerElement); return headerMap; } public Map<String,String> getBodyMap() { if ( bodyMap != null ) return bodyMap; if ( bodyElement == null ) return null; bodyMap = XMLMap.getMap(bodyElement); return bodyMap; } public String getPostBody() { return postBody; } // Normal Constructor public IMSPOXRequest(String oauth_consumer_key, String oauth_secret, HttpServletRequest request) { loadFromRequest(request); if ( ! valid ) return; validateRequest(oauth_consumer_key, oauth_secret, request); } // Constructor for delayed validation public IMSPOXRequest(HttpServletRequest request) { loadFromRequest(request); } // Constructor for testing... public IMSPOXRequest(String bodyString) { postBody = bodyString; parsePostBody(); } // Load but do not check the authentication public void loadFromRequest(HttpServletRequest request) { String contentType = request.getContentType(); if ( ! "application/xml".equals(contentType) ) { errorMessage = "Content Type must be application/xml"; Log.info(errorMessage+"\n"+contentType); return; } setAuthHeader(request.getHeader("Authorization")); if ( oauth_body_hash == null ) { errorMessage = "Did not find oauth_body_hash"; Log.info(errorMessage+"\n"+header); return; } try { Reader in = request.getReader(); postBody = readPostBody(in); } catch(Exception e) { errorMessage = "Could not read message body:"+e.getMessage(); return; } validatePostBody(); if (errorMessage != null) return; parsePostBody(); } @SuppressWarnings("deprecation") public void setAuthHeader(String header) { this.header = header; oauth_body_hash = null; if ( header != null ) { if (header.startsWith("OAuth ")) header = header.substring(5); String [] parms = header.split(","); for ( String parm : parms ) { parm = parm.trim(); if ( parm.startsWith("oauth_body_hash=") ) { String [] pieces = parm.split("\""); oauth_body_hash = URLDecoder.decode(pieces[1]); } if ( parm.startsWith("oauth_consumer_key=") ) { String [] pieces = parm.split("\""); oauth_consumer_key = URLDecoder.decode(pieces[1]); } } } } public static String readPostBody(Reader in) throws IOException { // System.out.println("OBH="+oauth_body_hash); final char[] buffer = new char[0x10000]; StringBuilder out = new StringBuilder(); int read; do { read = in.read(buffer, 0, buffer.length); if (read > 0) { out.append(buffer, 0, read); } } while (read >= 0); return out.toString(); } public static String getBodyHash(String postBody) throws GeneralSecurityException { MessageDigest md = MessageDigest.getInstance("SHA1"); md.update(postBody.getBytes()); byte[] output = Base64.encodeBase64(md.digest()); return new String(output); } public void validatePostBody() { try { String hash = getBodyHash(postBody); // System.out.println("HASH="+hash); if ( ! hash.equals(oauth_body_hash) ) { errorMessage = "Body hash does not match header"; return; } } catch (Exception e) { errorMessage = "Could not compute body hash"; return; } } public void parsePostBody() { try { DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); DocumentBuilder db = dbf.newDocumentBuilder(); postDom = db.parse(new ByteArrayInputStream(postBody.getBytes())); }catch(Exception e) { errorMessage = "Could not parse XML: "+e.getMessage(); return; } try { XPath xpath = XPathFactory.newInstance().newXPath(); XPathExpression expr = xpath.compile("/imsx_POXEnvelopeRequest/imsx_POXBody/*"); Object result = expr.evaluate(postDom, XPathConstants.NODESET); NodeList nodes = (NodeList) result; bodyElement = (Element) nodes.item(0); operation = bodyElement.getNodeName(); expr = xpath.compile("/imsx_POXEnvelopeRequest/imsx_POXHeader/*"); result = expr.evaluate(postDom, XPathConstants.NODESET); nodes = (NodeList) result; headerElement = (Element) nodes.item(0); }catch(javax.xml.xpath.XPathExpressionException e) { errorMessage = "Could not parse XPATH: "+e.getMessage(); return; }catch(Exception e) { errorMessage = "Could not parse input XML: "+e.getMessage(); return; } if ( operation == null || bodyElement == null ) { errorMessage = "Could not find operation"; return; } valid = true; } // Assumes data is all loaded public void validateRequest(String oauth_consumer_key, String oauth_secret, HttpServletRequest request) { validateRequest(oauth_consumer_key, oauth_secret, request, null) ; } public void validateRequest(String oauth_consumer_key, String oauth_secret, HttpServletRequest request, String URL) { valid = false; OAuthMessage oam = OAuthServlet.getMessage(request, URL); OAuthValidator oav = new SimpleOAuthValidator(); OAuthConsumer cons = new OAuthConsumer("about:blank#OAuth+CallBack+NotUsed", oauth_consumer_key, oauth_secret, null); OAuthAccessor acc = new OAuthAccessor(cons); try { base_string = OAuthSignatureMethod.getBaseString(oam); } catch (Exception e) { base_string = null; } try { oav.validateMessage(oam,acc); } catch(Exception e) { errorMessage = "Launch fails OAuth validation: "+e.getMessage(); return; } valid = true; } public static String fetchTag(org.w3c.dom.Element element, String tag) { try { org.w3c.dom.NodeList elements = element.getElementsByTagName(tag); int numElements = elements.getLength(); if (numElements > 0) { org.w3c.dom.Element e = (org.w3c.dom.Element)elements.item(0); if (e.hasChildNodes()) { return e.getFirstChild().getNodeValue(); } } } catch (Throwable t) { Log.warning(t.getMessage()); // t.printStackTrace(); } return null; } public boolean inArray(final String [] theArray, final String theString) { if ( theString == null ) return false; for ( String str : theArray ) { if ( theString.equals(str) ) return true; } return false; } static final String fatalMessage = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<imsx_POXEnvelopeResponse xmlns = \"http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0\">\n" + " <imsx_POXHeader>\n" + " <imsx_POXResponseHeaderInfo>\n" + " <imsx_version>V1.0</imsx_version>\n" + " <imsx_messageIdentifier>%s</imsx_messageIdentifier>\n" + " <imsx_statusInfo>\n" + " <imsx_codeMajor>failure</imsx_codeMajor>\n" + " <imsx_severity>error</imsx_severity>\n" + " <imsx_description>%s</imsx_description>\n" + " <imsx_operationRefIdentifier>%s</imsx_operationRefIdentifier>" + " </imsx_statusInfo>\n" + " </imsx_POXResponseHeaderInfo>\n" + " </imsx_POXHeader>\n" + " <imsx_POXBody/>\n" + "</imsx_POXEnvelopeResponse>"; public static String getFatalResponse(String description) { return getFatalResponse(description, "unknown"); } public static String getFatalResponse(String description, String message_id) { Date dt = new Date(); String messageId = ""+dt.getTime(); return String.format(fatalMessage, StringEscapeUtils.escapeXml(messageId), StringEscapeUtils.escapeXml(description), StringEscapeUtils.escapeXml(message_id)); } static final String responseMessage = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<imsx_POXEnvelopeResponse xmlns = \"http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0\">\n" + " <imsx_POXHeader>\n" + " <imsx_POXResponseHeaderInfo>\n" + " <imsx_version>V1.0</imsx_version>\n" + " <imsx_messageIdentifier>%s</imsx_messageIdentifier>\n" + " <imsx_statusInfo>\n" + " <imsx_codeMajor>%s</imsx_codeMajor>\n" + " <imsx_severity>%s</imsx_severity>\n" + " <imsx_description>%s</imsx_description>\n" + " <imsx_messageRefIdentifier>%s</imsx_messageRefIdentifier>\n" + " <imsx_operationRefIdentifier>%s</imsx_operationRefIdentifier>" + "%s\n"+ " </imsx_statusInfo>\n" + " </imsx_POXResponseHeaderInfo>\n" + " </imsx_POXHeader>\n" + " <imsx_POXBody>\n" + "%s%s"+ " </imsx_POXBody>\n" + "</imsx_POXEnvelopeResponse>"; public String getResponseUnsupported(String desc) { return getResponse(desc, MAJOR_UNSUPPORTED, null, null, null, null); } public String getResponseFailure(String desc, Properties minor) { return getResponse(desc, null, null, null, minor, null); } public String getResponseFailure(String desc, Properties minor, String bodyString) { return getResponse(desc, null, null, null, minor, bodyString); } public String getResponseSuccess(String desc, String bodyString) { return getResponse(desc, MAJOR_SUCCESS, null, null, null, bodyString); } public String getResponse(String description, String major, String severity, String messageId, Properties minor, String bodyString) { StringBuffer internalError = new StringBuffer(); if ( major == null ) major = MAJOR_FAILURE; if ( severity == null && MAJOR_PROCESSING.equals(major) ) severity = SEVERITY_STATUS; if ( severity == null && MAJOR_SUCCESS.equals(major) ) severity = SEVERITY_STATUS; if ( severity == null ) severity = SEVERITY_ERROR; if ( messageId == null ) { Date dt = new Date(); messageId = ""+dt.getTime(); } StringBuffer sb = new StringBuffer(); if ( minor != null && minor.size() > 0 ) { for(Object okey : minor.keySet() ) { String key = (String) okey; String value = minor.getProperty(key); if ( key == null || value == null ) continue; if ( !inArray(validMinor, value) ) { if ( internalError.length() > 0 ) sb.append(", "); internalError.append("Invalid imsx_codeMinorFieldValue="+major); continue; } if ( sb.length() == 0 ) sb.append("\n <imsx_codeMinor>\n"); sb.append(" <imsx_codeMinorField>\n <imsx_codeMinorFieldName>"); sb.append(key); sb.append("</imsx_codeMinorFieldName>\n <imsx_codeMinorFieldValue>"); sb.append(StringEscapeUtils.escapeXml(value)); sb.append("</imsx_codeMinorFieldValue>\n </imsx_codeMinorField>\n"); } if ( sb.length() > 0 ) sb.append(" </imsx_codeMinor>"); } String minorString = sb.toString(); if ( ! inArray(validMajor, major) ) { if ( internalError.length() > 0 ) sb.append(", "); internalError.append("Invalid imsx_codeMajor="+major); } if ( ! inArray(validSeverity, severity) ) { if ( internalError.length() > 0 ) sb.append(", "); internalError.append("Invalid imsx_severity="+major); } if ( internalError.length() > 0 ) { description = description + " (Internal error: " + internalError.toString() + ")"; Log.warning(internalError.toString()); } if ( bodyString == null ) bodyString = ""; // Trim off XML header if ( bodyString.startsWith("<?xml") ) { int pos = bodyString.indexOf("<",1); if ( pos > 0 ) bodyString = bodyString.substring(pos); } bodyString = bodyString.trim(); String newLine = ""; if ( bodyString.length() > 0 ) newLine = "\n"; return String.format(responseMessage, StringEscapeUtils.escapeXml(messageId), StringEscapeUtils.escapeXml(major), StringEscapeUtils.escapeXml(severity), StringEscapeUtils.escapeXml(description), StringEscapeUtils.escapeXml(getHeaderMessageIdentifier()), StringEscapeUtils.escapeXml(operation), StringEscapeUtils.escapeXml(minorString), bodyString, newLine); } /** * A template string for creating ReplaceResult messages. * * Similar to {@link #replaceResultMessage}, except has support for messageIdentifier * * Use like: * <pre> * String.format( * ReplaceResultMessageTemplate, * messageId, * sourcedId, * resultScore, * resultDataXml * ) * </pre> * * */ public static final String ReplaceResultMessageTemplate = "<?xml version = \"1.0\" encoding = \"UTF-8\"?>"+ "<imsx_POXEnvelopeRequest xmlns=\"http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0\">"+ " <imsx_POXHeader>"+ " <imsx_POXRequestHeaderInfo>"+ " <imsx_version>V1.0</imsx_version>"+ " <imsx_messageIdentifier>%s</imsx_messageIdentifier>"+ " </imsx_POXRequestHeaderInfo>"+ " </imsx_POXHeader>"+ " <imsx_POXBody>"+ " <replaceResultRequest>"+ " <resultRecord>"+ " <sourcedGUID>"+ " <sourcedId>%s</sourcedId>"+ " </sourcedGUID>"+ " <result>"+ " <resultScore>"+ " <language>en</language>"+ " <textString>%s</textString>"+ " </resultScore>"+ " %s"+ " </result>"+ " </resultRecord>"+ " </replaceResultRequest>"+ " </imsx_POXBody>"+ "</imsx_POXEnvelopeRequest>"; /** * @deprecated use {@link #ReplaceResultMessageTemplate} instead. */ @Deprecated public static final String replaceResultMessage = "<?xml version = \"1.0\" encoding = \"UTF-8\"?>"+ "<imsx_POXEnvelopeRequest xmlns=\"http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0\">"+ " <imsx_POXHeader>"+ " <imsx_POXRequestHeaderInfo>"+ " <imsx_version>V1.0</imsx_version>"+ " </imsx_POXRequestHeaderInfo>"+ " </imsx_POXHeader>"+ " <imsx_POXBody>"+ " <replaceResultRequest>"+ " <resultRecord>"+ " <sourcedGUID>"+ " <sourcedId>%s</sourcedId>"+ " </sourcedGUID>"+ " <result>"+ " <resultScore>"+ " <language>en</language>"+ " <textString>%s</textString>"+ " </resultScore>"+ " %s"+ " </result>"+ " </resultRecord>"+ " </replaceResultRequest>"+ " </imsx_POXBody>"+ "</imsx_POXEnvelopeRequest>"; static final String resultDataText = "<resultData><text>%s</text></resultData>"; static final String resultDataUrl = "<resultData><url>%s</url></resultData>"; public static void sendReplaceResult(String url, String key, String secret, String sourcedid, String score) throws IOException, OAuthException, GeneralSecurityException { sendReplaceResult(url, key, secret, sourcedid, score, null); } public static void sendReplaceResult(String url, String key, String secret, String sourcedid, String score, String resultData) throws IOException, OAuthException, GeneralSecurityException { sendReplaceResult(url, key, secret, sourcedid, score, resultData, false); } public static void sendReplaceResult(String url, String key, String secret, String sourcedid, String score, String resultData, Boolean isUrl) throws IOException, OAuthException, GeneralSecurityException { HttpPost request = buildReplaceResult(url, key, secret, sourcedid, score, resultData, isUrl); DefaultHttpClient client = new DefaultHttpClient(); HttpResponse response = client.execute(request); if (response.getStatusLine().getStatusCode() >= 400) { throw new HttpResponseException(response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); } } public static HttpPost buildReplaceResult(String url, String key, String secret, String sourcedid, String score, String resultData, Boolean isUrl) throws IOException, OAuthException, GeneralSecurityException { return buildReplaceResult(url, key, secret, sourcedid, score, resultData, isUrl, null); } public static HttpPost buildReplaceResult(String url, String key, String secret, String sourcedid, String score, String resultData, Boolean isUrl, String messageId) throws IOException, OAuthException, GeneralSecurityException { String dataXml = ""; if (resultData != null) { String format = isUrl ? resultDataUrl : resultDataText; dataXml = String.format(format, StringEscapeUtils.escapeXml(resultData)); } String messageIdentifier = StringUtils.isBlank(messageId) ? String.valueOf(new Date().getTime()) : messageId; String xml = String.format(ReplaceResultMessageTemplate, StringEscapeUtils.escapeXml(messageIdentifier), StringEscapeUtils.escapeXml(sourcedid), StringEscapeUtils.escapeXml(score), dataXml); HttpParameters parameters = new HttpParameters(); String hash = getBodyHash(xml); parameters.put("oauth_body_hash", URLEncoder.encode(hash, "UTF-8")); CommonsHttpOAuthConsumer signer = new CommonsHttpOAuthConsumer(key, secret); HttpPost request = new HttpPost(url); request.setHeader("Content-Type", "application/xml"); request.setEntity(new StringEntity(xml, "UTF-8")); signer.setAdditionalParameters(parameters); signer.sign(request); return request; } /* roleType: Learner Instructor ContentDeveloper Member Manager Mentor Administrator TeachingAssistant fieldType: Boolean Integer Real String <readMembershipResponse xmlns="http://www.imsglobal.org/services/lis/mms2p0/wsdl11/sync/imsmms_v2p0"> <membershipRecord> <sourcedId>GUID.TYPE</sourcedId> <membership> <collectionSourcedId>GUID.TYPE</collectionSourcedId> <membershipIdType>MEMBERSHIPIDTYPE.TYPE</membershipIdType> <member> <personSourcedId>GUID.TYPE</personSourcedId> <role> <roleType>STRING</roleType> <subRole>STRING</subRole> <timeFrame> <begin>DATETIME</begin> <end>DATETIME</end> <restrict>BOOLEAN</restrict> <adminPeriod> <language>LANGUAGESET.TYPE</language> <textString>STRING</textString> </adminPeriod> </timeFrame> <status>STATUS.TYPE</status> <dateTime>DATETIME</dateTime> <dataSource>GUID.TYPE</dataSource> <recordInfo> <extensionField> <fieldName>STRING</fieldName> <fieldType>FIELDTYPE.TYPE</fieldType> <fieldValue>STRING</fieldValue> </extensionField> </recordInfo> <extension> <extensionField> <fieldName>STRING</fieldName> <fieldType>FIELDTYPE.TYPE</fieldType> <fieldValue>STRING</fieldValue> </extensionField> </extension> </role> </member> <creditHours>INTEGER</creditHours> <dataSource>GUID.TYPE</dataSource> </membership> </membershipRecord> </readMembershipResponse> */ }