From b7de6d897b6ad749c39730b7c572f835d27077a7 Mon Sep 17 00:00:00 2001 From: jfederico Date: Thu, 3 Mar 2016 15:56:54 -0500 Subject: [PATCH] bbb-lti: Implemented changes to the LTI integration - Fixed issue with app not responding to https requests - Added configuration for ignoring/considering lti key/secret - Extended logParameters to show them ordered --- bbb-lti/grails-app/conf/lti.properties | 6 +- .../org/bigbluebutton/ToolController.groovy | 68 +++++++++-------- .../org/bigbluebutton/LtiService.groovy | 76 +++++++++++-------- bbb-lti/lti.nginx | 8 +- 4 files changed, 87 insertions(+), 71 deletions(-) diff --git a/bbb-lti/grails-app/conf/lti.properties b/bbb-lti/grails-app/conf/lti.properties index 236dd12acf..c2a898c2d5 100644 --- a/bbb-lti/grails-app/conf/lti.properties +++ b/bbb-lti/grails-app/conf/lti.properties @@ -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: [|extended] ltiMode=extended +# Defines if LTI credentials are required +# Format: [false|] +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} diff --git a/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy b/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy index 0e0ae92d6d..e0cfe3b479 100644 --- a/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy +++ b/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy @@ -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 result = new HashMap() ArrayList missingParams = new ArrayList() 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 - } } diff --git a/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy b/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy index 5f610351e1..2048d06383 100644 --- a/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy +++ b/bbb-lti/grails-app/services/org/bigbluebutton/LtiService.groovy @@ -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 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 ordered_params = new LinkedHashMap(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); + } } diff --git a/bbb-lti/lti.nginx b/bbb-lti/lti.nginx index 62a83d64ee..747d2a46be 100644 --- a/bbb-lti/lti.nginx +++ b/bbb-lti/lti.nginx @@ -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;