org.tsugi.pox.IMSPOXRequest Maven / Gradle / Ivy
The newest version!
package org.tsugi.pox;
import java.io.ByteArrayInputStream;
import java.io.Reader;
import java.net.URLDecoder;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
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 org.apache.commons.text.StringEscapeUtils;
import org.tsugi.basiclti.Base64;
import org.tsugi.basiclti.XMLMap;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
public class IMSPOXRequest {
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;
private String oauth_signature_method = null;
public boolean valid = false;
private String operation = null;
public String errorMessage = null;
public String base_string = null;
private Map bodyMap = null;
private Map 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 getHeaderMap()
{
if ( headerMap != null ) return headerMap;
if ( headerElement == null ) return null;
headerMap = XMLMap.getMap(headerElement);
return headerMap;
}
public Map 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
@SuppressWarnings("deprecation")
public void loadFromRequest(HttpServletRequest request)
{
String contentType = request.getContentType();
String baseContentType;
try {
MimeType mimeType = new MimeType(contentType);
baseContentType = mimeType.getBaseType();
} catch (MimeTypeParseException e){
errorMessage = "Unable to parse mime type";
log.info("{}\n{}", errorMessage, contentType);
return;
}
if ( ! "application/xml".equals(baseContentType) ) {
errorMessage = "Content Type must be application/xml";
log.info("{}\n{}", errorMessage, contentType);
return;
}
header = request.getHeader("Authorization");
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]);
}
if ( parm.startsWith("oauth_signature_method=") ) {
String [] pieces = parm.split("\"");
oauth_signature_method = URLDecoder.decode(pieces[1]);
}
}
}
if ( oauth_body_hash == null ) {
errorMessage = "Did not find oauth_body_hash";
log.info("{}\n{}", errorMessage, header);
return;
}
log.debug("OBH={}", oauth_body_hash);
log.debug("OSM={}", oauth_signature_method);
final char[] buffer = new char[0x10000];
try {
StringBuilder out = new StringBuilder();
Reader in = request.getReader();
int read;
do {
read = in.read(buffer, 0, buffer.length);
if (read>0) {
out.append(buffer, 0, read);
}
} while (read>=0);
postBody = out.toString();
} catch(Exception e) {
errorMessage = "Could not read message body:"+e.getMessage();
return;
}
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
if ( "HMAC-SHA256".equals(oauth_signature_method) ) {
md = MessageDigest.getInstance("SHA-256");
}
md.update(postBody.getBytes());
byte[] output = Base64.encode(md.digest());
String hash = new String(output);
log.debug("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;
}
parsePostBody();
}
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);
log.debug("POX base_string={}",base_string);
} 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.warn(t.getMessage(), t);
}
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 =
"\n" +
"\n" +
" \n" +
" \n" +
" V1.0 \n" +
" %s \n" +
" \n" +
" failure \n" +
" error \n" +
" %s \n" +
" %s " +
" \n" +
" \n" +
" \n" +
" \n" +
" ";
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.escapeXml11(messageId),
StringEscapeUtils.escapeXml11(description),
StringEscapeUtils.escapeXml11(message_id));
}
static final String responseMessage =
"\n" +
"\n" +
" \n" +
" \n" +
" V1.0 \n" +
" %s \n" +
" \n" +
" %s \n" +
" %s \n" +
" %s \n" +
" %s \n" +
" %s " +
"%s\n"+
" \n" +
" \n" +
" \n" +
" \n" +
"%s%s"+
" \n" +
" ";
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 \n");
sb.append(" \n ");
sb.append(key);
sb.append(" \n ");
sb.append(StringEscapeUtils.escapeXml11(value));
sb.append(" \n \n");
}
if ( sb.length() > 0 ) sb.append(" ");
}
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.warn(internalError.toString());
}
if ( bodyString == null ) bodyString = "";
// Trim off XML header
if ( bodyString.startsWith(" 0 ) bodyString = bodyString.substring(pos);
}
bodyString = bodyString.trim();
String newLine = "";
if ( bodyString.length() > 0 ) newLine = "\n";
return String.format(responseMessage,
StringEscapeUtils.escapeXml11(messageId),
StringEscapeUtils.escapeXml11(major),
StringEscapeUtils.escapeXml11(severity),
StringEscapeUtils.escapeXml11(description),
StringEscapeUtils.escapeXml11(getHeaderMessageIdentifier()),
StringEscapeUtils.escapeXml11(operation),
StringEscapeUtils.escapeXml11(minorString),
bodyString, newLine);
}
/** Unit Tests */
static final String inputTestData = "\n" +
"\n" +
"\n" +
"\n" +
"V1.0 \n" +
"999999123 \n" +
" \n" +
" \n" +
"\n" +
"\n" +
"\n" +
"\n" +
"3124567 \n" +
" \n" +
"\n" +
"\n" +
"en-us \n" +
"A \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" ";
public static void runTest() {
log.debug("Runnig test.");
IMSPOXRequest pox = new IMSPOXRequest(inputTestData);
log.debug("Version = {}", pox.getHeaderVersion());
log.debug("Operation = {}", pox.getOperation());
Map bodyMap = pox.getBodyMap();
String guid = bodyMap.get("/resultRecord/sourcedGUID/sourcedId");
log.debug("guid={}", guid);
String grade = bodyMap.get("/resultRecord/result/resultScore/textString");
log.debug("grade={}", grade);
String desc = "Message received and validated operation="+pox.getOperation()+
" guid="+guid+" grade="+grade;
String output = pox.getResponseUnsupported(desc);
log.debug("---- Unsupported ----");
log.debug(output);
Properties props = new Properties();
props.setProperty("fred","zap");
props.setProperty("sam",IMSPOXRequest.MINOR_IDALLOC);
log.debug("---- Generate logger Error ----");
output = pox.getResponseFailure(desc,props);
log.debug("---- Failure ----");
log.debug(output);
Map theMap = new TreeMap ();
theMap.put("/readMembershipResponse/membershipRecord/sourcedId", "123course456");
List