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