- group errors and send them all at once to the caller of the api

- make errors consistent
 - return back createTime on a create API call
 - require createTime parameter on a join API call. This way users with stale join link won't be
   able to join a running meeting.
This commit is contained in:
Richard Alam 2011-06-20 16:18:57 -04:00
parent 356c77b41f
commit 3b67e6b5ef
7 changed files with 277 additions and 99 deletions

View File

@ -36,6 +36,7 @@ import org.codehaus.groovy.grails.commons.ConfigurationHolder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import grails.converters.XML;
import org.bigbluebutton.api.ApiErrors;
class ApiController {
private static final Integer SESSION_TIMEOUT = 10800 // 3 hours
@ -65,34 +66,42 @@ class ApiController {
}
}
/* CREATE (API) */
/***********************************
* CREATE (API)
***********************************/
def create = {
String API_CALL = 'create'
log.debug CONTROLLER_NAME + "#${API_CALL}"
ApiErrors errors = new ApiErrors();
// Do we have a checksum? If not, complain.
if (StringUtils.isEmpty(params.checksum)) {
invalid("missingParamChecksum", "You must pass a checksum and query string.");
return
errors.missingParamError("checksum");
}
// Do we have a meeting name? If not, complain.
String meetingName = params.name
if (StringUtils.isEmpty(meetingName) ) {
invalid("missingParamName", "You must specify a name for the meeting.");
return
errors.missingParamError("name");
}
// Do we have a meeting id? If not, complain.
String externalMeetingId = params.meetingID
if (StringUtils.isEmpty(externalMeetingId)) {
invalid("missingParamMeetingID", "You must specify a meeting ID for the meeting.");
return
errors.missingParamError("meetingID");
}
if (errors.hasErrors()) {
respondWithErrors(errors)
return
}
// Do we agree with the checksum? If not, complain.
if (! dynamicConferenceService.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) {
invalidChecksum(); return;
errors.checksumError()
respondWithErrors(errors)
return
}
// Get the viewer and moderator password. If none is provided, generate one.
@ -125,15 +134,16 @@ class ApiController {
if (existing != null) {
log.debug "Existing conference found"
if (existing.getViewerPassword().equals(viewerPass) && existing.getModeratorPassword().equals(modPass)) {
// trying to create a conference a second time
// return success, but give extra info
// trying to create a conference a second time, return success, but give extra info
uploadDocuments(existing);
respondWithConference(existing, "duplicateWarning", "This conference was already in existence and may currently be in progress.");
} else {
// enforce meetingID unique-ness
invalid("idNotUnique", "A meeting already exists with that meeting ID. Please use a different meeting ID.");
}
return;
errors.nonUniqueMeetingIdError()
respondWithErrors(errors)
}
return;
}
// Check if this is a test meeting. NOTE: This should not belong here. Extract this out.
@ -149,16 +159,16 @@ class ApiController {
if(meta.length == 2){
meetingInfo.put(meta[1], params.get(metadata))
}
}
}
// Create a unique internal id by appending the current time. This way, the 3rd-party
// app can reuse the external meeting id.
internalMeetingId = internalMeetingId + '-' + new Long(System.currentTimeMillis()).toString()
long createTime = System.currentTimeMillis()
internalMeetingId = internalMeetingId + '-' + new Long(createTime).toString()
// Create the meeting with all passed in parameters.
Meeting meeting = new Meeting.Builder(externalMeetingId, internalMeetingId)
Meeting meeting = new Meeting.Builder(externalMeetingId, internalMeetingId, createTime)
.withName(meetingName).withMaxUsers(maxUsers).withModeratorPass(modPass)
.withViewerPass(viewerPass).withRecording(record)
.withLogoutUrl(logoutUrl).withTelVoice(telVoice).withWebVoice(webVoice).withDialNumber(dialNumber)
@ -172,43 +182,52 @@ class ApiController {
respondWithConference(meeting, null, null)
}
/**
/**********************************************
* JOIN API
*/
*********************************************/
def join = {
String API_CALL = 'join'
log.debug CONTROLLER_NAME + "#${API_CALL}"
ApiErrors errors = new ApiErrors()
// Do we have a checksum? If none, complain.
if (StringUtils.isEmpty(params.checksum)) {
invalid("missingParamChecksum", "You must pass a checksum and query string.");
return
errors.missingParamError("checksum");
}
// Do we have a name for the user joining? If none, complain.
String fullName = params.fullName
if (StringUtils.isEmpty(fullName)) {
invalid("missingParamFullName", "You must specify a name for the attendee who will be joining the meeting.");
return
errors.missingParamError("fullName");
}
// Do we have a meeting id? If none, complain.
String externalMeetingId = params.meetingID
if (StringUtils.isEmpty(externalMeetingId)) {
invalid("missingParamMeetingID", "You must specify a meeting ID for the meeting.");
return
errors.missingParamError("meetingID");
}
// Do we have a password? If not, complain.
String attPW = params.password
if (StringUtils.isEmpty(attPW)) {
invalid("missingParamPassword", "You must specify a password for the meeting.");
return
errors.missingParamError("password");
}
String createTime = params.createTime
if (StringUtils.isEmpty(createTime)) {
errors.missingParamError("createTime");
}
if (errors.hasErrors()) {
respondWithErrors(errors)
return
}
// Do we agree on the checksum? If not, complain.
if (! dynamicConferenceService.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) {
invalidChecksum(); return;
errors.checksumError()
respondWithErrors(errors)
return
}
// Everything is good so far. Translate the external meeting id to an internal meeting id. If
@ -217,13 +236,15 @@ class ApiController {
log.info("Retrieving meeting ${internalMeetingId}")
Meeting meeting = dynamicConferenceService.getMeeting(internalMeetingId);
if (meeting == null) {
invalid("invalidMeetingIdentifier", "The meeting ID that you supplied did not match any existing meetings");
return;
errors.invalidMeetingIdError();
respondWithErrors(errors)
return;
}
// Is this user joining a meeting that has been ended. If so, complain.
if (meeting.isForciblyEnded()) {
invalid("meetingForciblyEnded", "You can not re-join a meeting that has already been forcibly ended. However, once the meeting is removed from memory (according to the timeout configured on this server, you will be able to once again create a meeting with the same meeting ID");
errors.meetingForciblyEndedError();
respondWithErrors(errors)
return;
}
@ -236,7 +257,9 @@ class ApiController {
}
if (role == null) {
invalidPassword("You either did not supply a password or the password supplied is neither the attendee or moderator password for this conference."); return;
errors.invalidPasswordError()
respondWithErrors(errors)
return;
}
String webVoice = StringUtils.isEmpty(params.webVoiceConf) ? meeting.getTelVoice() : params.webVoiceConf
@ -267,29 +290,36 @@ class ApiController {
redirect(url: dynamicConferenceService.defaultClientUrl)
}
/**
* IS_MEETING_RUNNING API
*/
/*******************************************
* IS_MEETING_RUNNING API
*******************************************/
def isMeetingRunning = {
String API_CALL = 'isMeetingRunning'
log.debug CONTROLLER_NAME + "#${API_CALL}"
ApiErrors errors = new ApiErrors()
// Do we have a checksum? If none, complain.
if (StringUtils.isEmpty(params.checksum)) {
invalid("missingParamChecksum", "You must pass a checksum and query string.");
return
errors.missingParamError("checksum");
}
// Do we have a meeting id? If none, complain.
String externalMeetingId = params.meetingID
if (StringUtils.isEmpty(externalMeetingId)) {
invalid("missingParamMeetingID", "You must specify a meeting ID for the meeting.");
return
errors.missingParamError("meetingID");
}
if (errors.hasErrors()) {
respondWithErrors(errors)
return
}
// Do we agree on the checksum? If not, complain.
if (! dynamicConferenceService.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) {
invalidChecksum(); return;
errors.checksumError()
respondWithErrors(errors)
return
}
// Everything is good so far. Translate the external meeting id to an internal meeting id. If
@ -298,8 +328,9 @@ class ApiController {
log.info("Retrieving meeting ${internalMeetingId}")
Meeting meeting = dynamicConferenceService.getMeeting(internalMeetingId);
if (meeting == null) {
invalid("invalidMeetingIdentifier", "The meeting ID that you supplied did not match any existing meetings");
return;
errors.invalidMeetingIdError();
respondWithErrors(errors)
return;
}
response.addHeader("Cache-Control", "no-cache")
@ -315,36 +346,42 @@ class ApiController {
}
}
/**
/************************************
* END API
*/
************************************/
def end = {
String API_CALL = "end"
log.debug CONTROLLER_NAME + "#${API_CALL}"
log.debug CONTROLLER_NAME + "#${API_CALL}"
ApiErrors errors = new ApiErrors()
// Do we have a checksum? If none, complain.
if (StringUtils.isEmpty(params.checksum)) {
invalid("missingParamChecksum", "You must pass a checksum and query string.");
return
errors.missingParamError("checksum");
}
// Do we have a meeting id? If none, complain.
String externalMeetingId = params.meetingID
if (StringUtils.isEmpty(externalMeetingId)) {
invalid("missingParamMeetingID", "You must specify a meeting ID for the meeting.");
return
errors.missingParamError("meetingID");
}
// Do we have a password? If not, complain.
String modPW = params.password
if (StringUtils.isEmpty(modPW)) {
invalid("missingParamPassword", "You must specify a password for the meeting.");
return
errors.missingParamError("password");
}
if (errors.hasErrors()) {
respondWithErrors(errors)
return
}
// Do we agree on the checksum? If not, complain.
if (! dynamicConferenceService.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) {
invalidChecksum(); return;
errors.checksumError()
respondWithErrors(errors)
return
}
// Everything is good so far. Translate the external meeting id to an internal meeting id. If
@ -353,13 +390,15 @@ class ApiController {
log.info("Retrieving meeting ${internalMeetingId}")
Meeting meeting = dynamicConferenceService.getMeeting(internalMeetingId);
if (meeting == null) {
invalid("invalidMeetingIdentifier", "The meeting ID that you supplied did not match any existing meetings");
return;
errors.invalidMeetingIdError();
respondWithErrors(errors)
return;
}
if (meeting.getModeratorPassword().equals(modPW) == false) {
invalidPassword("You must supply the moderator password for this call.");
return;
errors.invalidPasswordError();
respondWithErrors(errors)
return;
}
meeting.setForciblyEnded(true);
@ -380,35 +419,42 @@ class ApiController {
}
}
/**
* GETMEETINGINFO API
*/
/*****************************************
* GETMEETINGINFO API
*****************************************/
def getMeetingInfo = {
String API_CALL = "getMeetingInfo"
log.debug CONTROLLER_NAME + "#${API_CALL}"
ApiErrors errors = new ApiErrors()
// Do we have a checksum? If none, complain.
if (StringUtils.isEmpty(params.checksum)) {
invalid("missingParamChecksum", "You must pass a checksum and query string.");
return
errors.missingParamError("checksum");
}
// Do we have a meeting id? If none, complain.
String externalMeetingId = params.meetingID
if (StringUtils.isEmpty(externalMeetingId)) {
invalid("missingParamMeetingID", "You must specify a meeting ID for the meeting.");
return
errors.missingParamError("meetingID");
}
// Do we have a password? If not, complain.
String modPW = params.password
if (StringUtils.isEmpty(modPW)) {
invalid("missingParamPassword", "You must specify a password for the meeting.");
return
errors.missingParamError("password");
}
if (errors.hasErrors()) {
respondWithErrors(errors)
return
}
// Do we agree on the checksum? If not, complain.
if (! dynamicConferenceService.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) {
invalidChecksum(); return;
errors.checksumError()
respondWithErrors(errors)
return
}
// Everything is good so far. Translate the external meeting id to an internal meeting id. If
@ -417,13 +463,15 @@ class ApiController {
log.info("Retrieving meeting ${internalMeetingId}")
Meeting meeting = dynamicConferenceService.getMeeting(internalMeetingId);
if (meeting == null) {
invalid("invalidMeetingIdentifier", "The meeting ID that you supplied did not match any existing meetings");
return;
errors.invalidMeetingIdError();
respondWithErrors(errors)
return;
}
if (meeting.getModeratorPassword().equals(modPW) == false) {
invalidPassword("You must supply the moderator password for this call.");
return;
errors.invalidPasswordError();
respondWithErrors(errors)
return;
}
respondWithConferenceDetails(meeting, null, null, null);
@ -435,15 +483,24 @@ class ApiController {
def getMeetings = {
String API_CALL = "getMeetings"
log.debug CONTROLLER_NAME + "#${API_CALL}"
ApiErrors errors = new ApiErrors()
// Do we have a checksum? If none, complain.
if (StringUtils.isEmpty(params.checksum)) {
invalid("missingParamChecksum", "You must pass a checksum and query string.");
return
errors.missingParamError("checksum");
}
if (errors.hasErrors()) {
respondWithErrors(errors)
return
}
// Do we agree on the checksum? If not, complain.
if (! dynamicConferenceService.isChecksumSame(API_CALL, params.checksum, request.getQueryString())) {
invalidChecksum(); return;
errors.checksumError()
respondWithErrors(errors)
return
}
Collection<Meeting> mtgs = dynamicConferenceService.getAllMeetings();
@ -750,6 +807,7 @@ class ApiController {
meetingID(meeting.getExternalId())
attendeePW(meeting.getViewerPassword())
moderatorPW(meeting.getModeratorPassword())
createTime(meeting.getCreateTime())
hasBeenForciblyEnded(meeting.isForciblyEnded() ? "true" : "false")
messageKey(msgKey == null ? "" : msgKey)
message(msg == null ? "" : msg)
@ -791,6 +849,33 @@ class ApiController {
}
}
def respondWithErrors(errorList) {
log.debug CONTROLLER_NAME + "#invalid"
response.addHeader("Cache-Control", "no-cache")
withFormat {
xml {
render(contentType:"text/xml") {
response() {
returncode(RESP_CODE_FAILED)
errors() {
errorList.keySet().each { er ->
error(key: er, message: errorList.get(er))
}
}
}
}
}
json {
log.debug "Rendering as json"
render(contentType:"text/json") {
returncode(RESP_CODE_FAILED)
messageKey(key)
message(msg)
}
}
}
}
def parseBoolean(obj) {
if (obj instanceof Number) {
return ((Number) obj).intValue() == 1;

View File

@ -0,0 +1,36 @@
package org.bigbluebutton.api;
import java.util.ArrayList;
public class ApiErrors {
private ArrayList<String[]> errors = new ArrayList<String[]>();
public void missingParamError(String param) {
errors.add(new String[] {"MissingParam", "You did not pass a " + param + " parameter."});
}
public void checksumError() {
errors.add( new String[] {"checksumError", "You did not pass the checksum security check"});
}
public void nonUniqueMeetingIdError() {
errors.add(new String[] {"NotUniqueMeetingID", "A meeting already exists with that meeting ID. Please use a different meeting ID."});
}
public void invalidMeetingIdError() {
errors.add(new String[] {"invalidMeetingId", "The meeting ID that you supplied did not match any existing meetings"});
}
public void meetingForciblyEndedError() {
errors.add(new String[] {"meetingForciblyEnded", "You can not re-join a meeting that has already been forcibly ended."});
}
public void invalidPasswordError() {
errors.add(new String[] {"invalidPassword", "The password you submitted is not valid."});
}
public boolean hasErrors() {
return errors.size() > 0;
}
}

View File

@ -41,7 +41,7 @@ public class MeetingServiceImp implements MeetingService {
}
long now = System.currentTimeMillis();
long millisSinceStored = now - m.getCreatedTime();
long millisSinceStored = now - m.getCreateTime();
long millisSinceEnd = now - m.getEndTime();
if (m.getStartTime() > 0 && millisSinceEnd > (minutesElapsedBeforeMeetingExpiration * 60000)) {

View File

@ -31,7 +31,7 @@ public class Meeting {
private String extMeetingId;
private String intMeetingId;
private int duration;
private long createdTime;
private long createdTime = 0;
private long startTime = 0;
private long endTime = 0;
private boolean forciblyEnded = false;
@ -63,8 +63,7 @@ public class Meeting {
welcomeMsg = builder.welcomeMsg;
dialNumber = builder.dialNumber;
metadata = builder.metadata;
createdTime = System.currentTimeMillis();
createdTime = builder.createdTime;
users = new ConcurrentHashMap<String, User>();
metadata.put("meetingId", extMeetingId);
@ -86,7 +85,7 @@ public class Meeting {
startTime = t;
}
public long getCreatedTime() {
public long getCreateTime() {
return createdTime;
}
@ -214,10 +213,12 @@ public class Meeting {
private String logoutUrl;
private Map<String, String> metadata;
private String dialNumber;
private long createdTime;
public Builder(String externalId, String internalId) {
public Builder(String externalId, String internalId, long createTime) {
this.externalId = externalId;
this.internalId = internalId;
this.createdTime = createTime;
}
public Builder withName(String name) {

View File

@ -0,0 +1,56 @@
package org.bigbluebutton.api.messaging;
import java.util.Map;
public class NullMessagingService implements MessagingService {
@Override
public void start() {
// TODO Auto-generated method stub
}
@Override
public void stop() {
// TODO Auto-generated method stub
}
@Override
public void recordMeetingInfo(String meetingId, Map<String, String> info) {
// TODO Auto-generated method stub
}
@Override
public void recordMeetingMetadata(String meetingId,
Map<String, String> metadata) {
// TODO Auto-generated method stub
}
@Override
public void endMeeting(String meetingId) {
// TODO Auto-generated method stub
}
@Override
public void send(String channel, String message) {
// TODO Auto-generated method stub
}
@Override
public void addListener(MessageListener listener) {
// TODO Auto-generated method stub
}
@Override
public void removeListener(MessageListener listener) {
// TODO Auto-generated method stub
}
}

View File

@ -1,7 +1,5 @@
package org.bigbluebutton.api.messaging;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@ -49,9 +47,7 @@ public class RedisMessagingService implements MessagingService {
log.warn("Cannot record the info meeting:"+meetingId,e);
} finally {
redisPool.returnResource(jedis);
}
}
}
public void recordMeetingMetadata(String meetingId, Map<String, String> metadata) {

View File

@ -6,6 +6,8 @@ import org.apache.commons.codec.digest.DigestUtils;
import org.bigbluebutton.api.*;
import org.bigbluebutton.api.domain.Meeting;
import org.bigbluebutton.api.domain.User;
import org.bigbluebutton.api.messaging.NullMessagingService;
import org.bigbluebutton.api.messaging.MessagingService;
class ApiControllerTests extends ControllerUnitTestCase {
String API_VERSION = "0.7"
@ -15,7 +17,7 @@ class ApiControllerTests extends ControllerUnitTestCase {
String MOD_PASS = "modpass"
String VIEW_PASS = "viewpass"
String CLIENT_URL = "http://localhost/client/BigBlueButton.html"
long CREATE_TIME = 1234567890
def dynamicConferenceService
MeetingServiceImp meetingService
@ -25,6 +27,8 @@ class ApiControllerTests extends ControllerUnitTestCase {
mockLogging(DynamicConferenceService)
dynamicConferenceService = new DynamicConferenceService()
meetingService = new MeetingServiceImp()
MessagingService ms = new NullMessagingService();
meetingService.setMessagingService(ms);
dynamicConferenceService.setMeetingService(meetingService)
dynamicConferenceService.apiVersion = API_VERSION
dynamicConferenceService.securitySalt = SALT
@ -68,7 +72,7 @@ class ApiControllerTests extends ControllerUnitTestCase {
joinMeeting(controller2)
controller2.join()
println "controller response = " + controller2.response.contentAsString
/**
* Need to use controller2.redirectArgs['url'] instead of controller2.response.redirectedUrl as
* shown in the grails doc because it is returning null for me.
@ -78,8 +82,7 @@ class ApiControllerTests extends ControllerUnitTestCase {
assertEquals CLIENT_URL, controller2.redirectArgs['url']
}
void testIsMeetingRunningAPI() {
void testIsMeetingRunningAPI() {
ApiController controller3 = new ApiController()
mockLogging(ApiController)
controller3.setDynamicConferenceService(dynamicConferenceService)
@ -93,8 +96,7 @@ class ApiControllerTests extends ControllerUnitTestCase {
}
void testEndAPI() {
void testEndAPI() {
ApiController endCtlr = new ApiController()
mockLogging(ApiController)
endCtlr.setDynamicConferenceService(dynamicConferenceService)
@ -151,7 +153,7 @@ class ApiControllerTests extends ControllerUnitTestCase {
private Meeting createDefaultMeeting() {
String internalMeetingId = dynamicConferenceService.getInternalMeetingId(MEETING_ID)
String internalMeetingId = dynamicConferenceService.convertToInternalMeetingId(MEETING_ID)
String logoutUrl = "http://localhost"
String telVoice = "85115"
String webVoice = "bbb-85115"
@ -165,8 +167,8 @@ class ApiControllerTests extends ControllerUnitTestCase {
mInfo.put('contributor', "Popen3");
mInfo.put('language', "en-US");
mInfo.put('identifier', "ttmg-5001-2");
Meeting defaultMeeting = new Meeting.Builder().withName(MEETING_NAME).withExternalId(MEETING_ID).withInternalId(internalMeetingId)
Meeting defaultMeeting = new Meeting.Builder(MEETING_ID, internalMeetingId, CREATE_TIME).withName(MEETING_NAME)
.withMaxUsers(30).withModeratorPass(MOD_PASS).withViewerPass(VIEW_PASS).withRecording(true)
.withLogoutUrl(logoutUrl).withTelVoice(telVoice).withWebVoice(webVoice).withDialNumber(dialNumber)
.withMetadata(mInfo).withWelcomeMessage(welcomeMessage).build()
@ -176,7 +178,7 @@ class ApiControllerTests extends ControllerUnitTestCase {
private Meeting createAnotherMeeting() {
String externalMeetingId = "cook-with-omar"
String internalMeetingId = dynamicConferenceService.getInternalMeetingId(externalMeetingId)
String internalMeetingId = dynamicConferenceService.convertToInternalMeetingId(externalMeetingId)
String logoutUrl = "http://localhost"
String telVoice = "85116"
String webVoice = "bbb-85116"
@ -190,8 +192,8 @@ class ApiControllerTests extends ControllerUnitTestCase {
mInfo.put('contributor', "Popen3");
mInfo.put('language', "en-US");
mInfo.put('identifier', "olc-101-2");
Meeting defaultMeeting = new Meeting.Builder().withName("Omar's Cooking").withExternalId("cook-with-omar").withInternalId(internalMeetingId)
long createTime = 1234567899
Meeting defaultMeeting = new Meeting.Builder("cook-with-omar", internalMeetingId, createTime).withName("Omar's Cooking")
.withMaxUsers(30).withModeratorPass(MOD_PASS).withViewerPass(VIEW_PASS).withRecording(true)
.withLogoutUrl(logoutUrl).withTelVoice(telVoice).withWebVoice(webVoice).withDialNumber(dialNumber)
.withMetadata(mInfo).withWelcomeMessage(welcomeMessage).build()
@ -245,7 +247,7 @@ class ApiControllerTests extends ControllerUnitTestCase {
String username = "Richard"
String modPass = "testm"
String queryString = "meetingID=${MEETING_ID}&fullName=${username}&password=${MOD_PASS}"
String queryString = "meetingID=${MEETING_ID}&fullName=${username}&password=${MOD_PASS}&createTime=${CREATE_TIME}"
String checksum = DigestUtils.shaHex("join" + queryString + SALT)
queryString += "&checksum=${checksum}"
@ -253,6 +255,7 @@ class ApiControllerTests extends ControllerUnitTestCase {
mockParams.meetingID = MEETING_ID
mockParams.password = MOD_PASS
mockParams.checksum = checksum
mockParams.createTime = CREATE_TIME
mockRequest.queryString = queryString
}
@ -267,5 +270,6 @@ class ApiControllerTests extends ControllerUnitTestCase {
mockParams.moderatorPW = MOD_PASS
mockParams.attendeePW = VIEW_PASS
mockRequest.queryString = queryString
}
}