From 2298ec2cba9941709a76dd772d4edf4897c25302 Mon Sep 17 00:00:00 2001 From: Andrew Bonney Date: Wed, 27 Nov 2019 11:15:39 +0000 Subject: [PATCH 1/3] Refactor source-filter checks into a separate RFC named file --- checkRFC4570.js | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ checkST2110.js | 19 +----------------- index.js | 2 ++ sdpoker.js | 7 ++++--- 4 files changed, 58 insertions(+), 21 deletions(-) create mode 100644 checkRFC4570.js diff --git a/checkRFC4570.js b/checkRFC4570.js new file mode 100644 index 0000000..d542802 --- /dev/null +++ b/checkRFC4570.js @@ -0,0 +1,51 @@ +/* Copyright 2018 Streampunk Media Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +const splitLines = sdp => sdp.match(/[^\r\n]+/g); +const concat = arrays => Array.prototype.concat.apply([], arrays); + +const sourceFilterPattern = /a=source-filter:\s(incl|excl)/; + +// Section 3 Test 1 - Source-filter correctly formatted if present +const test_30_1 = sdp => { + let lines = splitLines(sdp); + let errors = []; + for ( let x = 0 ; x < lines.length ; x++ ) { + if (lines[x].startsWith('a=source-filter:')) { + let sourceFilterMatch = lines[x].match(sourceFilterPattern); + if (!sourceFilterMatch) { + errors.push(new Error(`Line ${x + 1}: Source-filters must follow the pattern 'a=source-filter: ' as per RFC 4570 Section 3.`)); + continue; + } + } + } + return errors; +}; + +const section_30 = (sdp, params) => { + let tests = [ test_30_1 ]; + return concat(tests.map(t => t(sdp, params))); +}; + +const allSections = (sdp, params) => { + let sections = [ + section_30 ]; + return concat(sections.map(s => s(sdp, params))); +}; + +module.exports = { + allSections, + section_30, +}; diff --git a/checkST2110.js b/checkST2110.js index cda578c..2b2eac8 100644 --- a/checkST2110.js +++ b/checkST2110.js @@ -19,7 +19,6 @@ const concat = arrays => Array.prototype.concat.apply([], arrays); const mediaclkPattern = /[\r\n]a=mediaclk/; const mediaclkTypePattern = /[\r\n]a=mediaclk[^\s=]+/g; const mediaclkDirectPattern = /[\r\n]a=mediaclk:direct=\d+\s+/g; -const sourceFilterPattern = /a=source-filter:\s(incl|excl)/; const tsrefclkPattern = /[\r\n]a=ts-refclk/; const ptpPattern = /traceable|((([0-9a-fA-F]{2}-){7}[0-9a-fA-F]{2})(:(\d+|domain-name=\S+))?)/; const macPattern = /(([0-9a-fA-F]{2}-){5}[0-9a-fA-F]{2})/; @@ -110,22 +109,6 @@ const test_10_81_2 = (sdp, params) => { } }; -// Test ST 2110-10 Section 8.1 Test 3 - Source-filter correctly formatted if present -const test_10_81_3 = sdp => { - let lines = splitLines(sdp); - let errors = []; - for ( let x = 0 ; x < lines.length ; x++ ) { - if (lines[x].startsWith('a=source-filter:')) { - let sourceFilterMatch = lines[x].match(sourceFilterPattern); - if (!sourceFilterMatch) { - errors.push(new Error(`Line ${x + 1}: Source-filters must follow the pattern 'a=source-filter: ' as defined in RFC 4570.`)); - continue; - } - } - } - return errors; -}; - // Test ST2110-10 Section 8.1 Test 1 - Shall have a media-level ts-refclk const test_10_82_1 = sdp => { let errors = []; @@ -1097,7 +1080,7 @@ const section_10_74 = (sdp, params) => { }; const section_10_81 = (sdp, params) => { - let tests = [ test_10_81_1, test_10_81_2, test_10_81_3 ]; + let tests = [ test_10_81_1, test_10_81_2 ]; return concat(tests.map(t => t(sdp, params))); }; diff --git a/index.js b/index.js index 74e6e2f..763d83b 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,7 @@ const fs = require('fs'); const util = require('util'); const readFile = util.promisify(fs.readFile); const { allSections : checkRFC4566 } = require('./checkRFC4566.js'); +const { allSections : checkRFC4570 } = require('./checkRFC4570.js'); const { allSections : checkST2110 } = require('./checkST2110.js'); const getSDP = (path, nmos = true) => { @@ -42,5 +43,6 @@ const getSDP = (path, nmos = true) => { module.exports = { getSDP, checkRFC4566, + checkRFC4570, checkST2110 }; diff --git a/sdpoker.js b/sdpoker.js index 1731cfe..350d05e 100755 --- a/sdpoker.js +++ b/sdpoker.js @@ -14,7 +14,7 @@ limitations under the License. */ -const { getSDP, checkRFC4566, checkST2110 } = require('./index.js'); +const { getSDP, checkRFC4566, checkRFC4570, checkST2110 } = require('./index.js'); const yargs = require('yargs'); const { accessSync, R_OK } = require('fs'); @@ -78,9 +78,10 @@ const args = yargs async function test (args) { try { let sdp = await getSDP(args._[0], args.nmos); - let rfcErrors = checkRFC4566(sdp, args); + let rfc4566Errors = checkRFC4566(sdp, args); + let rfc4570Errors = checkRFC4570(sdp, args); let st2110Errors = checkST2110(sdp, args); - let errors = rfcErrors.concat(st2110Errors); + let errors = rfc4566Errors.concat(rfc4570Errors, st2110Errors); if (errors.length !== 0) { console.error(`Found ${errors.length} error(s) in SDP file:`); for ( let c in errors ) { From 244f7ae552069d1cf50ed4330ebfa23d13b23bc7 Mon Sep 17 00:00:00 2001 From: Andrew Bonney Date: Wed, 27 Nov 2019 11:29:31 +0000 Subject: [PATCH 2/3] rfc4570: add checks for source-filter contents --- checkRFC4570.js | 74 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/checkRFC4570.js b/checkRFC4570.js index d542802..05f383c 100644 --- a/checkRFC4570.js +++ b/checkRFC4570.js @@ -16,7 +16,9 @@ const splitLines = sdp => sdp.match(/[^\r\n]+/g); const concat = arrays => Array.prototype.concat.apply([], arrays); -const sourceFilterPattern = /a=source-filter:\s(incl|excl)/; +const sourceFilterPattern = /a=source-filter:\s(incl|excl)\sIN\s(IP4|IP6|\*)\s(\S+)(?:\s(\S+))+/; +const cPattern = /^c=IN\s+(IP[46])\s+([^\s/]+)(\/\d+)?(\/[1-9]\d*)?$/; +const multiPattern = /^((22[4-9]|23[0-9])(\.(\d\d?\d?)){3})|(ff[0-7][123458e]::[^\s]+)$/; // Section 3 Test 1 - Source-filter correctly formatted if present const test_30_1 = sdp => { @@ -34,18 +36,80 @@ const test_30_1 = sdp => { return errors; }; +// Section 3 Test 2 - Source-filters are present when multicast addresses are used +const test_30_2 = sdp => { + let lines = splitLines(sdp); + let errors = []; + let isMulticast = false; + let seenSourceFilter = false; + for ( let x = 0 ; x < lines.length ; x++ ) { + if (lines[x].startsWith('c=')) { + let addrMatch = lines[x].match(cPattern); + if (!addrMatch) { + continue; + } + if (multiPattern.test(addrMatch[2])) { + isMulticast = true; + } + } + if (lines[x].startsWith('a=source-filter:')) { + seenSourceFilter = true; + } + } + if (isMulticast && !seenSourceFilter) { + // A basic check that at least one source-filter is included when using a multicast destination + errors.push(new Error('SDP file includes one or more multicast destinations but does not include any a=source-filter lines as per RFC 4570 Section 3.')); + } + return errors; +}; + +// Section 3.0 Test 3 - Source-filters match addresses used in connection attributes +const test_30_3 = sdp => { + let lines = splitLines(sdp); + let errors = []; + let globalAddr = null; + let mediaAddr = null; + let inMedia = false; + for ( let x = 0 ; x < lines.length ; x++ ) { + if (lines[x].startsWith('c=')) { + let addrMatch = lines[x].match(cPattern); + if (!addrMatch) { + continue; + } + if (!inMedia) { + globalAddr = addrMatch[2]; + } else { + mediaAddr = addrMatch[2]; + } + } + if (lines[x].startsWith('m=')) { + inMedia = true; + mediaAddr = null; + } + if (lines[x].startsWith('a=source-filter:')) { + let sourceFilterMatch = lines[x].match(sourceFilterPattern); + if (!sourceFilterMatch) { + continue; + } + if (sourceFilterMatch[3] != globalAddr && sourceFilterMatch[3] != mediaAddr && sourceFilterMatch[3] != '*') { + errors.push(new Error(`Line ${x + 1}: Source-filter destination addresses must match one or more connection address as per RFC 4570 Section 3.`)); + } + } + } + return errors; +}; + const section_30 = (sdp, params) => { - let tests = [ test_30_1 ]; + let tests = [ test_30_1, test_30_2, test_30_3 ]; return concat(tests.map(t => t(sdp, params))); }; const allSections = (sdp, params) => { - let sections = [ - section_30 ]; + let sections = [ section_30 ]; return concat(sections.map(s => s(sdp, params))); }; module.exports = { allSections, - section_30, + section_30 }; From c005068c3d9078de1ad6e1fcf9991c3274b05b66 Mon Sep 17 00:00:00 2001 From: Andrew Bonney Date: Wed, 27 Nov 2019 14:10:28 +0000 Subject: [PATCH 3/3] readme: update to match addition of rfc4570 tests --- README.md | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index eea1017..25f7423 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ Install SDPoker as a dependency for the project you are working on: Use the module in your project with the following line: ```javascript -const { getSDP, checkRFC4566, checkST2110 } = require('sdpoker'); +const { getSDP, checkRFC4566, checkRFC4570, checkST2110 } = require('sdpoker'); ``` ### Get SDP @@ -56,7 +56,7 @@ getSDP('http://localhost:3123/sdps/video_stream_1.sdp') If the `nmos` flag is set to `true`, the SDP file is required to be retrieved over HTTP and must have filename extension `.sdp`. -The value of a fulfilled promise is the contents of an SDP file as a string. SDP files are assumed to be UTF8 character sets. Pass the result into the `checkRFC4566` and `checkST2110` methods. +The value of a fulfilled promise is the contents of an SDP file as a string. SDP files are assumed to be UTF8 character sets. Pass the result into the `checkRFC4566`, `checkRFC4570` and `checkST2110` methods. ### Check RFC4566 @@ -75,6 +75,23 @@ The `params` parameter is an object that, when present, can be used to configure The return value of the method is an array of [Javascript Errors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error). The array is empty if no errors occurred. +### Check RFC4570 + +The `checkRFC4570(sdp, params)` takes a string representation of the contents of an SDP file (`sdp`) and runs source-filter tests relevant to SMPTE ST 2110. + +For example: + +```javascript +getSDP('examples/st2110-10.sdp') + .then(sdp => checkRFC4570(sdp, {})) + .then(errs => { if (errs.length > 0) console.log(errs); }) + .catch(console.error); +``` + +The `params` parameter is an object that, when present, can be used to configure the tests. See the [parameters](#parameters) section below for more information. + +The return value of the method is an array of [Javascript Errors](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error). The array is empty if no errors occurred. + ### Check ST2110 The `checkST2110(sdp, params)` takes a string representation of the contents of an SDP file (`sdp`) and runs through the relevant clauses of the SMPTE 2110-10/20/30 documents, and referenced standards such as AES-67 and SMPTE ST 2022-7, applying appropriate tests. @@ -123,18 +140,17 @@ let params = { }; ``` -Currently, the `whitespace` flag forces a check as to whether the format parameter field (`a=fmtp`) has a whitespace character after the final semicolon on the line. Strict reading of the standard suggests that it should, although the this could also be viewed as ambiguous as the term _carriage return_ can also be interpreted as whitespace. Further white space checks may be added, such as should a space be included between `a=source-filter:` and `incl`. +Currently, the `whitespace` flag forces a check as to whether the format parameter field (`a=fmtp`) has a whitespace character after the final semicolon on the line. Strict reading of the standard suggests that it should, although the this could also be viewed as ambiguous as the term _carriage return_ can also be interpreted as whitespace. # Tests -For now, please see the comments in files `checkRFC4566.js` and `checkST2110.js` for a description of the tests. A more formal and separate list may be provided in the future. +For now, please see the comments in files `checkRFC4566.js`, `checkRFC4570.js` and `checkST2110.js` for a description of the tests. A more formal and separate list may be provided in the future. # Enhancements The following items are known deficiencies of SDPoker and may be added in the future: * Tests for attribute `a=recvonly` -* Tests for attribute `a=sourcefilter` * Testing whether an advertised connection address can be resolved, joined or pinged. * Testing whether advertised clocks are available. * Testing that, for AES-67 audio streams, the `ptime` attribute matches the sample rate and number of channels.