Index: lams_bb_integration/RELEASE_NOTES.TXT =================================================================== diff -u -rae04251a89574dd7737a7f54f8c52a6bb716d311 -r840e92f9895380a9c595ef0c05e19e03a917b7bd --- lams_bb_integration/RELEASE_NOTES.TXT (.../RELEASE_NOTES.TXT) (revision ae04251a89574dd7737a7f54f8c52a6bb716d311) +++ lams_bb_integration/RELEASE_NOTES.TXT (.../RELEASE_NOTES.TXT) (revision 840e92f9895380a9c595ef0c05e19e03a917b7bd) @@ -60,3 +60,7 @@ 1.2.5 Release Fixes =================== * LDEV-3285: include lessonid in integration hash + +1.2.6 Release Fixes +=================== +* LDEV-3382: Add datetime parameter check to LoginRequest \ No newline at end of file Index: lams_bb_integration/WEB-INF/bb-manifest.xml =================================================================== diff -u -rae04251a89574dd7737a7f54f8c52a6bb716d311 -r840e92f9895380a9c595ef0c05e19e03a917b7bd --- lams_bb_integration/WEB-INF/bb-manifest.xml (.../bb-manifest.xml) (revision ae04251a89574dd7737a7f54f8c52a6bb716d311) +++ lams_bb_integration/WEB-INF/bb-manifest.xml (.../bb-manifest.xml) (revision 840e92f9895380a9c595ef0c05e19e03a917b7bd) @@ -5,7 +5,7 @@ - + Index: lams_bb_integration/build.xml =================================================================== diff -u -rae04251a89574dd7737a7f54f8c52a6bb716d311 -r840e92f9895380a9c595ef0c05e19e03a917b7bd --- lams_bb_integration/build.xml (.../build.xml) (revision ae04251a89574dd7737a7f54f8c52a6bb716d311) +++ lams_bb_integration/build.xml (.../build.xml) (revision 840e92f9895380a9c595ef0c05e19e03a917b7bd) @@ -2,7 +2,7 @@ - + @@ -53,6 +53,7 @@ + Index: lams_bb_integration/conf/main.properties =================================================================== diff -u --- lams_bb_integration/conf/main.properties (revision 0) +++ lams_bb_integration/conf/main.properties (revision 840e92f9895380a9c595ef0c05e19e03a917b7bd) @@ -0,0 +1 @@ +lams.server.time.refresh.interval=24 \ No newline at end of file Index: lams_bb_integration/src/org/lamsfoundation/ld/integration/blackboard/LamsSecurityUtil.java =================================================================== diff -u -ra6321ec69f3de68d5f6cbdc44f20edf5ca42e6f8 -r840e92f9895380a9c595ef0c05e19e03a917b7bd --- lams_bb_integration/src/org/lamsfoundation/ld/integration/blackboard/LamsSecurityUtil.java (.../LamsSecurityUtil.java) (revision a6321ec69f3de68d5f6cbdc44f20edf5ca42e6f8) +++ lams_bb_integration/src/org/lamsfoundation/ld/integration/blackboard/LamsSecurityUtil.java (.../LamsSecurityUtil.java) (revision 840e92f9895380a9c595ef0c05e19e03a917b7bd) @@ -22,6 +22,7 @@ */ package org.lamsfoundation.ld.integration.blackboard; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; @@ -35,6 +36,8 @@ import java.rmi.RemoteException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.Properties; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -44,7 +47,11 @@ import org.apache.log4j.Logger; import org.w3c.dom.Document; +import blackboard.persist.PersistenceException; import blackboard.platform.context.Context; +import blackboard.portal.data.ExtraInfo; +import blackboard.portal.data.PortalExtraInfo; +import blackboard.portal.servlet.PortalUtil; /** * This class creates URLs, servlet calls and webservice calls for communication with LAMS @@ -54,6 +61,24 @@ public class LamsSecurityUtil { private static Logger logger = Logger.getLogger(LamsSecurityUtil.class); + + /** + * How often it should refresh LAMS server time. Measured in hours. + */ + private static long INTERVAL; + + static { + //set default value + INTERVAL = 24; + + Properties props = new Properties(); + try { + props.load(LamsSecurityUtil.class.getResourceAsStream("/main.properties")); + INTERVAL = Long.parseLong(props.getProperty("lams.server.time.refresh.interval")); + } catch (IOException e) { + logger.error("Error loading propertis from main.properties file due to " + e.getMessage()); + } + } /** * Generates login requests to LAMS for author, monitor and learner @@ -65,9 +90,11 @@ * @param lsid * lesson id. It is expected to be present in case of "monitor" and "learnerStrictAuth" * @return a url pointing to the LAMS lesson, monitor, author session + * @throws IOException + * @throws PersistenceException * @throws Exception */ - public static String generateRequestURL(Context ctx, String method, String lsid) { + public static String generateRequestURL(Context ctx, String method, String lsid) throws PersistenceException, IOException { String serverAddr = getServerAddress(); String serverId = getServerID(); @@ -78,18 +105,29 @@ throw new RuntimeException("Configuration Exception " + serverAddr + ", " + serverId); } - String timestamp = new Long(System.currentTimeMillis()).toString(); + String timestamp = getServerTime(); String username = ctx.getUser().getUserName(); String firstName = ctx.getUser().getGivenName(); String lastName = ctx.getUser().getFamilyName(); String email = ctx.getUser().getEmailAddress(); - String hash = generateAuthenticationHash(timestamp, username, method, lsid, serverId); String courseId = ctx.getCourse().getCourseId(); - String locale = ctx.getUser().getLocale(); String country = getCountry(locale); String lang = getLanguage(locale); + String secretkey = LamsPluginUtil.getSecretKey(); + + // in case of learnerStrictAuth we should also include lsid value when creating hash: [ts + uid + method + lsid + // + serverID + serverKey] + // regular case: [ts + uid + method + serverID + serverKey] + String plaintext = "learnerStrictAuth".equals(method) ? timestamp.toLowerCase().trim() + + username.toLowerCase().trim() + method.toLowerCase().trim() + lsid.toLowerCase().trim() + + serverId.toLowerCase().trim() + secretkey.toLowerCase().trim() : timestamp.toLowerCase().trim() + + username.toLowerCase().trim() + method.toLowerCase().trim() + serverId.toLowerCase().trim() + + secretkey.toLowerCase().trim(); + // generate authentication hash code to validate parameters + String hash = sha1(plaintext); + String url; try { url = serverAddr + "/LoginRequest?" + "&uid=" + URLEncoder.encode(username, "UTF8") + "&method=" + method @@ -210,23 +248,8 @@ serviceURL += "&folderID=" + folderId; } - URL url = new URL(serviceURL); - URLConnection conn = url.openConnection(); - if (!(conn instanceof HttpURLConnection)) { - throw new RuntimeException("Unable to open connection to: " + serviceURL); - } + InputStream is = LamsSecurityUtil.callLamsServer(serviceURL); - HttpURLConnection httpConn = (HttpURLConnection) conn; - - if (httpConn.getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new RuntimeException( - "Problem with getting LAMS learning designs. LAMS server responded with HTTP response code: " - + httpConn.getResponseCode() + ", HTTP response message: " - + httpConn.getResponseMessage()); - } - - InputStream is = conn.getInputStream(); - // Read/convert response to a String StringWriter writer = new StringWriter(); IOUtils.copy(is, writer, "UTF-8"); @@ -286,8 +309,6 @@ String timestamp = new Long(System.currentTimeMillis()).toString(); String hash = generateAuthenticationHash(timestamp, username, serverId); - // (serverId, datetime, hashValue, username, ldId, courseId, title, desc, country, lang) - String serviceURL = serverAddr + "/services/xml/LessonManager?" + "&serverId=" + URLEncoder.encode(serverId, "utf8") + "&datetime=" + timestamp + "&username=" + URLEncoder.encode(username, "utf8") + "&hashValue=" + hash + "&courseId=" @@ -296,23 +317,9 @@ + URLEncoder.encode(title, "utf8").trim() + "&desc=" + URLEncoder.encode(desc, "utf8").trim(); logger.info("LAMS START LESSON Req: " + serviceURL); - // System.out.println("START LESSON: " + serviceURL); - URL url = new URL(serviceURL); - URLConnection conn = url.openConnection(); - if (!(conn instanceof HttpURLConnection)) { - throw new RuntimeException("Unable to open connection to: " + serviceURL); - } - - HttpURLConnection httpConn = (HttpURLConnection) conn; - - if (httpConn.getResponseCode() != HttpURLConnection.HTTP_OK) { - throw new RuntimeException("HTTP Response Code: " + httpConn.getResponseCode() - + ", HTTP Response Message: " + httpConn.getResponseMessage()); - } - // InputStream is = url.openConnection().getInputStream(); - InputStream is = conn.getInputStream(); + InputStream is = LamsSecurityUtil.callLamsServer(serviceURL); // parse xml response DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); @@ -348,8 +355,116 @@ } } + + /** + * Make a call to LAMS server. + * + * @param serviceURL + * @return resulted InputStream + * @throws IOException + */ + private static InputStream callLamsServer(String serviceURL) throws IOException { + URL url = new URL(serviceURL); + URLConnection conn = url.openConnection(); + if (!(conn instanceof HttpURLConnection)) { + throw new RuntimeException("Unable to open connection to: " + serviceURL); + } + HttpURLConnection httpConn = (HttpURLConnection) conn; + + if (httpConn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new RuntimeException("LAMS server responded with HTTP response code: " + httpConn.getResponseCode() + + ", HTTP response message: " + httpConn.getResponseMessage()); + } + + // InputStream is = url.openConnection().getInputStream(); + InputStream is = conn.getInputStream(); + + return is; + } + + public static String getServerTime() throws IOException, PersistenceException { + long now = (new Date()).getTime(); + + // get LamsServerTime from the storage + PortalExtraInfo pei = PortalUtil.loadPortalExtraInfo(null, null, "LamsServerTimeStorage"); + ExtraInfo ei = pei.getExtraInfo(); + + String lamsServerTimeDeltaStr = ei.getValue("LAMSServerTimeDelta"); + long lamsServerTimeDelta = (lamsServerTimeDeltaStr == null) ? -1 : Long.parseLong(lamsServerTimeDeltaStr); + + //check if it's time to update + String lastUpdateTimeStr = ei.getValue("lastUpdateTime"); + long lastUpdateTime = (lastUpdateTimeStr == null) ? -1 : Long.parseLong(lastUpdateTimeStr); + long lamsServerTime; + + if ((lamsServerTimeDeltaStr == null) || (lastUpdateTime + INTERVAL * 60 * 60 * 1000 < now)) { + + // refresh time from LAMS server + String serverAddr = getServerAddress(); + String serviceURL = serverAddr + "/services/getServerTime"; + logger.info("LAMS Get Server Time request: " + serviceURL); + InputStream is = LamsSecurityUtil.callLamsServer(serviceURL); + + StringWriter writer = new StringWriter(); + IOUtils.copy(is, writer, "UTF-8"); + + try { + lamsServerTime = Long.parseLong(writer.toString().trim()); + } catch (NumberFormatException e) { + throw new RuntimeException("LAMS server returned wrong time format on call to /service/getServerTime", + e); + } + lamsServerTimeDelta = now - lamsServerTime; + + // Store LAMSServerTime and lastUpdateTime to the storage + ei.setValue("LAMSServerTimeDelta", "" + lamsServerTimeDelta); + ei.setValue("lastUpdateTime", "" + now); + PortalUtil.savePortalExtraInfo(pei); + } else { + + //no need to refresh - use stored value + lamsServerTime = now - lamsServerTimeDelta; + } + + return "" + lamsServerTime; + } + /** + * Gets the app.version property value from + * the ./main.properties file of the base folder + * + * @return app.version string + * @throws IOException + */ + public static String getAppVersion() throws IOException{ + + String versionString = null; + + //to load application's properties, we use this class + Properties mainProperties = new Properties(); + + FileInputStream file; + + //the base folder is ./, the root of the main.properties file + String path = "./main.properties"; + + //load the file handle for main.properties + file = new FileInputStream(path); + + //load all the properties from this file + mainProperties.load(file); + + //we have loaded the properties, so close the file handle + file.close(); + + //retrieve the property we are intrested, the app.version + versionString = mainProperties.getProperty("app.version"); + + return versionString; + } + + /** * @return gets server address from the lams.properties file */ public static String getServerAddress() { @@ -377,76 +492,7 @@ return LamsPluginUtil.getProperties().getProperty(LamsPluginUtil.PROP_REQ_SRC); } -// /** -// * -// * @param node -// * the node from which to do the recursive conversion -// * @return the string converted to tigra format -// */ -// public static String convertToTigraFormat(Node node) { -// -// StringBuilder sb = new StringBuilder(); -// -// if (node.getNodeName().equals(Constants.ELEM_FOLDER)) { -// -// StringBuilder attribute = new StringBuilder(node.getAttributes().getNamedItem(Constants.ATTR_NAME) -// .getNodeValue().replace("'", "\\'")); -// -// sb.append("{type:'Text', label:'" + attribute + "',id:0"); -// -// NodeList children = node.getChildNodes(); -// if (children.getLength() == 0) { -// sb.append(",expanded:0,children:[{type:'HTML',html:'-empty-', id:0}]}"); -// return sb.toString(); -// } else { -// sb.append(",children:["); -// -// -// sb.append(convertToTigraFormat(children.item(0))); -// for (int i = 1; i < children.getLength(); i++) { -// sb.append(',').append(convertToTigraFormat(children.item(i))); -// } -// -// sb.append("]}"); -// } -// -// } else if (node.getNodeName().equals(Constants.ELEM_LEARNING_DESIGN)) { -// -// -//// $ld_name = preg_replace("/'/", "$1\'", $xml_node['@']['name']); -//// $output .= "{type:'Text',label:'" . $ld_name . "',id:'" . $xml_node['@']['resourceId'] . "'}"; -// -// StringBuilder attrName = new StringBuilder(node.getAttributes().getNamedItem(Constants.ATTR_NAME) -// .getNodeValue().replace("'", "\\'")); -// StringBuilder attrResId = new StringBuilder(node.getAttributes().getNamedItem(Constants.ATTR_RESOURCE_ID) -// .getNodeValue().replace("'", "\\'")); -// -// sb.append("{type:'Text',label:'"); -// sb.append(attrName); -// sb.append("',id:'"); -// sb.append(attrResId); -// sb.append("'}"); -// } -// return sb.toString(); -// } - // generate authentication hash code to validate parameters - public static String generateAuthenticationHash(String datetime, String login, String method, String lsid, String serverId) { - String secretkey = LamsPluginUtil.getSecretKey(); - - //in case of learnerStrictAuth we should also include lsid value when creating hash: [ts + uid + method + lsid + serverID + serverKey] - //regular case: [ts + uid + method + serverID + serverKey] - String plaintext = "learnerStrictAuth".equals(method) ? datetime.toLowerCase().trim() - + login.toLowerCase().trim() + method.toLowerCase().trim() + lsid.toLowerCase().trim() - + serverId.toLowerCase().trim() + secretkey.toLowerCase().trim() : datetime.toLowerCase().trim() - + login.toLowerCase().trim() + method.toLowerCase().trim() + serverId.toLowerCase().trim() - + secretkey.toLowerCase().trim(); - - String hash = sha1(plaintext); - return hash; - } - - // generate authentication hash code to validate parameters public static String generateAuthenticationHash(String datetime, String login, String serverId) { String secretkey = getServerKey(); @@ -458,16 +504,6 @@ return hash; } - // generate authentication hash code to validate parameters - public static String generateAuthenticationHash(String datetime, String serverId) throws NoSuchAlgorithmException { - String secretkey = LamsPluginUtil.getSecretKey(); - - String plaintext = datetime.toLowerCase().trim() + serverId.toLowerCase().trim() - + secretkey.toLowerCase().trim(); - - return sha1(plaintext); - } - /** * The parameters are: uid - the username on the external system method - either author, monitor or learner ts - * timestamp sid - serverID str is [ts + uid + method + serverID + serverKey] (Note: all lower case)