1 /* 2 * Jicofo, the Jitsi Conference Focus. 3 * 4 * Copyright @ 2015 Atlassian Pty Ltd 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 package org.jitsi.jicofo.recording.jibri; 19 20 import org.jitsi.jicofo.util.*; 21 import org.jitsi.xmpp.extensions.jibri.*; 22 import net.java.sip.communicator.service.protocol.*; 23 import org.jitsi.jicofo.*; 24 import org.jitsi.protocol.xmpp.*; 25 import org.jitsi.utils.logging.*; 26 import org.jivesoftware.smack.packet.*; 27 import org.jxmpp.jid.*; 28 import org.osgi.framework.*; 29 30 import java.util.*; 31 import java.util.concurrent.*; 32 33 import static org.jivesoftware.smack.packet.XMPPError.Condition.*; 34 import static org.jivesoftware.smack.packet.XMPPError.from; 35 import static org.jivesoftware.smack.packet.XMPPError.getBuilder; 36 37 /** 38 * Common stuff shared between {@link JibriRecorder} (which can deal with only 39 * 1 {@link JibriSession}) and {@link JibriSipGateway} (which is capable of 40 * handling multiple, simultaneous {@link JibriSession}s). 41 * 42 * @author Pawel Domas 43 */ 44 public abstract class CommonJibriStuff 45 { 46 /** 47 * OSGI bundle context. 48 */ 49 protected final BundleContext bundleContext; 50 51 /** 52 * The Jitsi Meet conference instance. 53 */ 54 protected final JitsiMeetConferenceImpl conference; 55 56 /** 57 * The {@link XmppConnection} used for communication. 58 */ 59 protected final XmppConnection connection; 60 61 /** 62 * The global config used by this instance to obtain some config options 63 * like {@link JitsiMeetGlobalConfig#getJibriPendingTimeout()}. 64 */ 65 final JitsiMeetGlobalConfig globalConfig; 66 67 /** 68 * The logger instance pass to the constructor that wil be used by this 69 * instance for logging. 70 */ 71 protected final Logger logger; 72 73 /** 74 * Meet tools instance used to inject packet extensions to Jicofo's MUC 75 * presence. 76 */ 77 final OperationSetJitsiMeetTools meetTools; 78 79 /** 80 * Jibri detector which notifies about Jibri availability status changes. 81 */ 82 final JibriDetector jibriDetector; 83 84 /** 85 * Executor service used by {@link JibriSession} to schedule pending timeout 86 * tasks. 87 */ 88 final ScheduledExecutorService scheduledExecutor; 89 90 /** 91 * The length of the session id field we generate to uniquely identify a 92 * Jibri session 93 */ 94 static final int SESSION_ID_LENGTH = 16; 95 96 /** 97 * Creates new instance of <tt>JibriRecorder</tt>. 98 * @param bundleContext OSGi {@link BundleContext}. 99 * @param isSIP indicates whether this stuff is for SIP Jibri or for regular 100 * Jibris. 101 * @param conference <tt>JitsiMeetConference</tt> to be recorded by new 102 * instance. 103 * @param xmppConnection XMPP operation set which wil be used to send XMPP 104 * queries. 105 * @param scheduledExecutor the executor service used by this instance 106 * @param globalConfig the global config that provides some values required 107 * by <tt>JibriRecorder</tt> to work. 108 */ CommonJibriStuff( BundleContext bundleContext, boolean isSIP, JitsiMeetConferenceImpl conference, XmppConnection xmppConnection, ScheduledExecutorService scheduledExecutor, JitsiMeetGlobalConfig globalConfig, Logger logger)109 CommonJibriStuff( BundleContext bundleContext, 110 boolean isSIP, 111 JitsiMeetConferenceImpl conference, 112 XmppConnection xmppConnection, 113 ScheduledExecutorService scheduledExecutor, 114 JitsiMeetGlobalConfig globalConfig, 115 Logger logger) 116 { 117 this.bundleContext = Objects.requireNonNull(bundleContext, "bundleContext"); 118 this.connection 119 = Objects.requireNonNull(xmppConnection, "xmppConnection"); 120 this.conference = Objects.requireNonNull(conference, "conference"); 121 this.scheduledExecutor 122 = Objects.requireNonNull(scheduledExecutor, "scheduledExecutor"); 123 this.globalConfig 124 = Objects.requireNonNull(globalConfig, "globalConfig"); 125 this.jibriDetector 126 = isSIP 127 ? conference.getServices().getSipJibriDetector() 128 : conference.getServices().getJibriDetector(); 129 130 ProtocolProviderService protocolService = conference.getXmppProvider(); 131 132 this.meetTools 133 = protocolService.getOperationSet(OperationSetJitsiMeetTools.class); 134 135 this.logger = logger; 136 } 137 138 /** 139 * Tries to figure out if there is any current {@link JibriSession} for 140 * the given IQ coming from the Jitsi Meet. If extending class can deal with 141 * only 1 {@link JibriSession} at a time it should return it. If it's 142 * capable of handling multiple sessions then it should try to identify the 143 * session based on the information specified in the <tt>iq</tt>. If it's 144 * unable to match any session it should return <tt>null</tt>. 145 * 146 * The purpose of having this method abstract is to share the logic for 147 * handling start and stop requests. For example if there's incoming stop 148 * request it will be handled if this method return a valid 149 * {@link JibriSession} instance. In case of a start request a new session 150 * will be created if this method returns <tt>null</tt>. 151 * 152 * @param iq the IQ originated from the Jitsi Meet participant (start or 153 * stop request) 154 * @return {@link JibriSession} if there is any {@link JibriSession} 155 * currently active for given IQ. 156 */ getJibriSessionForMeetIq(JibriIq iq)157 protected abstract JibriSession getJibriSessionForMeetIq(JibriIq iq); 158 159 /** 160 * @return a list with all {@link JibriSession}s used by this instance. 161 */ getJibriSessions()162 public abstract List<JibriSession> getJibriSessions(); 163 164 /** 165 * This method will be called when start IQ arrives from Jitsi Meet 166 * participant and {@link #getJibriSessionForMeetIq(JibriIq)} returns 167 * <tt>null</tt>. The implementing class should allocate and store new 168 * {@link JibriSession}. Once {@link JibriSession} is created it must be 169 * started by the implementing class. 170 * @param iq the Jibri IQ which is a start request coming from Jitsi Meet 171 * participant 172 * @return the response to the given <tt>iq</tt>. It should be 'result' if 173 * new session has been started or 'error' otherwise. 174 */ handleStartRequest(JibriIq iq)175 protected abstract IQ handleStartRequest(JibriIq iq); 176 177 /** 178 * Method called by {@link JitsiMeetConferenceImpl} when the conference is 179 * being stopped. 180 */ dispose()181 public void dispose() 182 { 183 } 184 185 /** 186 * Checks if the IQ is from a member of this room or from an active Jibri 187 * session. 188 * @param iq a random incoming Jibri IQ. 189 * @return <tt>true</tt>, when the IQ is from a member of this room or from 190 * an active Jibri session. 191 */ accept(JibriIq iq)192 public final boolean accept(JibriIq iq) 193 { 194 // Process if it belongs to an active recording session 195 JibriSession session = getJibriSessionForMeetIq(iq); 196 if (session != null && session.accept(iq)) 197 { 198 return true; 199 } 200 201 // Check if the implementation wants to deal with this IQ sub-type 202 if (!acceptType(iq)) 203 { 204 return false; 205 } 206 207 Jid from = iq.getFrom(); 208 BareJid roomName = from.asBareJid(); 209 if (!conference.getRoomName().equals(roomName)) 210 { 211 return false; 212 } 213 214 XmppChatMember chatMember = conference.findMember(from); 215 if (chatMember == null) 216 { 217 logger.warn("Chat member not found for: " + from); 218 return false; 219 } 220 221 return true; 222 } 223 224 /** 225 * Implementors of this class decided here if they want to deal with 226 * the incoming JibriIQ. 227 * @param packet the Jibri IQ to check. 228 * @return <tt>true</tt> if the implementation should handle it. 229 */ acceptType(JibriIq packet)230 protected abstract boolean acceptType(JibriIq packet); 231 232 /** 233 * <tt>JibriIq</tt> processing. Handles start and stop requests. Will verify 234 * if the user is a moderator. 235 */ handleIQRequest(JibriIq iq)236 final synchronized IQ handleIQRequest(JibriIq iq) 237 { 238 if (logger.isDebugEnabled()) 239 { 240 logger.debug("Jibri request. IQ: " + iq.toXML()); 241 } 242 243 // Process if it belongs to an active recording session 244 JibriSession session = getJibriSessionForMeetIq(iq); 245 if (session != null && session.accept(iq)) 246 { 247 return session.processJibriIqRequestFromJibri(iq); 248 } 249 250 JibriIq.Action action = iq.getAction(); 251 if (JibriIq.Action.UNDEFINED.equals(action)) 252 { 253 return IQ.createErrorResponse(iq, getBuilder(bad_request)); 254 } 255 256 // verifyModeratorRole create 'not_allowed' error on when not moderator 257 XMPPError error = verifyModeratorRole(iq); 258 if (error != null) 259 { 260 logger.warn("Ignored Jibri request from non-moderator."); 261 return IQ.createErrorResponse(iq, error); 262 } 263 264 JibriSession jibriSession = getJibriSessionForMeetIq(iq); 265 266 // start ? 267 if (JibriIq.Action.START.equals(action)) 268 { 269 if (jibriSession == null) 270 { 271 return handleStartRequest(iq); 272 } 273 else 274 { 275 // If there's a session active, we know there are Jibri's connected 276 // (so it isn't XMPPError.Condition.service_unavailable), so it 277 // must be that they're all busy. 278 logger.info("Failed to start a Jibri session, all Jibris were busy"); 279 return ErrorResponse.create( 280 iq, 281 XMPPError.Condition.resource_constraint, 282 "all Jibris are busy"); 283 } 284 } 285 // stop ? 286 else if (JibriIq.Action.STOP.equals(action) && 287 jibriSession != null) 288 { 289 jibriSession.stop(iq.getFrom()); 290 return IQ.createResultIQ(iq); 291 } 292 293 logger.warn("Discarded: " + iq.toXML() + " - nothing to be done, "); 294 295 // Bad request 296 return IQ.createErrorResponse( 297 iq, from(bad_request, "Unable to handle: " + action)); 298 } 299 verifyModeratorRole(JibriIq iq)300 private XMPPError verifyModeratorRole(JibriIq iq) 301 { 302 Jid from = iq.getFrom(); 303 ChatRoomMemberRole role = conference.getRoleForMucJid(from); 304 305 if (role == null) 306 { 307 // Only room members are allowed to send requests 308 return getBuilder(forbidden).build(); 309 } 310 311 if (ChatRoomMemberRole.MODERATOR.compareTo(role) < 0) 312 { 313 // Moderator permission is required 314 return getBuilder(not_allowed).build(); 315 } 316 317 return null; 318 } 319 generateSessionId()320 protected String generateSessionId() 321 { 322 return Utils.generateSessionId(SESSION_ID_LENGTH); 323 } 324 } 325