bigbluebutton-Github/bbb-lti/grails-app/controllers/org/bigbluebutton/ToolController.groovy

392 lines
19 KiB
Groovy
Raw Normal View History

package org.bigbluebutton
/*
BigBlueButton open source conferencing system - http://www.bigbluebutton.org/
Copyright (c) 2012 BigBlueButton Inc. and by respective authors (see below).
This program is free software; you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
Foundation; either version 3.0 of the License, or (at your option) any later
version.
BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
*/
import java.util.ArrayList
import java.util.HashMap
import java.util.List
import java.util.Map
import java.util.Properties
import org.apache.commons.codec.digest.DigestUtils
import net.oauth.OAuthMessage
import net.oauth.signature.OAuthSignatureMethod
import net.oauth.signature.HMAC_SHA1
import org.bigbluebutton.lti.Parameter
class ToolController {
private static final String CONTROLLER_NAME = 'ToolController'
private static final String RESP_CODE_SUCCESS = 'SUCCESS'
private static final String RESP_CODE_FAILED = 'FAILED'
private static final String REQUEST_METHOD = "request_method";
LtiService ltiService
BigbluebuttonService bigbluebuttonService
def test = {
log.debug CONTROLLER_NAME + "#test"
render(text: "<xml></xml>", contentType: "text/xml", encoding: "UTF-8")
}
def index = {
log.debug CONTROLLER_NAME + "#index"
if( ltiService.consumerMap == null) ltiService.initConsumerMap()
setLocalization(params)
params.put(REQUEST_METHOD, request.getMethod().toUpperCase())
2014-03-27 03:45:05 +08:00
ltiService.logParameters(params)
if( request.post ){
def endPoint = (request.isSecure()?"https":"http") + "://" + ltiService.endPoint + "/" + grailsApplication.metadata['app.name'] + "/" + params.get("controller") + (params.get("format") != null? "." + params.get("format"): "")
Map<String, String> result = new HashMap<String, String>()
ArrayList<String> missingParams = new ArrayList<String>()
log.debug "Checking for required parameters"
2014-03-27 03:45:05 +08:00
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( !"extended".equals(ltiService.mode) ) {
log.debug "LTI service running in simple mode."
result = doJoinMeeting(params)
} else {
log.debug "LTI service running in extended mode."
if ( !Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) ) {
log.debug "No bbb_record parameter was sent; immediately redirecting to BBB session!"
result = doJoinMeeting(params)
}
}
} else {
2014-03-27 03:45:05 +08:00
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.")
}
} else {
String missingStr = ""
for(String str:missingParams) {
missingStr += str + ", ";
}
result.put("resultMessageKey", "MissingRequiredParameter")
result.put("resultMessage", "Missing parameters [$missingStr]")
}
2014-03-27 03:45:05 +08:00
if( result.containsKey("resultMessageKey") ) {
log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
2014-03-27 03:45:05 +08:00
} else {
session["params"] = params
List<Object> recordings = bigbluebuttonService.getRecordings(params)
for(Map<String, Object> recording: recordings){
/// Calculate duration
long endTime = Long.parseLong((String)recording.get("endTime"))
endTime -= (endTime % 1000)
long startTime = Long.parseLong((String)recording.get("startTime"))
startTime -= (startTime % 1000)
int duration = (endTime - startTime) / 60000
/// Add duration
recording.put("duration", duration )
}
render(view: "index", model: ['params': params, 'recordingList': recordings, 'ismoderator': bigbluebuttonService.isModerator(params)])
}
} else {
render(text: getCartridgeXML(), contentType: "text/xml", encoding: "UTF-8")
}
}
2014-03-27 03:45:05 +08:00
def join = {
if( ltiService.consumerMap == null) ltiService.initConsumerMap()
log.debug CONTROLLER_NAME + "#join"
Map<String, String> result
2014-03-27 03:45:05 +08:00
def sessionParams = session["params"]
2014-03-27 03:45:05 +08:00
if( sessionParams != null ) {
log.debug "params: " + params
log.debug "sessionParams: " + sessionParams
result = doJoinMeeting(sessionParams)
} else {
result = new HashMap<String, String>()
result.put("resultMessageKey", "InvalidSession")
result.put("resultMessage", "Invalid session. User can not execute this action.")
}
2014-03-27 03:45:05 +08:00
if( result.containsKey("resultMessageKey")) {
log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
}
}
def publish = {
log.debug CONTROLLER_NAME + "#publish"
Map<String, String> result
2014-03-27 03:45:05 +08:00
def sessionParams = session["params"]
2014-03-27 03:45:05 +08:00
if( sessionParams == null ) {
result = new HashMap<String, String>()
result.put("resultMessageKey", "InvalidSession")
result.put("resultMessage", "Invalid session. User can not execute this action.")
} else if ( !bigbluebuttonService.isModerator(sessionParams) ) {
result = new HashMap<String, String>()
result.put("resultMessageKey", "NotAllowed")
result.put("resultMessage", "User not allowed to execute this action.")
} else {
log.debug "params: " + params
log.debug "sessionParams: " + sessionParams
//Execute the publish command
result = bigbluebuttonService.doPublishRecordings(params)
}
2014-03-27 03:45:05 +08:00
if( result.containsKey("resultMessageKey")) {
log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
} else {
List<Object> recordings = bigbluebuttonService.getRecordings(sessionParams)
for(Map<String, Object> recording: recordings){
/// Calculate duration
long endTime = Long.parseLong((String)recording.get("endTime"))
endTime -= (endTime % 1000)
long startTime = Long.parseLong((String)recording.get("startTime"))
startTime -= (startTime % 1000)
int duration = (endTime - startTime) / 60000
/// Add duration
recording.put("duration", duration )
}
render(view: "index", model: ['params': sessionParams, 'recordingList': recordings, 'ismoderator': bigbluebuttonService.isModerator(sessionParams)])
}
}
def delete = {
log.debug CONTROLLER_NAME + "#delete"
Map<String, String> result
2014-03-27 03:45:05 +08:00
def sessionParams = session["params"]
2014-03-27 03:45:05 +08:00
if( sessionParams == null ) {
result = new HashMap<String, String>()
result.put("resultMessageKey", "InvalidSession")
result.put("resultMessage", "Invalid session. User can not execute this action.")
} else if ( !bigbluebuttonService.isModerator(sessionParams) ) {
result = new HashMap<String, String>()
result.put("resultMessageKey", "NotAllowed")
result.put("resultMessage", "User not allowed to execute this action.")
} else {
log.debug "params: " + params
log.debug "sessionParams: " + sessionParams
//Execute the delete command
result = bigbluebuttonService.doDeleteRecordings(params)
}
2014-03-27 03:45:05 +08:00
if( result.containsKey("resultMessageKey")) {
log.debug "Error [resultMessageKey:'" + result.get("resultMessageKey") + "', resultMessage:'" + result.get("resultMessage") + "']"
render(view: "error", model: ['resultMessageKey': result.get("resultMessageKey"), 'resultMessage': result.get("resultMessage")])
} else {
List<Object> recordings = bigbluebuttonService.getRecordings(sessionParams)
for(Map<String, Object> recording: recordings){
/// Calculate duration
long endTime = Long.parseLong((String)recording.get("endTime"))
endTime -= (endTime % 1000)
long startTime = Long.parseLong((String)recording.get("startTime"))
startTime -= (startTime % 1000)
int duration = (endTime - startTime) / 60000
/// Add duration
recording.put("duration", duration )
}
2014-03-27 03:45:05 +08:00
render(view: "index", model: ['params': sessionParams, 'recordingList': recordings, 'ismoderator': bigbluebuttonService.isModerator(sessionParams)])
}
}
2014-03-27 03:45:05 +08:00
private void setLocalization(params){
String locale = params.get(Parameter.LAUNCH_LOCALE)
locale = (locale == null || locale.equals("")?"en":locale)
String[] localeCodes = locale.split("_")
//Localize the default welcome message
if( localeCodes.length > 1 )
session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0], localeCodes[1])
else
session['org.springframework.web.servlet.i18n.SessionLocaleResolver.LOCALE'] = new Locale(localeCodes[0])
}
private Object doJoinMeeting(params) {
Map<String, String> result = new HashMap<String, String>()
setLocalization(params)
2014-03-27 03:45:05 +08:00
String welcome = message(code: "bigbluebutton.welcome.header", args: ["\"{0}\"", "\"{1}\""]) + "<br>"
log.debug "Localized default welcome message: [" + welcome + "]"
// Check for [custom_]welcome parameter being passed from the LTI
if ( params.containsKey(Parameter.CUSTOM_WELCOME) && params.get(Parameter.CUSTOM_WELCOME) != null ) {
welcome = params.get(Parameter.CUSTOM_WELCOME) + "<br>"
log.debug "Overriding default welcome message with: [" + welcome + "]"
}
2014-03-27 03:45:05 +08:00
if ( params.containsKey(Parameter.CUSTOM_RECORD) && Boolean.parseBoolean(params.get(Parameter.CUSTOM_RECORD)) ) {
2014-03-27 03:45:05 +08:00
welcome += "<br><b>" + message(code: "bigbluebutton.welcome.record") + "</b><br>"
log.debug "Adding record warning to welcome message, welcome is now: [" + welcome + "]"
}
if ( params.containsKey(Parameter.CUSTOM_DURATION) && Integer.parseInt(params.get(Parameter.CUSTOM_DURATION)) > 0 ) {
welcome += "<br><b>" + message(code: "bigbluebutton.welcome.duration", args: [params.get(Parameter.CUSTOM_DURATION)]) + "</b><br>"
2014-03-27 03:45:05 +08:00
log.debug "Adding duration warning to welcome message, welcome is now: [" + welcome + "]"
}
welcome += "<br>" + message(code: "bigbluebutton.welcome.footer") + "<br>"
log.debug "Localized default welcome message including footer: [" + welcome + "]"
String destinationURL = bigbluebuttonService.getJoinURL(params, welcome, ltiService.mode)
log.debug "redirecting to " + destinationURL
if( destinationURL != null ) {
redirect(url:destinationURL)
} else {
result.put("resultMessageKey", "BigBlueButtonServerError")
result.put("resultMessage", "The join could not be completed")
}
return result
}
/**
* Assemble all parameters passed that is required to sign the request.
* @param the HTTP request parameters
* @return the key:val pairs needed for Basic LTI
*/
private Properties sanitizePrametersForBaseString(params) {
log.debug params
Properties reqProp = new Properties();
for (String key : params.keySet()) {
if (key == "action" || key == "controller" || key == "format") {
// Ignore as these are the grails controller and action tied to this request.
continue
} else if (key == "oauth_signature") {
// We don't need this as part of the base string
continue
} else if (key == "request_method") {
// As this is was added by the controller, we don't want it as part of the base string
continue
}
reqProp.setProperty(key, params.get(key));
}
return reqProp
}
/**
* Check if all required parameters have been passed in the request.
* @param params - the HTTP request parameters
* @param missingParams - a list of missing parameters
* @return - true if all required parameters have been passed in
*/
private boolean hasAllRequiredParams(Object params, Object missingParams) {
boolean hasAllParams = true
if (! ((Map<String, String>)params).containsKey(Parameter.CONSUMER_ID)) {
((ArrayList<String>)missingParams).add(Parameter.CONSUMER_ID);
hasAllParams = false;
}
if (! ((Map<String, String>)params).containsKey(Parameter.OAUTH_SIGNATURE)) {
((ArrayList<String>)missingParams).add(Parameter.OAUTH_SIGNATURE);
hasAllParams = false;
}
if (! ((Map<String, String>)params).containsKey(Parameter.RESOURCE_LINK_ID)) {
((ArrayList<String>)missingParams).add(Parameter.RESOURCE_LINK_ID);
hasAllParams = false;
}
return hasAllParams
}
/**
* Check if the passed signature is valid.
* @param method - POST or GET method used to make the request
* @param URL - The target URL for the Basic LTI tool
* @param conSecret - The consumer secret key
* @param postProp - the parameters passed in from the tool
* @param signature - the passed in signature calculated from the client
* @return - TRUE if the signatures matches the calculated signature
*/
private boolean checkValidSignature(String method, String URL, String conSecret, Object postProp, String signature) {
log.debug( "Starting checkValidSignature()" )
2014-03-27 03:45:05 +08:00
OAuthMessage oam = new OAuthMessage(method, URL, ((Properties)postProp).entrySet())
log.debug( "OAuthMessage oam = " + oam.toString() )
2014-03-27 03:45:05 +08:00
HMAC_SHA1 hmac = new HMAC_SHA1()
log.debug( "HMAC_SHA1 hmac = " + hmac.toString() )
2014-03-27 03:45:05 +08:00
hmac.setConsumerSecret(conSecret)
2014-03-27 03:45:05 +08:00
log.debug("Base Message String = [ " + hmac.getBaseString(oam) + " ]\n")
String calculatedSignature = hmac.getSignature(hmac.getBaseString(oam))
2014-03-27 03:45:05 +08:00
log.debug("Calculated: " + calculatedSignature + " Received: " + signature)
return calculatedSignature.equals(signature)
}
private String getCartridgeXML(){
def lti_endpoint = ltiService.retrieveBasicLtiEndpoint() + '/' + grailsApplication.metadata['app.name']
def launch_url = 'http://' + lti_endpoint + '/tool'
def secure_launch_url = 'https://' + lti_endpoint + '/tool'
def icon = 'http://' + lti_endpoint + '/images/icon.ico'
def secure_icon = 'https://' + lti_endpoint + '/images/icon.ico'
def isSSLEnabled = ltiService.isSSLEnabled('https://' + lti_endpoint + '/tool/test')
def cartridge = '' +
'<?xml version="1.0" encoding="UTF-8"?>' +
'<cartridge_basiclti_link xmlns="http://www.imsglobal.org/xsd/imslticc_v1p0"' +
' xmlns:blti = "http://www.imsglobal.org/xsd/imsbasiclti_v1p0"' +
' xmlns:lticm ="http://www.imsglobal.org/xsd/imslticm_v1p0"' +
' xmlns:lticp ="http://www.imsglobal.org/xsd/imslticp_v1p0"' +
' xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"' +
' xsi:schemaLocation = "http://www.imsglobal.org/xsd/imslticc_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticc_v1p0.xsd' +
' http://www.imsglobal.org/xsd/imsbasiclti_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imsbasiclti_v1p0.xsd' +
' http://www.imsglobal.org/xsd/imslticm_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticm_v1p0.xsd' +
' http://www.imsglobal.org/xsd/imslticp_v1p0 http://www.imsglobal.org/xsd/lti/ltiv1p0/imslticp_v1p0.xsd">' +
' <blti:title>BigBlueButton</blti:title>' +
' <blti:description>Single Sign On into BigBlueButton</blti:description>' +
' <blti:launch_url>' + launch_url + '</blti:launch_url>' +
(isSSLEnabled? ' <blti:secure_launch_url>' + secure_launch_url + '</blti:secure_launch_url>': '') +
' <blti:icon>' + icon + '</blti:icon>' +
(isSSLEnabled? ' <blti:secure_icon>' + secure_icon + '</blti:secure_icon>': '') +
' <blti:vendor>' +
' <lticp:code>BBB</lticp:code>' +
' <lticp:name>BigBlueButton</lticp:name>' +
' <lticp:description>Open source web conferencing system for distance learning.</lticp:description>' +
' <lticp:url>http://www.bigbluebutton.org/</lticp:url>' +
' </blti:vendor>' +
' <cartridge_bundle identifierref="BLTI001_Bundle"/>' +
' <cartridge_icon identifierref="BLTI001_Icon"/>' +
'</cartridge_basiclti_link>'
return cartridge
}
}