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