All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.tsugi.pox.IMSPOXRequest Maven / Gradle / Ivy

There is a newer version: 23.3
Show 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> lm = new ArrayList>();
		Map mm = new TreeMap();
		mm.put("/personSourcedId","123user456");
		mm.put("/role/roleType","Learner");
		lm.add(mm);

		mm = new TreeMap();
		mm.put("/personSourcedId","789user123");
		mm.put("/role/roleType","Instructor");
		lm.add(mm);
		theMap.put("/readMembershipResponse/membershipRecord/membership/member", lm);

		String theXml = XMLMap.getXMLFragment(theMap, true);
		log.debug("th={}", theXml);
		output = pox.getResponseSuccess(desc,theXml);
		log.debug("---- Success String ----");
		log.debug(output);
	}

	/*

roleType:
Learner
Instructor
ContentDeveloper
Member
Manager
Mentor
Administrator
TeachingAssistant

fieldType:
Boolean
Integer
Real
String



GUID.TYPE

GUID.TYPE
MEMBERSHIPIDTYPE.TYPE

GUID.TYPE

STRING
STRING

DATETIME
DATETIME
BOOLEAN

LANGUAGESET.TYPE
STRING


STATUS.TYPE
DATETIME
GUID.TYPE


STRING
FIELDTYPE.TYPE
STRING




STRING
FIELDTYPE.TYPE
STRING




INTEGER
GUID.TYPE



	 */
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy