├── .circleci └── config.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── file_parser.rs └── sdps │ ├── 02.sdp │ ├── 03.sdp │ ├── 04.sdp │ ├── 05.sdp │ ├── 06.sdp │ ├── 07.sdp │ ├── 08.sdp │ ├── 09.sdp │ ├── 10.sdp │ ├── 11.sdp │ ├── 12.sdp │ ├── 13.sdp │ ├── 14.sdp │ ├── 15.sdp │ ├── 16.sdp │ ├── 17.sdp │ ├── 18.sdp │ ├── 19.sdp │ ├── 20.sdp │ ├── 21.sdp │ ├── 22.sdp │ ├── 23.sdp │ ├── 24.sdp │ ├── 25.sdp │ ├── 26.sdp │ ├── 27.sdp │ ├── 28.sdp │ ├── 29.sdp │ ├── 30.sdp │ ├── 31.sdp │ ├── 32.sdp │ ├── 33.sdp │ ├── 34.sdp │ ├── 35.sdp │ ├── 36.sdp │ ├── 37.sdp │ ├── 38.sdp │ ├── 39.sdp │ ├── 40.sdp │ ├── 41.sdp │ └── extract.sh ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ └── fuzz_target_parse_sdp.rs ├── src ├── address.rs ├── address_tests.rs ├── anonymizer.rs ├── anonymizer_tests.rs ├── attribute_type.rs ├── attribute_type_tests.rs ├── error.rs ├── error_tests.rs ├── lib.rs ├── lib_tests.rs ├── media_type.rs ├── media_type_tests.rs ├── network.rs └── network_tests.rs └── tests └── parse_sdp_tests.rs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | jobs: 3 | build: 4 | parameters: 5 | features: 6 | type: string 7 | rustversion: 8 | type: string 9 | latestrustversion: 10 | type: string 11 | docker: 12 | - image: cimg/rust:<< parameters.rustversion >> 13 | environment: 14 | FEATURES: << parameters.features >> 15 | RUST_VERSION: << parameters.rustversion >> 16 | LATEST_RUST: << parameters.latestrustversion >> 17 | steps: 18 | - checkout 19 | # rustfmt - https://github.com/rust-lang/rustfmt#checking-style-on-a-ci-server 20 | - run: "[ $LATEST_RUST != $RUST_VERSION ] && echo Skipping installing rustfmt || rustup component add rustfmt" 21 | - run: "[ $LATEST_RUST != $RUST_VERSION ] && echo Skipping installing clippy || rustup component add clippy" 22 | - run: cargo --version 23 | # cargo-tarpaulin for code coverage 24 | # This will be landed in a future update 25 | # - run: "[ $LATEST_RUST != $RUST_VERSION ] && echo Skipping cargo tarpaulin || RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin -f" 26 | # cargo-audit 27 | - run: sudo apt-get update 28 | - run: sudo apt-get install pkg-config libssl-dev 29 | - run: cargo generate-lockfile 30 | # NOTE: not to be done except with the latest version 31 | - run: "[ $LATEST_RUST != $RUST_VERSION ] && echo Skipping cargo audit || (cargo install --force cargo-audit && cargo audit)" 32 | # NOTE: not to be done in nightly 33 | - run: "[ $LATEST_RUST != $RUST_VERSION ] && echo Skipping cargo fmt || cargo fmt --all -- --check" 34 | - run: cargo clean 35 | - run: cargo build --verbose --all --features=$FEATURES 36 | # NOTE: not to be done in nightly 37 | - run: "[ $LATEST_RUST != $RUST_VERSION ] && echo Skipping cargo clippy || cargo clippy --all-targets --all-features -- -D warnings" 38 | - run: cargo test --all-targets --all-features --verbose --all 39 | # Parser tests 40 | - run: cargo run --example file_parser examples/sdps/02.sdp 41 | - run: cargo run --example file_parser examples/sdps/03.sdp --expect-failure 42 | - run: cargo run --example file_parser examples/sdps/04.sdp 43 | - run: cargo run --example file_parser examples/sdps/05.sdp 44 | - run: cargo run --example file_parser examples/sdps/06.sdp 45 | - run: cargo run --example file_parser examples/sdps/07.sdp 46 | - run: cargo run --example file_parser examples/sdps/08.sdp --expect-failure 47 | - run: cargo run --example file_parser examples/sdps/09.sdp 48 | - run: cargo run --example file_parser examples/sdps/10.sdp 49 | - run: cargo run --example file_parser examples/sdps/11.sdp --expect-failure 50 | - run: cargo run --example file_parser examples/sdps/12.sdp --expect-failure 51 | - run: cargo run --example file_parser examples/sdps/13.sdp 52 | - run: cargo run --example file_parser examples/sdps/14.sdp --expect-failure 53 | - run: cargo run --example file_parser examples/sdps/15.sdp --expect-failure 54 | - run: cargo run --example file_parser examples/sdps/16.sdp --expect-failure 55 | - run: cargo run --example file_parser examples/sdps/17.sdp --expect-failure 56 | - run: cargo run --example file_parser examples/sdps/18.sdp --expect-failure 57 | - run: cargo run --example file_parser examples/sdps/19.sdp --expect-failure 58 | - run: cargo run --example file_parser examples/sdps/20.sdp --expect-failure 59 | - run: cargo run --example file_parser examples/sdps/21.sdp --expect-failure 60 | - run: cargo run --example file_parser examples/sdps/22.sdp --expect-failure 61 | - run: cargo run --example file_parser examples/sdps/23.sdp --expect-failure 62 | - run: cargo run --example file_parser examples/sdps/24.sdp --expect-failure 63 | - run: cargo run --example file_parser examples/sdps/25.sdp --expect-failure 64 | - run: cargo run --example file_parser examples/sdps/26.sdp --expect-failure 65 | - run: cargo run --example file_parser examples/sdps/27.sdp --expect-failure 66 | - run: cargo run --example file_parser examples/sdps/28.sdp --expect-failure 67 | - run: cargo run --example file_parser examples/sdps/29.sdp --expect-failure 68 | - run: cargo run --example file_parser examples/sdps/30.sdp --expect-failure 69 | - run: cargo run --example file_parser examples/sdps/31.sdp --expect-failure 70 | - run: cargo run --example file_parser examples/sdps/32.sdp --expect-failure 71 | - run: cargo run --example file_parser examples/sdps/33.sdp --expect-failure 72 | - run: cargo run --example file_parser examples/sdps/34.sdp 73 | - run: cargo run --example file_parser examples/sdps/35.sdp 74 | - run: cargo run --example file_parser examples/sdps/36.sdp 75 | - run: cargo run --example file_parser examples/sdps/37.sdp 76 | - run: cargo run --example file_parser examples/sdps/38.sdp 77 | - run: cargo run --example file_parser examples/sdps/39.sdp 78 | - run: cargo run --example file_parser examples/sdps/40.sdp 79 | - run: cargo run --example file_parser examples/sdps/41.sdp --expect-failure 80 | #TODO run code coverage 81 | 82 | #TODO split into build and test 83 | 84 | workflows: 85 | workflow: 86 | jobs: 87 | - build: 88 | matrix: 89 | parameters: 90 | features: ["", "serialize"] 91 | rustversion: ["1.70.0", "1.81"] 92 | latestrustversion: ["1.81"] 93 | 94 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | cache: cargo 3 | sudo: required 4 | dist: trusty 5 | os: 6 | - linux 7 | - osx 8 | 9 | env: 10 | - FEATURES="" 11 | - FEATURES="serialize" 12 | rust: 13 | - nightly 14 | - beta 15 | - stable 16 | # minimum stable version 17 | - 1.45.0 18 | 19 | matrix: 20 | allow_failures: 21 | - rust: nightly 22 | 23 | addons: 24 | apt: 25 | packages: 26 | - libcurl4-openssl-dev 27 | - zlib1g-dev 28 | - libiberty-dev 29 | - libelf-dev 30 | - libdw-dev 31 | - cmake 32 | - gcc 33 | - binutils-dev 34 | - libssl-dev 35 | 36 | # Add tarpaulin 37 | before_cache: | 38 | if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_RUST_VERSION" == "nightly" && "$FEATURES" == "serialize" ]]; then 39 | RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install cargo-tarpaulin -f 40 | fi 41 | 42 | # Add clippy 43 | before_script: 44 | - export PATH=$PATH:~/.cargo/bin 45 | - | 46 | if [[ "$TRAVIS_RUST_VERSION" == "stable" ]]; then 47 | rustup component add rustfmt-preview 48 | rustup component add clippy 49 | cargo install --force cargo-audit 50 | cargo generate-lockfile 51 | fi 52 | 53 | script: 54 | - echo FEATURES="$FEATURES" 55 | - | 56 | if [[ "$TRAVIS_RUST_VERSION" == "stable" ]]; then 57 | cargo audit 58 | cargo fmt --all -- --check 59 | fi 60 | - cargo clean 61 | - cargo build --verbose --all --features="$FEATURES" 62 | - | 63 | if [[ "$TRAVIS_RUST_VERSION" == "stable" ]]; then 64 | cargo clippy --all-targets --all-features -- -D warnings; 65 | fi 66 | - cargo test --all-targets --all-features --verbose --all 67 | - cargo run --example file_parser examples/sdps/02.sdp 68 | - cargo run --example file_parser examples/sdps/03.sdp --expect-failure 69 | - cargo run --example file_parser examples/sdps/04.sdp 70 | - cargo run --example file_parser examples/sdps/05.sdp 71 | - cargo run --example file_parser examples/sdps/06.sdp 72 | - cargo run --example file_parser examples/sdps/07.sdp 73 | - cargo run --example file_parser examples/sdps/08.sdp --expect-failure 74 | - cargo run --example file_parser examples/sdps/09.sdp 75 | - cargo run --example file_parser examples/sdps/10.sdp 76 | - cargo run --example file_parser examples/sdps/11.sdp --expect-failure 77 | - cargo run --example file_parser examples/sdps/12.sdp --expect-failure 78 | - cargo run --example file_parser examples/sdps/13.sdp 79 | - cargo run --example file_parser examples/sdps/14.sdp --expect-failure 80 | - cargo run --example file_parser examples/sdps/15.sdp --expect-failure 81 | - cargo run --example file_parser examples/sdps/16.sdp --expect-failure 82 | - cargo run --example file_parser examples/sdps/17.sdp --expect-failure 83 | - cargo run --example file_parser examples/sdps/18.sdp --expect-failure 84 | - cargo run --example file_parser examples/sdps/19.sdp --expect-failure 85 | - cargo run --example file_parser examples/sdps/20.sdp --expect-failure 86 | - cargo run --example file_parser examples/sdps/21.sdp --expect-failure 87 | - cargo run --example file_parser examples/sdps/22.sdp --expect-failure 88 | - cargo run --example file_parser examples/sdps/23.sdp --expect-failure 89 | - cargo run --example file_parser examples/sdps/24.sdp --expect-failure 90 | - cargo run --example file_parser examples/sdps/25.sdp --expect-failure 91 | - cargo run --example file_parser examples/sdps/26.sdp --expect-failure 92 | - cargo run --example file_parser examples/sdps/27.sdp --expect-failure 93 | - cargo run --example file_parser examples/sdps/28.sdp --expect-failure 94 | - cargo run --example file_parser examples/sdps/29.sdp --expect-failure 95 | - cargo run --example file_parser examples/sdps/30.sdp --expect-failure 96 | - cargo run --example file_parser examples/sdps/31.sdp --expect-failure 97 | - cargo run --example file_parser examples/sdps/32.sdp --expect-failure 98 | - cargo run --example file_parser examples/sdps/33.sdp --expect-failure 99 | - cargo run --example file_parser examples/sdps/34.sdp 100 | - cargo run --example file_parser examples/sdps/35.sdp 101 | - cargo run --example file_parser examples/sdps/36.sdp 102 | - cargo run --example file_parser examples/sdps/37.sdp 103 | - cargo run --example file_parser examples/sdps/38.sdp 104 | - cargo run --example file_parser examples/sdps/39.sdp 105 | - cargo run --example file_parser examples/sdps/40.sdp 106 | - cargo run --example file_parser examples/sdps/41.sdp --expect-failure 107 | 108 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | ## [0.3.13] - 2024-09-24 3 | 4 | - Add support for AV1 5 | - Minimum Rust version >= 1.70 6 | 7 | ## [0.3.12] - 2024-06-18 8 | 9 | - Add support for RFC4566/RFC8866 framerate 10 | 11 | ## [0.3.11] - 2024-01-17 12 | 13 | - Permit a wider set of payload type numbers 14 | 15 | ## [0.3.10] - 2023-01-05 16 | 17 | - Permit inconsistent simulcast directions 18 | 19 | ## [0.3.9] - 2022-01-12 20 | 21 | - Add support for RFC8858 rtcp-mux-only 22 | - Correct seperation of tokens in FMTP parameters 23 | - Do not emit an empty line after a media description 24 | 25 | ## [0.3.8] - 2021-01-16 26 | 27 | - fmt numbers 35 to 63 are now usable for dynamic allocation 28 | - parse extmap-allow-mixed as per RFC 8285 29 | 30 | ## [0.3.7] - 2020-11-23 31 | 32 | - Minimum Rust version >= 1.45 33 | - Added feature for parse object tree wide debug formatting, defaulted to on for now 34 | - Moved check for multiple c lines within an m section out of the setter and into the parsing logic, credit Mnwa 35 | 36 | ## [0.3.6] - 2020-05-07 37 | 38 | - Added support for Opus FMTP parameters ptime, maxptime, minptime, and maxaveragebitrate 39 | 40 | ## [0.3.5] - 2020-04-07 41 | 42 | ### Fixed 43 | 44 | - RTX apt can now be zero 45 | 46 | ## [0.3.4] - 2020-03-31 47 | 48 | ### Fixed 49 | 50 | - Fixed new clippy warnings in stable 51 | - Accept a lack of c= lines if there are no m= lines (for JSEP compat.) 52 | 53 | ### Changed 54 | 55 | - Added support for ssrc-group 56 | - Added support for RTX FMTP parameters 57 | - Example runner can no be told to expect failure 58 | 59 | ## [0.3.3] - 2019-12-10 60 | 61 | ### Changed 62 | 63 | - Changed handling of default channel counts 64 | 65 | ## [0.3.2] - 2019-12-02 66 | 67 | ### Changed 68 | 69 | - Fixed handling of spaces in fmtp attributes 70 | - Minimum Rust version >= 1.36 71 | 72 | ## [0.3.1] - 2019-09-12 73 | 74 | ### Changed 75 | 76 | - Updated `urls` dependency to `0.2.1` 77 | 78 | ### Removed 79 | 80 | - Removed `TcpTlsRtpSavpf` protocl token 81 | - Removed dependency on `enum-display-derive` 82 | 83 | ## [0.3.0] - 2019-08-08 84 | 85 | ### Changed 86 | 87 | - Unsafe code is forbidden now 88 | 89 | ### Fixed 90 | 91 | - Fixed panic from slicing unicode character in image attr braces 92 | 93 | ### Added 94 | 95 | - Added support for FQDN addresses 96 | - Added support for parsing ice-pacing 97 | - Added fuzzing target 98 | 99 | ## [0.2.2] - 2019-06-21 100 | 101 | ### Changed 102 | 103 | - Minimum Rust version >= 1.35 104 | 105 | ## [0.2.0] - 2019-06-15 106 | 107 | ### Changed 108 | 109 | - Minimum Rust version >= 1.30.0 110 | - Changed code coverage from kcov to tarpaulin 111 | - Moved file parser example to examples sub directory 112 | - Replaced cause() with source() in unit test 113 | - Moved all unit tests into tests modules 114 | 115 | ### Fixed 116 | 117 | - Unknown extensions in candidate attributes (#103) 118 | - Reduced amount of internal clone() calls significantly 119 | - Added dyn to error:Error impl required by more recent rust versions 120 | 121 | ### Added 122 | 123 | - Support for anonymization to enable logging of SDP without personal 124 | information 125 | - Quite a bit more unit testing got added 126 | 127 | ### Removed 128 | 129 | - Replaced unsupported types with errors directly in lib.rs 130 | 131 | ## [0.1.0] - 2019-01-26 132 | 133 | - Initial release 134 | - Minimum Rust version >= 1.17.0 135 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our [How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/) page. 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webrtc-sdp" 3 | version = "0.3.13" 4 | authors = ["Nicolas Grunbaum ", "Nils Ohlmeier "] 5 | description = "webrtc-sdp parses strings in the format of the Session Description Protocol according to RFC4566. It specifically supports the subset of features required to support WebRTC according to the JSEP draft." 6 | repository = "https://github.com/mozilla/webrtc-sdp" 7 | readme = "README.md" 8 | keywords = ["webrtc", "sdp", "jsep"] 9 | categories = ["parsing", "network-programming"] 10 | license = "MPL-2.0" 11 | 12 | [badges] 13 | travis-ci = { repository = "mozilla/webrtc-sdp", branch = "master" } 14 | codecov = { repository = "mozilla/webrtc-sdp", branch = "master", service = "github" } 15 | 16 | [features] 17 | default = ["enhanced_debug"] 18 | # debugging output 19 | enhanced_debug = [] 20 | # serializability 21 | serialize = ["serde", "serde_derive"] 22 | 23 | [dependencies] 24 | log = {version = "0.4"} 25 | serde = {version = "1.0" , optional = true} 26 | serde_derive = {version = "1.0" , optional = true} 27 | url = {version="2.1.0"} 28 | 29 | [dev-dependencies] 30 | serde_json = {version = "1.0"} 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webrtc-sdp 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/webrtc-sdp.svg)](https://crates.io/crates/webrtc-sdp) 4 | [![Build Status](https://travis-ci.org/mozilla/webrtc-sdp.svg?branch=master)](https://travis-ci.org/mozilla/webrtc-sdp) 5 | [![Codecov coverage status](https://codecov.io/gh/mozilla/webrtc-sdp/branch/master/graph/badge.svg)](https://codecov.io/gh/webrtc-sdp/webrtc-sdp) 6 | [![License: MPL 2.0](https://img.shields.io/badge/License-MPL%202.0-brightgreen.svg)](#License) 7 | [![dependency status](https://deps.rs/repo/github/mozilla/webrtc-sdp/status.svg)](https://deps.rs/repo/github/mozilla/webrtc-sdp) 8 | 9 | A SDP parser written in Rust specifically aimed to handle WebRTC SDP offers and answers. 10 | 11 | ## Dependecies 12 | 13 | * Rust >= 1.70.0 14 | * log module 15 | * serde module 16 | * serde-derive module 17 | 18 | Cargo installs the missing modules automatically when building webrtc-sdp for the first time. 19 | 20 | ## The webrtc-sdp API 21 | 22 | The main function is: 23 | ```rust 24 | fn parse_sdp(sdp: &str, fail_on_warning: bool) -> Result 25 | ``` 26 | The `sdp` parameter is the string which will get parsed. The `fail_on_warning` parameter determines how to treat warnings encountered during parsing. Any problems encountered during are stored until the whole string has been parsed. Any problem during parsing falls into two catgeories: 27 | 28 | * Fatal error preventing further parsing or processing of the SDP 29 | * Warning which don't block further processing of the SDP 30 | 31 | Warnings will be for example unknown parameters in attributes. Setting `fail_on_warning` to `true` makes most sense during development, when you want to be aware of all potential problems. In production `fail_on_warning` is expected to be `false`. 32 | 33 | `parse_sdp()` returns either an `SdpSession` struct ([code](https://github.com/mozilla/webrtc-sdp/blob/master/src/lib.rs#L137)) which contains all the parsed information. Or in case a fatal error was encountered (or if `fail_on_warning` was set to `true` and any warnings were encountered) an `SdpParserError` ([code](https://github.com/mozilla/webrtc-sdp/blob/master/src/error.rs#L117)) will be returned as a `Result`. 34 | 35 | ## Examples 36 | 37 | The [file parser](https://github.com/mozilla/webrtc-sdp/blob/master/examples/file_parser.rs) in the webrtc-sdp package gives you an easy example of how to invoke the webrtc-sdp parser. 38 | 39 | ## Contributing 40 | 41 | As the Travis CI runs are checking for code formating and clippy warnings please run the following commands locally, before submitting a Pull Request. 42 | 43 | If you haven't clippy and Rust format installed already you add them like this: 44 | ``` 45 | rustup component add rustfmt-preview 46 | rustup component add clippy 47 | ``` 48 | 49 | Check with clippy for warnings in the code: 50 | ``` 51 | cargo clippy 52 | ``` 53 | 54 | And format all of the code according to Rust code style convention: 55 | ``` 56 | cargo fmt --all 57 | ``` 58 | 59 | ## Fuzzing 60 | 61 | Install cargo-fuzz like this: 62 | ``` 63 | cargo install cargo-fuzz 64 | ``` 65 | 66 | With rust nightly you can start fuzzing like this: 67 | ``` 68 | cargo fuzz run fuzz_target_parse_sdp 69 | ``` 70 | 71 | ## License 72 | 73 | Licensed under [MPL-2.0](https://www.mozilla.org/MPL/2.0/) 74 | -------------------------------------------------------------------------------- /examples/file_parser.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use std::fs::File; 6 | use std::io::prelude::*; 7 | use std::panic; 8 | use std::path::Path; 9 | extern crate webrtc_sdp; 10 | 11 | // Takes the filename of a file that contains SDP, and optionally the trailing 12 | // flag --expect-failure. 13 | // If --expect-failure is passed, then the program will exit with a successful 14 | // exit code if the file fails to parse. 15 | fn main() { 16 | let mut args = std::env::args(); 17 | let filename = match args.nth(1) { 18 | None => { 19 | eprintln!("Missing file name argument!"); 20 | std::process::exit(1); 21 | } 22 | Some(x) => x, 23 | }; 24 | 25 | let path = Path::new(filename.as_str()); 26 | let display = path.display(); 27 | 28 | let mut file = match File::open(path) { 29 | Err(why) => panic!("Failed to open {}: {}", display, why), 30 | Ok(file) => file, 31 | }; 32 | 33 | let mut s = String::new(); 34 | match file.read_to_string(&mut s) { 35 | Err(why) => panic!("Couldn't read {}: {}", display, why), 36 | Ok(s) => s, 37 | }; 38 | 39 | // Hook up the panic handler if it is expected to fail to parse 40 | let expect_failure = if let Some(x) = args.next() { 41 | if x.to_lowercase() != "--expect-failure" { 42 | eprintln!("Extra arguments passed!"); 43 | std::process::exit(1); 44 | } 45 | panic::set_hook(Box::new(|_| { 46 | println!("Exited with failure, as expected."); 47 | std::process::exit(0); 48 | })); 49 | true 50 | } else { 51 | false 52 | }; 53 | 54 | // Remove comment lines 55 | let s = s 56 | .lines() 57 | .filter(|&l| !l.trim_start().starts_with(';')) 58 | .collect::>() 59 | .join("\r\n"); 60 | 61 | let res = webrtc_sdp::parse_sdp(&s, true); 62 | match res { 63 | Err(why) => panic!("Failed to parse SDP with error: {}", why), 64 | Ok(sdp) => println!("Parsed SDP structure:\n{sdp:#?}"), 65 | } 66 | 67 | if expect_failure { 68 | eprintln!("Successfully parsed SDP that was expected to fail. You may need to update the example expectations."); 69 | std::process::exit(1); 70 | } 71 | println!("Successfully parsed SDP"); 72 | } 73 | -------------------------------------------------------------------------------- /examples/sdps/02.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4294967296 2 IN IP4 127.0.0.1 3 | s=SIP Call 4 | c=IN IP4 198.51.100.7 5 | t=0 0 6 | m=video 56436 RTP/SAVPF 120 7 | a=rtpmap:120 VP8/90000 8 | -------------------------------------------------------------------------------- /examples/sdps/03.sdp: -------------------------------------------------------------------------------- 1 | ; Many RTCP-FB feed back types take a parameter 2 | ; Most of these will not parse. 3 | ; One can experiment by commenting out individual lines. 4 | ; Note: comment lines only work in the example parser. 5 | v=0 6 | o=- 4294967296 2 IN IP4 127.0.0.1 7 | s=SIP Call 8 | c=IN IP4 198.51.100.7 9 | t=0 0 10 | m=video 56436 RTP/SAVPF 120 11 | a=rtpmap:120 VP8/90000 12 | a=rtpmap:122 red/90000 13 | a=rtcp-fb:120 ack rpsi 14 | a=rtcp-fb:120 ack app 15 | a=rtcp-fb:120 ack app foo 16 | a=rtcp-fb:120 ack foo bar 17 | a=rtcp-fb:120 ack foo bar baz 18 | a=rtcp-fb:120 nack 19 | a=rtcp-fb:120 nack pli 20 | a=rtcp-fb:120 nack sli 21 | a=rtcp-fb:120 nack rpsi 22 | a=rtcp-fb:120 nack app 23 | a=rtcp-fb:120 nack app foo 24 | a=rtcp-fb:120 nack app foo bar 25 | a=rtcp-fb:120 nack foo bar baz 26 | a=rtcp-fb:120 trr-int 0 27 | a=rtcp-fb:120 trr-int 123 28 | a=rtcp-fb:120 goog-remb 29 | a=rtcp-fb:120 ccm fir 30 | a=rtcp-fb:120 ccm tmmbr 31 | a=rtcp-fb:120 ccm tstr 32 | a=rtcp-fb:120 ccm vbcm 123 456 789 33 | a=rtcp-fb:120 ccm foo 34 | a=rtcp-fb:120 ccm foo bar baz 35 | a=rtcp-fb:120 foo 36 | a=rtcp-fb:120 foo bar 37 | a=rtcp-fb:120 foo bar baz 38 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 39 | -------------------------------------------------------------------------------- /examples/sdps/04.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4294967296 2 IN IP4 127.0.0.1 3 | s=SIP Call 4 | t=0 0 5 | m=video 56436 RTP/SAVPF 120 6 | c=IN IP4 198.51.100.7 7 | a=rtpmap:120 VP8/90000 8 | -------------------------------------------------------------------------------- /examples/sdps/05.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4294967296 2 IN IP4 127.0.0.1 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=ice-lite 7 | -------------------------------------------------------------------------------- /examples/sdps/06.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4294967296 2 IN IP4 127.0.0.1 3 | s=SIP Call 4 | c=IN IP4 198.51.100.7 5 | b=CT:5000 6 | b=FOOBAR:10 7 | b=AS:4 8 | t=0 0 9 | m=video 56436 RTP/SAVPF 120 10 | a=rtpmap:120 VP8/90000 11 | m=audio 12345/2 RTP/SAVPF 0 12 | a=rtpmap:0 PCMU/8000 13 | -------------------------------------------------------------------------------- /examples/sdps/07.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4294967296 2 IN IP4 127.0.0.1 3 | s= 4 | c=IN IP4 198.51.100.7 5 | t=0 0 6 | m=video 56436 RTP/SAVPF 120 7 | b=CT:1000 8 | a=rtpmap:120 VP8/90000 9 | -------------------------------------------------------------------------------- /examples/sdps/08.sdp: -------------------------------------------------------------------------------- 1 | ; 2 | ; This example fails to parse because of a mismatched fingerprint length 3 | ; 4 | v=0 5 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 6 | s=SIP Call 7 | c=IN IP4 224.0.0.1/100/12 8 | t=0 0 9 | a=ice-ufrag:4a799b2e 10 | a=ice-pwd:e4cc12a910f106a0a744719425510e17 11 | a=ice-lite 12 | a=ice-options:trickle foo 13 | a=msid-semantic:WMS stream streama 14 | a=msid-semantic:foo stream 15 | a=fingerprint:sha-256 DF:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C 16 | a=identity:eyJpZHAiOnsiZG9tYWluIjoiZXhhbXBsZS5vcmciLCJwcm90b2NvbCI6ImJvZ3VzIn0sImFzc2VydGlvbiI6IntcImlkZW50aXR5XCI6XCJib2JAZXhhbXBsZS5vcmdcIixcImNvbnRlbnRzXCI6XCJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3l6XCIsXCJzaWduYXR1cmVcIjpcIjAxMDIwMzA0MDUwNlwifSJ9 17 | a=group:BUNDLE first second 18 | a=group:BUNDLE third 19 | a=group:LS first third 20 | m=audio 9 RTP/SAVPF 109 9 0 8 101 21 | c=IN IP4 0.0.0.0 22 | a=mid:first 23 | a=rtpmap:109 opus/48000/2 24 | a=ptime:20 25 | a=maxptime:20 26 | a=rtpmap:9 G722/8000 27 | a=rtpmap:0 PCMU/8000 28 | a=rtpmap:8 PCMA/8000 29 | a=rtpmap:101 telephone-event/8000 30 | a=fmtp:101 0-15,66,32-34,67 31 | a=ice-ufrag:00000000 32 | a=ice-pwd:0000000000000000000000000000000 33 | a=sendonly 34 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 35 | a=setup:actpass 36 | a=rtcp-mux 37 | a=msid:stream track 38 | a=candidate:0 1 UDP 2130379007 10.0.0.36 62453 typ host 39 | a=candidate:2 1 UDP 1694236671 24.6.134.204 62453 typ srflx raddr 10.0.0.36 rport 62453 40 | a=candidate:3 1 UDP 100401151 162.222.183.171 49761 typ relay raddr 162.222.183.171 rport 49761 41 | a=candidate:6 1 UDP 16515071 162.222.183.171 51858 typ relay raddr 162.222.183.171 rport 51858 42 | a=candidate:3 2 UDP 100401150 162.222.183.171 62454 typ relay raddr 162.222.183.171 rport 62454 43 | a=candidate:2 2 UDP 1694236670 24.6.134.204 55428 typ srflx raddr 10.0.0.36 rport 55428 44 | a=candidate:6 2 UDP 16515070 162.222.183.171 50340 typ relay raddr 162.222.183.171 rport 50340 45 | a=candidate:0 2 UDP 2130379006 10.0.0.36 55428 typ host 46 | a=rtcp:62454 IN IP4 162.222.183.171 47 | a=end-of-candidates 48 | a=ssrc:5150 49 | m=video 9 RTP/SAVPF 120 121 122 123 50 | c=IN IP6 ::1 51 | a=fingerprint:sha-1 DF:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7 52 | a=mid:second 53 | a=rtpmap:120 VP8/90000 54 | a=rtpmap:121 VP9/90000 55 | a=rtpmap:122 red/90000 56 | a=rtpmap:123 ulpfec/90000 57 | a=recvonly 58 | a=rtcp-fb:120 nack 59 | a=rtcp-fb:120 nack pli 60 | a=rtcp-fb:120 ccm fir 61 | a=rtcp-fb:121 nack 62 | a=rtcp-fb:121 nack pli 63 | a=rtcp-fb:121 ccm fir 64 | a=setup:active 65 | a=rtcp-mux 66 | a=msid:streama tracka 67 | a=msid:streamb trackb 68 | a=candidate:0 1 UDP 2130379007 10.0.0.36 59530 typ host 69 | a=candidate:0 2 UDP 2130379006 10.0.0.36 64378 typ host 70 | a=candidate:2 2 UDP 1694236670 24.6.134.204 64378 typ srflx raddr 10.0.0.36 rport 64378 71 | a=candidate:6 2 UDP 16515070 162.222.183.171 64941 typ relay raddr 162.222.183.171 rport 64941 72 | a=candidate:6 1 UDP 16515071 162.222.183.171 64800 typ relay raddr 162.222.183.171 rport 64800 73 | a=candidate:2 1 UDP 1694236671 24.6.134.204 59530 typ srflx raddr 10.0.0.36 rport 59530 74 | a=candidate:3 1 UDP 100401151 162.222.183.171 62935 typ relay raddr 162.222.183.171 rport 62935 75 | a=candidate:3 2 UDP 100401150 162.222.183.171 61026 typ relay raddr 162.222.183.171 rport 61026 76 | a=rtcp:61026 77 | a=end-of-candidates 78 | a=ssrc:1111 foo 79 | a=ssrc:1111 foo:bar 80 | a=imageattr:120 send * recv * 81 | m=audio 9 RTP/SAVPF 0 82 | a=mid:third 83 | a=rtpmap:0 PCMU/8000 84 | a=ice-lite 85 | a=ice-options:foo bar 86 | a=msid:noappdata 87 | a=bundle-only 88 | -------------------------------------------------------------------------------- /examples/sdps/09.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4294967296 2 IN IP4 127.0.0.1 3 | s=SIP Call 4 | c=IN IP4 198.51.100.7 5 | t=0 0 6 | m=audio 9 RTP/SAVPF 109 9 0 8 101 7 | c=IN IP4 0.0.0.0 8 | a=mid:first 9 | a=rtpmap:109 opus/48000/2 10 | a=ptime:20 11 | a=maxptime:20 12 | a=rtpmap:9 G722/8000 13 | a=rtpmap:0 PCMU/8000 14 | a=rtpmap:8 PCMA/8000 15 | a=rtpmap:101 telephone-event/8000 16 | a=fmtp:101 0-15 17 | a=fmtp:101 0-5. 18 | a=fmtp:101 0-15,66,67 19 | a=fmtp:101 0,1,2-4,5-15,66,67 20 | a=fmtp:101 5,6,7 21 | a=fmtp:101 0 22 | a=fmtp:101 1 23 | a=fmtp:101 123 24 | a=fmtp:101 0-123 25 | a=fmtp:101 -12 26 | a=fmtp:101 12- 27 | a=fmtp:101 1,12-,4 28 | a=fmtp:101 ,2,3 29 | a=fmtp:101 ,,,2,3 30 | a=fmtp:101 1,,,,,,,,3 31 | a=fmtp:101 1,2,3, 32 | a=fmtp:101 1-2-3 33 | a=fmtp:101 112233 34 | a=fmtp:101 33-2 35 | -------------------------------------------------------------------------------- /examples/sdps/10.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4294967296 2 IN IP4 127.0.0.1 3 | s=SIP Call 4 | c=IN IP4 198.51.100.7 5 | t=0 0 6 | m=video 9 RTP/SAVPF 97 120 121 122 123 7 | c=IN IP6 ::1 8 | a=fingerprint:sha-1 DF:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C 9 | a=rtpmap:97 H264/90000 10 | a=rtpmap:120 VP8/90000 11 | a=rtpmap:121 VP9/90000 12 | a=rtpmap:122 red/90000 13 | a=rtpmap:123 ulpfec/90000 14 | -------------------------------------------------------------------------------- /examples/sdps/11.sdp: -------------------------------------------------------------------------------- 1 | ; 2 | ; This example fails to parse because a=ice-lite is session only 3 | ; 4 | v=0 5 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 6 | s=SIP Call 7 | c=IN IP4 224.0.0.1/100/12 8 | t=0 0 9 | a=ice-ufrag:4a799b2e 10 | a=ice-pwd:e4cc12a910f106a0a744719425510e17 11 | a=ice-lite 12 | a=msid-semantic:WMS stream streama 13 | a=fingerprint:sha-256 DF:2E:AC:8A:FD:0A:8E:99:BF:5D:E8:3C:E7:FA:FB:08:3B:3C:54:1D:D7:D4:05:77:A0:72:9B:14:08:6D:0F:4C 14 | a=group:BUNDLE first second 15 | a=group:BUNDLE third 16 | a=group:LS first third 17 | m=audio 9 RTP/SAVPF 109 9 0 8 101 18 | c=IN IP4 0.0.0.0 19 | a=mid:first 20 | a=rtpmap:109 opus/48000/2 21 | a=ptime:20 22 | a=maxptime:20 23 | a=rtpmap:9 G722/8000 24 | a=rtpmap:0 PCMU/8000 25 | a=rtpmap:8 PCMA/8000 26 | a=rtpmap:101 telephone-event/8000 27 | a=fmtp:101 0-15,66,32-34,67 28 | a=ice-ufrag:00000000 29 | a=ice-pwd:0000000000000000000000000000000 30 | a=sendonly 31 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 32 | a=setup:actpass 33 | a=rtcp-mux 34 | a=msid:stream track 35 | a=candidate:0 1 UDP 2130379007 10.0.0.36 62453 typ host 36 | a=candidate:2 1 UDP 1694236671 24.6.134.204 62453 typ srflx raddr 10.0.0.36 rport 62453 37 | a=candidate:3 1 UDP 100401151 162.222.183.171 49761 typ relay raddr 162.222.183.171 rport 49761 38 | a=candidate:6 1 UDP 16515071 162.222.183.171 51858 typ relay raddr 162.222.183.171 rport 51858 39 | a=candidate:3 2 UDP 100401150 162.222.183.171 62454 typ relay raddr 162.222.183.171 rport 62454 40 | a=candidate:2 2 UDP 1694236670 24.6.134.204 55428 typ srflx raddr 10.0.0.36 rport 55428 41 | a=candidate:6 2 UDP 16515070 162.222.183.171 50340 typ relay raddr 162.222.183.171 rport 50340 42 | a=candidate:0 2 UDP 2130379006 10.0.0.36 55428 typ host 43 | m=video 9 RTP/SAVPF 97 98 120 44 | c=IN IP6 ::1 45 | a=mid:second 46 | a=rtpmap:97 H264/90000 47 | a=rtpmap:98 H264/90000 48 | a=rtpmap:120 VP8/90000 49 | a=recvonly 50 | a=setup:active 51 | a=rtcp-mux 52 | a=msid:streama tracka 53 | a=msid:streamb trackb 54 | a=candidate:0 1 UDP 2130379007 10.0.0.36 59530 typ host 55 | a=candidate:0 2 UDP 2130379006 10.0.0.36 64378 typ host 56 | a=candidate:2 2 UDP 1694236670 24.6.134.204 64378 typ srflx raddr 10.0.0.36 rport 64378 57 | a=candidate:6 2 UDP 16515070 162.222.183.171 64941 typ relay raddr 162.222.183.171 rport 64941 58 | a=candidate:6 1 UDP 16515071 162.222.183.171 64800 typ relay raddr 162.222.183.171 rport 64800 59 | a=candidate:2 1 UDP 1694236671 24.6.134.204 59530 typ srflx raddr 10.0.0.36 rport 59530 60 | a=candidate:3 1 UDP 100401151 162.222.183.171 62935 typ relay raddr 162.222.183.171 rport 62935 61 | a=candidate:3 2 UDP 100401150 162.222.183.171 61026 typ relay raddr 162.222.183.171 rport 61026 62 | m=audio 9 RTP/SAVPF 0 63 | a=mid:third 64 | a=rtpmap:0 PCMU/8000 65 | a=ice-lite 66 | a=msid:noappdata 67 | -------------------------------------------------------------------------------- /examples/sdps/12.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 27987 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | t=0 0 5 | a=ice-ufrag:8a39d2ae 6 | a=ice-pwd:601d53aba51a318351b3ecf5ee00048f 7 | a=fingerprint:sha-256 30:FF:8E:2B:AC:9D:ED:70:18:10:67:C8:AE:9E:68:F3:86:53:51:B0:AC:31:B7:BE:6D:CF:A4:2E:D3:6E:B4:28 8 | m=audio 9 RTP/SAVPF 109 9 0 8 101 9 | c=IN IP4 0.0.0.0 10 | a=rtpmap:109 opus/48000/2 11 | a=ptime:20 12 | a=rtpmap:9 G722/8000 13 | a=rtpmap:0 PCMU/8000 14 | a=rtpmap:8 PCMA/8000 15 | a=rtpmap:101 telephone-event/8000 16 | a=fmtp:101 0-15 17 | a=sendrecv 18 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 19 | a=extmap:2/sendonly some_extension 20 | a=extmap:3 some_other_extension some_params some more params 21 | a=setup:actpass 22 | a=rtcp-mux 23 | m=video 9 RTP/SAVPF 120 126 97 24 | c=IN IP4 0.0.0.0 25 | a=rtpmap:120 VP8/90000 26 | a=rtpmap:126 H264/90000 27 | a=rtpmap:97 H264/90000 28 | a=sendrecv 29 | a=rtcp-fb:120 ack rpsi 30 | a=rtcp-fb:120 ack app foo 31 | a=rtcp-fb:120 ack foo 32 | a=rtcp-fb:120 nack 33 | a=rtcp-fb:120 nack sli 34 | a=rtcp-fb:120 nack pli 35 | a=rtcp-fb:120 nack rpsi 36 | a=rtcp-fb:120 nack app foo 37 | a=rtcp-fb:120 nack foo 38 | a=rtcp-fb:120 ccm fir 39 | a=rtcp-fb:120 ccm tmmbr 40 | a=rtcp-fb:120 ccm tstr 41 | a=rtcp-fb:120 ccm vbcm 42 | a=rtcp-fb:120 ccm foo 43 | a=rtcp-fb:120 trr-int 10 44 | a=rtcp-fb:120 goog-remb 45 | a=rtcp-fb:120 foo 46 | a=rtcp-fb:126 nack 47 | a=rtcp-fb:126 nack pli 48 | a=rtcp-fb:126 ccm fir 49 | a=rtcp-fb:97 nack 50 | a=rtcp-fb:97 nack pli 51 | a=rtcp-fb:97 ccm fir 52 | a=rtcp-fb:* ccm tmmbr 53 | a=setup:actpass 54 | a=rtcp-mux 55 | m=application 9 DTLS/SCTP 5000 56 | c=IN IP4 0.0.0.0 57 | a=sctpmap:5000 webrtc-datachannel 16 58 | a=setup:actpass 59 | -------------------------------------------------------------------------------- /examples/sdps/13.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 27987 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | t=0 0 5 | a=ice-ufrag:8a39d2ae 6 | a=ice-pwd:601d53aba51a318351b3ecf5ee00048f 7 | a=fingerprint:sha-256 30:FF:8E:2B:AC:9D:ED:70:18:10:67:C8:AE:9E:68:F3:86:53:51:B0:AC:31:B7:BE:6D:CF:A4:2E:D3:6E:B4:28 8 | m=application 9 UDP/DTLS/SCTP webrtc-datachannel 9 | c=IN IP4 0.0.0.0 10 | a=sctp-port:5000 11 | a=max-message-size:10000 12 | a=setup:actpass 13 | -------------------------------------------------------------------------------- /examples/sdps/14.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=candidate:0 1 UDP 2130379007 10.0.0.36 62453 typ host 7 | m=audio 9 RTP/SAVPF 109 9 0 8 101 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:109 opus/48000/2 10 | -------------------------------------------------------------------------------- /examples/sdps/15.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=bundle-only 7 | m=audio 9 RTP/SAVPF 109 9 0 8 101 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:109 opus/48000/2 10 | -------------------------------------------------------------------------------- /examples/sdps/16.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=fmtp:109 0-15 7 | m=audio 9 RTP/SAVPF 109 9 0 8 101 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:109 opus/48000/2 10 | -------------------------------------------------------------------------------- /examples/sdps/17.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=ice-mismatch 7 | m=audio 9 RTP/SAVPF 109 9 0 8 101 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:109 opus/48000/2 10 | -------------------------------------------------------------------------------- /examples/sdps/18.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=imageattr:120 send * recv * 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/19.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=label:foobar 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/20.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=maxptime:100 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/21.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=mid:foobar 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/22.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=msid:foobar 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/23.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=ptime:50 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/24.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=remote-candidates:0 10.0.0.1 5555 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/25.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=rtcp:5555 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/26.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=rtcp-fb:120 nack 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/27.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=rtcp-mux 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/28.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=rtcp-rsize 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/29.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=rtpmap:120 VP8/90000 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | -------------------------------------------------------------------------------- /examples/sdps/30.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=sctpmap:5000 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/31.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=ssrc:5000 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/32.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | a=ssrc-group:FID 5000 7 | m=video 9 RTP/SAVPF 120 8 | c=IN IP4 0.0.0.0 9 | a=rtpmap:120 VP8/90000 10 | -------------------------------------------------------------------------------- /examples/sdps/33.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | m=video 9 RTP/SAVPF 120 7 | c=IN IP4 0.0.0.0 8 | a=rtpmap:120 VP8/90000 9 | a=imageattr:flob 10 | -------------------------------------------------------------------------------- /examples/sdps/34.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4294967296 2 IN IP4 127.0.0.1 3 | s=SIP Call 4 | c=IN IP4 198.51.100.7 5 | b=CT:5000 6 | t=0 0 7 | m=video 56436 RTP/SAVPF 120 8 | a=rtpmap:120 VP8/90000 9 | a=sendrecv 10 | -------------------------------------------------------------------------------- /examples/sdps/35.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4294967296 2 IN IP4 127.0.0.1 3 | s=SIP Call 4 | c=IN IP4 198.51.100.7 5 | b=CT:5000 6 | t=0 0 7 | m=video 56436 RTP/SAVPF 120 8 | a=rtpmap:120 VP8/90000 9 | a=sendrecv 10 | -------------------------------------------------------------------------------- /examples/sdps/36.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4294967296 2 IN IP4 127.0.0.1 3 | s=SIP Call 4 | c=IN IP4 198.51.100.7 5 | b=CT:5000 6 | t=0 0 7 | m=video 56436 RTP/SAVPF 120 8 | a=rtpmap:120 VP8/90000 9 | a=sendrecv 10 | -------------------------------------------------------------------------------- /examples/sdps/37.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4294967296 2 IN IP4 127.0.0.1 3 | s=SIP Call 4 | c=IN IP4 198.51.100.7 5 | b=CT:5000 6 | t=0 0 7 | m=video 56436 RTP/SAVPF 120 8 | a=rtpmap:120 VP8/90000 9 | a=recvonly 10 | -------------------------------------------------------------------------------- /examples/sdps/38.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 4294967296 2 IN IP4 127.0.0.1 3 | s=SIP Call 4 | c=IN IP4 198.51.100.7 5 | b=CT:5000 6 | t=0 0 7 | m=video 56436 RTP/SAVPF 120 8 | a=rtpmap:120 VP8/90000 9 | a=sendonly 10 | -------------------------------------------------------------------------------- /examples/sdps/39.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | m=video 9 RTP/SAVPF 120 7 | c=IN IP4 0.0.0.0 8 | a=rtpmap:120 VP8/90000 9 | -------------------------------------------------------------------------------- /examples/sdps/40.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=Mozilla-SIPUA-35.0a1 5184 0 IN IP4 0.0.0.0 3 | s=SIP Call 4 | c=IN IP4 224.0.0.1/100/12 5 | t=0 0 6 | m=video 9 RTP/SAVPF 120 7 | c=IN IP4 0.0.0.0 8 | a=rtpmap:120 VP8/90000 9 | -------------------------------------------------------------------------------- /examples/sdps/41.sdp: -------------------------------------------------------------------------------- 1 | v=0 2 | o=- 1109973417102828257 2 IN IP4 127.0.0.1 3 | s=- 4 | t=0 0 5 | a=group:BUNDLE audio video 6 | a=msid-semantic: WMS 1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP 7 | m=audio 32952 UDP/TLS/RTP/SAVPF 111 103 104 0 8 107 106 105 13 126 8 | c=IN IP4 128.64.32.16 9 | a=rtcp:32952 IN IP4 128.64.32.16 10 | a=candidate:77142221 1 udp 2113937151 192.168.137.1 54081 typ host generation 0 11 | a=candidate:77142221 2 udp 2113937151 192.168.137.1 54081 typ host generation 0 12 | a=candidate:983072742 1 udp 2113937151 172.22.0.56 54082 typ host generation 0 13 | a=candidate:983072742 2 udp 2113937151 172.22.0.56 54082 typ host generation 0 14 | a=candidate:2245074553 1 udp 1845501695 32.64.128.1 62397 typ srflx raddr 192.168.137.1 rport 54081 generation 0 15 | a=candidate:2245074553 2 udp 1845501695 32.64.128.1 62397 typ srflx raddr 192.168.137.1 rport 54081 generation 0 16 | a=candidate:2479353907 1 udp 1845501695 32.64.128.1 54082 typ srflx raddr 172.22.0.56 rport 54082 generation 0 17 | a=candidate:2479353907 2 udp 1845501695 32.64.128.1 54082 typ srflx raddr 172.22.0.56 rport 54082 generation 0 18 | a=candidate:1243276349 1 tcp 1509957375 192.168.137.1 0 typ host generation 0 19 | a=candidate:1243276349 2 tcp 1509957375 192.168.137.1 0 typ host generation 0 20 | a=candidate:1947960086 1 tcp 1509957375 172.22.0.56 0 typ host generation 0 21 | a=candidate:1947960086 2 tcp 1509957375 172.22.0.56 0 typ host generation 0 22 | a=candidate:1808221584 1 udp 33562367 128.64.32.16 32952 typ relay raddr 32.64.128.1 rport 62398 generation 0 23 | a=candidate:1808221584 2 udp 33562367 128.64.32.16 32952 typ relay raddr 32.64.128.1 rport 62398 generation 0 24 | a=candidate:507872740 1 udp 33562367 128.64.32.16 40975 typ relay raddr 32.64.128.1 rport 54085 generation 0 25 | a=candidate:507872740 2 udp 33562367 128.64.32.16 40975 typ relay raddr 32.64.128.1 rport 54085 generation 0 26 | a=ice-ufrag:xQuJwjX3V3eMA81k 27 | a=ice-pwd:ZUiRmjS2GDhG140p73dAsSVP 28 | a=ice-options:google-ice 29 | a=fingerprint:sha-256 59:4A:8B:73:A7:73:53:71:88:D7:4D:58:28:0C:79:72:31:29:9B:05:37:DD:58:43:C2:D4:85:A2:B3:66:38:7A 30 | a=setup:active 31 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level 32 | a=sendrecv 33 | a=mid:audio 34 | a=rtcp-mux 35 | a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:/U44g3ULdtapeiSg+T3n6dDLBKIjpOhb/NXAL/2b 36 | a=rtpmap:111 opus/48000/2 37 | a=rtpmap:103 ISAC/16000 38 | a=rtpmap:104 ISAC/32000 39 | a=rtpmap:0 PCMU/8000 40 | a=rtpmap:8 PCMA/8000 41 | a=rtpmap:107 CN/48000 42 | a=rtpmap:106 CN/32000 43 | a=rtpmap:105 CN/16000 44 | a=rtpmap:13 CN/8000 45 | a=rtpmap:126 telephone-event/8000 46 | a=maxptime:60 47 | a=ssrc:2271517329 cname:mKDNt7SQf6pwDlIn 48 | a=ssrc:2271517329 msid:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP 1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIPa0 49 | a=ssrc:2271517329 mslabel:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP 50 | a=ssrc:2271517329 label:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIPa0 51 | m=video 32952 UDP/TLS/RTP/SAVPF 100 116 117 52 | c=IN IP4 128.64.32.16 53 | a=rtcp:32952 IN IP4 128.64.32.16 54 | a=candidate:77142221 1 udp 2113937151 192.168.137.1 54081 typ host generation 0 55 | a=candidate:77142221 2 udp 2113937151 192.168.137.1 54081 typ host generation 0 56 | a=candidate:983072742 1 udp 2113937151 172.22.0.56 54082 typ host generation 0 57 | a=candidate:983072742 2 udp 2113937151 172.22.0.56 54082 typ host generation 0 58 | a=candidate:2245074553 1 udp 1845501695 32.64.128.1 62397 typ srflx raddr 192.168.137.1 rport 54081 generation 0 59 | a=candidate:2245074553 2 udp 1845501695 32.64.128.1 62397 typ srflx raddr 192.168.137.1 rport 54081 generation 0 60 | a=candidate:2479353907 1 udp 1845501695 32.64.128.1 54082 typ srflx raddr 172.22.0.56 rport 54082 generation 0 61 | a=candidate:2479353907 2 udp 1845501695 32.64.128.1 54082 typ srflx raddr 172.22.0.56 rport 54082 generation 0 62 | a=candidate:1243276349 1 tcp 1509957375 192.168.137.1 0 typ host generation 0 63 | a=candidate:1243276349 2 tcp 1509957375 192.168.137.1 0 typ host generation 0 64 | a=candidate:1947960086 1 tcp 1509957375 172.22.0.56 0 typ host generation 0 65 | a=candidate:1947960086 2 tcp 1509957375 172.22.0.56 0 typ host generation 0 66 | a=candidate:1808221584 1 udp 33562367 128.64.32.16 32952 typ relay raddr 32.64.128.1 rport 62398 generation 0 67 | a=candidate:1808221584 2 udp 33562367 128.64.32.16 32952 typ relay raddr 32.64.128.1 rport 62398 generation 0 68 | a=candidate:507872740 1 udp 33562367 128.64.32.16 40975 typ relay raddr 32.64.128.1 rport 54085 generation 0 69 | a=candidate:507872740 2 udp 33562367 128.64.32.16 40975 typ relay raddr 32.64.128.1 rport 54085 generation 0 70 | a=ice-ufrag:xQuJwjX3V3eMA81k 71 | a=ice-pwd:ZUiRmjS2GDhG140p73dAsSVP 72 | a=ice-options:google-ice 73 | a=fingerprint:sha-256 59:4A:8B:73:A7:73:53:71:88:D7:4D:58:28:0C:79:72:31:29:9B:05:37:DD:58:43:C2:D4:85:A2:B3:66:38:7A 74 | a=setup:active 75 | a=extmap:2 urn:ietf:params:rtp-hdrext:toffset 76 | a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time 77 | a=sendrecv 78 | a=mid:video 79 | a=rtcp-mux 80 | a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:/U44g3ULdtapeiSg+T3n6dDLBKIjpOhb/NXAL/2b 81 | a=rtpmap:100 VP8/90000 82 | a=rtcp-fb:100 ccm fir 83 | a=rtcp-fb:100 nack 84 | a=rtcp-fb:100 goog-remb 85 | a=rtpmap:116 red/90000 86 | a=rtpmap:117 ulpfec/90000 87 | a=ssrc:54724160 cname:mKDNt7SQf6pwDlIn 88 | a=ssrc:54724160 msid:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP 1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIPv0 89 | a=ssrc:54724160 mslabel:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIP 90 | a=ssrc:54724160 label:1PBxet5BYh0oYodwsvNM4k6KiO2eWCX40VIPv0 91 | 92 | -------------------------------------------------------------------------------- /examples/sdps/extract.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | grep '\"[ a-z]=[^=]*$' sdp_unittests.cpp | grep -v 'ParseSdp(kVideoSdp' | grep -v 'kVideoWithRedAndUlpfec' | grep -v 'ASSERT_NE' | grep -v 'BASE64_DTLS_HELLO' | grep -v '^\/\/' | sed 's/ParseSdp(//g' | sed 's/^[[:space:]]*//' | sed 's/+ //' | sed 's/ \/\/.*$//' | sed 's/\;$//' | sed 's/)$//' | sed 's/, false//' | sed 's/" CRLF//' | sed 's/^\"//' | sed 's/\"$//' | sed 's/\\r\\n//' | gawk -v RS='(^|\n)v=' '/./ { print "v="$0 > NR".sdp" }' 4 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "webrtc-sdp-fuzz" 4 | version = "0.0.1" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | 8 | [package.metadata] 9 | cargo-fuzz = true 10 | 11 | [dependencies.webrtc-sdp] 12 | path = ".." 13 | [dependencies.libfuzzer-sys] 14 | git = "https://github.com/rust-fuzz/libfuzzer-sys.git" 15 | 16 | # Prevent this from interfering with workspaces 17 | [workspace] 18 | members = ["."] 19 | 20 | [[bin]] 21 | name = "fuzz_target_parse_sdp" 22 | path = "fuzz_targets/fuzz_target_parse_sdp.rs" 23 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_target_parse_sdp.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | #![no_main] 6 | #[macro_use] extern crate libfuzzer_sys; 7 | extern crate webrtc_sdp; 8 | 9 | fuzz_target!(|data: &[u8]| { 10 | if let Ok(s) = std::str::from_utf8(data) { 11 | let _ = webrtc_sdp::parse_sdp(s, true); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /src/address.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | extern crate url; 6 | use self::url::Host; 7 | use error::SdpParserInternalError; 8 | use std::convert::TryFrom; 9 | use std::fmt; 10 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 11 | use std::str::FromStr; 12 | 13 | #[derive(Clone, Debug)] 14 | #[cfg_attr(feature = "serialize", derive(Serialize))] 15 | pub enum Address { 16 | Fqdn(String), 17 | Ip(IpAddr), 18 | } 19 | 20 | impl fmt::Display for Address { 21 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 22 | match self { 23 | Address::Fqdn(fqdn) => fqdn.fmt(f), 24 | Address::Ip(ip) => ip.fmt(f), 25 | } 26 | } 27 | } 28 | 29 | impl FromStr for Address { 30 | type Err = SdpParserInternalError; 31 | fn from_str(s: &str) -> Result { 32 | let mut e: Option = None; 33 | if s.find(':').is_some() { 34 | match IpAddr::from_str(s) { 35 | Ok(ip) => return Ok(Address::Ip(ip)), 36 | Err(err) => e = Some(err.into()), 37 | } 38 | } 39 | Host::parse(s) 40 | .map(|host| match host { 41 | Host::Domain(s) => Address::Fqdn(s), 42 | Host::Ipv4(ip) => Address::Ip(IpAddr::V4(ip)), 43 | Host::Ipv6(ip) => Address::Ip(IpAddr::V6(ip)), 44 | }) 45 | .map_err(|err| e.unwrap_or_else(|| err.into())) 46 | } 47 | } 48 | 49 | impl From for Address { 50 | fn from(item: ExplicitlyTypedAddress) -> Self { 51 | match item { 52 | ExplicitlyTypedAddress::Fqdn { domain, .. } => Address::Fqdn(domain), 53 | ExplicitlyTypedAddress::Ip(ip) => Address::Ip(ip), 54 | } 55 | } 56 | } 57 | 58 | impl PartialEq for Address { 59 | fn eq(&self, other: &Self) -> bool { 60 | match (self, other) { 61 | (Address::Fqdn(a), Address::Fqdn(b)) => a.to_lowercase() == b.to_lowercase(), 62 | (Address::Ip(a), Address::Ip(b)) => a == b, 63 | (_, _) => false, 64 | } 65 | } 66 | } 67 | 68 | #[derive(Clone, Copy, PartialEq, Debug)] 69 | #[cfg_attr(feature = "serialize", derive(Serialize))] 70 | pub enum AddressType { 71 | IpV4 = 4, 72 | IpV6 = 6, 73 | } 74 | 75 | impl fmt::Display for AddressType { 76 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 77 | match self { 78 | AddressType::IpV4 => "IP4", 79 | AddressType::IpV6 => "IP6", 80 | } 81 | .fmt(f) 82 | } 83 | } 84 | 85 | impl FromStr for AddressType { 86 | type Err = SdpParserInternalError; 87 | fn from_str(s: &str) -> Result { 88 | match s.to_uppercase().as_str() { 89 | "IP4" => Ok(AddressType::IpV4), 90 | "IP6" => Ok(AddressType::IpV6), 91 | _ => Err(SdpParserInternalError::UnknownAddressType(s.to_owned())), 92 | } 93 | } 94 | } 95 | 96 | pub trait AddressTyped { 97 | fn address_type(&self) -> AddressType; 98 | } 99 | 100 | impl AddressTyped for IpAddr { 101 | fn address_type(&self) -> AddressType { 102 | match self { 103 | IpAddr::V4(_) => AddressType::IpV4, 104 | IpAddr::V6(_) => AddressType::IpV6, 105 | } 106 | } 107 | } 108 | 109 | #[derive(Clone, Debug)] 110 | #[cfg_attr(feature = "serialize", derive(Serialize))] 111 | pub enum ExplicitlyTypedAddress { 112 | Fqdn { 113 | address_type: AddressType, 114 | domain: String, 115 | }, 116 | Ip(IpAddr), 117 | } 118 | 119 | impl fmt::Display for ExplicitlyTypedAddress { 120 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 121 | write!(f, "IN {} ", self.address_type())?; 122 | match self { 123 | ExplicitlyTypedAddress::Fqdn { domain, .. } => domain.fmt(f), 124 | ExplicitlyTypedAddress::Ip(ip) => ip.fmt(f), 125 | } 126 | } 127 | } 128 | 129 | impl AddressTyped for ExplicitlyTypedAddress { 130 | fn address_type(&self) -> AddressType { 131 | match self { 132 | ExplicitlyTypedAddress::Fqdn { address_type, .. } => *address_type, 133 | ExplicitlyTypedAddress::Ip(ip) => ip.address_type(), 134 | } 135 | } 136 | } 137 | 138 | impl From for ExplicitlyTypedAddress { 139 | fn from(item: IpAddr) -> Self { 140 | ExplicitlyTypedAddress::Ip(item) 141 | } 142 | } 143 | 144 | impl From for ExplicitlyTypedAddress { 145 | fn from(item: Ipv4Addr) -> Self { 146 | ExplicitlyTypedAddress::Ip(IpAddr::V4(item)) 147 | } 148 | } 149 | 150 | impl From for ExplicitlyTypedAddress { 151 | fn from(item: Ipv6Addr) -> Self { 152 | ExplicitlyTypedAddress::Ip(IpAddr::V6(item)) 153 | } 154 | } 155 | 156 | impl TryFrom<(AddressType, &str)> for ExplicitlyTypedAddress { 157 | type Error = SdpParserInternalError; 158 | fn try_from(item: (AddressType, &str)) -> Result { 159 | match Address::from_str(item.1)? { 160 | Address::Ip(ip) => { 161 | if ip.address_type() != item.0 { 162 | Err(SdpParserInternalError::AddressTypeMismatch { 163 | found: ip.address_type(), 164 | expected: item.0, 165 | }) 166 | } else { 167 | Ok(ExplicitlyTypedAddress::Ip(ip)) 168 | } 169 | } 170 | Address::Fqdn(domain) => Ok(ExplicitlyTypedAddress::Fqdn { 171 | address_type: item.0, 172 | domain, 173 | }), 174 | } 175 | } 176 | } 177 | 178 | impl PartialEq for ExplicitlyTypedAddress { 179 | fn eq(&self, other: &Self) -> bool { 180 | match (self, other) { 181 | ( 182 | ExplicitlyTypedAddress::Fqdn { 183 | address_type: a1, 184 | domain: d1, 185 | }, 186 | ExplicitlyTypedAddress::Fqdn { 187 | address_type: a2, 188 | domain: d2, 189 | }, 190 | ) => a1 == a2 && d1.to_lowercase() == d2.to_lowercase(), 191 | (ExplicitlyTypedAddress::Ip(a), ExplicitlyTypedAddress::Ip(b)) => a == b, 192 | (_, _) => false, 193 | } 194 | } 195 | } 196 | 197 | #[cfg(test)] 198 | #[path = "./address_tests.rs"] 199 | mod address_tests; 200 | -------------------------------------------------------------------------------- /src/address_tests.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use self::url::ParseError; 6 | use super::*; 7 | use std::error::Error; 8 | use std::net::{AddrParseError, Ipv4Addr, Ipv6Addr}; 9 | 10 | #[derive(Debug)] 11 | enum ParseTestError { 12 | Host(ParseError), 13 | Ip(AddrParseError), 14 | } 15 | impl fmt::Display for ParseTestError { 16 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 17 | match self { 18 | ParseTestError::Host(inner) => inner.fmt(f), 19 | ParseTestError::Ip(inner) => inner.fmt(f), 20 | } 21 | } 22 | } 23 | impl From for ParseTestError { 24 | fn from(err: ParseError) -> Self { 25 | ParseTestError::Host(err) 26 | } 27 | } 28 | impl From for ParseTestError { 29 | fn from(err: AddrParseError) -> Self { 30 | ParseTestError::Ip(err) 31 | } 32 | } 33 | impl Error for ParseTestError { 34 | fn source(&self) -> Option<&(dyn Error + 'static)> { 35 | // Generic error, underlying cause isn't tracked. 36 | match self { 37 | ParseTestError::Host(a) => Some(a), 38 | ParseTestError::Ip(a) => Some(a), 39 | } 40 | } 41 | } 42 | #[test] 43 | fn test_domain_name_parsing() -> Result<(), ParseTestError> { 44 | let address = Host::parse("this.is.a.fqdn")?; 45 | if let Host::Domain(domain) = address { 46 | assert_eq!(domain, "this.is.a.fqdn"); 47 | } else { 48 | panic!(); 49 | } 50 | Ok(()) 51 | } 52 | 53 | #[test] 54 | fn test_ipv4_address_parsing() -> Result<(), ParseTestError> { 55 | let address = Host::parse("1.0.0.1")?; 56 | if let Host::Ipv4(ip) = address { 57 | assert_eq!(ip, "1.0.0.1".parse::()?); 58 | } else { 59 | panic!(); 60 | } 61 | Ok(()) 62 | } 63 | 64 | #[test] 65 | fn test_ipv6_address_parsing() -> Result<(), ParseTestError> { 66 | let address = Host::parse("[::1]")?; 67 | if let Host::Ipv6(ip) = address { 68 | assert_eq!(ip, "::1".parse::()?); 69 | } else { 70 | panic!(); 71 | } 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /src/anonymizer.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | extern crate url; 6 | use address::{Address, ExplicitlyTypedAddress}; 7 | use std::collections::HashMap; 8 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; 9 | use std::num::Wrapping; 10 | 11 | pub trait AnonymizingClone { 12 | fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self; 13 | } 14 | 15 | pub trait ToBytesVec { 16 | fn to_byte_vec(&self) -> Vec; 17 | } 18 | 19 | impl ToBytesVec for u64 { 20 | fn to_byte_vec(&self) -> Vec { 21 | let mut bytes = Vec::new(); 22 | let mut val = *self; 23 | for _ in 0..8 { 24 | bytes.push(val as u8); 25 | val <<= 8; 26 | } 27 | bytes.reverse(); 28 | bytes 29 | } 30 | } 31 | 32 | /* 33 | * Anonymizes SDP in a stateful fashion, such that a pre-anonymized value will 34 | * always be transformed into the same anonymized value within the context of 35 | * the anonymizer. 36 | * Stores the opaque state necessary for intelligent anonymization of SDP. This 37 | * state can be stored and reused during the offer-answer period, and it 38 | * will maintain a stable set of masked values. 39 | */ 40 | pub struct StatefulSdpAnonymizer { 41 | ips: HashMap, 42 | ip_v4_inc: Wrapping, 43 | ip_v6_inc: Wrapping, 44 | host_names: AnonymizationStrMap, 45 | ports: HashMap, 46 | port_inc: Wrapping, 47 | origin_users: AnonymizationStrMap, 48 | ice_passwords: AnonymizationStrMap, 49 | ice_users: AnonymizationStrMap, 50 | cert_finger_prints: HashMap, Vec>, 51 | cert_finger_print_inc: Wrapping, 52 | cnames: AnonymizationStrMap, 53 | } 54 | 55 | impl Default for StatefulSdpAnonymizer { 56 | fn default() -> Self { 57 | Self::new() 58 | } 59 | } 60 | 61 | impl StatefulSdpAnonymizer { 62 | pub fn new() -> Self { 63 | StatefulSdpAnonymizer { 64 | ips: HashMap::new(), 65 | ip_v4_inc: Wrapping(0), 66 | ip_v6_inc: Wrapping(0), 67 | host_names: AnonymizationStrMap::new("fqdn-", 8), 68 | ports: HashMap::new(), 69 | port_inc: Wrapping(0), 70 | origin_users: AnonymizationStrMap::new("origin-user-", 8), 71 | ice_passwords: AnonymizationStrMap::new("ice-password-", 8), 72 | ice_users: AnonymizationStrMap::new("ice-user-", 8), 73 | cert_finger_prints: HashMap::new(), 74 | cert_finger_print_inc: Wrapping(0), 75 | cnames: AnonymizationStrMap::new("cname-", 8), 76 | } 77 | } 78 | 79 | pub fn mask_host(&mut self, host: &str) -> String { 80 | self.host_names.mask(host) 81 | } 82 | 83 | pub fn mask_ip(&mut self, addr: &IpAddr) -> IpAddr { 84 | if let Some(address) = self.ips.get(addr) { 85 | return *address; 86 | } 87 | let mapped = match addr { 88 | IpAddr::V4(_) => { 89 | self.ip_v4_inc += Wrapping(1); 90 | IpAddr::V4(Ipv4Addr::from(self.ip_v4_inc.0)) 91 | } 92 | IpAddr::V6(_) => { 93 | self.ip_v6_inc += Wrapping(1); 94 | IpAddr::V6(Ipv6Addr::from(self.ip_v6_inc.0)) 95 | } 96 | }; 97 | self.ips.insert(*addr, mapped); 98 | mapped 99 | } 100 | 101 | pub fn mask_address(&mut self, address: &Address) -> Address { 102 | match address { 103 | Address::Fqdn(host) => Address::Fqdn(self.mask_host(host)), 104 | Address::Ip(ip) => Address::Ip(self.mask_ip(ip)), 105 | } 106 | } 107 | 108 | pub fn mask_typed_address( 109 | &mut self, 110 | address: &ExplicitlyTypedAddress, 111 | ) -> ExplicitlyTypedAddress { 112 | match address { 113 | ExplicitlyTypedAddress::Fqdn { 114 | address_type, 115 | domain, 116 | } => ExplicitlyTypedAddress::Fqdn { 117 | address_type: *address_type, 118 | domain: self.mask_host(domain), 119 | }, 120 | ExplicitlyTypedAddress::Ip(ip) => ExplicitlyTypedAddress::Ip(self.mask_ip(ip)), 121 | } 122 | } 123 | 124 | pub fn mask_port(&mut self, port: u32) -> u32 { 125 | if let Some(stored) = self.ports.get(&port) { 126 | return *stored; 127 | } 128 | self.port_inc += Wrapping(1); 129 | self.ports.insert(port, self.port_inc.0); 130 | self.port_inc.0 131 | } 132 | 133 | pub fn mask_origin_user(&mut self, user: &str) -> String { 134 | self.origin_users.mask(user) 135 | } 136 | 137 | pub fn mask_ice_password(&mut self, password: &str) -> String { 138 | self.ice_passwords.mask(password) 139 | } 140 | 141 | pub fn mask_ice_user(&mut self, user: &str) -> String { 142 | self.ice_users.mask(user) 143 | } 144 | 145 | pub fn mask_cert_finger_print(&mut self, finger_print: &[u8]) -> Vec { 146 | if let Some(stored) = self.cert_finger_prints.get(finger_print) { 147 | return stored.clone(); 148 | } 149 | self.cert_finger_print_inc += Wrapping(1); 150 | self.cert_finger_prints.insert( 151 | finger_print.to_vec(), 152 | self.cert_finger_print_inc.0.to_byte_vec(), 153 | ); 154 | self.cert_finger_print_inc.0.to_byte_vec() 155 | } 156 | 157 | pub fn mask_cname(&mut self, cname: &str) -> String { 158 | self.cnames.mask(cname) 159 | } 160 | } 161 | 162 | struct AnonymizationStrMap { 163 | map: HashMap, 164 | counter: Wrapping, 165 | prefix: &'static str, 166 | padding: usize, 167 | } 168 | 169 | impl AnonymizationStrMap { 170 | pub fn new(prefix: &'static str, padding: usize) -> Self { 171 | Self { 172 | map: HashMap::new(), 173 | counter: Wrapping(0), 174 | prefix, 175 | padding, 176 | } 177 | } 178 | 179 | pub fn mask(&mut self, value: &str) -> String { 180 | let key = value.to_owned(); 181 | if let Some(stored) = self.map.get(&key) { 182 | return stored.clone(); 183 | } 184 | self.counter += Wrapping(1); 185 | let store = format!( 186 | "{}{:0padding$}", 187 | self.prefix, 188 | self.counter.0, 189 | padding = self.padding 190 | ); 191 | self.map.insert(key, store.clone()); 192 | store 193 | } 194 | } 195 | 196 | #[cfg(test)] 197 | #[path = "./anonymizer_tests.rs"] 198 | mod tests; 199 | -------------------------------------------------------------------------------- /src/anonymizer_tests.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::*; 6 | 7 | #[test] 8 | fn test_mask_ip() { 9 | let mut anon = StatefulSdpAnonymizer::default(); 10 | let v4 = [ 11 | Ipv4Addr::new(127, 0, 0, 1), 12 | Ipv4Addr::new(10, 0, 0, 1), 13 | Ipv4Addr::new(1, 1, 1, 1), 14 | ]; 15 | let v4_masked = [ 16 | Ipv4Addr::new(0, 0, 0, 1), 17 | Ipv4Addr::new(0, 0, 0, 2), 18 | Ipv4Addr::new(0, 0, 0, 3), 19 | ]; 20 | let v6 = [ 21 | Ipv6Addr::from(0), 22 | Ipv6Addr::from(528_189_235), 23 | Ipv6Addr::from(1_623_734_988_148_990), 24 | ]; 25 | let v6_masked = [Ipv6Addr::from(1), Ipv6Addr::from(2), Ipv6Addr::from(3)]; 26 | for _ in 0..2 { 27 | assert_eq!(anon.mask_ip(&IpAddr::V4(v4[0])), v4_masked[0]); 28 | assert_eq!(anon.mask_ip(&IpAddr::V6(v6[0])), v6_masked[0]); 29 | 30 | assert_eq!(anon.mask_ip(&IpAddr::V4(v4[1])), v4_masked[1]); 31 | assert_eq!(anon.mask_ip(&IpAddr::V6(v6[1])), v6_masked[1]); 32 | 33 | assert_eq!(anon.mask_ip(&IpAddr::V4(v4[2])), v4_masked[2]); 34 | assert_eq!(anon.mask_ip(&IpAddr::V6(v6[2])), v6_masked[2]); 35 | } 36 | } 37 | 38 | #[test] 39 | fn test_mask_port() { 40 | let mut anon = StatefulSdpAnonymizer::default(); 41 | let ports = [0, 125, 12346]; 42 | let masked_ports = [1, 2, 3]; 43 | for _ in 0..2 { 44 | assert_eq!(anon.mask_port(ports[0]), masked_ports[0]); 45 | assert_eq!(anon.mask_port(ports[1]), masked_ports[1]); 46 | assert_eq!(anon.mask_port(ports[2]), masked_ports[2]); 47 | } 48 | } 49 | 50 | #[test] 51 | fn test_mask_ice_password() { 52 | let mut anon = StatefulSdpAnonymizer::default(); 53 | let passwords = ["vasdfioqwenl14082`14", "0", "ncp HY878hp(poh"]; 54 | let masked_passwords = [ 55 | "ice-password-00000001", 56 | "ice-password-00000002", 57 | "ice-password-00000003", 58 | ]; 59 | for _ in 0..2 { 60 | assert_eq!(anon.mask_ice_password(passwords[0]), masked_passwords[0]); 61 | assert_eq!(anon.mask_ice_password(passwords[1]), masked_passwords[1]); 62 | assert_eq!(anon.mask_ice_password(passwords[2]), masked_passwords[2]); 63 | } 64 | } 65 | 66 | #[test] 67 | fn test_mask_ice_user() { 68 | let mut anon = StatefulSdpAnonymizer::default(); 69 | let users = ["user1", "user2", "8109q2asdf"]; 70 | let masked_users = [ 71 | "ice-user-00000001", 72 | "ice-user-00000002", 73 | "ice-user-00000003", 74 | ]; 75 | for _ in 0..2 { 76 | assert_eq!(anon.mask_ice_user(users[0]), masked_users[0]); 77 | assert_eq!(anon.mask_ice_user(users[1]), masked_users[1]); 78 | assert_eq!(anon.mask_ice_user(users[2]), masked_users[2]); 79 | } 80 | } 81 | 82 | #[test] 83 | fn test_mask_cert_fingerprint() { 84 | let mut anon = StatefulSdpAnonymizer::default(); 85 | let prints: [Vec; 3] = [ 86 | vec![ 87 | 0x59u8, 0x4A, 0x8B, 0x73, 0xA7, 0x73, 0x53, 0x71, 0x88, 0xD7, 0x4D, 0x58, 0x28, 0x0C, 88 | 0x79, 0x72, 0x31, 0x29, 0x9B, 0x05, 0x37, 0xDD, 0x58, 0x43, 0xC2, 0xD4, 0x85, 0xA2, 89 | 0xB3, 0x66, 0x38, 0x7A, 90 | ], 91 | vec![ 92 | 0x30u8, 0xFF, 0x8E, 0x2B, 0xAC, 0x9D, 0xED, 0x70, 0x18, 0x10, 0x67, 0xC8, 0xAE, 0x9E, 93 | 0x68, 0xF3, 0x86, 0x53, 0x51, 0xB0, 0xAC, 0x31, 0xB7, 0xBE, 0x6D, 0xCF, 0xA4, 0x2E, 94 | 0xD3, 0x6E, 0xB4, 0x28, 95 | ], 96 | vec![ 97 | 0xDFu8, 0x2E, 0xAC, 0x8A, 0xFD, 0x0A, 0x8E, 0x99, 0xBF, 0x5D, 0xE8, 0x3C, 0xE7, 0xFA, 98 | 0xFB, 0x08, 0x3B, 0x3C, 0x54, 0x1D, 0xD7, 0xD4, 0x05, 0x77, 0xA0, 0x72, 0x9B, 0x14, 99 | 0x08, 0x6D, 0x0F, 0x4C, 100 | ], 101 | ]; 102 | 103 | let masked_prints = [1u64.to_byte_vec(), 2u64.to_byte_vec(), 3u64.to_byte_vec()]; 104 | for _ in 0..2 { 105 | assert_eq!(anon.mask_cert_finger_print(&prints[0]), masked_prints[0]); 106 | assert_eq!(anon.mask_cert_finger_print(&prints[1]), masked_prints[1]); 107 | assert_eq!(anon.mask_cert_finger_print(&prints[2]), masked_prints[2]); 108 | } 109 | } 110 | 111 | #[test] 112 | fn test_mask_cname() { 113 | let mut anon = StatefulSdpAnonymizer::default(); 114 | let cnames = ["mailto:foo@bar", "JohnDoe", "Jane Doe"]; 115 | let masked_cnames = ["cname-00000001", "cname-00000002", "cname-00000003"]; 116 | for _ in 0..2 { 117 | assert_eq!(anon.mask_cname(cnames[0]), masked_cnames[0]); 118 | assert_eq!(anon.mask_cname(cnames[1]), masked_cnames[1]); 119 | assert_eq!(anon.mask_cname(cnames[2]), masked_cnames[2]); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | #[cfg(feature = "serialize")] 6 | use serde::ser::{Serialize, SerializeStruct, Serializer}; 7 | use std::error; 8 | use std::error::Error; 9 | use std::fmt; 10 | extern crate url; 11 | use address::AddressType; 12 | use std::num::ParseFloatError; 13 | use std::num::ParseIntError; 14 | 15 | #[derive(Debug, Clone)] 16 | pub enum SdpParserInternalError { 17 | UnknownAddressType(String), 18 | AddressTypeMismatch { 19 | found: AddressType, 20 | expected: AddressType, 21 | }, 22 | Generic(String), 23 | Unsupported(String), 24 | Integer(ParseIntError), 25 | Float(ParseFloatError), 26 | Domain(url::ParseError), 27 | IpAddress(std::net::AddrParseError), 28 | } 29 | 30 | const INTERNAL_ERROR_MESSAGE_UNKNOWN_ADDRESS_TYPE: &str = "Unknown address type"; 31 | const INTERNAL_ERROR_MESSAGE_ADDRESS_TYPE_MISMATCH: &str = 32 | "Address is of a different type(1) than declared(2)"; 33 | 34 | impl fmt::Display for SdpParserInternalError { 35 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 36 | match *self { 37 | SdpParserInternalError::UnknownAddressType(ref unknown) => write!( 38 | f, 39 | "{INTERNAL_ERROR_MESSAGE_UNKNOWN_ADDRESS_TYPE}: {unknown}" 40 | ), 41 | SdpParserInternalError::AddressTypeMismatch { found, expected } => write!( 42 | f, 43 | "{INTERNAL_ERROR_MESSAGE_ADDRESS_TYPE_MISMATCH}: {found}, {expected}" 44 | ), 45 | SdpParserInternalError::Generic(ref message) => write!(f, "Parsing error: {message}"), 46 | SdpParserInternalError::Unsupported(ref message) => { 47 | write!(f, "Unsupported parsing error: {message}") 48 | } 49 | SdpParserInternalError::Integer(ref error) => { 50 | write!(f, "Integer parsing error: {error}") 51 | } 52 | SdpParserInternalError::Float(ref error) => write!(f, "Float parsing error: {error}"), 53 | SdpParserInternalError::Domain(ref error) => { 54 | write!(f, "Domain name parsing error: {error}") 55 | } 56 | SdpParserInternalError::IpAddress(ref error) => { 57 | write!(f, "IP address parsing error: {error}") 58 | } 59 | } 60 | } 61 | } 62 | 63 | impl Error for SdpParserInternalError { 64 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 65 | match *self { 66 | SdpParserInternalError::Integer(ref error) => Some(error), 67 | SdpParserInternalError::Float(ref error) => Some(error), 68 | SdpParserInternalError::Domain(ref error) => Some(error), 69 | SdpParserInternalError::IpAddress(ref error) => Some(error), 70 | // Can't tell much more about our internal errors 71 | _ => None, 72 | } 73 | } 74 | } 75 | 76 | #[derive(Debug, Clone)] 77 | pub enum SdpParserError { 78 | Line { 79 | error: SdpParserInternalError, 80 | line: String, 81 | line_number: usize, 82 | }, 83 | Unsupported { 84 | error: SdpParserInternalError, 85 | line: String, 86 | line_number: usize, 87 | }, 88 | Sequence { 89 | message: String, 90 | line_number: usize, 91 | }, 92 | } 93 | 94 | #[cfg(feature = "serialize")] 95 | impl Serialize for SdpParserError { 96 | fn serialize(&self, serializer: S) -> Result 97 | where 98 | S: Serializer, 99 | { 100 | let mut state = serializer.serialize_struct( 101 | "error", 102 | match self { 103 | SdpParserError::Sequence { .. } => 3, 104 | _ => 4, 105 | }, 106 | )?; 107 | match self { 108 | SdpParserError::Line { error, line, .. } => { 109 | state.serialize_field("type", "Line")?; 110 | state.serialize_field("message", &format!("{error}"))?; 111 | state.serialize_field("line", line)? 112 | } 113 | SdpParserError::Unsupported { error, line, .. } => { 114 | state.serialize_field("type", "Unsupported")?; 115 | state.serialize_field("message", &format!("{error}"))?; 116 | state.serialize_field("line", line)? 117 | } 118 | SdpParserError::Sequence { message, .. } => { 119 | state.serialize_field("type", "Sequence")?; 120 | state.serialize_field("message", message)?; 121 | } 122 | }; 123 | state.serialize_field( 124 | "line_number", 125 | &match *self { 126 | SdpParserError::Line { line_number, .. } => line_number, 127 | SdpParserError::Unsupported { line_number, .. } => line_number, 128 | SdpParserError::Sequence { line_number, .. } => line_number, 129 | }, 130 | )?; 131 | state.end() 132 | } 133 | } 134 | 135 | impl fmt::Display for SdpParserError { 136 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 137 | match self { 138 | SdpParserError::Line { 139 | error, 140 | line, 141 | line_number, 142 | } => write!(f, "Line error: {error} in line({line_number}): {line}"), 143 | SdpParserError::Unsupported { 144 | error, 145 | line, 146 | line_number, 147 | } => write!(f, "Unsupported: {error} in line({line_number}): {line}",), 148 | SdpParserError::Sequence { 149 | message, 150 | line_number, 151 | } => write!(f, "Sequence error in line({line_number}): {message}"), 152 | } 153 | } 154 | } 155 | 156 | impl Error for SdpParserError { 157 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 158 | match self { 159 | SdpParserError::Line { error, .. } | SdpParserError::Unsupported { error, .. } => { 160 | Some(error) 161 | } 162 | // Can't tell much more about our internal errors 163 | _ => None, 164 | } 165 | } 166 | } 167 | 168 | impl From for SdpParserInternalError { 169 | fn from(err: ParseIntError) -> SdpParserInternalError { 170 | SdpParserInternalError::Integer(err) 171 | } 172 | } 173 | 174 | impl From for SdpParserInternalError { 175 | fn from(err: url::ParseError) -> SdpParserInternalError { 176 | SdpParserInternalError::Domain(err) 177 | } 178 | } 179 | 180 | impl From for SdpParserInternalError { 181 | fn from(err: std::net::AddrParseError) -> SdpParserInternalError { 182 | SdpParserInternalError::IpAddress(err) 183 | } 184 | } 185 | 186 | impl From for SdpParserInternalError { 187 | fn from(err: ParseFloatError) -> SdpParserInternalError { 188 | SdpParserInternalError::Float(err) 189 | } 190 | } 191 | 192 | #[cfg(test)] 193 | #[path = "./error_tests.rs"] 194 | mod tests; 195 | -------------------------------------------------------------------------------- /src/error_tests.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::*; 6 | use address::Address; 7 | use std::str::FromStr; 8 | #[test] 9 | fn test_sdp_parser_internal_error_unknown_address_type() { 10 | let error = SdpParserInternalError::UnknownAddressType("foo".to_string()); 11 | assert_eq!( 12 | format!("{error}"), 13 | format!("{}: {}", INTERNAL_ERROR_MESSAGE_UNKNOWN_ADDRESS_TYPE, "foo") 14 | ); 15 | assert!(error.source().is_none()); 16 | } 17 | #[test] 18 | fn test_sdp_parser_internal_error_address_type_mismatch() { 19 | let error = SdpParserInternalError::AddressTypeMismatch { 20 | found: AddressType::IpV4, 21 | expected: AddressType::IpV6, 22 | }; 23 | assert_eq!( 24 | format!("{error}"), 25 | format!( 26 | "{}: {}, {}", 27 | INTERNAL_ERROR_MESSAGE_ADDRESS_TYPE_MISMATCH, 28 | AddressType::IpV4, 29 | AddressType::IpV6 30 | ) 31 | ); 32 | assert!(error.source().is_none()); 33 | } 34 | 35 | #[test] 36 | fn test_sdp_parser_internal_error_generic() { 37 | let generic = SdpParserInternalError::Generic("generic message".to_string()); 38 | assert_eq!(format!("{generic}"), "Parsing error: generic message"); 39 | assert!(generic.source().is_none()); 40 | } 41 | 42 | #[test] 43 | fn test_sdp_parser_internal_error_unsupported() { 44 | let unsupported = 45 | SdpParserInternalError::Unsupported("unsupported internal message".to_string()); 46 | assert_eq!( 47 | format!("{unsupported}"), 48 | "Unsupported parsing error: unsupported internal message" 49 | ); 50 | assert!(unsupported.source().is_none()); 51 | } 52 | 53 | #[test] 54 | fn test_sdp_parser_internal_error_integer() { 55 | let v = "12a"; 56 | let integer = v.parse::(); 57 | assert!(integer.is_err()); 58 | let int_err = SdpParserInternalError::Integer(integer.err().unwrap()); 59 | assert_eq!( 60 | format!("{int_err}"), 61 | "Integer parsing error: invalid digit found in string" 62 | ); 63 | assert!(int_err.source().is_some()); 64 | } 65 | 66 | #[test] 67 | fn test_sdp_parser_internal_error_float() { 68 | let v = "12.2a"; 69 | let float = v.parse::(); 70 | assert!(float.is_err()); 71 | let int_err = SdpParserInternalError::Float(float.err().unwrap()); 72 | assert_eq!( 73 | format!("{int_err}"), 74 | "Float parsing error: invalid float literal" 75 | ); 76 | assert!(int_err.source().is_some()); 77 | } 78 | 79 | #[test] 80 | fn test_sdp_parser_internal_error_address() { 81 | let v = "127.0.0.500"; 82 | let addr_err = Address::from_str(v).err().unwrap(); 83 | assert_eq!( 84 | format!("{addr_err}"), 85 | "Domain name parsing error: invalid IPv4 address" 86 | ); 87 | assert!(addr_err.source().is_some()); 88 | } 89 | 90 | #[test] 91 | fn test_sdp_parser_error_line() { 92 | let line1 = SdpParserError::Line { 93 | error: SdpParserInternalError::Generic("test message".to_string()), 94 | line: "test line".to_string(), 95 | line_number: 13, 96 | }; 97 | assert_eq!( 98 | format!("{line1}"), 99 | "Line error: Parsing error: test message in line(13): test line" 100 | ); 101 | assert!(line1.source().is_some()); 102 | } 103 | 104 | #[test] 105 | fn test_sdp_parser_error_unsupported() { 106 | let unsupported1 = SdpParserError::Unsupported { 107 | error: SdpParserInternalError::Generic("unsupported value".to_string()), 108 | line: "unsupported line".to_string(), 109 | line_number: 21, 110 | }; 111 | assert_eq!( 112 | format!("{unsupported1}"), 113 | "Unsupported: Parsing error: unsupported value in line(21): unsupported line" 114 | ); 115 | assert!(unsupported1.source().is_some()); 116 | } 117 | 118 | #[test] 119 | fn test_sdp_parser_error_sequence() { 120 | let sequence1 = SdpParserError::Sequence { 121 | message: "sequence message".to_string(), 122 | line_number: 42, 123 | }; 124 | assert_eq!( 125 | format!("{sequence1}"), 126 | "Sequence error in line(42): sequence message" 127 | ); 128 | assert!(sequence1.source().is_none()); 129 | } 130 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | #![warn(clippy::all)] 6 | #![forbid(unsafe_code)] 7 | 8 | #[macro_use] 9 | extern crate log; 10 | #[cfg(feature = "serialize")] 11 | #[macro_use] 12 | extern crate serde_derive; 13 | #[cfg(feature = "serialize")] 14 | extern crate serde; 15 | use std::convert::TryFrom; 16 | use std::fmt; 17 | 18 | #[macro_use] 19 | pub mod attribute_type; 20 | pub mod address; 21 | pub mod anonymizer; 22 | pub mod error; 23 | pub mod media_type; 24 | pub mod network; 25 | 26 | use address::{AddressTyped, ExplicitlyTypedAddress}; 27 | use anonymizer::{AnonymizingClone, StatefulSdpAnonymizer}; 28 | use attribute_type::{ 29 | parse_attribute, SdpAttribute, SdpAttributeRid, SdpAttributeSimulcastVersion, SdpAttributeType, 30 | SdpSingleDirection, 31 | }; 32 | use error::{SdpParserError, SdpParserInternalError}; 33 | use media_type::{ 34 | parse_media, parse_media_vector, SdpFormatList, SdpMedia, SdpMediaLine, SdpMediaValue, 35 | SdpProtocolValue, 36 | }; 37 | use network::{parse_address_type, parse_network_type}; 38 | 39 | /* 40 | * RFC4566 41 | * bandwidth-fields = *(%x62 "=" bwtype ":" bandwidth CRLF) 42 | */ 43 | #[derive(Clone)] 44 | #[cfg_attr(feature = "serialize", derive(Serialize))] 45 | #[cfg_attr(feature = "enhanced_debug", derive(Debug))] 46 | pub enum SdpBandwidth { 47 | As(u32), 48 | Ct(u32), 49 | Tias(u32), 50 | Unknown(String, u32), 51 | } 52 | 53 | impl fmt::Display for SdpBandwidth { 54 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 55 | let (tp_string, value) = match *self { 56 | SdpBandwidth::As(ref x) => ("AS", x), 57 | SdpBandwidth::Ct(ref x) => ("CT", x), 58 | SdpBandwidth::Tias(ref x) => ("TIAS", x), 59 | SdpBandwidth::Unknown(ref tp, ref x) => (&tp[..], x), 60 | }; 61 | write!(f, "{tp_string}:{value}") 62 | } 63 | } 64 | 65 | /* 66 | * RFC4566 67 | * connection-field = [%x63 "=" nettype SP addrtype SP 68 | * connection-address CRLF] 69 | */ 70 | #[derive(Clone)] 71 | #[cfg_attr(feature = "serialize", derive(Serialize))] 72 | #[cfg_attr(feature = "enhanced_debug", derive(Debug))] 73 | pub struct SdpConnection { 74 | pub address: ExplicitlyTypedAddress, 75 | pub ttl: Option, 76 | pub amount: Option, 77 | } 78 | 79 | impl fmt::Display for SdpConnection { 80 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 81 | self.address.fmt(f)?; 82 | write_option_string!(f, "/{}", self.ttl)?; 83 | write_option_string!(f, "/{}", self.amount) 84 | } 85 | } 86 | 87 | impl AnonymizingClone for SdpConnection { 88 | fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self { 89 | let mut masked = self.clone(); 90 | masked.address = anon.mask_typed_address(&self.address); 91 | masked 92 | } 93 | } 94 | 95 | /* 96 | * RFC4566 97 | * origin-field = %x6f "=" username SP sess-id SP sess-version SP 98 | * nettype SP addrtype SP unicast-address CRLF 99 | */ 100 | #[derive(Clone)] 101 | #[cfg_attr(feature = "serialize", derive(Serialize))] 102 | #[cfg_attr(feature = "enhanced_debug", derive(Debug))] 103 | pub struct SdpOrigin { 104 | pub username: String, 105 | pub session_id: u64, 106 | pub session_version: u64, 107 | pub unicast_addr: ExplicitlyTypedAddress, 108 | } 109 | 110 | impl fmt::Display for SdpOrigin { 111 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 112 | write!( 113 | f, 114 | "{username} {sess_id} {sess_vers} {unicast_addr}", 115 | username = self.username, 116 | sess_id = self.session_id, 117 | sess_vers = self.session_version, 118 | unicast_addr = self.unicast_addr 119 | ) 120 | } 121 | } 122 | 123 | impl AnonymizingClone for SdpOrigin { 124 | fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self { 125 | let mut masked = self.clone(); 126 | masked.username = anon.mask_origin_user(&self.username); 127 | masked.unicast_addr = anon.mask_typed_address(&masked.unicast_addr); 128 | masked 129 | } 130 | } 131 | 132 | /* 133 | * RFC4566 134 | * time-fields = 1*( %x74 "=" start-time SP stop-time 135 | * *(CRLF repeat-fields) CRLF) 136 | * [zone-adjustments CRLF] 137 | */ 138 | #[derive(Clone)] 139 | #[cfg_attr(feature = "serialize", derive(Serialize))] 140 | #[cfg_attr(feature = "enhanced_debug", derive(Debug))] 141 | pub struct SdpTiming { 142 | pub start: u64, 143 | pub stop: u64, 144 | } 145 | 146 | impl fmt::Display for SdpTiming { 147 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 148 | write!(f, "{start} {stop}", start = self.start, stop = self.stop) 149 | } 150 | } 151 | 152 | #[cfg_attr(feature = "serialize", derive(Serialize))] 153 | #[cfg_attr(feature = "enhanced_debug", derive(Debug))] 154 | pub enum SdpType { 155 | // Note: Email, Information, Key, Phone, Repeat, Uri and Zone are left out 156 | // on purposes as we don't want to support them. 157 | Attribute(SdpAttribute), 158 | Bandwidth(SdpBandwidth), 159 | Connection(SdpConnection), 160 | Media(SdpMediaLine), 161 | Origin(SdpOrigin), 162 | Session(String), 163 | Timing(SdpTiming), 164 | Version(u64), 165 | } 166 | 167 | #[cfg_attr(feature = "serialize", derive(Serialize))] 168 | #[cfg_attr(feature = "enhanced_debug", derive(Debug))] 169 | pub struct SdpLine { 170 | pub line_number: usize, 171 | pub sdp_type: SdpType, 172 | pub text: String, 173 | } 174 | 175 | /* 176 | * RFC4566 177 | * ; SDP Syntax 178 | * session-description = proto-version 179 | * origin-field 180 | * session-name-field 181 | * information-field 182 | * uri-field 183 | * email-fields 184 | * phone-fields 185 | * connection-field 186 | * bandwidth-fields 187 | * time-fields 188 | * key-field 189 | * attribute-fields 190 | * media-descriptions 191 | */ 192 | #[derive(Clone)] 193 | #[cfg_attr(feature = "serialize", derive(Serialize))] 194 | #[cfg_attr(feature = "enhanced_debug", derive(Debug))] 195 | pub struct SdpSession { 196 | pub version: u64, 197 | pub origin: SdpOrigin, 198 | pub session: Option, 199 | pub connection: Option, 200 | pub bandwidth: Vec, 201 | pub timing: Option, 202 | pub attribute: Vec, 203 | pub media: Vec, 204 | pub warnings: Vec, // unsupported values: 205 | // information: Option, 206 | // uri: Option, 207 | // email: Option, 208 | // phone: Option, 209 | // repeat: Option, 210 | // zone: Option, 211 | // key: Option 212 | } 213 | 214 | impl fmt::Display for SdpSession { 215 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 216 | write!( 217 | f, 218 | "v={version}\r\n\ 219 | o={origin}\r\n\ 220 | s={session}\r\n\ 221 | {timing}\ 222 | {bandwidth}\ 223 | {connection}\ 224 | {session_attributes}\ 225 | {media_sections}", 226 | version = self.version, 227 | origin = self.origin, 228 | session = self.get_session_text(), 229 | timing = option_to_string!("t={}\r\n", self.timing), 230 | bandwidth = maybe_vector_to_string!("b={}\r\n", self.bandwidth, "\r\nb="), 231 | connection = option_to_string!("c={}\r\n", self.connection), 232 | session_attributes = maybe_vector_to_string!("a={}\r\n", self.attribute, "\r\na="), 233 | media_sections = self.media.iter().map(|s| s.to_string()).collect::(), 234 | ) 235 | } 236 | } 237 | 238 | impl SdpSession { 239 | pub fn new(version: u64, origin: SdpOrigin, session: String) -> SdpSession { 240 | let session = match session.trim() { 241 | s if !s.is_empty() => Some(s.to_owned()), 242 | _ => None, 243 | }; 244 | SdpSession { 245 | version, 246 | origin, 247 | session, 248 | connection: None, 249 | bandwidth: Vec::new(), 250 | timing: None, 251 | attribute: Vec::new(), 252 | media: Vec::new(), 253 | warnings: Vec::new(), 254 | } 255 | } 256 | 257 | pub fn get_version(&self) -> u64 { 258 | self.version 259 | } 260 | 261 | pub fn get_origin(&self) -> &SdpOrigin { 262 | &self.origin 263 | } 264 | 265 | pub fn get_session(&self) -> &Option { 266 | &self.session 267 | } 268 | 269 | pub fn get_session_text(&self) -> &str { 270 | if let Some(text) = &self.session { 271 | text.as_str() 272 | } else { 273 | " " 274 | } 275 | } 276 | pub fn get_connection(&self) -> &Option { 277 | &self.connection 278 | } 279 | 280 | pub fn set_connection(&mut self, c: SdpConnection) { 281 | self.connection = Some(c) 282 | } 283 | 284 | pub fn add_bandwidth(&mut self, b: SdpBandwidth) { 285 | self.bandwidth.push(b) 286 | } 287 | 288 | pub fn set_timing(&mut self, t: SdpTiming) { 289 | self.timing = Some(t) 290 | } 291 | 292 | pub fn add_attribute(&mut self, a: SdpAttribute) -> Result<(), SdpParserInternalError> { 293 | if !a.allowed_at_session_level() { 294 | return Err(SdpParserInternalError::Generic(format!( 295 | "{a} not allowed at session level" 296 | ))); 297 | }; 298 | self.attribute.push(a); 299 | Ok(()) 300 | } 301 | 302 | pub fn extend_media(&mut self, v: Vec) { 303 | self.media.extend(v) 304 | } 305 | 306 | pub fn parse_session_vector(&mut self, lines: &mut Vec) -> Result<(), SdpParserError> { 307 | while !lines.is_empty() { 308 | let line = lines.remove(0); 309 | match line.sdp_type { 310 | SdpType::Attribute(a) => { 311 | let _line_number = line.line_number; 312 | self.add_attribute(a).map_err(|e: SdpParserInternalError| { 313 | SdpParserError::Sequence { 314 | message: format!("{e}"), 315 | line_number: _line_number, 316 | } 317 | })? 318 | } 319 | SdpType::Bandwidth(b) => self.add_bandwidth(b), 320 | SdpType::Timing(t) => self.set_timing(t), 321 | SdpType::Connection(c) => self.set_connection(c), 322 | 323 | SdpType::Origin(_) | SdpType::Session(_) | SdpType::Version(_) => { 324 | return Err(SdpParserError::Sequence { 325 | message: "version, origin or session at wrong level".to_string(), 326 | line_number: line.line_number, 327 | }); 328 | } 329 | SdpType::Media(_) => { 330 | return Err(SdpParserError::Sequence { 331 | message: "media line not allowed in session parser".to_string(), 332 | line_number: line.line_number, 333 | }); 334 | } 335 | } 336 | } 337 | Ok(()) 338 | } 339 | 340 | pub fn get_attribute(&self, t: SdpAttributeType) -> Option<&SdpAttribute> { 341 | self.attribute 342 | .iter() 343 | .find(|a| SdpAttributeType::from(*a) == t) 344 | } 345 | 346 | pub fn add_media( 347 | &mut self, 348 | media_type: SdpMediaValue, 349 | direction: SdpAttribute, 350 | port: u32, 351 | protocol: SdpProtocolValue, 352 | addr: ExplicitlyTypedAddress, 353 | ) -> Result<(), SdpParserInternalError> { 354 | let mut media = SdpMedia::new(SdpMediaLine { 355 | media: media_type, 356 | port, 357 | port_count: 1, 358 | proto: protocol, 359 | formats: SdpFormatList::Integers(Vec::new()), 360 | }); 361 | 362 | media.add_attribute(direction)?; 363 | 364 | media.set_connection(SdpConnection { 365 | address: addr, 366 | ttl: None, 367 | amount: None, 368 | }); 369 | 370 | self.media.push(media); 371 | 372 | Ok(()) 373 | } 374 | } 375 | 376 | impl AnonymizingClone for SdpSession { 377 | fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self { 378 | let mut masked: SdpSession = SdpSession { 379 | version: self.version, 380 | session: self.session.clone(), 381 | origin: self.origin.masked_clone(anon), 382 | connection: self.connection.clone(), 383 | timing: self.timing.clone(), 384 | bandwidth: self.bandwidth.clone(), 385 | attribute: Vec::new(), 386 | media: Vec::new(), 387 | warnings: Vec::new(), 388 | }; 389 | masked.origin = self.origin.masked_clone(anon); 390 | masked.connection = masked.connection.map(|con| con.masked_clone(anon)); 391 | for i in &self.attribute { 392 | masked.attribute.push(i.masked_clone(anon)); 393 | } 394 | masked 395 | } 396 | } 397 | 398 | /* removing this wrap would not allow us to call this from the match statement inside 399 | * parse_sdp_line() */ 400 | #[allow(clippy::unnecessary_wraps)] 401 | fn parse_session(value: &str) -> Result { 402 | trace!("session: {}", value); 403 | Ok(SdpType::Session(String::from(value))) 404 | } 405 | 406 | fn parse_version(value: &str) -> Result { 407 | let ver = value.parse::()?; 408 | if ver != 0 { 409 | return Err(SdpParserInternalError::Generic(format!( 410 | "version type contains unsupported value {ver}" 411 | ))); 412 | }; 413 | trace!("version: {}", ver); 414 | Ok(SdpType::Version(ver)) 415 | } 416 | 417 | fn parse_origin(value: &str) -> Result { 418 | let mut tokens = value.split_whitespace(); 419 | let username = match tokens.next() { 420 | None => { 421 | return Err(SdpParserInternalError::Generic( 422 | "Origin type is missing username token".to_string(), 423 | )); 424 | } 425 | Some(x) => x, 426 | }; 427 | let session_id = match tokens.next() { 428 | None => { 429 | return Err(SdpParserInternalError::Generic( 430 | "Origin type is missing session ID token".to_string(), 431 | )); 432 | } 433 | Some(x) => x.parse::()?, 434 | }; 435 | let session_version = match tokens.next() { 436 | None => { 437 | return Err(SdpParserInternalError::Generic( 438 | "Origin type is missing session version token".to_string(), 439 | )); 440 | } 441 | Some(x) => x.parse::()?, 442 | }; 443 | match tokens.next() { 444 | None => { 445 | return Err(SdpParserInternalError::Generic( 446 | "Origin type is missing network type token".to_string(), 447 | )); 448 | } 449 | Some(x) => parse_network_type(x)?, 450 | }; 451 | let addrtype = match tokens.next() { 452 | None => { 453 | return Err(SdpParserInternalError::Generic( 454 | "Origin type is missing address type token".to_string(), 455 | )); 456 | } 457 | Some(x) => parse_address_type(x)?, 458 | }; 459 | let unicast_addr = match tokens.next() { 460 | None => { 461 | return Err(SdpParserInternalError::Generic( 462 | "Origin type is missing IP address token".to_string(), 463 | )); 464 | } 465 | Some(x) => ExplicitlyTypedAddress::try_from((addrtype, x))?, 466 | }; 467 | if addrtype != unicast_addr.address_type() { 468 | return Err(SdpParserInternalError::Generic( 469 | "Origin addrtype does not match address.".to_string(), 470 | )); 471 | } 472 | let o = SdpOrigin { 473 | username: String::from(username), 474 | session_id, 475 | session_version, 476 | unicast_addr, 477 | }; 478 | trace!("origin: {}", o); 479 | Ok(SdpType::Origin(o)) 480 | } 481 | 482 | fn parse_connection(value: &str) -> Result { 483 | let cv: Vec<&str> = value.split_whitespace().collect(); 484 | if cv.len() != 3 { 485 | return Err(SdpParserInternalError::Generic( 486 | "connection attribute must have three tokens".to_string(), 487 | )); 488 | } 489 | parse_network_type(cv[0])?; 490 | let addrtype = parse_address_type(cv[1])?; 491 | let mut ttl = None; 492 | let mut amount = None; 493 | let mut addr_token = cv[2]; 494 | if addr_token.find('/').is_some() { 495 | let addr_tokens: Vec<&str> = addr_token.split('/').collect(); 496 | if addr_tokens.len() >= 3 { 497 | amount = Some(addr_tokens[2].parse::()?); 498 | } 499 | ttl = Some(addr_tokens[1].parse::()?); 500 | addr_token = addr_tokens[0]; 501 | } 502 | let address = ExplicitlyTypedAddress::try_from((addrtype, addr_token))?; 503 | let c = SdpConnection { 504 | address, 505 | ttl, 506 | amount, 507 | }; 508 | trace!("connection: {}", c); 509 | Ok(SdpType::Connection(c)) 510 | } 511 | 512 | fn parse_bandwidth(value: &str) -> Result { 513 | let bv: Vec<&str> = value.split(':').collect(); 514 | if bv.len() != 2 { 515 | return Err(SdpParserInternalError::Generic( 516 | "bandwidth attribute must have two tokens".to_string(), 517 | )); 518 | } 519 | let bandwidth = bv[1].parse::()?; 520 | let bw = match bv[0].to_uppercase().as_ref() { 521 | "AS" => SdpBandwidth::As(bandwidth), 522 | "CT" => SdpBandwidth::Ct(bandwidth), 523 | "TIAS" => SdpBandwidth::Tias(bandwidth), 524 | _ => SdpBandwidth::Unknown(String::from(bv[0]), bandwidth), 525 | }; 526 | trace!("bandwidth: {}", bw); 527 | Ok(SdpType::Bandwidth(bw)) 528 | } 529 | 530 | fn parse_timing(value: &str) -> Result { 531 | let tv: Vec<&str> = value.split_whitespace().collect(); 532 | if tv.len() != 2 { 533 | return Err(SdpParserInternalError::Generic( 534 | "timing attribute must have two tokens".to_string(), 535 | )); 536 | } 537 | let start = tv[0].parse::()?; 538 | let stop = tv[1].parse::()?; 539 | let t = SdpTiming { start, stop }; 540 | trace!("timing: {}", t); 541 | Ok(SdpType::Timing(t)) 542 | } 543 | 544 | pub fn parse_sdp_line(line: &str, line_number: usize) -> Result { 545 | if line.find('=').is_none() { 546 | return Err(SdpParserError::Line { 547 | error: SdpParserInternalError::Generic("missing = character in line".to_string()), 548 | line: line.to_string(), 549 | line_number, 550 | }); 551 | } 552 | let mut splitted_line = line.splitn(2, '='); 553 | let line_type = match splitted_line.next() { 554 | None => { 555 | return Err(SdpParserError::Line { 556 | error: SdpParserInternalError::Generic("missing type".to_string()), 557 | line: line.to_string(), 558 | line_number, 559 | }); 560 | } 561 | Some(t) => { 562 | let trimmed = t.trim(); 563 | if trimmed.len() > 1 { 564 | return Err(SdpParserError::Line { 565 | error: SdpParserInternalError::Generic("type too long".to_string()), 566 | line: line.to_string(), 567 | line_number, 568 | }); 569 | } 570 | if trimmed.is_empty() { 571 | return Err(SdpParserError::Line { 572 | error: SdpParserInternalError::Generic("type is empty".to_string()), 573 | line: line.to_string(), 574 | line_number, 575 | }); 576 | } 577 | trimmed.to_lowercase() 578 | } 579 | }; 580 | let (line_value, untrimmed_line_value) = match splitted_line.next() { 581 | None => { 582 | return Err(SdpParserError::Line { 583 | error: SdpParserInternalError::Generic("missing value".to_string()), 584 | line: line.to_string(), 585 | line_number, 586 | }); 587 | } 588 | Some(v) => { 589 | let trimmed = v.trim(); 590 | // For compatibility with sites that don't adhere to "s=-" for no session ID 591 | if trimmed.is_empty() && line_type.as_str() != "s" { 592 | return Err(SdpParserError::Line { 593 | error: SdpParserInternalError::Generic("value is empty".to_string()), 594 | line: line.to_string(), 595 | line_number, 596 | }); 597 | } 598 | (trimmed, v) 599 | } 600 | }; 601 | match line_type.as_ref() { 602 | "a" => parse_attribute(line_value), 603 | "b" => parse_bandwidth(line_value), 604 | "c" => parse_connection(line_value), 605 | "e" => Err(SdpParserInternalError::Generic(format!( 606 | "unsupported type email: {line_value}" 607 | ))), 608 | "i" => Err(SdpParserInternalError::Generic(format!( 609 | "unsupported type information: {line_value}" 610 | ))), 611 | "k" => Err(SdpParserInternalError::Generic(format!( 612 | "unsupported insecure key exchange: {line_value}" 613 | ))), 614 | "m" => parse_media(line_value), 615 | "o" => parse_origin(line_value), 616 | "p" => Err(SdpParserInternalError::Generic(format!( 617 | "unsupported type phone: {line_value}" 618 | ))), 619 | "r" => Err(SdpParserInternalError::Generic(format!( 620 | "unsupported type repeat: {line_value}" 621 | ))), 622 | "s" => parse_session(untrimmed_line_value), 623 | "t" => parse_timing(line_value), 624 | "u" => Err(SdpParserInternalError::Generic(format!( 625 | "unsupported type uri: {line_value}" 626 | ))), 627 | "v" => parse_version(line_value), 628 | "z" => Err(SdpParserInternalError::Generic(format!( 629 | "unsupported type zone: {line_value}" 630 | ))), 631 | _ => Err(SdpParserInternalError::Generic( 632 | "unknown sdp type".to_string(), 633 | )), 634 | } 635 | .map(|sdp_type| SdpLine { 636 | line_number, 637 | sdp_type, 638 | text: line.to_owned(), 639 | }) 640 | .map_err(|e| match e { 641 | SdpParserInternalError::UnknownAddressType(..) 642 | | SdpParserInternalError::AddressTypeMismatch { .. } 643 | | SdpParserInternalError::Generic(..) 644 | | SdpParserInternalError::Integer(..) 645 | | SdpParserInternalError::Float(..) 646 | | SdpParserInternalError::Domain(..) 647 | | SdpParserInternalError::IpAddress(..) => SdpParserError::Line { 648 | error: e, 649 | line: line.to_string(), 650 | line_number, 651 | }, 652 | SdpParserInternalError::Unsupported(..) => SdpParserError::Unsupported { 653 | error: e, 654 | line: line.to_string(), 655 | line_number, 656 | }, 657 | }) 658 | } 659 | 660 | fn sanity_check_sdp_session(session: &SdpSession) -> Result<(), SdpParserError> { 661 | let make_seq_error = |x: &str| SdpParserError::Sequence { 662 | message: x.to_string(), 663 | line_number: 0, 664 | }; 665 | 666 | if session.timing.is_none() { 667 | return Err(make_seq_error("Missing timing type at session level")); 668 | } 669 | // Checks that all media have connections if there is no top level 670 | // This explicitly allows for zero connection lines if there are no media 671 | // sections for interoperability reasons. 672 | let media_cons = &session.media.iter().all(|m| m.get_connection().is_some()); 673 | if !media_cons && session.get_connection().is_none() { 674 | return Err(make_seq_error( 675 | "Without connection type at session level all media sections must have connection types", 676 | )); 677 | } 678 | 679 | // Check that extmaps are not defined on session and media level 680 | if session.get_attribute(SdpAttributeType::Extmap).is_some() { 681 | for msection in &session.media { 682 | if msection.get_attribute(SdpAttributeType::Extmap).is_some() { 683 | return Err(make_seq_error( 684 | "Extmap can't be define at session and media level", 685 | )); 686 | } 687 | } 688 | } 689 | 690 | for msection in &session.media { 691 | if msection 692 | .get_attribute(SdpAttributeType::RtcpMuxOnly) 693 | .is_some() 694 | && msection.get_attribute(SdpAttributeType::RtcpMux).is_none() 695 | { 696 | return Err(make_seq_error( 697 | "rtcp-mux-only media sections must also contain the rtcp-mux attribute", 698 | )); 699 | } 700 | 701 | let rids: Vec<&SdpAttributeRid> = msection 702 | .get_attributes() 703 | .iter() 704 | .filter_map(|attr| match *attr { 705 | SdpAttribute::Rid(ref rid) => Some(rid), 706 | _ => None, 707 | }) 708 | .collect(); 709 | let recv_rids: Vec<&str> = rids 710 | .iter() 711 | .filter_map(|rid| match rid.direction { 712 | SdpSingleDirection::Recv => Some(rid.id.as_str()), 713 | _ => None, 714 | }) 715 | .collect(); 716 | let send_rids: Vec<&str> = rids 717 | .iter() 718 | .filter_map(|rid| match rid.direction { 719 | SdpSingleDirection::Send => Some(rid.id.as_str()), 720 | _ => None, 721 | }) 722 | .collect(); 723 | 724 | for rid_format in rids.iter().flat_map(|rid| &rid.formats) { 725 | match *msection.get_formats() { 726 | SdpFormatList::Integers(ref int_fmt) => { 727 | if !int_fmt.contains(&(u32::from(*rid_format))) { 728 | return Err(make_seq_error( 729 | "Rid pts must be declared in the media section", 730 | )); 731 | } 732 | } 733 | SdpFormatList::Strings(ref str_fmt) => { 734 | if !str_fmt.contains(&rid_format.to_string()) { 735 | return Err(make_seq_error( 736 | "Rid pts must be declared in the media section", 737 | )); 738 | } 739 | } 740 | } 741 | } 742 | 743 | if let Some(SdpAttribute::Simulcast(simulcast)) = 744 | msection.get_attribute(SdpAttributeType::Simulcast) 745 | { 746 | let check_defined_rids = 747 | |simulcast_version_list: &Vec, 748 | rid_ids: &[&str]| 749 | -> Result<(), SdpParserError> { 750 | for simulcast_rid in simulcast_version_list.iter().flat_map(|x| &x.ids) { 751 | if !rid_ids.contains(&simulcast_rid.id.as_str()) { 752 | return Err(make_seq_error( 753 | "Simulcast RIDs must be defined in any rid attribute", 754 | )); 755 | } 756 | } 757 | Ok(()) 758 | }; 759 | 760 | check_defined_rids(&simulcast.receive, &recv_rids)?; 761 | check_defined_rids(&simulcast.send, &send_rids)?; 762 | } 763 | } 764 | 765 | Ok(()) 766 | } 767 | 768 | fn parse_sdp_vector(lines: &mut Vec) -> Result { 769 | if lines.len() < 4 { 770 | return Err(SdpParserError::Sequence { 771 | message: "SDP neeeds at least 4 lines".to_string(), 772 | line_number: 0, 773 | }); 774 | } 775 | 776 | let version = match lines.remove(0).sdp_type { 777 | SdpType::Version(v) => v, 778 | _ => { 779 | return Err(SdpParserError::Sequence { 780 | message: "first line needs to be version number".to_string(), 781 | line_number: 0, 782 | }); 783 | } 784 | }; 785 | let origin = match lines.remove(0).sdp_type { 786 | SdpType::Origin(v) => v, 787 | _ => { 788 | return Err(SdpParserError::Sequence { 789 | message: "second line needs to be origin".to_string(), 790 | line_number: 1, 791 | }); 792 | } 793 | }; 794 | let session = match lines.remove(0).sdp_type { 795 | SdpType::Session(v) => v, 796 | _ => { 797 | return Err(SdpParserError::Sequence { 798 | message: "third line needs to be session".to_string(), 799 | line_number: 2, 800 | }); 801 | } 802 | }; 803 | let mut sdp_session = SdpSession::new(version, origin, session); 804 | 805 | let _media_pos = lines 806 | .iter() 807 | .position(|l| matches!(l.sdp_type, SdpType::Media(_))); 808 | 809 | match _media_pos { 810 | Some(p) => { 811 | let mut media: Vec<_> = lines.drain(p..).collect(); 812 | sdp_session.parse_session_vector(lines)?; 813 | sdp_session.extend_media(parse_media_vector(&mut media)?); 814 | } 815 | None => sdp_session.parse_session_vector(lines)?, 816 | }; 817 | 818 | sanity_check_sdp_session(&sdp_session)?; 819 | Ok(sdp_session) 820 | } 821 | 822 | pub fn parse_sdp(sdp: &str, fail_on_warning: bool) -> Result { 823 | if sdp.is_empty() { 824 | return Err(SdpParserError::Line { 825 | error: SdpParserInternalError::Generic("empty SDP".to_string()), 826 | line: sdp.to_string(), 827 | line_number: 0, 828 | }); 829 | } 830 | // see test_parse_sdp_minimal_sdp_successfully 831 | if sdp.len() < 51 { 832 | return Err(SdpParserError::Line { 833 | error: SdpParserInternalError::Generic("string too short to be valid SDP".to_string()), 834 | line: sdp.to_string(), 835 | line_number: 0, 836 | }); 837 | } 838 | let lines = sdp.lines(); 839 | let mut errors: Vec = Vec::new(); 840 | let mut warnings: Vec = Vec::new(); 841 | let mut sdp_lines: Vec = Vec::new(); 842 | for (line_number, line) in lines.enumerate() { 843 | let stripped_line = line.trim(); 844 | if stripped_line.is_empty() { 845 | continue; 846 | } 847 | match parse_sdp_line(line, line_number) { 848 | Ok(n) => { 849 | sdp_lines.push(n); 850 | } 851 | Err(e) => { 852 | match e { 853 | // TODO is this really a good way to accomplish this? 854 | SdpParserError::Line { 855 | error, 856 | line, 857 | line_number, 858 | } => errors.push(SdpParserError::Line { 859 | error, 860 | line, 861 | line_number, 862 | }), 863 | SdpParserError::Unsupported { 864 | error, 865 | line, 866 | line_number, 867 | } => { 868 | warnings.push(SdpParserError::Unsupported { 869 | error, 870 | line, 871 | line_number, 872 | }); 873 | } 874 | SdpParserError::Sequence { 875 | message, 876 | line_number, 877 | } => errors.push(SdpParserError::Sequence { 878 | message, 879 | line_number, 880 | }), 881 | } 882 | } 883 | }; 884 | } 885 | 886 | if fail_on_warning && (!warnings.is_empty()) { 887 | return Err(warnings.remove(0)); 888 | } 889 | 890 | // We just return the last of the errors here 891 | if let Some(e) = errors.pop() { 892 | return Err(e); 893 | }; 894 | 895 | let mut session = parse_sdp_vector(&mut sdp_lines)?; 896 | session.warnings = warnings; 897 | 898 | for warning in &session.warnings { 899 | warn!("Warning: {}", &warning); 900 | } 901 | 902 | Ok(session) 903 | } 904 | 905 | #[cfg(test)] 906 | #[path = "./lib_tests.rs"] 907 | mod tests; 908 | -------------------------------------------------------------------------------- /src/lib_tests.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | extern crate url; 6 | use super::*; 7 | use address::{Address, AddressType}; 8 | use anonymizer::ToBytesVec; 9 | use std::net::IpAddr; 10 | use std::net::Ipv4Addr; 11 | 12 | fn create_dummy_sdp_session() -> SdpSession { 13 | let origin = parse_origin("mozilla 506705521068071134 0 IN IP4 0.0.0.0"); 14 | assert!(origin.is_ok()); 15 | let connection = parse_connection("IN IP4 198.51.100.7"); 16 | assert!(connection.is_ok()); 17 | let mut sdp_session; 18 | if let SdpType::Origin(o) = origin.unwrap() { 19 | sdp_session = SdpSession::new(0, o, "-".to_string()); 20 | 21 | if let Ok(SdpType::Connection(c)) = connection { 22 | sdp_session.connection = Some(c); 23 | } else { 24 | unreachable!(); 25 | } 26 | } else { 27 | unreachable!(); 28 | } 29 | sdp_session 30 | } 31 | 32 | pub fn create_dummy_media_section() -> SdpMedia { 33 | let media_line = SdpMediaLine { 34 | media: SdpMediaValue::Audio, 35 | port: 9, 36 | port_count: 0, 37 | proto: SdpProtocolValue::RtpSavpf, 38 | formats: SdpFormatList::Integers(Vec::new()), 39 | }; 40 | SdpMedia::new(media_line) 41 | } 42 | 43 | #[test] 44 | fn test_session_works() -> Result<(), SdpParserInternalError> { 45 | parse_session("topic")?; 46 | Ok(()) 47 | } 48 | 49 | #[test] 50 | fn test_version_works() -> Result<(), SdpParserInternalError> { 51 | parse_version("0")?; 52 | Ok(()) 53 | } 54 | 55 | #[test] 56 | fn test_version_unsupported_input() { 57 | assert!(parse_version("1").is_err()); 58 | assert!(parse_version("11").is_err()); 59 | assert!(parse_version("a").is_err()); 60 | } 61 | 62 | #[test] 63 | fn test_origin_works() -> Result<(), SdpParserInternalError> { 64 | parse_origin("mozilla 506705521068071134 0 IN IP4 0.0.0.0")?; 65 | parse_origin("mozilla 506705521068071134 0 IN IP6 2001:db8::1")?; 66 | Ok(()) 67 | } 68 | 69 | #[test] 70 | fn test_origin_missing_username() { 71 | assert!(parse_origin("").is_err()); 72 | } 73 | 74 | #[test] 75 | fn test_origin_missing_session_id() { 76 | assert!(parse_origin("mozilla ").is_err()); 77 | } 78 | 79 | #[test] 80 | fn test_origin_missing_session_version() { 81 | assert!(parse_origin("mozilla 506705521068071134 ").is_err()); 82 | } 83 | 84 | #[test] 85 | fn test_origin_missing_nettype() { 86 | assert!(parse_origin("mozilla 506705521068071134 0 ").is_err()); 87 | } 88 | 89 | #[test] 90 | fn test_origin_unsupported_nettype() { 91 | assert!(parse_origin("mozilla 506705521068071134 0 UNSUPPORTED IP4 0.0.0.0").is_err()); 92 | } 93 | 94 | #[test] 95 | fn test_origin_missing_addtype() { 96 | assert!(parse_origin("mozilla 506705521068071134 0 IN ").is_err()); 97 | } 98 | 99 | #[test] 100 | fn test_origin_missing_ip_addr() { 101 | assert!(parse_origin("mozilla 506705521068071134 0 IN IP4 ").is_err()); 102 | } 103 | 104 | #[test] 105 | fn test_origin_unsupported_addrtpe() { 106 | assert!(parse_origin("mozilla 506705521068071134 0 IN IP1 0.0.0.0").is_err()); 107 | } 108 | 109 | #[test] 110 | fn test_origin_invalid_ip_addr() { 111 | assert!(parse_origin("mozilla 506705521068071134 0 IN IP4 1.1.1.256").is_err()); 112 | assert!(parse_origin("mozilla 506705521068071134 0 IN IP6 ::g").is_err()); 113 | } 114 | 115 | #[test] 116 | fn test_origin_addr_type_mismatch() { 117 | assert!(parse_origin("mozilla 506705521068071134 0 IN IP4 ::1").is_err()); 118 | } 119 | 120 | #[test] 121 | fn connection_works() -> Result<(), SdpParserInternalError> { 122 | parse_connection("IN IP4 127.0.0.1")?; 123 | parse_connection("IN IP4 127.0.0.1/10/10")?; 124 | parse_connection("IN IP6 ::1")?; 125 | parse_connection("IN IP6 ::1/1/1")?; 126 | Ok(()) 127 | } 128 | 129 | #[test] 130 | fn connection_lots_of_whitespace() -> Result<(), SdpParserInternalError> { 131 | parse_connection("IN IP4 127.0.0.1")?; 132 | Ok(()) 133 | } 134 | 135 | #[test] 136 | fn connection_wrong_amount_of_tokens() { 137 | assert!(parse_connection("IN IP4").is_err()); 138 | assert!(parse_connection("IN IP4 0.0.0.0 foobar").is_err()); 139 | } 140 | 141 | #[test] 142 | fn connection_unsupported_nettype() { 143 | assert!(parse_connection("UNSUPPORTED IP4 0.0.0.0").is_err()); 144 | } 145 | 146 | #[test] 147 | fn connection_unsupported_addrtpe() { 148 | assert!(parse_connection("IN IP1 0.0.0.0").is_err()); 149 | } 150 | 151 | #[test] 152 | fn connection_broken_ip_addr() { 153 | assert!(parse_connection("IN IP4 1.1.1.256").is_err()); 154 | assert!(parse_connection("IN IP6 ::g").is_err()); 155 | } 156 | 157 | #[test] 158 | fn connection_addr_type_mismatch() { 159 | assert!(parse_connection("IN IP4 ::1").is_err()); 160 | } 161 | 162 | #[test] 163 | fn bandwidth_works() -> Result<(), SdpParserInternalError> { 164 | parse_bandwidth("AS:1")?; 165 | parse_bandwidth("CT:123")?; 166 | parse_bandwidth("TIAS:12345")?; 167 | Ok(()) 168 | } 169 | 170 | #[test] 171 | fn bandwidth_wrong_amount_of_tokens() { 172 | assert!(parse_bandwidth("TIAS").is_err()); 173 | assert!(parse_bandwidth("TIAS:12345:xyz").is_err()); 174 | } 175 | 176 | #[test] 177 | fn bandwidth_unsupported_type() -> Result<(), SdpParserInternalError> { 178 | parse_bandwidth("UNSUPPORTED:12345")?; 179 | Ok(()) 180 | } 181 | 182 | #[test] 183 | fn test_timing_works() -> Result<(), SdpParserInternalError> { 184 | parse_timing("0 0")?; 185 | Ok(()) 186 | } 187 | 188 | #[test] 189 | fn test_timing_non_numeric_tokens() { 190 | assert!(parse_timing("a 0").is_err()); 191 | assert!(parse_timing("0 a").is_err()); 192 | } 193 | 194 | #[test] 195 | fn test_timing_wrong_amount_of_tokens() { 196 | assert!(parse_timing("0").is_err()); 197 | assert!(parse_timing("0 0 0").is_err()); 198 | } 199 | 200 | #[test] 201 | fn test_parse_sdp_line_works() -> Result<(), SdpParserError> { 202 | parse_sdp_line("v=0", 0)?; 203 | parse_sdp_line("s=somesession", 0)?; 204 | Ok(()) 205 | } 206 | 207 | #[test] 208 | fn test_parse_sdp_line_empty_line() { 209 | assert!(parse_sdp_line("", 0).is_err()); 210 | } 211 | 212 | #[test] 213 | fn test_parse_sdp_line_unsupported_types() { 214 | assert!(parse_sdp_line("e=foobar", 0).is_err()); 215 | assert!(parse_sdp_line("i=foobar", 0).is_err()); 216 | assert!(parse_sdp_line("k=foobar", 0).is_err()); 217 | assert!(parse_sdp_line("p=foobar", 0).is_err()); 218 | assert!(parse_sdp_line("r=foobar", 0).is_err()); 219 | assert!(parse_sdp_line("u=foobar", 0).is_err()); 220 | assert!(parse_sdp_line("z=foobar", 0).is_err()); 221 | } 222 | 223 | #[test] 224 | fn test_parse_sdp_line_unknown_key() { 225 | assert!(parse_sdp_line("y=foobar", 0).is_err()); 226 | } 227 | 228 | #[test] 229 | fn test_parse_sdp_line_too_long_type() { 230 | assert!(parse_sdp_line("ab=foobar", 0).is_err()); 231 | } 232 | 233 | #[test] 234 | fn test_parse_sdp_line_without_equal() { 235 | assert!(parse_sdp_line("abcd", 0).is_err()); 236 | assert!(parse_sdp_line("ab cd", 0).is_err()); 237 | } 238 | 239 | #[test] 240 | fn test_parse_sdp_line_empty_value() { 241 | assert!(parse_sdp_line("v=", 0).is_err()); 242 | assert!(parse_sdp_line("o=", 0).is_err()); 243 | } 244 | 245 | #[test] 246 | fn test_parse_sdp_line_empty_name() { 247 | assert!(parse_sdp_line("=abc", 0).is_err()); 248 | } 249 | 250 | #[test] 251 | fn test_parse_sdp_line_valid_a_line() -> Result<(), SdpParserError> { 252 | parse_sdp_line("a=rtpmap:8 PCMA/8000", 0)?; 253 | Ok(()) 254 | } 255 | 256 | #[test] 257 | fn test_parse_sdp_line_invalid_a_line() { 258 | assert!(parse_sdp_line("a=rtpmap:200 PCMA/8000", 0).is_err()); 259 | } 260 | 261 | #[test] 262 | fn test_add_attribute() -> Result<(), SdpParserInternalError> { 263 | let mut sdp_session = create_dummy_sdp_session(); 264 | 265 | sdp_session.add_attribute(SdpAttribute::Sendrecv)?; 266 | assert!(sdp_session.add_attribute(SdpAttribute::BundleOnly).is_err()); 267 | assert_eq!(sdp_session.attribute.len(), 1); 268 | Ok(()) 269 | } 270 | 271 | #[test] 272 | fn test_sanity_check_sdp_session_timing() -> Result<(), SdpParserError> { 273 | let mut sdp_session = create_dummy_sdp_session(); 274 | sdp_session.extend_media(vec![create_dummy_media_section()]); 275 | 276 | assert!(sanity_check_sdp_session(&sdp_session).is_err()); 277 | 278 | let t = SdpTiming { start: 0, stop: 0 }; 279 | sdp_session.set_timing(t); 280 | 281 | sanity_check_sdp_session(&sdp_session)?; 282 | Ok(()) 283 | } 284 | 285 | #[test] 286 | fn test_sanity_check_sdp_session_media() -> Result<(), SdpParserError> { 287 | let mut sdp_session = create_dummy_sdp_session(); 288 | let t = SdpTiming { start: 0, stop: 0 }; 289 | sdp_session.set_timing(t); 290 | 291 | sanity_check_sdp_session(&sdp_session)?; 292 | 293 | sdp_session.extend_media(vec![create_dummy_media_section()]); 294 | 295 | sanity_check_sdp_session(&sdp_session)?; 296 | Ok(()) 297 | } 298 | 299 | #[test] 300 | fn test_sanity_check_sdp_connection() -> Result<(), SdpParserInternalError> { 301 | let origin = parse_origin("mozilla 506705521068071134 0 IN IP4 0.0.0.0")?; 302 | let mut sdp_session; 303 | if let SdpType::Origin(o) = origin { 304 | sdp_session = SdpSession::new(0, o, "-".to_string()); 305 | } else { 306 | unreachable!(); 307 | } 308 | let t = SdpTiming { start: 0, stop: 0 }; 309 | sdp_session.set_timing(t); 310 | 311 | assert!(sanity_check_sdp_session(&sdp_session).is_ok()); 312 | 313 | // the dummy media section doesn't contain a connection 314 | sdp_session.extend_media(vec![create_dummy_media_section()]); 315 | 316 | assert!(sanity_check_sdp_session(&sdp_session).is_err()); 317 | 318 | let connection = parse_connection("IN IP6 ::1")?; 319 | if let SdpType::Connection(c) = connection { 320 | sdp_session.connection = Some(c); 321 | } else { 322 | unreachable!(); 323 | } 324 | 325 | assert!(sanity_check_sdp_session(&sdp_session).is_ok()); 326 | 327 | let mut second_media = create_dummy_media_section(); 328 | let mconnection = parse_connection("IN IP4 0.0.0.0")?; 329 | if let SdpType::Connection(c) = mconnection { 330 | second_media.set_connection(c); 331 | } else { 332 | unreachable!(); 333 | } 334 | sdp_session.extend_media(vec![second_media]); 335 | assert!(sdp_session.media.len() == 2); 336 | 337 | assert!(sanity_check_sdp_session(&sdp_session).is_ok()); 338 | Ok(()) 339 | } 340 | 341 | #[test] 342 | fn test_sanity_check_sdp_session_extmap() -> Result<(), SdpParserInternalError> { 343 | let mut sdp_session = create_dummy_sdp_session(); 344 | let t = SdpTiming { start: 0, stop: 0 }; 345 | sdp_session.set_timing(t); 346 | sdp_session.extend_media(vec![create_dummy_media_section()]); 347 | 348 | let attribute = 349 | parse_attribute("extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time")?; 350 | if let SdpType::Attribute(a) = attribute { 351 | sdp_session.add_attribute(a)?; 352 | } else { 353 | unreachable!(); 354 | } 355 | assert!(sdp_session 356 | .get_attribute(SdpAttributeType::Extmap) 357 | .is_some()); 358 | 359 | assert!(sanity_check_sdp_session(&sdp_session).is_ok()); 360 | 361 | let mut second_media = create_dummy_media_section(); 362 | let mattribute = 363 | parse_attribute("extmap:1/sendonly urn:ietf:params:rtp-hdrext:ssrc-audio-level")?; 364 | if let SdpType::Attribute(ma) = mattribute { 365 | second_media.add_attribute(ma)?; 366 | } else { 367 | unreachable!(); 368 | } 369 | assert!(second_media 370 | .get_attribute(SdpAttributeType::Extmap) 371 | .is_some()); 372 | 373 | sdp_session.extend_media(vec![second_media]); 374 | assert!(sdp_session.media.len() == 2); 375 | 376 | assert!(sanity_check_sdp_session(&sdp_session).is_err()); 377 | 378 | sdp_session.attribute = Vec::new(); 379 | 380 | assert!(sanity_check_sdp_session(&sdp_session).is_ok()); 381 | Ok(()) 382 | } 383 | 384 | #[test] 385 | fn test_sanity_check_sdp_session_simulcast() -> Result<(), SdpParserError> { 386 | let mut sdp_session = create_dummy_sdp_session(); 387 | let t = SdpTiming { start: 0, stop: 0 }; 388 | sdp_session.set_timing(t); 389 | sdp_session.extend_media(vec![create_dummy_media_section()]); 390 | 391 | sanity_check_sdp_session(&sdp_session)?; 392 | Ok(()) 393 | } 394 | 395 | #[test] 396 | fn test_parse_sdp_zero_length_string_fails() { 397 | assert!(parse_sdp("", true).is_err()); 398 | } 399 | 400 | #[test] 401 | fn test_parse_sdp_to_short_string() { 402 | assert!(parse_sdp("fooooobarrrr", true).is_err()); 403 | } 404 | 405 | #[test] 406 | fn test_parse_sdp_minimal_sdp_successfully() -> Result<(), SdpParserError> { 407 | parse_sdp( 408 | "v=0\r\n 409 | o=- 0 0 IN IP6 ::1\r\n 410 | s=-\r\n 411 | c=IN IP6 ::1\r\n 412 | t=0 0\r\n", 413 | true, 414 | )?; 415 | Ok(()) 416 | } 417 | 418 | #[test] 419 | fn test_parse_sdp_too_short() { 420 | assert!(parse_sdp( 421 | "v=0\r\n 422 | o=- 0 0 IN IP4 0.0.0.0\r\n 423 | s=-\r\n", 424 | true 425 | ) 426 | .is_err()); 427 | } 428 | 429 | #[test] 430 | fn test_parse_sdp_line_error() { 431 | assert!(parse_sdp( 432 | "v=0\r\n 433 | o=- 0 0 IN IP4 0.0.0.0\r\n 434 | s=-\r\n 435 | t=0 foobar\r\n 436 | m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n", 437 | true 438 | ) 439 | .is_err()); 440 | } 441 | 442 | #[test] 443 | fn test_parse_sdp_unsupported_error() { 444 | assert!(parse_sdp( 445 | "v=0\r\n 446 | o=- 0 0 IN IP4 0.0.0.0\r\n 447 | s=-\r\n 448 | t=0 0\r\n 449 | m=foobar 0 UDP/TLS/RTP/SAVPF 0\r\n", 450 | true 451 | ) 452 | .is_err()); 453 | } 454 | 455 | #[test] 456 | fn test_parse_sdp_unsupported_warning() -> Result<(), SdpParserError> { 457 | parse_sdp( 458 | "v=0\r\n 459 | o=- 0 0 IN IP4 0.0.0.0\r\n 460 | s=-\r\n 461 | c=IN IP4 198.51.100.7\r\n 462 | t=0 0\r\n 463 | m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n 464 | a=unsupported\r\n", 465 | false, 466 | )?; 467 | Ok(()) 468 | } 469 | 470 | #[test] 471 | fn test_parse_sdp_sequence_error() { 472 | assert!(parse_sdp( 473 | "v=0\r\n 474 | o=- 0 0 IN IP4 0.0.0.0\r\n 475 | s=-\r\n 476 | t=0 0\r\n 477 | a=bundle-only\r\n 478 | m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n", 479 | true 480 | ) 481 | .is_err()); 482 | } 483 | 484 | #[test] 485 | fn test_parse_sdp_integer_error() { 486 | assert!(parse_sdp( 487 | "v=0\r\n 488 | o=- 0 0 IN IP4 0.0.0.0\r\n 489 | s=-\r\n 490 | t=0 0\r\n 491 | m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n 492 | a=rtcp:34er21\r\n", 493 | true 494 | ) 495 | .is_err()); 496 | } 497 | 498 | #[test] 499 | fn test_parse_sdp_ipaddr_error() { 500 | assert!(parse_sdp( 501 | "v=0\r\n 502 | o=- 0 0 IN IP4 0.a.b.0\r\n 503 | s=-\r\n 504 | t=0 0\r\n 505 | m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n", 506 | true 507 | ) 508 | .is_err()); 509 | } 510 | 511 | #[test] 512 | fn test_parse_sdp_invalid_session_attribute() { 513 | assert!(parse_sdp( 514 | "v=0\r\n 515 | o=- 0 0 IN IP4 0.a.b.0\r\n 516 | s=-\r\n 517 | t=0 0\r\n 518 | a=bundle-only\r\n 519 | m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n", 520 | true 521 | ) 522 | .is_err()); 523 | } 524 | 525 | #[test] 526 | fn test_parse_sdp_invalid_media_attribute() { 527 | assert!(parse_sdp( 528 | "v=0\r\n 529 | o=- 0 0 IN IP4 0.a.b.0\r\n 530 | s=-\r\n 531 | t=0 0\r\n 532 | m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n 533 | a=ice-lite\r\n", 534 | true 535 | ) 536 | .is_err()); 537 | } 538 | 539 | #[test] 540 | fn test_mask_origin() { 541 | let mut anon = StatefulSdpAnonymizer::new(); 542 | if let SdpType::Origin(origin_1) = 543 | parse_origin("mozilla 506705521068071134 0 IN IP4 0.0.0.0").unwrap() 544 | { 545 | for _ in 0..2 { 546 | let masked = origin_1.masked_clone(&mut anon); 547 | assert_eq!(masked.username, "origin-user-00000001"); 548 | assert_eq!( 549 | masked.unicast_addr, 550 | ExplicitlyTypedAddress::Ip(IpAddr::V4(Ipv4Addr::from(1))) 551 | ); 552 | } 553 | } else { 554 | unreachable!(); 555 | } 556 | } 557 | 558 | #[test] 559 | fn test_mask_sdp() { 560 | let mut anon = StatefulSdpAnonymizer::new(); 561 | let sdp = parse_sdp( 562 | "v=0\r\n 563 | o=ausername 4294967296 2 IN IP4 127.0.0.1\r\n 564 | s=SIP Call\r\n 565 | c=IN IP4 198.51.100.7/51\r\n 566 | a=ice-pwd:12340\r\n 567 | a=ice-ufrag:4a799b2e\r\n 568 | a=fingerprint:sha-1 CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC\r\n 569 | t=0 0\r\n 570 | m=video 56436 RTP/SAVPF 120\r\n 571 | a=candidate:77142221 1 udp 2113937151 192.168.137.1 54081 typ host\r\n 572 | a=remote-candidates:0 10.0.0.1 5555\r\n 573 | a=rtpmap:120 VP8/90000\r\n", 574 | true, 575 | ) 576 | .unwrap(); 577 | let mut masked = sdp.masked_clone(&mut anon); 578 | assert_eq!(masked.origin.username, "origin-user-00000001"); 579 | assert_eq!( 580 | masked.origin.unicast_addr, 581 | ExplicitlyTypedAddress::Ip(IpAddr::V4(Ipv4Addr::from(1))) 582 | ); 583 | assert_eq!( 584 | masked.connection.unwrap().address, 585 | ExplicitlyTypedAddress::Ip(IpAddr::V4(Ipv4Addr::from(2))) 586 | ); 587 | let mut attributes = masked.attribute; 588 | for m in &mut masked.media { 589 | for attribute in m.get_attributes() { 590 | attributes.push(attribute.clone()); 591 | } 592 | } 593 | for attribute in attributes { 594 | match attribute { 595 | SdpAttribute::Candidate(c) => { 596 | assert_eq!(c.address, Address::Ip(IpAddr::V4(Ipv4Addr::from(3)))); 597 | assert_eq!(c.port, 1); 598 | } 599 | SdpAttribute::Fingerprint(f) => { 600 | assert_eq!(f.fingerprint, 1u64.to_byte_vec()); 601 | } 602 | SdpAttribute::IcePwd(p) => { 603 | assert_eq!(p, "ice-password-00000001"); 604 | } 605 | SdpAttribute::IceUfrag(u) => { 606 | assert_eq!(u, "ice-user-00000001"); 607 | } 608 | SdpAttribute::RemoteCandidate(r) => { 609 | assert_eq!(r.address, Address::Ip(IpAddr::V4(Ipv4Addr::from(4)))); 610 | assert_eq!(r.port, 2); 611 | } 612 | _ => {} 613 | } 614 | } 615 | } 616 | 617 | #[test] 618 | fn test_parse_session_vector() -> Result<(), SdpParserError> { 619 | let mut sdp_session = create_dummy_sdp_session(); 620 | let mut lines: Vec = vec![parse_sdp_line("a=sendrecv", 1)?]; 621 | sdp_session.parse_session_vector(&mut lines)?; 622 | assert_eq!(sdp_session.attribute.len(), 1); 623 | Ok(()) 624 | } 625 | 626 | #[test] 627 | fn test_parse_session_vector_non_session_attribute() -> Result<(), SdpParserError> { 628 | let mut sdp_session = create_dummy_sdp_session(); 629 | let mut lines: Vec = vec![parse_sdp_line("a=bundle-only", 2)?]; 630 | assert!(sdp_session.parse_session_vector(&mut lines).is_err()); 631 | assert_eq!(sdp_session.attribute.len(), 0); 632 | Ok(()) 633 | } 634 | 635 | #[test] 636 | fn test_parse_session_vector_version_repeated() -> Result<(), SdpParserError> { 637 | let mut sdp_session = create_dummy_sdp_session(); 638 | let mut lines: Vec = vec![parse_sdp_line("v=0", 3)?]; 639 | assert!(sdp_session.parse_session_vector(&mut lines).is_err()); 640 | Ok(()) 641 | } 642 | 643 | #[test] 644 | fn test_parse_session_vector_contains_media_type() -> Result<(), SdpParserError> { 645 | let mut sdp_session = create_dummy_sdp_session(); 646 | let mut lines: Vec = vec![parse_sdp_line("m=audio 0 UDP/TLS/RTP/SAVPF 0", 4)?]; 647 | assert!(sdp_session.parse_session_vector(&mut lines).is_err()); 648 | Ok(()) 649 | } 650 | 651 | #[test] 652 | fn test_parse_sdp_vector_no_media_section() -> Result<(), SdpParserError> { 653 | let mut lines: Vec = vec![parse_sdp_line("v=0", 1)?]; 654 | lines.push(parse_sdp_line( 655 | "o=ausername 4294967296 2 IN IP4 127.0.0.1", 656 | 1, 657 | )?); 658 | lines.push(parse_sdp_line("s=SIP Call", 1)?); 659 | lines.push(parse_sdp_line("t=0 0", 1)?); 660 | lines.push(parse_sdp_line("c=IN IP6 ::1", 1)?); 661 | assert!(parse_sdp_vector(&mut lines).is_ok()); 662 | Ok(()) 663 | } 664 | 665 | #[test] 666 | fn test_parse_sdp_vector_with_media_section() -> Result<(), SdpParserError> { 667 | let mut lines: Vec = vec![parse_sdp_line("v=0", 1)?]; 668 | lines.push(parse_sdp_line( 669 | "o=ausername 4294967296 2 IN IP4 127.0.0.1", 670 | 1, 671 | )?); 672 | lines.push(parse_sdp_line("s=SIP Call", 1)?); 673 | lines.push(parse_sdp_line("t=0 0", 1)?); 674 | lines.push(parse_sdp_line("m=video 56436 RTP/SAVPF 120", 1)?); 675 | lines.push(parse_sdp_line("c=IN IP6 ::1", 1)?); 676 | assert!(parse_sdp_vector(&mut lines).is_ok()); 677 | Ok(()) 678 | } 679 | 680 | #[test] 681 | fn test_parse_sdp_vector_with_missing_rtcp_mux() -> Result<(), SdpParserError> { 682 | let mut lines: Vec = vec![parse_sdp_line("v=0", 1)?]; 683 | lines.push(parse_sdp_line( 684 | "o=ausername 4294967296 2 IN IP4 127.0.0.1", 685 | 1, 686 | )?); 687 | lines.push(parse_sdp_line("s=SIP Call", 1)?); 688 | lines.push(parse_sdp_line("t=0 0", 1)?); 689 | lines.push(parse_sdp_line("m=video 56436 RTP/SAVPF 120", 1)?); 690 | lines.push(parse_sdp_line("c=IN IP6 ::1", 1)?); 691 | lines.push(parse_sdp_line("a=rtcp-mux-only", 1)?); 692 | assert!(parse_sdp_vector(&mut lines).is_err()); 693 | Ok(()) 694 | } 695 | 696 | #[test] 697 | fn test_parse_sdp_vector_too_short() -> Result<(), SdpParserError> { 698 | let mut lines: Vec = vec![parse_sdp_line("v=0", 1)?]; 699 | assert!(parse_sdp_vector(&mut lines).is_err()); 700 | Ok(()) 701 | } 702 | 703 | #[test] 704 | fn test_parse_sdp_vector_missing_version() -> Result<(), SdpParserError> { 705 | let mut lines: Vec = vec![parse_sdp_line( 706 | "o=ausername 4294967296 2 IN IP4 127.0.0.1", 707 | 1, 708 | )?]; 709 | for _ in 0..3 { 710 | lines.push(parse_sdp_line("a=sendrecv", 1)?); 711 | } 712 | assert!(parse_sdp_vector(&mut lines).is_err()); 713 | Ok(()) 714 | } 715 | 716 | #[test] 717 | fn test_parse_sdp_vector_missing_origin() -> Result<(), SdpParserError> { 718 | let mut lines: Vec = vec![parse_sdp_line("v=0", 1)?]; 719 | for _ in 0..3 { 720 | lines.push(parse_sdp_line("a=sendrecv", 1)?); 721 | } 722 | assert!(parse_sdp_vector(&mut lines).is_err()); 723 | Ok(()) 724 | } 725 | 726 | #[test] 727 | fn test_parse_sdp_vector_missing_session() -> Result<(), SdpParserError> { 728 | let mut lines: Vec = vec![parse_sdp_line("v=0", 1)?]; 729 | lines.push(parse_sdp_line( 730 | "o=ausername 4294967296 2 IN IP4 127.0.0.1", 731 | 1, 732 | )?); 733 | for _ in 0..2 { 734 | lines.push(parse_sdp_line("a=sendrecv", 1)?); 735 | } 736 | assert!(parse_sdp_vector(&mut lines).is_err()); 737 | Ok(()) 738 | } 739 | 740 | #[test] 741 | fn test_session_add_media_works() { 742 | let mut sdp_session = create_dummy_sdp_session(); 743 | assert!(sdp_session 744 | .add_media( 745 | SdpMediaValue::Audio, 746 | SdpAttribute::Sendrecv, 747 | 99, 748 | SdpProtocolValue::RtpSavpf, 749 | ExplicitlyTypedAddress::from(Ipv4Addr::new(127, 0, 0, 1)) 750 | ) 751 | .is_ok()); 752 | assert!(sdp_session.get_connection().is_some()); 753 | assert_eq!(sdp_session.attribute.len(), 0); 754 | assert_eq!(sdp_session.media.len(), 1); 755 | assert_eq!(sdp_session.media[0].get_attributes().len(), 1); 756 | assert!(sdp_session.media[0] 757 | .get_attribute(SdpAttributeType::Sendrecv) 758 | .is_some()); 759 | } 760 | 761 | #[test] 762 | fn test_session_add_media_invalid_attribute_fails() -> Result<(), SdpParserInternalError> { 763 | let mut sdp_session = create_dummy_sdp_session(); 764 | assert!(sdp_session 765 | .add_media( 766 | SdpMediaValue::Audio, 767 | SdpAttribute::IceLite, 768 | 99, 769 | SdpProtocolValue::RtpSavpf, 770 | ExplicitlyTypedAddress::try_from((AddressType::IpV4, "127.0.0.1"))? 771 | ) 772 | .is_err()); 773 | Ok(()) 774 | } 775 | -------------------------------------------------------------------------------- /src/media_type.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use anonymizer::{AnonymizingClone, StatefulSdpAnonymizer}; 6 | use attribute_type::{ 7 | maybe_print_param, SdpAttribute, SdpAttributeRtpmap, SdpAttributeSctpmap, SdpAttributeType, 8 | }; 9 | use error::{SdpParserError, SdpParserInternalError}; 10 | use std::fmt; 11 | use {SdpBandwidth, SdpConnection, SdpLine, SdpType}; 12 | 13 | /* 14 | * RFC4566 15 | * media-field = %x6d "=" media SP port ["/" integer] 16 | * SP proto 1*(SP fmt) CRLF 17 | */ 18 | #[derive(Clone)] 19 | #[cfg_attr(feature = "serialize", derive(Serialize))] 20 | #[cfg_attr(feature = "enhanced_debug", derive(Debug))] 21 | pub struct SdpMediaLine { 22 | pub media: SdpMediaValue, 23 | pub port: u32, 24 | pub port_count: u32, 25 | pub proto: SdpProtocolValue, 26 | pub formats: SdpFormatList, 27 | } 28 | 29 | impl fmt::Display for SdpMediaLine { 30 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 31 | write!( 32 | f, 33 | "{media} {port}{pcount} {proto} {formats}", 34 | media = self.media, 35 | port = self.port, 36 | pcount = maybe_print_param("/", self.port_count, 0), 37 | proto = self.proto, 38 | formats = self.formats 39 | ) 40 | } 41 | } 42 | 43 | #[derive(Debug, PartialEq, Clone)] 44 | #[cfg_attr(feature = "serialize", derive(Serialize))] 45 | pub enum SdpMediaValue { 46 | Audio, 47 | Video, 48 | Application, 49 | } 50 | 51 | impl fmt::Display for SdpMediaValue { 52 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 53 | match *self { 54 | SdpMediaValue::Audio => "audio", 55 | SdpMediaValue::Video => "video", 56 | SdpMediaValue::Application => "application", 57 | } 58 | .fmt(f) 59 | } 60 | } 61 | 62 | #[derive(Debug, PartialEq, Clone)] 63 | #[cfg_attr(feature = "serialize", derive(Serialize))] 64 | pub enum SdpProtocolValue { 65 | RtpAvp, /* RTP/AVP [RFC4566] */ 66 | RtpAvpf, /* RTP/AVPF [RFC4585] */ 67 | RtpSavp, /* RTP/SAVP [RFC3711] */ 68 | RtpSavpf, /* RTP/SAVPF [RFC5124] */ 69 | TcpDtlsRtpSavp, /* TCP/DTLS/RTP/SAVP [RFC7850] */ 70 | TcpDtlsRtpSavpf, /* TCP/DTLS/RTP/SAVPF [RFC7850] */ 71 | UdpTlsRtpSavp, /* UDP/TLS/RTP/SAVP [RFC5764] */ 72 | UdpTlsRtpSavpf, /* UDP/TLS/RTP/SAVPF [RFC5764] */ 73 | DtlsSctp, /* DTLS/SCTP [draft-ietf-mmusic-sctp-sdp-07] */ 74 | UdpDtlsSctp, /* UDP/DTLS/SCTP [draft-ietf-mmusic-sctp-sdp-26] */ 75 | TcpDtlsSctp, /* TCP/DTLS/SCTP [draft-ietf-mmusic-sctp-sdp-26] */ 76 | } 77 | 78 | impl fmt::Display for SdpProtocolValue { 79 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 80 | match *self { 81 | SdpProtocolValue::RtpAvp => "RTP/AVP", 82 | SdpProtocolValue::RtpAvpf => "RTP/AVPF", 83 | SdpProtocolValue::RtpSavp => "RTP/SAVP", 84 | SdpProtocolValue::RtpSavpf => "RTP/SAVPF", 85 | SdpProtocolValue::TcpDtlsRtpSavp => "TCP/DTLS/RTP/SAVP", 86 | SdpProtocolValue::TcpDtlsRtpSavpf => "TCP/DTLS/RTP/SAVPF", 87 | SdpProtocolValue::UdpTlsRtpSavp => "UDP/TLS/RTP/SAVP", 88 | SdpProtocolValue::UdpTlsRtpSavpf => "UDP/TLS/RTP/SAVPF", 89 | SdpProtocolValue::DtlsSctp => "DTLS/SCTP", 90 | SdpProtocolValue::UdpDtlsSctp => "UDP/DTLS/SCTP", 91 | SdpProtocolValue::TcpDtlsSctp => "TCP/DTLS/SCTP", 92 | } 93 | .fmt(f) 94 | } 95 | } 96 | 97 | #[derive(Clone)] 98 | #[cfg_attr(feature = "serialize", derive(Serialize))] 99 | #[cfg_attr(feature = "enhanced_debug", derive(Debug))] 100 | pub enum SdpFormatList { 101 | Integers(Vec), 102 | Strings(Vec), 103 | } 104 | 105 | impl fmt::Display for SdpFormatList { 106 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 107 | match *self { 108 | SdpFormatList::Integers(ref x) => maybe_vector_to_string!("{}", x, " "), 109 | SdpFormatList::Strings(ref x) => x.join(" "), 110 | } 111 | .fmt(f) 112 | } 113 | } 114 | 115 | /* 116 | * RFC4566 117 | * media-descriptions = *( media-field 118 | * information-field 119 | * *connection-field 120 | * bandwidth-fields 121 | * key-field 122 | * attribute-fields ) 123 | */ 124 | #[derive(Clone)] 125 | #[cfg_attr(feature = "serialize", derive(Serialize))] 126 | #[cfg_attr(feature = "enhanced_debug", derive(Debug))] 127 | pub struct SdpMedia { 128 | media: SdpMediaLine, 129 | connection: Option, 130 | bandwidth: Vec, 131 | attribute: Vec, 132 | // unsupported values: 133 | // information: Option, 134 | // key: Option, 135 | } 136 | 137 | impl fmt::Display for SdpMedia { 138 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 139 | write!( 140 | f, 141 | "m={mline}\r\n{bw}{connection}{attributes}", 142 | mline = self.media, 143 | bw = maybe_vector_to_string!("b={}\r\n", self.bandwidth, "\r\nb="), 144 | connection = option_to_string!("c={}\r\n", self.connection), 145 | attributes = maybe_vector_to_string!("a={}\r\n", self.attribute, "\r\na=") 146 | ) 147 | } 148 | } 149 | 150 | impl SdpMedia { 151 | pub fn new(media: SdpMediaLine) -> SdpMedia { 152 | SdpMedia { 153 | media, 154 | connection: None, 155 | bandwidth: Vec::new(), 156 | attribute: Vec::new(), 157 | } 158 | } 159 | 160 | pub fn get_type(&self) -> &SdpMediaValue { 161 | &self.media.media 162 | } 163 | 164 | pub fn set_port(&mut self, port: u32) { 165 | self.media.port = port; 166 | } 167 | 168 | pub fn get_port(&self) -> u32 { 169 | self.media.port 170 | } 171 | 172 | pub fn get_port_count(&self) -> u32 { 173 | self.media.port_count 174 | } 175 | 176 | pub fn get_proto(&self) -> &SdpProtocolValue { 177 | &self.media.proto 178 | } 179 | 180 | pub fn get_formats(&self) -> &SdpFormatList { 181 | &self.media.formats 182 | } 183 | 184 | pub fn get_bandwidth(&self) -> &Vec { 185 | &self.bandwidth 186 | } 187 | 188 | pub fn add_bandwidth(&mut self, bw: SdpBandwidth) { 189 | self.bandwidth.push(bw) 190 | } 191 | 192 | pub fn get_attributes(&self) -> &Vec { 193 | &self.attribute 194 | } 195 | 196 | pub fn add_attribute(&mut self, attr: SdpAttribute) -> Result<(), SdpParserInternalError> { 197 | if !attr.allowed_at_media_level() { 198 | return Err(SdpParserInternalError::Generic(format!( 199 | "{attr} not allowed at media level" 200 | ))); 201 | } 202 | self.attribute.push(attr); 203 | Ok(()) 204 | } 205 | 206 | pub fn get_attribute(&self, t: SdpAttributeType) -> Option<&SdpAttribute> { 207 | self.attribute 208 | .iter() 209 | .find(|a| SdpAttributeType::from(*a) == t) 210 | } 211 | 212 | pub fn remove_attribute(&mut self, t: SdpAttributeType) { 213 | self.attribute.retain(|a| SdpAttributeType::from(a) != t); 214 | } 215 | 216 | pub fn set_attribute(&mut self, attr: SdpAttribute) -> Result<(), SdpParserInternalError> { 217 | self.remove_attribute(SdpAttributeType::from(&attr)); 218 | self.add_attribute(attr) 219 | } 220 | 221 | pub fn remove_codecs(&mut self) { 222 | match self.media.formats { 223 | SdpFormatList::Integers(_) => self.media.formats = SdpFormatList::Integers(Vec::new()), 224 | SdpFormatList::Strings(_) => self.media.formats = SdpFormatList::Strings(Vec::new()), 225 | } 226 | 227 | self.attribute.retain({ 228 | |x| { 229 | !matches!( 230 | *x, 231 | SdpAttribute::Rtpmap(_) 232 | | SdpAttribute::Fmtp(_) 233 | | SdpAttribute::Rtcpfb(_) 234 | | SdpAttribute::Sctpmap(_) 235 | | SdpAttribute::SctpPort(_) 236 | ) 237 | } 238 | }); 239 | } 240 | 241 | pub fn add_codec(&mut self, rtpmap: SdpAttributeRtpmap) -> Result<(), SdpParserInternalError> { 242 | match self.media.formats { 243 | SdpFormatList::Integers(ref mut x) => x.push(u32::from(rtpmap.payload_type)), 244 | SdpFormatList::Strings(ref mut x) => x.push(rtpmap.payload_type.to_string()), 245 | } 246 | 247 | self.add_attribute(SdpAttribute::Rtpmap(rtpmap))?; 248 | Ok(()) 249 | } 250 | 251 | pub fn get_attributes_of_type(&self, t: SdpAttributeType) -> Vec<&SdpAttribute> { 252 | self.attribute 253 | .iter() 254 | .filter(|a| SdpAttributeType::from(*a) == t) 255 | .collect() 256 | } 257 | 258 | pub fn get_connection(&self) -> &Option { 259 | &self.connection 260 | } 261 | 262 | pub fn set_connection(&mut self, c: SdpConnection) { 263 | self.connection = Some(c) 264 | } 265 | 266 | pub fn add_datachannel( 267 | &mut self, 268 | name: String, 269 | port: u16, 270 | streams: u16, 271 | msg_size: u32, 272 | ) -> Result<(), SdpParserInternalError> { 273 | // Only one allowed, for now. This may change as the specs (and deployments) evolve. 274 | match self.media.proto { 275 | SdpProtocolValue::UdpDtlsSctp | SdpProtocolValue::TcpDtlsSctp => { 276 | // new data channel format according to draft 21 277 | self.media.formats = SdpFormatList::Strings(vec![name]); 278 | self.set_attribute(SdpAttribute::SctpPort(u64::from(port)))?; 279 | } 280 | _ => { 281 | // old data channels format according to draft 05 282 | self.media.formats = SdpFormatList::Integers(vec![u32::from(port)]); 283 | self.set_attribute(SdpAttribute::Sctpmap(SdpAttributeSctpmap { 284 | port, 285 | channels: u32::from(streams), 286 | }))?; 287 | } 288 | } 289 | if msg_size > 0 { 290 | self.set_attribute(SdpAttribute::MaxMessageSize(u64::from(msg_size)))?; 291 | } 292 | self.media.media = SdpMediaValue::Application; 293 | 294 | Ok(()) 295 | } 296 | } 297 | 298 | impl AnonymizingClone for SdpMedia { 299 | fn masked_clone(&self, anon: &mut StatefulSdpAnonymizer) -> Self { 300 | let mut masked = SdpMedia { 301 | media: self.media.clone(), 302 | bandwidth: self.bandwidth.clone(), 303 | connection: self.connection.clone(), 304 | attribute: Vec::new(), 305 | }; 306 | for i in &self.attribute { 307 | masked.attribute.push(i.masked_clone(anon)); 308 | } 309 | masked 310 | } 311 | } 312 | 313 | fn parse_media_token(value: &str) -> Result { 314 | Ok(match value.to_lowercase().as_ref() { 315 | "audio" => SdpMediaValue::Audio, 316 | "video" => SdpMediaValue::Video, 317 | "application" => SdpMediaValue::Application, 318 | _ => { 319 | return Err(SdpParserInternalError::Unsupported(format!( 320 | "unsupported media value: {value}" 321 | ))); 322 | } 323 | }) 324 | } 325 | 326 | fn parse_protocol_token(value: &str) -> Result { 327 | Ok(match value.to_uppercase().as_ref() { 328 | "RTP/AVP" => SdpProtocolValue::RtpAvp, 329 | "RTP/AVPF" => SdpProtocolValue::RtpAvpf, 330 | "RTP/SAVP" => SdpProtocolValue::RtpSavp, 331 | "RTP/SAVPF" => SdpProtocolValue::RtpSavpf, 332 | "TCP/DTLS/RTP/SAVP" => SdpProtocolValue::TcpDtlsRtpSavp, 333 | "TCP/DTLS/RTP/SAVPF" => SdpProtocolValue::TcpDtlsRtpSavpf, 334 | "UDP/TLS/RTP/SAVP" => SdpProtocolValue::UdpTlsRtpSavp, 335 | "UDP/TLS/RTP/SAVPF" => SdpProtocolValue::UdpTlsRtpSavpf, 336 | "DTLS/SCTP" => SdpProtocolValue::DtlsSctp, 337 | "UDP/DTLS/SCTP" => SdpProtocolValue::UdpDtlsSctp, 338 | "TCP/DTLS/SCTP" => SdpProtocolValue::TcpDtlsSctp, 339 | _ => { 340 | return Err(SdpParserInternalError::Unsupported(format!( 341 | "unsupported protocol value: {value}" 342 | ))); 343 | } 344 | }) 345 | } 346 | 347 | pub fn parse_media(value: &str) -> Result { 348 | let mv: Vec<&str> = value.split_whitespace().collect(); 349 | if mv.len() < 4 { 350 | return Err(SdpParserInternalError::Generic( 351 | "media attribute must have at least four tokens".to_string(), 352 | )); 353 | } 354 | let media = parse_media_token(mv[0])?; 355 | let mut ptokens = mv[1].split('/'); 356 | let port = match ptokens.next() { 357 | None => { 358 | return Err(SdpParserInternalError::Generic( 359 | "missing port token".to_string(), 360 | )); 361 | } 362 | Some(p) => p.parse::()?, 363 | }; 364 | if port > 65535 { 365 | return Err(SdpParserInternalError::Generic( 366 | "media port token is too big".to_string(), 367 | )); 368 | } 369 | let port_count = match ptokens.next() { 370 | None => 0, 371 | Some(c) => c.parse::()?, 372 | }; 373 | let proto = parse_protocol_token(mv[2])?; 374 | let fmt_slice: &[&str] = &mv[3..]; 375 | let formats = match media { 376 | SdpMediaValue::Audio | SdpMediaValue::Video => { 377 | let mut fmt_vec: Vec = vec![]; 378 | for num in fmt_slice { 379 | let fmt_num = num.parse::()?; 380 | if matches!(fmt_num, 1 | 2 | 19 | 64..=95 | 128 .. ) { 381 | return Err(SdpParserInternalError::Generic( 382 | "format number in media line is out of range".to_string(), 383 | )); 384 | } 385 | fmt_vec.push(fmt_num); 386 | } 387 | SdpFormatList::Integers(fmt_vec) 388 | } 389 | SdpMediaValue::Application => { 390 | let mut fmt_vec: Vec = vec![]; 391 | // TODO enforce length == 1 and content 'webrtc-datachannel' only? 392 | for token in fmt_slice { 393 | fmt_vec.push(String::from(*token)); 394 | } 395 | SdpFormatList::Strings(fmt_vec) 396 | } 397 | }; 398 | let m = SdpMediaLine { 399 | media, 400 | port, 401 | port_count, 402 | proto, 403 | formats, 404 | }; 405 | trace!("media: {}, {}, {}, {}", m.media, m.port, m.proto, m.formats); 406 | Ok(SdpType::Media(m)) 407 | } 408 | 409 | pub fn parse_media_vector(lines: &mut Vec) -> Result, SdpParserError> { 410 | let mut media_sections: Vec = Vec::new(); 411 | 412 | let media_line = lines.remove(0); 413 | let mut sdp_media = match media_line.sdp_type { 414 | SdpType::Media(v) => SdpMedia::new(v), 415 | _ => { 416 | return Err(SdpParserError::Sequence { 417 | message: "first line in media section needs to be a media line".to_string(), 418 | line_number: media_line.line_number, 419 | }); 420 | } 421 | }; 422 | 423 | while !lines.is_empty() { 424 | let line = lines.remove(0); 425 | let _line_number = line.line_number; 426 | match line.sdp_type { 427 | SdpType::Connection(c) => { 428 | if sdp_media.connection.is_some() { 429 | return Err(SdpParserError::Sequence { 430 | message: "connection type already exists at this media level".to_string(), 431 | line_number: _line_number, 432 | }); 433 | } 434 | 435 | sdp_media.set_connection(c); 436 | } 437 | SdpType::Bandwidth(b) => sdp_media.add_bandwidth(b), 438 | SdpType::Attribute(a) => { 439 | match a { 440 | SdpAttribute::DtlsMessage(_) => { 441 | // Ignore this attribute on media level 442 | Ok(()) 443 | } 444 | SdpAttribute::Rtpmap(rtpmap) => { 445 | sdp_media.add_attribute(SdpAttribute::Rtpmap(SdpAttributeRtpmap { 446 | payload_type: rtpmap.payload_type, 447 | codec_name: rtpmap.codec_name.clone(), 448 | frequency: rtpmap.frequency, 449 | channels: rtpmap.channels, 450 | })) 451 | } 452 | _ => sdp_media.add_attribute(a), 453 | } 454 | .map_err(|e: SdpParserInternalError| SdpParserError::Sequence { 455 | message: format!("{e}"), 456 | line_number: _line_number, 457 | })? 458 | } 459 | SdpType::Media(v) => { 460 | media_sections.push(sdp_media); 461 | sdp_media = SdpMedia::new(v); 462 | } 463 | 464 | SdpType::Origin(_) | SdpType::Session(_) | SdpType::Timing(_) | SdpType::Version(_) => { 465 | return Err(SdpParserError::Sequence { 466 | message: "invalid type in media section".to_string(), 467 | line_number: line.line_number, 468 | }); 469 | } 470 | }; 471 | } 472 | 473 | media_sections.push(sdp_media); 474 | 475 | Ok(media_sections) 476 | } 477 | 478 | #[cfg(test)] 479 | #[path = "./media_type_tests.rs"] 480 | mod media_type_tests; 481 | -------------------------------------------------------------------------------- /src/media_type_tests.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::*; 6 | use address::{AddressType, ExplicitlyTypedAddress}; 7 | use attribute_type::{ 8 | SdpAttributeFmtp, SdpAttributeFmtpParameters, SdpAttributePayloadType, SdpAttributeRtcpFb, 9 | SdpAttributeRtcpFbType, 10 | }; 11 | use std::convert::TryFrom; 12 | 13 | pub fn create_dummy_media_section() -> SdpMedia { 14 | let media_line = SdpMediaLine { 15 | media: SdpMediaValue::Audio, 16 | port: 9, 17 | port_count: 0, 18 | proto: SdpProtocolValue::RtpSavpf, 19 | formats: SdpFormatList::Integers(Vec::new()), 20 | }; 21 | SdpMedia::new(media_line) 22 | } 23 | 24 | // TODO is this useful outside of tests? 25 | impl SdpFormatList { 26 | fn len(&self) -> usize { 27 | match *self { 28 | SdpFormatList::Integers(ref x) => x.len(), 29 | SdpFormatList::Strings(ref x) => x.len(), 30 | } 31 | } 32 | } 33 | 34 | pub fn add_dummy_attributes(media: &mut SdpMedia) { 35 | assert!(media 36 | .add_attribute(SdpAttribute::Rtcpfb(SdpAttributeRtcpFb { 37 | payload_type: SdpAttributePayloadType::Wildcard, 38 | feedback_type: SdpAttributeRtcpFbType::Ack, 39 | parameter: "".to_string(), 40 | extra: "".to_string(), 41 | },)) 42 | .is_ok()); 43 | assert!(media 44 | .add_attribute(SdpAttribute::Fmtp(SdpAttributeFmtp { 45 | payload_type: 1, 46 | parameters: SdpAttributeFmtpParameters { 47 | packetization_mode: 0, 48 | level_asymmetry_allowed: false, 49 | profile_level_id: 0x0042_0010, 50 | max_fs: 0, 51 | max_cpb: 0, 52 | max_dpb: 0, 53 | max_br: 0, 54 | max_mbps: 0, 55 | usedtx: false, 56 | stereo: false, 57 | useinbandfec: false, 58 | cbr: false, 59 | max_fr: 0, 60 | profile: None, 61 | level_idx: None, 62 | tier: None, 63 | maxplaybackrate: 48000, 64 | maxaveragebitrate: 0, 65 | ptime: 0, 66 | minptime: 0, 67 | maxptime: 0, 68 | encodings: Vec::new(), 69 | dtmf_tones: "".to_string(), 70 | rtx: None, 71 | unknown_tokens: Vec::new() 72 | } 73 | },)) 74 | .is_ok()); 75 | assert!(media 76 | .add_attribute(SdpAttribute::Sctpmap(SdpAttributeSctpmap { 77 | port: 5000, 78 | channels: 2, 79 | })) 80 | .is_ok()); 81 | assert!(media.add_attribute(SdpAttribute::BundleOnly).is_ok()); 82 | assert!(media.add_attribute(SdpAttribute::SctpPort(5000)).is_ok()); 83 | 84 | assert!(media.get_attribute(SdpAttributeType::Rtpmap).is_some()); 85 | assert!(media.get_attribute(SdpAttributeType::Rtcpfb).is_some()); 86 | assert!(media.get_attribute(SdpAttributeType::Fmtp).is_some()); 87 | assert!(media.get_attribute(SdpAttributeType::Sctpmap).is_some()); 88 | assert!(media.get_attribute(SdpAttributeType::SctpPort).is_some()); 89 | assert!(media.get_attribute(SdpAttributeType::BundleOnly).is_some()); 90 | } 91 | 92 | fn check_parse(media_line_str: &str) -> SdpMediaLine { 93 | if let Ok(SdpType::Media(media_line)) = parse_media(media_line_str) { 94 | media_line 95 | } else { 96 | unreachable!() 97 | } 98 | } 99 | 100 | fn check_parse_and_serialize(media_line_str: &str) { 101 | let parsed = check_parse(media_line_str); 102 | assert_eq!(parsed.to_string(), media_line_str.to_string()); 103 | } 104 | 105 | #[test] 106 | fn test_get_set_port() { 107 | let mut msection = create_dummy_media_section(); 108 | assert_eq!(msection.get_port(), 9); 109 | msection.set_port(2048); 110 | assert_eq!(msection.get_port(), 2048); 111 | } 112 | 113 | #[test] 114 | fn test_add_codec() -> Result<(), SdpParserInternalError> { 115 | let mut msection = create_dummy_media_section(); 116 | msection.add_codec(SdpAttributeRtpmap::new(96, "foobar".to_string(), 1000))?; 117 | assert_eq!(msection.get_formats().len(), 1); 118 | assert!(msection.get_attribute(SdpAttributeType::Rtpmap).is_some()); 119 | 120 | let mut msection = create_dummy_media_section(); 121 | msection.media.formats = SdpFormatList::Strings(Vec::new()); 122 | msection.add_codec(SdpAttributeRtpmap::new(97, "boofar".to_string(), 1001))?; 123 | assert_eq!(msection.get_formats().len(), 1); 124 | assert!(msection.get_attribute(SdpAttributeType::Rtpmap).is_some()); 125 | Ok(()) 126 | } 127 | 128 | #[test] 129 | fn test_remove_codecs() -> Result<(), SdpParserInternalError> { 130 | let mut msection = create_dummy_media_section(); 131 | msection.add_codec(SdpAttributeRtpmap::new(96, "foobar".to_string(), 1000))?; 132 | assert_eq!(msection.get_formats().len(), 1); 133 | assert!(msection.get_attribute(SdpAttributeType::Rtpmap).is_some()); 134 | msection.remove_codecs(); 135 | assert_eq!(msection.get_formats().len(), 0); 136 | assert!(msection.get_attribute(SdpAttributeType::Rtpmap).is_none()); 137 | 138 | let mut msection = create_dummy_media_section(); 139 | msection.media.formats = SdpFormatList::Strings(Vec::new()); 140 | msection.add_codec(SdpAttributeRtpmap::new(97, "boofar".to_string(), 1001))?; 141 | assert_eq!(msection.get_formats().len(), 1); 142 | 143 | add_dummy_attributes(&mut msection); 144 | 145 | msection.remove_codecs(); 146 | assert_eq!(msection.get_formats().len(), 0); 147 | assert!(msection.get_attribute(SdpAttributeType::Rtpmap).is_none()); 148 | assert!(msection.get_attribute(SdpAttributeType::Rtcpfb).is_none()); 149 | assert!(msection.get_attribute(SdpAttributeType::Fmtp).is_none()); 150 | assert!(msection.get_attribute(SdpAttributeType::Sctpmap).is_none()); 151 | assert!(msection.get_attribute(SdpAttributeType::SctpPort).is_none()); 152 | Ok(()) 153 | } 154 | 155 | #[test] 156 | fn test_add_datachannel() -> Result<(), SdpParserInternalError> { 157 | let mut msection = create_dummy_media_section(); 158 | msection.add_datachannel("foo".to_string(), 5000, 256, 0)?; 159 | assert_eq!(*msection.get_type(), SdpMediaValue::Application); 160 | assert!(msection.get_attribute(SdpAttributeType::SctpPort).is_none()); 161 | assert!(msection 162 | .get_attribute(SdpAttributeType::MaxMessageSize) 163 | .is_none()); 164 | assert!(msection.get_attribute(SdpAttributeType::Sctpmap).is_some()); 165 | match *msection.get_attribute(SdpAttributeType::Sctpmap).unwrap() { 166 | SdpAttribute::Sctpmap(ref s) => { 167 | assert_eq!(s.port, 5000); 168 | assert_eq!(s.channels, 256); 169 | } 170 | _ => unreachable!(), 171 | } 172 | 173 | let mut msection = create_dummy_media_section(); 174 | msection.add_datachannel("foo".to_string(), 5000, 256, 1234)?; 175 | assert_eq!(*msection.get_type(), SdpMediaValue::Application); 176 | assert!(msection.get_attribute(SdpAttributeType::SctpPort).is_none()); 177 | assert!(msection 178 | .get_attribute(SdpAttributeType::MaxMessageSize) 179 | .is_some()); 180 | match *msection 181 | .get_attribute(SdpAttributeType::MaxMessageSize) 182 | .unwrap() 183 | { 184 | SdpAttribute::MaxMessageSize(m) => { 185 | assert_eq!(m, 1234); 186 | } 187 | _ => unreachable!(), 188 | } 189 | 190 | let mut msection = create_dummy_media_section(); 191 | msection.media.proto = SdpProtocolValue::UdpDtlsSctp; 192 | msection.add_datachannel("foo".to_string(), 5000, 256, 5678)?; 193 | assert_eq!(*msection.get_type(), SdpMediaValue::Application); 194 | assert!(msection.get_attribute(SdpAttributeType::Sctpmap).is_none()); 195 | assert!(msection.get_attribute(SdpAttributeType::SctpPort).is_some()); 196 | match *msection.get_attribute(SdpAttributeType::SctpPort).unwrap() { 197 | SdpAttribute::SctpPort(s) => { 198 | assert_eq!(s, 5000); 199 | } 200 | _ => unreachable!(), 201 | } 202 | assert!(msection 203 | .get_attribute(SdpAttributeType::MaxMessageSize) 204 | .is_some()); 205 | match *msection 206 | .get_attribute(SdpAttributeType::MaxMessageSize) 207 | .unwrap() 208 | { 209 | SdpAttribute::MaxMessageSize(m) => { 210 | assert_eq!(m, 5678); 211 | } 212 | _ => unreachable!(), 213 | } 214 | Ok(()) 215 | } 216 | 217 | #[test] 218 | fn test_parse_media_token() -> Result<(), SdpParserInternalError> { 219 | let audio = parse_media_token("audio")?; 220 | assert_eq!(audio, SdpMediaValue::Audio); 221 | let video = parse_media_token("VIDEO")?; 222 | assert_eq!(video, SdpMediaValue::Video); 223 | let app = parse_media_token("aPplIcatIOn")?; 224 | assert_eq!(app, SdpMediaValue::Application); 225 | 226 | assert!(parse_media_token("").is_err()); 227 | assert!(parse_media_token("foobar").is_err()); 228 | Ok(()) 229 | } 230 | 231 | #[test] 232 | fn test_parse_protocol_rtp_token() -> Result<(), SdpParserInternalError> { 233 | fn parse_and_serialize_protocol_token( 234 | token: &str, 235 | result: SdpProtocolValue, 236 | ) -> Result<(), SdpParserInternalError> { 237 | let rtps = parse_protocol_token(token)?; 238 | assert_eq!(rtps, result); 239 | assert_eq!(rtps.to_string(), token.to_uppercase()); 240 | Ok(()) 241 | } 242 | parse_and_serialize_protocol_token("rtp/avp", SdpProtocolValue::RtpAvp)?; 243 | parse_and_serialize_protocol_token("rtp/avpf", SdpProtocolValue::RtpAvpf)?; 244 | parse_and_serialize_protocol_token("rtp/savp", SdpProtocolValue::RtpSavp)?; 245 | parse_and_serialize_protocol_token("rtp/savpf", SdpProtocolValue::RtpSavpf)?; 246 | parse_and_serialize_protocol_token("udp/tls/rtp/savp", SdpProtocolValue::UdpTlsRtpSavp)?; 247 | parse_and_serialize_protocol_token("udp/tls/rtp/savpf", SdpProtocolValue::UdpTlsRtpSavpf)?; 248 | parse_and_serialize_protocol_token("TCP/dtls/rtp/savp", SdpProtocolValue::TcpDtlsRtpSavp)?; 249 | parse_and_serialize_protocol_token("tcp/DTLS/rtp/savpf", SdpProtocolValue::TcpDtlsRtpSavpf)?; 250 | 251 | assert!(parse_protocol_token("").is_err()); 252 | assert!(parse_protocol_token("foobar").is_err()); 253 | Ok(()) 254 | } 255 | 256 | #[test] 257 | fn test_parse_protocol_sctp_token() -> Result<(), SdpParserInternalError> { 258 | fn parse_and_serialize_protocol_token( 259 | token: &str, 260 | result: SdpProtocolValue, 261 | ) -> Result<(), SdpParserInternalError> { 262 | let rtps = parse_protocol_token(token)?; 263 | assert_eq!(rtps, result); 264 | assert_eq!(rtps.to_string(), token.to_uppercase()); 265 | Ok(()) 266 | } 267 | parse_and_serialize_protocol_token("dtLs/ScTP", SdpProtocolValue::DtlsSctp)?; 268 | parse_and_serialize_protocol_token("udp/DTLS/sctp", SdpProtocolValue::UdpDtlsSctp)?; 269 | parse_and_serialize_protocol_token("tcp/dtls/SCTP", SdpProtocolValue::TcpDtlsSctp)?; 270 | Ok(()) 271 | } 272 | 273 | #[test] 274 | fn test_media_works() { 275 | check_parse_and_serialize("audio 9 UDP/TLS/RTP/SAVPF 109"); 276 | check_parse_and_serialize("video 9 UDP/TLS/RTP/SAVPF 126"); 277 | check_parse_and_serialize("application 9 DTLS/SCTP 5000"); 278 | check_parse_and_serialize("application 9 UDP/DTLS/SCTP webrtc-datachannel"); 279 | 280 | check_parse_and_serialize("audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8"); 281 | check_parse_and_serialize("audio 0 UDP/TLS/RTP/SAVPF 8"); 282 | check_parse_and_serialize("audio 9/2 UDP/TLS/RTP/SAVPF 8"); 283 | } 284 | 285 | #[test] 286 | fn test_media_missing_token() { 287 | assert!(parse_media("video 9 UDP/TLS/RTP/SAVPF").is_err()); 288 | } 289 | 290 | #[test] 291 | fn test_media_invalid_port_number() { 292 | assert!(parse_media("video 75123 UDP/TLS/RTP/SAVPF 8").is_err()); 293 | } 294 | 295 | #[test] 296 | fn test_media_invalid_type() { 297 | assert!(parse_media("invalid 9 UDP/TLS/RTP/SAVPF 8").is_err()); 298 | } 299 | 300 | #[test] 301 | fn test_media_invalid_port() { 302 | assert!(parse_media("audio / UDP/TLS/RTP/SAVPF 8").is_err()); 303 | } 304 | 305 | #[test] 306 | fn test_media_invalid_transport() { 307 | assert!(parse_media("audio 9 invalid/invalid 8").is_err()); 308 | } 309 | 310 | #[test] 311 | fn test_media_invalid_payload() { 312 | assert!(parse_media("audio 9 UDP/TLS/RTP/SAVPF 300").is_err()); 313 | } 314 | 315 | #[test] 316 | fn test_media_vector_first_line_failure() { 317 | let mut sdp_lines: Vec = Vec::new(); 318 | let line = SdpLine { 319 | line_number: 0, 320 | sdp_type: SdpType::Session("hello".to_string()), 321 | text: "".to_owned(), 322 | }; 323 | sdp_lines.push(line); 324 | assert!(parse_media_vector(&mut sdp_lines).is_err()); 325 | } 326 | 327 | #[test] 328 | fn test_media_vector_multiple_connections() { 329 | let mut sdp_lines: Vec = Vec::new(); 330 | let media_line = SdpMediaLine { 331 | media: SdpMediaValue::Audio, 332 | port: 9, 333 | port_count: 0, 334 | proto: SdpProtocolValue::RtpSavpf, 335 | formats: SdpFormatList::Integers(Vec::new()), 336 | }; 337 | let media = SdpLine { 338 | line_number: 0, 339 | sdp_type: SdpType::Media(media_line), 340 | text: "".to_owned(), 341 | }; 342 | sdp_lines.push(media); 343 | let c = SdpConnection { 344 | address: ExplicitlyTypedAddress::try_from((AddressType::IpV4, "127.0.0.1")).unwrap(), 345 | ttl: None, 346 | amount: None, 347 | }; 348 | let c1 = SdpLine { 349 | line_number: 1, 350 | sdp_type: SdpType::Connection(c.clone()), 351 | text: "".to_owned(), 352 | }; 353 | sdp_lines.push(c1); 354 | let c2 = SdpLine { 355 | line_number: 2, 356 | sdp_type: SdpType::Connection(c), 357 | text: "".to_owned(), 358 | }; 359 | sdp_lines.push(c2); 360 | assert!(parse_media_vector(&mut sdp_lines).is_err()); 361 | } 362 | 363 | #[test] 364 | fn test_media_vector_invalid_types() { 365 | let mut sdp_lines: Vec = Vec::new(); 366 | let media_line = SdpMediaLine { 367 | media: SdpMediaValue::Audio, 368 | port: 9, 369 | port_count: 0, 370 | proto: SdpProtocolValue::RtpSavpf, 371 | formats: SdpFormatList::Integers(Vec::new()), 372 | }; 373 | let media = SdpLine { 374 | line_number: 0, 375 | sdp_type: SdpType::Media(media_line), 376 | text: "".to_owned(), 377 | }; 378 | sdp_lines.push(media); 379 | use SdpTiming; 380 | let t = SdpTiming { start: 0, stop: 0 }; 381 | let tline = SdpLine { 382 | line_number: 1, 383 | sdp_type: SdpType::Timing(t), 384 | text: "".to_owned(), 385 | }; 386 | sdp_lines.push(tline); 387 | assert!(parse_media_vector(&mut sdp_lines).is_err()); 388 | } 389 | 390 | #[test] 391 | fn test_media_vector_invalid_media_level_attribute() { 392 | let mut sdp_lines: Vec = Vec::new(); 393 | let media_line = SdpMediaLine { 394 | media: SdpMediaValue::Audio, 395 | port: 9, 396 | port_count: 0, 397 | proto: SdpProtocolValue::RtpSavpf, 398 | formats: SdpFormatList::Integers(Vec::new()), 399 | }; 400 | let media = SdpLine { 401 | line_number: 0, 402 | sdp_type: SdpType::Media(media_line), 403 | text: "".to_owned(), 404 | }; 405 | sdp_lines.push(media); 406 | let a = SdpAttribute::IceLite; 407 | let aline = SdpLine { 408 | line_number: 1, 409 | sdp_type: SdpType::Attribute(a), 410 | text: "".to_owned(), 411 | }; 412 | sdp_lines.push(aline); 413 | assert!(parse_media_vector(&mut sdp_lines).is_err()); 414 | } 415 | -------------------------------------------------------------------------------- /src/network.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use address::{Address, AddressType}; 6 | use error::SdpParserInternalError; 7 | use std::net::IpAddr; 8 | use std::str::FromStr; 9 | 10 | pub fn ip_address_to_string(addr: IpAddr) -> String { 11 | match addr { 12 | IpAddr::V4(ipv4) => format!("IN IP4 {ipv4}"), 13 | IpAddr::V6(ipv6) => format!("IN IP6 {ipv6}"), 14 | } 15 | } 16 | 17 | pub fn parse_network_type(value: &str) -> Result<(), SdpParserInternalError> { 18 | if value.to_uppercase() != "IN" { 19 | return Err(SdpParserInternalError::Generic( 20 | "nettype must be IN".to_string(), 21 | )); 22 | }; 23 | Ok(()) 24 | } 25 | 26 | pub fn parse_address_type(value: &str) -> Result { 27 | AddressType::from_str(value.to_uppercase().as_str()) 28 | .map_err(|_| SdpParserInternalError::Generic("address type must be IP4 or IP6".to_string())) 29 | } 30 | 31 | pub fn parse_unicast_address(value: &str) -> Result { 32 | Address::from_str(value) 33 | } 34 | 35 | #[cfg(test)] 36 | #[path = "./network_tests.rs"] 37 | mod tests; 38 | -------------------------------------------------------------------------------- /src/network_tests.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::*; 6 | 7 | #[test] 8 | fn test_parse_network_type() -> Result<(), SdpParserInternalError> { 9 | parse_network_type("iN")?; 10 | 11 | assert!(parse_network_type("").is_err()); 12 | assert!(parse_network_type("FOO").is_err()); 13 | Ok(()) 14 | } 15 | 16 | #[test] 17 | fn test_parse_address_type() -> Result<(), SdpParserInternalError> { 18 | let ip4 = parse_address_type("iP4")?; 19 | assert_eq!(ip4, AddressType::IpV4); 20 | let ip6 = parse_address_type("Ip6")?; 21 | assert_eq!(ip6, AddressType::IpV6); 22 | 23 | assert!(parse_address_type("").is_err()); 24 | assert!(parse_address_type("IP5").is_err()); 25 | Ok(()) 26 | } 27 | 28 | #[test] 29 | fn test_parse_unicast_address() -> Result<(), SdpParserInternalError> { 30 | parse_unicast_address("127.0.0.1")?; 31 | parse_unicast_address("::1")?; 32 | Ok(()) 33 | } 34 | -------------------------------------------------------------------------------- /tests/parse_sdp_tests.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | extern crate webrtc_sdp; 6 | 7 | #[cfg(test)] 8 | fn check_parse_and_serialize(sdp_str: &str) { 9 | let parsed_sdp = webrtc_sdp::parse_sdp(sdp_str, true); 10 | assert!(parsed_sdp.is_ok()); 11 | let serialized_sdp = parsed_sdp.unwrap().to_string(); 12 | assert_eq!(serialized_sdp, sdp_str) 13 | } 14 | 15 | #[test] 16 | fn parse_minimal_sdp() { 17 | let sdp_str = "v=0\r\n\ 18 | o=- 1 1 IN IP4 0.0.0.0\r\n\ 19 | s=-\r\n\ 20 | t=0 0\r\n\ 21 | c=IN IP4 0.0.0.0\r\n\ 22 | m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n"; 23 | let sdp_res = webrtc_sdp::parse_sdp(sdp_str, true); 24 | assert!(sdp_res.is_ok()); 25 | let sdp_opt = sdp_res.ok(); 26 | assert!(sdp_opt.is_some()); 27 | let sdp = sdp_opt.unwrap(); 28 | assert_eq!(sdp.get_version(), 0); 29 | let o = sdp.get_origin(); 30 | assert_eq!(o.username, "-"); 31 | assert_eq!(o.session_id, 1); 32 | assert_eq!(o.session_version, 1); 33 | assert_eq!(sdp.get_session(), &Some("-".to_owned())); 34 | assert!(sdp.timing.is_some()); 35 | assert!(sdp.get_connection().is_some()); 36 | assert_eq!(sdp.attribute.len(), 0); 37 | assert_eq!(sdp.media.len(), 1); 38 | 39 | let msection = &(sdp.media[0]); 40 | assert_eq!( 41 | *msection.get_type(), 42 | webrtc_sdp::media_type::SdpMediaValue::Audio 43 | ); 44 | assert_eq!(msection.get_port(), 0); 45 | assert_eq!(msection.get_port_count(), 0); 46 | assert_eq!( 47 | *msection.get_proto(), 48 | webrtc_sdp::media_type::SdpProtocolValue::UdpTlsRtpSavpf 49 | ); 50 | assert!(msection.get_attributes().is_empty()); 51 | assert!(msection.get_bandwidth().is_empty()); 52 | assert!(msection.get_connection().is_none()); 53 | 54 | check_parse_and_serialize(sdp_str); 55 | } 56 | 57 | #[test] 58 | fn parse_minimal_sdp_with_emtpy_lines() { 59 | let sdp = "v=0\r\n 60 | \r\n 61 | o=- 0 0 IN IP4 0.0.0.0\r\n 62 | \r\n 63 | s=-\r\n 64 | c=IN IP4 0.0.0.0\r\n 65 | t=0 0\r\n 66 | m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n"; 67 | let sdp_res = webrtc_sdp::parse_sdp(sdp, false); 68 | assert!(sdp_res.is_ok()); 69 | let sdp_opt = sdp_res.ok(); 70 | assert!(sdp_opt.is_some()); 71 | let sdp = sdp_opt.unwrap(); 72 | assert_eq!(sdp.get_version(), 0); 73 | assert_eq!(sdp.get_session(), &Some("-".to_owned())); 74 | } 75 | 76 | #[test] 77 | fn parse_minimal_sdp_with_single_space_session() { 78 | let sdp = "v=0\r\n 79 | \r\n 80 | o=- 0 0 IN IP4 0.0.0.0\r\n 81 | \r\n 82 | s= \r\n 83 | c=IN IP4 0.0.0.0\r\n 84 | t=0 0\r\n 85 | m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n"; 86 | let sdp_res = webrtc_sdp::parse_sdp(sdp, false); 87 | assert!(sdp_res.is_ok()); 88 | let sdp_opt = sdp_res.ok(); 89 | assert!(sdp_opt.is_some()); 90 | let sdp = sdp_opt.unwrap(); 91 | assert_eq!(sdp.get_version(), 0); 92 | assert_eq!(sdp.get_session(), &None); 93 | } 94 | 95 | #[test] 96 | fn parse_minimal_sdp_with_most_session_types() { 97 | let sdp_str = "v=0\r\n\ 98 | o=- 0 0 IN IP4 0.0.0.0\r\n\ 99 | s=-\r\n\ 100 | t=0 0\r\n\ 101 | b=AS:1\r\n\ 102 | b=CT:123\r\n\ 103 | b=TIAS:12345\r\n\ 104 | b=UNKNOWN:9\r\n\ 105 | c=IN IP6 ::1/1/1\r\n\ 106 | a=ice-options:trickle\r\n\ 107 | m=audio 0 UDP/TLS/RTP/SAVPF 0\r\n"; 108 | let sdp_res = webrtc_sdp::parse_sdp(sdp_str, false); 109 | assert!(sdp_res.is_ok()); 110 | let sdp_opt = sdp_res.ok(); 111 | assert!(sdp_opt.is_some()); 112 | let sdp = sdp_opt.unwrap(); 113 | assert_eq!(sdp.version, 0); 114 | assert_eq!(sdp.session, Some("-".to_owned())); 115 | assert!(sdp.get_connection().is_some()); 116 | 117 | check_parse_and_serialize(sdp_str); 118 | } 119 | 120 | #[test] 121 | fn parse_minimal_sdp_with_most_media_types() { 122 | let sdp_str = "v=0\r\n\ 123 | o=- 0 0 IN IP4 0.0.0.0\r\n\ 124 | s=-\r\n\ 125 | t=0 0\r\n\ 126 | m=video 0 UDP/TLS/RTP/SAVPF 0\r\n\ 127 | b=AS:1\r\n\ 128 | b=CT:123\r\n\ 129 | b=TIAS:12345\r\n\ 130 | c=IN IP4 0.0.0.0\r\n\ 131 | a=sendrecv\r\n"; 132 | let sdp_res = webrtc_sdp::parse_sdp(sdp_str, false); 133 | assert!(sdp_res.is_ok()); 134 | let sdp_opt = sdp_res.ok(); 135 | assert!(sdp_opt.is_some()); 136 | let sdp = sdp_opt.unwrap(); 137 | assert_eq!(sdp.version, 0); 138 | assert_eq!(sdp.session, Some("-".to_owned())); 139 | assert_eq!(sdp.attribute.len(), 0); 140 | assert_eq!(sdp.media.len(), 1); 141 | 142 | let msection = &(sdp.media[0]); 143 | assert_eq!( 144 | *msection.get_type(), 145 | webrtc_sdp::media_type::SdpMediaValue::Video 146 | ); 147 | assert_eq!(msection.get_port(), 0); 148 | assert_eq!( 149 | *msection.get_proto(), 150 | webrtc_sdp::media_type::SdpProtocolValue::UdpTlsRtpSavpf 151 | ); 152 | assert!(!msection.get_bandwidth().is_empty()); 153 | assert!(!msection.get_connection().is_none()); 154 | assert!(!msection.get_attributes().is_empty()); 155 | assert!(msection 156 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Sendrecv) 157 | .is_some()); 158 | 159 | check_parse_and_serialize(sdp_str); 160 | } 161 | 162 | #[test] 163 | fn parse_firefox_audio_offer() { 164 | let sdp_str = "v=0\r\n\ 165 | o=mozilla...THIS_IS_SDPARTA-52.0a1 506705521068071134 0 IN IP4 0.0.0.0\r\n\ 166 | s=-\r\n\ 167 | t=0 0\r\n\ 168 | a=fingerprint:sha-256 CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC:BF:2F:E3:91:CB:57:A9:9D:4A:A2:0B:40\r\n\ 169 | a=group:BUNDLE sdparta_0\r\n\ 170 | a=ice-options:trickle\r\n\ 171 | a=msid-semantic:WMS *\r\n\ 172 | m=audio 9 UDP/TLS/RTP/SAVPF 109 9 0 8\r\n\ 173 | c=IN IP4 0.0.0.0\r\n\ 174 | a=sendrecv\r\n\ 175 | a=extmap:1/sendonly urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n\ 176 | a=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\n\ 177 | a=ice-pwd:e3baa26dd2fa5030d881d385f1e36cce\r\n\ 178 | a=ice-ufrag:58b99ead\r\n\ 179 | a=mid:sdparta_0\r\n\ 180 | a=msid:{5a990edd-0568-ac40-8d97-310fc33f3411} {218cfa1c-617d-2249-9997-60929ce4c405}\r\n\ 181 | a=rtcp-mux\r\n\ 182 | a=rtpmap:109 opus/48000/2\r\n\ 183 | a=rtpmap:9 G722/8000/1\r\n\ 184 | a=rtpmap:0 PCMU/8000\r\n\ 185 | a=rtpmap:8 PCMA/8000\r\n\ 186 | a=setup:actpass\r\n\ 187 | a=ssrc:2655508255 cname:{735484ea-4f6c-f74a-bd66-7425f8476c2e}\r\n"; 188 | let sdp_res = webrtc_sdp::parse_sdp(sdp_str, true); 189 | assert!(sdp_res.is_ok()); 190 | let sdp_opt = sdp_res.ok(); 191 | assert!(sdp_opt.is_some()); 192 | let sdp = sdp_opt.unwrap(); 193 | assert_eq!(sdp.version, 0); 194 | assert_eq!(sdp.media.len(), 1); 195 | 196 | let msection = &(sdp.media[0]); 197 | assert_eq!( 198 | *msection.get_type(), 199 | webrtc_sdp::media_type::SdpMediaValue::Audio 200 | ); 201 | assert_eq!(msection.get_port(), 9); 202 | assert_eq!(msection.get_port_count(), 0); 203 | assert_eq!( 204 | *msection.get_proto(), 205 | webrtc_sdp::media_type::SdpProtocolValue::UdpTlsRtpSavpf 206 | ); 207 | assert!(msection.get_connection().is_some()); 208 | assert!(msection.get_bandwidth().is_empty()); 209 | assert!(!msection.get_attributes().is_empty()); 210 | assert!(msection 211 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Sendrecv) 212 | .is_some()); 213 | assert!(msection 214 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Extmap) 215 | .is_some()); 216 | assert!(msection 217 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Fmtp) 218 | .is_some()); 219 | assert!(msection 220 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::IcePwd) 221 | .is_some()); 222 | assert!(msection 223 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::IceUfrag) 224 | .is_some()); 225 | assert!(msection 226 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Mid) 227 | .is_some()); 228 | assert!(msection 229 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Mid) 230 | .is_some()); 231 | assert!(msection 232 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Msid) 233 | .is_some()); 234 | assert!(msection 235 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::RtcpMux) 236 | .is_some()); 237 | assert_eq!( 238 | msection 239 | .get_attributes_of_type(webrtc_sdp::attribute_type::SdpAttributeType::Rtpmap) 240 | .len(), 241 | 4 242 | ); 243 | assert!(msection 244 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Setup) 245 | .is_some()); 246 | assert!(msection 247 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Ssrc) 248 | .is_some()); 249 | } 250 | 251 | #[test] 252 | fn parse_firefox_video_offer() { 253 | let sdp_str = "v=0\r\n\ 254 | o=mozilla...THIS_IS_SDPARTA-52.0a1 506705521068071134 0 IN IP4 0.0.0.0\r\n\ 255 | s=-\r\n\ 256 | t=0 0\r\n\ 257 | a=extmap-allow-mixed\r\n 258 | a=fingerprint:sha-256 CD:34:D1:62:16:95:7B:B7:EB:74:E2:39:27:97:EB:0B:23:73:AC:BC:BF:2F:E3:91:CB:57:A9:9D:4A:A2:0B:40\r\n\ 259 | a=group:BUNDLE sdparta_2\r\n\ 260 | a=ice-options:trickle\r\n\ 261 | a=msid-semantic:WMS *\r\n\ 262 | m=video 9 UDP/TLS/RTP/SAVPF 126 120 97\r\n\ 263 | c=IN IP4 0.0.0.0\r\n\ 264 | a=recvonly\r\n\ 265 | a=fmtp:126 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1\r\n\ 266 | a=fmtp:120 max-fs=12288;max-fr=60\r\n\ 267 | a=fmtp:97 profile-level-id=42e01f;level-asymmetry-allowed=1\r\n\ 268 | a=ice-pwd:e3baa26dd2fa5030d881d385f1e36cce\r\n\ 269 | a=ice-ufrag:58b99ead\r\n\ 270 | a=mid:sdparta_2\r\n\ 271 | a=rtcp-fb:126 nack\r\n\ 272 | a=rtcp-fb:126 nack pli\r\n\ 273 | a=rtcp-fb:126 ccm fir\r\n\ 274 | a=rtcp-fb:126 goog-remb\r\n\ 275 | a=rtcp-fb:120 nack\r\n\ 276 | a=rtcp-fb:120 nack pli\r\n\ 277 | a=rtcp-fb:120 ccm fir\r\n\ 278 | a=rtcp-fb:120 goog-remb\r\n\ 279 | a=rtcp-fb:97 nack\r\n\ 280 | a=rtcp-fb:97 nack pli\r\n\ 281 | a=rtcp-fb:97 ccm fir\r\n\ 282 | a=rtcp-fb:97 goog-remb\r\n\ 283 | a=rtcp-mux\r\n\ 284 | a=rtpmap:126 H264/90000\r\n\ 285 | a=rtpmap:120 VP8/90000\r\n\ 286 | a=rtpmap:97 H264/90000\r\n\ 287 | a=setup:actpass\r\n\ 288 | a=extmap-allow-mixed\r\n 289 | a=ssrc:2709871439 cname:{735484ea-4f6c-f74a-bd66-7425f8476c2e}"; 290 | let sdp_res = webrtc_sdp::parse_sdp(sdp_str, true); 291 | assert!(sdp_res.is_ok()); 292 | let sdp_opt = sdp_res.ok(); 293 | assert!(sdp_opt.is_some()); 294 | let sdp = sdp_opt.unwrap(); 295 | assert_eq!(sdp.version, 0); 296 | assert_eq!(sdp.media.len(), 1); 297 | 298 | let msection = &(sdp.media[0]); 299 | assert_eq!( 300 | *msection.get_type(), 301 | webrtc_sdp::media_type::SdpMediaValue::Video 302 | ); 303 | assert_eq!(msection.get_port(), 9); 304 | assert_eq!( 305 | *msection.get_proto(), 306 | webrtc_sdp::media_type::SdpProtocolValue::UdpTlsRtpSavpf 307 | ); 308 | assert!(msection.get_connection().is_some()); 309 | assert!(msection.get_bandwidth().is_empty()); 310 | assert!(!msection.get_attributes().is_empty()); 311 | assert!(msection 312 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Recvonly) 313 | .is_some()); 314 | assert!(msection 315 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Extmap) 316 | .is_none()); 317 | assert_eq!( 318 | msection 319 | .get_attributes_of_type(webrtc_sdp::attribute_type::SdpAttributeType::Fmtp) 320 | .len(), 321 | 3 322 | ); 323 | assert!(msection 324 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::IcePwd) 325 | .is_some()); 326 | assert!(msection 327 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::IceUfrag) 328 | .is_some()); 329 | assert!(msection 330 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Mid) 331 | .is_some()); 332 | assert!(msection 333 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Mid) 334 | .is_some()); 335 | assert!(msection 336 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Msid) 337 | .is_none()); 338 | assert_eq!( 339 | msection 340 | .get_attributes_of_type(webrtc_sdp::attribute_type::SdpAttributeType::Rtcpfb) 341 | .len(), 342 | 12 343 | ); 344 | assert!(msection 345 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::RtcpMux) 346 | .is_some()); 347 | assert_eq!( 348 | msection 349 | .get_attributes_of_type(webrtc_sdp::attribute_type::SdpAttributeType::Rtpmap) 350 | .len(), 351 | 3 352 | ); 353 | assert!(msection 354 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Setup) 355 | .is_some()); 356 | assert!(msection 357 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Ssrc) 358 | .is_some()); 359 | } 360 | #[test] 361 | fn parse_firefox_datachannel_offer() { 362 | let sdp_str = "v=0\r\n\ 363 | o=mozilla...THIS_IS_SDPARTA-52.0a2 3327975756663609975 0 IN IP4 0.0.0.0\r\n\ 364 | s=-\r\n\ 365 | t=0 0\r\n\ 366 | a=sendrecv\r\n\ 367 | a=fingerprint:sha-256 AC:72:CB:D6:1E:A3:A3:B0:E7:97:77:25:03:4B:5B:FF:19:6C:02:C6:93:7D:EB:5C:81:6F:36:D9:02:32:F8:23\r\n\ 368 | a=ice-options:trickle\r\n\ 369 | a=msid-semantic:WMS *\r\n\ 370 | m=application 49760 DTLS/SCTP 5000\r\n\ 371 | c=IN IP4 172.16.156.106\r\n\ 372 | a=candidate:0 1 UDP 2122252543 172.16.156.106 49760 typ host\r\n\ 373 | a=sendrecv\r\n\ 374 | a=end-of-candidates\r\n\ 375 | a=ice-pwd:24f485c580129b36447b65df77429a82\r\n\ 376 | a=ice-ufrag:4cba30fe\r\n\ 377 | a=mid:sdparta_0\r\n\ 378 | a=sctpmap:5000 webrtc-datachannel 256\r\n\ 379 | a=setup:active\r\n\ 380 | a=ssrc:3376683177 cname:{62f78ee0-620f-a043-86ca-b69f189f1aea}\r\n"; 381 | let sdp_res = webrtc_sdp::parse_sdp(sdp_str, true); 382 | assert!(sdp_res.is_ok()); 383 | let sdp_opt = sdp_res.ok(); 384 | assert!(sdp_opt.is_some()); 385 | let sdp = sdp_opt.unwrap(); 386 | assert_eq!(sdp.version, 0); 387 | assert_eq!(sdp.media.len(), 1); 388 | 389 | let msection = &(sdp.media[0]); 390 | assert_eq!( 391 | *msection.get_type(), 392 | webrtc_sdp::media_type::SdpMediaValue::Application 393 | ); 394 | assert_eq!(msection.get_port(), 49760); 395 | assert_eq!( 396 | *msection.get_proto(), 397 | webrtc_sdp::media_type::SdpProtocolValue::DtlsSctp 398 | ); 399 | assert!(msection.get_connection().is_some()); 400 | assert!(msection.get_bandwidth().is_empty()); 401 | assert!(!msection.get_attributes().is_empty()); 402 | assert!(msection 403 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Sendrecv) 404 | .is_some()); 405 | assert!(msection 406 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Extmap) 407 | .is_none()); 408 | assert!(msection 409 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::IcePwd) 410 | .is_some()); 411 | assert!(msection 412 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::IceUfrag) 413 | .is_some()); 414 | assert!(msection 415 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::EndOfCandidates) 416 | .is_some()); 417 | assert!(msection 418 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Mid) 419 | .is_some()); 420 | assert!(msection 421 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Msid) 422 | .is_none()); 423 | assert!(msection 424 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Rtcpfb) 425 | .is_none()); 426 | assert!(msection 427 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::RtcpMux) 428 | .is_none()); 429 | assert!(msection 430 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Rtpmap) 431 | .is_none()); 432 | assert!(msection 433 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Sctpmap) 434 | .is_some()); 435 | assert!(msection 436 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Setup) 437 | .is_some()); 438 | assert!(msection 439 | .get_attribute(webrtc_sdp::attribute_type::SdpAttributeType::Ssrc) 440 | .is_some()); 441 | 442 | check_parse_and_serialize(sdp_str); 443 | } 444 | 445 | #[test] 446 | fn parse_chrome_audio_video_offer() { 447 | let sdp = "v=0\r\n 448 | o=- 3836772544440436510 2 IN IP4 127.0.0.1\r\n 449 | s=-\r\n 450 | t=0 0\r\n 451 | a=group:BUNDLE audio video\r\n 452 | a=msid-semantic: WMS HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n 453 | m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126\r\n 454 | c=IN IP4 0.0.0.0\r\n 455 | a=rtcp:9 IN IP4 0.0.0.0\r\n 456 | a=ice-ufrag:A4by\r\n 457 | a=ice-pwd:Gfvb2rbYMiW0dZz8ZkEsXICs\r\n 458 | a=fingerprint:sha-256 15:B0:92:1F:C7:40:EE:22:A6:AF:26:EF:EA:FF:37:1D:B3:EF:11:0B:8B:73:4F:01:7D:C9:AE:26:4F:87:E0:95\r\n 459 | a=setup:actpass\r\n 460 | a=mid:audio\r\n 461 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n 462 | a=sendrecv\r\n 463 | a=rtcp-mux\r\n 464 | a=rtpmap:111 opus/48000/2\r\n 465 | a=rtcp-fb:111 transport-cc\r\n 466 | a=fmtp:111 minptime=10;useinbandfec=1\r\n 467 | a=rtpmap:103 ISAC/16000\r\n 468 | a=rtpmap:104 ISAC/32000\r\n 469 | a=rtpmap:9 G722/8000\r\n 470 | a=rtpmap:0 PCMU/8000\r\n 471 | a=rtpmap:8 PCMA/8000\r\n 472 | a=rtpmap:106 CN/32000\r\n 473 | a=rtpmap:105 CN/16000\r\n 474 | a=rtpmap:13 CN/8000\r\n 475 | a=rtpmap:126 telephone-event/8000\r\n 476 | a=ssrc:162559313 cname:qPTZ+BI+42mgbOi+\r\n 477 | a=ssrc:162559313 msid:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP f6188af5-d8d6-462c-9c75-f12bc41fe322\r\n 478 | a=ssrc:162559313 mslabel:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n 479 | a=ssrc:162559313 label:f6188af5-d8d6-462c-9c75-f12bc41fe322\r\n 480 | m=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116 117 96 97 99 98\r\n 481 | c=IN IP4 0.0.0.0\r\n 482 | a=rtcp:9 IN IP4 0.0.0.0\r\n 483 | a=ice-ufrag:A4by\r\n 484 | a=ice-pwd:Gfvb2rbYMiW0dZz8ZkEsXICs\r\n 485 | a=fingerprint:sha-256 15:B0:92:1F:C7:40:EE:22:A6:AF:26:EF:EA:FF:37:1D:B3:EF:11:0B:8B:73:4F:01:7D:C9:AE:26:4F:87:E0:95\r\n 486 | a=setup:actpass\r\n 487 | a=mid:video\r\n 488 | a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n 489 | a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n 490 | a=extmap:4 urn:3gpp:video-orientation\r\n 491 | a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n 492 | a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\n 493 | a=sendrecv\r\n 494 | a=rtcp-mux\r\n 495 | a=rtcp-rsize\r\n 496 | a=rtpmap:100 VP8/90000\r\n 497 | a=rtcp-fb:100 ccm fir\r\n 498 | a=rtcp-fb:100 nack\r\n 499 | a=rtcp-fb:100 nack pli\r\n 500 | a=rtcp-fb:100 goog-remb\r\n 501 | a=rtcp-fb:100 transport-cc\r\n 502 | a=rtpmap:101 VP9/90000\r\n 503 | a=rtcp-fb:101 ccm fir\r\n 504 | a=rtcp-fb:101 nack\r\n 505 | a=rtcp-fb:101 nack pli\r\n 506 | a=rtcp-fb:101 goog-remb\r\n 507 | a=rtcp-fb:101 transport-cc\r\n 508 | a=rtpmap:107 H264/90000\r\n 509 | a=rtcp-fb:107 ccm fir\r\n 510 | a=rtcp-fb:107 nack\r\n 511 | a=rtcp-fb:107 nack pli\r\n 512 | a=rtcp-fb:107 goog-remb\r\n 513 | a=rtcp-fb:107 transport-cc\r\n 514 | a=fmtp:107 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n 515 | a=rtpmap:116 red/90000\r\n 516 | a=rtpmap:117 ulpfec/90000\r\n 517 | a=rtpmap:96 rtx/90000\r\n 518 | a=fmtp:96 apt=100\r\n 519 | a=rtpmap:97 rtx/90000\r\n 520 | a=fmtp:97 apt=101\r\n 521 | a=rtpmap:99 rtx/90000\r\n 522 | a=fmtp:99 apt=107\r\n 523 | a=rtpmap:98 rtx/90000\r\n 524 | a=fmtp:98 apt=116\r\n 525 | a=ssrc-group:FID 3156517279 2673335628\r\n 526 | a=ssrc:3156517279 cname:qPTZ+BI+42mgbOi+\r\n 527 | a=ssrc:3156517279 msid:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP b6ec5178-c611-403f-bbec-3833ed547c09\r\n 528 | a=ssrc:3156517279 mslabel:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n 529 | a=ssrc:3156517279 label:b6ec5178-c611-403f-bbec-3833ed547c09\r\n 530 | a=ssrc:2673335628 cname:qPTZ+BI+42mgbOi+\r\n 531 | a=ssrc:2673335628 msid:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP b6ec5178-c611-403f-bbec-3833ed547c09\r\n 532 | a=ssrc:2673335628 mslabel:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n 533 | a=ssrc:2673335628 label:b6ec5178-c611-403f-bbec-3833ed547c09\r\n"; 534 | let sdp_res = webrtc_sdp::parse_sdp(sdp, true); 535 | assert!(sdp_res.is_ok()); 536 | let sdp_opt = sdp_res.ok(); 537 | assert!(sdp_opt.is_some()); 538 | let sdp = sdp_opt.unwrap(); 539 | assert_eq!(sdp.version, 0); 540 | assert_eq!(sdp.media.len(), 2); 541 | 542 | let msection1 = &(sdp.media[0]); 543 | assert_eq!( 544 | *msection1.get_type(), 545 | webrtc_sdp::media_type::SdpMediaValue::Audio 546 | ); 547 | assert_eq!(msection1.get_port(), 9); 548 | assert_eq!( 549 | *msection1.get_proto(), 550 | webrtc_sdp::media_type::SdpProtocolValue::UdpTlsRtpSavpf 551 | ); 552 | assert!(!msection1.get_attributes().is_empty()); 553 | assert!(msection1.get_connection().is_some()); 554 | assert!(msection1.get_bandwidth().is_empty()); 555 | 556 | let msection2 = &(sdp.media[1]); 557 | assert_eq!( 558 | *msection2.get_type(), 559 | webrtc_sdp::media_type::SdpMediaValue::Video 560 | ); 561 | assert_eq!(msection2.get_port(), 9); 562 | assert_eq!( 563 | *msection2.get_proto(), 564 | webrtc_sdp::media_type::SdpProtocolValue::UdpTlsRtpSavpf 565 | ); 566 | assert!(!msection2.get_attributes().is_empty()); 567 | assert!(msection2.get_connection().is_some()); 568 | assert!(msection2.get_bandwidth().is_empty()); 569 | } 570 | 571 | #[test] 572 | fn parse_firefox_simulcast_offer() { 573 | let sdp = "v=0\r\n 574 | o=mozilla...THIS_IS_SDPARTA-55.0a1 983028567300715536 0 IN IP4 0.0.0.0\r\n 575 | s=-\r\n 576 | t=0 0\r\n 577 | a=fingerprint:sha-256 68:42:13:88:B6:C1:7D:18:79:07:8A:C6:DC:28:D6:DC:DD:E3:C9:41:E7:80:A7:FE:02:65:FB:76:A0:CD:58:ED\r\n 578 | a=ice-options:trickle\r\n 579 | a=msid-semantic:WMS *\r\n 580 | m=video 9 UDP/TLS/RTP/SAVPF 120 121 126 97\r\n 581 | c=IN IP4 0.0.0.0\r\n 582 | a=sendrecv\r\n 583 | a=extmap:1 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n 584 | a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n 585 | a=extmap:3/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n 586 | a=fmtp:126 profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1\r\n 587 | a=fmtp:97 profile-level-id=42e01f;level-asymmetry-allowed=1\r\n 588 | a=fmtp:120 max-fs=12288;max-fr=60\r\n 589 | a=fmtp:121 max-fs=12288;max-fr=60\r\n 590 | a=ice-pwd:4af388405d558b91f5ba6c2c48f161bf\r\n 591 | a=ice-ufrag:ce1ac488\r\n 592 | a=mid:sdparta_0\r\n 593 | a=msid:{fb6d1fa3-d993-f244-a0fe-d9fb99214c23} {8be9a0f7-9272-6c42-90f3-985d55bd8de5}\r\n 594 | a=rid:foo send\r\n 595 | a=rid:bar send\r\n 596 | a=rtcp-fb:120 nack\r\n 597 | a=rtcp-fb:120 nack pli\r\n 598 | a=rtcp-fb:120 ccm fir\r\n 599 | a=rtcp-fb:120 goog-remb\r\n 600 | a=rtcp-fb:121 nack\r\n 601 | a=rtcp-fb:121 nack pli\r\n 602 | a=rtcp-fb:121 ccm fir\r\n 603 | a=rtcp-fb:121 goog-remb\r\n 604 | a=rtcp-fb:126 nack\r\n 605 | a=rtcp-fb:126 nack pli\r\n 606 | a=rtcp-fb:126 ccm fir\r\n 607 | a=rtcp-fb:126 goog-remb\r\n 608 | a=rtcp-fb:97 nack\r\n 609 | a=rtcp-fb:97 nack pli\r\n 610 | a=rtcp-fb:97 ccm fir\r\n 611 | a=rtcp-fb:97 goog-remb\r\n 612 | a=rtcp-mux\r\n 613 | a=rtpmap:120 VP8/90000\r\n 614 | a=rtpmap:121 VP9/90000\r\n 615 | a=rtpmap:126 H264/90000\r\n 616 | a=rtpmap:97 H264/90000\r\n 617 | a=setup:actpass\r\n 618 | a=simulcast: send rid=foo;bar\r\n 619 | a=ssrc:2988475468 cname:{77067f00-2e8d-8b4c-8992-cfe338f56851}\r\n 620 | a=ssrc:1649784806 cname:{77067f00-2e8d-8b4c-8992-cfe338f56851}\r\n"; 621 | let sdp_res = webrtc_sdp::parse_sdp(sdp, true); 622 | assert!(sdp_res.is_ok()); 623 | let sdp_opt = sdp_res.ok(); 624 | assert!(sdp_opt.is_some()); 625 | let sdp = sdp_opt.unwrap(); 626 | assert_eq!(sdp.version, 0); 627 | assert_eq!(sdp.media.len(), 1); 628 | } 629 | 630 | #[test] 631 | fn parse_firefox_simulcast_answer() { 632 | let sdp_str = "v=0\r\n\ 633 | o=mozilla...THIS_IS_SDPARTA-55.0a1 7548296603161351381 0 IN IP4 0.0.0.0\r\n\ 634 | s=-\r\n\ 635 | t=0 0\r\n\ 636 | a=fingerprint:sha-256 B1:47:49:4F:7D:83:03:BE:E9:FC:73:A3:FB:33:38:40:0B:3B:6A:56:78:EB:EE:D5:6D:2D:D5:3A:B6:13:97:E7\r\n\ 637 | a=ice-options:trickle\r\n\ 638 | a=msid-semantic:WMS *\r\n\ 639 | m=video 9 UDP/TLS/RTP/SAVPF 120\r\n\ 640 | c=IN IP4 0.0.0.0\r\n 641 | a=recvonly\r\n\ 642 | a=extmap:1 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n\ 643 | a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n\ 644 | a=fmtp:120 max-fs=12288;max-fr=60\r\n\ 645 | a=ice-pwd:c886e2caf2ae397446312930cd1afe51\r\n\ 646 | a=ice-ufrag:f57396c0\r\n\ 647 | a=mid:sdparta_0\r\n\ 648 | a=rtcp-fb:120 nack\r\n\ 649 | a=rtcp-fb:120 nack pli\r\n\ 650 | a=rtcp-fb:120 ccm fir\r\n\ 651 | a=rtcp-fb:120 goog-remb\r\n\ 652 | a=rtcp-mux\r\n\ 653 | a=rtpmap:120 VP8/90000\r\n\ 654 | a=setup:active\r\n\ 655 | a=ssrc:2564157021 cname:{cae1cd32-7433-5b48-8dc8-8e3f8b2f96cd}\r\n\ 656 | a=simulcast: recv rid=foo;bar\r\n\ 657 | a=rid:foo recv\r\n\ 658 | a=rid:bar recv\r\n\ 659 | a=extmap:3/recvonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n"; 660 | let sdp_res = webrtc_sdp::parse_sdp(sdp_str, true); 661 | assert!(sdp_res.is_ok()); 662 | let sdp_opt = sdp_res.ok(); 663 | assert!(sdp_opt.is_some()); 664 | let sdp = sdp_opt.unwrap(); 665 | assert_eq!(sdp.version, 0); 666 | assert_eq!(sdp.media.len(), 1); 667 | } 668 | 669 | #[test] 670 | fn parse_and_serialize_sdp_with_unusual_attributes() { 671 | let sdp_str = "v=0\r\n\ 672 | o=- 0 0 IN IP6 2001:db8::4444\r\n\ 673 | s=-\r\n\ 674 | t=0 0\r\n\ 675 | a=ice-pacing:500\r\n\ 676 | m=video 0 UDP/TLS/RTP/SAVPF 0\r\n\ 677 | b=UNSUPPORTED:12345\r\n\ 678 | c=IN IP6 ::1\r\n\ 679 | a=rtcp:9 IN IP6 2001:db8::8888\r\n\ 680 | a=rtcp-fb:* nack\r\n\ 681 | a=extmap:1/recvonly urn:ietf:params:rtp-hdrext:toffset\r\n\ 682 | a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset\r\n\ 683 | a=extmap:3/sendrecv urn:ietf:params:rtp-hdrext:toffset\r\n\ 684 | a=imageattr:* send [x=330,y=250,sar=[1.1,1.3,1.9],q=0.1] recv [x=800,y=[50,80,30],sar=1.1]\r\n\ 685 | a=imageattr:97 send [x=[480:16:800],y=[100,200,300],par=[1.2-1.3],q=0.6] [x=1080,y=[144:176],sar=[0.5-0.7]] recv *\r\n\ 686 | a=sendrecv\r\n"; 687 | 688 | check_parse_and_serialize(sdp_str); 689 | } 690 | 691 | #[test] 692 | fn serialize_complex_sdp_and_validate_no_empty_lines() { 693 | let sdp = "v=0\r\n 694 | o=- 3836772544440436510 2 IN IP4 127.0.0.1\r\n 695 | s=-\r\n 696 | t=0 0\r\n 697 | a=group:BUNDLE audio video\r\n 698 | a=msid-semantic: WMS HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n 699 | m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 126\r\n 700 | c=IN IP4 0.0.0.0\r\n 701 | a=rtcp:9 IN IP4 0.0.0.0\r\n 702 | a=ice-ufrag:A4by\r\n 703 | a=ice-pwd:Gfvb2rbYMiW0dZz8ZkEsXICs\r\n 704 | a=fingerprint:sha-256 15:B0:92:1F:C7:40:EE:22:A6:AF:26:EF:EA:FF:37:1D:B3:EF:11:0B:8B:73:4F:01:7D:C9:AE:26:4F:87:E0:95\r\n 705 | a=setup:actpass\r\n 706 | a=mid:audio\r\n 707 | a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n 708 | a=sendrecv\r\n 709 | a=rtcp-mux\r\n 710 | a=rtcp-mux-only\r\n 711 | a=rtpmap:111 opus/48000/2\r\n 712 | a=rtcp-fb:111 transport-cc\r\n 713 | a=fmtp:111 minptime=10;useinbandfec=1\r\n 714 | a=rtpmap:103 ISAC/16000\r\n 715 | a=rtpmap:104 ISAC/32000\r\n 716 | a=rtpmap:9 G722/8000\r\n 717 | a=rtpmap:0 PCMU/8000\r\n 718 | a=rtpmap:8 PCMA/8000\r\n 719 | a=rtpmap:106 CN/32000\r\n 720 | a=rtpmap:105 CN/16000\r\n 721 | a=rtpmap:13 CN/8000\r\n 722 | a=rtpmap:126 telephone-event/8000\r\n 723 | a=ssrc:162559313 cname:qPTZ+BI+42mgbOi+\r\n 724 | a=ssrc:162559313 msid:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP f6188af5-d8d6-462c-9c75-f12bc41fe322\r\n 725 | a=ssrc:162559313 mslabel:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n 726 | a=ssrc:162559313 label:f6188af5-d8d6-462c-9c75-f12bc41fe322\r\n 727 | m=video 9 UDP/TLS/RTP/SAVPF 100 101 107 116 117 96 97 99 98\r\n 728 | c=IN IP4 0.0.0.0\r\n 729 | a=rtcp:9 IN IP4 0.0.0.0\r\n 730 | a=ice-ufrag:A4by\r\n 731 | a=ice-pwd:Gfvb2rbYMiW0dZz8ZkEsXICs\r\n 732 | a=fingerprint:sha-256 15:B0:92:1F:C7:40:EE:22:A6:AF:26:EF:EA:FF:37:1D:B3:EF:11:0B:8B:73:4F:01:7D:C9:AE:26:4F:87:E0:95\r\n 733 | a=setup:actpass\r\n 734 | a=mid:video\r\n 735 | a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n 736 | a=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n 737 | a=extmap:4 urn:3gpp:video-orientation\r\n 738 | a=extmap:5 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n 739 | a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\n 740 | a=sendrecv\r\n 741 | a=rtcp-mux\r\n 742 | a=rtcp-rsize\r\n 743 | a=rtpmap:100 VP8/90000\r\n 744 | a=rtcp-fb:100 ccm fir\r\n 745 | a=rtcp-fb:100 nack\r\n 746 | a=rtcp-fb:100 nack pli\r\n 747 | a=rtcp-fb:100 goog-remb\r\n 748 | a=rtcp-fb:100 transport-cc\r\n 749 | a=rtpmap:101 VP9/90000\r\n 750 | a=rtcp-fb:101 ccm fir\r\n 751 | a=rtcp-fb:101 nack\r\n 752 | a=rtcp-fb:101 nack pli\r\n 753 | a=rtcp-fb:101 goog-remb\r\n 754 | a=rtcp-fb:101 transport-cc\r\n 755 | a=rtpmap:107 H264/90000\r\n 756 | a=rtcp-fb:107 ccm fir\r\n 757 | a=rtcp-fb:107 nack\r\n 758 | a=rtcp-fb:107 nack pli\r\n 759 | a=rtcp-fb:107 goog-remb\r\n 760 | a=rtcp-fb:107 transport-cc\r\n 761 | a=fmtp:107 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\n 762 | a=rtpmap:116 red/90000\r\n 763 | a=rtpmap:117 ulpfec/90000\r\n 764 | a=rtpmap:96 rtx/90000\r\n 765 | a=fmtp:96 apt=100\r\n 766 | a=rtpmap:97 rtx/90000\r\n 767 | a=fmtp:97 apt=101\r\n 768 | a=rtpmap:99 rtx/90000\r\n 769 | a=fmtp:99 apt=107\r\n 770 | a=rtpmap:98 rtx/90000\r\n 771 | a=fmtp:98 apt=116\r\n 772 | a=ssrc-group:FID 3156517279 2673335628\r\n 773 | a=ssrc:3156517279 cname:qPTZ+BI+42mgbOi+\r\n 774 | a=ssrc:3156517279 msid:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP b6ec5178-c611-403f-bbec-3833ed547c09\r\n 775 | a=ssrc:3156517279 mslabel:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n 776 | a=ssrc:3156517279 label:b6ec5178-c611-403f-bbec-3833ed547c09\r\n 777 | a=ssrc:2673335628 cname:qPTZ+BI+42mgbOi+\r\n 778 | a=ssrc:2673335628 msid:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP b6ec5178-c611-403f-bbec-3833ed547c09\r\n 779 | a=ssrc:2673335628 mslabel:HWpbmTmXleVSnlssQd80bPuw9cxQFroDkkBP\r\n 780 | a=ssrc:2673335628 label:b6ec5178-c611-403f-bbec-3833ed547c09\r\n"; 781 | let parsed_sdp = webrtc_sdp::parse_sdp(sdp, true).unwrap(); 782 | assert!(!parsed_sdp.to_string().contains("\r\n\r\n")); 783 | } 784 | 785 | #[test] 786 | fn serialize_av1_sdp_with_default_parameters() { 787 | // Checks each parameter in turn to ensure that the serialized SDP contains 788 | // only the parameter that was set. 789 | let sdp = "v=0\r\n 790 | o=mozilla...THIS_IS_SDPARTA-55.0a1 983028567300715536 0 IN IP4 0.0.0.0\r\n 791 | s=-\r\n 792 | t=0 0\r\n 793 | a=fingerprint:sha-256 68:42:13:88:B6:C1:7D:18:79:07:8A:C6:DC:28:D6:DC:DD:E3:C9:41:E7:80:A7:FE:02:65:FB:76:A0:CD:58:ED\r\n 794 | a=ice-options:trickle\r\n 795 | a=msid-semantic:WMS *\r\n 796 | m=video 9 UDP/TLS/RTP/SAVPF 120 121 126 97\r\n 797 | c=IN IP4 0.0.0.0\r\n 798 | a=sendrecv\r\n 799 | a=extmap:1 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n 800 | a=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\n 801 | a=extmap:3/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n 802 | a=rtpmap:98 AV1/90000\r\n 803 | a=ice-pwd:4af388405d558b91f5ba6c2c48f161bf\r\n 804 | a=ice-ufrag:ce1ac488\r\n 805 | a=mid:sdparta_0\r\n 806 | a=msid:{fb6d1fa3-d993-f244-a0fe-d9fb99214c23} \r\n 807 | a=rtcp-fb:98 nack\r\n 808 | a=rtcp-fb:98 nack pli\r\n 809 | a=rtcp-fb:98 ccm fir\r\n 810 | a=rtcp-fb:98 goog-remb\r\n 811 | a=rtcp-mux\r\n 812 | a=setup:actpass\r\n 813 | a=ssrc:1649784806 cname:{77067f00-2e8d-8b4c-8992-cfe338f56851}\r\n"; 814 | 815 | let parameters = ["profile", "level-idx", "tier"]; 816 | for param in ¶meters { 817 | // Check that the parameter is not present in the original SDP. 818 | assert!(!sdp.contains(format!("{param}=").as_str())); 819 | // Add the parameter to the SDP and parse it. 820 | let sdp_with_param = format!("{sdp}a=fmtp:98 {param}=0\r\n"); 821 | let sdp_res = webrtc_sdp::parse_sdp(sdp_with_param.as_str(), true); 822 | assert!(sdp_res.is_ok(), "Failed to parse AV1 SDP: {:?}", sdp_res); 823 | let sdp_opt = sdp_res.ok(); 824 | assert!(sdp_opt.is_some()); 825 | let sdp = sdp_opt.unwrap(); 826 | assert_eq!(sdp.version, 0); 827 | assert_eq!(sdp.media.len(), 1); 828 | let reserialized = sdp.to_string(); 829 | match *param { 830 | "profile" => { 831 | assert!(reserialized.contains("profile=0")); 832 | assert!(!reserialized.contains("level-idx=")); 833 | assert!(!reserialized.contains("tier=")); 834 | } 835 | "level-idx" => { 836 | assert!(reserialized.contains("level-idx=0")); 837 | assert!(!reserialized.contains("profile=")); 838 | assert!(!reserialized.contains("tier=")); 839 | } 840 | "tier" => { 841 | assert!(reserialized.contains("tier=0")); 842 | assert!(!reserialized.contains("profile=")); 843 | assert!(!reserialized.contains("level-idx=")); 844 | } 845 | _ => panic!("Unknown parameter: {}", param), 846 | } 847 | } 848 | } 849 | --------------------------------------------------------------------------------