org.imsglobal.lti.BasicLTIUtil Maven / Gradle / Ivy
Show all versions of basiclti-util Show documentation
/*
* $URL: https://source.sakaiproject.org/svn/basiclti/trunk/basiclti-util/src/java/org/imsglobal/basiclti/BasicLTIUtil.java $
* $Id: BasicLTIUtil.java 133995 2014-02-02 22:06:40Z [email protected] $
*
* Copyright (c) 2008 IMS GLobal Learning Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package org.imsglobal.lti;
import static org.imsglobal.lti.BasicLTIConstants.CUSTOM_PREFIX;
import static org.imsglobal.lti.BasicLTIConstants.EXTENSION_PREFIX;
import static org.imsglobal.lti.BasicLTIConstants.LTI_MESSAGE_TYPE;
import static org.imsglobal.lti.BasicLTIConstants.LTI_VERSION;
import static org.imsglobal.lti.BasicLTIConstants.OAUTH_PREFIX;
import static org.imsglobal.lti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_CONTACT_EMAIL;
import static org.imsglobal.lti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_DESCRIPTION;
import static org.imsglobal.lti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_GUID;
import static org.imsglobal.lti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_NAME;
import static org.imsglobal.lti.BasicLTIConstants.TOOL_CONSUMER_INSTANCE_URL;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.TreeMap;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest;
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 org.apache.commons.lang3.StringUtils;
import org.imsglobal.lti.launch.LtiError;
import org.imsglobal.lti.launch.LtiLaunch;
import org.imsglobal.lti.launch.LtiVerificationResult;
/* Leave out until we have JTidy 0.8 in the repository
import org.w3c.tidy.Tidy;
import java.io.ByteArrayOutputStream;
*/
/**
* Some Utility code for IMS LTI http://www.anyexample.com/programming/java
* /java_simple_class_to_compute_sha_1_hash.xml
*
* Sample Descriptor
*
*
* <?xml version="1.0" encoding="UTF-8"?>
* <basic_lti_link xmlns="http://www.imsglobal.org/xsd/imsbasiclti_v1p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
* <title>generated by tp+user</title>
* <description>generated by tp+user</description>
* <custom>
* <parameter key="keyname">value</parameter>
* </custom>
* <extensions platform="www.lms.com">
* <parameter key="keyname">value</parameter>
* </extensions>
* <launch_url>url to the lti launch URL</launch_url>
* <secure_launch_url>url to the lti launch URL</secure_launch_url>
* <icon>url to an icon for this tool (optional)</icon>
* <secure_icon>url to an icon for this tool (optional)</secure_icon>
* <cartridge_icon identifierref="BLTI001_Icon"/>
* <vendor>
* <code>vendor.com</code>
* <name>Vendor Name</name>
* <description>
* This is a Grade Book that supports many column types.
* </description>
* <contact>
* <email>[email protected]</email>
* </contact>
* <url>http://www.vendor.com/product</url>
* </vendor>
* </basic_lti_link>
*
*/
public class BasicLTIUtil {
// We use the built-in Java logger because this code needs to be very generic
private static Logger M_log = Logger.getLogger(BasicLTIUtil.class.toString());
/**
* To turn on really verbose debugging
*/
private static boolean verbosePrint = false;
public static final String BASICLTI_SUBMIT = "ext_basiclti_submit";
private static final Pattern CUSTOM_REGEX = Pattern.compile("[^A-Za-z0-9]");
private static final String UNDERSCORE = "_";
// Simple Debug Print Mechanism
public static void dPrint(String str) {
if (verbosePrint) {
System.out.println(str);
}
M_log.fine(str);
}
public static LtiVerificationResult validateMessage(HttpServletRequest request, String URL, String oauth_secret) {
OAuthMessage oam = OAuthServlet.getMessage(request, URL);
String oauth_consumer_key = null;
try {
oauth_consumer_key = oam.getConsumerKey();
} catch (Exception e) {
return new LtiVerificationResult(false, LtiError.BAD_REQUEST, "Unable to find consumer key in message");
}
OAuthValidator oav = new SimpleOAuthValidator();
OAuthConsumer cons = new OAuthConsumer("about:blank#OAuth+CallBack+NotUsed", oauth_consumer_key, oauth_secret, null);
OAuthAccessor acc = new OAuthAccessor(cons);
String base_string = null;
try {
base_string = OAuthSignatureMethod.getBaseString(oam);
} catch (IOException|URISyntaxException e) {
return new LtiVerificationResult(false, LtiError.BAD_REQUEST, "Unable to find base string");
}
try {
oav.validateMessage(oam, acc);
} catch (Exception e) {
if (base_string != null) {
return new LtiVerificationResult(false, LtiError.BAD_REQUEST, "Failed to validate: " + e.getLocalizedMessage() + "\nBase String\n" + base_string);
} else {
return new LtiVerificationResult(false, LtiError.BAD_REQUEST, "Failed to validate: " + e.getLocalizedMessage());
}
}
return new LtiVerificationResult(true, new LtiLaunch(request));
}
public static String validateDescriptor(String descriptor) {
if (descriptor == null) {
return null;
}
if (descriptor.indexOf(" tm = XMLMap.getFullMap(descriptor.trim());
if (tm == null) {
return null;
}
// We demand at least an endpoint
String ltiSecureLaunch = XMLMap.getString(tm,
"/basic_lti_link/secure_launch_url");
// We demand at least an endpoint
if (ltiSecureLaunch != null && ltiSecureLaunch.trim().length() > 0) {
return ltiSecureLaunch;
}
String ltiLaunch = XMLMap.getString(tm, "/basic_lti_link/launch_url");
if (ltiLaunch != null && ltiLaunch.trim().length() > 0) {
return ltiLaunch;
}
return null;
}
/**
* Any properties which are not well known (i.e. in
* {@link BasicLTIConstants#validPropertyNames}) will be mapped to custom
* properties per the specified semantics. NOTE: no blacklisting of keys is
* performed.
*
* @param rawProperties A set of properties that will be cleaned.
* @return A cleansed version of rawProperties.
*/
public static Map cleanupProperties(
final Map rawProperties) {
return cleanupProperties(rawProperties, null);
}
/**
* Any properties which are not well known (i.e. in
* {@link BasicLTIConstants#validPropertyNames}) will be mapped to custom
* properties per the specified semantics.
*
* @param rawProperties A set of properties that will be cleaned.
* @param blackList An array of {@link String}s which are considered unsafe
* to be included in launch data. Any matches will be removed from the
* return.
* @return A cleansed version of rawProperties.
*/
public static Map cleanupProperties(
final Map rawProperties, final String[] blackList) {
final Map newProp = new HashMap(
rawProperties.size()); // roughly the same size
for (String okey : rawProperties.keySet()) {
final String key = okey.trim();
if (blackList != null) {
boolean blackListed = false;
for (String blackKey : blackList) {
if (blackKey.equals(key)) {
blackListed = true;
break;
}
}
if (blackListed) {
continue;
}
}
final String value = rawProperties.get(key);
if (value == null || "".equals(value)) {
// remove null or empty values
continue;
}
if (isSpecifiedPropertyName(key)) {
// a well known property name
newProp.put(key, value);
} else {
// convert to a custom property name
newProp.put(adaptToCustomPropertyName(key), value);
}
}
return newProp;
}
/**
* Any properties which are not well known (i.e. in
* {@link BasicLTIConstants#validPropertyNames}) will be mapped to custom
* properties per the specified semantics.
*
* @deprecated See {@link #cleanupProperties(Map)}
* @param rawProperties A set of {@link Properties} that will be cleaned.
* Keys must be of type {@link String}.
* @return A cleansed version of {@link Properties}.
*/
public static Properties cleanupProperties(final Properties rawProperties) {
final Map map = cleanupProperties(
convertToMap(rawProperties), null);
return convertToProperties(map);
}
/**
* Checks to see if the passed propertyName is equal to one of the Strings
* contained in {@link BasicLTIConstants#validPropertyNames}. String
* matching is case sensitive.
*
* @param propertyName
* @return true if propertyName is equal to one of the Strings contained in
* {@link BasicLTIConstants#validPropertyNames} or is a custom parameter oe
* extension parameter ; else return false.
*/
public static boolean isSpecifiedPropertyName(final String propertyName) {
boolean found = false;
if (propertyName.startsWith(CUSTOM_PREFIX)) {
return true;
}
if (propertyName.startsWith(EXTENSION_PREFIX)) {
return true;
}
if (propertyName.startsWith(OAUTH_PREFIX)) {
return true;
}
for (String key : BasicLTIConstants.validPropertyNames) {
if (key.equals(propertyName)) {
found = true;
break;
}
}
return found;
}
/**
* A simple utility method which implements the specified semantics of
* custom properties.
*
* i.e. The parameter names are mapped to lower case and any character that
* is neither a number nor letter in a parameter name is replaced with an
* "underscore".
*
* e.g. Review:Chapter=1.2.56 would map to custom_review_chapter=1.2.56.
*
* @param propertyName
* @return
*/
public static String adaptToCustomPropertyName(final String propertyName) {
if (propertyName == null || "".equals(propertyName)) {
throw new IllegalArgumentException("propertyName cannot be null");
}
String customName = propertyName.toLowerCase();
customName = CUSTOM_REGEX.matcher(customName).replaceAll(UNDERSCORE);
if (!customName.startsWith(CUSTOM_PREFIX)) {
customName = CUSTOM_PREFIX + customName;
}
return customName;
}
/**
* Add the necessary fields and sign.
*
* @deprecated See:
* {@link BasicLTIUtil#signProperties(Map, String, String, String, String, String, String, String, String, String)}
*
* @param postProp
* @param url
* @param method
* @param oauth_consumer_key
* @param oauth_consumer_secret
* @param org_id See: {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_GUID}
* @param org_desc See:
* {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_DESCRIPTION}
* @param org_url See: {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_URL}
* @return
*/
public static Properties signProperties(Properties postProp, String url,
String method, String oauth_consumer_key, String oauth_consumer_secret,
String org_id, String org_desc, String org_url) {
final Map signedMap = signProperties(
convertToMap(postProp), url, method, oauth_consumer_key,
oauth_consumer_secret, org_id, org_desc, org_url, null, null);
return convertToProperties(signedMap);
}
/**
* Add the necessary fields and sign.
*
* @param postProp
* @param url
* @param method
* @param oauth_consumer_key
* @param oauth_consumer_secret
* @param tool_consumer_instance_guid See:
* {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_GUID}
* @param tool_consumer_instance_description See:
* {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_DESCRIPTION}
* @param tool_consumer_instance_url See:
* {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_URL}
* @param tool_consumer_instance_name See:
* {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_NAME}
* @param tool_consumer_instance_contact_email See:
* {@link BasicLTIConstants#TOOL_CONSUMER_INSTANCE_CONTACT_EMAIL}
* @return
*/
public static Map signProperties(
Map postProp, String url, String method,
String oauth_consumer_key, String oauth_consumer_secret,
String tool_consumer_instance_guid,
String tool_consumer_instance_description,
String tool_consumer_instance_url, String tool_consumer_instance_name,
String tool_consumer_instance_contact_email) {
postProp = BasicLTIUtil.cleanupProperties(postProp);
if (postProp.get(LTI_VERSION) == null) {
postProp.put(LTI_VERSION, "LTI-1p0");
}
if (postProp.get(LTI_MESSAGE_TYPE) == null) {
postProp.put(LTI_MESSAGE_TYPE, "basic-lti-launch-request");
}
// Allow caller to internationalize this for us...
if (postProp.get(BASICLTI_SUBMIT) == null) {
postProp.put(BASICLTI_SUBMIT, "Launch Endpoint with BasicLTI Data");
}
if (tool_consumer_instance_guid != null) {
postProp.put(TOOL_CONSUMER_INSTANCE_GUID, tool_consumer_instance_guid);
}
if (tool_consumer_instance_description != null) {
postProp.put(TOOL_CONSUMER_INSTANCE_DESCRIPTION,
tool_consumer_instance_description);
}
if (tool_consumer_instance_url != null) {
postProp.put(TOOL_CONSUMER_INSTANCE_URL, tool_consumer_instance_url);
}
if (tool_consumer_instance_name != null) {
postProp.put(TOOL_CONSUMER_INSTANCE_NAME, tool_consumer_instance_name);
}
if (tool_consumer_instance_contact_email != null) {
postProp.put(TOOL_CONSUMER_INSTANCE_CONTACT_EMAIL,
tool_consumer_instance_contact_email);
}
if (postProp.get("oauth_callback") == null) {
postProp.put("oauth_callback", "about:blank");
}
if (oauth_consumer_key == null || oauth_consumer_secret == null) {
dPrint("No signature generated in signProperties");
return postProp;
}
OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet());
OAuthConsumer cons = new OAuthConsumer("about:blank", oauth_consumer_key,
oauth_consumer_secret, null);
OAuthAccessor acc = new OAuthAccessor(cons);
try {
oam.addRequiredParameters(acc);
// System.out.println("Base Message String\n"+OAuthSignatureMethod.getBaseString(oam)+"\n");
List> params = oam.getParameters();
Map nextProp = new HashMap();
// Convert to Map
for (final Map.Entry entry : params) {
nextProp.put(entry.getKey(), entry.getValue());
}
return nextProp;
} catch (net.oauth.OAuthException e) {
M_log.warning("BasicLTIUtil.signProperties OAuth Exception "
+ e.getMessage());
throw new Error(e);
} catch (java.io.IOException e) {
M_log.warning("BasicLTIUtil.signProperties IO Exception "
+ e.getMessage());
throw new Error(e);
} catch (java.net.URISyntaxException e) {
M_log.warning("BasicLTIUtil.signProperties URI Syntax Exception "
+ e.getMessage());
throw new Error(e);
}
}
/**
* Check if the properties are properly signed
*
* @deprecated See:
* {@link BasicLTIUtil#checkProperties(Map, String, String, String, String)}
*
* @param postProp
* @param url
* @param method
* @param oauth_consumer_key
* @param oauth_consumer_secret
* @return
*/
public static boolean checkProperties(Properties postProp, String url,
String method, String oauth_consumer_key, String oauth_consumer_secret) {
return checkProperties(convertToMap(postProp), url, method,
oauth_consumer_key, oauth_consumer_secret);
}
/**
* Check if the fields are properly signed
*
* @param postProp
* @param url
* @param method
* @param oauth_consumer_key
* @param oauth_consumer_secret
*
* @return
*/
public static boolean checkProperties(
Map postProp, String url, String method,
String oauth_consumer_key, String oauth_consumer_secret) {
OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet());
OAuthConsumer cons = new OAuthConsumer("about:blank", oauth_consumer_key,
oauth_consumer_secret, null);
OAuthValidator oav = new SimpleOAuthValidator();
OAuthAccessor acc = new OAuthAccessor(cons);
String base_string = null;
try {
base_string = OAuthSignatureMethod.getBaseString(oam);
} catch (Exception e) {
M_log.warning(e.getLocalizedMessage());
base_string = null;
return false;
}
try {
oav.validateMessage(oam, acc);
} catch (Exception e) {
M_log.warning("Provider failed to validate message");
M_log.warning(e.getLocalizedMessage());
if (base_string != null) {
M_log.warning(base_string);
}
return false;
}
return true;
}
/**
* Create the HTML to render a POST form and then automatically submit it.
* Make sure to call {@link #cleanupProperties(Properties)} before signing.
*
* @deprecated Moved to {@link #postLaunchHTML(Map, String, boolean)}
* @param cleanProperties Assumes you have called
* {@link #cleanupProperties(Properties)} beforehand.
* @param endpoint The LTI launch url.
* @param debug Useful for viewing the HTML before posting to end point.
* @return the HTML ready for IFRAME src = inclusion.
*/
public static String postLaunchHTML(final Properties cleanProperties,
String endpoint, boolean debug) {
Map map = convertToMap(cleanProperties);
return postLaunchHTML(map, endpoint, debug);
}
/**
* Create the HTML to render a POST form and then automatically submit it.
* Make sure to call {@link #cleanupProperties(Properties)} before signing.
*
* @param cleanProperties Assumes you have called
* {@link #cleanupProperties(Properties)} beforehand.
* @param endpoint The LTI launch url.
* @param debug Useful for viewing the HTML before posting to end point.
* @return the HTML ready for IFRAME src = inclusion.
*/
public static String postLaunchHTML(
final Map cleanProperties, String endpoint, boolean debug) {
if (cleanProperties == null || cleanProperties.isEmpty()) {
throw new IllegalArgumentException(
"cleanProperties == null || cleanProperties.isEmpty()");
}
if (endpoint == null) {
throw new IllegalArgumentException("endpoint == null");
}
Map newMap = null;
if (debug) {
// sort the properties for readability
newMap = new TreeMap(cleanProperties);
} else {
newMap = cleanProperties;
}
StringBuilder text = new StringBuilder();
// paint form
text.append("\n");
text.append("\n");
text.append("\n");
// Paint the auto-pop up if we are transitioning from https: to http:
// and are not already the top frame...
text.append("\n");
// paint debug output
if (debug) {
text.append("\n");
text.append("BasicLTI Endpoint\n");
text.append(endpoint);
text.append("\n\n");
text.append("BasicLTI Parameters:\n");
for (Entry entry : newMap.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (value == null) {
continue;
}
text.append(htmlspecialchars(key));
text.append("=");
text.append(htmlspecialchars(value));
text.append("\n");
}
text.append("
\n");
} else {
// paint auto submit script
text
.append(" \n");
}
String htmltext = text.toString();
return htmltext;
}
/**
* @deprecated See: {@link #parseDescriptor(Map, Map, String)}
* @param launch_info Variable is mutated by this method.
* @param postProp Variable is mutated by this method.
* @param descriptor
* @return
*/
public static boolean parseDescriptor(Properties launch_info,
Properties postProp, String descriptor) {
// this is an ugly copy/paste of the non-@deprecated method
// could not convert data types as they variables get mutated (ugh)
Map tm = null;
try {
tm = XMLMap.getFullMap(descriptor.trim());
} catch (Exception e) {
M_log.warning("BasicLTIUtil exception parsing BasicLTI descriptor: "
+ e.getMessage());
return false;
}
if (tm == null) {
M_log.warning("Unable to parse XML in parseDescriptor");
return false;
}
String launch_url = StringUtils.stripToNull(XMLMap.getString(tm, "/basic_lti_link/launch_url"));
String secure_launch_url = StringUtils.stripToNull(XMLMap.getString(tm, "/basic_lti_link/secure_launch_url"));
if (launch_url == null && secure_launch_url == null) {
return false;
}
setProperty(launch_info, "launch_url", launch_url);
setProperty(launch_info, "secure_launch_url", secure_launch_url);
// Extensions for hand-authored placements - The export process should scrub these
setProperty(launch_info, "key", StringUtils.stripToNull(XMLMap.getString(tm, "/basic_lti_link/x-secure/launch_key")));
setProperty(launch_info, "secret", StringUtils.stripToNull(XMLMap.getString(tm, "/basic_lti_link/x-secure/launch_secret")));
List