Merge pull request #3038 from jfederico/master

bbb-lti: Implemented improvements to the LTI integration
This commit is contained in:
Richard Alam 2016-03-07 11:58:05 -05:00
commit 603fa151c1
4 changed files with 87 additions and 71 deletions

View File

@ -32,12 +32,15 @@ bigbluebuttonSalt=bbb_salt
# e.g. localhost or localhost:port
ltiEndPoint=localhost
# The list of consumers allowed to access this lti service.
# Format: {consumerId1:sharedSecret1}
# Format: {consumerId1:sharedSecret1,consumerId2:sharedSecret2,consumerIdN:sharedSecretN}
##ltiConsumers=bbb:bbb_salt
ltiConsumers=bbb:welcome
# The mode used to interact with BigBlueButton
# Format: [<simple>|extended]
ltiMode=extended
# Defines if LTI credentials are required
# Format: [false|<true>]
ltiRestrictedAccess=true
#----------------------------------------------------
# Inject configuration values into BigbluebuttonSrvice beans
@ -49,4 +52,5 @@ beans.bigbluebuttonService.salt=${bigbluebuttonSalt}
beans.ltiService.endPoint=${ltiEndPoint}
beans.ltiService.consumers=${ltiConsumers}
beans.ltiService.mode=${ltiMode}
beans.ltiService.restrictedAccess=${ltiRestrictedAccess}

View File

@ -54,17 +54,26 @@ class ToolController {
ltiService.logParameters(params)
if( request.post ){
def endPoint = getScheme() + "://" + ltiService.endPoint + "/" + grailsApplication.metadata['app.name'] + "/" + params.get("controller") + (params.get("format") != null? "." + params.get("format"): "")
def scheme = request.isSecure()? "https": "http"
def endPoint = scheme + "://" + ltiService.endPoint + "/" + grailsApplication.metadata['app.name'] + "/" + params.get("controller") + (params.get("format") != null? "." + params.get("format"): "")
log.info "endPoint: " + endPoint
Map<String, String> result = new HashMap<String, String>()
ArrayList<String> missingParams = new ArrayList<String>()
if (hasAllRequiredParams(params, missingParams)) {
def sanitizedParams = sanitizePrametersForBaseString(params)
def consumer = ltiService.getConsumer(params.get(Parameter.CONSUMER_ID))
if (consumer != null) {
log.debug "Found consumer with key " + consumer.get("key") //+ " and sharedSecret " + consumer.get("secret")
if (checkValidSignature(params.get(REQUEST_METHOD), endPoint, consumer.get("secret"), sanitizedParams, params.get(Parameter.OAUTH_SIGNATURE))) {
log.debug "The message has a valid signature."
if ( !ltiService.hasRestrictedAccess() || consumer != null) {
if (ltiService.hasRestrictedAccess() ) {
log.debug "Found consumer with key " + consumer.get("key") //+ " and sharedSecret " + consumer.get("secret")
}
if (!ltiService.hasRestrictedAccess() || checkValidSignature(params.get(REQUEST_METHOD), endPoint, consumer.get("secret"), sanitizedParams, params.get(Parameter.OAUTH_SIGNATURE))) {
if (!ltiService.hasRestrictedAccess() ) {
log.debug "Access not restricted, valid signature is not required."
} else {
log.debug "The message has a valid signature."
}
def mode = params.containsKey(Parameter.CUSTOM_MODE)? params.get(Parameter.CUSTOM_MODE): ltiService.mode
if( !"extended".equals(mode) ) {
@ -77,11 +86,13 @@ class ToolController {
result = doJoinMeeting(params)
}
}
} else {
log.debug "The message has NOT a valid signature."
result.put("resultMessageKey", "InvalidSignature")
result.put("resultMessage", "Invalid signature (" + params.get(Parameter.OAUTH_SIGNATURE) + ").")
}
} else {
result.put("resultMessageKey", "ConsumerNotFound")
result.put("resultMessage", "Consumer with id = " + params.get(Parameter.CONSUMER_ID) + " was not found.")
@ -311,12 +322,12 @@ class ToolController {
log.debug "Checking for required parameters"
boolean hasAllParams = true
if ( !params.containsKey(Parameter.CONSUMER_ID) ) {
if ( ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.CONSUMER_ID) ) {
missingParams.add(Parameter.CONSUMER_ID);
hasAllParams = false;
}
if ( !params.containsKey(Parameter.OAUTH_SIGNATURE)) {
if ( ltiService.hasRestrictedAccess() && !params.containsKey(Parameter.OAUTH_SIGNATURE)) {
missingParams.add(Parameter.OAUTH_SIGNATURE);
hasAllParams = false;
}
@ -341,22 +352,27 @@ class ToolController {
private boolean checkValidSignature(String method, String url, String conSecret, Properties postProp, String signature) {
def validSignature = false
try {
OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet())
//log.debug "OAuthMessage oam = " + oam.toString()
if ( ltiService.hasRestrictedAccess() ) {
try {
OAuthMessage oam = new OAuthMessage(method, url, postProp.entrySet())
//log.debug "OAuthMessage oam = " + oam.toString()
HMAC_SHA1 hmac = new HMAC_SHA1()
//log.debug "HMAC_SHA1 hmac = " + hmac.toString()
HMAC_SHA1 hmac = new HMAC_SHA1()
//log.debug "HMAC_SHA1 hmac = " + hmac.toString()
hmac.setConsumerSecret(conSecret)
hmac.setConsumerSecret(conSecret)
log.debug "Base Message String = [ " + hmac.getBaseString(oam) + " ]\n"
String calculatedSignature = hmac.getSignature(hmac.getBaseString(oam))
log.debug "Calculated: " + calculatedSignature + " Received: " + signature
log.debug "Base Message String = [ " + hmac.getBaseString(oam) + " ]\n"
String calculatedSignature = hmac.getSignature(hmac.getBaseString(oam))
log.debug "Calculated: " + calculatedSignature + " Received: " + signature
validSignature = calculatedSignature.equals(signature)
} catch( Exception e ) {
log.debug "Exception error: " + e.message
validSignature = calculatedSignature.equals(signature)
} catch( Exception e ) {
log.debug "Exception error: " + e.message
}
} else {
validSignature = true
}
return validSignature
@ -398,18 +414,4 @@ class ToolController {
return cartridge
}
private String getScheme(){
def scheme
if ( request.isSecure() ) {
scheme = 'https'
} else {
scheme = request.getHeader("scheme")
if ( scheme == null || !(scheme == 'http' || scheme == 'https') ) {
scheme = 'http'
}
}
return scheme
}
}

View File

@ -21,6 +21,7 @@ import java.util.Map;
import javax.crypto.spec.SecretKeySpec
import javax.crypto.Mac
import org.apache.commons.codec.binary.Base64
class LtiService {
@ -30,8 +31,7 @@ class LtiService {
def endPoint = "localhost"
def consumers = "demo:welcome"
def mode = "simple"
def ssl_enabled
def restrictedAccess = "true"
Map<String, String> consumerMap
@ -92,45 +92,55 @@ class LtiService {
return Base64.encodeBase64URLSafeString(signBytes)
}
def logParameters(Object params) {
log.debug "----------------------------------"
for( param in params ) log.debug "${param.getKey()}=${param.getValue()}"
log.debug "----------------------------------"
def logParameters(Object params, boolean debug = false) {
def divider = "----------------------------------"
Map<String, String> ordered_params = new LinkedHashMap<String, String>(params)
ordered_params = ordered_params.sort {it.key}
if( debug ) log.debug divider else log.info divider
for( param in ordered_params ) {
if( debug ) {
log.debug "${param.getKey()}=${param.getValue()}"
} else {
log.info "${param.getKey()}=${param.getValue()}"
}
}
if( debug ) log.debug divider else log.info divider
}
def boolean isSSLEnabled(String query) {
if ( ssl_enabled == null ) {
ssl_enabled = false
log.debug("Pinging SSL connection")
def ssl_enabled = false
try {
// open connection
StringBuilder urlStr = new StringBuilder(query)
URL url = new URL(urlStr.toString())
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection()
httpConnection.setUseCaches(false)
httpConnection.setDoOutput(true)
httpConnection.setRequestMethod("HEAD")
httpConnection.setConnectTimeout(5000)
httpConnection.connect()
log.debug("Pinging SSL connection")
try {
// open connection
StringBuilder urlStr = new StringBuilder(query)
URL url = new URL(urlStr.toString())
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection()
httpConnection.setUseCaches(false)
httpConnection.setDoOutput(true)
httpConnection.setRequestMethod("HEAD")
httpConnection.setConnectTimeout(5000)
httpConnection.connect()
int responseCode = httpConnection.getResponseCode()
if (responseCode == HttpURLConnection.HTTP_OK) {
ssl_enabled = true
} else {
log.debug("HTTPERROR: Message=" + "BBB server responded with HTTP status code " + responseCode)
}
int responseCode = httpConnection.getResponseCode()
if (responseCode == HttpURLConnection.HTTP_OK) {
ssl_enabled = true
} else {
log.debug("HTTPERROR: Message=" + "BBB server responded with HTTP status code " + responseCode)
}
} catch(IOException e) {
log.debug("IOException: Message=" + e.getMessage())
} catch(IllegalArgumentException e) {
log.debug("IllegalArgumentException: Message=" + e.getMessage())
} catch(Exception e) {
log.debug("Exception: Message=" + e.getMessage())
}
}
} catch(IOException e) {
log.debug("IOException: Message=" + e.getMessage())
} catch(IllegalArgumentException e) {
log.debug("IllegalArgumentException: Message=" + e.getMessage())
} catch(Exception e) {
log.debug("Exception: Message=" + e.getMessage())
}
return ssl_enabled
}
def boolean hasRestrictedAccess() {
return Boolean.parseBoolean(this.restrictedAccess);
}
}

View File

@ -1,12 +1,12 @@
# Handle request to bbb-web running within Tomcat. This is for
# the BBB-API and Presentation.
# Handle request to bbb-web running within Tomcat. This is for
# the BBB-API and Presentation.
location /lti {
proxy_pass http://127.0.0.1:8080;
proxy_redirect default;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Scheme $scheme;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
# Allow 30M uploaded presentation document.
# Allow 30M uploaded presentation document.
client_max_body_size 30m;
client_body_buffer_size 128k;