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;
19 
20 import org.jitsi.xmpp.extensions.colibri.*;
21 import org.jitsi.xmpp.extensions.jingle.*;
22 
23 import org.jitsi.protocol.xmpp.util.*;
24 import org.jitsi.utils.*;
25 import org.jitsi.utils.logging.*;
26 
27 import org.jivesoftware.smack.packet.*;
28 import org.jxmpp.jid.*;
29 
30 import java.util.*;
31 import java.util.stream.*;
32 
33 /**
34  * Utility class that wraps the process of validating new sources and source
35  * groups that are to be added to the conference.
36  *
37  * @author Pawel Domas
38  */
39 public class SSRCValidator
40 {
41     /**
42      * The logger used by this class
43      */
44     private final static Logger classLogger
45         = Logger.getLogger(SSRCValidator.class);
46 
47     /**
48      * The logger used by this instance. It uses the log level delegate from
49      * the logger passed to the constructor.
50      */
51     private final Logger logger;
52 
53     /**
54      * Each validation happens when a participant sends new sources and one
55      * instance of {@link SSRCValidator} is only good for one such validation.
56      * This field stores participant's endpoint ID used for printing log
57      * messages.
58      */
59     private final String endpointId;
60 
61     /**
62      * The source map obtained from the participant which reflects current
63      * source status. It's a clone and modifications done here do not affect
64      * the version held by {@link Participant}.
65      */
66     private final MediaSourceMap sources;
67 
68     /**
69      * Same as {@link #sources}, but for source groups.
70      */
71     private final MediaSourceGroupMap sourceGroups;
72 
73     /**
74      * The limit sources count per media type allowed to be stored by
75      * each {@link Participant} at a time.
76      */
77     private final int maxSourceCount;
78 
79     /**
80      * Filters out FID groups that do belong to any simulcast grouping.
81      *
82      * @param simGroupings the list of all {@link SimulcastGrouping}s.
83      * @param fidGroups the list of all FID groups even these which are part of
84      *        the <tt>simGroupings</tt>.
85      *
86      * @return a list of FID groups that are not part of any SIM grouping.
87      */
getIndependentFidGroups( List<SimulcastGrouping> simGroupings, List<SourceGroup> fidGroups)88     static private List<SourceGroup> getIndependentFidGroups(
89             List<SimulcastGrouping>    simGroupings,
90             List<SourceGroup>          fidGroups)
91     {
92         if (simGroupings.isEmpty())
93         {
94             // Nothing to be done here...
95             return new ArrayList<>(fidGroups);
96         }
97 
98         return fidGroups.stream()
99             .filter(
100                 fidGroup -> simGroupings.stream()
101                     .noneMatch(
102                         simGroup ->
103                             simGroup.belongsToSimulcastGrouping(fidGroup))
104                 ).collect(Collectors.toList());
105     }
106 
107     /**
108      * Checks if there are no MSID conflicts across all independent FID groups.
109      *
110      * @param independentFidGroups the list of all independent FID groups (that
111      *        do not belong to any other higher level grouping).
112      *
113      * @throws InvalidSSRCsException in case of MSID conflict
114      */
verifyNoMsidConflictsAcrossFidGroups( List<SourceGroup> independentFidGroups)115     static private void verifyNoMsidConflictsAcrossFidGroups(
116             List<SourceGroup> independentFidGroups)
117         throws InvalidSSRCsException
118     {
119         for (SourceGroup fidGroup : independentFidGroups)
120         {
121             // NOTE at this point we're sure that every source has MSID
122             String fidGroupMsid = fidGroup.getGroupMsid();
123             List<SourceGroup> withTheMsid
124                 = SSRCSignaling.selectWithMsid(
125                         independentFidGroups, fidGroupMsid);
126 
127             for (SourceGroup conflictingGroup : withTheMsid)
128             {
129                 if (conflictingGroup != fidGroup)
130                 {
131                     throw new InvalidSSRCsException(
132                             "MSID conflict across FID groups: "
133                                 + fidGroupMsid + ", " + conflictingGroup
134                                 + " conflicts with group " + fidGroup);
135                 }
136             }
137         }
138     }
139 
140     /**
141      * Checks if there are no MSID conflicts across simulcast groups.
142      *
143      * @param mediaType the media type to be checked in this call
144      * @param groupedSources the map holding all sources which belong to any
145      *        group for all media types.
146      * @param simGroupings the list of all {@link SimulcastGrouping}s for
147      *        the <tt>mediaType</tt>.
148      *
149      * @throws InvalidSSRCsException in case of MSID conflict
150      */
verifyNoMsidConflictsAcrossSimGroupings( String mediaType, MediaSourceMap groupedSources, List<SimulcastGrouping> simGroupings)151     static private void verifyNoMsidConflictsAcrossSimGroupings(
152             String                     mediaType,
153             MediaSourceMap             groupedSources,
154             List<SimulcastGrouping>    simGroupings)
155         throws InvalidSSRCsException
156     {
157         for (SimulcastGrouping simGrouping : simGroupings)
158         {
159             String simulcastMsid = simGrouping.getSimulcastMsid();
160 
161             if (simGrouping.isUsingRidSignaling())
162             {
163                 // Skip RID simulcast group
164                 continue;
165             }
166             else if (StringUtils.isNullOrEmpty(simulcastMsid))
167             {
168                 throw new InvalidSSRCsException(
169                         "No MSID in simulcast group: " + simGrouping);
170             }
171 
172             List<SourcePacketExtension> sourcesWithTheMsid
173                 = groupedSources.findSourcesWithMsid(
174                         mediaType, simulcastMsid);
175 
176             for (SourcePacketExtension src : sourcesWithTheMsid)
177             {
178                 if (!simGrouping.belongsToSimulcastGrouping(src))
179                 {
180                     throw new InvalidSSRCsException(
181                         "MSID conflict across SIM groups: "
182                             + simulcastMsid + ", " + src
183                             + " conflicts with group " + simGrouping);
184                 }
185             }
186         }
187     }
188 
189     /**
190      * Creates new <tt>SSRCValidator</tt>
191      * @param endpointId participant's endpoint ID for whom the new
192      * sources/groups will be validated.
193      * @param sources the map which holds sources of the whole conference.
194      * @param sourceGroups the map which holds source groups currently present
195      * in the conference.
196      * @param maxSourceCount the source limit, tells how many sources per media
197      * type can be stored at a time by each conference participant.
198      * @param logLevelDelegate a <tt>Logger</tt> which will be used as
199      * the logging level delegate.
200      */
SSRCValidator(String endpointId, MediaSourceMap sources, MediaSourceGroupMap sourceGroups, int maxSourceCount, Logger logLevelDelegate)201     public SSRCValidator(String              endpointId,
202                          MediaSourceMap      sources,
203                          MediaSourceGroupMap sourceGroups,
204                          int                 maxSourceCount,
205                          Logger              logLevelDelegate)
206     {
207         this.endpointId = endpointId;
208         this.sources = sources.copyDeep();
209         this.sourceGroups = sourceGroups.copy();
210         this.maxSourceCount = maxSourceCount;
211         this.logger = Logger.getLogger(classLogger, logLevelDelegate);
212     }
213 
214     /**
215      * Checks how many sources are current in the conference for given
216      * participant.
217      *
218      * @param owner An owner's JID (can be <tt>null</tt> to check for not owned
219      * sources)
220      * @param mediaType The type of the media for which sources will be counted.
221      * @return how many sources are currently in the conference source map for
222      * given media type and owner's JID.
223      */
getSourceCountForOwner(Jid owner, String mediaType)224     private long getSourceCountForOwner(Jid owner, String mediaType)
225     {
226         return this.sources.getSourcesForMedia(mediaType)
227             .stream()
228             .filter(
229                 source -> Objects.equals(
230                     SSRCSignaling.getSSRCOwner(source), owner))
231             .count();
232 
233     }
234 
235     /**
236      * Makes an attempt to add given sources and source groups to the current state.
237      * It checks some constraints that prevent from injecting invalid
238      * description into the conference:
239      * 1. Allow SSRC value between 1 and 0xFFFFFFFF (note that 0 is a valid
240      *    value, but it breaks WebRTC stack in Chrome, so not allowed here)
241      * 2. Does not allow the same source to appear more than once per media type
242      * 3. Truncates sources above the limit (configured in the constructor)
243      * 4. Filters out SSRC parameters other than 'cname' and 'msid'
244      * 5. Drop empty source groups
245      * 6. Skips duplicated groups (the same semantics and contained sources)
246      * 7. Looks for MSID conflicts between SSRCs which do not belong to the same
247      *    group
248      * 8. Makes sure that sources described by groups exist in media description
249      *
250      * @param newSources the sources to add
251      * @param newGroups the groups to add
252      *
253      * @return an array of two objects where first one is <tt>MediaSourceMap</tt>
254      * contains the sources that have been accepted and the second one is
255      * <tt>MediaSourceGroupMap</tt> with <tt>SourceGroup</tt>s accepted by this
256      * validator instance.
257      *
258      * @throws InvalidSSRCsException if a critical problem has been found
259      * with the new sources/groups which would probably result in
260      * "setRemoteDescription" error on the client.
261      */
tryAddSourcesAndGroups( MediaSourceMap newSources, MediaSourceGroupMap newGroups)262     public Object[] tryAddSourcesAndGroups(
263             MediaSourceMap newSources, MediaSourceGroupMap newGroups)
264         throws InvalidSSRCsException
265     {
266         MediaSourceMap acceptedSources = new MediaSourceMap();
267         for (String mediaType : newSources.getMediaTypes())
268         {
269             List<SourcePacketExtension> mediaSources
270                 = newSources.getSourcesForMedia(mediaType);
271 
272             for (SourcePacketExtension source : mediaSources)
273             {
274                 if (!source.hasSSRC() && !source.hasRid())
275                 {
276                     // SourcePacketExtension treats -1 as lack of SSRC
277                     throw new InvalidSSRCsException(
278                             "Source with no value was passed"
279                                 + " (parsed from negative ?)");
280                 }
281                 else if (source.hasSSRC())
282                 {
283                     long ssrcValue = source.getSSRC();
284 
285                     // NOTE Technically SSRC == 0 is allowed, but it breaks Chrome
286                     if (ssrcValue <= 0L || ssrcValue > 0xFFFFFFFFL)
287                     {
288                         throw new InvalidSSRCsException(
289                                 "Illegal SSRC value: " + ssrcValue);
290                     }
291                 }
292 
293                 // Check for duplicates
294                 String conflictingMediaType
295                     = sources.getMediaTypeForSource(source);
296                 if (conflictingMediaType != null)
297                 {
298                     throw new InvalidSSRCsException(
299                         "Source "  + source.toString() + " is in "
300                             + conflictingMediaType + " already");
301                 }
302 
303                 // Check for Source limit exceeded
304                 Jid owner = SSRCSignaling.getSSRCOwner(source);
305                 long sourceCount = getSourceCountForOwner(owner, mediaType);
306                 if (sourceCount >= maxSourceCount)
307                 {
308                     logger.error(
309                         "Too many sources signalled by "
310                             + endpointId + " - dropping: " + source.toString());
311                     // Abort - can't add any more SSRCs.
312                     break;
313                 }
314 
315                 SourcePacketExtension copy = source.copy();
316 
317                 filterOutParams(copy);
318 
319                 acceptedSources.addSource(mediaType, copy);
320                 this.sources.addSource(mediaType, copy);
321             }
322         }
323         // Go over groups
324         MediaSourceGroupMap acceptedGroups = new MediaSourceGroupMap();
325 
326         // Cross check if any source belongs to any existing group already
327         for (String mediaType : newGroups.getMediaTypes())
328         {
329             for (SourceGroup groupToAdd
330                 : newGroups.getSourceGroupsForMedia(mediaType))
331             {
332                 if (groupToAdd.isEmpty())
333                 {
334                     logger.warn("Empty group signalled by: " + endpointId);
335                     continue;
336                 }
337 
338                 if (sourceGroups.containsGroup(mediaType, groupToAdd))
339                 {
340                     logger.warn(
341                         endpointId
342                             + " is trying to add an existing group :"
343                             + groupToAdd);
344                 }
345                 else
346                 {
347                     acceptedGroups.addSourceGroup(mediaType, groupToAdd);
348                     sourceGroups.addSourceGroup(mediaType, groupToAdd);
349                 }
350             }
351         }
352 
353         this.validateStreams();
354 
355         return new Object[] { acceptedSources, acceptedGroups };
356     }
357 
358     /**
359      * Makes an attempt to remove given sources and source groups from
360      * the current state.
361      *
362      * @param sourcesToRemove the sources to be removed
363      * @param groupsToRemove the groups to be removed
364      *
365      * @return an array of two objects where first one is <tt>MediaSourceMap</tt>
366      * contains the sources that have been removed and the second one is
367      * <tt>MediaSourceGroupMap</tt> with <tt>SourceGroup</tt>s removed by this
368      * validator instance.
369      *
370      * @throws InvalidSSRCsException if a critical problem has been found
371      * after sources/groups removal which would probably would result in
372      * "setRemoteDescription" error on the client.
373      */
tryRemoveSourcesAndGroups( MediaSourceMap sourcesToRemove, MediaSourceGroupMap groupsToRemove)374     public Object[] tryRemoveSourcesAndGroups(
375             MediaSourceMap sourcesToRemove,
376             MediaSourceGroupMap groupsToRemove)
377         throws InvalidSSRCsException
378     {
379         MediaSourceMap removedSources = sources.remove(sourcesToRemove);
380         MediaSourceGroupMap removedGroups = sourceGroups.remove(groupsToRemove);
381 
382         this.validateStreams();
383 
384         return new Object[] { removedSources, removedGroups };
385     }
386 
filterOutParams(SourcePacketExtension copy)387     private void filterOutParams(SourcePacketExtension copy)
388     {
389         Iterator<? extends ExtensionElement> params
390             = copy.getChildExtensions().iterator();
391         while (params.hasNext())
392         {
393             ExtensionElement ext = params.next();
394             if (ext instanceof ParameterPacketExtension)
395             {
396                 ParameterPacketExtension ppe = (ParameterPacketExtension) ext;
397                 if (!"cname".equalsIgnoreCase(ppe.getName()) &&
398                     !"msid".equalsIgnoreCase(ppe.getName()))
399                 {
400                     params.remove();
401                 }
402             }
403         }
404     }
405 
validateStreams()406     private void validateStreams()
407         throws InvalidSSRCsException
408     {
409         // Migrate source attributes from SourcePacketExtensions stored in
410         // the media section to SourcePacketExtensions stored by the groups
411         // directly in order to simplify the stream validation process.
412         // The reason for that is that <tt>SourcePacketExtension</tt>s stored
413         // in groups are empty and contain only SSRC number without any
414         // parameters. Without this step we'd have to access separate collection
415         // to check source's parameters like MSID.
416         SSRCSignaling.copySourceParamsToGroups(sourceGroups, sources);
417 
418         // Holds sources that belongs to any group
419         MediaSourceMap groupedSources = new MediaSourceMap();
420 
421         // Go over every group and check if they have corresponding SSRCs
422         for (String mediaType : sourceGroups.getMediaTypes())
423         {
424             List<SourceGroup> mediaGroups
425                 = sourceGroups.getSourceGroupsForMedia(mediaType);
426             for (SourceGroup group : mediaGroups)
427             {
428                 List<SourcePacketExtension> groupSources = group.getSources();
429                 // NOTE that empty groups are not allowed at this point and
430                 // should have been filtered out earlier
431                 String groupMSID = group.getGroupMsid();
432 
433                 for (SourcePacketExtension source : groupSources)
434                 {
435                     if (source.hasSSRC())
436                     {
437                         String msid = SSRCSignaling.getMsid(source);
438                         // Grouped SSRC needs to have a valid MSID
439                         if (StringUtils.isNullOrEmpty(groupMSID))
440                         {
441                             throw new InvalidSSRCsException(
442                                     "Grouped " + source + " has no 'msid'");
443                         }
444                         // Verify if MSID is the same across all SSRCs which
445                         // belong to the same group
446                         else if (!groupMSID.equals(msid))
447                         {
448                             throw new InvalidSSRCsException(
449                                     "MSID mismatch detected in group " + group);
450                         }
451                     }
452 
453                     groupedSources.addSource(mediaType, source);
454                 }
455             }
456         }
457 
458         // Verify SIM/FID grouping
459         for (String mediaType : sourceGroups.getMediaTypes())
460         {
461             // FIXME migrate logic to use MediaType instead of String
462             if (!MediaType.VIDEO.toString().equalsIgnoreCase(mediaType))
463             {
464                 // Verify Simulcast only for the video media type
465                 continue;
466             }
467 
468             List<SimulcastGrouping> simGroupings;
469 
470             try
471             {
472                 simGroupings = sourceGroups.findSimulcastGroupings();
473             }
474             // If groups are in invalid state a SIM grouping may fail to
475             // initialize with IllegalArgumentException
476             catch (IllegalArgumentException exc)
477             {
478                 throw new InvalidSSRCsException(exc.getMessage());
479             }
480 
481             // Check if this SIM group's MSID does not appear in any other
482             // simulcast grouping
483             verifyNoMsidConflictsAcrossSimGroupings(
484                     mediaType, groupedSources, simGroupings);
485 
486             // Check for MSID conflicts across FID groups that do not belong to
487             // any Simulcast grouping.
488             List<SourceGroup> fidGroups = sourceGroups.getRtxGroups();
489             List<SourceGroup> independentFidGroups
490                 = getIndependentFidGroups(simGroupings, fidGroups);
491 
492             verifyNoMsidConflictsAcrossFidGroups(independentFidGroups);
493         }
494 
495         MediaSourceMap notGroupedSSRCs = this.sources.copyDeep();
496         notGroupedSSRCs.remove(groupedSources);
497 
498         // Check for duplicated 'MSID's across each media type in
499         // non grouped-sources
500         for (String mediaType : notGroupedSSRCs.getMediaTypes())
501         {
502             Map<String, SourcePacketExtension> streamMap
503                 = new HashMap<>();
504 
505             List<SourcePacketExtension> mediaSSRCs
506                 = notGroupedSSRCs.getSourcesForMedia(mediaType);
507             for (SourcePacketExtension ssrc : mediaSSRCs)
508             {
509                 String msid = SSRCSignaling.getMsid(ssrc);
510                 if (msid != null)
511                 {
512                     SourcePacketExtension conflictingSSRC
513                         = streamMap.get(msid);
514                     if (conflictingSSRC != null)
515                     {
516                         throw new InvalidSSRCsException(
517                             "Not grouped SSRC " + ssrc.getSSRC()
518                                 + " has conflicting MSID '" + msid
519                                 + "' with " + conflictingSSRC.getSSRC());
520                     }
521                     else
522                     {
523                         streamMap.put(msid, ssrc);
524                     }
525                 }
526                 // else
527                 // That could be recv-only SSRC (no MSID)
528             }
529         }
530     }
531 }
532