diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 8942b80b4..3c89eed50 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -12,45 +12,82 @@ env: SECRET_RESULTS_SHEET_ID: ${{ secrets.RESULTS_SHEET_ID }} jobs: build_and_test: - name: '${{ matrix.os }}: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }}, force cpprest asio: ${{ matrix.force_cpprest_asio }})' + name: '${{ matrix.os }}: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }}, force cpprest asio: ${{ matrix.force_cpprest_asio }}, dns-sd mode: ${{ matrix.dns_sd_mode}}, enable_authorization: ${{ matrix.enable_authorization }})' runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ubuntu-18.04, macos-latest, windows-latest] - install_mdns: [false] + os: [ubuntu-20.04, windows-2019] + install_mdns: [false, true] use_conan: [true] force_cpprest_asio: [false] + dns_sd_mode: [multicast, unicast] + enable_authorization: [false, true] + exclude: + # install_mdns is only meaningful on Linux + - os: windows-2019 + enable_authorization: false + - os: ubuntu-20.04 + enable_authorization: false + - os: windows-2019 + install_mdns: true + # for now, unicast DNS-SD tests are only implemented on Linux + - os: windows-2019 + dns_sd_mode: unicast + # for now, exclude unicast DNS-SD with mDNSResponder due to + # intermittent *** buffer overflow detected *** in mdnsd + - os: ubuntu-20.04 + install_mdns: true + dns_sd_mode: unicast + enable_authorization: true include: - - install_mdns: true + - os: windows-2022 + install_mdns: false use_conan: true - force_cpprest_asio: false - os: ubuntu-18.04 - - install_mdns: true + force_cpprest_asio: true + dns_sd_mode: multicast + enable_authorization: true + - os: windows-2022 + install_mdns: false + use_conan: true + force_cpprest_asio: true + dns_sd_mode: multicast + enable_authorization: false + - os: ubuntu-22.04 + install_mdns: false use_conan: true force_cpprest_asio: false - os: ubuntu-20.04 - - install_mdns: false + dns_sd_mode: multicast + enable_authorization: true + - os: ubuntu-22.04 + install_mdns: false use_conan: true - force_cpprest_asio: true - os: windows-latest + force_cpprest_asio: false + dns_sd_mode: multicast + enable_authorization: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set environment variables shell: bash run: | + if [[ "${{ matrix.enable_authorization }}" == "true" ]]; then + authorization_mode=auth + else + authorization_mode=noauth + fi + if [[ "${{ runner.os }}" == "Linux" ]]; then if [[ "${{ matrix.install_mdns }}" == "true" ]]; then - echo "BUILD_NAME=${{ matrix.os }}_mdns" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_mdns_${{ matrix.dns_sd_mode }}_$authorization_mode" >> $GITHUB_ENV else - echo "BUILD_NAME=${{ matrix.os }}_avahi" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_avahi_${{ matrix.dns_sd_mode }}_$authorization_mode" >> $GITHUB_ENV fi elif [[ "${{ matrix.force_cpprest_asio }}" == "true" ]]; then - echo "BUILD_NAME=${{ matrix.os }}_asio" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_asio_$authorization_mode" >> $GITHUB_ENV else - echo "BUILD_NAME=${{ matrix.os }}" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_$authorization_mode" >> $GITHUB_ENV fi GITHUB_COMMIT=`echo "${{ github.sha }}" | cut -c1-7` echo "GITHUB_COMMIT=$GITHUB_COMMIT" >> $GITHUB_ENV @@ -58,7 +95,7 @@ jobs: echo "RUNNER_WORKSPACE=${{ runner.workspace }}" >> $GITHUB_ENV - name: install python - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: 3.8 @@ -74,16 +111,20 @@ jobs: mkdir -p gdrive echo "${{ env.SECRET_GOOGLE_CREDENTIALS }}" | openssl base64 -d -A -out gdrive/credentials.json echo "GDRIVE_CREDENTIALS=`pwd`/gdrive/credentials.json" >> $GITHUB_ENV - + - name: install conan if: matrix.use_conan == true run: | - pip install conan - conan config set general.revisions_enabled=1 - + pip install conan~=2.4.1 + + - name: 'ubuntu-14.04: install cmake' + if: matrix.os == 'ubuntu-14.04' + uses: lukka/get-cmake@v3.24.2 + - name: install cmake - uses: lukka/get-cmake@v3.18.3 - + if: matrix.os != 'ubuntu-14.04' + uses: lukka/get-cmake@v3.28.3 + - name: setup bash path working-directory: ${{ env.GITHUB_WORKSPACE }} shell: bash @@ -91,12 +132,12 @@ jobs: # translate GITHUB_WORKSPACE into a bash path from a windows path workspace_dir=`pwd` echo "GITHUB_WORKSPACE_BASH=${workspace_dir}" >> $GITHUB_ENV - + - name: windows setup if: runner.os == 'Windows' run: | # set compiler to cl.exe to avoid building with gcc. - echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DCMAKE_C_COMPILER=cl.exe -DCMAKE_CXX_COMPILER=cl.exe" >> $env:GITHUB_ENV + echo "CMAKE_COMPILER_ARGS=-DCMAKE_C_COMPILER=cl.exe -DCMAKE_CXX_COMPILER=cl.exe" >> $env:GITHUB_ENV # disable unused network interface netsh interface set interface name="vEthernet (nat)" admin=DISABLED # get host IP address @@ -109,16 +150,18 @@ jobs: ).IPv4Address.IPAddress echo "HOST_IP_ADDRESS=$env:hostip" >> $env:GITHUB_ENV ipconfig - # add the CRL Distribution Point to hosts so that it's discoverable when runing the AMWA test suite in mDNS mode + # add the CRL Distribution Point to hosts so that it's discoverable when running the AMWA test suite in mDNS mode # and avoid SSL Error: WINHTTP_CALLBACK_STATUS_FLAG_CERT_REV_FAILED failed to check revocation status. Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip crl.testsuite.nmos.tv`n" + # add the OCSP server to hosts so that it's discoverable when running the AMWA test suite in mDNS mode + Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip ocsp.testsuite.nmos.tv`n" # add nmos-api.local to hosts to workaround mDNS lookups on windows being very slow and causing the AMWA test suite to take 2-3 hours to complete Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-api.local`n" # add nmos-mocks.local to hosts to workaround mDNS lookups on windows being very slow and causing the AMWA test suite IS-04-01 test_05 to fail due to latency messing up the apparent heart beat interval Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-mocks.local`n" # Configure SCHANNEL, e.g. to disable TLS 1.0 and TLS 1.1 reg import ${{ env.GITHUB_WORKSPACE }}/Sandbox/configure_schannel.reg - + - name: windows install bonjour if: runner.os == 'Windows' run: | @@ -126,7 +169,7 @@ jobs: curl -L https://download.info.apple.com/Mac_OS_X/061-8098.20100603.gthyu/BonjourPSSetup.exe -o BonjourPSSetup.exe -q & 7z.exe e BonjourPSSetup.exe Bonjour64.msi -y msiexec /i ${{ env.GITHUB_WORKSPACE }}\Bonjour64.msi /qn /norestart - + - name: mac setup if: runner.os == 'macOS' run: | @@ -137,11 +180,11 @@ jobs: ifconfig echo "CTEST_EXTRA_ARGS=$CTEST_EXTRA_ARGS -E testMdnsResolveAPIs" >> $GITHUB_ENV echo "CTEST_EXPECTED_FAILURES=$CTEST_EXPECTED_FAILURES -R testMdnsResolveAPIs" >> $GITHUB_ENV - # add the CRL Distribution Point to hosts so that it's discoverable when runing the AMWA test suite in mDNS mode - echo "$hostip crl.testsuite.nmos.tv" | sudo tee -a /etc/hosts + # add the CRL Distribution Point and the OCSP server to hosts so that it's discoverable when running the AMWA test suite in mDNS mode + echo -e "$hostip crl.testsuite.nmos.tv\n$hostip ocsp.testsuite.nmos.tv" | sudo tee -a /etc/hosts > /dev/null # testssl.sh needs "timeout" brew install coreutils - + - name: mac docker install # installs docker on a mac runner. Github's documentation states docker is already available so this shouldn't be necessary # can be used to run AWMA test suite but test suite doesn't seem to be able to communicate with nodes running on the host @@ -167,7 +210,7 @@ jobs: echo "DOCKER_TLS_VERIFY=$DOCKER_TLS_VERIFY" >> $GITHUB_ENV echo "DOCKER_HOST=$DOCKER_HOST" >> $GITHUB_ENV echo "DOCKER_CERT_PATH=$DOCKER_CERT_PATH" >> $GITHUB_ENV - + - name: ubuntu setup if: runner.os == 'Linux' run: | @@ -175,21 +218,22 @@ jobs: hostip=$(hostname -I | cut -f1 -d' ') echo "HOST_IP_ADDRESS=$hostip" >> $GITHUB_ENV ip address - # add the CRL Distribution Point to hosts so that it's discoverable when runing the AMWA test suite in mDNS mode - echo "$hostip crl.testsuite.nmos.tv" | sudo tee -a /etc/hosts + # add the CRL Distribution Point and the OCSP server to hosts so that it's discoverable when running the AMWA test suite in mDNS mode + echo -e "$hostip crl.testsuite.nmos.tv\n$hostip ocsp.testsuite.nmos.tv" | sudo tee -a /etc/hosts > /dev/null # re-synchronize the package index sudo apt-get update -q - + - name: ubuntu mdns install if: runner.os == 'Linux' && matrix.install_mdns == true run: | cd ${{ env.GITHUB_WORKSPACE }} - curl https://opensource.apple.com/tarballs/mDNSResponder/mDNSResponder-878.200.35.tar.gz -o mDNSResponder-878.200.35.tar.gz - tar -xzf mDNSResponder-878.200.35.tar.gz - patch -d mDNSResponder-878.200.35/ -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/unicast.patch - patch -d mDNSResponder-878.200.35/ -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/permit-over-long-service-types.patch - patch -d mDNSResponder-878.200.35/ -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/poll-rather-than-select.patch - cd mDNSResponder-878.200.35/mDNSPosix + mkdir mDNSResponder + cd mDNSResponder + curl -L https://github.com/apple-oss-distributions/mDNSResponder/archive/mDNSResponder-878.200.35.tar.gz -s | tar -xvzf - --strip-components=1 > /dev/null + patch -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/unicast.patch + patch -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/permit-over-long-service-types.patch + patch -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/poll-rather-than-select.patch + cd mDNSPosix make os=linux && sudo make os=linux install # install Name Service Cache Daemon to speed up repeated mDNS name discovery sudo apt-get install -f nscd @@ -198,7 +242,9 @@ jobs: mkdir -p /var/run/nscd nscd fi - + # force dependency on mDNSResponder + echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DNMOS_CPP_USE_AVAHI:BOOL=\"0\"" >> $GITHUB_ENV + - name: ubuntu non-conan setup if: runner.os == 'Linux' && matrix.use_conan == false run: | @@ -212,23 +258,21 @@ jobs: libboost-filesystem-dev \ openssl \ libssl-dev - + cd ${{ env.RUNNER_WORKSPACE }} - git clone --recurse-submodules --branch v2.10.16 https://github.com/Microsoft/cpprestsdk + git clone --recurse-submodules --branch v2.10.19 https://github.com/Microsoft/cpprestsdk cd cpprestsdk/Release mkdir build cd build cmake .. -DCMAKE_BUILD_TYPE:STRING="Release" -DWERROR:BOOL="0" -DBUILD_SAMPLES:BOOL="0" -DBUILD_TESTS:BOOL="0" make -j 2 && sudo make install - - echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DWEBSOCKETPP_INCLUDE_DIR:PATH=\"${{ env.RUNNER_WORKSPACE }}/cpprestsdk/Release/libs/websocketpp\"" >> $GITHUB_ENV - - - name: disable conan - if: matrix.use_conan == false - shell: bash - run: | - echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DUSE_CONAN:BOOL=\"0\"" >> $GITHUB_ENV - + + echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }}" \ + "-DWEBSOCKETPP_INCLUDE_DIR:PATH=\"${{ env.RUNNER_WORKSPACE }}/cpprestsdk/Release/libs/websocketpp\"" \ + "-DNMOS_CPP_USE_SUPPLIED_JSON_SCHEMA_VALIDATOR:BOOL=\"1\"" \ + "-DNMOS_CPP_USE_SUPPLIED_JWT_CPP:BOOL=\"1\"" \ + >> $GITHUB_ENV + - name: ubuntu avahi setup if: runner.os == 'Linux' && matrix.install_mdns == false run: | @@ -240,45 +284,98 @@ jobs: sudo systemctl restart avahi-daemon # install Name Service Cache Daemon to speed up repeated mDNS name discovery sudo apt-get install -f nscd - + # force dependency on avahi + echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DNMOS_CPP_USE_AVAHI:BOOL=\"1\"" >> $GITHUB_ENV + - name: force cpprest asio - if: matrix.force_cpprest_asio == true + if: matrix.force_cpprest_asio == true && matrix.use_conan == true shell: bash run: | - echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DNMOS_CPP_CONAN_OPTIONS:STRING=\"cpprestsdk:http_client_impl=asio;cpprestsdk:http_listener_impl=asio\"" >> $GITHUB_ENV - - - uses: ilammy/msvc-dev-cmd@v1 + echo "CONAN_INSTALL_EXTRA_ARGS=--options\;cpprestsdk/*:http_client_impl=asio\;--options\;cpprestsdk/*:http_listener_impl=asio" >> $GITHUB_ENV + + - name: enable conan + if: matrix.use_conan == true + shell: bash + run: | + echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }}" \ + "-DCMAKE_PROJECT_TOP_LEVEL_INCLUDES:STRING=\"third_party/cmake/conan_provider.cmake\"" \ + "-DCONAN_INSTALL_ARGS:STRING=\"--build=missing\;${{ env.CONAN_INSTALL_EXTRA_ARGS }}\;--lockfile-out=conan.lock\"" \ + >> $GITHUB_ENV + cat $GITHUB_ENV + + - name: setup developer command prompt for Microsoft Visual C++ + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + - name: build - uses: lukka/run-cmake@v2.0 + uses: lukka/run-cmake@v3.4 with: cmakeListsOrSettingsJson: CMakeListsTxtAdvanced cmakeListsTxtPath: '${{ env.GITHUB_WORKSPACE }}/Development/CMakeLists.txt' buildDirectory: '${{ env.RUNNER_WORKSPACE }}/build/' - cmakeAppendedArgs: '-GNinja -DCMAKE_BUILD_TYPE=Release ${{ env.CMAKE_EXTRA_ARGS }}' - - + cmakeAppendedArgs: '-GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="${{ env.RUNNER_WORKSPACE }}/install" ${{ env.CMAKE_COMPILER_ARGS }} ${{ env.CMAKE_EXTRA_ARGS }}' + + - name: dump conan lockfile + if: matrix.use_conan == true + run: | + cat ${{ env.RUNNER_WORKSPACE }}/build/conan.lock + - name: unit test run: | cd ${{ env.RUNNER_WORKSPACE }}/build/ ctest --output-on-failure ${{ env.CTEST_EXTRA_ARGS }} - + - name: expected unit test failures if: env.CTEST_EXPECTED_FAILURES != null continue-on-error: true run: | cd ${{ env.RUNNER_WORKSPACE }}/build/ ctest --output-on-failure ${{ env.CTEST_EXPECTED_FAILURES }} - - + + - name: install + uses: lukka/run-cmake@v3.4 + with: + cmakeListsOrSettingsJson: CMakeListsTxtAdvanced + cmakeListsTxtPath: '${{ env.GITHUB_WORKSPACE }}/Development/CMakeLists.txt' + buildDirectory: '${{ env.RUNNER_WORKSPACE }}/build/' + buildWithCMakeArgs: '--target install' + + - name: set install test environment variable + shell: bash + run: | + # replace backslashes with forward slashes on Windows + echo "CMAKE_WORKSPACE=${RUNNER_WORKSPACE//\\/\/}" >> $GITHUB_ENV + + - name: install test + if: runner.os != 'macOS' + uses: lukka/run-cmake@v3.4 + with: + cmakeListsOrSettingsJson: CMakeListsTxtAdvanced + cmakeListsTxtPath: '${{ env.GITHUB_WORKSPACE }}/Sandbox/my-nmos-node/CMakeLists.txt' + buildDirectory: '${{ env.RUNNER_WORKSPACE }}/build-my-nmos-node/' + cmakeAppendedArgs: '-GNinja + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_FIND_PACKAGE_PREFER_CONFIG="1" + -DCMAKE_MODULE_PATH="${{ env.CMAKE_WORKSPACE }}/build/conan" + -DCMAKE_PREFIX_PATH="${{ env.CMAKE_WORKSPACE }}/install" + -DCMAKE_INSTALL_PREFIX="${{ env.CMAKE_WORKSPACE }}/build/conan" + ${{ env.CMAKE_COMPILER_ARGS }}' + + - name: install test log + if: runner.os != 'macOS' + run: | + # dump the log file created in Sandbox/my-nmos-node/CMakeLists.txt + cat ${{ env.RUNNER_WORKSPACE }}/build-my-nmos-node/my-nmos-node_include-release.txt + - name: install wsl if: runner.os == 'Windows' run: | - & curl -L https://aka.ms/wslubuntu2004 -o ubuntu204.appx - Rename-Item .\ubuntu204.appx .\ubuntu204.zip - Expand-Archive .\ubuntu204.zip .\ubuntu204 - cd ubuntu204 - .\ubuntu2004.exe install --root - + & curl -L https://aka.ms/wsl-ubuntu-1804 -o ubuntu-1804.appx + Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip + Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804 + cd ubuntu-1804 + .\ubuntu1804.exe install --root + - name: AMWA test suite shell: bash working-directory: ${{ env.RUNNER_WORKSPACE }} @@ -286,465 +383,195 @@ jobs: | set -x root_dir=`pwd` - + # Install AMWA NMOS Testing Tool git clone https://github.com/AMWA-TV/nmos-testing.git cd nmos-testing - - # Configure the Testing Tool so all APIs are tested with TLS - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\n" > nmostesting/UserConfig.py - + + # Configure the Testing Tool so all APIs are tested with TLS and authorization + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py + # Set the DNS-SD mode + printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py + # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token + if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + hostname=nmos-api.local + else + hostname=api.testsuite.nmos.tv + fi + printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py + + if [[ "${{matrix.enable_authorization}}" == "true" ]]; then + printf 'CONFIG.ENABLE_AUTH = True\n' >> nmostesting/UserConfig.py + else + printf 'CONFIG.ENABLE_AUTH = False\n' >> nmostesting/UserConfig.py + fi + # Download testssl cd testssl - curl -L https://github.com/drwetter/testssl.sh/archive/3.0.2.tar.gz | tar -xvzf - --strip-components=1 + curl -L https://github.com/drwetter/testssl.sh/archive/v3.0.7.tar.gz -s | tar -xvzf - --strip-components=1 > /dev/null cd .. - + # Create output directories mkdir results mkdir badges - + if [[ "${{ env.DOCKER_TEST_SUITE }}" == "true" ]]; then # run test suite in amwa/nmos-testing docker container docker pull amwa/nmos-testing docker run -d --name "nmos_testing" --entrypoint="/usr/bin/tail" -v `pwd`/results:/home/nmos-testing/results amwa/nmos-testing -f /dev/null run_python="docker exec -i nmos_testing python3" - elif [[ "${{ env.VAGRANT_TEST_SUITE }}" == "true" ]]; then + elif [[ "${{ env.VAGRANT_TEST_SUITE }}" == "true" ]]; then # run test suite in vagrant VM cp ${{ env.GITHUB_WORKSPACE_BASH }}/.github/workflows/mac_Vagrantfile ./Vagrantfile vagrant plugin install vagrant-scp vagrant up vagrant ssh -- mkdir results run_python="vagrant ssh -- python3" + elif [[ "${{ runner.os }}" == "Linux" && "${{ matrix.dns_sd_mode }}" == "unicast" ]]; then + # run test suite directly + sudo pip install --upgrade -r requirements.txt + # install SDPoker + npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git + run_python="sudo python" else # run test suite directly pip install -r requirements.txt - - # Install SDPoker - if [[ "${{ matrix.os }}" == "windows-latest" || "$EUID" == "0" ]]; then - npm install -g AMWA-TV/sdpoker - else - sudo npm install -g AMWA-TV/sdpoker - fi + # install SDPoker + npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="python" fi pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt - - if [[ "${{ matrix.os }}" == "windows-latest" ]]; then - certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem - certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem - certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx - certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\rsa.api.testsuite.nmos.tv.cert.chain.pfx - - # RSA - netsh http add sslcert ipport=0.0.0.0:1080 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" - # ECDSA - #netsh http add sslcert ipport=0.0.0.0:1080 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" - - # RSA - netsh http add sslcert ipport=0.0.0.0:8088 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" - # ECDSA - #netsh http add sslcert ipport=0.0.0.0:8088 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" + + if [[ "${{ runner.os }}" == "Windows" ]]; then + # install certificates + certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem + certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem + certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx + certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\rsa.api.testsuite.nmos.tv.cert.chain.pfx + + # RSA + netsh http add sslcert ipport=0.0.0.0:1080 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" + # ECDSA + #netsh http add sslcert ipport=0.0.0.0:1080 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" + + # RSA + netsh http add sslcert ipport=0.0.0.0:8088 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" + # ECDSA + #netsh http add sslcert ipport=0.0.0.0:8088 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" fi - - if [[ "${{ matrix.os }}" == "macos-latest" ]]; then + + if [[ "${{ runner.os }}" == "macOS" ]]; then # force DNS lookups to IPv4 as mDNS lookups on macos seem to wait for the IPv6 lookup to timeout before returning the IPv4 result mv nmostesting/GenericTest.py nmostesting/GenericTest.py.old printf 'import socket\nold_getaddrinfo = socket.getaddrinfo\ndef new_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):\n return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)\nsocket.getaddrinfo = new_getaddrinfo\n' > nmostesting/GenericTest.py cat nmostesting/GenericTest.py.old >> nmostesting/GenericTest.py fi - - if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.install_mdns }}" == "false" ]]; then - # nmos-cpp-node doesn't currently support advertising hostnames to Avahi - avahi-publish -a -R nmos-api.local ${{ env.HOST_IP_ADDRESS }} & - fi - + if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.use_conan }}" == "false" ]]; then # ubuntu 14 non-conan build uses boost 1.54.0 which doesn't support disabling TLS 1.1 mkdir -p ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/nmos-testing-options/ echo "--ignore test_01" > ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/nmos-testing-options/BCP-003-01.txt echo "1" > ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/nmos-testing-options/BCP-003-01_max_disabled.txt fi - - ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/run_nmos_testing.sh "$run_python" ${root_dir}/build/nmos-cpp-node ${root_dir}/build/nmos-cpp-registry results badges ${{ env.HOST_IP_ADDRESS }} "${{ env.GITHUB_COMMIT }}-${{ env.BUILD_NAME }}-" - - if [[ "${{ env.DOCKER_TEST_SUITE }}" == "true" ]]; then - docker stop nmos_testing - docker rm nmos_testing - fi - if [[ "${{ env.VAGRANT_TEST_SUITE }}" == "true" ]]; then - vagrant scp :results/* results/ - vagrant destroy -f - fi - exit 0 - - - name: upload to google sheets - if: github.ref == 'refs/heads/master' && github.event_name == 'push' - working-directory: ${{ env.RUNNER_WORKSPACE }} - shell: bash - run: | - export SHEET=https://docs.google.com/spreadsheets/d/${{ env.SECRET_RESULTS_SHEET_ID }} - python nmos-testing/utilities/run-test-suites/gsheetsImport/resultsImporter.py --credentials ${{ env.GDRIVE_CREDENTIALS }} --sheet "$SHEET" --insert --json nmos-testing/results/${{ env.GITHUB_COMMIT }}-*.json || echo "upload failed" - - - uses: actions/upload-artifact@v1 - with: - name: ${{ env.BUILD_NAME }}_badges - path: ${{ runner.workspace }}/nmos-testing/badges - - - uses: actions/upload-artifact@v1 - with: - name: ${{ env.BUILD_NAME }}_results - path: ${{ runner.workspace }}/nmos-testing/results - - - - build_and_test_ubuntu_14: - name: 'ubuntu-14.04: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }})' - runs-on: ubuntu-latest - container: - image: ubuntu:14.04 - strategy: - fail-fast: false - matrix: - install_mdns: [true] - use_conan: [false] - - steps: - - uses: actions/checkout@v2 - - - name: set environment variables - run: | - echo "BUILD_NAME=ubuntu-14.04_mdns" >> $GITHUB_ENV - GITHUB_COMMIT=`echo "${{ github.sha }}" | cut -c1-7` - echo "GITHUB_COMMIT=$GITHUB_COMMIT" >> $GITHUB_ENV - # github.workspace points to the host path not the docker path, the home directory defaults to the workspace directory - echo "GITHUB_WORKSPACE=`pwd`" >> $GITHUB_ENV - cd .. - echo "RUNNER_WORKSPACE=`pwd`" >> $GITHUB_ENV - - - name: install build tools - run: | - apt-get update -q - apt-get install -y software-properties-common - add-apt-repository ppa:deadsnakes/ppa -y - apt-get --allow-unauthenticated update -q - apt-get --allow-unauthenticated install -y curl g++ git make patch python3.6 python3.6-gdbm bsdmainutils dnsutils unzip - update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.6 3 - ln -s /usr/bin/python3.6 /usr/bin/python - curl -sS https://bootstrap.pypa.io/get-pip.py | python - curl -sS https://nodejs.org/dist/v12.16.2/node-v12.16.2-linux-x64.tar.xz | tar -xJ - echo "`pwd`/node-v12.16.2-linux-x64/bin" >> $GITHUB_PATH - - name: setup google credentials - if: env.SECRET_GOOGLE_CREDENTIALS - shell: bash - working-directory: ${{ env.GITHUB_WORKSPACE }} - run: | - mkdir -p gdrive - echo "${{ env.SECRET_GOOGLE_CREDENTIALS }}" | openssl base64 -d -A -out gdrive/credentials.json - echo "GDRIVE_CREDENTIALS=`pwd`/gdrive/credentials.json" >> $GITHUB_ENV - - - name: install conan - if: matrix.use_conan == true - run: | - pip install conan - conan config set general.revisions_enabled=1 - - - name: install cmake - uses: lukka/get-cmake@v3.18.3 - - - name: setup bash path - working-directory: ${{ env.GITHUB_WORKSPACE }} - shell: bash - run: | - # translate GITHUB_WORKSPACE into a bash path from a windows path - workspace_dir=`pwd` - echo "GITHUB_WORKSPACE_BASH=${workspace_dir}" >> $GITHUB_ENV - - - name: windows setup - if: runner.os == 'Windows' - run: | - # set compiler to cl.exe to avoid building with gcc. - echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DCMAKE_C_COMPILER=cl.exe -DCMAKE_CXX_COMPILER=cl.exe" >> $env:GITHUB_ENV - # disable unused network interface - netsh interface set interface name="vEthernet (nat)" admin=DISABLED - # get host IP address - $env:hostip = ( - Get-NetIPConfiguration | - Where-Object { - $_.IPv4DefaultGateway -ne $null -and - $_.NetAdapter.Status -ne "Disconnected" - } - ).IPv4Address.IPAddress - echo "HOST_IP_ADDRESS=$env:hostip" >> $env:GITHUB_ENV - ipconfig - # add the CRL Distribution Point to hosts so that it's discoverable when runing the AMWA test suite in mDNS mode - # and avoid SSL Error: WINHTTP_CALLBACK_STATUS_FLAG_CERT_REV_FAILED failed to check revocation status. - Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip crl.testsuite.nmos.tv`n" - # add nmos-api.local to hosts to workaround mDNS lookups on windows being very slow and causing the AMWA test suite to take 2-3 hours to complete - Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-api.local`n" - # add nmos-mocks.local to hosts to workaround mDNS lookups on windows being very slow and causing the AMWA test suite IS-04-01 test_05 to fail due to latency messing up the apparent heart beat interval - Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-mocks.local`n" - # Configure SCHANNEL, e.g. to disable TLS 1.0 and TLS 1.1 - reg import ${{ env.GITHUB_WORKSPACE }}/Sandbox/configure_schannel.reg - - - name: windows install bonjour - if: runner.os == 'Windows' - run: | - # download bonjour installer - curl -L https://download.info.apple.com/Mac_OS_X/061-8098.20100603.gthyu/BonjourPSSetup.exe -o BonjourPSSetup.exe -q - & 7z.exe e BonjourPSSetup.exe Bonjour64.msi -y - msiexec /i ${{ env.GITHUB_WORKSPACE }}\Bonjour64.msi /qn /norestart - - - name: mac setup - if: runner.os == 'macOS' - run: | - hostip=$(ipconfig getifaddr en0) - echo "HOST_IP_ADDRESS=$hostip" >> $GITHUB_ENV - active_xcode_version=`xcode-select -p` - echo "SDKROOT=${active_xcode_version}/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk" >> $GITHUB_ENV - ifconfig - echo "CTEST_EXTRA_ARGS=$CTEST_EXTRA_ARGS -E testMdnsResolveAPIs" >> $GITHUB_ENV - echo "CTEST_EXPECTED_FAILURES=$CTEST_EXPECTED_FAILURES -R testMdnsResolveAPIs" >> $GITHUB_ENV - # add the CRL Distribution Point to hosts so that it's discoverable when runing the AMWA test suite in mDNS mode - echo "$hostip crl.testsuite.nmos.tv" | sudo tee -a /etc/hosts - # testssl.sh needs "timeout" - brew install coreutils - - - name: mac docker install - # installs docker on a mac runner. Github's documentation states docker is already available so this shouldn't be necessary - # can be used to run AWMA test suite but test suite doesn't seem to be able to communicate with nodes running on the host - if: false - run: | - brew install docker docker-compose docker-machine xhyve docker-machine-driver-xhyve - sudo chown root:wheel $(brew --prefix)/opt/docker-machine-driver-xhyve/bin/docker-machine-driver-xhyve - sudo chmod u+s $(brew --prefix)/opt/docker-machine-driver-xhyve/bin/docker-machine-driver-xhyve - mkdir -p /Users/runner/.docker/machine/cache/ - # workaround "docker-machine" failing to download boot2docker.iso - curl -Lo ~/.docker/machine/cache/boot2docker.iso https://github.com/boot2docker/boot2docker/releases/download/v19.03.5/boot2docker.iso - i=0 - while ! docker-machine "--github-api-token=${{ secrets.GITHUB_TOKEN }}" create default --driver xhyve; do - docker-machine rm -f default - sleep 1 - $(( i++ )) - if [[ $i -gt 5 ]]; then - exit 1 + if [[ "${{ runner.os }}" == "Linux" ]]; then + if [[ "${{ matrix.install_mdns }}" == "true" ]]; then + echo "Stopping mdnsd" + sudo /etc/init.d/mdns stop + else + echo "Stopping avahi-daemon" + sudo systemctl stop avahi-daemon fi - done - eval $(docker-machine env default) - echo "DOCKER_MACHINE_NAME=$DOCKER_MACHINE_NAME" >> $GITHUB_ENV - echo "DOCKER_TLS_VERIFY=$DOCKER_TLS_VERIFY" >> $GITHUB_ENV - echo "DOCKER_HOST=$DOCKER_HOST" >> $GITHUB_ENV - echo "DOCKER_CERT_PATH=$DOCKER_CERT_PATH" >> $GITHUB_ENV - - - name: ubuntu setup - if: runner.os == 'Linux' - run: | - sudo ip addr flush dev docker0 || echo "remove docker ip failed" - hostip=$(hostname -I | cut -f1 -d' ') - echo "HOST_IP_ADDRESS=$hostip" >> $GITHUB_ENV - ip address - # add the CRL Distribution Point to hosts so that it's discoverable when runing the AMWA test suite in mDNS mode - echo "$hostip crl.testsuite.nmos.tv" | sudo tee -a /etc/hosts - # re-synchronize the package index - sudo apt-get update -q - - - name: ubuntu mdns install - if: runner.os == 'Linux' && matrix.install_mdns == true - run: | - cd ${{ env.GITHUB_WORKSPACE }} - curl https://opensource.apple.com/tarballs/mDNSResponder/mDNSResponder-878.200.35.tar.gz -o mDNSResponder-878.200.35.tar.gz - tar -xzf mDNSResponder-878.200.35.tar.gz - patch -d mDNSResponder-878.200.35/ -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/unicast.patch - patch -d mDNSResponder-878.200.35/ -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/permit-over-long-service-types.patch - patch -d mDNSResponder-878.200.35/ -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/poll-rather-than-select.patch - cd mDNSResponder-878.200.35/mDNSPosix - make os=linux && sudo make os=linux install - # install Name Service Cache Daemon to speed up repeated mDNS name discovery - sudo apt-get install -f nscd - if [ -f /.dockerenv ]; then - # nscd doesn't run automatically under docker - mkdir -p /var/run/nscd - nscd fi - - - name: ubuntu non-conan setup - if: runner.os == 'Linux' && matrix.use_conan == false - run: | - sudo apt-get install -y \ - libboost-chrono-dev \ - libboost-date-time-dev \ - libboost-regex-dev \ - libboost-system-dev \ - libboost-thread-dev \ - libboost-random-dev \ - libboost-filesystem-dev \ - openssl \ - libssl-dev - - cd ${{ env.RUNNER_WORKSPACE }} - git clone --recurse-submodules --branch v2.10.16 https://github.com/Microsoft/cpprestsdk - cd cpprestsdk/Release - mkdir build - cd build - cmake .. -DCMAKE_BUILD_TYPE:STRING="Release" -DWERROR:BOOL="0" -DBUILD_SAMPLES:BOOL="0" -DBUILD_TESTS:BOOL="0" - make -j 2 && sudo make install - - echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DWEBSOCKETPP_INCLUDE_DIR:PATH=\"${{ env.RUNNER_WORKSPACE }}/cpprestsdk/Release/libs/websocketpp\"" >> $GITHUB_ENV - - - name: disable conan - if: matrix.use_conan == false - shell: bash - run: | - echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DUSE_CONAN:BOOL=\"0\"" >> $GITHUB_ENV - - - name: ubuntu avahi setup - if: runner.os == 'Linux' && matrix.install_mdns == false - run: | - sudo apt-get install -f libavahi-compat-libdnssd-dev libnss-mdns avahi-utils - echo "CTEST_EXTRA_ARGS=$CTEST_EXTRA_ARGS -E testMdnsAdvertiseAddress" >> $GITHUB_ENV - echo "CTEST_EXPECTED_FAILURES=$CTEST_EXPECTED_FAILURES -R testMdnsAdvertiseAddress" >> $GITHUB_ENV - # make avahi only respond on the "eth0" interface - sudo sed -i 's/#*allow-interfaces=.*/allow-interfaces=eth0/g' /etc/avahi/avahi-daemon.conf - sudo systemctl restart avahi-daemon - # install Name Service Cache Daemon to speed up repeated mDNS name discovery - sudo apt-get install -f nscd - - - name: force cpprest asio - if: matrix.force_cpprest_asio == true - shell: bash - run: | - echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DNMOS_CPP_CONAN_OPTIONS:STRING=\"cpprestsdk:http_client_impl=asio;cpprestsdk:http_listener_impl=asio\"" >> $GITHUB_ENV - - - uses: ilammy/msvc-dev-cmd@v1 - - name: build - uses: lukka/run-cmake@v2.0 - with: - cmakeListsOrSettingsJson: CMakeListsTxtAdvanced - cmakeListsTxtPath: '${{ env.GITHUB_WORKSPACE }}/Development/CMakeLists.txt' - buildDirectory: '${{ env.RUNNER_WORKSPACE }}/build/' - cmakeAppendedArgs: '-GNinja -DCMAKE_BUILD_TYPE=Release ${{ env.CMAKE_EXTRA_ARGS }}' - - - - name: unit test - run: | - cd ${{ env.RUNNER_WORKSPACE }}/build/ - ctest --output-on-failure ${{ env.CTEST_EXTRA_ARGS }} - - - name: expected unit test failures - if: env.CTEST_EXPECTED_FAILURES != null - continue-on-error: true - run: | - cd ${{ env.RUNNER_WORKSPACE }}/build/ - ctest --output-on-failure ${{ env.CTEST_EXPECTED_FAILURES }} - - - - name: install wsl - if: runner.os == 'Windows' - run: | - & curl -L https://aka.ms/wslubuntu2004 -o ubuntu204.appx - Rename-Item .\ubuntu204.appx .\ubuntu204.zip - Expand-Archive .\ubuntu204.zip .\ubuntu204 - cd ubuntu204 - .\ubuntu2004.exe install --root - - - name: AMWA test suite - shell: bash - working-directory: ${{ env.RUNNER_WORKSPACE }} - run: - | - set -x - root_dir=`pwd` - - # Install AMWA NMOS Testing Tool - git clone https://github.com/AMWA-TV/nmos-testing.git - cd nmos-testing - - # Configure the Testing Tool so all APIs are tested with TLS - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\n" > nmostesting/UserConfig.py - - # Download testssl - cd testssl - curl -L https://github.com/drwetter/testssl.sh/archive/3.0.2.tar.gz | tar -xvzf - --strip-components=1 - cd .. - - # Create output directories - mkdir results - mkdir badges - - if [[ "${{ env.DOCKER_TEST_SUITE }}" == "true" ]]; then - # run test suite in amwa/nmos-testing docker container - docker pull amwa/nmos-testing - docker run -d --name "nmos_testing" --entrypoint="/usr/bin/tail" -v `pwd`/results:/home/nmos-testing/results amwa/nmos-testing -f /dev/null - run_python="docker exec -i nmos_testing python3" - elif [[ "${{ env.VAGRANT_TEST_SUITE }}" == "true" ]]; then - # run test suite in vagrant VM - cp ${{ env.GITHUB_WORKSPACE_BASH }}/.github/workflows/mac_Vagrantfile ./Vagrantfile - vagrant plugin install vagrant-scp - vagrant up - vagrant ssh -- mkdir results - run_python="vagrant ssh -- python3" + + if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + domain=local else - # run test suite directly - pip install -r requirements.txt - - # Install SDPoker - if [[ "${{ matrix.os }}" == "windows-latest" || "$EUID" == "0" ]]; then - npm install -g AMWA-TV/sdpoker + domain=testsuite.nmos.tv + if [[ "${{ runner.os }}" == "Linux" ]]; then + # add host names + echo -e "${{ env.HOST_IP_ADDRESS }} api.$domain\n${{ env.HOST_IP_ADDRESS }} mocks.$domain" | sudo tee -a /etc/hosts > /dev/null + # force testing tool to cache specification repos before changing the resolver configuration + $run_python nmos-test.py suite IS-04-01 --selection auto --host ${{ env.HOST_IP_ADDRESS }} --port 444 --version v1.3 || true + # change the resolver configuration to use only the testing tool's mock DNS server + # and instead configure the mock DNS server to use an upstream DNS server + # as unicast DNS-SD test results are inconsistent if other servers are also configured + dns_upstream_ip=$(cat /etc/resolv.conf | grep ^nameserver | tr -s [:space:] ' ' | cut -f2 -d ' ' -s) + if [[ ! -z "$dns_upstream_ip" ]]; then + printf 'CONFIG.DNS_UPSTREAM_IP = "'${dns_upstream_ip}'"\n' >> nmostesting/UserConfig.py + fi + sudo cp /etc/resolv.conf /etc/resolv.conf.bak + echo -e "nameserver ${{ env.HOST_IP_ADDRESS }}" | sudo tee /etc/resolv.conf > /dev/null else - sudo npm install -g AMWA-TV/sdpoker + echo "Unicast DNS-SD testing not yet supported on ${{ runner.os }}" && false fi - run_python="python" fi - pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt - - if [[ "${{ matrix.os }}" == "windows-latest" ]]; then - certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem - certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem - certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx - certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\rsa.api.testsuite.nmos.tv.cert.chain.pfx - - # RSA - netsh http add sslcert ipport=0.0.0.0:1080 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" - # ECDSA - #netsh http add sslcert ipport=0.0.0.0:1080 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" - - # RSA - netsh http add sslcert ipport=0.0.0.0:8088 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" - # ECDSA - #netsh http add sslcert ipport=0.0.0.0:8088 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" - fi - - if [[ "${{ matrix.os }}" == "macos-latest" ]]; then - # force DNS lookups to IPv4 as mDNS lookups on macos seem to wait for the IPv6 lookup to timeout before returning the IPv4 result - mv nmostesting/GenericTest.py nmostesting/GenericTest.py.old - printf 'import socket\nold_getaddrinfo = socket.getaddrinfo\ndef new_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):\n return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)\nsocket.getaddrinfo = new_getaddrinfo\n' > nmostesting/GenericTest.py - cat nmostesting/GenericTest.py.old >> nmostesting/GenericTest.py + + if [[ "${{ runner.os }}" == "Linux" ]]; then + if [[ "${{ matrix.install_mdns }}" == "true" ]]; then + echo "Restarting mdnsd" + sudo /etc/init.d/mdns start + #sudo /usr/sbin/mdnsd -debug & + sleep 2 + + dns-sd -V + else + echo "Restarting avahi-daemon" + sudo systemctl start avahi-daemon + sleep 2 + + ps -e | grep avahi-daemon + avahi-daemon -V + fi fi - + if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.install_mdns }}" == "false" ]]; then # nmos-cpp-node doesn't currently support advertising hostnames to Avahi avahi-publish -a -R nmos-api.local ${{ env.HOST_IP_ADDRESS }} & fi - - if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.use_conan }}" == "false" ]]; then - # ubuntu 14 non-conan build uses boost 1.54.0 which doesn't support disabling TLS 1.1 - mkdir -p ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/nmos-testing-options/ - echo "--ignore test_01" > ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/nmos-testing-options/BCP-003-01.txt - echo "1" > ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/nmos-testing-options/BCP-003-01_max_disabled.txt + + ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/run_nmos_testing.sh "$run_python" ${domain} ${root_dir}/build/nmos-cpp-node ${root_dir}/build/nmos-cpp-registry results badges $GITHUB_STEP_SUMMARY ${{ env.HOST_IP_ADDRESS }} "${{ env.GITHUB_COMMIT }}-${{ env.BUILD_NAME }}-" + + if [[ "${{ runner.os }}" == "Linux" ]]; then + if [[ "${{ matrix.install_mdns }}" == "true" ]]; then + ps -e | grep mdnsd || true + else + ps -e | grep avahi-daemon || true + fi + fi + + if [[ "${{ matrix.dns_sd_mode }}" == "unicast" ]]; then + if [[ "${{ runner.os }}" == "Linux" ]]; then + # restore DNS Server + if [[ "${{ matrix.install_mdns }}" == "true" ]]; then + echo "Stopping mdnsd" + sudo /etc/init.d/mdns stop + else + echo "Stopping avahi-daemon" + sudo systemctl stop avahi-daemon + fi + cat /etc/resolv.conf.bak | sudo tee /etc/resolv.conf > /dev/null + if [[ "${{ matrix.install_mdns }}" == "true" ]]; then + echo "Restarting mdnsd" + sudo /etc/init.d/mdns start + else + echo "Restarting avahi-daemon" + sudo systemctl start avahi-daemon + fi + fi fi - - ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/run_nmos_testing.sh "$run_python" ${root_dir}/build/nmos-cpp-node ${root_dir}/build/nmos-cpp-registry results badges ${{ env.HOST_IP_ADDRESS }} "${{ env.GITHUB_COMMIT }}-${{ env.BUILD_NAME }}-" - + if [[ "${{ env.DOCKER_TEST_SUITE }}" == "true" ]]; then docker stop nmos_testing docker rm nmos_testing fi - if [[ "${{ env.VAGRANT_TEST_SUITE }}" == "true" ]]; then + if [[ "${{ env.VAGRANT_TEST_SUITE }}" == "true" ]]; then vagrant scp :results/* results/ vagrant destroy -f fi + exit 0 - + - name: upload to google sheets if: github.ref == 'refs/heads/master' && github.event_name == 'push' working-directory: ${{ env.RUNNER_WORKSPACE }} @@ -752,25 +579,24 @@ jobs: run: | export SHEET=https://docs.google.com/spreadsheets/d/${{ env.SECRET_RESULTS_SHEET_ID }} python nmos-testing/utilities/run-test-suites/gsheetsImport/resultsImporter.py --credentials ${{ env.GDRIVE_CREDENTIALS }} --sheet "$SHEET" --insert --json nmos-testing/results/${{ env.GITHUB_COMMIT }}-*.json || echo "upload failed" - - - uses: actions/upload-artifact@v1 + + - uses: actions/upload-artifact@v4 with: name: ${{ env.BUILD_NAME }}_badges path: ${{ runner.workspace }}/nmos-testing/badges - - - uses: actions/upload-artifact@v1 + + - uses: actions/upload-artifact@v4 with: name: ${{ env.BUILD_NAME }}_results path: ${{ runner.workspace }}/nmos-testing/results - make_badges: if: github.ref == 'refs/heads/master' && github.event_name == 'push' - needs: [build_and_test, build_and_test_ubuntu_14] - runs-on: ubuntu-latest + needs: [build_and_test] + runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set environment variables shell: bash @@ -780,14 +606,14 @@ jobs: echo "GITHUB_WORKSPACE=${{ github.workspace }}" >> $GITHUB_ENV echo "RUNNER_WORKSPACE=${{ runner.workspace }}" >> $GITHUB_ENV - - uses: actions/download-artifact@v2.0 + - uses: actions/download-artifact@v4 with: path: ${{ runner.workspace }}/artifacts - name: make badges run: | - # combine badges from all builds, exclude macos-latest - ${{ github.workspace }}/Sandbox/make_badges.sh ${{ github.workspace }} ${{ runner.workspace }}/artifacts macos-latest + # combine badges from all builds, exclude macos-13 + ${{ github.workspace }}/Sandbox/make_badges.sh ${{ github.workspace }} ${{ runner.workspace }}/artifacts macos-13_auth macos-13_noauth # force push to github onto an orphan 'badges' branch cd ${{ github.workspace }} diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 7607ef828..89396a152 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -1,11 +1,11 @@ - name: install wsl if: runner.os == 'Windows' run: | - & curl -L https://aka.ms/wslubuntu2004 -o ubuntu204.appx - Rename-Item .\ubuntu204.appx .\ubuntu204.zip - Expand-Archive .\ubuntu204.zip .\ubuntu204 - cd ubuntu204 - .\ubuntu2004.exe install --root + & curl -L https://aka.ms/wsl-ubuntu-1804 -o ubuntu-1804.appx + Rename-Item .\ubuntu-1804.appx .\ubuntu-1804.zip + Expand-Archive .\ubuntu-1804.zip .\ubuntu-1804 + cd ubuntu-1804 + .\ubuntu1804.exe install --root - name: AMWA test suite shell: bash @@ -19,12 +19,27 @@ git clone https://github.com/AMWA-TV/nmos-testing.git cd nmos-testing - # Configure the Testing Tool so all APIs are tested with TLS - printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\n" > nmostesting/UserConfig.py + # Configure the Testing Tool so all APIs are tested with TLS and authorization + printf "from . import Config as CONFIG\nCONFIG.ENABLE_HTTPS = True\nCONFIG.MOCK_SERVICES_WARM_UP_DELAY = 30\nCONFIG.HTTP_TIMEOUT = 2\n" > nmostesting/UserConfig.py + # Set the DNS-SD mode + printf 'CONFIG.DNS_SD_MODE = "'${{ matrix.dns_sd_mode }}'"\n' >> nmostesting/UserConfig.py + # Set the client JWKS_URI for mock Authorization Server to obtain the client JSON Web Key Set (public keys) to verify the client_assertion, when the client is requesting the access token + if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + hostname=nmos-api.local + else + hostname=api.testsuite.nmos.tv + fi + printf 'CONFIG.JWKS_URI = "https://'${hostname}':1080/x-authorization/jwks"\n' >> nmostesting/UserConfig.py + if [[ "${{matrix.enable_authorization}}" == "true" ]]; then + printf 'CONFIG.ENABLE_AUTH = True\n' >> nmostesting/UserConfig.py + else + printf 'CONFIG.ENABLE_AUTH = False\n' >> nmostesting/UserConfig.py + fi + # Download testssl cd testssl - curl -L https://github.com/drwetter/testssl.sh/archive/3.0.2.tar.gz | tar -xvzf - --strip-components=1 + curl -L https://github.com/drwetter/testssl.sh/archive/v3.0.7.tar.gz -s | tar -xvzf - --strip-components=1 > /dev/null cd .. # Create output directories @@ -36,56 +51,53 @@ docker pull amwa/nmos-testing docker run -d --name "nmos_testing" --entrypoint="/usr/bin/tail" -v `pwd`/results:/home/nmos-testing/results amwa/nmos-testing -f /dev/null run_python="docker exec -i nmos_testing python3" - elif [[ "${{ env.VAGRANT_TEST_SUITE }}" == "true" ]]; then + elif [[ "${{ env.VAGRANT_TEST_SUITE }}" == "true" ]]; then # run test suite in vagrant VM cp ${{ env.GITHUB_WORKSPACE_BASH }}/.github/workflows/mac_Vagrantfile ./Vagrantfile vagrant plugin install vagrant-scp vagrant up vagrant ssh -- mkdir results run_python="vagrant ssh -- python3" + elif [[ "${{ runner.os }}" == "Linux" && "${{ matrix.dns_sd_mode }}" == "unicast" ]]; then + # run test suite directly + sudo pip install --upgrade -r requirements.txt + # install SDPoker + npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git + run_python="sudo python" else # run test suite directly pip install -r requirements.txt - - # Install SDPoker - if [[ "${{ matrix.os }}" == "windows-latest" || "$EUID" == "0" ]]; then - npm install -g AMWA-TV/sdpoker - else - sudo npm install -g AMWA-TV/sdpoker - fi + # install SDPoker + npm install -g git+https://git@github.com/AMWA-TV/sdpoker.git run_python="python" fi pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt - - if [[ "${{ matrix.os }}" == "windows-latest" ]]; then - certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem - certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem - certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx - certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\rsa.api.testsuite.nmos.tv.cert.chain.pfx - - # RSA - netsh http add sslcert ipport=0.0.0.0:1080 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" - # ECDSA - #netsh http add sslcert ipport=0.0.0.0:1080 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" - - # RSA - netsh http add sslcert ipport=0.0.0.0:8088 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" - # ECDSA - #netsh http add sslcert ipport=0.0.0.0:8088 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" + + if [[ "${{ runner.os }}" == "Windows" ]]; then + # install certificates + certutil -enterprise -addstore -user root test_data\\BCP00301\\ca\\certs\\ca.cert.pem + certutil -enterprise -addstore -user ca test_data\\BCP00301\\ca\\intermediate\\certs\\intermediate.cert.pem + certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\ecdsa.api.testsuite.nmos.tv.cert.chain.pfx + certutil -importpfx -enterprise test_data\\BCP00301\\ca\\intermediate\\certs\\rsa.api.testsuite.nmos.tv.cert.chain.pfx + + # RSA + netsh http add sslcert ipport=0.0.0.0:1080 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" + # ECDSA + #netsh http add sslcert ipport=0.0.0.0:1080 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" + + # RSA + netsh http add sslcert ipport=0.0.0.0:8088 certhash=021d50df2177c07095485184206ee2297e50b65c appid="{00000000-0000-0000-0000-000000000000}" + # ECDSA + #netsh http add sslcert ipport=0.0.0.0:8088 certhash=875eca592c49120254b32bb8bed90ac3679015a5 appid="{00000000-0000-0000-0000-000000000000}" fi - if [[ "${{ matrix.os }}" == "macos-latest" ]]; then + if [[ "${{ runner.os }}" == "macOS" ]]; then # force DNS lookups to IPv4 as mDNS lookups on macos seem to wait for the IPv6 lookup to timeout before returning the IPv4 result mv nmostesting/GenericTest.py nmostesting/GenericTest.py.old printf 'import socket\nold_getaddrinfo = socket.getaddrinfo\ndef new_getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):\n return old_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)\nsocket.getaddrinfo = new_getaddrinfo\n' > nmostesting/GenericTest.py cat nmostesting/GenericTest.py.old >> nmostesting/GenericTest.py fi - if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.install_mdns }}" == "false" ]]; then - # nmos-cpp-node doesn't currently support advertising hostnames to Avahi - avahi-publish -a -R nmos-api.local ${{ env.HOST_IP_ADDRESS }} & - fi - if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.use_conan }}" == "false" ]]; then # ubuntu 14 non-conan build uses boost 1.54.0 which doesn't support disabling TLS 1.1 mkdir -p ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/nmos-testing-options/ @@ -93,14 +105,100 @@ echo "1" > ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/nmos-testing-options/BCP-003-01_max_disabled.txt fi - ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/run_nmos_testing.sh "$run_python" ${root_dir}/build/nmos-cpp-node ${root_dir}/build/nmos-cpp-registry results badges ${{ env.HOST_IP_ADDRESS }} "${{ env.GITHUB_COMMIT }}-${{ env.BUILD_NAME }}-" + if [[ "${{ runner.os }}" == "Linux" ]]; then + if [[ "${{ matrix.install_mdns }}" == "true" ]]; then + echo "Stopping mdnsd" + sudo /etc/init.d/mdns stop + else + echo "Stopping avahi-daemon" + sudo systemctl stop avahi-daemon + fi + fi + + if [[ "${{ matrix.dns_sd_mode }}" == "multicast" ]]; then + domain=local + else + domain=testsuite.nmos.tv + if [[ "${{ runner.os }}" == "Linux" ]]; then + # add host names + echo -e "${{ env.HOST_IP_ADDRESS }} api.$domain\n${{ env.HOST_IP_ADDRESS }} mocks.$domain" | sudo tee -a /etc/hosts > /dev/null + # force testing tool to cache specification repos before changing the resolver configuration + $run_python nmos-test.py suite IS-04-01 --selection auto --host ${{ env.HOST_IP_ADDRESS }} --port 444 --version v1.3 || true + # change the resolver configuration to use only the testing tool's mock DNS server + # and instead configure the mock DNS server to use an upstream DNS server + # as unicast DNS-SD test results are inconsistent if other servers are also configured + dns_upstream_ip=$(cat /etc/resolv.conf | grep ^nameserver | tr -s [:space:] ' ' | cut -f2 -d ' ' -s) + if [[ ! -z "$dns_upstream_ip" ]]; then + printf 'CONFIG.DNS_UPSTREAM_IP = "'${dns_upstream_ip}'"\n' >> nmostesting/UserConfig.py + fi + sudo cp /etc/resolv.conf /etc/resolv.conf.bak + echo -e "nameserver ${{ env.HOST_IP_ADDRESS }}" | sudo tee /etc/resolv.conf > /dev/null + else + echo "Unicast DNS-SD testing not yet supported on ${{ runner.os }}" && false + fi + fi + + if [[ "${{ runner.os }}" == "Linux" ]]; then + if [[ "${{ matrix.install_mdns }}" == "true" ]]; then + echo "Restarting mdnsd" + sudo /etc/init.d/mdns start + #sudo /usr/sbin/mdnsd -debug & + sleep 2 + + dns-sd -V + else + echo "Restarting avahi-daemon" + sudo systemctl start avahi-daemon + sleep 2 + + ps -e | grep avahi-daemon + avahi-daemon -V + fi + fi + + if [[ "${{ runner.os }}" == "Linux" && "${{ matrix.install_mdns }}" == "false" ]]; then + # nmos-cpp-node doesn't currently support advertising hostnames to Avahi + avahi-publish -a -R nmos-api.local ${{ env.HOST_IP_ADDRESS }} & + fi + + ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/run_nmos_testing.sh "$run_python" ${domain} ${root_dir}/build/nmos-cpp-node ${root_dir}/build/nmos-cpp-registry results badges $GITHUB_STEP_SUMMARY ${{ env.HOST_IP_ADDRESS }} "${{ env.GITHUB_COMMIT }}-${{ env.BUILD_NAME }}-" + + if [[ "${{ runner.os }}" == "Linux" ]]; then + if [[ "${{ matrix.install_mdns }}" == "true" ]]; then + ps -e | grep mdnsd || true + else + ps -e | grep avahi-daemon || true + fi + fi + + if [[ "${{ matrix.dns_sd_mode }}" == "unicast" ]]; then + if [[ "${{ runner.os }}" == "Linux" ]]; then + # restore DNS Server + if [[ "${{ matrix.install_mdns }}" == "true" ]]; then + echo "Stopping mdnsd" + sudo /etc/init.d/mdns stop + else + echo "Stopping avahi-daemon" + sudo systemctl stop avahi-daemon + fi + cat /etc/resolv.conf.bak | sudo tee /etc/resolv.conf > /dev/null + if [[ "${{ matrix.install_mdns }}" == "true" ]]; then + echo "Restarting mdnsd" + sudo /etc/init.d/mdns start + else + echo "Restarting avahi-daemon" + sudo systemctl start avahi-daemon + fi + fi + fi if [[ "${{ env.DOCKER_TEST_SUITE }}" == "true" ]]; then docker stop nmos_testing docker rm nmos_testing fi - if [[ "${{ env.VAGRANT_TEST_SUITE }}" == "true" ]]; then + if [[ "${{ env.VAGRANT_TEST_SUITE }}" == "true" ]]; then vagrant scp :results/* results/ vagrant destroy -f fi + exit 0 diff --git a/.github/workflows/src/build-and-test.yml b/.github/workflows/src/build-and-test.yml index 7af965594..b6c093ef6 100644 --- a/.github/workflows/src/build-and-test.yml +++ b/.github/workflows/src/build-and-test.yml @@ -6,6 +6,10 @@ @import unit-test +@import install + +@import install-test + @import amwa-test @import save-results diff --git a/.github/workflows/src/build-setup.yml b/.github/workflows/src/build-setup.yml index becf60dcd..525d9554d 100644 --- a/.github/workflows/src/build-setup.yml +++ b/.github/workflows/src/build-setup.yml @@ -1,11 +1,15 @@ - name: install conan if: matrix.use_conan == true run: | - pip install conan - conan config set general.revisions_enabled=1 + pip install conan~=2.4.1 + +- name: 'ubuntu-14.04: install cmake' + if: matrix.os == 'ubuntu-14.04' + uses: lukka/get-cmake@v3.24.2 - name: install cmake - uses: lukka/get-cmake@v3.18.3 + if: matrix.os != 'ubuntu-14.04' + uses: lukka/get-cmake@v3.28.3 - name: setup bash path working-directory: ${{ env.GITHUB_WORKSPACE }} @@ -19,7 +23,7 @@ if: runner.os == 'Windows' run: | # set compiler to cl.exe to avoid building with gcc. - echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DCMAKE_C_COMPILER=cl.exe -DCMAKE_CXX_COMPILER=cl.exe" >> $env:GITHUB_ENV + echo "CMAKE_COMPILER_ARGS=-DCMAKE_C_COMPILER=cl.exe -DCMAKE_CXX_COMPILER=cl.exe" >> $env:GITHUB_ENV # disable unused network interface netsh interface set interface name="vEthernet (nat)" admin=DISABLED # get host IP address @@ -32,9 +36,11 @@ ).IPv4Address.IPAddress echo "HOST_IP_ADDRESS=$env:hostip" >> $env:GITHUB_ENV ipconfig - # add the CRL Distribution Point to hosts so that it's discoverable when runing the AMWA test suite in mDNS mode + # add the CRL Distribution Point to hosts so that it's discoverable when running the AMWA test suite in mDNS mode # and avoid SSL Error: WINHTTP_CALLBACK_STATUS_FLAG_CERT_REV_FAILED failed to check revocation status. Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip crl.testsuite.nmos.tv`n" + # add the OCSP server to hosts so that it's discoverable when running the AMWA test suite in mDNS mode + Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip ocsp.testsuite.nmos.tv`n" # add nmos-api.local to hosts to workaround mDNS lookups on windows being very slow and causing the AMWA test suite to take 2-3 hours to complete Add-Content $env:WINDIR\System32\Drivers\Etc\Hosts "`n$env:hostip nmos-api.local`n" # add nmos-mocks.local to hosts to workaround mDNS lookups on windows being very slow and causing the AMWA test suite IS-04-01 test_05 to fail due to latency messing up the apparent heart beat interval @@ -60,8 +66,8 @@ ifconfig echo "CTEST_EXTRA_ARGS=$CTEST_EXTRA_ARGS -E testMdnsResolveAPIs" >> $GITHUB_ENV echo "CTEST_EXPECTED_FAILURES=$CTEST_EXPECTED_FAILURES -R testMdnsResolveAPIs" >> $GITHUB_ENV - # add the CRL Distribution Point to hosts so that it's discoverable when runing the AMWA test suite in mDNS mode - echo "$hostip crl.testsuite.nmos.tv" | sudo tee -a /etc/hosts + # add the CRL Distribution Point and the OCSP server to hosts so that it's discoverable when running the AMWA test suite in mDNS mode + echo -e "$hostip crl.testsuite.nmos.tv\n$hostip ocsp.testsuite.nmos.tv" | sudo tee -a /etc/hosts > /dev/null # testssl.sh needs "timeout" brew install coreutils @@ -98,8 +104,8 @@ hostip=$(hostname -I | cut -f1 -d' ') echo "HOST_IP_ADDRESS=$hostip" >> $GITHUB_ENV ip address - # add the CRL Distribution Point to hosts so that it's discoverable when runing the AMWA test suite in mDNS mode - echo "$hostip crl.testsuite.nmos.tv" | sudo tee -a /etc/hosts + # add the CRL Distribution Point and the OCSP server to hosts so that it's discoverable when running the AMWA test suite in mDNS mode + echo -e "$hostip crl.testsuite.nmos.tv\n$hostip ocsp.testsuite.nmos.tv" | sudo tee -a /etc/hosts > /dev/null # re-synchronize the package index sudo apt-get update -q @@ -107,12 +113,13 @@ if: runner.os == 'Linux' && matrix.install_mdns == true run: | cd ${{ env.GITHUB_WORKSPACE }} - curl https://opensource.apple.com/tarballs/mDNSResponder/mDNSResponder-878.200.35.tar.gz -o mDNSResponder-878.200.35.tar.gz - tar -xzf mDNSResponder-878.200.35.tar.gz - patch -d mDNSResponder-878.200.35/ -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/unicast.patch - patch -d mDNSResponder-878.200.35/ -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/permit-over-long-service-types.patch - patch -d mDNSResponder-878.200.35/ -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/poll-rather-than-select.patch - cd mDNSResponder-878.200.35/mDNSPosix + mkdir mDNSResponder + cd mDNSResponder + curl -L https://github.com/apple-oss-distributions/mDNSResponder/archive/mDNSResponder-878.200.35.tar.gz -s | tar -xvzf - --strip-components=1 > /dev/null + patch -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/unicast.patch + patch -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/permit-over-long-service-types.patch + patch -p1 < ${{ env.GITHUB_WORKSPACE }}/Development/third_party/mDNSResponder/poll-rather-than-select.patch + cd mDNSPosix make os=linux && sudo make os=linux install # install Name Service Cache Daemon to speed up repeated mDNS name discovery sudo apt-get install -f nscd @@ -121,6 +128,8 @@ mkdir -p /var/run/nscd nscd fi + # force dependency on mDNSResponder + echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DNMOS_CPP_USE_AVAHI:BOOL=\"0\"" >> $GITHUB_ENV - name: ubuntu non-conan setup if: runner.os == 'Linux' && matrix.use_conan == false @@ -137,20 +146,18 @@ libssl-dev cd ${{ env.RUNNER_WORKSPACE }} - git clone --recurse-submodules --branch v2.10.16 https://github.com/Microsoft/cpprestsdk + git clone --recurse-submodules --branch v2.10.19 https://github.com/Microsoft/cpprestsdk cd cpprestsdk/Release mkdir build cd build cmake .. -DCMAKE_BUILD_TYPE:STRING="Release" -DWERROR:BOOL="0" -DBUILD_SAMPLES:BOOL="0" -DBUILD_TESTS:BOOL="0" make -j 2 && sudo make install - echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DWEBSOCKETPP_INCLUDE_DIR:PATH=\"${{ env.RUNNER_WORKSPACE }}/cpprestsdk/Release/libs/websocketpp\"" >> $GITHUB_ENV - -- name: disable conan - if: matrix.use_conan == false - shell: bash - run: | - echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DUSE_CONAN:BOOL=\"0\"" >> $GITHUB_ENV + echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }}" \ + "-DWEBSOCKETPP_INCLUDE_DIR:PATH=\"${{ env.RUNNER_WORKSPACE }}/cpprestsdk/Release/libs/websocketpp\"" \ + "-DNMOS_CPP_USE_SUPPLIED_JSON_SCHEMA_VALIDATOR:BOOL=\"1\"" \ + "-DNMOS_CPP_USE_SUPPLIED_JWT_CPP:BOOL=\"1\"" \ + >> $GITHUB_ENV - name: ubuntu avahi setup if: runner.os == 'Linux' && matrix.install_mdns == false @@ -163,9 +170,21 @@ sudo systemctl restart avahi-daemon # install Name Service Cache Daemon to speed up repeated mDNS name discovery sudo apt-get install -f nscd + # force dependency on avahi + echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DNMOS_CPP_USE_AVAHI:BOOL=\"1\"" >> $GITHUB_ENV - name: force cpprest asio - if: matrix.force_cpprest_asio == true + if: matrix.force_cpprest_asio == true && matrix.use_conan == true + shell: bash + run: | + echo "CONAN_INSTALL_EXTRA_ARGS=--options\;cpprestsdk/*:http_client_impl=asio\;--options\;cpprestsdk/*:http_listener_impl=asio" >> $GITHUB_ENV + +- name: enable conan + if: matrix.use_conan == true shell: bash run: | - echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }} -DNMOS_CPP_CONAN_OPTIONS:STRING=\"cpprestsdk:http_client_impl=asio;cpprestsdk:http_listener_impl=asio\"" >> $GITHUB_ENV + echo "CMAKE_EXTRA_ARGS=${{ env.CMAKE_EXTRA_ARGS }}" \ + "-DCMAKE_PROJECT_TOP_LEVEL_INCLUDES:STRING=\"third_party/cmake/conan_provider.cmake\"" \ + "-DCONAN_INSTALL_ARGS:STRING=\"--build=missing\;${{ env.CONAN_INSTALL_EXTRA_ARGS }}\;--lockfile-out=conan.lock\"" \ + >> $GITHUB_ENV + cat $GITHUB_ENV diff --git a/.github/workflows/src/build-test.yml b/.github/workflows/src/build-test.yml index 10aabe1f5..3934c8ce2 100644 --- a/.github/workflows/src/build-test.yml +++ b/.github/workflows/src/build-test.yml @@ -12,45 +12,82 @@ env: SECRET_RESULTS_SHEET_ID: ${{ secrets.RESULTS_SHEET_ID }} jobs: build_and_test: - name: '${{ matrix.os }}: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }}, force cpprest asio: ${{ matrix.force_cpprest_asio }})' + name: '${{ matrix.os }}: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }}, force cpprest asio: ${{ matrix.force_cpprest_asio }}, dns-sd mode: ${{ matrix.dns_sd_mode}}, enable_authorization: ${{ matrix.enable_authorization }})' runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - os: [ubuntu-18.04, macos-latest, windows-latest] - install_mdns: [false] + os: [ubuntu-20.04, windows-2019] + install_mdns: [false, true] use_conan: [true] force_cpprest_asio: [false] + dns_sd_mode: [multicast, unicast] + enable_authorization: [false, true] + exclude: + # install_mdns is only meaningful on Linux + - os: windows-2019 + enable_authorization: false + - os: ubuntu-20.04 + enable_authorization: false + - os: windows-2019 + install_mdns: true + # for now, unicast DNS-SD tests are only implemented on Linux + - os: windows-2019 + dns_sd_mode: unicast + # for now, exclude unicast DNS-SD with mDNSResponder due to + # intermittent *** buffer overflow detected *** in mdnsd + - os: ubuntu-20.04 + install_mdns: true + dns_sd_mode: unicast + enable_authorization: true include: - - install_mdns: true + - os: windows-2022 + install_mdns: false use_conan: true - force_cpprest_asio: false - os: ubuntu-18.04 - - install_mdns: true + force_cpprest_asio: true + dns_sd_mode: multicast + enable_authorization: true + - os: windows-2022 + install_mdns: false + use_conan: true + force_cpprest_asio: true + dns_sd_mode: multicast + enable_authorization: false + - os: ubuntu-22.04 + install_mdns: false use_conan: true force_cpprest_asio: false - os: ubuntu-20.04 - - install_mdns: false + dns_sd_mode: multicast + enable_authorization: true + - os: ubuntu-22.04 + install_mdns: false use_conan: true - force_cpprest_asio: true - os: windows-latest + force_cpprest_asio: false + dns_sd_mode: multicast + enable_authorization: false steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set environment variables shell: bash run: | + if [[ "${{ matrix.enable_authorization }}" == "true" ]]; then + authorization_mode=auth + else + authorization_mode=noauth + fi + if [[ "${{ runner.os }}" == "Linux" ]]; then if [[ "${{ matrix.install_mdns }}" == "true" ]]; then - echo "BUILD_NAME=${{ matrix.os }}_mdns" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_mdns_${{ matrix.dns_sd_mode }}_$authorization_mode" >> $GITHUB_ENV else - echo "BUILD_NAME=${{ matrix.os }}_avahi" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_avahi_${{ matrix.dns_sd_mode }}_$authorization_mode" >> $GITHUB_ENV fi elif [[ "${{ matrix.force_cpprest_asio }}" == "true" ]]; then - echo "BUILD_NAME=${{ matrix.os }}_asio" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_asio_$authorization_mode" >> $GITHUB_ENV else - echo "BUILD_NAME=${{ matrix.os }}" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_$authorization_mode" >> $GITHUB_ENV fi GITHUB_COMMIT=`echo "${{ github.sha }}" | cut -c1-7` echo "GITHUB_COMMIT=$GITHUB_COMMIT" >> $GITHUB_ENV @@ -58,7 +95,7 @@ jobs: echo "RUNNER_WORKSPACE=${{ runner.workspace }}" >> $GITHUB_ENV - name: install python - uses: actions/setup-python@v1 + uses: actions/setup-python@v4 with: python-version: 3.8 @@ -68,51 +105,12 @@ jobs: @import build-and-test - build_and_test_ubuntu_14: - name: 'ubuntu-14.04: build and test (install mdns: ${{ matrix.install_mdns }}, use conan: ${{ matrix.use_conan }})' - runs-on: ubuntu-latest - container: - image: ubuntu:14.04 - strategy: - fail-fast: false - matrix: - install_mdns: [true] - use_conan: [false] - - steps: - - uses: actions/checkout@v2 - - - name: set environment variables - run: | - echo "BUILD_NAME=ubuntu-14.04_mdns" >> $GITHUB_ENV - GITHUB_COMMIT=`echo "${{ github.sha }}" | cut -c1-7` - echo "GITHUB_COMMIT=$GITHUB_COMMIT" >> $GITHUB_ENV - # github.workspace points to the host path not the docker path, the home directory defaults to the workspace directory - echo "GITHUB_WORKSPACE=`pwd`" >> $GITHUB_ENV - cd .. - echo "RUNNER_WORKSPACE=`pwd`" >> $GITHUB_ENV - - - name: install build tools - run: | - apt-get update -q - apt-get install -y software-properties-common - add-apt-repository ppa:deadsnakes/ppa -y - apt-get --allow-unauthenticated update -q - apt-get --allow-unauthenticated install -y curl g++ git make patch python3.6 python3.6-gdbm bsdmainutils dnsutils unzip - update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.6 3 - ln -s /usr/bin/python3.6 /usr/bin/python - curl -sS https://bootstrap.pypa.io/get-pip.py | python - curl -sS https://nodejs.org/dist/v12.16.2/node-v12.16.2-linux-x64.tar.xz | tar -xJ - echo "`pwd`/node-v12.16.2-linux-x64/bin" >> $GITHUB_PATH - - @import build-and-test - make_badges: if: github.ref == 'refs/heads/master' && github.event_name == 'push' - needs: [build_and_test, build_and_test_ubuntu_14] - runs-on: ubuntu-latest + needs: [build_and_test] + runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: set environment variables shell: bash @@ -122,14 +120,14 @@ jobs: echo "GITHUB_WORKSPACE=${{ github.workspace }}" >> $GITHUB_ENV echo "RUNNER_WORKSPACE=${{ runner.workspace }}" >> $GITHUB_ENV - - uses: actions/download-artifact@v2.0 + - uses: actions/download-artifact@v4 with: path: ${{ runner.workspace }}/artifacts - name: make badges run: | - # combine badges from all builds, exclude macos-latest - ${{ github.workspace }}/Sandbox/make_badges.sh ${{ github.workspace }} ${{ runner.workspace }}/artifacts macos-latest + # combine badges from all builds, exclude macos-13 + ${{ github.workspace }}/Sandbox/make_badges.sh ${{ github.workspace }} ${{ runner.workspace }}/artifacts macos-13_auth macos-13_noauth # force push to github onto an orphan 'badges' branch cd ${{ github.workspace }} diff --git a/.github/workflows/src/build.yml b/.github/workflows/src/build.yml index 3ac02c5ed..80b669d62 100644 --- a/.github/workflows/src/build.yml +++ b/.github/workflows/src/build.yml @@ -1,9 +1,16 @@ -- uses: ilammy/msvc-dev-cmd@v1 +- name: setup developer command prompt for Microsoft Visual C++ + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + - name: build - uses: lukka/run-cmake@v2.0 + uses: lukka/run-cmake@v3.4 with: cmakeListsOrSettingsJson: CMakeListsTxtAdvanced cmakeListsTxtPath: '${{ env.GITHUB_WORKSPACE }}/Development/CMakeLists.txt' buildDirectory: '${{ env.RUNNER_WORKSPACE }}/build/' - cmakeAppendedArgs: '-GNinja -DCMAKE_BUILD_TYPE=Release ${{ env.CMAKE_EXTRA_ARGS }}' + cmakeAppendedArgs: '-GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="${{ env.RUNNER_WORKSPACE }}/install" ${{ env.CMAKE_COMPILER_ARGS }} ${{ env.CMAKE_EXTRA_ARGS }}' +- name: dump conan lockfile + if: matrix.use_conan == true + run: | + cat ${{ env.RUNNER_WORKSPACE }}/build/conan.lock diff --git a/.github/workflows/src/google-setup.yml b/.github/workflows/src/google-setup.yml index 132bdb71d..6eab515d1 100644 --- a/.github/workflows/src/google-setup.yml +++ b/.github/workflows/src/google-setup.yml @@ -5,4 +5,4 @@ run: | mkdir -p gdrive echo "${{ env.SECRET_GOOGLE_CREDENTIALS }}" | openssl base64 -d -A -out gdrive/credentials.json - echo "GDRIVE_CREDENTIALS=`pwd`/gdrive/credentials.json" >> $GITHUB_ENV \ No newline at end of file + echo "GDRIVE_CREDENTIALS=`pwd`/gdrive/credentials.json" >> $GITHUB_ENV diff --git a/.github/workflows/src/install-test.yml b/.github/workflows/src/install-test.yml new file mode 100644 index 000000000..95926816b --- /dev/null +++ b/.github/workflows/src/install-test.yml @@ -0,0 +1,26 @@ +- name: set install test environment variable + shell: bash + run: | + # replace backslashes with forward slashes on Windows + echo "CMAKE_WORKSPACE=${RUNNER_WORKSPACE//\\/\/}" >> $GITHUB_ENV + +- name: install test + if: runner.os != 'macOS' + uses: lukka/run-cmake@v3.4 + with: + cmakeListsOrSettingsJson: CMakeListsTxtAdvanced + cmakeListsTxtPath: '${{ env.GITHUB_WORKSPACE }}/Sandbox/my-nmos-node/CMakeLists.txt' + buildDirectory: '${{ env.RUNNER_WORKSPACE }}/build-my-nmos-node/' + cmakeAppendedArgs: '-GNinja + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_FIND_PACKAGE_PREFER_CONFIG="1" + -DCMAKE_MODULE_PATH="${{ env.CMAKE_WORKSPACE }}/build/conan" + -DCMAKE_PREFIX_PATH="${{ env.CMAKE_WORKSPACE }}/install" + -DCMAKE_INSTALL_PREFIX="${{ env.CMAKE_WORKSPACE }}/build/conan" + ${{ env.CMAKE_COMPILER_ARGS }}' + +- name: install test log + if: runner.os != 'macOS' + run: | + # dump the log file created in Sandbox/my-nmos-node/CMakeLists.txt + cat ${{ env.RUNNER_WORKSPACE }}/build-my-nmos-node/my-nmos-node_include-release.txt diff --git a/.github/workflows/src/install.yml b/.github/workflows/src/install.yml new file mode 100644 index 000000000..35cda17b4 --- /dev/null +++ b/.github/workflows/src/install.yml @@ -0,0 +1,7 @@ +- name: install + uses: lukka/run-cmake@v3.4 + with: + cmakeListsOrSettingsJson: CMakeListsTxtAdvanced + cmakeListsTxtPath: '${{ env.GITHUB_WORKSPACE }}/Development/CMakeLists.txt' + buildDirectory: '${{ env.RUNNER_WORKSPACE }}/build/' + buildWithCMakeArgs: '--target install' diff --git a/.github/workflows/src/save-results.yml b/.github/workflows/src/save-results.yml index 6ea55fefd..c4991df13 100644 --- a/.github/workflows/src/save-results.yml +++ b/.github/workflows/src/save-results.yml @@ -6,13 +6,12 @@ export SHEET=https://docs.google.com/spreadsheets/d/${{ env.SECRET_RESULTS_SHEET_ID }} python nmos-testing/utilities/run-test-suites/gsheetsImport/resultsImporter.py --credentials ${{ env.GDRIVE_CREDENTIALS }} --sheet "$SHEET" --insert --json nmos-testing/results/${{ env.GITHUB_COMMIT }}-*.json || echo "upload failed" -- uses: actions/upload-artifact@v1 +- uses: actions/upload-artifact@v4 with: name: ${{ env.BUILD_NAME }}_badges path: ${{ runner.workspace }}/nmos-testing/badges -- uses: actions/upload-artifact@v1 +- uses: actions/upload-artifact@v4 with: name: ${{ env.BUILD_NAME }}_results path: ${{ runner.workspace }}/nmos-testing/results - diff --git a/.github/workflows/src/tmate.yml b/.github/workflows/src/tmate.yml index e45b2e42f..5b1364952 100644 --- a/.github/workflows/src/tmate.yml +++ b/.github/workflows/src/tmate.yml @@ -1,4 +1,3 @@ - name: setup tmate uses: mxschmitt/action-tmate@v2 if: runner.os == 'macOS' - diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 1cc4977d4..000000000 --- a/.travis.yml +++ /dev/null @@ -1,270 +0,0 @@ -language: cpp - -before_install: - # As we run the test suite within the same host, and the test suite checks IP addresses of mDNS advertisements (e.g. IS-04-03) - # and the 'remote' address of requests received by the test suite mocks (e.g. IS-09-02), things are much simpler with a single - # interface configured with an IP address. Although 'docker0' is down, it has an address, so flush that, leaving only 'eth0'. - - sudo ip addr flush dev docker0 - - # Get IP address and short form Git Hash - - | - export HOST_IP_ADDRESS="$(hostname -I | cut -f1 -d' ')" - echo $HOST_IP_ADDRESS - GITHUB_COMMIT=$(echo "$TRAVIS_COMMIT" | cut -c1-7) - echo $GITHUB_COMMIT - - # Allow Travis folds - - | - export -f travis_nanoseconds - export -f travis_fold - export -f travis_time_start - export -f travis_time_finish - -_install_cmake: &install_cmake - # Install minimum required CMake - | - cd $TRAVIS_BUILD_DIR/.. - wget "https://github.com/Kitware/CMake/releases/download/v3.9.0/cmake-3.9.0-Linux-x86_64.sh" - sudo mkdir /opt/cmake - sudo sh cmake-3.9.0-Linux-x86_64.sh --prefix=/opt/cmake --skip-license - sudo ln -s /opt/cmake/bin/cmake /usr/local/bin/cmake - -_install_cpprestsdk: &install_cpprestsdk - # Retrieve and build C++ REST SDK - | - cd $TRAVIS_BUILD_DIR/.. - git clone --recurse-submodules --branch v2.10.16 https://github.com/Microsoft/cpprestsdk - cd cpprestsdk/Release - mkdir build - cd build - cmake .. -DCMAKE_BUILD_TYPE:STRING="Release" -DWERROR:BOOL="0" -DBUILD_SAMPLES:BOOL="0" -DBUILD_TESTS:BOOL="0" - make -j 2 && sudo make install - -_install_mdns: &install_mdns - # Install mDNSResponder - | - cd $TRAVIS_BUILD_DIR/.. - wget https://opensource.apple.com/tarballs/mDNSResponder/mDNSResponder-878.200.35.tar.gz - tar -xzf mDNSResponder-878.200.35.tar.gz - patch -d mDNSResponder-878.200.35/ -p1 < $TRAVIS_BUILD_DIR/Development/third_party/mDNSResponder/unicast.patch - patch -d mDNSResponder-878.200.35/ -p1 < $TRAVIS_BUILD_DIR/Development/third_party/mDNSResponder/permit-over-long-service-types.patch - patch -d mDNSResponder-878.200.35/ -p1 < $TRAVIS_BUILD_DIR/Development/third_party/mDNSResponder/poll-rather-than-select.patch - cd mDNSResponder-878.200.35/mDNSPosix - make os=linux && sudo make os=linux install - -jobs: - include: - - os: linux - dist: trusty - name: Ubuntu 14.04 / GCC 4.8.4 - compiler: gcc - addons: - apt: - packages: - - libboost-chrono-dev - - libboost-date-time-dev - - libboost-regex-dev - - libboost-system-dev - - libboost-thread-dev - - libboost-random-dev - - libboost-filesystem-dev - - openssl - - libssl-dev - install: - # Install minimum required CMake - - *install_cmake - # Retrieve and build C++ REST SDK - - *install_cpprestsdk - # Install mDNSResponder - - *install_mdns - - os: linux - dist: bionic - name: Ubuntu 18.04 / GCC 7.4.0 - compiler: gcc - addons: - apt: - packages: - - python3-pip - - python3-setuptools - install: - # Install Conan - - python3 -m pip install conan - # Install mDNSResponder - - *install_mdns - -script: - # Build nmos-cpp - - travis_fold start "cmake" - - | - cd $TRAVIS_BUILD_DIR/Development - mkdir -p build - cd build - - | - if [[ ${TRAVIS_DIST} == "trusty" ]]; then - cmake .. -DCMAKE_BUILD_TYPE:STRING="Release" -DUSE_CONAN:BOOL="0" -DWEBSOCKETPP_INCLUDE_DIR:PATH="$TRAVIS_BUILD_DIR/../cpprestsdk/Release/libs/websocketpp" - else - # Use Conan by default - cmake .. -DCMAKE_BUILD_TYPE:STRING="Release" - fi - - travis_fold end "cmake" - - travis_fold start "make.nmos-cpp" - - make -j 2 - - travis_fold end "make.nmos-cpp" - - travis_fold start "make.test" - - make test - - travis_fold end "make.test" - -after_script: - - cd $TRAVIS_BUILD_DIR/.. - - # Install AMWA test suite. Tests are run in after_script so as not to fail the build stage. - - git clone https://github.com/AMWA-TV/nmos-testing.git && cd nmos-testing - - # Install minimum required Python for the testing tool - - | - if [[ ${TRAVIS_DIST} == "trusty" ]]; then - sudo add-apt-repository ppa:deadsnakes/ppa -y - sudo apt-get --allow-unauthenticated update -q - sudo apt-get --allow-unauthenticated install python3.6 python3.6-gdbm - sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.6 3 - fi - - # Install pip and dependencies - - | - curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py - sudo -H python3 get-pip.py - sudo -H python3 -m pip install -r requirements.txt - sudo -H python3 -m pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt - - # Install SDPoker - - npm install -g AMWA-TV/sdpoker - - # Create output directories - - mkdir results && mkdir badges - - # Run Node tests - - | - $TRAVIS_BUILD_DIR/Development/build/nmos-cpp-node "{\"http_port\":1080,\"domain\":\"local.\",\"logging_level\":-40}" > nodeoutput 2>&1 & - NODE_PID=$! - - # IS-04-01 - - | - case $(python3 nmos-test.py suite IS-04-01 --selection all --host "$HOST_IP_ADDRESS" --port 1080 --version v1.3 --output "results/$GITHUB_COMMIT-IS-04-01.json" >/dev/null 2>&1; echo $?) in - [0-1]) testmessage="Pass" && testcolour="brightgreen";; - *) testmessage="Fail" && testcolour="red";; - esac - printf "{\"schemaVersion\":1,\"label\":\"IS-04-01\",\"message\":\"$testmessage\",\"color\":\"$testcolour\"}" > badges/IS-04-01.json - - # IS-04-03 - - | - case $(python3 nmos-test.py suite IS-04-03 --selection all --host "$HOST_IP_ADDRESS" --port 1080 --version v1.3 --output "results/$GITHUB_COMMIT-IS-04-03.json" >/dev/null 2>&1; echo $?) in - [0-1]) testmessage="Pass" && testcolour="brightgreen";; - *) testmessage="Fail" && testcolour="red";; - esac - printf "{\"schemaVersion\":1,\"label\":\"IS-04-03\",\"message\":\"$testmessage\",\"color\":\"$testcolour\"}" > badges/IS-04-03.json - - # IS-05-01 - - | - case $(python3 nmos-test.py suite IS-05-01 --selection all --host "$HOST_IP_ADDRESS" --port 1080 --version v1.1 --output "results/$GITHUB_COMMIT-IS-05-01.json" >/dev/null 2>&1; echo $?) in - [0-1]) testmessage="Pass" && testcolour="brightgreen";; - *) testmessage="Fail" && testcolour="red";; - esac - printf "{\"schemaVersion\":1,\"label\":\"IS-05-01\",\"message\":\"$testmessage\",\"color\":\"$testcolour\"}" > badges/IS-05-01.json - - # IS-05-02 - - | - case $(python3 nmos-test.py suite IS-05-02 --selection all --host "$HOST_IP_ADDRESS" "$HOST_IP_ADDRESS" --port 1080 1080 --version v1.3 v1.1 --output "results/$GITHUB_COMMIT-IS-05-02.json" >/dev/null 2>&1; echo $?) in - [0-1]) testmessage="Pass" && testcolour="brightgreen";; - *) testmessage="Fail" && testcolour="red";; - esac - printf "{\"schemaVersion\":1,\"label\":\"IS-05-02\",\"message\":\"$testmessage\",\"color\":\"$testcolour\"}" > badges/IS-05-02.json - - # IS-07-01 - - | - case $(python3 nmos-test.py suite IS-07-01 --selection all --host "$HOST_IP_ADDRESS" --port 1080 --version v1.0 --output "results/$GITHUB_COMMIT-IS-07-01.json" >/dev/null 2>&1; echo $?) in - [0-1]) testmessage="Pass" && testcolour="brightgreen";; - *) testmessage="Fail" && testcolour="red";; - esac - printf "{\"schemaVersion\":1,\"label\":\"IS-07-01\",\"message\":\"$testmessage\",\"color\":\"$testcolour\"}" > badges/IS-07-01.json - - # IS-07-02 - - | - case $(python3 nmos-test.py suite IS-07-02 --selection all --host "$HOST_IP_ADDRESS" "$HOST_IP_ADDRESS" "$HOST_IP_ADDRESS" --port 1080 1080 1080 --version v1.3 v1.1 v1.0 --output "results/$GITHUB_COMMIT-IS-07-02.json" >/dev/null 2>&1; echo $?) in - [0-1]) testmessage="Pass" && testcolour="brightgreen";; - *) testmessage="Fail" && testcolour="red";; - esac - printf "{\"schemaVersion\":1,\"label\":\"IS-07-02\",\"message\":\"$testmessage\",\"color\":\"$testcolour\"}" > badges/IS-07-02.json - - # IS-09-02 - - | - case $(python3 nmos-test.py suite IS-09-02 --selection all --host "$HOST_IP_ADDRESS" null --port 0 0 --version null v1.0 --output "results/$GITHUB_COMMIT-IS-09-02.json" >/dev/null 2>&1; echo $?) in - [0-1]) testmessage="Pass" && testcolour="brightgreen";; - *) testmessage="Fail" && testcolour="red";; - esac - printf "{\"schemaVersion\":1,\"label\":\"IS-09-02\",\"message\":\"$testmessage\",\"color\":\"$testcolour\"}" > badges/IS-09-02.json - - # Run Registry tests (leave Node running) - - | - $TRAVIS_BUILD_DIR/Development/build/nmos-cpp-registry "{\"pri\":0,\"http_port\":8080,\"domain\":\"local.\",\"logging_level\":-40}" > registryoutput 2>&1 & - REGISTRY_PID=$! - # short delay to give the Registry a chance to start up and the Node a chance to register before running the Registry test suite - sleep 2 - # add a persistent Query WebSocket API subscription before running the Registry test suite - curl "http://localhost:8080/x-nmos/query/v1.3/subscriptions" -H "Content-Type: application/json" -d "{\"max_update_rate_ms\": 100, \"resource_path\": \"/nodes\", \"params\": {\"label\": \"host1\"}, \"persist\": true, \"secure\": false}" - - # IS-04-02 - - | - case $(python3 nmos-test.py suite IS-04-02 --selection all --host "$HOST_IP_ADDRESS" "$HOST_IP_ADDRESS" --port 8080 8080 --version v1.3 v1.3 --output "results/$GITHUB_COMMIT-IS-04-02.json" >/dev/null 2>&1; echo $?) in - [0-1]) testmessage="Pass" && testcolour="brightgreen";; - *) testmessage="Fail" && testcolour="red";; - esac - printf "{\"schemaVersion\":1,\"label\":\"IS-04-02\",\"message\":\"$testmessage\",\"color\":\"$testcolour\"}" > badges/IS-04-02.json - - # IS-09-01 - - | - case $(python3 nmos-test.py suite IS-09-01 --selection all --host "$HOST_IP_ADDRESS" --port 8080 --version v1.0 --output "results/$GITHUB_COMMIT-IS-09-01.json" >/dev/null 2>&1; echo $?) in - [0-1]) testmessage="Pass" && testcolour="brightgreen";; - *) testmessage="Fail" && testcolour="red";; - esac - printf "{\"schemaVersion\":1,\"label\":\"IS-09-01\",\"message\":\"$testmessage\",\"color\":\"$testcolour\"}" > badges/IS-09-01.json - - # Stop Node and Registry - - kill $NODE_PID - - kill $REGISTRY_PID - - # Get Google Drive CLI and decrypt credentials - - cd $TRAVIS_BUILD_DIR/.. - - openssl aes-256-cbc -K $encrypted_credentials_K -iv $encrypted_credentials_iv -in nmos-cpp/.gdrive-credentials.json.enc -out credentials.json -d - - mkdir ~/.gdrive && mv credentials.json ~/.gdrive - - # Upload output files - - | - wget https://github.com/gdrive-org/gdrive/releases/download/2.1.0/gdrive-linux-x64 - mv ./gdrive-linux-x64 ./gdrive - chmod +x ./gdrive - - export JOB_FOLDER="$(./gdrive mkdir --service-account credentials.json --parent 1XgPUnYddfVfC0f8B1diI-x-7PFpVfgWN $TRAVIS_JOB_NUMBER | cut -f2 -d' ')" - - for f in nmos-testing/results/*; do ./gdrive upload --service-account credentials.json --parent $JOB_FOLDER $f; done - - ./gdrive upload --service-account credentials.json --parent $JOB_FOLDER nmos-testing/nodeoutput - - ./gdrive upload --service-account credentials.json --parent $JOB_FOLDER nmos-testing/registryoutput - - - tail -n +1 nmos-testing/results/* - - tail -n +1 nmos-testing/badges/* - - cat nmos-testing/nodeoutput - - cat nmos-testing/registryoutput - - - | - if [[ ${TRAVIS_DIST} == "bionic" && ${TRAVIS_BRANCH} == "master" && ${TRAVIS_PULL_REQUEST} == "false" ]]; then - ./gdrive update --service-account credentials.json 1VrCPcYeTs5uoBgECxbfuWbbhJZpbHcPy nmos-testing/badges/IS-04-01.json - ./gdrive update --service-account credentials.json 14vgZF4CSx2oayEAbeNFGiHmPW95HKMXt nmos-testing/badges/IS-04-02.json - ./gdrive update --service-account credentials.json 16616xSByskr3PbeqhnCcNTjfJcDdzUav nmos-testing/badges/IS-04-03.json - ./gdrive update --service-account credentials.json 1tW25Xim9LymIvPXnxM5taGmlLVsXa71p nmos-testing/badges/IS-05-01.json - ./gdrive update --service-account credentials.json 1MkQNv8v2r0ydB1mQ55k-pktlzE8LZ3g9 nmos-testing/badges/IS-05-02.json - ./gdrive update --service-account credentials.json 1XQuAN13xAQ81G_Eokj6AAYv5kMInPXkZ nmos-testing/badges/IS-07-01.json - ./gdrive update --service-account credentials.json 16t7XCmsQaOw5eEqq6yuuy1U9I3J-9zN9 nmos-testing/badges/IS-07-02.json - ./gdrive update --service-account credentials.json 16t7ncRp3SbHHoftQY-RBi2NFC283fOTn nmos-testing/badges/IS-09-01.json - ./gdrive update --service-account credentials.json 1f4FHD6vI1LotF7Sm8U6tmNp58seW9397 nmos-testing/badges/IS-09-02.json - - export SHEET=https://docs.google.com/spreadsheets/d/1UgZoI0lGCMDn9-zssccf2Azil3WN6jogroMT8Wh6H64 - for f in nmos-testing/results/$GITHUB_COMMIT-*; do python3 nmos-testing/utilities/run-test-suites/gsheetsImport/resultsImporter.py --credentials ~/.gdrive/credentials.json --sheet "$SHEET" --json $f --insert; done - fi - - echo "Done" diff --git a/Development/.gitignore b/Development/.gitignore index a0155cb29..d96449642 100644 --- a/Development/.gitignore +++ b/Development/.gitignore @@ -1 +1,3 @@ /build*/ +/.vs/ +/out/ diff --git a/Development/CMakeLists.txt b/Development/CMakeLists.txt index f5b3032b2..a221b8df0 100644 --- a/Development/CMakeLists.txt +++ b/Development/CMakeLists.txt @@ -1,95 +1,48 @@ -# CMake 3.9 is required due to cpprestsdk-config.cmake using find_dependency with COMPONENTS -cmake_minimum_required(VERSION 3.9 FATAL_ERROR) +# the injection point may be used to configure conan, but that requires CMake 3.24 or higher +# so avoid confusion and reject invocations which attempt to use it on lower versions +# see https://cmake.org/cmake/help/v3.24/variable/CMAKE_PROJECT_TOP_LEVEL_INCLUDES.html +# the alternative is to run conan install first instead +if(CMAKE_PROJECT_TOP_LEVEL_INCLUDES) + cmake_minimum_required(VERSION 3.24 FATAL_ERROR) +else() + cmake_minimum_required(VERSION 3.17 FATAL_ERROR) +endif() # project name project(nmos-cpp) -# The default nmos-cpp root directory -set(NMOS_CPP_DIR ${PROJECT_SOURCE_DIR}) - -set (USE_CONAN ON CACHE BOOL "Use Conan to acquire dependencies") - -if (${USE_CONAN}) - include (${NMOS_CPP_DIR}/cmake/NmosCppConan.cmake) -endif() - -# Common setup and dependency checking -include (${NMOS_CPP_DIR}/cmake/NmosCppCommon.cmake) +# enable or disable the example applications +set(NMOS_CPP_BUILD_EXAMPLES ON CACHE BOOL "Build example applications") -# Setup for the libraries -include (${NMOS_CPP_DIR}/cmake/NmosCppLibraries.cmake) +# enable or disable the unit test suite +set(NMOS_CPP_BUILD_TESTS ON CACHE BOOL "Build test suite application") -# nmos-cpp-node executable +# enable or disable the LLDP support library (lldp) +# and its additional dependencies +set(NMOS_CPP_BUILD_LLDP OFF CACHE BOOL "Build LLDP support library") +mark_as_advanced(FORCE NMOS_CPP_BUILD_LLDP) -set(NMOS_CPP_NODE_SOURCES - ${NMOS_CPP_DIR}/nmos-cpp-node/main.cpp - ${NMOS_CPP_DIR}/nmos-cpp-node/node_implementation.cpp - ) -set(NMOS_CPP_NODE_HEADERS - ${NMOS_CPP_DIR}/nmos-cpp-node/node_implementation.h - ) +# common config +include(cmake/NmosCppCommon.cmake) -add_executable( - nmos-cpp-node - ${NMOS_CPP_NODE_SOURCES} - ${NMOS_CPP_NODE_HEADERS} - ${NMOS_CPP_DIR}/nmos-cpp-node/config.json - ) +# nmos-cpp dependencies +include(cmake/NmosCppDependencies.cmake) -source_group("Source Files" FILES ${NMOS_CPP_NODE_SOURCES}) -source_group("Header Files" FILES ${NMOS_CPP_NODE_HEADERS}) +# nmos-cpp libraries +include(cmake/NmosCppLibraries.cmake) -target_link_libraries( - nmos-cpp-node - nmos-cpp_static - ${CPPRESTSDK_TARGET} - ${PLATFORM_LIBS} - ${Boost_LIBRARIES} - ) +if(NMOS_CPP_BUILD_EXAMPLES) + # nmos-cpp-node executable + include(cmake/NmosCppNode.cmake) -if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") - # Conan packages usually don't include PDB files so suppress the resulting warning - set_target_properties( - nmos-cpp-node - PROPERTIES - LINK_FLAGS "/ignore:4099" - ) + # nmos-cpp-registry executable + include(cmake/NmosCppRegistry.cmake) endif() -# nmos-cpp-registry executable - -set(NMOS_CPP_REGISTRY_SOURCES - ${NMOS_CPP_DIR}/nmos-cpp-registry/main.cpp - ) -set(NMOS_CPP_REGISTRY_HEADERS - ) - -add_executable( - nmos-cpp-registry - ${NMOS_CPP_REGISTRY_SOURCES} - ${NMOS_CPP_REGISTRY_HEADERS} - ${NMOS_CPP_DIR}/nmos-cpp-registry/config.json - ) - -source_group("Source Files" FILES ${NMOS_CPP_REGISTRY_SOURCES}) -source_group("Header Files" FILES ${NMOS_CPP_REGISTRY_HEADERS}) - -target_link_libraries( - nmos-cpp-registry - nmos-cpp_static - ${CPPRESTSDK_TARGET} - ${PLATFORM_LIBS} - ${Boost_LIBRARIES} - ) - -if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") - # Conan packages usually don't include PDB files so suppress the resulting warning - set_target_properties( - nmos-cpp-registry - PROPERTIES - LINK_FLAGS "/ignore:4099" - ) +if(NMOS_CPP_BUILD_TESTS) + # nmos-cpp-test executable + include(cmake/NmosCppTest.cmake) endif() -# nmos-cpp-test executable -include (${NMOS_CPP_DIR}/cmake/NmosCppTest.cmake) +# export the config-file package +include(cmake/NmosCppExports.cmake) diff --git a/Development/README.md b/Development/README.md index e5057c40e..c4464e9ec 100644 --- a/Development/README.md +++ b/Development/README.md @@ -12,6 +12,10 @@ C++ source code and build files for the software Extensions to the [C++ REST SDK](https://github.com/Microsoft/cpprestsdk) - [detail](detail) Small general purpose utilties and header files to facilitate cross-platform development +- [jwk](jwk) + An implementation of the conversion between JSON Web Key and public key +- [lldp](lldp) + A simple API for LLDP and an implementation using the PCAP *pcap.h* API - [mdns](mdns) A simple API for mDNS Service Discovery (DNS-SD) and an implementation using the original Bonjour *dns_sd.h* API - [nmos](nmos) diff --git a/Development/boost/asio/ssl/use_tmp_ecdh.hpp b/Development/boost/asio/ssl/use_tmp_ecdh.hpp index 922a955c5..ab4d8b3bf 100644 --- a/Development/boost/asio/ssl/use_tmp_ecdh.hpp +++ b/Development/boost/asio/ssl/use_tmp_ecdh.hpp @@ -16,6 +16,11 @@ # define BOOST_ASIO_SYNC_OP_VOID_RETURN(e) return #endif +#if OPENSSL_VERSION_NUMBER >= 0x30000000L +#include +#include +#endif + namespace boost { namespace asio { namespace ssl { @@ -40,16 +45,19 @@ struct evp_pkey_cleanup ~evp_pkey_cleanup() { if (p) ::EVP_PKEY_free(p); } }; +#if OPENSSL_VERSION_NUMBER < 0x30000000L struct ec_key_cleanup { EC_KEY *p; ~ec_key_cleanup() { if (p) ::EC_KEY_free(p); } }; +#endif inline -BOOST_ASIO_SYNC_OP_VOID do_use_tmp_ecdh_file(boost::asio::ssl::context& ctx, +BOOST_ASIO_SYNC_OP_VOID do_use_tmp_ecdh(boost::asio::ssl::context& ctx, BIO* bio, boost::system::error_code& ec) { +#if OPENSSL_VERSION_NUMBER < 0x30000000L ::ERR_clear_error(); int nid = NID_undef; @@ -63,7 +71,7 @@ BOOST_ASIO_SYNC_OP_VOID do_use_tmp_ecdh_file(boost::asio::ssl::context& ctx, ec_key_cleanup key = { ::EVP_PKEY_get1_EC_KEY(pkey.p) }; if (key.p) { - const EC_GROUP *group = EC_KEY_get0_group(key.p); + const EC_GROUP* group = EC_KEY_get0_group(key.p); nid = EC_GROUP_get_curve_name(group); } } @@ -83,6 +91,33 @@ BOOST_ASIO_SYNC_OP_VOID do_use_tmp_ecdh_file(boost::asio::ssl::context& ctx, static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()); BOOST_ASIO_SYNC_OP_VOID_RETURN(ec); +#else + ::ERR_clear_error(); + + x509_cleanup x509 = { ::PEM_read_bio_X509(bio, NULL, 0, NULL) }; + if (x509.p) + { + evp_pkey_cleanup pkey = { ::X509_get_pubkey(x509.p) }; + if (pkey.p) + { + char curve_name[64]; + size_t return_size{ 0 }; + if (::EVP_PKEY_get_utf8_string_param(pkey.p, OSSL_PKEY_PARAM_GROUP_NAME, curve_name, sizeof(curve_name), &return_size)) + { + if (::SSL_CTX_set1_groups_list(ctx.native_handle(), curve_name) == 1) + { + ec = boost::system::error_code(); + BOOST_ASIO_SYNC_OP_VOID_RETURN(ec); + } + } + } + } + + ec = boost::system::error_code( + static_cast(::ERR_get_error()), + boost::asio::error::get_ssl_category()); + BOOST_ASIO_SYNC_OP_VOID_RETURN(ec); +#endif } inline @@ -94,7 +129,7 @@ BOOST_ASIO_SYNC_OP_VOID use_tmp_ecdh_file(boost::asio::ssl::context& ctx, bio_cleanup bio = { ::BIO_new_file(certificate.c_str(), "r") }; if (bio.p) { - return do_use_tmp_ecdh_file(ctx, bio.p, ec); + return do_use_tmp_ecdh(ctx, bio.p, ec); } ec = boost::system::error_code( @@ -111,9 +146,36 @@ void use_tmp_ecdh_file(boost::asio::ssl::context& ctx, const std::string& certif boost::asio::detail::throw_error(ec, "use_tmp_ecdh_file"); } +inline +BOOST_ASIO_SYNC_OP_VOID use_tmp_ecdh(boost::asio::ssl::context& ctx, + const boost::asio::const_buffer& certificate, boost::system::error_code& ec) +{ + ::ERR_clear_error(); + + bio_cleanup bio = { ::BIO_new_mem_buf(const_cast(boost::asio::buffer_cast(certificate)), static_cast(boost::asio::buffer_size(certificate))) }; + if (bio.p) + { + return do_use_tmp_ecdh(ctx, bio.p, ec); + } + + ec = boost::system::error_code( + static_cast(::ERR_get_error()), + boost::asio::error::get_ssl_category()); + BOOST_ASIO_SYNC_OP_VOID_RETURN(ec); +} + +inline +void use_tmp_ecdh(boost::asio::ssl::context& ctx, const boost::asio::const_buffer& certificate) +{ + boost::system::error_code ec; + use_tmp_ecdh(ctx, certificate, ec); + boost::asio::detail::throw_error(ec, "use_tmp_ecdh"); +} + } // namespace use_tmp_ecdh_details using use_tmp_ecdh_details::use_tmp_ecdh_file; +using use_tmp_ecdh_details::use_tmp_ecdh; } // namespace ssl } // namespace asio diff --git a/Development/bst/any.h b/Development/bst/any.h new file mode 100644 index 000000000..1ee53af80 --- /dev/null +++ b/Development/bst/any.h @@ -0,0 +1,60 @@ +#ifndef BST_ANY_H +#define BST_ANY_H + +// Provide bst::any, etc. using either std:: or boost:: symbols +// C++17 feature test macro: __has_include(), __cpp_lib_any + +#if !defined(BST_ANY_STD) && !defined(BST_ANY_BOOST) + +#if defined(__GNUC__) +// std::any is available from GCC 7.1, with -std=c++17 +//#if __GNUC__ > 7 || (__GNUC__ == 7 && __GNUC_MINOR__ >= 1) +#if __cplusplus >= 201703L +#define BST_ANY_STD +#else +#define BST_ANY_BOOST +#endif + +#elif defined(_MSC_VER) + +#if _MSC_VER >= 1910 +// From VS2017, /std:c++17 switch is introduced, but this is only indicated in __cplusplus if /Zc:__cplusplus is also specified +#if __cplusplus >= 201703L +#define BST_ANY_STD +#else +#define BST_ANY_BOOST +#endif +#else +// Earlier +#define BST_ANY_BOOST +#endif + +#else + +// Default to C++17 +#define BST_ANY_STD + +#endif + +#endif + +#if defined(BST_ANY_STD) + +#include +namespace bst_any = std; + +#elif defined(BST_ANY_BOOST) + +#include +namespace bst_any = boost; + +#endif + +namespace bst +{ + // Note that Boost.Any does not provide make_any or the in-place any constructors + using bst_any::any; + using bst_any::any_cast; +} + +#endif diff --git a/Development/bst/filesystem.h b/Development/bst/filesystem.h index 24d50cd4c..f5b0a0bb0 100644 --- a/Development/bst/filesystem.h +++ b/Development/bst/filesystem.h @@ -82,6 +82,7 @@ namespace bst using bst_filesystem::file_size; using bst_filesystem::create_directory; using bst_filesystem::remove_all; + using bst_filesystem::temp_directory_path; } } diff --git a/Development/bst/shared_mutex.h b/Development/bst/shared_mutex.h index a7b8cd713..f74e2d8cf 100644 --- a/Development/bst/shared_mutex.h +++ b/Development/bst/shared_mutex.h @@ -2,10 +2,14 @@ #define BST_SHARED_MUTEX_H // Provide bst::shared_mutex, bst::shared_lock etc. using either std:: or boost:: symbols -// To do: Detect whether std::shared_mutex is available using __cplusplus (and compiler/library-specific preprocessor definitions) +#if !defined(BST_SHARED_MUTEX_STD) && !defined(BST_SHARED_MUTEX_BOOST) +// To do: Detect whether std::shared_mutex is available using __cplusplus (and compiler/library-specific preprocessor definitions) // Note: If this isn't defined under the same condition as BST_THREAD_BOOST, adding shared_timed_mutex is problematic // because its timeout functionality won't be consistent with bst::chrono +#define BST_SHARED_MUTEX_BOOST +#endif + #ifndef BST_SHARED_MUTEX_BOOST #include diff --git a/Development/bst/test/detail/boost_1_57_0.h b/Development/bst/test/detail/boost_1_57_0.h index dabf7f04c..4c47aac38 100644 --- a/Development/bst/test/detail/boost_1_57_0.h +++ b/Development/bst/test/detail/boost_1_57_0.h @@ -51,6 +51,7 @@ PRAGMA_WARNING_POP #define BST_CHECK_GT(expected, actual) BOOST_CHECK_GT(expected, actual) #define BST_CHECK_GE(expected, actual) BOOST_CHECK_GE(expected, actual) #define BST_CHECK_THROW(expr, exception) BOOST_CHECK_THROW(expr, exception) +#define BST_CHECK_NO_THROW(expr) BOOST_CHECK_NO_THROW(expr) #define BST_REQUIRE(expr) BOOST_REQUIRE(expr) #define BST_REQUIRE_EQUAL(expected, actual) BOOST_REQUIRE_EQUAL(expected, actual) #define BST_REQUIRE_NE(expected, actual) BOOST_REQUIRE_NE(expected, actual) @@ -59,6 +60,7 @@ PRAGMA_WARNING_POP #define BST_REQUIRE_GT(expected, actual) BOOST_REQUIRE_GT(expected, actual) #define BST_REQUIRE_GE(expected, actual) BOOST_REQUIRE_GE(expected, actual) #define BST_REQUIRE_THROW(expr, exception) BOOST_REQUIRE_THROW(expr, exception) +#define BST_REQUIRE_NO_THROW(expr) BOOST_REQUIRE_NO_THROW(expr) #define BST_WARN(expr) BOOST_WARN(expr) #define BST_WARN_EQUAL(expected, actual) BOOST_WARN_EQUAL(expected, actual) #define BST_WARN_NE(expected, actual) BOOST_WARN_NE(expected, actual) @@ -66,6 +68,8 @@ PRAGMA_WARNING_POP #define BST_WARN_LE(expected, actual) BOOST_WARN_LE(expected, actual) #define BST_WARN_GT(expected, actual) BOOST_WARN_GT(expected, actual) #define BST_WARN_GE(expected, actual) BOOST_WARN_GE(expected, actual) +#define BST_WARN_THROW(expr, exception) BOOST_WARN_THROW(expr, exception) +#define BST_WARN_NO_THROW(expr) BOOST_WARN_NO_THROW(expr) // Explicit STRING macros to work around the different behaviours of the frameworks when comparing two char* or wchar_t* #define BST_CHECK_STRING_EQUAL(expected, actual) BOOST_CHECK_EQUAL(expected, actual) diff --git a/Development/bst/test/detail/boost_1_65_1.h b/Development/bst/test/detail/boost_1_65_1.h index 16ed593fc..4ed0ad9e0 100644 --- a/Development/bst/test/detail/boost_1_65_1.h +++ b/Development/bst/test/detail/boost_1_65_1.h @@ -56,6 +56,7 @@ do { #define BST_CHECK_GT(expected, actual) BOOST_CHECK_GT(expected, actual) #define BST_CHECK_GE(expected, actual) BOOST_CHECK_GE(expected, actual) #define BST_CHECK_THROW(expr, exception) BOOST_CHECK_THROW(expr, exception) +#define BST_CHECK_NO_THROW(expr) BOOST_CHECK_NO_THROW(expr) #define BST_REQUIRE(expr) BOOST_REQUIRE(expr) #define BST_REQUIRE_EQUAL(expected, actual) BOOST_REQUIRE_EQUAL(expected, actual) #define BST_REQUIRE_NE(expected, actual) BOOST_REQUIRE_NE(expected, actual) @@ -64,6 +65,7 @@ do { #define BST_REQUIRE_GT(expected, actual) BOOST_REQUIRE_GT(expected, actual) #define BST_REQUIRE_GE(expected, actual) BOOST_REQUIRE_GE(expected, actual) #define BST_REQUIRE_THROW(expr, exception) BOOST_REQUIRE_THROW(expr, exception) +#define BST_REQUIRE_NO_THROW(expr) BOOST_REQUIRE_NO_THROW(expr) #define BST_WARN(expr) BOOST_WARN(expr) #define BST_WARN_EQUAL(expected, actual) BOOST_WARN_EQUAL(expected, actual) #define BST_WARN_NE(expected, actual) BOOST_WARN_NE(expected, actual) @@ -71,6 +73,8 @@ do { #define BST_WARN_LE(expected, actual) BOOST_WARN_LE(expected, actual) #define BST_WARN_GT(expected, actual) BOOST_WARN_GT(expected, actual) #define BST_WARN_GE(expected, actual) BOOST_WARN_GE(expected, actual) +#define BST_WARN_THROW(expr, exception) BOOST_WARN_THROW(expr, exception) +#define BST_WARN_NO_THROW(expr) BOOST_WARN_NO_THROW(expr) // Explicit STRING macros to work around the different behaviours of the frameworks when comparing two char* or wchar_t* #define BST_CHECK_STRING_EQUAL(expected, actual) BOOST_CHECK_EQUAL(expected, actual) diff --git a/Development/bst/test/detail/catch-1.0.h b/Development/bst/test/detail/catch-1.0.h index 6ae159f4f..d9b984e8f 100644 --- a/Development/bst/test/detail/catch-1.0.h +++ b/Development/bst/test/detail/catch-1.0.h @@ -53,7 +53,9 @@ PRAGMA_WARNING_POP #define CATCH_CHECK_CATCH_AS( exceptionType ) INTERNAL_CATCH_CATCH_AS( exceptionType ) #define CATCH_CHECK_CATCH_AS_NOFAIL( exceptionType ) INTERNAL_CATCH_CATCH_AS( exceptionType ) +#define CATCH_CHECK_THROWS_NOFAIL( expr ) INTERNAL_CATCH_THROWS( expr, Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, "CATCH_CHECK_THROWS_NOFAIL" ) #define CATCH_CHECK_THROWS_AS_NOFAIL( expr, exceptionType ) INTERNAL_CATCH_THROWS_AS( expr, exceptionType, Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, "CATCH_CHECK_THROWS_AS_NOFAIL" ) +#define CATCH_CHECK_NOTHROW_NOFAIL( expr ) INTERNAL_CATCH_NO_THROW( expr, Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, "CATCH_CHECK_NOTHROW_NOFAIL" ) // If CATCH_CONFIG_PREFIX_ALL is not defined then the CATCH_ prefix is not required #else @@ -66,7 +68,9 @@ PRAGMA_WARNING_POP #define CHECK_CATCH_AS( exceptionType ) INTERNAL_CATCH_CATCH_AS( exceptionType ) #define CHECK_CATCH_AS_NOFAIL( exceptionType ) INTERNAL_CATCH_CATCH_AS( exceptionType ) +#define CHECK_THROWS_NOFAIL( expr ) INTERNAL_CATCH_THROWS( expr, Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, "CHECK_THROWS_NOFAIL" ) #define CHECK_THROWS_AS_NOFAIL( expr, exceptionType ) INTERNAL_CATCH_THROWS_AS( expr, exceptionType, Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, "CHECK_THROWS_AS_NOFAIL" ) +#define CHECK_NOTHROW_NOFAIL( expr ) INTERNAL_CATCH_NO_THROW( expr, Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, "CHECK_NOTHROW_NOFAIL" ) #endif //- Break INTERNAL_CATCH_THROWS_AS in two so the statement can contain commas, etc. @@ -222,6 +226,7 @@ PRAGMA_WARNING_POP #define BST_CHECK_GT(expected, actual) CATCH_CHECK((expected) > (actual)) #define BST_CHECK_GE(expected, actual) CATCH_CHECK((expected) >= (actual)) #define BST_CHECK_THROW(expr, exception) CATCH_CHECK_THROWS_AS(expr, exception) +#define BST_CHECK_NO_THROW(expr) CATCH_CHECK_NOTHROW(expr) #define BST_REQUIRE(expr) CATCH_REQUIRE(expr) #define BST_REQUIRE_EQUAL(expected, actual) CATCH_REQUIRE((expected) == (actual)) #define BST_REQUIRE_NE(expected, actual) CATCH_REQUIRE((expected) != (actual)) @@ -230,6 +235,7 @@ PRAGMA_WARNING_POP #define BST_REQUIRE_GT(expected, actual) CATCH_REQUIRE((expected) > (actual)) #define BST_REQUIRE_GE(expected, actual) CATCH_REQUIRE((expected) >= (actual)) #define BST_REQUIRE_THROW(expr, exception) CATCH_REQUIRE_THROWS_AS(expr, exception) +#define BST_REQUIRE_NO_THROW(expr) CATCH_REQUIRE_NOTHROW(expr) #define BST_WARN(expr) CATCH_CHECK_NOFAIL(expr) #define BST_WARN_EQUAL(expected, actual) CATCH_CHECK_NOFAIL((expected) == (actual)) #define BST_WARN_NE(expected, actual) CATCH_CHECK_NOFAIL((expected) != (actual)) @@ -237,6 +243,8 @@ PRAGMA_WARNING_POP #define BST_WARN_LE(expected, actual) CATCH_CHECK_NOFAIL((expected) <= (actual)) #define BST_WARN_GT(expected, actual) CATCH_CHECK_NOFAIL((expected) > (actual)) #define BST_WARN_GE(expected, actual) CATCH_CHECK_NOFAIL((expected) >= (actual)) +#define BST_WARN_THROW(expr, exception) CATCH_CHECK_THROWS_AS_NOFAIL(expr, exception) +#define BST_WARN_NO_THROW(expr) CATCH_CHECK_NOTHROW_NOFAIL(expr) // Explicit STRING macros to work around the different behaviours of the frameworks when comparing two char* or wchar_t* namespace bst_test_detail diff --git a/Development/bst/test/detail/catch-1.10.0.h b/Development/bst/test/detail/catch-1.10.0.h index 90c1370d9..25db7ddbe 100644 --- a/Development/bst/test/detail/catch-1.10.0.h +++ b/Development/bst/test/detail/catch-1.10.0.h @@ -53,7 +53,9 @@ PRAGMA_WARNING_POP #define CATCH_CHECK_CATCH_AS( exceptionType ) INTERNAL_CATCH_CATCH_AS( exceptionType ) #define CATCH_CHECK_CATCH_AS_NOFAIL( exceptionType ) INTERNAL_CATCH_CATCH_AS( exceptionType ) +#define CATCH_CHECK_THROWS_NOFAIL( expr ) INTERNAL_CATCH_THROWS( "CATCH_CHECK_THROWS_NOFAIL", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, "", expr ) #define CATCH_CHECK_THROWS_AS_NOFAIL( expr, exceptionType ) INTERNAL_CATCH_THROWS_AS( "CATCH_CHECK_THROWS_AS_NOFAIL", exceptionType, Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, expr ) +#define CATCH_CHECK_NOTHROW_NOFAIL( expr ) INTERNAL_CATCH_NO_THROW( "CATCH_CHECK_NOTHROW_NOFAIL", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, expr ) // If CATCH_CONFIG_PREFIX_ALL is not defined then the CATCH_ prefix is not required #else @@ -66,7 +68,9 @@ PRAGMA_WARNING_POP #define CHECK_CATCH_AS( exceptionType ) INTERNAL_CATCH_CATCH_AS( exceptionType ) #define CHECK_CATCH_AS_NOFAIL( exceptionType ) INTERNAL_CATCH_CATCH_AS( exceptionType ) +#define CHECK_THROWS_NOFAIL( expr ) INTERNAL_CATCH_THROWS( "CHECK_THROWS_NOFAIL", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, "", expr ) #define CHECK_THROWS_AS_NOFAIL( expr, exceptionType ) INTERNAL_CATCH_THROWS_AS( "CHECK_THROWS_AS_NOFAIL", exceptionType, Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, expr ) +#define CHECK_NOTHROW_NOFAIL( expr ) INTERNAL_CATCH_NO_THROW( "CHECK_NOTHROW_NOFAIL", Catch::ResultDisposition::ContinueOnFailure | Catch::ResultDisposition::SuppressFail, expr ) #endif //- Break INTERNAL_CATCH_THROWS_AS in two so the statement can contain commas, etc. @@ -231,6 +235,7 @@ PRAGMA_WARNING_POP #define BST_CHECK_GT(expected, actual) CATCH_CHECK((expected) > (actual)) #define BST_CHECK_GE(expected, actual) CATCH_CHECK((expected) >= (actual)) #define BST_CHECK_THROW(expr, exception) CATCH_CHECK_THROWS_AS(expr, exception) +#define BST_CHECK_NO_THROW(expr) CATCH_CHECK_NOTHROW(expr) #define BST_REQUIRE(expr) CATCH_REQUIRE(expr) #define BST_REQUIRE_EQUAL(expected, actual) CATCH_REQUIRE((expected) == (actual)) #define BST_REQUIRE_NE(expected, actual) CATCH_REQUIRE((expected) != (actual)) @@ -239,6 +244,7 @@ PRAGMA_WARNING_POP #define BST_REQUIRE_GT(expected, actual) CATCH_REQUIRE((expected) > (actual)) #define BST_REQUIRE_GE(expected, actual) CATCH_REQUIRE((expected) >= (actual)) #define BST_REQUIRE_THROW(expr, exception) CATCH_REQUIRE_THROWS_AS(expr, exception) +#define BST_REQUIRE_NO_THROW(expr) CATCH_REQUIRE_NOTHROW(expr) #define BST_WARN(expr) CATCH_CHECK_NOFAIL(expr) #define BST_WARN_EQUAL(expected, actual) CATCH_CHECK_NOFAIL((expected) == (actual)) #define BST_WARN_NE(expected, actual) CATCH_CHECK_NOFAIL((expected) != (actual)) @@ -246,6 +252,8 @@ PRAGMA_WARNING_POP #define BST_WARN_LE(expected, actual) CATCH_CHECK_NOFAIL((expected) <= (actual)) #define BST_WARN_GT(expected, actual) CATCH_CHECK_NOFAIL((expected) > (actual)) #define BST_WARN_GE(expected, actual) CATCH_CHECK_NOFAIL((expected) >= (actual)) +#define BST_WARN_THROW(expr, exception) CATCH_CHECK_THROWS_AS_NOFAIL(expr, exception) +#define BST_WARN_NO_THROW(expr) CATCH_CHECK_NOTHROW_NOFAIL(expr) // Explicit STRING macros to work around the different behaviours of the frameworks when comparing two char* or wchar_t* namespace bst_test_detail diff --git a/Development/bst/test/detail/googletest-release-1.7.0.h b/Development/bst/test/detail/googletest-release-1.7.0.h index 7f7f58440..fbea91907 100644 --- a/Development/bst/test/detail/googletest-release-1.7.0.h +++ b/Development/bst/test/detail/googletest-release-1.7.0.h @@ -86,6 +86,7 @@ #define BST_CHECK_GT(expected, actual) EXPECT_GT(expected, actual) #define BST_CHECK_GE(expected, actual) EXPECT_GE(expected, actual) #define BST_CHECK_THROW(expr, exception) EXPECT_THROW(expr, exception) +#define BST_CHECK_NO_THROW(expr) EXPECT_NO_THROW(expr) #define BST_REQUIRE(expr) ASSERT_TRUE(expr) #define BST_REQUIRE_EQUAL(expected, actual) ASSERT_EQ(expected, actual) #define BST_REQUIRE_NE(expected, actual) ASSERT_NE(expected, actual) @@ -94,6 +95,7 @@ #define BST_REQUIRE_GT(expected, actual) ASSERT_GT(expected, actual) #define BST_REQUIRE_GE(expected, actual) ASSERT_GE(expected, actual) #define BST_REQUIRE_THROW(expr, exception) ASSERT_THROW(expr, exception) +#define BST_REQUIRE_NO_THROW(expr) ASSERT_NO_THROW(expr) // Hmm, Google Test doesn't have the equivalent of WARN? #define BST_WARN(expr) EXPECT_TRUE(expr) #define BST_WARN_EQUAL(expected, actual) EXPECT_EQ(expected, actual) @@ -102,6 +104,8 @@ #define BST_WARN_LE(expected, actual) EXPECT_LE(expected, actual) #define BST_WARN_GT(expected, actual) EXPECT_GT(expected, actual) #define BST_WARN_GE(expected, actual) EXPECT_GE(expected, actual) +#define BST_WARN_THROW(expr, exception) EXPECT_THROW(expr, exception) +#define BST_WARN_NO_THROW(expr) EXPECT_NO_THROW(expr) // Explicit STRING macros to work around the different behaviours of the frameworks when comparing two char* or wchar_t* namespace bst_test_detail diff --git a/Development/bst/test/detail/googletest-release-1.8.0.h b/Development/bst/test/detail/googletest-release-1.8.0.h index 97b327a79..86af8edd3 100644 --- a/Development/bst/test/detail/googletest-release-1.8.0.h +++ b/Development/bst/test/detail/googletest-release-1.8.0.h @@ -87,6 +87,7 @@ #define BST_CHECK_GT(expected, actual) EXPECT_GT(expected, actual) #define BST_CHECK_GE(expected, actual) EXPECT_GE(expected, actual) #define BST_CHECK_THROW(expr, exception) EXPECT_THROW(expr, exception) +#define BST_CHECK_NO_THROW(expr) EXPECT_NO_THROW(expr) #define BST_REQUIRE(expr) ASSERT_TRUE(expr) #define BST_REQUIRE_EQUAL(expected, actual) ASSERT_EQ(expected, actual) #define BST_REQUIRE_NE(expected, actual) ASSERT_NE(expected, actual) @@ -95,6 +96,7 @@ #define BST_REQUIRE_GT(expected, actual) ASSERT_GT(expected, actual) #define BST_REQUIRE_GE(expected, actual) ASSERT_GE(expected, actual) #define BST_REQUIRE_THROW(expr, exception) ASSERT_THROW(expr, exception) +#define BST_REQUIRE_NO_THROW(expr) ASSERT_NO_THROW(expr) // Hmm, Google Test doesn't have the equivalent of WARN? #define BST_WARN(expr) EXPECT_TRUE(expr) #define BST_WARN_EQUAL(expected, actual) EXPECT_EQ(expected, actual) @@ -103,6 +105,8 @@ #define BST_WARN_LE(expected, actual) EXPECT_LE(expected, actual) #define BST_WARN_GT(expected, actual) EXPECT_GT(expected, actual) #define BST_WARN_GE(expected, actual) EXPECT_GE(expected, actual) +#define BST_WARN_THROW(expr, exception) EXPECT_THROW(expr, exception) +#define BST_WARN_NO_THROW(expr) EXPECT_NO_THROW(expr) // Explicit STRING macros to work around the different behaviours of the frameworks when comparing two char* or wchar_t* namespace bst_test_detail diff --git a/Development/cmake/NmosCppCommon.cmake b/Development/cmake/NmosCppCommon.cmake index dac0b2f51..b9518b7e9 100644 --- a/Development/cmake/NmosCppCommon.cmake +++ b/Development/cmake/NmosCppCommon.cmake @@ -1,304 +1,47 @@ -# nmos-cpp Common CMake setup and dependency checking +# since moving to Conan 2 and CMake 3.24 or higher, the injection point is used to configure conan +# see https://cmake.org/cmake/help/v3.24/variable/CMAKE_PROJECT_TOP_LEVEL_INCLUDES.html +unset(NMOS_CPP_USE_CONAN CACHE) -# caller can set NMOS_CPP_DIR if the project is different -if (NOT DEFINED NMOS_CPP_DIR) - set (NMOS_CPP_DIR ${PROJECT_SOURCE_DIR}) -endif() +include(GNUInstallDirs) + +# if both variables aren't empty strings, join them +string(JOIN "/" NMOS_CPP_INSTALL_INCLUDEDIR ${CMAKE_INSTALL_INCLUDEDIR} ${NMOS_CPP_INCLUDE_PREFIX}) -# compile-time control of logging loquacity -# use slog::never_log_severity to strip all logging at compile-time, or slog::max_verbosity for full control at run-time -set (SLOG_LOGGING_SEVERITY slog::max_verbosity CACHE STRING "Compile-time logging level, e.g. between 40 (least verbose, only fatal messages) and -40 (most verbose)") +set(NMOS_CPP_INSTALL_LIBDIR "${CMAKE_INSTALL_LIBDIR}") +set(NMOS_CPP_INSTALL_BINDIR "${CMAKE_INSTALL_BINDIR}") +if(WIN32) + string(APPEND NMOS_CPP_INSTALL_LIBDIR "/$,Debug,Release>") + string(APPEND NMOS_CPP_INSTALL_BINDIR "/$,Debug,Release>") +endif() -# enable C++11 +# enable C++ enable_language(CXX) -set(CMAKE_CXX_STANDARD 11 CACHE STRING "Default value for CXX_STANDARD property of targets") +# check C++11 or higher if(CMAKE_CXX_STANDARD STREQUAL "98") message(FATAL_ERROR "CMAKE_CXX_STANDARD must be 11 or higher; C++98 is not supported") endif() -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) - -# note: to see the output of any failed tests, set CTEST_OUTPUT_ON_FAILURE=1 in the environment -# and also remember that CMake doesn't add dependencies to the "test" (or "RUN_TESTS") target -# so after changing code under test, it is important to "make all" (or build "ALL_BUILD") -enable_testing() - -# location of additional CMake modules -set(CMAKE_MODULE_PATH - ${CMAKE_MODULE_PATH} - ${CMAKE_BINARY_DIR} - ${NMOS_CPP_DIR}/third_party/cmake - ${NMOS_CPP_DIR}/cmake - ) - -# location of Config.cmake files created by Conan -set(CMAKE_PREFIX_PATH - ${CMAKE_PREFIX_PATH} - ${CMAKE_BINARY_DIR} - ) - -if(${USE_CONAN} AND CMAKE_CONFIGURATION_TYPES AND NOT CMAKE_BUILD_TYPE) - # use Config.cmake - set(FIND_PACKAGE_USE_CONFIG CONFIG) -endif() - -# guard against in-source builds and bad build-type strings -include(safeguards) - -# enable or disable the LLDP support library (lldp_static) -set (BUILD_LLDP OFF CACHE BOOL "Build LLDP support library") - -# find dependencies - -# cpprestsdk -# note: 2.10.16 or higher is recommended (which is the first version with cpprestsdk-configVersion.cmake) -set (CPPRESTSDK_VERSION_MIN "2.10.11") -set (CPPRESTSDK_VERSION_CUR "2.10.17") -find_package(cpprestsdk REQUIRED ${FIND_PACKAGE_USE_CONFIG}) -if (NOT cpprestsdk_VERSION) - message(STATUS "Found cpprestsdk unknown version; minimum version: " ${CPPRESTSDK_VERSION_MIN}) -elseif (cpprestsdk_VERSION VERSION_LESS CPPRESTSDK_VERSION_MIN) - message(FATAL_ERROR "Found cpprestsdk version " ${cpprestsdk_VERSION} " that is lower than the minimum version: " ${CPPRESTSDK_VERSION_MIN}) -elseif(cpprestsdk_VERSION VERSION_GREATER CPPRESTSDK_VERSION_CUR) - message(STATUS "Found cpprestsdk version " ${cpprestsdk_VERSION} " that is higher than the current tested version: " ${CPPRESTSDK_VERSION_CUR}) -else() - message(STATUS "Found cpprestsdk version " ${cpprestsdk_VERSION}) -endif() -if (TARGET cpprestsdk::cpprest) - set(CPPRESTSDK_TARGET cpprestsdk::cpprest) -else() - set(CPPRESTSDK_TARGET cpprestsdk::cpprestsdk) +if(NOT DEFINED CMAKE_CXX_STANDARD_REQUIRED) + set(CMAKE_CXX_STANDARD_REQUIRED ON) endif() -message(STATUS "Using cpprestsdk target ${CPPRESTSDK_TARGET}") -if (DEFINED CPPREST_INCLUDE_DIR) - message(STATUS "Using cpprestsdk include directory at ${CPPREST_INCLUDE_DIR}") +if(NOT DEFINED CMAKE_CXX_EXTENSIONS) + set(CMAKE_CXX_EXTENSIONS OFF) endif() -# websocketpp -# note: good idea to use same version as cpprestsdk was built with! -if(DEFINED WEBSOCKETPP_INCLUDE_DIR) - message(STATUS "Using configured websocketpp include directory at " ${WEBSOCKETPP_INCLUDE_DIR}) -else() - set (WEBSOCKETPP_VERSION_MIN "0.5.1") - set (WEBSOCKETPP_VERSION_CUR "0.8.2") - find_package(websocketpp REQUIRED ${FIND_PACKAGE_USE_CONFIG}) - if (NOT websocketpp_VERSION) - message(STATUS "Found websocketpp unknown version; minimum version: " ${WEBSOCKETPP_VERSION_MIN}) - elseif (websocketpp_VERSION VERSION_LESS WEBSOCKETPP_VERSION_MIN) - message(FATAL_ERROR "Found websocketpp version " ${websocketpp_VERSION} " that is lower than the minimum version: " ${WEBSOCKETPP_VERSION_MIN}) - elseif(websocketpp_VERSION VERSION_GREATER WEBSOCKETPP_VERSION_CUR) - message(STATUS "Found websocketpp version " ${websocketpp_VERSION} " that is higher than the current tested version: " ${WEBSOCKETPP_VERSION_CUR}) - else() - message(STATUS "Found websocketpp version " ${websocketpp_VERSION}) - endif() - if (DEFINED WEBSOCKETPP_INCLUDE_DIR) - message(STATUS "Using websocketpp include directory at ${WEBSOCKETPP_INCLUDE_DIR}") - endif() +if(NMOS_CPP_BUILD_TESTS AND CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + # note: to see the output of any failed tests, set CTEST_OUTPUT_ON_FAILURE=1 in the environment + # and also remember that CMake doesn't add dependencies to the "test" (or "RUN_TESTS") target + # so after changing code under test, it is important to "make all" (or build "ALL_BUILD") + enable_testing() endif() -# boost -# note: some components are only required for one platform or other -# so find_package(Boost) is called after adding those components -list(APPEND FIND_BOOST_COMPONENTS system date_time regex) - -# openssl -# note: good idea to use same version as cpprestsk was built with! -find_package(OpenSSL REQUIRED ${FIND_PACKAGE_USE_CONFIG}) -if (TARGET OpenSSL::SSL) - set(OPENSSL_TARGETS OpenSSL::Crypto OpenSSL::SSL) -else() - set(OPENSSL_TARGETS OpenSSL::OpenSSL) -endif() -message(STATUS "Using OpenSSL target(s) ${OPENSSL_TARGETS}") -if (DEFINED OPENSSL_INCLUDE_DIR) - message(STATUS "Using OpenSSL include directory at ${OPENSSL_INCLUDE_DIR}") -endif() - -# platform-specific dependencies - -if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") - # find Bonjour or Avahi compatibility library for the mDNS support library (mdns_static) - # note: BONJOUR_INCLUDE and BONJOUR_LIB_DIR aren't set, the headers and library are assumed to be installed in the system paths - set (BONJOUR_LIB -ldns_sd) -endif() - -if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") - # find resolver (for cpprest/host_utils.cpp) - list(APPEND PLATFORM_LIBS -lresolv) - - # define __STDC_LIMIT_MACROS to work around C99 vs. C++11 bug in glibc 2.17 - # should be harmless with newer glibc or in other scenarios - # see https://sourceware.org/bugzilla/show_bug.cgi?id=15366 - # and https://sourceware.org/ml/libc-alpha/2013-04/msg00598.html - add_definitions(/D__STDC_LIMIT_MACROS) - - # add dependency required by nmos/filesystem_route.cpp - if((CMAKE_CXX_COMPILER_ID MATCHES GNU) AND (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 5.3)) - list(APPEND PLATFORM_LIBS -lstdc++fs) - else() - list(APPEND FIND_BOOST_COMPONENTS filesystem) - endif() - - if(BUILD_LLDP) - # find libpcap for the LLDP support library (lldp_static) - set (PCAP_LIB -lpcap) - endif() -endif() - -if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") - # find Bonjour for the mDNS support library (mdns_static) - set (MDNS_SYSTEM_BONJOUR OFF CACHE BOOL "Use installed Bonjour SDK") - if(MDNS_SYSTEM_BONJOUR) - # note: BONJOUR_INCLUDE and BONJOUR_LIB_DIR are now set by default to the location used by the Bonjour SDK Installer (bonjoursdksetup.exe) 3.0.0 - set (BONJOUR_INCLUDE "$ENV{PROGRAMFILES}/Bonjour SDK/Include" CACHE PATH "Bonjour SDK include directory") - set (BONJOUR_LIB_DIR "$ENV{PROGRAMFILES}/Bonjour SDK/Lib/x64" CACHE PATH "Bonjour SDK library directory") - set (BONJOUR_LIB dnssd) - # dnssd.lib is built with /MT, so exclude libcmt if we're building nmos-cpp with the dynamically-linked runtime library - if(CMAKE_VERSION VERSION_LESS 3.15) - foreach(Config ${CMAKE_CONFIGURATION_TYPES}) - string(TOUPPER ${Config} CONFIG) - # default is /MD or /MDd - if(NOT("${CMAKE_CXX_FLAGS_${CONFIG}}" MATCHES "/MT")) - message(STATUS "Excluding libcmt for ${Config} because CMAKE_CXX_FLAGS_${CONFIG} is: ${CMAKE_CXX_FLAGS_${CONFIG}}") - set (CMAKE_EXE_LINKER_FLAGS_${CONFIG} "${CMAKE_EXE_LINKER_FLAGS_${CONFIG}} /NODEFAULTLIB:libcmt") - endif() - endforeach() - else() - # default is "MultiThreaded$<$:Debug>DLL" - if((NOT DEFINED CMAKE_MSVC_RUNTIME_LIBRARY) OR (${CMAKE_MSVC_RUNTIME_LIBRARY} MATCHES "DLL")) - message(STATUS "Excluding libcmt because CMAKE_MSVC_RUNTIME_LIBRARY is: ${CMAKE_MSVC_RUNTIME_LIBRARY}") - set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /NODEFAULTLIB:libcmt") - endif() - endif() - else() - # note: use the patched files rather than the system installed version - set (BONJOUR_INCLUDE "${NMOS_CPP_DIR}/third_party/mDNSResponder/mDNSShared") - unset (BONJOUR_LIB_DIR) - unset (BONJOUR_LIB) - set (BONJOUR_SOURCES - ${NMOS_CPP_DIR}/third_party/mDNSResponder/mDNSWindows/DLLStub/DLLStub.cpp - ) - set_property( - SOURCE ${NMOS_CPP_DIR}/third_party/mDNSResponder/mDNSWindows/DLLStub/DLLStub.cpp - PROPERTY COMPILE_DEFINITIONS - WIN32_LEAN_AND_MEAN - ) - set (BONJOUR_HEADERS - ${NMOS_CPP_DIR}/third_party/mDNSResponder/mDNSWindows/DLLStub/DLLStub.h - ) - endif() - - # define _WIN32_WINNT because Boost.Asio gets terribly noisy otherwise - # notes: - # cpprestsdk adds /D_WIN32_WINNT=0x0600 (Windows Vista) explicitly... - # calculating the value from CMAKE_SYSTEM_VERSION might be better? - # adding a force include for could be another option - # see: - # https://docs.microsoft.com/en-gb/cpp/porting/modifying-winver-and-win32-winnt - # https://stackoverflow.com/questions/9742003/platform-detection-in-cmake - add_definitions(/D_WIN32_WINNT=0x0600) - - if(BUILD_LLDP) - # find WinPcap for the LLDP support library (lldp_static) - set (PCAP_INCLUDE_DIR "${NMOS_CPP_DIR}/third_party/WpdPack/Include" CACHE PATH "WinPcap include directory") - set (PCAP_LIB_DIR "${NMOS_CPP_DIR}/third_party/WpdPack/Lib/x64" CACHE PATH "WinPcap library directory") - set (PCAP_LIB wpcap) - - # enable 'new' WinPcap functions like pcap_open, pcap_findalldevs_ex - add_definitions(/DHAVE_REMOTE) - endif() -endif() - -# since std::shared_mutex is not available until C++17 -list(APPEND FIND_BOOST_COMPONENTS thread) -add_definitions(/DBST_SHARED_MUTEX_BOOST) - -# find boost -set (BOOST_VERSION_MIN "1.54.0") -set (BOOST_VERSION_CUR "1.75.0") -# note: 1.57.0 doesn't work due to https://svn.boost.org/trac10/ticket/10754 -find_package(Boost ${BOOST_VERSION_MIN} REQUIRED COMPONENTS ${FIND_BOOST_COMPONENTS} ${FIND_PACKAGE_USE_CONFIG}) -# cope with historical versions of FindBoost.cmake -if (DEFINED Boost_VERSION_STRING) - set(Boost_VERSION_COMPONENTS "${Boost_VERSION_STRING}") -elseif (DEFINED Boost_VERSION_MAJOR) - set(Boost_VERSION_COMPONENTS "${Boost_VERSION_MAJOR}.${Boost_VERSION_MINOR}.${Boost_VERSION_PATCH}") -elseif (DEFINED Boost_MAJOR_VERSION) - set(Boost_VERSION_COMPONENTS "${Boost_MAJOR_VERSION}.${Boost_MINOR_VERSION}.${Boost_SUBMINOR_VERSION}") -elseif (DEFINED Boost_VERSION) - set(Boost_VERSION_COMPONENTS "${Boost_VERSION}") -else() - message(FATAL_ERROR "Boost_VERSION_STRING is not defined") -endif() -if (Boost_VERSION_COMPONENTS VERSION_LESS BOOST_VERSION_MIN) - message(FATAL_ERROR "Found Boost version " ${Boost_VERSION_COMPONENTS} " that is lower than the minimum version: " ${BOOST_VERSION_MIN}) -elseif(Boost_VERSION_COMPONENTS VERSION_GREATER BOOST_VERSION_CUR) - message(STATUS "Found Boost version " ${Boost_VERSION_COMPONENTS} " that is higher than the current tested version: " ${BOOST_VERSION_CUR}) -else() - message(STATUS "Found Boost version " ${Boost_VERSION_COMPONENTS}) -endif() -if (DEFINED Boost_INCLUDE_DIRS) - message(STATUS "Using Boost include directories at ${Boost_INCLUDE_DIRS}") -endif() - -# set common C++ compiler flags -if(CMAKE_CXX_COMPILER_ID MATCHES GNU) - set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g3") - set(CMAKE_CXX_FLAGS_RELEASE "-O3") -elseif(MSVC) - # set CharacterSet to Unicode rather than MultiByte - add_definitions(/DUNICODE /D_UNICODE) -endif() - -# set most compiler warnings on -if(CMAKE_CXX_COMPILER_ID MATCHES GNU) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wstrict-aliasing -fstrict-aliasing -Wextra -Wno-unused-parameter -pedantic -Wno-long-long") - if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-missing-field-initializers") - endif() - if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DJSON_SCHEMA_BOOST_REGEX") - endif() - if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.8) - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fpermissive -D_GLIBCXX_USE_NANOSLEEP -DBOOST_RESULT_OF_USE_DECLTYPE -DSLOG_DETAIL_NO_REF_QUALIFIERS -DJSON_SCHEMA_BOOST_REGEX") - endif() -elseif(MSVC) - if(CMAKE_CXX_FLAGS MATCHES "/W[0-4]") - string(REGEX REPLACE "/W[0-4]" "/W4" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}") - else() - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4") - endif() - set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FI\"${NMOS_CPP_DIR}/detail/vc_disable_warnings.h\"") -endif() - -# location of header files (should be using specific target_include_directories?) -# though these will be determined from INTERFACE_INCLUDE_DIRECTORIES for targets -# mentioned in target_link_libraries -include_directories( - ${NMOS_CPP_DIR} - ${NMOS_CPP_DIR}/third_party - ${NMOS_CPP_DIR}/third_party/nlohmann - ${CPPREST_INCLUDE_DIR} - ${WEBSOCKETPP_INCLUDE_DIR} - ${Boost_INCLUDE_DIRS} - ${OPENSSL_INCLUDE_DIR} - ${BONJOUR_INCLUDE} - ${PCAP_INCLUDE_DIR} - ) - -# location of libraries -link_directories( - ${Boost_LIBRARY_DIRS} - ${BONJOUR_LIB_DIR} - ${PCAP_LIB_DIR} +# location of additional CMake modules +list(APPEND CMAKE_MODULE_PATH + ${CMAKE_CURRENT_SOURCE_DIR}/third_party/cmake + ${CMAKE_CURRENT_SOURCE_DIR}/cmake ) -# additional configuration for common dependencies - -# cpprestsdk -if (MSVC AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.10 AND Boost_VERSION_COMPONENTS VERSION_GREATER_EQUAL 1.58.0) - set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FI\"${NMOS_CPP_DIR}/cpprest/details/boost_u_workaround.h\"") -endif() +# guard against in-source builds and bad build-type strings +include(safeguards) -# slog -add_definitions(/DSLOG_STATIC /DSLOG_LOGGING_SEVERITY=${SLOG_LOGGING_SEVERITY}) +# common compiler flags and warnings +include(cmake/NmosCppCompileSettings.cmake) diff --git a/Development/cmake/NmosCppCompileSettings.cmake b/Development/cmake/NmosCppCompileSettings.cmake new file mode 100644 index 000000000..6b0128e6e --- /dev/null +++ b/Development/cmake/NmosCppCompileSettings.cmake @@ -0,0 +1,61 @@ +add_library(compile-settings INTERFACE) +target_compile_features(compile-settings INTERFACE cxx_std_11) + +# set common C++ compiler flags +if(CMAKE_CXX_COMPILER_ID MATCHES GNU) + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.8) + # required for std::this_thread::sleep_for in e.g. mdns/test/mdns_test.cpp + # see https://stackoverflow.com/questions/12523122/what-is-glibcxx-use-nanosleep-all-about + target_compile_definitions(compile-settings INTERFACE _GLIBCXX_USE_NANOSLEEP) + endif() +elseif(MSVC) + # set CharacterSet to Unicode rather than MultiByte + target_compile_definitions(compile-settings INTERFACE UNICODE _UNICODE) +endif() + +# set most compiler warnings on +if(CMAKE_CXX_COMPILER_ID MATCHES GNU) + target_compile_options(compile-settings INTERFACE + -Wall + -Wstrict-aliasing + -fstrict-aliasing + -Wextra + -Wno-unused-parameter + -pedantic + -Wno-long-long + ) + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 5) + target_compile_options(compile-settings INTERFACE + -Wno-missing-field-initializers + ) + endif() + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.8) + target_compile_options(compile-settings INTERFACE + -fpermissive + ) + endif() +elseif(MSVC) + # see https://cmake.org/cmake/help/latest/policy/CMP0092.html + target_compile_options(compile-settings INTERFACE /W4) + target_compile_options(compile-settings INTERFACE "$") + target_compile_options(compile-settings INTERFACE "$/${NMOS_CPP_INSTALL_INCLUDEDIR}/detail/vc_disable_warnings.h>") + + set(COMPILE_SETTINGS_DETAIL_HEADERS + detail/vc_disable_dll_warnings.h + detail/vc_disable_warnings.h + ) + install(FILES ${COMPILE_SETTINGS_DETAIL_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/detail) + + # Conan packages usually don't include PDB files so suppress the resulting warning + # which is otherwise reported more than 500 times (across cpprest.pdb, ossl_static.pdb and zlibstatic.pdb) + # when linking to nmos-cpp and its dependencies + # see https://github.com/conan-io/conan-center-index/blob/master/docs/faqs.md#why-pdb-files-are-not-allowed + # and https://github.com/conan-io/conan-center-index/issues/1982 + target_link_options( + compile-settings INTERFACE + /ignore:4099 + ) +endif() + +list(APPEND NMOS_CPP_TARGETS compile-settings) +add_library(nmos-cpp::compile-settings ALIAS compile-settings) diff --git a/Development/cmake/NmosCppConan.cmake b/Development/cmake/NmosCppConan.cmake deleted file mode 100644 index 023a563c8..000000000 --- a/Development/cmake/NmosCppConan.cmake +++ /dev/null @@ -1,28 +0,0 @@ -if(NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake") - message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan") - file(DOWNLOAD "https://github.com/conan-io/cmake-conan/raw/v0.15/conan.cmake" - "${CMAKE_BINARY_DIR}/conan.cmake") -endif() - -include(${CMAKE_BINARY_DIR}/conan.cmake) - -set (NMOS_CPP_CONAN_BUILD_LIBS "missing" CACHE STRING "Semicolon separated list of libraries to build rather than download") -set (NMOS_CPP_CONAN_OPTIONS "" CACHE STRING "Semicolon separated list of Conan options") - -if(CMAKE_CONFIGURATION_TYPES AND NOT CMAKE_BUILD_TYPE) - # e.g. Visual Studio - conan_cmake_run(CONANFILE conanfile.txt - BASIC_SETUP - GENERATORS cmake_find_package_multi - KEEP_RPATHS - OPTIONS ${NMOS_CPP_CONAN_OPTIONS} - BUILD ${NMOS_CPP_CONAN_BUILD_LIBS}) -else() - conan_cmake_run(CONANFILE conanfile.txt - BASIC_SETUP - NO_OUTPUT_DIRS - GENERATORS cmake_find_package - KEEP_RPATHS - OPTIONS ${NMOS_CPP_CONAN_OPTIONS} - BUILD ${NMOS_CPP_CONAN_BUILD_LIBS}) -endif() diff --git a/Development/cmake/NmosCppDependencies.cmake b/Development/cmake/NmosCppDependencies.cmake new file mode 100644 index 000000000..b57d6e04a --- /dev/null +++ b/Development/cmake/NmosCppDependencies.cmake @@ -0,0 +1,497 @@ +# Boost + +set(BOOST_VERSION_MIN "1.54.0") +set(BOOST_VERSION_CUR "1.83.0") +# note: 1.57.0 doesn't work due to https://svn.boost.org/trac10/ticket/10754 +# note: some components are only required for one platform or other +# so find_package(Boost) is called after adding those components +# adding the "headers" component seems to be unnecessary (and the target alias "boost" doesn't work at all) +list(APPEND FIND_BOOST_COMPONENTS system date_time regex) +if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + if(NOT (CMAKE_CXX_COMPILER_ID MATCHES GNU AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 5.3)) + # add filesystem (for bst/filesystem.h, used by nmos/filesystem_route.cpp) + list(APPEND FIND_BOOST_COMPONENTS filesystem) + endif() +endif() +# since std::shared_mutex is not available until C++17 +# see bst/shared_mutex.h +list(APPEND FIND_BOOST_COMPONENTS thread) +find_package(Boost ${BOOST_VERSION_MIN} REQUIRED COMPONENTS ${FIND_BOOST_COMPONENTS}) +# cope with historical versions of FindBoost.cmake +if(DEFINED Boost_VERSION_STRING) + set(Boost_VERSION_COMPONENTS "${Boost_VERSION_STRING}") +elseif(DEFINED Boost_VERSION_MAJOR) + set(Boost_VERSION_COMPONENTS "${Boost_VERSION_MAJOR}.${Boost_VERSION_MINOR}.${Boost_VERSION_PATCH}") +elseif(DEFINED Boost_MAJOR_VERSION) + set(Boost_VERSION_COMPONENTS "${Boost_MAJOR_VERSION}.${Boost_MINOR_VERSION}.${Boost_SUBMINOR_VERSION}") +elseif(DEFINED Boost_VERSION) + set(Boost_VERSION_COMPONENTS "${Boost_VERSION}") +else() + message(FATAL_ERROR "Boost_VERSION_STRING is not defined") +endif() +if(Boost_VERSION_COMPONENTS VERSION_LESS BOOST_VERSION_MIN) + message(FATAL_ERROR "Found Boost version " ${Boost_VERSION_COMPONENTS} " that is lower than the minimum version: " ${BOOST_VERSION_MIN}) +elseif(Boost_VERSION_COMPONENTS VERSION_GREATER BOOST_VERSION_CUR) + message(STATUS "Found Boost version " ${Boost_VERSION_COMPONENTS} " that is higher than the current tested version: " ${BOOST_VERSION_CUR}) +else() + message(STATUS "Found Boost version " ${Boost_VERSION_COMPONENTS}) +endif() +if(DEFINED Boost_INCLUDE_DIRS) + message(STATUS "Using Boost include directories at ${Boost_INCLUDE_DIRS}") +endif() +# Boost_LIBRARIES is provided by the CMake FindBoost.cmake module and recently also by Conan for most generators +# but with cmake_find_package_multi it isn't, and mapping the required components to targets seems robust anyway +string(REGEX REPLACE "([^;]+)" "Boost::\\1" BOOST_TARGETS "${FIND_BOOST_COMPONENTS}") +message(STATUS "Using Boost targets ${BOOST_TARGETS}, not Boost libraries ${Boost_LIBRARIES}") + +# this target means the nmos-cpp libraries can just link a single Boost dependency +# and also provides a common location to inject some additional compile definitions +add_library(Boost INTERFACE) +target_link_libraries(Boost INTERFACE "${BOOST_TARGETS}") +if(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + # Boost.Uuid needs and therefore auto-links bcrypt by default on Windows since 1.67.0 + # but provides this definition to force that behaviour because if find_package(Boost) + # found BoostConfig.cmake, the Boost:: targets all define BOOST_ALL_NO_LIB + target_compile_definitions( + Boost INTERFACE + BOOST_UUID_FORCE_AUTO_LINK + ) + # define _WIN32_WINNT because Boost.Asio gets terribly noisy otherwise + # note: adding a force include for could be another option + # see: + # https://docs.microsoft.com/en-gb/cpp/porting/modifying-winver-and-win32-winnt + # https://stackoverflow.com/questions/9742003/platform-detection-in-cmake + if(${CMAKE_SYSTEM_VERSION} VERSION_GREATER_EQUAL 10) # Windows 10 + set(WIN32_WINNT 0x0A00) + elseif(${CMAKE_SYSTEM_VERSION} VERSION_GREATER_EQUAL 6.3) # Windows 8.1 + set(WIN32_WINNT 0x0603) + elseif(${CMAKE_SYSTEM_VERSION} VERSION_GREATER_EQUAL 6.2) # Windows 8 + set(WIN32_WINNT 0x0602) + elseif(${CMAKE_SYSTEM_VERSION} VERSION_GREATER_EQUAL 6.1) # Windows 7 + set(WIN32_WINNT 0x0601) + elseif(${CMAKE_SYSTEM_VERSION} VERSION_GREATER_EQUAL 6.0) # Windows Vista + set(WIN32_WINNT 0x0600) + else() # Windows XP (5.1) + set(WIN32_WINNT 0x0501) + endif() + target_compile_definitions( + Boost INTERFACE + _WIN32_WINNT=${WIN32_WINNT} + ) +endif() +if(CMAKE_CXX_COMPILER_ID MATCHES GNU) + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.8) + target_compile_definitions( + Boost INTERFACE + BOOST_RESULT_OF_USE_DECLTYPE + ) + endif() +endif() + +list(APPEND NMOS_CPP_TARGETS Boost) +add_library(nmos-cpp::Boost ALIAS Boost) + +# cpprestsdk + +# note: 2.10.16 or higher is recommended (which is the first version with cpprestsdk-configVersion.cmake) +set(CPPRESTSDK_VERSION_MIN "2.10.11") +set(CPPRESTSDK_VERSION_CUR "2.10.19") +find_package(cpprestsdk REQUIRED) +if(NOT cpprestsdk_VERSION) + message(STATUS "Found cpprestsdk unknown version; minimum version: " ${CPPRESTSDK_VERSION_MIN}) +elseif(cpprestsdk_VERSION VERSION_LESS CPPRESTSDK_VERSION_MIN) + message(FATAL_ERROR "Found cpprestsdk version " ${cpprestsdk_VERSION} " that is lower than the minimum version: " ${CPPRESTSDK_VERSION_MIN}) +elseif(cpprestsdk_VERSION VERSION_GREATER CPPRESTSDK_VERSION_CUR) + message(STATUS "Found cpprestsdk version " ${cpprestsdk_VERSION} " that is higher than the current tested version: " ${CPPRESTSDK_VERSION_CUR}) +else() + message(STATUS "Found cpprestsdk version " ${cpprestsdk_VERSION}) +endif() + +# this target provides a common location to inject additional compile options +add_library(cpprestsdk INTERFACE) +target_link_libraries(cpprestsdk INTERFACE cpprestsdk::cpprest) +if(MSVC AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 19.10 AND Boost_VERSION_COMPONENTS VERSION_GREATER_EQUAL 1.58.0) + target_compile_options(cpprestsdk INTERFACE "$") + target_compile_options(cpprestsdk INTERFACE "$/${NMOS_CPP_INSTALL_INCLUDEDIR}/cpprest/details/boost_u_workaround.h>") + # note: the Boost::boost target has been around longer but these days is an alias for Boost::headers + # when using either BoostConfig.cmake from installed boost or FindBoost.cmake from CMake + target_link_libraries(cpprestsdk INTERFACE Boost::boost) +endif() + +list(APPEND NMOS_CPP_TARGETS cpprestsdk) +add_library(nmos-cpp::cpprestsdk ALIAS cpprestsdk) + +# websocketpp + +# note: good idea to use same version as cpprestsdk was built with! +if(DEFINED WEBSOCKETPP_INCLUDE_DIR) + message(STATUS "Using configured websocketpp include directory at " ${WEBSOCKETPP_INCLUDE_DIR}) +else() + set(WEBSOCKETPP_VERSION_MIN "0.5.1") + set(WEBSOCKETPP_VERSION_CUR "0.8.2") + find_package(websocketpp REQUIRED) + if(NOT websocketpp_VERSION) + message(STATUS "Found websocketpp unknown version; minimum version: " ${WEBSOCKETPP_VERSION_MIN}) + elseif(websocketpp_VERSION VERSION_LESS WEBSOCKETPP_VERSION_MIN) + message(FATAL_ERROR "Found websocketpp version " ${websocketpp_VERSION} " that is lower than the minimum version: " ${WEBSOCKETPP_VERSION_MIN}) + elseif(websocketpp_VERSION VERSION_GREATER WEBSOCKETPP_VERSION_CUR) + message(STATUS "Found websocketpp version " ${websocketpp_VERSION} " that is higher than the current tested version: " ${WEBSOCKETPP_VERSION_CUR}) + else() + message(STATUS "Found websocketpp version " ${websocketpp_VERSION}) + endif() + if(DEFINED WEBSOCKETPP_INCLUDE_DIR) + message(STATUS "Using websocketpp include directory at ${WEBSOCKETPP_INCLUDE_DIR}") + endif() +endif() + +# this target provides a common location to inject additional compile definitions +add_library(websocketpp INTERFACE) +if(TARGET websocketpp::websocketpp) + target_link_libraries(websocketpp INTERFACE websocketpp::websocketpp) +else() + target_include_directories(websocketpp INTERFACE "${WEBSOCKETPP_INCLUDE_DIR}") +endif() +if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + # define __STDC_LIMIT_MACROS to work around C99 vs. C++11 bug in glibc 2.17 + # should be harmless with newer glibc or in other scenarios + # see https://sourceware.org/bugzilla/show_bug.cgi?id=15366 + # and https://sourceware.org/ml/libc-alpha/2013-04/msg00598.html + target_compile_definitions( + websocketpp INTERFACE + __STDC_LIMIT_MACROS + ) +endif() + +list(APPEND NMOS_CPP_TARGETS websocketpp) +add_library(nmos-cpp::websocketpp ALIAS websocketpp) + +# OpenSSL + +# note: good idea to use same version as cpprestsdk was built with! +find_package(OpenSSL REQUIRED) +if(NOT OpenSSL_VERSION) + message(STATUS "Found OpenSSL unknown version") +else() + message(STATUS "Found OpenSSL version " ${OpenSSL_VERSION}) +endif() +if(DEFINED OPENSSL_INCLUDE_DIR) + message(STATUS "Using OpenSSL include directory at ${OPENSSL_INCLUDE_DIR}") +endif() + +# this target means the nmos-cpp libraries can just link a single OpenSSL dependency +add_library(OpenSSL INTERFACE) +target_link_libraries(OpenSSL INTERFACE OpenSSL::Crypto OpenSSL::SSL) + +list(APPEND NMOS_CPP_TARGETS OpenSSL) +add_library(nmos-cpp::OpenSSL ALIAS OpenSSL) + +# json schema validator library + +set(NMOS_CPP_USE_SUPPLIED_JSON_SCHEMA_VALIDATOR OFF CACHE BOOL "Use supplied third_party/nlohmann") +if(NOT NMOS_CPP_USE_SUPPLIED_JSON_SCHEMA_VALIDATOR) + set(JSON_SCHEMA_VALIDATOR_VERSION_MIN "2.1.0") + set(JSON_SCHEMA_VALIDATOR_VERSION_CUR "2.3.0") + find_package(nlohmann_json_schema_validator REQUIRED) + if(NOT nlohmann_json_schema_validator_VERSION) + message(STATUS "Found nlohmann_json_schema_validator unknown version; minimum version: " ${JSON_SCHEMA_VALIDATOR_VERSION_MIN}) + elseif(nlohmann_json_schema_validator_VERSION VERSION_LESS JSON_SCHEMA_VALIDATOR_VERSION_MIN) + message(FATAL_ERROR "Found nlohmann_json_schema_validator version " ${nlohmann_json_schema_validator_VERSION} " that is lower than the minimum version: " ${JSON_SCHEMA_VALIDATOR_VERSION_MIN}) + elseif(nlohmann_json_schema_validator_VERSION VERSION_GREATER JSON_SCHEMA_VALIDATOR_VERSION_CUR) + message(STATUS "Found nlohmann_json_schema_validator version " ${nlohmann_json_schema_validator_VERSION} " that is higher than the current tested version: " ${JSON_SCHEMA_VALIDATOR_VERSION_CUR}) + else() + message(STATUS "Found nlohmann_json_schema_validator version " ${nlohmann_json_schema_validator_VERSION}) + endif() + + set(NLOHMANN_JSON_VERSION_MIN "3.6.0") + set(NLOHMANN_JSON_VERSION_CUR "3.11.3") + find_package(nlohmann_json REQUIRED) + if(NOT nlohmann_json_VERSION) + message(STATUS "Found nlohmann_json unknown version; minimum version: " ${NLOHMANN_JSON_VERSION_MIN}) + elseif(nlohmann_json_VERSION VERSION_LESS NLOHMANN_JSON_VERSION_MIN) + message(FATAL_ERROR "Found nlohmann_json version " ${nlohmann_json_VERSION} " that is lower than the minimum version: " ${NLOHMANN_JSON_VERSION_MIN}) + elseif(nlohmann_json_VERSION VERSION_GREATER NLOHMANN_JSON_VERSION_CUR) + message(STATUS "Found nlohmann_json version " ${nlohmann_json_VERSION} " that is higher than the current tested version: " ${NLOHMANN_JSON_VERSION_CUR}) + else() + message(STATUS "Found nlohmann_json version " ${nlohmann_json_VERSION}) + endif() + + add_library(json_schema_validator INTERFACE) + target_link_libraries(json_schema_validator INTERFACE nlohmann_json_schema_validator nlohmann_json::nlohmann_json) +else() + message(STATUS "Using sources at third_party/nlohmann instead of external \"nlohman_json_schema_validator\" and \"nlohmann_json\" packages.") + + set(JSON_SCHEMA_VALIDATOR_SOURCES + third_party/nlohmann/json-patch.cpp + third_party/nlohmann/json-schema-draft7.json.cpp + third_party/nlohmann/json-uri.cpp + third_party/nlohmann/json-validator.cpp + third_party/nlohmann/smtp-address-validator.cpp + third_party/nlohmann/string-format-check.cpp + ) + + set(JSON_SCHEMA_VALIDATOR_HEADERS + third_party/nlohmann/json-patch.hpp + third_party/nlohmann/json-schema.hpp + third_party/nlohmann/json.hpp + third_party/nlohmann/smtp-address-validator.hpp + ) + + add_library( + json_schema_validator STATIC + ${JSON_SCHEMA_VALIDATOR_SOURCES} + ${JSON_SCHEMA_VALIDATOR_HEADERS} + ) + + source_group("Source Files" FILES ${JSON_SCHEMA_VALIDATOR_SOURCES}) + source_group("Header Files" FILES ${JSON_SCHEMA_VALIDATOR_HEADERS}) + + if(CMAKE_CXX_COMPILER_ID MATCHES GNU) + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.9) + target_compile_definitions( + json_schema_validator PRIVATE + JSON_SCHEMA_BOOST_REGEX + ) + target_link_libraries( + json_schema_validator PRIVATE + Boost::regex + ) + endif() + endif() + + target_link_libraries( + json_schema_validator PRIVATE + nmos-cpp::compile-settings + ) + target_include_directories(json_schema_validator PUBLIC + $ + $ + ) +endif() + +list(APPEND NMOS_CPP_TARGETS json_schema_validator) +add_library(nmos-cpp::json_schema_validator ALIAS json_schema_validator) + +# DNS-SD library + +# this target means the nmos-cpp libraries can link the same dependency +# even if the DLL stub library is built from the patched sources on Windows +add_library(DNSSD INTERFACE) + +macro(find_avahi) + # third_party/cmake/FindAvahi.cmake uses the package name and target namespace 'Avahi' + # but newer revisions of the 'avahi' recipe on Conan Center Index do not override the conan default + # for cmake package name and target namespace, which is the conan package name lower-cased... + # find_package treats as case-insensitive when searching for a find module or + # config package, and ultimately creates _FOUND and _VERSION with the + # specified case, but that doesn't apply to targets created by the find module or config package + # so one find_package call is sufficient here, but need to detect the different target namespace + find_package(Avahi REQUIRED) + if(NOT Avahi_VERSION) + message(STATUS "Found Avahi unknown version") + else() + message(STATUS "Found Avahi version " ${Avahi_VERSION}) + endif() + + if(TARGET Avahi::compat-libdns_sd) + target_link_libraries(DNSSD INTERFACE Avahi::compat-libdns_sd) + else() + target_link_libraries(DNSSD INTERFACE avahi::compat-libdns_sd) + endif() +endmacro() + +macro(find_mdnsresponder) + # third_party/cmake/FindDNSSD.cmake uses the package name and target namespace 'DNSSD' + # but newer revisions of the 'mdnsresponder' recipe on CCI may not override the conan default + # for cmake package name and target namespace, which is the conan package name lower-cased... + # so may need two find_package attempts here + find_package(mdnsresponder QUIET) + if(mdnsresponder_FOUND) + if(NOT mdnsresponder_VERSION) + message(STATUS "Found mdnsresponder unknown version") + else() + message(STATUS "Found mdnsresponder version " ${mdnsresponder_VERSION}) + endif() + + target_link_libraries(DNSSD INTERFACE mdnsresponder::mdnsresponder) + else() + message(STATUS "Could not find a package configuration file provided by \"mdnsresponder\".\n" + "Trying \"DNSSD\" instead.") + find_package(DNSSD REQUIRED) + + if(NOT DNSSD_VERSION) + message(STATUS "Found DNSSD unknown version") + else() + message(STATUS "Found DNSSD version " ${DNSSD_VERSION}) + endif() + + target_link_libraries(DNSSD INTERFACE DNSSD::DNSSD) + endif() +endmacro() + +if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + # find Bonjour or Avahi compatibility library for the mDNS support library (mdns) + set(NMOS_CPP_USE_AVAHI ON CACHE BOOL "Use Avahi compatibility library rather than mDNSResponder") + if(NMOS_CPP_USE_AVAHI) + find_avahi() + else() + find_mdnsresponder() + endif() +elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + # find Bonjour for the mDNS support library (mdns) + # on Windows, there are three components involved - the Bonjour service, the client DLL (dnssd.dll), and the DLL stub library (dnssd.lib) + # the first two are installed by Bonjour64.msi, which is part of the Bonjour SDK or can be extracted from BonjourPSSetup.exe (print service installer) + # the DLL is commonly installed in C:\Windows\System32 + # the DLL stub library is provided by Bonjour SDK instead of a straightforward import library, and uses LoadLibrary and GetProcAddress to load the DLL + # however, the DLL stub library is built with /MT and has a bug which affects DNSServiceRegisterRecord, hence we default to building a patched version + # either way, the Bonjour service and the client DLL (dnssd.dll) still need to be installed on the target system + set(NMOS_CPP_USE_BONJOUR_SDK OFF CACHE BOOL "Use dnssd.lib from the installed Bonjour SDK") + mark_as_advanced(FORCE NMOS_CPP_USE_BONJOUR_SDK) + if(NMOS_CPP_USE_BONJOUR_SDK) + find_mdnsresponder() + + # dnssd.lib is built with /MT, so exclude libcmt if we're building nmos-cpp with the dynamically-linked runtime library + # default is "MultiThreaded$<$:Debug>DLL" + # see https://cmake.org/cmake/help/latest/policy/CMP0091.html + if((NOT DEFINED CMAKE_MSVC_RUNTIME_LIBRARY) OR (${CMAKE_MSVC_RUNTIME_LIBRARY} MATCHES "DLL")) + message(STATUS "Excluding libcmt because CMAKE_MSVC_RUNTIME_LIBRARY is: ${CMAKE_MSVC_RUNTIME_LIBRARY}") + target_link_options(DNSSD INTERFACE /NODEFAULTLIB:libcmt) + endif() + else() + message(STATUS "Using sources at third_party/mDNSResponder instead of external \"mdnsresponder\" package.") + + # hm, where best to install dns_sd.h? + set(BONJOUR_INCLUDE + "$" + "$" + ) + set(BONJOUR_SOURCES + third_party/mDNSResponder/mDNSWindows/DLLStub/DLLStub.cpp + ) + set_property( + SOURCE third_party/mDNSResponder/mDNSWindows/DLLStub/DLLStub.cpp + PROPERTY COMPILE_DEFINITIONS + WIN32_LEAN_AND_MEAN + ) + set(BONJOUR_HEADERS + third_party/mDNSResponder/mDNSWindows/DLLStub/DLLStub.h + ) + set(BONJOUR_HEADERS_INSTALL + third_party/mDNSResponder/mDNSShared/dns_sd.h + ) + + add_library( + Bonjour STATIC + ${BONJOUR_SOURCES} + ${BONJOUR_HEADERS} + ) + set_property(TARGET Bonjour PROPERTY OUTPUT_NAME dnssd) + + source_group("Source Files" FILES ${BONJOUR_SOURCES}) + source_group("Header Files" FILES ${BONJOUR_HEADERS}) + + target_link_libraries( + Bonjour PRIVATE + nmos-cpp::compile-settings + ) + target_include_directories(Bonjour PUBLIC + ${BONJOUR_INCLUDE} + ) + target_include_directories(Bonjour PRIVATE + third_party + ) + + install(FILES ${BONJOUR_HEADERS_INSTALL} DESTINATION "${NMOS_CPP_INSTALL_INCLUDEDIR}/.") + + list(APPEND NMOS_CPP_TARGETS Bonjour) + add_library(nmos-cpp::Bonjour ALIAS Bonjour) + + target_link_libraries(DNSSD INTERFACE nmos-cpp::Bonjour) + endif() +endif() + +list(APPEND NMOS_CPP_TARGETS DNSSD) +add_library(nmos-cpp::DNSSD ALIAS DNSSD) + +# PCAP library + +if(NMOS_CPP_BUILD_LLDP) + # this target means the nmos-cpp libraries can link the same dependency + # whether it's based on libpcap or winpcap + add_library(PCAP INTERFACE) + + # hmm, this needs replacing with a proper find-module + if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + # find libpcap for the LLDP support library (lldp) + message(STATUS "Using system libpcap") + target_link_libraries(PCAP INTERFACE pcap) + elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows") + # find WinPcap for the LLDP support library (lldp) + set(PCAP_INCLUDE_DIR "third_party/WpdPack/Include" CACHE PATH "WinPcap include directory") + set(PCAP_LIB_DIR "third_party/WpdPack/Lib/x64" CACHE PATH "WinPcap library directory") + set(PCAP_LIB wpcap.lib) + message(STATUS "Using configured WinPcap include directory at " ${PCAP_INCLUDE_DIR}) + + # enable 'new' WinPcap functions like pcap_open, pcap_findalldevs_ex + target_compile_definitions(PCAP INTERFACE HAVE_REMOTE) + + get_filename_component(PCAP_INCLUDE_DIR_ABSOLUTE "${PCAP_INCLUDE_DIR}" ABSOLUTE) + target_include_directories(PCAP INTERFACE "$") + if(IS_ABSOLUTE ${PCAP_INCLUDE_DIR}) + target_include_directories(PCAP INTERFACE "$") + else() + # hmm, for now, not installing the headers so nothing for the INSTALL_INTERFACE + endif() + + # using absolute paths to libraries seems more robust in the long term than separately specifying target_link_directories + get_filename_component(PCAP_LIB_DIR_ABSOLUTE "${PCAP_LIB_DIR}" ABSOLUTE) + get_filename_component(PCAP_LIB_ABSOLUTE "${PCAP_LIB}" ABSOLUTE BASE_DIR "${PCAP_LIB_DIR_ABSOLUTE}") + target_link_libraries(PCAP INTERFACE "$") + if(IS_ABSOLUTE ${PCAP_LIB_DIR}) + target_link_libraries(PCAP INTERFACE "$") + else() + install(FILES "${PCAP_LIB_DIR}/${PCAP_LIB}" DESTINATION "${CMAKE_INSTALL_LIBDIR}") + target_link_libraries(PCAP INTERFACE "$/${CMAKE_INSTALL_LIBDIR}/${PCAP_LIB}>") + endif() + endif() + + list(APPEND NMOS_CPP_TARGETS PCAP) + add_library(nmos-cpp::PCAP ALIAS PCAP) +endif() + +# jwt library + +set(NMOS_CPP_USE_SUPPLIED_JWT_CPP OFF CACHE BOOL "Use supplied third_party/jwt-cpp") +if(NOT NMOS_CPP_USE_SUPPLIED_JWT_CPP) + set(JWT_VERSION_MIN "0.5.1") + set(JWT_VERSION_CUR "0.7.0") + find_package(jwt-cpp REQUIRED) + if(NOT jwt-cpp_VERSION) + message(STATUS "Found jwt-cpp unknown version; minimum version: " ${JWT_VERSION_MIN}) + elseif(jwt-cpp_VERSION VERSION_LESS JWT_VERSION_MIN) + message(FATAL_ERROR "Found jwt-cpp version " ${jwt-cpp_VERSION} " that is lower than the minimum version: " ${JWT_VERSION_MIN}) + elseif(jwt-cpp_VERSION VERSION_GREATER JWT_VERSION_CUR) + message(STATUS "Found jwt-cpp version " ${jwt-cpp_VERSION} " that is higher than the current tested version: " ${JWT_VERSION_CUR}) + else() + message(STATUS "Found jwt-cpp version " ${jwt-cpp_VERSION}) + endif() + + add_library(jwt-cpp INTERFACE) + target_link_libraries(jwt-cpp INTERFACE jwt-cpp::jwt-cpp) +else() + message(STATUS "Using sources at third_party/jwt-cpp instead of external \"jwt-cpp\" package.") + + add_library(jwt-cpp INTERFACE) + target_include_directories(jwt-cpp INTERFACE + $ + $ + ) +endif() + +target_compile_definitions( + jwt-cpp INTERFACE + JWT_DISABLE_PICOJSON + ) + +set_target_properties(jwt-cpp PROPERTIES LINKER_LANGUAGE CXX) +list(APPEND NMOS_CPP_TARGETS jwt-cpp) +add_library(nmos-cpp::jwt-cpp ALIAS jwt-cpp) diff --git a/Development/cmake/NmosCppExports.cmake b/Development/cmake/NmosCppExports.cmake new file mode 100644 index 000000000..c20cf23ab --- /dev/null +++ b/Development/cmake/NmosCppExports.cmake @@ -0,0 +1,33 @@ +# see https://cmake.org/cmake/help/latest/guide/importing-exporting/index.html + +install(TARGETS ${NMOS_CPP_TARGETS} + EXPORT nmos-cpp-targets + LIBRARY DESTINATION ${NMOS_CPP_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${NMOS_CPP_INSTALL_LIBDIR} + RUNTIME DESTINATION ${NMOS_CPP_INSTALL_BINDIR} + ) + +install(EXPORT nmos-cpp-targets + FILE nmos-cpp-targets.cmake + NAMESPACE nmos-cpp:: + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/nmos-cpp + ) + +include(CMakePackageConfigHelpers) + +configure_package_config_file(cmake/nmos-cpp-config.cmake.in + "${CMAKE_CURRENT_BINARY_DIR}/cmake/nmos-cpp-config.cmake" + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/nmos-cpp + ) + +install(FILES "${CMAKE_CURRENT_BINARY_DIR}/cmake/nmos-cpp-config.cmake" DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/nmos-cpp) + +# export custom find-modules +# see https://discourse.cmake.org/t/exporting-packages-with-a-custom-find-module/3820 + +install(FILES + ${CMAKE_CURRENT_SOURCE_DIR}/third_party/cmake/FindAvahi.cmake + ${CMAKE_CURRENT_SOURCE_DIR}/third_party/cmake/FindDBus.cmake + ${CMAKE_CURRENT_SOURCE_DIR}/third_party/cmake/FindDNSSD.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/nmos-cpp + ) diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index 6ee1c8175..b49f78736 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -1,88 +1,121 @@ -# CMake instructions for making all the nmos-cpp libraries - -# caller can set NMOS_CPP_DIR if the project is different -if (NOT DEFINED NMOS_CPP_DIR) - set (NMOS_CPP_DIR ${PROJECT_SOURCE_DIR}) -endif() - include(CMakeRegexEscape) -string(REGEX REPLACE ${MATCH_MATCH} ${MATCH_REPLACE} NMOS_CPP_DIR_MATCH "${NMOS_CPP_DIR}") -string(REGEX REPLACE ${REPLACE_MATCH} ${REPLACE_REPLACE} CMAKE_BINARY_DIR_REPLACE "${CMAKE_BINARY_DIR}") +string(REGEX REPLACE ${REPLACE_MATCH} ${REPLACE_REPLACE} CMAKE_CURRENT_BINARY_DIR_REPLACE "${CMAKE_CURRENT_BINARY_DIR}") # detail headers set(DETAIL_HEADERS - ${NMOS_CPP_DIR}/detail/default_init_allocator.h - ${NMOS_CPP_DIR}/detail/for_each_reversed.h - ${NMOS_CPP_DIR}/detail/pragma_warnings.h - ${NMOS_CPP_DIR}/detail/private_access.h + detail/default_init_allocator.h + detail/for_each_reversed.h + detail/pragma_warnings.h + detail/private_access.h ) -if(MSVC) - list(APPEND DETAIL_HEADERS - ${NMOS_CPP_DIR}/detail/vc_disable_dll_warnings.h - ${NMOS_CPP_DIR}/detail/vc_disable_warnings.h - ) +install(FILES ${DETAIL_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/detail) + +# slog library + +# compile-time control of logging loquacity +# use slog::never_log_severity to strip all logging at compile-time, or slog::max_verbosity for full control at run-time +set(SLOG_LOGGING_SEVERITY slog::max_verbosity CACHE STRING "Compile-time logging level, e.g. between 40 (least verbose, only fatal messages) and -40 (most verbose)") +mark_as_advanced(FORCE SLOG_LOGGING_SEVERITY) + +set(SLOG_HEADERS + slog/all_in_one.h + ) + +add_library(slog INTERFACE) + +target_compile_definitions( + slog INTERFACE + SLOG_STATIC + SLOG_LOGGING_SEVERITY=${SLOG_LOGGING_SEVERITY} + ) +if(CMAKE_CXX_COMPILER_ID MATCHES GNU) + if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 4.8) + target_compile_definitions( + slog INTERFACE + SLOG_DETAIL_NO_REF_QUALIFIERS + ) + endif() endif() +target_include_directories(slog INTERFACE + $ + $ + ) + +install(FILES ${SLOG_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/slog) -install(FILES ${DETAIL_HEADERS} DESTINATION include${NMOS_CPP_INCLUDE_PREFIX}/detail) +list(APPEND NMOS_CPP_TARGETS slog) +add_library(nmos-cpp::slog ALIAS slog) # mDNS support library set(MDNS_SOURCES - ${NMOS_CPP_DIR}/mdns/core.cpp - ${NMOS_CPP_DIR}/mdns/dns_sd_impl.cpp - ${NMOS_CPP_DIR}/mdns/service_advertiser_impl.cpp - ${NMOS_CPP_DIR}/mdns/service_discovery_impl.cpp + mdns/core.cpp + mdns/dns_sd_impl.cpp + mdns/service_advertiser_impl.cpp + mdns/service_discovery_impl.cpp ) set(MDNS_HEADERS - ${NMOS_CPP_DIR}/mdns/core.h - ${NMOS_CPP_DIR}/mdns/dns_sd_impl.h - ${NMOS_CPP_DIR}/mdns/service_advertiser.h - ${NMOS_CPP_DIR}/mdns/service_advertiser_impl.h - ${NMOS_CPP_DIR}/mdns/service_discovery.h - ${NMOS_CPP_DIR}/mdns/service_discovery_impl.h + mdns/core.h + mdns/dns_sd_impl.h + mdns/service_advertiser.h + mdns/service_advertiser_impl.h + mdns/service_discovery.h + mdns/service_discovery_impl.h ) add_library( - mdns_static STATIC + mdns STATIC ${MDNS_SOURCES} ${MDNS_HEADERS} - ${BONJOUR_SOURCES} - ${BONJOUR_HEADERS} ) source_group("mdns\\Source Files" FILES ${MDNS_SOURCES}) source_group("mdns\\Header Files" FILES ${MDNS_HEADERS}) -source_group("Source Files" FILES ${BONJOUR_SOURCES}) -source_group("Header Files" FILES ${BONJOUR_HEADERS}) -# ensure e.g. target_compile_definitions for cppprestsdk are applied when building this target target_link_libraries( - mdns_static - ${CPPRESTSDK_TARGET} - ${BONJOUR_LIB} + mdns PRIVATE + nmos-cpp::compile-settings + ) +target_link_libraries( + mdns PUBLIC + nmos-cpp::slog + nmos-cpp::cpprestsdk + nmos-cpp::Boost + ) +# CMake 3.17 is required in order to get the INTERFACE_LINK_OPTIONS +# see https://cmake.org/cmake/help/latest/policy/CMP0099.html +target_link_libraries( + mdns PRIVATE + nmos-cpp::DNSSD + ) +target_include_directories(mdns PUBLIC + $ + $ ) -install(TARGETS mdns_static DESTINATION lib) -install(FILES ${MDNS_HEADERS} DESTINATION include${NMOS_CPP_INCLUDE_PREFIX}/mdns) +install(FILES ${MDNS_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/mdns) + +list(APPEND NMOS_CPP_TARGETS mdns) +add_library(nmos-cpp::mdns ALIAS mdns) # LLDP support library -if (BUILD_LLDP) +if(NMOS_CPP_BUILD_LLDP) set(LLDP_SOURCES - ${NMOS_CPP_DIR}/lldp/lldp.cpp - ${NMOS_CPP_DIR}/lldp/lldp_frame.cpp - ${NMOS_CPP_DIR}/lldp/lldp_manager_impl.cpp + lldp/lldp.cpp + lldp/lldp_frame.cpp + lldp/lldp_manager_impl.cpp ) set(LLDP_HEADERS - ${NMOS_CPP_DIR}/lldp/lldp.h - ${NMOS_CPP_DIR}/lldp/lldp_frame.h - ${NMOS_CPP_DIR}/lldp/lldp_manager.h + lldp/lldp.h + lldp/lldp_frame.h + lldp/lldp_manager.h ) add_library( - lldp_static STATIC + lldp STATIC ${LLDP_SOURCES} ${LLDP_HEADERS} ) @@ -90,26 +123,39 @@ if (BUILD_LLDP) source_group("lldp\\Source Files" FILES ${LLDP_SOURCES}) source_group("lldp\\Header Files" FILES ${LLDP_HEADERS}) - # ensure e.g. target_compile_definitions for cppprestsdk::cpprest are applied when building this target target_link_libraries( - lldp_static - ${CPPRESTSDK_TARGET} - ${PCAP_LIB} + lldp PRIVATE + nmos-cpp::compile-settings + ) + target_link_libraries( + lldp PUBLIC + nmos-cpp::slog + nmos-cpp::cpprestsdk + ) + target_link_libraries( + lldp PRIVATE + nmos-cpp::PCAP + nmos-cpp::Boost + ) + target_include_directories(lldp PUBLIC + $ + $ ) - target_compile_definitions( - lldp_static INTERFACE + lldp INTERFACE HAVE_LLDP ) - install(TARGETS lldp_static DESTINATION lib) - install(FILES ${LLDP_HEADERS} DESTINATION include${NMOS_CPP_INCLUDE_PREFIX}/lldp) + install(FILES ${LLDP_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/lldp) + + list(APPEND NMOS_CPP_TARGETS lldp) + add_library(nmos-cpp::lldp ALIAS lldp) endif() # nmos_is04_schemas library set(NMOS_IS04_SCHEMAS_HEADERS - ${NMOS_CPP_DIR}/nmos/is04_schemas/is04_schemas.h + nmos/is04_schemas/is04_schemas.h ) set(NMOS_IS04_V1_3_TAG v1.3.x) @@ -118,181 +164,181 @@ set(NMOS_IS04_V1_1_TAG v1.1.x) set(NMOS_IS04_V1_0_TAG v1.0.x) set(NMOS_IS04_V1_3_SCHEMAS_JSON - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/clock_internal.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/clock_ptp.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/device.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/devices.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/error.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flows.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_audio.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_audio_coded.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_audio_raw.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_core.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_data.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_json_data.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_mux.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_sdianc_data.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_video.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_video_coded.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_video_raw.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/node.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/nodeapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/nodeapi-receiver-target.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/nodes.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/queryapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/queryapi-subscription-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/queryapi-subscriptions-post-request.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/queryapi-subscriptions-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/queryapi-subscriptions-websocket.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receiver.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receivers.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receiver_audio.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receiver_core.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receiver_data.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receiver_mux.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receiver_video.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/registrationapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/registrationapi-health-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/registrationapi-resource-post-request.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/registrationapi-resource-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/resource_core.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/sender.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/senders.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/source.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/sources.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/source_audio.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/source_core.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/source_data.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_3_TAG}/APIs/schemas/source_generic.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/clock_internal.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/clock_ptp.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/device.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/devices.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/error.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flows.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_audio.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_audio_coded.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_audio_raw.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_core.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_data.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_json_data.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_mux.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_sdianc_data.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_video.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_video_coded.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/flow_video_raw.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/node.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/nodeapi-base.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/nodeapi-receiver-target.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/nodes.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/queryapi-base.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/queryapi-subscription-response.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/queryapi-subscriptions-post-request.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/queryapi-subscriptions-response.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/queryapi-subscriptions-websocket.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receiver.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receivers.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receiver_audio.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receiver_core.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receiver_data.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receiver_mux.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/receiver_video.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/registrationapi-base.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/registrationapi-health-response.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/registrationapi-resource-post-request.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/registrationapi-resource-response.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/resource_core.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/sender.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/senders.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/source.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/sources.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/source_audio.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/source_core.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/source_data.json + third_party/is-04/${NMOS_IS04_V1_3_TAG}/APIs/schemas/source_generic.json ) set(NMOS_IS04_V1_2_SCHEMAS_JSON - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/clock_internal.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/clock_ptp.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/device.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/devices.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/error.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flows.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_audio.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_audio_coded.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_audio_raw.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_core.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_data.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_mux.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_sdianc_data.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_video.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_video_coded.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_video_raw.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/node.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/nodeapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/nodeapi-receiver-target.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/nodes.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/queryapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/queryapi-subscription-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/queryapi-subscriptions-post-request.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/queryapi-subscriptions-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/queryapi-subscriptions-websocket.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receiver.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receivers.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receiver_audio.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receiver_core.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receiver_data.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receiver_mux.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receiver_video.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/registrationapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/registrationapi-health-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/registrationapi-resource-post-request.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/registrationapi-resource-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/resource_core.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/sender.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/senders.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/source.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/sources.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/source_audio.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/source_core.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_2_TAG}/APIs/schemas/source_generic.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/clock_internal.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/clock_ptp.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/device.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/devices.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/error.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flows.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_audio.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_audio_coded.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_audio_raw.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_core.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_data.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_mux.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_sdianc_data.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_video.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_video_coded.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/flow_video_raw.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/node.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/nodeapi-base.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/nodeapi-receiver-target.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/nodes.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/queryapi-base.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/queryapi-subscription-response.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/queryapi-subscriptions-post-request.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/queryapi-subscriptions-response.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/queryapi-subscriptions-websocket.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receiver.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receivers.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receiver_audio.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receiver_core.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receiver_data.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receiver_mux.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/receiver_video.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/registrationapi-base.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/registrationapi-health-response.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/registrationapi-resource-post-request.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/registrationapi-resource-response.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/resource_core.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/sender.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/senders.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/source.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/sources.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/source_audio.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/source_core.json + third_party/is-04/${NMOS_IS04_V1_2_TAG}/APIs/schemas/source_generic.json ) set(NMOS_IS04_V1_1_SCHEMAS_JSON - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/clock_internal.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/clock_ptp.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/device.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/devices.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/error.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flows.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_audio.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_audio_coded.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_audio_raw.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_core.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_data.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_mux.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_sdianc_data.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_video.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_video_coded.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_video_raw.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/node.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/nodeapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/nodeapi-receiver-target.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/nodes.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/queryapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/queryapi-subscription-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/queryapi-subscriptions-post-request.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/queryapi-subscriptions-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/queryapi-subscriptions-websocket.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receiver.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receivers.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receiver_audio.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receiver_core.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receiver_data.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receiver_mux.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receiver_video.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/registrationapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/registrationapi-health-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/registrationapi-resource-post-request.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/registrationapi-resource-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/resource_core.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/sender.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/senders.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/source.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/sources.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/source_audio.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/source_core.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_1_TAG}/APIs/schemas/source_generic.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/clock_internal.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/clock_ptp.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/device.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/devices.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/error.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flows.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_audio.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_audio_coded.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_audio_raw.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_core.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_data.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_mux.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_sdianc_data.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_video.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_video_coded.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/flow_video_raw.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/node.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/nodeapi-base.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/nodeapi-receiver-target.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/nodes.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/queryapi-base.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/queryapi-subscription-response.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/queryapi-subscriptions-post-request.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/queryapi-subscriptions-response.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/queryapi-subscriptions-websocket.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receiver.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receivers.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receiver_audio.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receiver_core.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receiver_data.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receiver_mux.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/receiver_video.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/registrationapi-base.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/registrationapi-health-response.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/registrationapi-resource-post-request.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/registrationapi-resource-response.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/resource_core.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/sender.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/senders.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/source.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/sources.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/source_audio.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/source_core.json + third_party/is-04/${NMOS_IS04_V1_1_TAG}/APIs/schemas/source_generic.json ) set(NMOS_IS04_V1_0_SCHEMAS_JSON - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/device.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/devices.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/error.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/flow.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/flows.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/node.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/nodeapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/nodeapi-receiver-target.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/nodes.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/queryapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/queryapi-subscription-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/queryapi-subscriptions-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/queryapi-v1.0-subscriptions-post-request.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/queryapi-v1.0-subscriptions-websocket.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/receiver.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/receivers.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/registrationapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/registrationapi-health-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/registrationapi-resource-response.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/registrationapi-v1.0-resource-post-request.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/sender-target.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/sender.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/senders.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/source.json - ${NMOS_CPP_DIR}/third_party/nmos-discovery-registration/${NMOS_IS04_V1_0_TAG}/APIs/schemas/sources.json - ) - -set(NMOS_IS04_SCHEMAS_JSON_MATCH "${NMOS_CPP_DIR_MATCH}/third_party/nmos-discovery-registration/([^/]+)/APIs/schemas/([^;]+)\\.json") -set(NMOS_IS04_SCHEMAS_SOURCE_REPLACE "${CMAKE_BINARY_DIR_REPLACE}/nmos/is04_schemas/\\1/\\2.cpp") + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/device.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/devices.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/error.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/flow.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/flows.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/node.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/nodeapi-base.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/nodeapi-receiver-target.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/nodes.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/queryapi-base.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/queryapi-subscription-response.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/queryapi-subscriptions-response.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/queryapi-v1.0-subscriptions-post-request.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/queryapi-v1.0-subscriptions-websocket.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/receiver.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/receivers.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/registrationapi-base.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/registrationapi-health-response.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/registrationapi-resource-response.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/registrationapi-v1.0-resource-post-request.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/sender-target.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/sender.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/senders.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/source.json + third_party/is-04/${NMOS_IS04_V1_0_TAG}/APIs/schemas/sources.json + ) + +set(NMOS_IS04_SCHEMAS_JSON_MATCH "third_party/is-04/([^/]+)/APIs/schemas/([^;]+)\\.json") +set(NMOS_IS04_SCHEMAS_SOURCE_REPLACE "${CMAKE_CURRENT_BINARY_DIR_REPLACE}/nmos/is04_schemas/\\1/\\2.cpp") string(REGEX REPLACE "${NMOS_IS04_SCHEMAS_JSON_MATCH}(;|$)" "${NMOS_IS04_SCHEMAS_SOURCE_REPLACE}\\3" NMOS_IS04_V1_3_SCHEMAS_SOURCES "${NMOS_IS04_V1_3_SCHEMAS_JSON}") string(REGEX REPLACE "${NMOS_IS04_SCHEMAS_JSON_MATCH}(;|$)" "${NMOS_IS04_SCHEMAS_SOURCE_REPLACE}\\3" NMOS_IS04_V1_2_SCHEMAS_SOURCES "${NMOS_IS04_V1_2_SCHEMAS_JSON}") string(REGEX REPLACE "${NMOS_IS04_SCHEMAS_JSON_MATCH}(;|$)" "${NMOS_IS04_SCHEMAS_SOURCE_REPLACE}\\3" NMOS_IS04_V1_1_SCHEMAS_SOURCES "${NMOS_IS04_V1_1_SCHEMAS_JSON}") @@ -328,7 +374,7 @@ namespace nmos\n\ endforeach() add_library( - nmos_is04_schemas_static STATIC + nmos_is04_schemas STATIC ${NMOS_IS04_SCHEMAS_HEADERS} ${NMOS_IS04_V1_3_SCHEMAS_SOURCES} ${NMOS_IS04_V1_2_SCHEMAS_SOURCES} @@ -343,84 +389,90 @@ source_group("nmos\\is04_schemas\\${NMOS_IS04_V1_1_TAG}\\Source Files" FILES ${N source_group("nmos\\is04_schemas\\${NMOS_IS04_V1_0_TAG}\\Source Files" FILES ${NMOS_IS04_V1_0_SCHEMAS_SOURCES}) target_link_libraries( - nmos_is04_schemas_static + nmos_is04_schemas PRIVATE + nmos-cpp::compile-settings + ) +target_include_directories(nmos_is04_schemas PUBLIC + $ + $ ) -install(TARGETS nmos_is04_schemas_static DESTINATION lib) +list(APPEND NMOS_CPP_TARGETS nmos_is04_schemas) +add_library(nmos-cpp::nmos_is04_schemas ALIAS nmos_is04_schemas) # nmos_is05_schemas library set(NMOS_IS05_SCHEMAS_HEADERS - ${NMOS_CPP_DIR}/nmos/is05_schemas/is05_schemas.h + nmos/is05_schemas/is05_schemas.h ) set(NMOS_IS05_V1_1_TAG v1.1.x) set(NMOS_IS05_V1_0_TAG v1.0.x) set(NMOS_IS05_V1_1_SCHEMAS_JSON - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/activation-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/activation-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/bulk-receiver-post-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/bulk-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/bulk-sender-post-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/connectionapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/connectionapi-bulk.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/connectionapi-receiver.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/connectionapi-sender.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/connectionapi-single.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/constraint-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/constraints-schema-mqtt.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/constraints-schema-rtp.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/constraints-schema-websocket.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/constraints-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/error.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver_transport_params.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver_transport_params_dash.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver_transport_params_ext.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver_transport_params_mqtt.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver_transport_params_rtp.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver_transport_params_websocket.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver-stage-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver-transport-file.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender_transport_params.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender_transport_params_dash.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender_transport_params_ext.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender_transport_params_mqtt.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender_transport_params_rtp.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender_transport_params_websocket.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender-receiver-base.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender-stage-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_1_TAG}/APIs/schemas/transporttype-response-schema.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/activation-response-schema.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/activation-schema.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/bulk-receiver-post-schema.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/bulk-response-schema.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/bulk-sender-post-schema.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/connectionapi-base.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/connectionapi-bulk.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/connectionapi-receiver.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/connectionapi-sender.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/connectionapi-single.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/constraint-schema.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/constraints-schema-mqtt.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/constraints-schema-rtp.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/constraints-schema-websocket.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/constraints-schema.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/error.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver_transport_params.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver_transport_params_dash.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver_transport_params_ext.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver_transport_params_mqtt.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver_transport_params_rtp.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver_transport_params_websocket.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver-response-schema.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver-stage-schema.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/receiver-transport-file.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender_transport_params.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender_transport_params_dash.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender_transport_params_ext.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender_transport_params_mqtt.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender_transport_params_rtp.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender_transport_params_websocket.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender-receiver-base.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender-response-schema.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/sender-stage-schema.json + third_party/is-05/${NMOS_IS05_V1_1_TAG}/APIs/schemas/transporttype-response-schema.json ) set(NMOS_IS05_V1_0_SCHEMAS_JSON - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/connectionapi-base.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/connectionapi-bulk.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/connectionapi-receiver.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/connectionapi-sender.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/connectionapi-single.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/error.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0_receiver_transport_params_dash.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0_receiver_transport_params_rtp.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0_sender_transport_params_dash.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0_sender_transport_params_rtp.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-activation-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-activation-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-bulk-receiver-post-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-bulk-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-bulk-sender-post-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-constraints-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-receiver-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-receiver-stage-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/sender-receiver-base.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-sender-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-device-connection-management/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-sender-stage-schema.json - ) - -set(NMOS_IS05_SCHEMAS_JSON_MATCH "${NMOS_CPP_DIR_MATCH}/third_party/nmos-device-connection-management/([^/]+)/APIs/schemas/([^;]+)\\.json") -set(NMOS_IS05_SCHEMAS_SOURCE_REPLACE "${CMAKE_BINARY_DIR_REPLACE}/nmos/is05_schemas/\\1/\\2.cpp") + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/connectionapi-base.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/connectionapi-bulk.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/connectionapi-receiver.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/connectionapi-sender.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/connectionapi-single.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/error.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0_receiver_transport_params_dash.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0_receiver_transport_params_rtp.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0_sender_transport_params_dash.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0_sender_transport_params_rtp.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-activation-response-schema.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-activation-schema.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-bulk-receiver-post-schema.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-bulk-response-schema.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-bulk-sender-post-schema.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-constraints-schema.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-receiver-response-schema.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-receiver-stage-schema.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/sender-receiver-base.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-sender-response-schema.json + third_party/is-05/${NMOS_IS05_V1_0_TAG}/APIs/schemas/v1.0-sender-stage-schema.json + ) + +set(NMOS_IS05_SCHEMAS_JSON_MATCH "third_party/is-05/([^/]+)/APIs/schemas/([^;]+)\\.json") +set(NMOS_IS05_SCHEMAS_SOURCE_REPLACE "${CMAKE_CURRENT_BINARY_DIR_REPLACE}/nmos/is05_schemas/\\1/\\2.cpp") string(REGEX REPLACE "${NMOS_IS05_SCHEMAS_JSON_MATCH}(;|$)" "${NMOS_IS05_SCHEMAS_SOURCE_REPLACE}\\3" NMOS_IS05_V1_1_SCHEMAS_SOURCES "${NMOS_IS05_V1_1_SCHEMAS_JSON}") string(REGEX REPLACE "${NMOS_IS05_SCHEMAS_JSON_MATCH}(;|$)" "${NMOS_IS05_SCHEMAS_SOURCE_REPLACE}\\3" NMOS_IS05_V1_0_SCHEMAS_SOURCES "${NMOS_IS05_V1_0_SCHEMAS_JSON}") @@ -454,7 +506,7 @@ namespace nmos\n\ endforeach() add_library( - nmos_is05_schemas_static STATIC + nmos_is05_schemas STATIC ${NMOS_IS05_SCHEMAS_HEADERS} ${NMOS_IS05_V1_1_SCHEMAS_SOURCES} ${NMOS_IS05_V1_0_SCHEMAS_SOURCES} @@ -465,48 +517,54 @@ source_group("nmos\\is05_schemas\\${NMOS_IS05_V1_1_TAG}\\Source Files" FILES ${N source_group("nmos\\is05_schemas\\${NMOS_IS05_V1_0_TAG}\\Source Files" FILES ${NMOS_IS05_V1_0_SCHEMAS_SOURCES}) target_link_libraries( - nmos_is05_schemas_static + nmos_is05_schemas PRIVATE + nmos-cpp::compile-settings + ) +target_include_directories(nmos_is05_schemas PUBLIC + $ + $ ) -install(TARGETS nmos_is05_schemas_static DESTINATION lib) +list(APPEND NMOS_CPP_TARGETS nmos_is05_schemas) +add_library(nmos-cpp::nmos_is05_schemas ALIAS nmos_is05_schemas) # nmos_is08_schemas library set(NMOS_IS08_SCHEMAS_HEADERS - ${NMOS_CPP_DIR}/nmos/is08_schemas/is08_schemas.h + nmos/is08_schemas/is08_schemas.h ) set(NMOS_IS08_V1_0_TAG v1.0.x) set(NMOS_IS08_V1_0_SCHEMAS_JSON - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/activation-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/activation-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/base-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/error.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/input-base-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/input-caps-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/input-channels-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/input-parent-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/input-properties-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/inputs-outputs-base-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/io-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-activations-activation-get-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-activations-get-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-activations-post-request-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-activations-post-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-active-output-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-active-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-base-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-entries-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/output-base-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/output-caps-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/output-channels-response-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/output-properties-schema.json - ${NMOS_CPP_DIR}/third_party/nmos-audio-channel-mapping/${NMOS_IS08_V1_0_TAG}/APIs/schemas/output-sourceid-response-schema.json - ) - -set(NMOS_IS08_SCHEMAS_JSON_MATCH "${NMOS_CPP_DIR_MATCH}/third_party/nmos-audio-channel-mapping/([^/]+)/APIs/schemas/([^;]+)\\.json") -set(NMOS_IS08_SCHEMAS_SOURCE_REPLACE "${CMAKE_BINARY_DIR_REPLACE}/nmos/is08_schemas/\\1/\\2.cpp") + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/activation-response-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/activation-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/base-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/error.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/input-base-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/input-caps-response-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/input-channels-response-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/input-parent-response-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/input-properties-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/inputs-outputs-base-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/io-response-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-activations-activation-get-response-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-activations-get-response-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-activations-post-request-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-activations-post-response-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-active-output-response-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-active-response-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-base-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/map-entries-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/output-base-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/output-caps-response-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/output-channels-response-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/output-properties-schema.json + third_party/is-08/${NMOS_IS08_V1_0_TAG}/APIs/schemas/output-sourceid-response-schema.json + ) + +set(NMOS_IS08_SCHEMAS_JSON_MATCH "third_party/is-08/([^/]+)/APIs/schemas/([^;]+)\\.json") +set(NMOS_IS08_SCHEMAS_SOURCE_REPLACE "${CMAKE_CURRENT_BINARY_DIR_REPLACE}/nmos/is08_schemas/\\1/\\2.cpp") string(REGEX REPLACE "${NMOS_IS08_SCHEMAS_JSON_MATCH}(;|$)" "${NMOS_IS08_SCHEMAS_SOURCE_REPLACE}\\3" NMOS_IS08_V1_0_SCHEMAS_SOURCES "${NMOS_IS08_V1_0_SCHEMAS_JSON}") foreach(JSON ${NMOS_IS08_V1_0_SCHEMAS_JSON}) @@ -539,7 +597,7 @@ namespace nmos\n\ endforeach() add_library( - nmos_is08_schemas_static STATIC + nmos_is08_schemas STATIC ${NMOS_IS08_SCHEMAS_HEADERS} ${NMOS_IS08_V1_0_SCHEMAS_SOURCES} ) @@ -548,28 +606,34 @@ source_group("nmos\\is08_schemas\\Header Files" FILES ${NMOS_IS08_SCHEMAS_HEADER source_group("nmos\\is08_schemas\\${NMOS_IS08_V1_0_TAG}\\Source Files" FILES ${NMOS_IS08_V1_0_SCHEMAS_SOURCES}) target_link_libraries( - nmos_is08_schemas_static + nmos_is08_schemas PRIVATE + nmos-cpp::compile-settings + ) +target_include_directories(nmos_is08_schemas PUBLIC + $ + $ ) -install(TARGETS nmos_is08_schemas_static DESTINATION lib) +list(APPEND NMOS_CPP_TARGETS nmos_is08_schemas) +add_library(nmos-cpp::nmos_is08_schemas ALIAS nmos_is08_schemas) # nmos_is09_schemas library set(NMOS_IS09_SCHEMAS_HEADERS - ${NMOS_CPP_DIR}/nmos/is09_schemas/is09_schemas.h + nmos/is09_schemas/is09_schemas.h ) set(NMOS_IS09_V1_0_TAG v1.0.x) set(NMOS_IS09_V1_0_SCHEMAS_JSON - ${NMOS_CPP_DIR}/third_party/nmos-system/${NMOS_IS09_V1_0_TAG}/APIs/schemas/base.json - ${NMOS_CPP_DIR}/third_party/nmos-system/${NMOS_IS09_V1_0_TAG}/APIs/schemas/error.json - ${NMOS_CPP_DIR}/third_party/nmos-system/${NMOS_IS09_V1_0_TAG}/APIs/schemas/global.json - ${NMOS_CPP_DIR}/third_party/nmos-system/${NMOS_IS09_V1_0_TAG}/APIs/schemas/resource_core.json + third_party/is-09/${NMOS_IS09_V1_0_TAG}/APIs/schemas/base.json + third_party/is-09/${NMOS_IS09_V1_0_TAG}/APIs/schemas/error.json + third_party/is-09/${NMOS_IS09_V1_0_TAG}/APIs/schemas/global.json + third_party/is-09/${NMOS_IS09_V1_0_TAG}/APIs/schemas/resource_core.json ) -set(NMOS_IS09_SCHEMAS_JSON_MATCH "${NMOS_CPP_DIR_MATCH}/third_party/nmos-system/([^/]+)/APIs/schemas/([^;]+)\\.json") -set(NMOS_IS09_SCHEMAS_SOURCE_REPLACE "${CMAKE_BINARY_DIR_REPLACE}/nmos/is09_schemas/\\1/\\2.cpp") +set(NMOS_IS09_SCHEMAS_JSON_MATCH "third_party/is-09/([^/]+)/APIs/schemas/([^;]+)\\.json") +set(NMOS_IS09_SCHEMAS_SOURCE_REPLACE "${CMAKE_CURRENT_BINARY_DIR_REPLACE}/nmos/is09_schemas/\\1/\\2.cpp") string(REGEX REPLACE "${NMOS_IS09_SCHEMAS_JSON_MATCH}(;|$)" "${NMOS_IS09_SCHEMAS_SOURCE_REPLACE}\\3" NMOS_IS09_V1_0_SCHEMAS_SOURCES "${NMOS_IS09_V1_0_SCHEMAS_JSON}") foreach(JSON ${NMOS_IS09_V1_0_SCHEMAS_JSON}) @@ -602,7 +666,7 @@ namespace nmos\n\ endforeach() add_library( - nmos_is09_schemas_static STATIC + nmos_is09_schemas STATIC ${NMOS_IS09_SCHEMAS_HEADERS} ${NMOS_IS09_V1_0_SCHEMAS_SOURCES} ) @@ -611,290 +675,498 @@ source_group("nmos\\is09_schemas\\Header Files" FILES ${NMOS_IS09_SCHEMAS_HEADER source_group("nmos\\is09_schemas\\${NMOS_IS09_V1_0_TAG}\\Source Files" FILES ${NMOS_IS09_V1_0_SCHEMAS_SOURCES}) target_link_libraries( - nmos_is09_schemas_static + nmos_is09_schemas PRIVATE + nmos-cpp::compile-settings + ) +target_include_directories(nmos_is09_schemas PUBLIC + $ + $ ) -install(TARGETS nmos_is09_schemas_static DESTINATION lib) +list(APPEND NMOS_CPP_TARGETS nmos_is09_schemas) +add_library(nmos-cpp::nmos_is09_schemas ALIAS nmos_is09_schemas) -# json schema validator library +# nmos_is10_schemas library -set(JSON_SCHEMA_VALIDATOR_SOURCES - ${NMOS_CPP_DIR}/third_party/nlohmann/json-schema-draft7.json.cpp - ${NMOS_CPP_DIR}/third_party/nlohmann/json-validator.cpp - ${NMOS_CPP_DIR}/third_party/nlohmann/json-uri.cpp +set(NMOS_IS10_SCHEMAS_HEADERS + nmos/is10_schemas/is10_schemas.h ) -set(JSON_SCHEMA_VALIDATOR_HEADERS - ${NMOS_CPP_DIR}/third_party/nlohmann/json-schema.hpp - ${NMOS_CPP_DIR}/third_party/nlohmann/json.hpp +set(NMOS_IS10_V1_0_TAG v1.0.x) + +set(NMOS_IS10_V1_0_SCHEMAS_JSON + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/auth_metadata.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/jwks_response.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/jwks_schema.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/register_client_error_response.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/register_client_request.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/register_client_response.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/token_error_response.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/token_response.json + third_party/is-10/${NMOS_IS10_V1_0_TAG}/APIs/schemas/token_schema.json ) +set(NMOS_IS10_SCHEMAS_JSON_MATCH "third_party/is-10/([^/]+)/APIs/schemas/([^;]+)\\.json") +set(NMOS_IS10_SCHEMAS_SOURCE_REPLACE "${CMAKE_CURRENT_BINARY_DIR_REPLACE}/nmos/is10_schemas/\\1/\\2.cpp") +string(REGEX REPLACE "${NMOS_IS10_SCHEMAS_JSON_MATCH}(;|$)" "${NMOS_IS10_SCHEMAS_SOURCE_REPLACE}\\3" NMOS_IS10_V1_0_SCHEMAS_SOURCES "${NMOS_IS10_V1_0_SCHEMAS_JSON}") + +foreach(JSON ${NMOS_IS10_V1_0_SCHEMAS_JSON}) + string(REGEX REPLACE "${NMOS_IS10_SCHEMAS_JSON_MATCH}" "${NMOS_IS10_SCHEMAS_SOURCE_REPLACE}" SOURCE "${JSON}") + string(REGEX REPLACE "${NMOS_IS10_SCHEMAS_JSON_MATCH}" "\\1" NS "${JSON}") + string(REGEX REPLACE "${NMOS_IS10_SCHEMAS_JSON_MATCH}" "\\2" VAR "${JSON}") + string(MAKE_C_IDENTIFIER "${NS}" NS) + string(MAKE_C_IDENTIFIER "${VAR}" VAR) + + file(WRITE "${SOURCE}.in" "\ +// Auto-generated from: ${JSON}\n\ +\n\ +namespace nmos\n\ +{\n\ + namespace is10_schemas\n\ + {\n\ + namespace ${NS}\n\ + {\n\ + const char* ${VAR} = R\"-auto-generated-(") + + file(READ "${JSON}" RAW) + file(APPEND "${SOURCE}.in" "${RAW}") + + file(APPEND "${SOURCE}.in" ")-auto-generated-\";\n\ + }\n\ + }\n\ +}\n") + + configure_file("${SOURCE}.in" "${SOURCE}" COPYONLY) +endforeach() + add_library( - json_schema_validator_static STATIC - ${JSON_SCHEMA_VALIDATOR_SOURCES} - ${JSON_SCHEMA_VALIDATOR_HEADERS} + nmos_is10_schemas STATIC + ${NMOS_IS10_SCHEMAS_HEADERS} + ${NMOS_IS10_V1_0_SCHEMAS_SOURCES} ) -source_group("Source Files" FILES ${JSON_SCHEMA_VALIDATOR_SOURCES}) -source_group("Header Files" FILES ${JSON_SCHEMA_VALIDATOR_HEADERS}) +source_group("nmos\\is10_schemas\\Header Files" FILES ${NMOS_IS10_SCHEMAS_HEADERS}) +source_group("nmos\\is10_schemas\\${NMOS_IS10_V1_0_TAG}\\Source Files" FILES ${NMOS_IS10_V1_0_SCHEMAS_SOURCES}) target_link_libraries( - json_schema_validator_static + nmos_is10_schemas PRIVATE + nmos-cpp::compile-settings ) +target_include_directories(nmos_is10_schemas PUBLIC + $ + $ + ) + +list(APPEND NMOS_CPP_TARGETS nmos_is10_schemas) +add_library(nmos-cpp::nmos_is10_schemas ALIAS nmos_is10_schemas) -install(TARGETS json_schema_validator_static DESTINATION lib) +# nmos_is12_schemas library + +set(NMOS_IS12_SCHEMAS_HEADERS + nmos/is12_schemas/is12_schemas.h + ) + +set(NMOS_IS12_V1_0_TAG v1.0.x) + +set(NMOS_IS12_V1_0_SCHEMAS_JSON + third_party/is-12/${NMOS_IS12_V1_0_TAG}/APIs/schemas/base-message.json + third_party/is-12/${NMOS_IS12_V1_0_TAG}/APIs/schemas/command-message.json + third_party/is-12/${NMOS_IS12_V1_0_TAG}/APIs/schemas/command-response-message.json + third_party/is-12/${NMOS_IS12_V1_0_TAG}/APIs/schemas/error-message.json + third_party/is-12/${NMOS_IS12_V1_0_TAG}/APIs/schemas/event-data.json + third_party/is-12/${NMOS_IS12_V1_0_TAG}/APIs/schemas/notification-message.json + third_party/is-12/${NMOS_IS12_V1_0_TAG}/APIs/schemas/property-changed-event-data.json + third_party/is-12/${NMOS_IS12_V1_0_TAG}/APIs/schemas/subscription-message.json + third_party/is-12/${NMOS_IS12_V1_0_TAG}/APIs/schemas/subscription-response-message.json + ) + +set(NMOS_IS12_SCHEMAS_JSON_MATCH "third_party/is-12/([^/]+)/APIs/schemas/([^;]+)\\.json") +set(NMOS_IS12_SCHEMAS_SOURCE_REPLACE "${CMAKE_CURRENT_BINARY_DIR_REPLACE}/nmos/is12_schemas/\\1/\\2.cpp") +string(REGEX REPLACE "${NMOS_IS12_SCHEMAS_JSON_MATCH}(;|$)" "${NMOS_IS12_SCHEMAS_SOURCE_REPLACE}\\3" NMOS_IS12_V1_0_SCHEMAS_SOURCES "${NMOS_IS12_V1_0_SCHEMAS_JSON}") + +foreach(JSON ${NMOS_IS12_V1_0_SCHEMAS_JSON}) + string(REGEX REPLACE "${NMOS_IS12_SCHEMAS_JSON_MATCH}" "${NMOS_IS12_SCHEMAS_SOURCE_REPLACE}" SOURCE "${JSON}") + string(REGEX REPLACE "${NMOS_IS12_SCHEMAS_JSON_MATCH}" "\\1" NS "${JSON}") + string(REGEX REPLACE "${NMOS_IS12_SCHEMAS_JSON_MATCH}" "\\2" VAR "${JSON}") + string(MAKE_C_IDENTIFIER "${NS}" NS) + string(MAKE_C_IDENTIFIER "${VAR}" VAR) + + file(WRITE "${SOURCE}.in" "\ +// Auto-generated from: ${JSON}\n\ +\n\ +namespace nmos\n\ +{\n\ + namespace is12_schemas\n\ + {\n\ + namespace ${NS}\n\ + {\n\ + const char* ${VAR} = R\"-auto-generated-(") + + file(READ "${JSON}" RAW) + file(APPEND "${SOURCE}.in" "${RAW}") + + file(APPEND "${SOURCE}.in" ")-auto-generated-\";\n\ + }\n\ + }\n\ +}\n") + + configure_file("${SOURCE}.in" "${SOURCE}" COPYONLY) +endforeach() + +add_library( + nmos_is12_schemas STATIC + ${NMOS_IS12_SCHEMAS_HEADERS} + ${NMOS_IS12_V1_0_SCHEMAS_SOURCES} + ) + +source_group("nmos\\is12_schemas\\Header Files" FILES ${NMOS_IS12_SCHEMAS_HEADERS}) +source_group("nmos\\is12_schemas\\${NMOS_IS12_V1_0_TAG}\\Source Files" FILES ${NMOS_IS12_V1_0_SCHEMAS_SOURCES}) + +target_link_libraries( + nmos_is12_schemas PRIVATE + nmos-cpp::compile-settings + ) +target_include_directories(nmos_is12_schemas PUBLIC + $ + $ + ) + +list(APPEND NMOS_CPP_TARGETS nmos_is12_schemas) +add_library(nmos-cpp::nmos_is12_schemas ALIAS nmos_is12_schemas) # nmos-cpp library set(NMOS_CPP_BST_SOURCES ) set(NMOS_CPP_BST_HEADERS - ${NMOS_CPP_DIR}/bst/filesystem.h - ${NMOS_CPP_DIR}/bst/optional.h - ${NMOS_CPP_DIR}/bst/regex.h - ${NMOS_CPP_DIR}/bst/shared_mutex.h + bst/any.h + bst/filesystem.h + bst/optional.h + bst/regex.h + bst/shared_mutex.h ) set(NMOS_CPP_CPPREST_SOURCES - ${NMOS_CPP_DIR}/cpprest/api_router.cpp - ${NMOS_CPP_DIR}/cpprest/host_utils.cpp - ${NMOS_CPP_DIR}/cpprest/http_utils.cpp - ${NMOS_CPP_DIR}/cpprest/json_escape.cpp - ${NMOS_CPP_DIR}/cpprest/json_storage.cpp - ${NMOS_CPP_DIR}/cpprest/json_utils.cpp - ${NMOS_CPP_DIR}/cpprest/json_validator_impl.cpp - ${NMOS_CPP_DIR}/cpprest/json_visit.cpp - ${NMOS_CPP_DIR}/cpprest/ws_listener_impl.cpp + cpprest/api_router.cpp + cpprest/host_utils.cpp + cpprest/http_utils.cpp + cpprest/json_escape.cpp + cpprest/json_storage.cpp + cpprest/json_utils.cpp + cpprest/json_validator_impl.cpp + cpprest/json_visit.cpp + cpprest/ws_listener_impl.cpp ) if(MSVC) # workaround for "fatal error C1128: number of sections exceeded object file format limit: compile with /bigobj" - set_source_files_properties(${NMOS_CPP_DIR}/cpprest/ws_listener_impl.cpp PROPERTIES COMPILE_FLAGS /bigobj) + set_source_files_properties(cpprest/ws_listener_impl.cpp PROPERTIES COMPILE_FLAGS /bigobj) endif() set(NMOS_CPP_CPPREST_HEADERS - ${NMOS_CPP_DIR}/cpprest/api_router.h - ${NMOS_CPP_DIR}/cpprest/basic_utils.h - ${NMOS_CPP_DIR}/cpprest/host_utils.h - ${NMOS_CPP_DIR}/cpprest/http_utils.h - ${NMOS_CPP_DIR}/cpprest/json_escape.h - ${NMOS_CPP_DIR}/cpprest/json_ops.h - ${NMOS_CPP_DIR}/cpprest/json_storage.h - ${NMOS_CPP_DIR}/cpprest/json_utils.h - ${NMOS_CPP_DIR}/cpprest/json_validator.h - ${NMOS_CPP_DIR}/cpprest/json_visit.h - ${NMOS_CPP_DIR}/cpprest/logging_utils.h - ${NMOS_CPP_DIR}/cpprest/regex_utils.h - ${NMOS_CPP_DIR}/cpprest/uri_schemes.h - ${NMOS_CPP_DIR}/cpprest/ws_listener.h - ${NMOS_CPP_DIR}/cpprest/ws_utils.h + cpprest/access_token_error.h + cpprest/api_router.h + cpprest/basic_utils.h + cpprest/client_type.h + cpprest/code_challenge_method.h + cpprest/grant_type.h + cpprest/host_utils.h + cpprest/http_utils.h + cpprest/json_escape.h + cpprest/json_ops.h + cpprest/json_storage.h + cpprest/json_utils.h + cpprest/json_validator.h + cpprest/json_visit.h + cpprest/logging_utils.h + cpprest/regex_utils.h + cpprest/resource_server_error.h + cpprest/response_type.h + cpprest/token_endpoint_auth_method.h + cpprest/uri_schemes.h + cpprest/ws_listener.h + cpprest/ws_utils.h ) set(NMOS_CPP_CPPREST_DETAILS_HEADERS - ${NMOS_CPP_DIR}/cpprest/details/boost_u_workaround.h - ${NMOS_CPP_DIR}/cpprest/details/pop_u.h - ${NMOS_CPP_DIR}/cpprest/details/push_undef_u.h - ${NMOS_CPP_DIR}/cpprest/details/system_error.h + cpprest/details/boost_u_workaround.h + cpprest/details/pop_u.h + cpprest/details/push_undef_u.h + cpprest/details/system_error.h + ) + +set(NMOS_CPP_JWK_HEADERS + jwk/algorithm.h + jwk/public_key_use.h ) set(NMOS_CPP_NMOS_SOURCES - ${NMOS_CPP_DIR}/nmos/activation_utils.cpp - ${NMOS_CPP_DIR}/nmos/admin_ui.cpp - ${NMOS_CPP_DIR}/nmos/api_downgrade.cpp - ${NMOS_CPP_DIR}/nmos/api_utils.cpp - ${NMOS_CPP_DIR}/nmos/capabilities.cpp - ${NMOS_CPP_DIR}/nmos/channelmapping_activation.cpp - ${NMOS_CPP_DIR}/nmos/channelmapping_api.cpp - ${NMOS_CPP_DIR}/nmos/channelmapping_resources.cpp - ${NMOS_CPP_DIR}/nmos/channels.cpp - ${NMOS_CPP_DIR}/nmos/client_utils.cpp - ${NMOS_CPP_DIR}/nmos/components.cpp - ${NMOS_CPP_DIR}/nmos/connection_activation.cpp - ${NMOS_CPP_DIR}/nmos/connection_api.cpp - ${NMOS_CPP_DIR}/nmos/connection_events_activation.cpp - ${NMOS_CPP_DIR}/nmos/connection_resources.cpp - ${NMOS_CPP_DIR}/nmos/did_sdid.cpp - ${NMOS_CPP_DIR}/nmos/events_api.cpp - ${NMOS_CPP_DIR}/nmos/events_resources.cpp - ${NMOS_CPP_DIR}/nmos/events_ws_api.cpp - ${NMOS_CPP_DIR}/nmos/events_ws_client.cpp - ${NMOS_CPP_DIR}/nmos/filesystem_route.cpp - ${NMOS_CPP_DIR}/nmos/group_hint.cpp - ${NMOS_CPP_DIR}/nmos/id.cpp - ${NMOS_CPP_DIR}/nmos/lldp_handler.cpp - ${NMOS_CPP_DIR}/nmos/lldp_manager.cpp - ${NMOS_CPP_DIR}/nmos/json_schema.cpp - ${NMOS_CPP_DIR}/nmos/log_model.cpp - ${NMOS_CPP_DIR}/nmos/logging_api.cpp - ${NMOS_CPP_DIR}/nmos/manifest_api.cpp - ${NMOS_CPP_DIR}/nmos/mdns.cpp - ${NMOS_CPP_DIR}/nmos/mdns_api.cpp - ${NMOS_CPP_DIR}/nmos/node_api.cpp - ${NMOS_CPP_DIR}/nmos/node_api_target_handler.cpp - ${NMOS_CPP_DIR}/nmos/node_behaviour.cpp - ${NMOS_CPP_DIR}/nmos/node_interfaces.cpp - ${NMOS_CPP_DIR}/nmos/node_resource.cpp - ${NMOS_CPP_DIR}/nmos/node_resources.cpp - ${NMOS_CPP_DIR}/nmos/node_server.cpp - ${NMOS_CPP_DIR}/nmos/node_system_behaviour.cpp - ${NMOS_CPP_DIR}/nmos/process_utils.cpp - ${NMOS_CPP_DIR}/nmos/query_api.cpp - ${NMOS_CPP_DIR}/nmos/query_utils.cpp - ${NMOS_CPP_DIR}/nmos/query_ws_api.cpp - ${NMOS_CPP_DIR}/nmos/rational.cpp - ${NMOS_CPP_DIR}/nmos/registration_api.cpp - ${NMOS_CPP_DIR}/nmos/registry_resources.cpp - ${NMOS_CPP_DIR}/nmos/registry_server.cpp - ${NMOS_CPP_DIR}/nmos/resource.cpp - ${NMOS_CPP_DIR}/nmos/resources.cpp - ${NMOS_CPP_DIR}/nmos/schemas_api.cpp - ${NMOS_CPP_DIR}/nmos/sdp_utils.cpp - ${NMOS_CPP_DIR}/nmos/server.cpp - ${NMOS_CPP_DIR}/nmos/server_utils.cpp - ${NMOS_CPP_DIR}/nmos/settings.cpp - ${NMOS_CPP_DIR}/nmos/settings_api.cpp - ${NMOS_CPP_DIR}/nmos/system_api.cpp - ${NMOS_CPP_DIR}/nmos/system_resources.cpp + nmos/activation_utils.cpp + nmos/admin_ui.cpp + nmos/api_downgrade.cpp + nmos/api_utils.cpp + nmos/authorization.cpp + nmos/authorization_handlers.cpp + nmos/authorization_redirect_api.cpp + nmos/authorization_behaviour.cpp + nmos/authorization_operation.cpp + nmos/authorization_state.cpp + nmos/authorization_utils.cpp + nmos/authorization_behaviour.cpp + nmos/capabilities.cpp + nmos/certificate_handlers.cpp + nmos/channelmapping_activation.cpp + nmos/channelmapping_api.cpp + nmos/channelmapping_resources.cpp + nmos/channels.cpp + nmos/client_utils.cpp + nmos/components.cpp + nmos/connection_activation.cpp + nmos/connection_api.cpp + nmos/connection_events_activation.cpp + nmos/connection_resources.cpp + nmos/control_protocol_handlers.cpp + nmos/control_protocol_methods.cpp + nmos/control_protocol_resource.cpp + nmos/control_protocol_resources.cpp + nmos/control_protocol_state.cpp + nmos/control_protocol_utils.cpp + nmos/control_protocol_ws_api.cpp + nmos/did_sdid.cpp + nmos/events_api.cpp + nmos/events_resources.cpp + nmos/events_ws_api.cpp + nmos/events_ws_client.cpp + nmos/filesystem_route.cpp + nmos/group_hint.cpp + nmos/id.cpp + nmos/lldp_handler.cpp + nmos/lldp_manager.cpp + nmos/json_schema.cpp + nmos/jwt_generator_impl.cpp + nmos/jwk_utils.cpp + nmos/jwks_uri_api.cpp + nmos/jwt_validator_impl.cpp + nmos/log_model.cpp + nmos/logging_api.cpp + nmos/manifest_api.cpp + nmos/mdns.cpp + nmos/mdns_api.cpp + nmos/node_api.cpp + nmos/node_api_target_handler.cpp + nmos/node_behaviour.cpp + nmos/node_interfaces.cpp + nmos/node_resource.cpp + nmos/node_resources.cpp + nmos/node_server.cpp + nmos/node_system_behaviour.cpp + nmos/ocsp_behaviour.cpp + nmos/ocsp_response_handler.cpp + nmos/ocsp_utils.cpp + nmos/process_utils.cpp + nmos/query_api.cpp + nmos/query_utils.cpp + nmos/query_ws_api.cpp + nmos/rational.cpp + nmos/registration_api.cpp + nmos/registry_resources.cpp + nmos/registry_server.cpp + nmos/resource.cpp + nmos/resources.cpp + nmos/schemas_api.cpp + nmos/sdp_attributes.cpp + nmos/sdp_utils.cpp + nmos/server.cpp + nmos/server_utils.cpp + nmos/settings.cpp + nmos/settings_api.cpp + nmos/system_api.cpp + nmos/system_resources.cpp + nmos/video_jxsv.cpp + nmos/ws_api_utils.cpp ) set(NMOS_CPP_NMOS_HEADERS - ${NMOS_CPP_DIR}/nmos/activation_mode.h - ${NMOS_CPP_DIR}/nmos/activation_utils.h - ${NMOS_CPP_DIR}/nmos/admin_ui.h - ${NMOS_CPP_DIR}/nmos/api_downgrade.h - ${NMOS_CPP_DIR}/nmos/api_utils.h - ${NMOS_CPP_DIR}/nmos/api_version.h - ${NMOS_CPP_DIR}/nmos/capabilities.h - ${NMOS_CPP_DIR}/nmos/channelmapping_activation.h - ${NMOS_CPP_DIR}/nmos/channelmapping_api.h - ${NMOS_CPP_DIR}/nmos/channelmapping_resources.h - ${NMOS_CPP_DIR}/nmos/channels.h - ${NMOS_CPP_DIR}/nmos/client_utils.h - ${NMOS_CPP_DIR}/nmos/clock_name.h - ${NMOS_CPP_DIR}/nmos/clock_ref_type.h - ${NMOS_CPP_DIR}/nmos/colorspace.h - ${NMOS_CPP_DIR}/nmos/components.h - ${NMOS_CPP_DIR}/nmos/copyable_atomic.h - ${NMOS_CPP_DIR}/nmos/connection_activation.h - ${NMOS_CPP_DIR}/nmos/connection_api.h - ${NMOS_CPP_DIR}/nmos/connection_events_activation.h - ${NMOS_CPP_DIR}/nmos/connection_resources.h - ${NMOS_CPP_DIR}/nmos/device_type.h - ${NMOS_CPP_DIR}/nmos/did_sdid.h - ${NMOS_CPP_DIR}/nmos/event_type.h - ${NMOS_CPP_DIR}/nmos/events_api.h - ${NMOS_CPP_DIR}/nmos/events_resources.h - ${NMOS_CPP_DIR}/nmos/events_ws_api.h - ${NMOS_CPP_DIR}/nmos/events_ws_client.h - ${NMOS_CPP_DIR}/nmos/filesystem_route.h - ${NMOS_CPP_DIR}/nmos/format.h - ${NMOS_CPP_DIR}/nmos/group_hint.h - ${NMOS_CPP_DIR}/nmos/health.h - ${NMOS_CPP_DIR}/nmos/id.h - ${NMOS_CPP_DIR}/nmos/interlace_mode.h - ${NMOS_CPP_DIR}/nmos/is04_versions.h - ${NMOS_CPP_DIR}/nmos/is05_versions.h - ${NMOS_CPP_DIR}/nmos/is07_versions.h - ${NMOS_CPP_DIR}/nmos/is08_versions.h - ${NMOS_CPP_DIR}/nmos/is09_versions.h - ${NMOS_CPP_DIR}/nmos/json_fields.h - ${NMOS_CPP_DIR}/nmos/json_schema.h - ${NMOS_CPP_DIR}/nmos/lldp_handler.h - ${NMOS_CPP_DIR}/nmos/lldp_manager.h - ${NMOS_CPP_DIR}/nmos/log_gate.h - ${NMOS_CPP_DIR}/nmos/log_manip.h - ${NMOS_CPP_DIR}/nmos/log_model.h - ${NMOS_CPP_DIR}/nmos/logging_api.h - ${NMOS_CPP_DIR}/nmos/manifest_api.h - ${NMOS_CPP_DIR}/nmos/mdns.h - ${NMOS_CPP_DIR}/nmos/mdns_api.h - ${NMOS_CPP_DIR}/nmos/mdns_versions.h - ${NMOS_CPP_DIR}/nmos/media_type.h - ${NMOS_CPP_DIR}/nmos/model.h - ${NMOS_CPP_DIR}/nmos/mutex.h - ${NMOS_CPP_DIR}/nmos/node_api.h - ${NMOS_CPP_DIR}/nmos/node_api_target_handler.h - ${NMOS_CPP_DIR}/nmos/node_behaviour.h - ${NMOS_CPP_DIR}/nmos/node_interfaces.h - ${NMOS_CPP_DIR}/nmos/node_resource.h - ${NMOS_CPP_DIR}/nmos/node_resources.h - ${NMOS_CPP_DIR}/nmos/node_server.h - ${NMOS_CPP_DIR}/nmos/node_system_behaviour.h - ${NMOS_CPP_DIR}/nmos/paging_utils.h - ${NMOS_CPP_DIR}/nmos/process_utils.h - ${NMOS_CPP_DIR}/nmos/query_api.h - ${NMOS_CPP_DIR}/nmos/query_utils.h - ${NMOS_CPP_DIR}/nmos/query_ws_api.h - ${NMOS_CPP_DIR}/nmos/random.h - ${NMOS_CPP_DIR}/nmos/rational.h - ${NMOS_CPP_DIR}/nmos/registration_api.h - ${NMOS_CPP_DIR}/nmos/registry_resources.h - ${NMOS_CPP_DIR}/nmos/registry_server.h - ${NMOS_CPP_DIR}/nmos/resource.h - ${NMOS_CPP_DIR}/nmos/resources.h - ${NMOS_CPP_DIR}/nmos/schemas_api.h - ${NMOS_CPP_DIR}/nmos/sdp_utils.h - ${NMOS_CPP_DIR}/nmos/server.h - ${NMOS_CPP_DIR}/nmos/server_utils.h - ${NMOS_CPP_DIR}/nmos/settings.h - ${NMOS_CPP_DIR}/nmos/settings_api.h - ${NMOS_CPP_DIR}/nmos/slog.h - ${NMOS_CPP_DIR}/nmos/ssl_context_options.h - ${NMOS_CPP_DIR}/nmos/string_enum.h - ${NMOS_CPP_DIR}/nmos/system_api.h - ${NMOS_CPP_DIR}/nmos/system_resources.h - ${NMOS_CPP_DIR}/nmos/tai.h - ${NMOS_CPP_DIR}/nmos/thread_utils.h - ${NMOS_CPP_DIR}/nmos/transfer_characteristic.h - ${NMOS_CPP_DIR}/nmos/transport.h - ${NMOS_CPP_DIR}/nmos/type.h - ${NMOS_CPP_DIR}/nmos/version.h - ${NMOS_CPP_DIR}/nmos/vpid_code.h - ${NMOS_CPP_DIR}/nmos/websockets.h + nmos/activation_mode.h + nmos/activation_utils.h + nmos/admin_ui.h + nmos/api_downgrade.h + nmos/api_utils.h + nmos/api_version.h + nmos/asset.h + nmos/authorization.h + nmos/authorization_handlers.h + nmos/authorization_redirect_api.h + nmos/authorization_behaviour.h + nmos/authorization_operation.h + nmos/authorization_scopes.h + nmos/authorization_state.h + nmos/authorization_utils.h + nmos/capabilities.h + nmos/certificate_handlers.h + nmos/certificate_settings.h + nmos/channelmapping_activation.h + nmos/channelmapping_api.h + nmos/channelmapping_resources.h + nmos/channels.h + nmos/client_utils.h + nmos/clock_name.h + nmos/clock_ref_type.h + nmos/colorspace.h + nmos/components.h + nmos/copyable_atomic.h + nmos/connection_activation.h + nmos/connection_api.h + nmos/connection_events_activation.h + nmos/connection_resources.h + nmos/control_protocol_handlers.h + nmos/control_protocol_methods.h + nmos/control_protocol_nmos_channel_mapping_resource_type.h + nmos/control_protocol_nmos_resource_type.h + nmos/control_protocol_resource.h + nmos/control_protocol_resources.h + nmos/control_protocol_state.h + nmos/control_protocol_typedefs.h + nmos/control_protocol_utils.h + nmos/control_protocol_ws_api.h + nmos/device_type.h + nmos/did_sdid.h + nmos/event_type.h + nmos/events_api.h + nmos/events_resources.h + nmos/events_ws_api.h + nmos/events_ws_client.h + nmos/filesystem_route.h + nmos/format.h + nmos/group_hint.h + nmos/health.h + nmos/id.h + nmos/interlace_mode.h + nmos/is04_versions.h + nmos/is05_versions.h + nmos/is07_versions.h + nmos/is08_versions.h + nmos/is09_versions.h + nmos/is10_versions.h + nmos/is12_versions.h + nmos/issuers.h + nmos/json_fields.h + nmos/json_schema.h + nmos/jwks_uri_api.h + nmos/jwk_utils.h + nmos/jwt_generator.h + nmos/jwt_validator.h + nmos/lldp_handler.h + nmos/lldp_manager.h + nmos/log_gate.h + nmos/log_manip.h + nmos/log_model.h + nmos/logging_api.h + nmos/manifest_api.h + nmos/mdns.h + nmos/mdns_api.h + nmos/mdns_versions.h + nmos/media_type.h + nmos/model.h + nmos/mutex.h + nmos/node_api.h + nmos/node_api_target_handler.h + nmos/node_behaviour.h + nmos/node_interfaces.h + nmos/node_resource.h + nmos/node_resources.h + nmos/node_server.h + nmos/node_system_behaviour.h + nmos/ocsp_behaviour.h + nmos/ocsp_response_handler.h + nmos/ocsp_state.h + nmos/ocsp_utils.h + nmos/paging_utils.h + nmos/process_utils.h + nmos/query_api.h + nmos/query_utils.h + nmos/query_ws_api.h + nmos/random.h + nmos/rational.h + nmos/registration_api.h + nmos/registry_resources.h + nmos/registry_server.h + nmos/resource.h + nmos/resources.h + nmos/schemas_api.h + nmos/scope.h + nmos/sdp_attributes.h + nmos/sdp_utils.h + nmos/server.h + nmos/server_utils.h + nmos/settings.h + nmos/settings_api.h + nmos/slog.h + nmos/ssl_context_options.h + nmos/st2110_21_sender_type.h + nmos/string_enum.h + nmos/string_enum_fwd.h + nmos/system_api.h + nmos/system_resources.h + nmos/tai.h + nmos/thread_utils.h + nmos/transfer_characteristic.h + nmos/transport.h + nmos/type.h + nmos/version.h + nmos/video_jxsv.h + nmos/vpid_code.h + nmos/websockets.h + nmos/ws_api_utils.h ) set(NMOS_CPP_PPLX_SOURCES - ${NMOS_CPP_DIR}/pplx/pplx_utils.cpp + pplx/pplx_utils.cpp ) set(NMOS_CPP_PPLX_HEADERS - ${NMOS_CPP_DIR}/pplx/pplx_utils.h + pplx/pplx_utils.h ) set(NMOS_CPP_RQL_SOURCES - ${NMOS_CPP_DIR}/rql/rql.cpp + rql/rql.cpp ) set(NMOS_CPP_RQL_HEADERS - ${NMOS_CPP_DIR}/rql/rql.h + rql/rql.h ) set(NMOS_CPP_SDP_SOURCES - ${NMOS_CPP_DIR}/sdp/sdp_grammar.cpp + sdp/sdp_grammar.cpp ) set(NMOS_CPP_SDP_HEADERS - ${NMOS_CPP_DIR}/sdp/json.h - ${NMOS_CPP_DIR}/sdp/ntp.h - ${NMOS_CPP_DIR}/sdp/sdp.h - ${NMOS_CPP_DIR}/sdp/sdp_grammar.h + sdp/json.h + sdp/ntp.h + sdp/sdp.h + sdp/sdp_grammar.h ) -set(NMOS_CPP_SLOG_HEADERS - ${NMOS_CPP_DIR}/slog/all_in_one.h +set(NMOS_CPP_SSL_SOURCES + ssl/ssl_utils.cpp + ) +set(NMOS_CPP_SSL_HEADERS + ssl/ssl_utils.h ) add_library( - nmos-cpp_static STATIC + nmos-cpp STATIC ${NMOS_CPP_BST_SOURCES} ${NMOS_CPP_BST_HEADERS} ${NMOS_CPP_CPPREST_SOURCES} ${NMOS_CPP_CPPREST_HEADERS} ${NMOS_CPP_NMOS_SOURCES} ${NMOS_CPP_NMOS_HEADERS} + ${NMOS_CPP_JWK_HEADERS} ${NMOS_CPP_PPLX_SOURCES} ${NMOS_CPP_PPLX_HEADERS} ${NMOS_CPP_RQL_SOURCES} ${NMOS_CPP_RQL_HEADERS} ${NMOS_CPP_SDP_SOURCES} ${NMOS_CPP_SDP_HEADERS} - ${NMOS_CPP_SLOG_HEADERS} + ${NMOS_CPP_SSL_SOURCES} + ${NMOS_CPP_SSL_HEADERS} ) source_group("bst\\Source Files" FILES ${NMOS_CPP_BST_SOURCES}) @@ -903,42 +1175,76 @@ source_group("nmos\\Source Files" FILES ${NMOS_CPP_NMOS_SOURCES}) source_group("pplx\\Source Files" FILES ${NMOS_CPP_PPLX_SOURCES}) source_group("rql\\Source Files" FILES ${NMOS_CPP_RQL_SOURCES}) source_group("sdp\\Source Files" FILES ${NMOS_CPP_SDP_SOURCES}) +source_group("ssl\\Source Files" FILES ${NMOS_CPP_SSL_SOURCES}) source_group("bst\\Header Files" FILES ${NMOS_CPP_BST_HEADERS}) source_group("cpprest\\Header Files" FILES ${NMOS_CPP_CPPREST_HEADERS}) +source_group("jwk\\Header Files" FILES ${NMOS_CPP_JWK_HEADERS}) source_group("nmos\\Header Files" FILES ${NMOS_CPP_NMOS_HEADERS}) source_group("pplx\\Header Files" FILES ${NMOS_CPP_PPLX_HEADERS}) source_group("rql\\Header Files" FILES ${NMOS_CPP_RQL_HEADERS}) source_group("sdp\\Header Files" FILES ${NMOS_CPP_SDP_HEADERS}) -source_group("slog\\Header Files" FILES ${NMOS_CPP_SLOG_HEADERS}) +source_group("ssl\\Header Files" FILES ${NMOS_CPP_SSL_HEADERS}) target_link_libraries( - nmos-cpp_static - mdns_static - ${CPPRESTSDK_TARGET} - json_schema_validator_static - nmos_is04_schemas_static - nmos_is05_schemas_static - nmos_is08_schemas_static - nmos_is09_schemas_static - ${Boost_LIBRARIES} - ${OPENSSL_TARGETS} - ${PLATFORM_LIBS} - ) -if (BUILD_LLDP) + nmos-cpp PRIVATE + nmos-cpp::compile-settings + ) +target_link_libraries( + nmos-cpp PUBLIC + nmos-cpp::nmos_is04_schemas + nmos-cpp::nmos_is05_schemas + nmos-cpp::nmos_is08_schemas + nmos-cpp::nmos_is09_schemas + nmos-cpp::nmos_is10_schemas + nmos-cpp::nmos_is12_schemas + nmos-cpp::mdns + nmos-cpp::slog + nmos-cpp::OpenSSL + nmos-cpp::cpprestsdk + nmos-cpp::Boost + nmos-cpp::jwt-cpp + ) +target_link_libraries( + nmos-cpp PRIVATE + nmos-cpp::websocketpp + nmos-cpp::json_schema_validator + ) +if(NMOS_CPP_BUILD_LLDP) target_link_libraries( - nmos-cpp_static - lldp_static + nmos-cpp PUBLIC + nmos-cpp::lldp ) endif() +if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin") + # link to resolver functions (for cpprest/host_utils.cpp) + # note: this is no longer required on all platforms + target_link_libraries( + nmos-cpp PUBLIC + resolv + ) + if(CMAKE_CXX_COMPILER_ID MATCHES GNU AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 5.3) + # link to std::filesystem functions (for bst/filesystem.h, used by nmos/filesystem_route.cpp) + target_link_libraries( + nmos-cpp PUBLIC + stdc++fs + ) + endif() +endif() +target_include_directories(nmos-cpp PUBLIC + $ + $ + ) -install(TARGETS nmos-cpp_static DESTINATION lib) - -install(FILES ${NMOS_CPP_BST_HEADERS} DESTINATION include${NMOS_CPP_INCLUDE_PREFIX}/bst) -install(FILES ${NMOS_CPP_CPPREST_HEADERS} DESTINATION include${NMOS_CPP_INCLUDE_PREFIX}/cpprest) -install(FILES ${NMOS_CPP_CPPREST_DETAILS_HEADERS} DESTINATION include${NMOS_CPP_INCLUDE_PREFIX}/cpprest/details) -install(FILES ${NMOS_CPP_NMOS_HEADERS} DESTINATION include${NMOS_CPP_INCLUDE_PREFIX}/nmos) -install(FILES ${NMOS_CPP_PPLX_HEADERS} DESTINATION include${NMOS_CPP_INCLUDE_PREFIX}/pplx) -install(FILES ${NMOS_CPP_RQL_HEADERS} DESTINATION include${NMOS_CPP_INCLUDE_PREFIX}/rql) -install(FILES ${NMOS_CPP_SDP_HEADERS} DESTINATION include${NMOS_CPP_INCLUDE_PREFIX}/sdp) -install(FILES ${NMOS_CPP_SLOG_HEADERS} DESTINATION include${NMOS_CPP_INCLUDE_PREFIX}/slog) +install(FILES ${NMOS_CPP_BST_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/bst) +install(FILES ${NMOS_CPP_CPPREST_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/cpprest) +install(FILES ${NMOS_CPP_CPPREST_DETAILS_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/cpprest/details) +install(FILES ${NMOS_CPP_JWK_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/jwk) +install(FILES ${NMOS_CPP_NMOS_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/nmos) +install(FILES ${NMOS_CPP_PPLX_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/pplx) +install(FILES ${NMOS_CPP_RQL_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/rql) +install(FILES ${NMOS_CPP_SDP_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/sdp) +install(FILES ${NMOS_CPP_SSL_HEADERS} DESTINATION ${NMOS_CPP_INSTALL_INCLUDEDIR}/ssl) + +list(APPEND NMOS_CPP_TARGETS nmos-cpp) +add_library(nmos-cpp::nmos-cpp ALIAS nmos-cpp) diff --git a/Development/cmake/NmosCppNode.cmake b/Development/cmake/NmosCppNode.cmake new file mode 100644 index 000000000..df1ab1c2b --- /dev/null +++ b/Development/cmake/NmosCppNode.cmake @@ -0,0 +1,31 @@ +# nmos-cpp-node executable + +set(NMOS_CPP_NODE_SOURCES + nmos-cpp-node/main.cpp + nmos-cpp-node/node_implementation.cpp + ) +set(NMOS_CPP_NODE_HEADERS + nmos-cpp-node/node_implementation.h + ) + +add_executable( + nmos-cpp-node + ${NMOS_CPP_NODE_SOURCES} + ${NMOS_CPP_NODE_HEADERS} + nmos-cpp-node/config.json + ) + +source_group("Source Files" FILES ${NMOS_CPP_NODE_SOURCES}) +source_group("Header Files" FILES ${NMOS_CPP_NODE_HEADERS}) + +target_link_libraries( + nmos-cpp-node + nmos-cpp::compile-settings + nmos-cpp::nmos-cpp + ) +# root directory to find e.g. nmos-cpp-node/node_implementation.h +target_include_directories(nmos-cpp-node PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ) + +list(APPEND NMOS_CPP_TARGETS nmos-cpp-node) diff --git a/Development/cmake/NmosCppRegistry.cmake b/Development/cmake/NmosCppRegistry.cmake new file mode 100644 index 000000000..08ee19701 --- /dev/null +++ b/Development/cmake/NmosCppRegistry.cmake @@ -0,0 +1,31 @@ +# nmos-cpp-registry executable + +set(NMOS_CPP_REGISTRY_SOURCES + nmos-cpp-registry/main.cpp + nmos-cpp-registry/registry_implementation.cpp + ) +set(NMOS_CPP_REGISTRY_HEADERS + nmos-cpp-registry/registry_implementation.h + ) + +add_executable( + nmos-cpp-registry + ${NMOS_CPP_REGISTRY_SOURCES} + ${NMOS_CPP_REGISTRY_HEADERS} + nmos-cpp-registry/config.json + ) + +source_group("Source Files" FILES ${NMOS_CPP_REGISTRY_SOURCES}) +source_group("Header Files" FILES ${NMOS_CPP_REGISTRY_HEADERS}) + +target_link_libraries( + nmos-cpp-registry + nmos-cpp::compile-settings + nmos-cpp::nmos-cpp + ) +# root directory to find e.g. nmos-cpp-registry/registry_implementation.h +target_include_directories(nmos-cpp-registry PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ) + +list(APPEND NMOS_CPP_TARGETS nmos-cpp-registry) diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index 51ba68055..45d953d30 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -1,14 +1,7 @@ -# CMake instructions for making the nmos-cpp test program - -# caller can set NMOS_CPP_DIR if the project is different -if (NOT DEFINED NMOS_CPP_DIR) - set (NMOS_CPP_DIR ${PROJECT_SOURCE_DIR}) -endif() - -# nmos-cpp-test +# nmos-cpp-test executable set(NMOS_CPP_TEST_SOURCES - ${NMOS_CPP_DIR}/nmos-cpp-test/main.cpp + nmos-cpp-test/main.cpp ) set(NMOS_CPP_TEST_HEADERS ) @@ -16,46 +9,73 @@ set(NMOS_CPP_TEST_HEADERS set(NMOS_CPP_TEST_BST_TEST_SOURCES ) set(NMOS_CPP_TEST_BST_TEST_HEADERS - ${NMOS_CPP_DIR}/bst/test/test.h + bst/test/test.h ) set(NMOS_CPP_TEST_CPPREST_TEST_SOURCES - ${NMOS_CPP_DIR}/cpprest/test/api_router_test.cpp - ${NMOS_CPP_DIR}/cpprest/test/http_utils_test.cpp - ${NMOS_CPP_DIR}/cpprest/test/json_utils_test.cpp - ${NMOS_CPP_DIR}/cpprest/test/json_visit_test.cpp - ${NMOS_CPP_DIR}/cpprest/test/regex_utils_test.cpp + cpprest/test/api_router_test.cpp + cpprest/test/basic_utils_test.cpp + cpprest/test/http_utils_test.cpp + cpprest/test/json_utils_test.cpp + cpprest/test/json_visit_test.cpp + cpprest/test/regex_utils_test.cpp + cpprest/test/ws_listener_test.cpp ) set(NMOS_CPP_TEST_CPPREST_TEST_HEADERS ) -if (BUILD_LLDP) +if(NMOS_CPP_BUILD_LLDP) set(NMOS_CPP_TEST_LLDP_TEST_SOURCES - ${NMOS_CPP_DIR}/lldp/test/lldp_test.cpp + lldp/test/lldp_test.cpp ) set(NMOS_CPP_TEST_LLDP_TEST_HEADERS ) endif() set(NMOS_CPP_TEST_MDNS_TEST_SOURCES - ${NMOS_CPP_DIR}/mdns/test/core_test.cpp - ${NMOS_CPP_DIR}/mdns/test/mdns_test.cpp + mdns/test/core_test.cpp + mdns/test/mdns_test.cpp ) set(NMOS_CPP_TEST_MDNS_TEST_HEADERS ) set(NMOS_CPP_TEST_NMOS_TEST_SOURCES - ${NMOS_CPP_DIR}/nmos/test/api_utils_test.cpp - ${NMOS_CPP_DIR}/nmos/test/channels_test.cpp - ${NMOS_CPP_DIR}/nmos/test/did_sdid_test.cpp - ${NMOS_CPP_DIR}/nmos/test/event_type_test.cpp - ${NMOS_CPP_DIR}/nmos/test/paging_utils_test.cpp + nmos/test/api_utils_test.cpp + nmos/test/capabilities_test.cpp + nmos/test/channels_test.cpp + nmos/test/control_protocol_test.cpp + nmos/test/control_protocol_methods_test.cpp + nmos/test/did_sdid_test.cpp + nmos/test/event_type_test.cpp + nmos/test/json_validator_test.cpp + nmos/test/jwt_generator_test.cpp + nmos/test/jwt_validation_test.cpp + nmos/test/paging_utils_test.cpp + nmos/test/query_api_test.cpp + nmos/test/sdp_test_utils.cpp + nmos/test/sdp_utils_test.cpp + nmos/test/slog_test.cpp + nmos/test/system_resources_test.cpp + nmos/test/video_jxsv_test.cpp ) set(NMOS_CPP_TEST_NMOS_TEST_HEADERS + nmos/test/sdp_test_utils.h + ) + +set(NMOS_CPP_TEST_PPLX_TEST_SOURCES + pplx/test/pplx_utils_test.cpp + ) +set(NMOS_CPP_TEST_PPLX_TEST_HEADERS + ) + +set(NMOS_CPP_TEST_RQL_TEST_SOURCES + rql/test/rql_test.cpp + ) +set(NMOS_CPP_TEST_RQL_TEST_HEADERS ) set(NMOS_CPP_TEST_SDP_TEST_SOURCES - ${NMOS_CPP_DIR}/sdp/test/sdp_test.cpp + sdp/test/sdp_test.cpp ) set(NMOS_CPP_TEST_SDP_TEST_HEADERS ) @@ -74,6 +94,10 @@ add_executable( ${NMOS_CPP_TEST_MDNS_TEST_HEADERS} ${NMOS_CPP_TEST_NMOS_TEST_SOURCES} ${NMOS_CPP_TEST_NMOS_TEST_HEADERS} + ${NMOS_CPP_TEST_PPLX_TEST_SOURCES} + ${NMOS_CPP_TEST_PPLX_TEST_HEADERS} + ${NMOS_CPP_TEST_RQL_TEST_SOURCES} + ${NMOS_CPP_TEST_RQL_TEST_HEADERS} ${NMOS_CPP_TEST_SDP_TEST_SOURCES} ${NMOS_CPP_TEST_SDP_TEST_HEADERS} ) @@ -84,6 +108,8 @@ source_group("cpprest\\test\\Source Files" FILES ${NMOS_CPP_TEST_CPPREST_TEST_SO source_group("lldp\\test\\Source Files" FILES ${NMOS_CPP_TEST_LLDP_TEST_SOURCES}) source_group("mdns\\test\\Source Files" FILES ${NMOS_CPP_TEST_MDNS_TEST_SOURCES}) source_group("nmos\\test\\Source Files" FILES ${NMOS_CPP_TEST_NMOS_TEST_SOURCES}) +source_group("pplx\\test\\Source Files" FILES ${NMOS_CPP_TEST_PPLX_TEST_SOURCES}) +source_group("rql\\test\\Source Files" FILES ${NMOS_CPP_TEST_RQL_TEST_SOURCES}) source_group("sdp\\test\\Source Files" FILES ${NMOS_CPP_TEST_SDP_TEST_SOURCES}) source_group("Header Files" FILES ${NMOS_CPP_TEST_HEADERS}) @@ -92,31 +118,31 @@ source_group("cpprest\\test\\Header Files" FILES ${NMOS_CPP_TEST_CPPREST_TEST_HE source_group("lldp\\test\\Header Files" FILES ${NMOS_CPP_TEST_LLDP_TEST_HEADERS}) source_group("mdns\\test\\Header Files" FILES ${NMOS_CPP_TEST_MDNS_TEST_HEADERS}) source_group("nmos\\test\\Header Files" FILES ${NMOS_CPP_TEST_NMOS_TEST_HEADERS}) +source_group("pplx\\test\\Header Files" FILES ${NMOS_CPP_TEST_PPLX_TEST_HEADERS}) +source_group("rql\\test\\Header Files" FILES ${NMOS_CPP_TEST_RQL_TEST_HEADERS}) source_group("sdp\\test\\Header Files" FILES ${NMOS_CPP_TEST_SDP_TEST_HEADERS}) target_link_libraries( nmos-cpp-test - nmos-cpp_static - mdns_static - ${CPPRESTSDK_TARGET} - ${PLATFORM_LIBS} - ${Boost_LIBRARIES} + nmos-cpp::compile-settings + nmos-cpp::nmos-cpp + nmos-cpp::mdns + nmos-cpp::cpprestsdk + nmos-cpp::Boost + nmos-cpp::jwt-cpp ) -if (BUILD_LLDP) +if(NMOS_CPP_BUILD_LLDP) target_link_libraries( nmos-cpp-test - lldp_static - ) -endif() - -if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") - # Conan packages usually don't include PDB files so suppress the resulting warning - set_target_properties( - nmos-cpp-test - PROPERTIES - LINK_FLAGS "/ignore:4099" + nmos-cpp::lldp ) endif() +# root directory to find e.g. bst/test/test.h +# third_party to find e.g. catch/catch.hpp +target_include_directories(nmos-cpp-test PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/third_party + ) include(Catch) diff --git a/Development/cmake/nmos-cpp-config.cmake.in b/Development/cmake/nmos-cpp-config.cmake.in new file mode 100644 index 000000000..9f49527bc --- /dev/null +++ b/Development/cmake/nmos-cpp-config.cmake.in @@ -0,0 +1,37 @@ +@PACKAGE_INIT@ + +# set custom find-module path +set(_NMOS_CPP_MODULE_PATH_SAVE "${CMAKE_MODULE_PATH}") +list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_LIST_DIR}") + +# find install dependencies cf. find_package calls in NmosCppDependencies.cmake +include(CMakeFindDependencyMacro) +find_dependency(Boost COMPONENTS @FIND_BOOST_COMPONENTS@) +find_dependency(cpprestsdk) +find_dependency(OpenSSL) +if(NOT @NMOS_CPP_USE_SUPPLIED_JSON_SCHEMA_VALIDATOR@) + find_dependency(nlohmann_json_schema_validator) +endif() +if(NOT @NMOS_CPP_USE_SUPPLIED_JWT_CPP@) + find_dependency(jwt-cpp) +endif() +if(@CMAKE_SYSTEM_NAME@ STREQUAL "Linux") + if(@NMOS_CPP_USE_AVAHI@) + find_dependency(Avahi) + else() + find_dependency(DNSSD) + endif() +elseif(@CMAKE_SYSTEM_NAME@ STREQUAL "Windows") + if(@NMOS_CPP_USE_BONJOUR_SDK@) + find_dependency(DNSSD) + endif() +endif() + +# unset custom find-module path +set(CMAKE_MODULE_PATH "${_NMOS_CPP_MODULE_PATH_SAVE}") +unset(_NMOS_CPP_MODULE_PATH_SAVE) + +# generate import targets cf. install(EXPORT) call in NmosCppExports.cmake +include("${CMAKE_CURRENT_LIST_DIR}/nmos-cpp-targets.cmake") + +check_required_components(nmos-cpp) diff --git a/Development/conanfile.txt b/Development/conanfile.txt index 3d28eb29e..aa55298b1 100644 --- a/Development/conanfile.txt +++ b/Development/conanfile.txt @@ -1,9 +1,12 @@ [requires] -boost/1.75.0 -openssl/1.1.1i -cpprestsdk/2.10.17 +boost/1.83.0 +cpprestsdk/2.10.19 websocketpp/0.8.2 -zlib/1.2.11 +openssl/3.2.1 +json-schema-validator/2.3.0 +nlohmann_json/3.11.3 +zlib/1.2.13 +jwt-cpp/0.7.0 [imports] bin, *.dll -> ./bin @@ -11,4 +14,7 @@ lib, *.so* -> ./lib lib, *.dylib* -> ./lib [options] -boost:shared=False +boost/*:shared=False + +[generators] +CMakeDeps diff --git a/Development/cpprest/access_token_error.h b/Development/cpprest/access_token_error.h new file mode 100644 index 000000000..0fa94f2a3 --- /dev/null +++ b/Development/cpprest/access_token_error.h @@ -0,0 +1,67 @@ +#ifndef CPPREST_ACCESS_TOKEN_ERROR_H +#define CPPREST_ACCESS_TOKEN_ERROR_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + namespace experimental + { + // for redirect error + // "If the resource owner denies the access request or if the request + // fails for reasons other than a missing or invalid redirection URI, + // the authorization server informs the client by adding the following + // parameters to the query component of the redirection URI using the + // "application/x-www-form-urlencoded" format" + // see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + + // for direct error: + // If the access token request is invalid or unauthorized + // "The authorization server responds with an HTTP 400 (Bad Request) + // status code(unless specified otherwise) and includes the following + // parameters with the response:" + // and https://tools.ietf.org/html/rfc6749#section-5.2 + // "The parameters are included in the entity-body of the HTTP response + // using the "application/json" media type" + + DEFINE_STRING_ENUM(access_token_error) + namespace access_token_errors + { + const access_token_error invalid_request{ U("invalid_request") }; // used for redirect error and direct error + const access_token_error unauthorized_client{ U("unauthorized_client") }; // used for redirect error and direct error + const access_token_error access_denied{ U("access_denied") }; // used for redirect error + const access_token_error unsupported_response_type{ U("unsupported_response_type") }; // used for redirect error + const access_token_error invalid_scope{ U("invalid_scope") }; // used for redirect error and direct error + const access_token_error server_error{ U("server_error") }; // used for redirect error + const access_token_error temporarily_unavailable{ U("temporarily_unavailable") }; // used for redirect error + const access_token_error invalid_client{ U("invalid_client") }; // used for direct error + const access_token_error invalid_grant{ U("invalid_grant") }; // used for direct error + const access_token_error unsupported_grant_type{ U("unsupported_grant_type") }; // used for direct error + } + + inline access_token_error to_access_token_error(const utility::string_t& error) + { + using namespace access_token_errors; + if (invalid_request.name == error) { return invalid_request; } + if (unauthorized_client.name == error) { return unauthorized_client; } + if (access_denied.name == error) { return access_denied; } + if (unsupported_response_type.name == error) { return unsupported_response_type; } + if (invalid_scope.name == error) { return invalid_scope; } + if (server_error.name == error) { return server_error; } + if (temporarily_unavailable.name == error) { return temporarily_unavailable; } + if (invalid_client.name == error) { return invalid_client; } + if (invalid_grant.name == error) { return invalid_grant; } + if (unsupported_grant_type.name == error) { return unsupported_grant_type; } + return{}; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/api_router.cpp b/Development/cpprest/api_router.cpp index d06f8ebbc..c0bceb3f3 100644 --- a/Development/cpprest/api_router.cpp +++ b/Development/cpprest/api_router.cpp @@ -9,7 +9,32 @@ namespace web { namespace listener { - utility::string_t api_router::get_route_relative_path(const web::http::http_request& req, const utility::string_t& route_path) + // api router implementation + namespace details + { + class api_router_impl : public std::enable_shared_from_this + { + public: + typedef std::pair regex_named_sub_matches_type; + struct route { match_flag_type flags; regex_named_sub_matches_type route_pattern; web::http::method method; route_handler handler; }; + typedef std::list route_handlers; + typedef route_handlers::iterator iterator; + + static pplx::task call(const route_handler& handler, const route_handler& exception_handler, web::http::http_request req, web::http::http_response res, const utility::string_t& route_path, const route_parameters& parameters); + static void handle_method_not_allowed(const route& route, web::http::http_response& res, const utility::string_t& route_path, const route_parameters& parameters); + static route_parameters insert(route_parameters&& into, const route_parameters& range); + + pplx::task operator()(web::http::http_request req, web::http::http_response res, const utility::string_t& route_path, const route_parameters& parameters, iterator route); + + // to allow routes to be added out-of-order, support() and mount() could easily be given overloads that accept and return an iterator (const_iterator in C++11) + iterator insert(iterator where, match_flag_type flags, const utility::string_t& route_pattern, const web::http::method& method, route_handler handler); + + route_handlers routes; + route_handler exception_handler; + }; + } + + utility::string_t details::get_route_relative_path(const web::http::http_request& req, const utility::string_t& route_path) { // If the route path is empty, then just return the listener-relative URI. if (route_path.empty() || route_path == _XPLATSTR("/")) @@ -32,6 +57,7 @@ namespace web } api_router::api_router() + : impl(new details::api_router_impl()) {} void api_router::operator()(web::http::http_request req) @@ -51,6 +77,9 @@ namespace web res.set_status_code(status_codes::NotFound); } + // the task returned by reply() silently 'observes' any exception thrown from the underlying server + // reply() itself can throw http_exception if a response has already been sent, but that would indicate a programming error + // so don't handle exceptions with the specified exception handler here req.reply(res); } }); @@ -60,10 +89,10 @@ namespace web pplx::task api_router::operator()(web::http::http_request req, web::http::http_response res, const utility::string_t& route_path, const route_parameters& parameters) { - return (*this)(req, res, route_path, parameters, routes.begin()); + return (*impl)(req, res, route_path, parameters, impl->routes.begin()); } - pplx::task api_router::operator()(web::http::http_request req, web::http::http_response res, const utility::string_t& route_path, const route_parameters& parameters, iterator route) + pplx::task details::api_router_impl::operator()(web::http::http_request req, web::http::http_response res, const utility::string_t& route_path, const route_parameters& parameters, iterator route) { const utility::string_t path = get_route_relative_path(req, route_path); // required, as must live longer than the match results for (; routes.end() != route; ++route) @@ -78,11 +107,14 @@ namespace web if (route->method == req.method() || any_method == route->method) { - return call(route->handler, exception_handler, req, res, merged_path, merged_parameters).then([=](bool continue_matching) + // capture shared_this to extend lifetime into the continuation + auto shared_this = shared_from_this(); + return call(route->handler, exception_handler, req, res, merged_path, merged_parameters) + .then([shared_this, this, req, res, route_path, parameters, route](bool continue_matching) { if (!continue_matching) { - // short-circuit other routes, e.g. if the hander actually sent a reply rather than just modifying the response object + // short-circuit other routes, e.g. if the handler actually sent a reply rather than just modifying the response object return pplx::task_from_result(false); } @@ -99,7 +131,7 @@ namespace web return pplx::task_from_result(true); } - pplx::task api_router::call(const route_handler& handler, const route_handler& exception_handler, web::http::http_request req, web::http::http_response res, const utility::string_t& route_path, const route_parameters& parameters) + pplx::task details::api_router_impl::call(const route_handler& handler, const route_handler& exception_handler, web::http::http_request req, web::http::http_response res, const utility::string_t& route_path, const route_parameters& parameters) { if (!exception_handler) { @@ -131,7 +163,7 @@ namespace web } } - void api_router::handle_method_not_allowed(const route& route, web::http::http_response& res, const utility::string_t& route_path, const route_parameters& parameters) + void details::api_router_impl::handle_method_not_allowed(const route& route, web::http::http_response& res, const utility::string_t& route_path, const route_parameters& parameters) { // a preceding route handler may have already set a status code, but if not, this is worth reporting as a "near miss" if (empty_status_code == res.status_code()) @@ -148,36 +180,41 @@ namespace web void api_router::support(const utility::string_t& route_pattern, const web::http::method& method, route_handler handler) { - insert(routes.end(), match_entire, route_pattern, method, handler); + impl->insert(impl->routes.end(), details::match_entire, route_pattern, method, handler); } void api_router::support(const utility::string_t& route_pattern, route_handler all_handler) { - insert(routes.end(), match_entire, route_pattern, any_method, all_handler); + impl->insert(impl->routes.end(), details::match_entire, route_pattern, any_method, all_handler); } void api_router::mount(const utility::string_t& route_pattern, const web::http::method& method, route_handler handler) { - insert(routes.end(), match_prefix, route_pattern, method, handler); + impl->insert(impl->routes.end(), details::match_prefix, route_pattern, method, handler); } void api_router::mount(const utility::string_t& route_pattern, route_handler all_handler) { - insert(routes.end(), match_prefix, route_pattern, any_method, all_handler); + impl->insert(impl->routes.end(), details::match_prefix, route_pattern, any_method, all_handler); + } + + void api_router::pop_back() + { + impl->routes.pop_back(); } void api_router::set_exception_handler(route_handler handler) { - exception_handler = handler; + impl->exception_handler = handler; } - api_router::iterator api_router::insert(iterator where, match_flag_type flags, const utility::string_t& route_pattern, const web::http::method& method, route_handler handler) + details::api_router_impl::iterator details::api_router_impl::insert(iterator where, match_flag_type flags, const utility::string_t& route_pattern, const web::http::method& method, route_handler handler) { auto parsed = utility::parse_regex_named_sub_matches(route_pattern); return routes.insert(where, { flags, { utility::regex_t(parsed.first), parsed.second }, method, handler }); } - route_parameters api_router::get_parameters(const utility::named_sub_matches_t& parameter_sub_matches, const utility::smatch_t& route_match) + route_parameters details::get_parameters(const utility::named_sub_matches_t& parameter_sub_matches, const utility::smatch_t& route_match) { route_parameters parameters; for (auto& named_sub_match : parameter_sub_matches) @@ -191,14 +228,14 @@ namespace web return parameters; } - route_parameters api_router::insert(route_parameters&& into, const route_parameters& range) + route_parameters details::api_router_impl::insert(route_parameters&& into, const route_parameters& range) { // unorderd_map::insert only inserts elements if the container doesn't already contain an element with an equivalent key into.insert(range.begin(), range.end()); return std::move(into); } - bool api_router::route_regex_match(const utility::string_t& path, utility::smatch_t& route_match, const utility::regex_t& route_regex, match_flag_type flags) + bool details::route_regex_match(const utility::string_t& path, utility::smatch_t& route_match, const utility::regex_t& route_regex, match_flag_type flags) { return match_prefix == flags ? bst::regex_search(path, route_match, route_regex, bst::regex_constants::match_continuous) diff --git a/Development/cpprest/api_router.h b/Development/cpprest/api_router.h index e6a9f15cc..38b53e6c0 100644 --- a/Development/cpprest/api_router.h +++ b/Development/cpprest/api_router.h @@ -6,8 +6,7 @@ #include #include "cpprest/http_utils.h" #include "cpprest/json_ops.h" // hmm, only for names used in using declarations -#include "cpprest/regex_utils.h" // hmm, only for types used in private static functions -#include "detail/private_access.h" +#include "cpprest/regex_utils.h" // hmm, only for types used in details functions // api_router is an extension to http_listener that uses regexes to define route patterns namespace web @@ -28,9 +27,20 @@ namespace web // a handler may e.g. reply to the request or initiate asynchronous processing, and returns a flag indicating whether to continue matching routes or not typedef std::function(web::http::http_request req, web::http::http_response res, const utility::string_t& route_path, const route_parameters& parameters)> route_handler; + // api router implementation + namespace details + { + class api_router_impl; + + enum match_flag_type { match_entire = 0, match_prefix = 1 }; + + utility::string_t get_route_relative_path(const web::http::http_request& req, const utility::string_t& route_path); + route_parameters get_parameters(const utility::named_sub_matches_t& parameter_sub_matches, const utility::smatch_t& route_match); + bool route_regex_match(const utility::string_t& path, utility::smatch_t& route_match, const utility::regex_t& route_regex, match_flag_type flags); + } + class api_router { - DETAIL_PRIVATE_ACCESS_DECLARATION public: api_router(); @@ -49,30 +59,14 @@ namespace web // add a handler to support all other requests for this route and sub-routes (must be added after any method-specific handlers) void mount(const utility::string_t& route_pattern, route_handler all_handler); + // pop back handler + void pop_back(); + // provide an exception handler for this route and sub-routes (using std::current_exception, etc.) void set_exception_handler(route_handler handler); private: - enum match_flag_type { match_entire = 0, match_prefix = 1 }; - typedef std::pair regex_named_sub_matches_type; - struct route { match_flag_type flags; regex_named_sub_matches_type route_pattern; web::http::method method; route_handler handler; }; - typedef std::list route_handlers; - typedef route_handlers::iterator iterator; - - static utility::string_t get_route_relative_path(const web::http::http_request& req, const utility::string_t& route_path); - static pplx::task call(const route_handler& handler, const route_handler& exception_handler, web::http::http_request req, web::http::http_response res, const utility::string_t& route_path, const route_parameters& parameters); - static void handle_method_not_allowed(const route& route, web::http::http_response& res, const utility::string_t& route_path, const route_parameters& parameters); - static route_parameters get_parameters(const utility::named_sub_matches_t& parameter_sub_matches, const utility::smatch_t& route_match); - static route_parameters insert(route_parameters&& into, const route_parameters& range); - static bool route_regex_match(const utility::string_t& path, utility::smatch_t& route_match, const utility::regex_t& route_regex, match_flag_type flags); - - pplx::task operator()(web::http::http_request req, web::http::http_response res, const utility::string_t& route_path, const route_parameters& parameters, iterator route); - - // to allow routes to be added out-of-order, support() and mount() could easily be given overloads that accept and return an iterator (const_iterator in C++11) - iterator insert(iterator where, match_flag_type flags, const utility::string_t& route_pattern, const web::http::method& method, route_handler handler); - - route_handlers routes; - route_handler exception_handler; + std::shared_ptr impl; }; // convenient using declarations to make defining routers less verbose diff --git a/Development/cpprest/basic_utils.h b/Development/cpprest/basic_utils.h index 7f53bb239..ae60371f5 100644 --- a/Development/cpprest/basic_utils.h +++ b/Development/cpprest/basic_utils.h @@ -30,6 +30,39 @@ namespace utility return !iss.fail() ? t : default_val; } } + + // Encode the given byte array into a base64url string + // using the alternative alphabet and skipping the padding + // as per https://tools.ietf.org/html/rfc4648#section-5 + inline utility::string_t to_base64url(const std::vector& data) + { + auto str = utility::conversions::to_base64(data); + auto it = str.begin(); + for (; str.end() != it; ++it) + { + auto& c = *it; + if (U('=') == c) break; + if (U('+') == c) c = U('-'); + else if (U('/') == c) c = U('_'); + } + str.erase(it, str.end()); + return str; + } + + // Decode the given base64url string to a byte array + // using the alternative alphabet and skipping the padding + // as per https://tools.ietf.org/html/rfc4648#section-5 + inline std::vector from_base64url(utility::string_t str) + { + for (auto& c : str) + { + if (U('-') == c) c = U('+'); + else if (U('_') == c) c = U('/'); + } + auto m4 = str.size() % 4; + if (0 != m4) str.insert(str.end(), 4 - m4, U('=')); + return utility::conversions::from_base64(str); + } } } diff --git a/Development/cpprest/client_type.h b/Development/cpprest/client_type.h new file mode 100644 index 000000000..c23ca9eab --- /dev/null +++ b/Development/cpprest/client_type.h @@ -0,0 +1,27 @@ +#ifndef CPPREST_CLIENT_TYPE_H +#define CPPREST_CLIENT_TYPE_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + // see https://tools.ietf.org/html/rfc6749#section-2.1 + namespace experimental + { + DEFINE_STRING_ENUM(client_type) + namespace client_types + { + const client_type confidential_client{ U("confidential_client") }; + const client_type public_client{ U("public_client") }; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/code_challenge_method.h b/Development/cpprest/code_challenge_method.h new file mode 100644 index 000000000..2fb09e541 --- /dev/null +++ b/Development/cpprest/code_challenge_method.h @@ -0,0 +1,26 @@ +#ifndef CPPREST_CODE_CHALLENGE_METHOD_H +#define CPPREST_CODE_CHALLENGE_METHOD_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + namespace experimental + { + DEFINE_STRING_ENUM(code_challenge_method) + namespace code_challenge_methods + { + const code_challenge_method S256{ U("S256") }; + const code_challenge_method plain{ U("plain") }; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/details/boost_u_workaround.h b/Development/cpprest/details/boost_u_workaround.h index 9d991eea3..197b0b75f 100644 --- a/Development/cpprest/details/boost_u_workaround.h +++ b/Development/cpprest/details/boost_u_workaround.h @@ -1,8 +1,8 @@ // Workaround for conflict between the 'U' macro and some uses of U as e.g. a template parameter in Boost libraries #pragma once #ifdef __cplusplus -#include "cpprest/details/push_undef_u.h" +#include "push_undef_u.h" // See https://stackoverflow.com/questions/43245055/issue-with-boost-1-64-and-visual-studio-2017 #include -#include "cpprest/details/pop_u.h" +#include "pop_u.h" #endif diff --git a/Development/cpprest/grant_type.h b/Development/cpprest/grant_type.h new file mode 100644 index 000000000..bddfe7cf6 --- /dev/null +++ b/Development/cpprest/grant_type.h @@ -0,0 +1,46 @@ +#ifndef CPPREST_GRANT_TYPE_H +#define CPPREST_GRANT_TYPE_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + // see https://tools.ietf.org/html/rfc7591#section-2 + namespace experimental + { + DEFINE_STRING_ENUM(grant_type) + namespace grant_types + { + const grant_type authorization_code{ U("authorization_code") }; + const grant_type implicit{ U("implicit") }; + const grant_type password{ U("password") }; + const grant_type client_credentials{ U("client_credentials") }; + const grant_type refresh_token{ U("refresh_token") }; + const grant_type urn_ietf_params_oauth_grant_type_jwt_bearer{ U("urn:ietf:params:oauth:grant-type:jwt-bearer") }; + const grant_type urn_ietf_params_oauth_grant_type_saml2_bearer{ U("urn:ietf:params:oauth:grant-type:saml2-bearer") }; + const grant_type device_code{ U("urn:ietf:params:oauth:grant-type:device_code") }; + } + + inline grant_type to_grant_type(const utility::string_t& grant) + { + if (grant_types::authorization_code.name == grant) { return grant_types::authorization_code; } + if (grant_types::implicit.name == grant) { return grant_types::implicit; } + if (grant_types::password.name == grant) { return grant_types::password; } + if (grant_types::client_credentials.name == grant) { return grant_types::client_credentials; } + if (grant_types::refresh_token.name == grant) { return grant_types::refresh_token; } + if (grant_types::urn_ietf_params_oauth_grant_type_jwt_bearer.name == grant) { return grant_types::urn_ietf_params_oauth_grant_type_jwt_bearer; } + if (grant_types::urn_ietf_params_oauth_grant_type_saml2_bearer.name == grant) { return grant_types::urn_ietf_params_oauth_grant_type_saml2_bearer; } + if (grant_types::device_code.name == grant) { return grant_types::device_code; } + return{}; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/host_utils.cpp b/Development/cpprest/host_utils.cpp index 2d631a151..9c9b0b7e0 100644 --- a/Development/cpprest/host_utils.cpp +++ b/Development/cpprest/host_utils.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include "cpprest/asyncrt_utils.h" // for utility::conversions #if defined(_WIN32) @@ -328,6 +330,16 @@ namespace web } return addresses; // empty if host_name cannot be resolved } + + // get the associated network interface name from an IP address + utility::string_t get_interface_name(const utility::string_t& address, const std::vector& host_interfaces) + { + const auto interface = boost::range::find_if(host_interfaces, [&](const web::hosts::experimental::host_interface& interface) + { + return interface.addresses.end() != boost::range::find(interface.addresses, address); + }); + return host_interfaces.end() != interface ? interface->name : utility::string_t{}; + } } } } diff --git a/Development/cpprest/host_utils.h b/Development/cpprest/host_utils.h index f9dc6ad95..40ed32e2c 100644 --- a/Development/cpprest/host_utils.h +++ b/Development/cpprest/host_utils.h @@ -30,6 +30,9 @@ namespace web std::vector host_names(const utility::string_t& address); std::vector host_addresses(const utility::string_t& host_name); + + // get the associated network interface name from an IP address + utility::string_t get_interface_name(const utility::string_t& address, const std::vector& host_interfaces = web::hosts::experimental::host_interfaces()); } } } diff --git a/Development/cpprest/http_utils.cpp b/Development/cpprest/http_utils.cpp index cb5661171..1760b7232 100644 --- a/Development/cpprest/http_utils.cpp +++ b/Development/cpprest/http_utils.cpp @@ -315,18 +315,139 @@ namespace web } } - if (!name.empty()) + switch (state) + { + case pre_value: + break; + case value_name: + result.push_back({ name, {} }); name.clear(); + break; + case pre_param: + break; + case pre_param_name: + throw std::invalid_argument("invalid parameter name, expected tchar"); + case param_name: + throw std::invalid_argument("invalid parameter, expected '='"); + case pre_param_value: + throw std::invalid_argument("invalid parameter, expected '='"); + case param_value: + throw std::invalid_argument("invalid parameter value, expected tchar or '\"'"); + case param_value_token: + break; + case param_value_quoted_string: + throw std::invalid_argument("invalid parameter value, expected '\"'"); + case param_value_quoted_string_escape: + throw std::invalid_argument("invalid parameter value, expected escaped char'"); + default: + throw std::logic_error("unreachable code"); + } + + return result; + } + + utility::string_t make_directives_header(const directives& values) + { + utility::string_t result; + for (auto& value : values) + { + if (!result.empty()) { result.push_back(U(';')); } + result.append(value.first); + if (!value.second.empty()) + { + result.push_back(U('=')); + result.append(value.second); + } + } + return result; + } + + directives parse_directives_header(const utility::string_t& value) + { + enum { + pre_directive, + pre_directive_name, + directive_name, + pre_directive_value, + directive_value, + directive_value_token, + directive_value_quoted_string, + directive_value_quoted_string_escape + } state = pre_directive_name; + + directives result; + utility::string_t name; + for (auto c : value) { switch (state) { - case value_name: - result.push_back({ name, {} }); name.clear(); break; - case param_name: - throw std::invalid_argument("invalid parameter, expected '='"); + case pre_directive: + if (U(';') == c) { state = pre_directive_name; break; } + if (U(' ') == c || U('\t') == c) { break; } + throw std::invalid_argument("invalid value, expected ';'"); + case pre_directive_name: + if (details::is_tchar(c)) { name.push_back(c); state = directive_name; break; } + if (U(' ') == c || U('\t') == c) { break; } + throw std::invalid_argument("invalid directive name, expected tchar"); + case directive_name: + if (details::is_tchar(c)) { name.push_back(c); break; } + result.push_back({ name, {} }); name.clear(); + if (U('=') == c) { state = directive_value; break; } + if (U(';') == c) { state = pre_directive_name; break; } + if (U(' ') == c || U('\t') == c) { state = pre_directive_value; break; } + throw std::invalid_argument("invalid directive name, expected tchar"); + case pre_directive_value: + if (U('=') == c) { state = directive_value; break; } + if (U(' ') == c || U('\t') == c) { break; } + if (U(';') == c) { state = pre_directive_name; break; } + throw std::invalid_argument("invalid directive, expected '='"); + case directive_value: + if (details::is_tchar(c)) { result.back().second.push_back(c); state = directive_value_token; break; } + if (U('"') == c) { state = directive_value_quoted_string; break; } + if (U(' ') == c || U('\t') == c) { break; } + throw std::invalid_argument("invalid directive value, expected tchar or '\"'"); + case directive_value_token: + if (details::is_tchar(c)) { result.back().second.push_back(c); break; } + if (U(';') == c) { state = pre_directive_name; break; } + if (U(' ') == c || U('\t') == c) { state = pre_directive; break; } + throw std::invalid_argument("invalid directive value, expected tchar"); + case directive_value_quoted_string: + if (U('"') == c) { state = pre_directive; break; } + if (U('\\') == c) { state = directive_value_quoted_string_escape; break; } + result.back().second.push_back(c); + break; + case directive_value_quoted_string_escape: + result.back().second.push_back(c); + state = directive_value_quoted_string; + break; default: throw std::logic_error("unreachable code"); } } + + switch (state) + { + case pre_directive: + break; + case pre_directive_name: + break; + case directive_name: + result.push_back({ name, {} }); name.clear(); + break; + case pre_directive_value: + break; + case directive_value: + throw std::invalid_argument("invalid directive value, expected tchar or '\"'"); + case directive_value_token: + break; + case directive_value_quoted_string: + throw std::invalid_argument("invalid directive value, expected '\"'"); + case directive_value_quoted_string_escape: + throw std::invalid_argument("invalid directive value, expected escaped char'"); + break; + default: + throw std::logic_error("unreachable code"); + } + return result; } @@ -370,6 +491,43 @@ namespace web } return results; } + + utility::string_t make_hsts_header(const hsts& value) + { + directives result; + result.push_back({ U("max-age"), utility::ostringstreamed(value.max_age) }); + if (value.include_sub_domains) result.push_back({ U("includeSubDomains"), {} }); + return make_directives_header(result); + } + + // "1. The order of appearance of directives is not significant. + // 2. All directives MUST appear only once in an STS header field. + // Directives are either optional or required, as stipulated in + // their definitions. + // 3. Directive names are case-insensitive." + // See https://tools.ietf.org/html/rfc6797#section-6.1 + inline directives::const_iterator find_directive(const directives& directives, const directive::first_type& directive_name) + { + return std::find_if(directives.begin(), directives.end(), [&](const directive& directive) { return boost::algorithm::iequals(directive.first, directive_name); }); + } + + hsts parse_hsts_header(const utility::string_t& value) + { + hsts result; + auto directives = parse_directives_header(value); + + // required + const auto max_age = find_directive(directives, U("max-age")); + if (directives.end() == max_age) throw std::invalid_argument("invalid Strict-Transport-Security header, missing max-age"); + // hm, invalid value is treated as 0 + result.max_age = utility::istringstreamed(max_age->second, 0u); + + // optional + const auto include_sub_domains = find_directive(directives, U("includeSubDomains")); + if (directives.end() != include_sub_domains) result.include_sub_domains = true; + + return result; + } } namespace details diff --git a/Development/cpprest/http_utils.h b/Development/cpprest/http_utils.h index 2727ec96c..0ef75d7fc 100644 --- a/Development/cpprest/http_utils.h +++ b/Development/cpprest/http_utils.h @@ -133,6 +133,18 @@ namespace web utility::string_t make_ptokens_header(const ptokens& values); ptokens parse_ptokens_header(const utility::string_t& value); + // directives = [ directive ] *( ";" [ directive ] ) + // directive = directive-name [ "=" directive-value ] + // directive-name = token + // directive-value = token | quoted-string + // E.g. Strict-Transport-Security uses this format + // See https://tools.ietf.org/html/rfc6797#section-6.1 + + typedef std::pair directive; + typedef std::vector directives; + + utility::string_t make_directives_header(const directives& values); + directives parse_directives_header(const utility::string_t& value); namespace header_names { @@ -143,6 +155,10 @@ namespace web // Resource Timing 2 // See https://www.w3.org/TR/resource-timing-2/#sec-timing-allow-origin const web::http::http_headers::key_type timing_allow_origin{ _XPLATSTR("Timing-Allow-Origin") }; + + // Strict Transport Security + // See https://tools.ietf.org/html/rfc6797#section-6.1 + const web::http::http_headers::key_type strict_transport_security{ _XPLATSTR("Strict-Transport-Security") }; } struct timing_metric @@ -150,6 +166,7 @@ namespace web utility::string_t name; double duration; // milliseconds utility::string_t description; + timing_metric(utility::string_t name, double duration = 0.0, utility::string_t description = {}) : name(name), duration(duration), description(description) {} timing_metric(utility::string_t name, utility::string_t description) : name(name), duration(0.0), description(description) {} @@ -162,6 +179,25 @@ namespace web utility::string_t make_timing_header(const timing_metrics& values); timing_metrics parse_timing_header(const utility::string_t& value); + + struct hsts + { + uint32_t max_age; // seconds + bool include_sub_domains; + + // "If the max-age header field value token has a value of zero, the + // UA MUST remove its cached HSTS Policy information if the HSTS Host is + // known, or the UA MUST NOT note this HSTS Host if it is not yet known." + // See https://tools.ietf.org/html/rfc6797#section-8.1 + hsts(uint32_t max_age = 0u, bool include_sub_domains = false) : max_age(max_age), include_sub_domains(include_sub_domains) {} + + auto tied() const -> decltype(std::tie(max_age, include_sub_domains)) { return std::tie(max_age, include_sub_domains); } + friend bool operator==(const hsts& lhs, const hsts& rhs) { return lhs.tied() == rhs.tied(); } + friend bool operator!=(const hsts& lhs, const hsts& rhs) { return !(lhs == rhs); } + }; + + utility::string_t make_hsts_header(const hsts& value); + hsts parse_hsts_header(const utility::string_t& value); } // Determine whether http_request::reply() has been called already @@ -177,10 +213,10 @@ namespace web typedef pplx::open_close_guard http_listener_guard; // platform-specific wildcard address to accept connections for any address -#if defined(_WIN32) && !defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) - const utility::string_t host_wildcard{ _XPLATSTR("*") }; // "weak wildcard" -#else +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) const utility::string_t host_wildcard{ _XPLATSTR("0.0.0.0") }; +#else + const utility::string_t host_wildcard{ _XPLATSTR("*") }; // "weak wildcard" #endif // make an address to be used to accept HTTP or HTTPS connections for the specified address and port diff --git a/Development/cpprest/json_ops.h b/Development/cpprest/json_ops.h index 8b43cca48..a1021d519 100644 --- a/Development/cpprest/json_ops.h +++ b/Development/cpprest/json_ops.h @@ -51,12 +51,7 @@ namespace web return !(lhs == rhs); } - inline bool operator<(const web::json::object& lhs, const web::json::object& rhs) - { - using std::begin; - using std::end; - return std::lexicographical_compare(begin(lhs), end(lhs), begin(rhs), end(rhs)); - } + inline bool operator<(const web::json::object& lhs, const web::json::object& rhs); inline bool operator>(const web::json::object& lhs, const web::json::object& rhs) { @@ -87,12 +82,7 @@ namespace web return !(lhs == rhs); } - inline bool operator<(const web::json::array& lhs, const web::json::array& rhs) - { - using std::begin; - using std::end; - return std::lexicographical_compare(begin(lhs), end(lhs), begin(rhs), end(rhs)); - } + inline bool operator<(const web::json::array& lhs, const web::json::array& rhs); inline bool operator>(const web::json::array& lhs, const web::json::array& rhs) { @@ -142,6 +132,20 @@ namespace web { return !(lhs < rhs); } + + inline bool operator<(const web::json::array& lhs, const web::json::array& rhs) + { + using std::begin; + using std::end; + return std::lexicographical_compare(begin(lhs), end(lhs), begin(rhs), end(rhs)); + } + + inline bool operator<(const web::json::object& lhs, const web::json::object& rhs) + { + using std::begin; + using std::end; + return std::lexicographical_compare(begin(lhs), end(lhs), begin(rhs), end(rhs)); + } } } @@ -260,6 +264,7 @@ namespace web value.erase(value.size() - 1); } + // deprecated, since pop_front is only found on std containers with fast deletion at the beginning inline void pop_front(web::json::value& value) { value.erase(0); @@ -292,6 +297,66 @@ namespace web } } +// json::array accessors and operations +namespace web +{ + namespace json + { + template + inline void push_back(web::json::array& value, const Value& element) + { + value[value.size()] = web::json::value{ element }; + } + + inline void push_back(web::json::array& value, web::json::value&& element) + { + value[value.size()] = std::move(element); + } + + inline void pop_back(web::json::array& value) + { + value.erase(value.size() - 1); + } + + inline web::json::value& front(web::json::array& value) + { + return value.at(0); + } + + inline const web::json::value& front(const web::json::array& value) + { + return value.at(0); + } + + inline web::json::value& back(web::json::array& value) + { + return value.at(value.size() - 1); + } + + inline const web::json::value& back(const web::json::array& value) + { + return value.at(value.size() - 1); + } + + inline bool empty(const web::json::array& value) + { + return 0 == value.size(); + } + } +} + +// json::object accessors and operations +namespace web +{ + namespace json + { + inline bool empty(const web::json::object& value) + { + return value.empty(); + } + } +} + // json::value initialization // web::json::value has a rather limited interface for the construction of objects and arrays; // value_of allows construction of both objects and array from a braced-init-list @@ -314,6 +379,10 @@ namespace web value_init(bool b) : value(b) {} value_init(utility::string_t s) : value(std::move(s)) {} value_init(const utility::char_t* s) : value(s) {} + + private: + // this prevents the surprising implicit conversion from e.g. char* to bool + template value_init(T*) = delete; }; } @@ -343,10 +412,10 @@ namespace web return result; } - namespace detail { class ambiguous; } + namespace details { class ambiguous; } // this overload intentionally makes value_of({}) ambiguous // since it's unclear whether that's intended to be an empty object or an empty array - web::json::value value_of(std::initializer_list); + web::json::value value_of(std::initializer_list); } } diff --git a/Development/cpprest/json_utils.cpp b/Development/cpprest/json_utils.cpp index 6966b8c03..efd8d5408 100644 --- a/Development/cpprest/json_utils.cpp +++ b/Development/cpprest/json_utils.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "cpprest/base_uri.h" // for web::uri::decode #include "cpprest/regex_utils.h" @@ -18,10 +19,10 @@ namespace web { // regex pattern matches JSON strings, or single or multi-line comments // only strings are captured - static const utility::regex_t string_or_comment(U(R"-regex-(("[^"\\]*(?:\\.[^"\\]*)*")|(?:\/\/[^\r\n]+)|(?:\/\*[\s\S]*?\*\/))-regex-")); + static const utility::regex_t string_or_comment(_XPLATSTR(R"-regex-(("[^"\\]*(?:\\.[^"\\]*)*")|(?:\/\/[^\r\n]+)|(?:\/\*[\s\S]*?\*\/))-regex-")); // format pattern uses the first capture group to copy strings into the output // having inserted a single space to ensure tokens are not coalesced - return bst::regex_replace(value, string_or_comment, U(" $1")); + return bst::regex_replace(value, string_or_comment, _XPLATSTR(" $1")); } } } @@ -42,15 +43,24 @@ namespace web // insert a field into the specified object at the specified key path (splitting it on '.' and inserting sub-objects as necessary) // only if the value doesn't already contain a field matching that key path (except for the required sub-objects or null values) - bool insert(web::json::object& object, const utility::string_t& key_path, const web::json::value& field_value) + bool insert(web::json::object& object, const utility::string_t& key_path_, const web::json::value& field_value) + { + std::vector key_path; + boost::algorithm::split(key_path, key_path_, [](utility::char_t c) { return _XPLATSTR('.') == c; }); + + return insert(object, key_path, field_value); + } + + // insert a field into the specified object at the specified key path + // only if the value doesn't already contain a field matching that key path (except for the required sub-objects or null values) + bool insert(web::json::object& object, const std::vector& key_path, const web::json::value& field_value) { web::json::object* pobject = &object; - utility::string_t::size_type key_first = 0; - do + + size_t count = 0; + for (auto key : key_path) { - const utility::string_t::size_type key_last = key_path.find_first_of(_XPLATSTR("."), key_first); - const utility::string_t key = key_path.substr(key_first, details::count(key_first, key_last)); - if (utility::string_t::npos != key_last) + if (++count < key_path.size()) { auto& field = (*pobject)[key]; // do not replace (non-null) values for duplicate keys; other policies (replace, promote to array, or throw) would be possible... @@ -65,26 +75,36 @@ namespace web if (field.is_null()) field = field_value; else return false; } - key_first = utility::string_t::npos != key_last ? key_last + 1 : key_last; - } while (utility::string_t::npos != key_first); + } + return true; } // find the value of a field or fields from the specified object, splitting the key path on '.' and searching arrays as necessary // returns true if the value has at least one field matching the key path // if any arrays are encountered on the key path, results is an array, otherwise it's a non-array value - bool extract(const web::json::object& object, web::json::value& results, const utility::string_t& key_path) + bool extract(const web::json::object& object, web::json::value& results, const utility::string_t& key_path_) + { + std::vector key_path; + boost::algorithm::split(key_path, key_path_, [](utility::char_t c) { return _XPLATSTR('.') == c; }); + + return extract(object, results, key_path); + } + + // find the value of a field or fields from the specified object, searching arrays as necessary + // returns true if the value has at least one field matching the key path + // if any arrays are encountered on the key path, results is an array, otherwise it's a non-array value + bool extract(const web::json::object& object, web::json::value& results, const std::vector& key_path) { bool match = false; results = web::json::value::null(); std::list pobjects(1, &object); - utility::string_t::size_type key_first = 0; - do + + size_t count = 0; + for (auto key : key_path) { - const utility::string_t::size_type key_last = key_path.find_first_of(_XPLATSTR("."), key_first); - const utility::string_t key = key_path.substr(key_first, details::count(key_first, key_last)); - if (utility::string_t::npos != key_last) + if (++count < key_path.size()) { // not the leaf key, so map each object to the specified field, searching arrays and filtering out other types for (auto it = pobjects.begin(); pobjects.end() != it; it = pobjects.erase(it)) @@ -153,8 +173,7 @@ namespace web } } } - key_first = utility::string_t::npos != key_last ? key_last + 1 : key_last; - } while (utility::string_t::npos != key_first); + } return match; } diff --git a/Development/cpprest/json_utils.h b/Development/cpprest/json_utils.h index 84783cf2f..778435c36 100644 --- a/Development/cpprest/json_utils.h +++ b/Development/cpprest/json_utils.h @@ -246,11 +246,20 @@ namespace web // only if the object doesn't already contain a field matching that key path (except for the required sub-objects or null values) bool insert(web::json::object& object, const utility::string_t& key_path, const web::json::value& field_value); + // insert a field into the specified object at the specified key path (inserting sub-objects as necessary) + // only if the object doesn't already contain a field matching that key path (except for the required sub-objects or null values) + bool insert(web::json::object& object, const std::vector& key_path, const web::json::value& field_value); + // find the value of a field or fields from the specified object, splitting the key path on '.' and searching arrays as necessary // returns true if the object has at least one field matching the key path // if any arrays are encountered on the key path, results is an array, otherwise it's a non-array value bool extract(const web::json::object& object, web::json::value& results, const utility::string_t& key_path); + // find the value of a field or fields from the specified object, searching arrays as necessary + // returns true if the value has at least one field matching the key path + // if any arrays are encountered on the key path, results is an array, otherwise it's a non-array value + bool extract(const web::json::object& object, web::json::value& results, const std::vector& key_path); + // match_flag_type is a bitmask enum match_flag_type { diff --git a/Development/cpprest/json_visit.cpp b/Development/cpprest/json_visit.cpp index b23134171..3c64238da 100644 --- a/Development/cpprest/json_visit.cpp +++ b/Development/cpprest/json_visit.cpp @@ -1,9 +1,45 @@ #include "cpprest/json_visit.h" +#include // for nlohmann/detail/conversions/to_chars.hpp + namespace web { namespace json { + namespace details + { + template <> + size_t num_put::put(char* buffer, size_t capacity, double v, std::streamsize precision) + { + if (std::numeric_limits::max_digits10 > precision) + { + return std::snprintf(buffer, capacity, "%.*g", (int)precision, v); + } + else + { + // max_digits10 is usually used to guarantee roundtrip, value -> string -> value, + // but this has the downside that some numbers are printed with more digits than + // necessary, for example, 59.94 is output as 59.939999999999998 + // the Grisu2 algorithm implemented by nlohmann::detail::to_chars does better! + const auto last = nlohmann::detail::to_chars(buffer, buffer + capacity - 1, v); + *last = '\0'; + return last - buffer; + } + } + +#ifdef _WIN32 + template <> + size_t num_put::put(wchar_t* wbuffer, size_t capacity, double v, std::streamsize precision) + { + const auto cap = (std::min)(capacity, capacity_double); + std::vector buffer(cap); + const auto len = num_put::put(&buffer[0], cap, v, precision); + std::copy_n(&buffer[0], (std::min)(len + 1, cap), wbuffer); + return len; + } +#endif + } + namespace experimental { // sample stylesheet for the HTML generated by html_visitor diff --git a/Development/cpprest/json_visit.h b/Development/cpprest/json_visit.h index 7e2d354d8..1ec57af36 100644 --- a/Development/cpprest/json_visit.h +++ b/Development/cpprest/json_visit.h @@ -215,22 +215,19 @@ namespace web struct num_put { // maximum required buffer capacity for double, based on a '-', no loss, '.', "e-123", null terminator - static const size_t capacity_double = 1 + (std::numeric_limits::digits10 + 2) + 1 + 5 + 1; + static const size_t capacity_double = 1 + (std::numeric_limits::max_digits10) + 1 + 5 + 1; // maximum required buffer capacity for signed, based on a '-', no loss, null terminator static const size_t capacity_signed = 1 + (std::numeric_limits::digits10 + 1) + 1; // maximum required buffer capacity for unsigned, based on no loss, null terminator static const size_t capacity_unsigned = (std::numeric_limits::digits10 + 1) + 1; - static size_t put(CharType* buffer, size_t capacity, double v, std::streamsize precision = 0); + static size_t put(CharType* buffer, size_t capacity, double v, std::streamsize precision = std::numeric_limits::max_digits10); static size_t put(CharType* buffer, size_t capacity, int64_t v); static size_t put(CharType* buffer, size_t capacity, uint64_t v); }; template <> - inline size_t num_put::put(char* buffer, size_t capacity, double v, std::streamsize precision) - { - return std::snprintf(buffer, capacity, "%.*g", (int)precision, v); - } + size_t num_put::put(char* buffer, size_t capacity, double v, std::streamsize precision); template <> inline size_t num_put::put(char* buffer, size_t capacity, int64_t v) @@ -246,10 +243,7 @@ namespace web #ifdef _WIN32 template <> - inline size_t num_put::put(wchar_t* buffer, size_t capacity, double v, std::streamsize precision) - { - return std::swprintf(buffer, capacity, L"%.*g", (int)precision, v); - } + size_t num_put::put(wchar_t* buffer, size_t capacity, double v, std::streamsize precision); template <> inline size_t num_put::put(wchar_t* buffer, size_t capacity, int64_t v) @@ -277,7 +271,7 @@ namespace web basic_ostream_visitor(std::basic_ostream& os) : os(os) { // this is *almost* always what the user wants - os.precision(std::numeric_limits::digits10 + 2); + os.precision(std::numeric_limits::max_digits10); } // visit callbacks diff --git a/Development/cpprest/resource_server_error.h b/Development/cpprest/resource_server_error.h new file mode 100644 index 000000000..bf790ca25 --- /dev/null +++ b/Development/cpprest/resource_server_error.h @@ -0,0 +1,31 @@ +#ifndef CPPREST_RESOURCE_SERVER_ERROR_H +#define CPPREST_RESOURCE_SERVER_ERROR_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + namespace experimental + { + // "When a request fails, the resource server responds using the + // appropriate HTTP status code (typically, 400, 401, 403, or 405) and + // includes one of the following error codes in the response:" + // see https://tools.ietf.org/html/rfc6750#section-3.1 + DEFINE_STRING_ENUM(resource_server_error) + namespace resource_server_errors + { + const resource_server_error invalid_request{ U("invalid_request") }; + const resource_server_error invalid_token{ U("invalid_token") }; + const resource_server_error insufficient_scope{ U("insufficient_scope") }; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/response_type.h b/Development/cpprest/response_type.h new file mode 100644 index 000000000..8946f2029 --- /dev/null +++ b/Development/cpprest/response_type.h @@ -0,0 +1,28 @@ +#ifndef CPPREST_RESPONSE_TYPE_H +#define CPPREST_RESPONSE_TYPE_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + // see https://tools.ietf.org/html/rfc7591#section-2 + namespace experimental + { + DEFINE_STRING_ENUM(response_type) + namespace response_types + { + const response_type none{ U("none") }; + const response_type code{ U("code") }; + const response_type token{ U("token") }; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/test/api_router_test.cpp b/Development/cpprest/test/api_router_test.cpp index bb4e50cab..6bc6fb92a 100644 --- a/Development/cpprest/test/api_router_test.cpp +++ b/Development/cpprest/test/api_router_test.cpp @@ -7,10 +7,11 @@ //////////////////////////////////////////////////////////////////////////////////////////// BST_TEST_CASE(testMakeListenerUri) { -#if defined(_WIN32) && !defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) - BST_REQUIRE_STRING_EQUAL("http://*:42/", utility::us2s(web::http::experimental::listener::make_listener_uri(42).to_string())); -#else +// cf. preprocessor conditions for web::http::experimental::listener::host_wildcard +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) BST_REQUIRE_STRING_EQUAL("http://0.0.0.0:42/", utility::us2s(web::http::experimental::listener::make_listener_uri(42).to_string())); +#else + BST_REQUIRE_STRING_EQUAL("http://*:42/", utility::us2s(web::http::experimental::listener::make_listener_uri(42).to_string())); #endif BST_REQUIRE_STRING_EQUAL("http://203.0.113.42:42/", utility::us2s(web::http::experimental::listener::make_listener_uri(U("203.0.113.42"), 42).to_string())); @@ -19,10 +20,10 @@ BST_TEST_CASE(testMakeListenerUri) } //////////////////////////////////////////////////////////////////////////////////////////// -BST_TEST_CASE_PRIVATE(testGetRouteRelativePath) +BST_TEST_CASE(testGetRouteRelativePath) { using utility::us2s; - using web::http::experimental::listener::api_router; + using web::http::experimental::listener::details::get_route_relative_path; using web::http::http_exception; web::http::http_request req; @@ -30,47 +31,49 @@ BST_TEST_CASE_PRIVATE(testGetRouteRelativePath) // clear specification - BST_REQUIRE_STRING_EQUAL("/foo/bar/baz", us2s(api_router::get_route_relative_path(req, U("")))); - BST_REQUIRE_STRING_EQUAL("/bar/baz", us2s(api_router::get_route_relative_path(req, U("/foo")))); - BST_REQUIRE_STRING_EQUAL("", us2s(api_router::get_route_relative_path(req, U("/foo/bar/baz")))); - BST_REQUIRE_THROW(api_router::get_route_relative_path(req, U("/qux")), http_exception); + BST_REQUIRE_STRING_EQUAL("/foo/bar/baz", us2s(get_route_relative_path(req, U("")))); + BST_REQUIRE_STRING_EQUAL("/bar/baz", us2s(get_route_relative_path(req, U("/foo")))); + BST_REQUIRE_STRING_EQUAL("", us2s(get_route_relative_path(req, U("/foo/bar/baz")))); + BST_REQUIRE_THROW(get_route_relative_path(req, U("/qux")), http_exception); // less clear specification // compatible with http_request::relative_uri(), but should it be "foo/bar/baz"? - BST_CHECK_STRING_EQUAL("/foo/bar/baz", us2s(api_router::get_route_relative_path(req, U("/")))); + BST_CHECK_STRING_EQUAL("/foo/bar/baz", us2s(get_route_relative_path(req, U("/")))); // should it be "/bar/baz"? - BST_CHECK_STRING_EQUAL("bar/baz", us2s(api_router::get_route_relative_path(req, U("/foo/")))); + BST_CHECK_STRING_EQUAL("bar/baz", us2s(get_route_relative_path(req, U("/foo/")))); // should it throw, no match? - BST_CHECK_STRING_EQUAL("ar/baz", us2s(api_router::get_route_relative_path(req, U("/foo/b")))); + BST_CHECK_STRING_EQUAL("ar/baz", us2s(get_route_relative_path(req, U("/foo/b")))); // should it be ""? - BST_CHECK_THROW(api_router::get_route_relative_path(req, U("/foo/bar/baz/")), http_exception); + BST_CHECK_THROW(get_route_relative_path(req, U("/foo/bar/baz/")), http_exception); } //////////////////////////////////////////////////////////////////////////////////////////// -BST_TEST_CASE_PRIVATE(testRouteRegexMatch) +BST_TEST_CASE(testRouteRegexMatch) { - using web::http::experimental::listener::api_router; + using web::http::experimental::listener::details::match_entire; + using web::http::experimental::listener::details::match_prefix; + using web::http::experimental::listener::details::route_regex_match; utility::smatch_t route_match; - BST_REQUIRE(api_router::route_regex_match(U("/foo/bar/baz"), route_match, utility::regex_t(U("/f../b../b..")), api_router::match_entire)); - BST_REQUIRE(api_router::route_regex_match(U("/foo/bar/baz"), route_match, utility::regex_t(U("/f../b../b..")), api_router::match_prefix)); + BST_REQUIRE(route_regex_match(U("/foo/bar/baz"), route_match, utility::regex_t(U("/f../b../b..")), match_entire)); + BST_REQUIRE(route_regex_match(U("/foo/bar/baz"), route_match, utility::regex_t(U("/f../b../b..")), match_prefix)); - BST_REQUIRE(!api_router::route_regex_match(U("/foo/bar/baz/qux"), route_match, utility::regex_t(U("/f../b../b..")), api_router::match_entire)); - BST_REQUIRE(api_router::route_regex_match(U("/foo/bar/baz/qux"), route_match, utility::regex_t(U("/f../b../b..")), api_router::match_prefix)); + BST_REQUIRE(!route_regex_match(U("/foo/bar/baz/qux"), route_match, utility::regex_t(U("/f../b../b..")), match_entire)); + BST_REQUIRE(route_regex_match(U("/foo/bar/baz/qux"), route_match, utility::regex_t(U("/f../b../b..")), match_prefix)); - BST_REQUIRE(!api_router::route_regex_match(U("/foo/bar/qux"), route_match, utility::regex_t(U("/f../b../b..")), api_router::match_prefix)); - BST_REQUIRE(!api_router::route_regex_match(U("/qux/foo/bar/baz"), route_match, utility::regex_t(U("/f../b../b..")), api_router::match_prefix)); + BST_REQUIRE(!route_regex_match(U("/foo/bar/qux"), route_match, utility::regex_t(U("/f../b../b..")), match_prefix)); + BST_REQUIRE(!route_regex_match(U("/qux/foo/bar/baz"), route_match, utility::regex_t(U("/f../b../b..")), match_prefix)); } //////////////////////////////////////////////////////////////////////////////////////////// -BST_TEST_CASE_PRIVATE(testGetParameters) +BST_TEST_CASE(testGetParameters) { - using web::http::experimental::listener::api_router; + using web::http::experimental::listener::details::get_parameters; using web::http::experimental::listener::route_parameters; const utility::string_t path{ U("ABCD") }; @@ -86,5 +89,5 @@ BST_TEST_CASE_PRIVATE(testGetParameters) utility::smatch_t route_match; BST_REQUIRE(bst::regex_match(path, route_match, route_regex)); - BST_REQUIRE(expected == api_router::get_parameters(parameter_sub_matches, route_match)); + BST_REQUIRE(expected == get_parameters(parameter_sub_matches, route_match)); } diff --git a/Development/cpprest/test/basic_utils_test.cpp b/Development/cpprest/test/basic_utils_test.cpp new file mode 100644 index 000000000..785912e42 --- /dev/null +++ b/Development/cpprest/test/basic_utils_test.cpp @@ -0,0 +1,28 @@ +// The first "test" is of course whether the header compiles standalone +#include "cpprest/basic_utils.h" + +#include "bst/test/test.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testBase64Url) +{ + // See https://tools.ietf.org/html/rfc4648#section-10 + const std::pair tests[] = { + { "", "" }, + { "f", "Zg" }, + { "fo", "Zm8" }, + { "foo", "Zm9v" }, + { "foob", "Zm9vYg" }, + { "fooba", "Zm9vYmE" }, + { "foobar", "Zm9vYmFy" }, + { "???~~~", "Pz8_fn5-" } + }; + + for (const auto& test : tests) + { + const std::vector data(test.first.begin(), test.first.end()); + const utility::string_t str(test.second.begin(), test.second.end()); + BST_REQUIRE_STRING_EQUAL(str, utility::conversions::to_base64url(data)); + BST_REQUIRE_EQUAL(data, utility::conversions::from_base64url(str)); + } +} diff --git a/Development/cpprest/test/http_utils_test.cpp b/Development/cpprest/test/http_utils_test.cpp index 497e46542..a930a68ae 100644 --- a/Development/cpprest/test/http_utils_test.cpp +++ b/Development/cpprest/test/http_utils_test.cpp @@ -146,6 +146,25 @@ BST_TEST_CASE(testParsePtokensHeaderOWS) BST_REQUIRE_EQUAL(params.second, web::http::experimental::parse_ptokens_header(U(" foo; a= 1 ;b =2 ,bar, baz ;c= \"\" ; d = \"miaow, purr\",qux;e= \"\\\"\\\\\" "))); } +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testParsePtokensHeaderSyntaxErrors) +{ + const std::vector bad_params{ + U("foo;"), + U("foo;a"), + U("foo;a "), + U("foo;a="), + U("foo;a=\""), + U("foo;a=\"\\"), + U("foo;,") + }; + + for (const auto& bad : bad_params) + { + BST_REQUIRE_THROW(web::http::experimental::parse_ptokens_header(bad), std::invalid_argument); + } +} + //////////////////////////////////////////////////////////////////////////////////////////// BST_TEST_CASE(testMakeTimingHeaderParseTimingHeader) { @@ -172,3 +191,33 @@ BST_TEST_CASE(testParseTimingHeaderEdgeCases) BST_REQUIRE_EQUAL(42.0, web::http::experimental::parse_timing_header(U("foo;dur=42;desc=bar;dur=57")).front().duration); BST_REQUIRE(web::http::experimental::parse_timing_header(U("foo;desc=\"\";dur=42;desc=bar")).front().description.empty()); } + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testMakeHSTSHeaderParseHSTSHeader) +{ + // see https://tools.ietf.org/html/rfc6797#section-6.2 + BST_REQUIRE_EQUAL(web::http::experimental::make_hsts_header({}), U("max-age=0")); + BST_REQUIRE_EQUAL(web::http::experimental::make_hsts_header({ 31536000, true }), U("max-age=31536000;includeSubDomains")); + + std::vector> examples{ + { U("max-age=\"31536000\";includeSubDomains"), { 31536000, true } }, + { U("max-age = 31536000 ; includeSubDomains"), { 31536000, true } }, + { U("includeSubDomains;max-age=31536000"), { 31536000, true } }, + { U("includeSubDomains ; max-age = \"31536000\" "), { 31536000, true } }, + { U("max-age=31536000;foo;bar=baz;includeSubDomains"), { 31536000, true } } + }; + + for (const auto& example : examples) + { + BST_REQUIRE_EQUAL(example.second, web::http::experimental::parse_hsts_header(example.first)); + } + + // missing directive value + BST_REQUIRE_THROW(web::http::experimental::parse_hsts_header(U("max-age=")), std::invalid_argument); + // improperly terminated quoted string values + BST_REQUIRE_THROW(web::http::experimental::parse_hsts_header(U("max-age=\"31536000")), std::invalid_argument); + // missing max-age which is required + BST_REQUIRE_THROW(web::http::experimental::parse_hsts_header(U("includeSubDomains")), std::invalid_argument); + // hm, invalid max-age + //BST_REQUIRE_THROW(web::http::experimental::parse_hsts_header(U("max-age=meow")), std::invalid_argument); +} diff --git a/Development/cpprest/test/json_utils_test.cpp b/Development/cpprest/test/json_utils_test.cpp index c91380c36..fa8ececf5 100644 --- a/Development/cpprest/test/json_utils_test.cpp +++ b/Development/cpprest/test/json_utils_test.cpp @@ -5,6 +5,63 @@ #include "boost/range/adaptor/transformed.hpp" #include "bst/test/test.h" +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testJsonInsertExtract) +{ + using web::json::value; + using web::json::value_of; + + const auto foo_bar_baz = U("foo.bar.baz"); + const auto foo_bardotbaz = std::vector{ U("foo"), U("bar.baz") }; + const auto foodotbar_baz = std::vector{ U("foo.bar"), U("baz") }; + + value expected = value_of({ + { U("foo"), value_of({ + { U("bar"), value_of({ + { U("baz"), U("meow") } + }) }, + { U("bar.baz"), U("purr") } + }) } + }); + + auto actual = value::object(); + BST_REQUIRE(web::json::insert(actual.as_object(), foo_bar_baz, value(U("meow")))); + BST_REQUIRE(web::json::insert(actual.as_object(), foo_bardotbaz, value(U("purr")))); + + BST_REQUIRE(expected == actual); + + value meow; + BST_REQUIRE(web::json::extract(actual.as_object(), meow, foo_bar_baz)); + BST_REQUIRE_STRING_EQUAL(U("meow"), meow.as_string()); + value purr; + BST_REQUIRE(web::json::extract(actual.as_object(), purr, foo_bardotbaz)); + BST_REQUIRE_STRING_EQUAL(U("purr"), purr.as_string()); + value none(U("hiss")); + BST_REQUIRE(!web::json::extract(actual.as_object(), none, foodotbar_baz)); + BST_REQUIRE(none.is_null()); + + value actual2 = value_of({ + { U("foo.bar"), value_of({ + value_of({ + { U("baz"), U("meow") } + }), + value_of({ + { U("baz"), U("purr") } + }), + value_of({ + { U("baz"), U("hiss") } + }), + }) } + }); + value catcalls; + BST_REQUIRE(web::json::extract(actual2.as_object(), catcalls, foodotbar_baz)); + BST_REQUIRE(catcalls.is_array()); + BST_REQUIRE_STRING_EQUAL(U("meow"), catcalls.at(0).as_string()); + BST_REQUIRE_STRING_EQUAL(U("purr"), catcalls.at(1).as_string()); + BST_REQUIRE_STRING_EQUAL(U("hiss"), catcalls.at(2).as_string()); +} + +//////////////////////////////////////////////////////////////////////////////////////////// namespace { web::json::value merged_original(web::json::value target, const web::json::value& source) diff --git a/Development/cpprest/test/json_visit_test.cpp b/Development/cpprest/test/json_visit_test.cpp index 76bae93f7..dc0d4d2d5 100644 --- a/Development/cpprest/test/json_visit_test.cpp +++ b/Development/cpprest/test/json_visit_test.cpp @@ -102,24 +102,37 @@ BST_TEST_CASE(testOstreamVisitorNumbers) { { web::json::value two_thirds(2.0 / 3.0); - const auto expected = two_thirds.serialize(); + const auto unexpected = U("0.66666666666666663"); + // web::json::value::serialize does not currently use Grisu2 + BST_CHECK_EQUAL(unexpected, two_thirds.serialize()); + const auto expected = U("0.6666666666666666"); utility::ostringstream_t os; web::json::visit(web::json::ostream_visitor(os), two_thirds); - BST_REQUIRE_EQUAL(two_thirds.serialize(), os.str()); + BST_REQUIRE_EQUAL(expected, os.str()); } { web::json::value big_uint(UINT64_C(12345678901234567890)); const auto expected = big_uint.serialize(); utility::ostringstream_t os; web::json::visit(web::json::ostream_visitor(os), big_uint); - BST_REQUIRE_EQUAL(big_uint.serialize(), os.str()); + BST_REQUIRE_EQUAL(expected, os.str()); } { web::json::value big_int(INT64_C(-1234567890123456789)); const auto expected = big_int.serialize(); utility::ostringstream_t os; web::json::visit(web::json::ostream_visitor(os), big_int); - BST_REQUIRE_EQUAL(big_int.serialize(), os.str()); + BST_REQUIRE_EQUAL(expected, os.str()); + } + { + web::json::value awkward(59.94); + const auto unexpected = U("59.939999999999998"); + // web::json::value::serialize does not currently use Grisu2 + BST_CHECK_EQUAL(unexpected, awkward.serialize()); + const auto expected = U("59.94"); + utility::ostringstream_t os; + web::json::visit(web::json::ostream_visitor(os), awkward); + BST_REQUIRE_EQUAL(expected, os.str()); } } diff --git a/Development/cpprest/test/ws_listener_test.cpp b/Development/cpprest/test/ws_listener_test.cpp new file mode 100644 index 000000000..6d91f8047 --- /dev/null +++ b/Development/cpprest/test/ws_listener_test.cpp @@ -0,0 +1,29 @@ +// The first "test" is of course whether the header compiles standalone +#include "cpprest/ws_listener.h" + +#include "bst/test/test.h" + +BST_TEST_CASE(testWebSocketListenerCloseOpen) +{ + for (auto port = 49152; port <= 65535; ++port) + { + web::websockets::experimental::listener::websocket_listener ws(web::uri_builder(U("ws://localhost")).set_port(port).to_uri()); + try + { + ws.open().wait(); + } + catch (const web::websockets::websocket_exception&) + { + // could well be that port is already in use, so just try the next one + continue; + } + ws.close().wait(); + // it now ought to be possible to reopen a closed websocket_listener + // i.e. this open task ought not to result in an exception! + ws.open().wait(); + ws.close().wait(); + return; + } + // hmm, ran out of dynamic ports?! + BST_REQUIRE(false); +} diff --git a/Development/cpprest/token_endpoint_auth_method.h b/Development/cpprest/token_endpoint_auth_method.h new file mode 100644 index 000000000..06299aed3 --- /dev/null +++ b/Development/cpprest/token_endpoint_auth_method.h @@ -0,0 +1,43 @@ +#ifndef CPPREST_TOKEN_ENDPOINT_AUTH_METHOD_H +#define CPPREST_TOKEN_ENDPOINT_AUTH_METHOD_H + +#include "nmos/string_enum.h" + +namespace web +{ + namespace http + { + namespace oauth2 + { + // experimental extension, for BCP-003-02 Authorization + // see https://tools.ietf.org/html/rfc7591#section-2 + namespace experimental + { + DEFINE_STRING_ENUM(token_endpoint_auth_method) + namespace token_endpoint_auth_methods + { + const token_endpoint_auth_method none{ U("none") }; + const token_endpoint_auth_method client_secret_post{ U("client_secret_post") }; + const token_endpoint_auth_method client_secret_basic{ U("client_secret_basic") }; + // openid support + // see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication + const token_endpoint_auth_method private_key_jwt{ U("private_key_jwt") }; + const token_endpoint_auth_method client_secret_jwt{ U("client_secret_jwt") }; + } + + inline token_endpoint_auth_method to_token_endpoint_auth_method(const utility::string_t& token_endpoint_auth_method) + { + using namespace token_endpoint_auth_methods; + if (token_endpoint_auth_method == client_secret_basic.name) { return client_secret_basic; } + if (token_endpoint_auth_method == client_secret_post.name) { return client_secret_post; } + if (token_endpoint_auth_method == none.name) { return none; } + if (token_endpoint_auth_method == private_key_jwt.name) { return private_key_jwt; } + if (token_endpoint_auth_method == client_secret_jwt.name) { return client_secret_jwt; } + return {}; + } + } + } + } +} + +#endif diff --git a/Development/cpprest/uri_schemes.h b/Development/cpprest/uri_schemes.h index 8963e5543..76e63e02b 100644 --- a/Development/cpprest/uri_schemes.h +++ b/Development/cpprest/uri_schemes.h @@ -22,6 +22,11 @@ namespace web inline utility::string_t http_scheme(bool secure) { return secure ? uri_schemes::https : uri_schemes::http; } inline utility::string_t ws_scheme(bool secure) { return secure ? uri_schemes::wss : uri_schemes::ws; } + + inline bool is_secure_uri_scheme(const utility::string_t& scheme) + { + return uri_schemes::https == scheme || uri_schemes::wss == scheme; + } } #endif diff --git a/Development/cpprest/ws_listener_impl.cpp b/Development/cpprest/ws_listener_impl.cpp index 32f20565c..101a54524 100644 --- a/Development/cpprest/ws_listener_impl.cpp +++ b/Development/cpprest/ws_listener_impl.cpp @@ -289,6 +289,7 @@ namespace web public: explicit websocket_listener_wspp(web::uri address, websocket_listener_config config) : websocket_listener_impl(std::move(address), std::move(config)) + , init(false) { // since we cannot set the callback function before the server constructor we can't get log message from that server.get_alog().set_log_handler(configuration().get_log_callback()); @@ -308,7 +309,21 @@ namespace web try { - server.init_asio(); + // either initialise or restart the io_service + if (!init) + { + server.init_asio(); + server.set_reuse_addr(true); + init = true; + } + else + { +#if BOOST_VERSION >= 106600 + server.get_io_service().restart(); +#else + server.get_io_service().reset(); +#endif + } server.start_perpetual(); // hmm, is one thread enough? thread = std::thread(&server_t::run, &server); @@ -623,6 +638,7 @@ namespace web server_t server; connections_t connections; std::mutex mutex; + bool init; // flag to identify whether to initialise or restart the io_service }; std::unique_ptr make_websocket_listener_impl(web::uri&& address, websocket_listener_config&& config) diff --git a/Development/jwk/algorithm.h b/Development/jwk/algorithm.h new file mode 100644 index 000000000..6d1ea20ef --- /dev/null +++ b/Development/jwk/algorithm.h @@ -0,0 +1,18 @@ +#ifndef JWK_ALGORITHM_H +#define JWK_ALGORITHM_H + +#include "nmos/string_enum.h" + +namespace jwk +{ + DEFINE_STRING_ENUM(algorithm) + namespace algorithms + { + // RS256/RS384/RS512 + const algorithm RS256{ U("RS256") }; + const algorithm RS384{ U("RS384") }; + const algorithm RS512{ U("RS512") }; + } +} + +#endif diff --git a/Development/jwk/public_key_use.h b/Development/jwk/public_key_use.h new file mode 100644 index 000000000..ac9ca2c9c --- /dev/null +++ b/Development/jwk/public_key_use.h @@ -0,0 +1,16 @@ +#ifndef JWK_PUBLICKEY_USE_H +#define JWK_PUBLICKEY_USE_H + +#include "nmos/string_enum.h" + +namespace jwk +{ + DEFINE_STRING_ENUM(public_key_use) + namespace public_key_uses + { + const public_key_use signing{ U("sig") }; + const public_key_use encryption{ U("enc") }; + } +} + +#endif diff --git a/Development/mdns/service_advertiser_impl.cpp b/Development/mdns/service_advertiser_impl.cpp index aab8735a7..c15718355 100644 --- a/Development/mdns/service_advertiser_impl.cpp +++ b/Development/mdns/service_advertiser_impl.cpp @@ -69,7 +69,7 @@ namespace mdns_details } } - static bool register_address(DNSServiceRef client, const std::string& host_name, const std::string& ip_address_, const std::string& domain, std::uint32_t interface_id, slog::base_gate& gate) + static bool register_address(DNSServiceRef& client, const std::string& host_name, const std::string& ip_address_, const std::string& domain, std::uint32_t interface_id, slog::base_gate& gate) { // since empty host_name is valid for other functions, check that logic error here if (host_name.empty()) return false; diff --git a/Development/mdns/service_discovery.h b/Development/mdns/service_discovery.h index 088da9349..7d89f50d3 100644 --- a/Development/mdns/service_discovery.h +++ b/Development/mdns/service_discovery.h @@ -40,7 +40,7 @@ namespace mdns struct resolve_result { - resolve_result() {} + resolve_result() : port(0), interface_id(0) {} resolve_result(const std::string& host_name, std::uint16_t port, const mdns::txt_records& txt_records, std::uint32_t interface_id = 0) : host_name(host_name), port(port), txt_records(txt_records), interface_id(interface_id) {} std::string host_name; @@ -55,6 +55,20 @@ namespace mdns // the callback must not throw typedef std::function resolve_handler; + struct address_result + { + address_result() : ttl(0), interface_id(0) {} + address_result(const std::string& host_name, const std::string& ip_address, std::uint32_t ttl = 0, std::uint32_t interface_id = 0) : host_name(host_name), ip_address(ip_address), ttl(ttl), interface_id(interface_id) {} + + std::string host_name; + std::string ip_address; + std::uint32_t ttl; + std::uint32_t interface_id; + }; + + // return true from the address result callback if the operation should be ended before its specified timeout once no more results are "imminent" + typedef std::function address_handler; + class service_discovery { public: @@ -63,6 +77,7 @@ namespace mdns pplx::task browse(const browse_handler& handler, const std::string& type, const std::string& domain, std::uint32_t interface_id, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token = pplx::cancellation_token::none()); pplx::task resolve(const resolve_handler& handler, const std::string& name, const std::string& type, const std::string& domain, std::uint32_t interface_id, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + pplx::task getaddrinfo(const address_handler& handler, const std::string& host_name, std::uint32_t interface_id, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token = pplx::cancellation_token::none()); template pplx::task browse(const browse_handler& handler, const std::string& type, const std::string& domain = {}, std::uint32_t interface_id = 0, const std::chrono::duration& timeout = std::chrono::seconds(default_timeout_seconds), const pplx::cancellation_token& token = pplx::cancellation_token::none()) @@ -74,6 +89,11 @@ namespace mdns { return resolve(handler, name, type, domain, interface_id, std::chrono::duration_cast(timeout), token); } + template + pplx::task getaddrinfo(const address_handler& handler, const std::string& host_name, std::uint32_t interface_id = 0, const std::chrono::duration& timeout = std::chrono::seconds(default_timeout_seconds), const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + return getaddrinfo(handler, host_name, interface_id, std::chrono::duration_cast(timeout), token); + } template pplx::task> browse(const std::string& type, const std::string& domain = {}, std::uint32_t interface_id = 0, const std::chrono::duration& timeout = std::chrono::seconds(default_timeout_seconds), const pplx::cancellation_token& token = pplx::cancellation_token::none()) @@ -89,6 +109,13 @@ namespace mdns return resolve([results](const resolve_result& result) { results->push_back(result); return true; }, name, type, domain, interface_id, std::chrono::duration_cast(timeout), token) .then([results](bool) { return std::move(*results); }); } + template + pplx::task> getaddrinfo(const std::string& host_name, std::uint32_t interface_id = 0, const std::chrono::duration& timeout = std::chrono::seconds(default_timeout_seconds), const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + std::shared_ptr> results(new std::vector()); + return getaddrinfo([results](const address_result& result) {results->push_back(result); return true; }, host_name, interface_id, std::chrono::duration_cast(timeout), token) + .then([results](bool) { return std::move(*results); }); + } service_discovery(service_discovery&& other); service_discovery& operator=(service_discovery&& other); diff --git a/Development/mdns/service_discovery_impl.cpp b/Development/mdns/service_discovery_impl.cpp index c06b57d92..10a8bc28d 100644 --- a/Development/mdns/service_discovery_impl.cpp +++ b/Development/mdns/service_discovery_impl.cpp @@ -1,5 +1,12 @@ #include "mdns/service_discovery_impl.h" +#if defined(_WIN32) +#include +#include +#pragma comment(lib, "IPHLPAPI.lib") +#else +#include +#endif #include #include #include @@ -25,6 +32,24 @@ namespace mdns_details return kDNSServiceInterfaceIndexLocalOnly == interfaceIndex ? kDNSServiceInterfaceIndexAny : interfaceIndex; } + static inline std::string get_interface_name(std::uint32_t interfaceIndex) + { + char if_name[IF_NAMESIZE]; + + if (kDNSServiceInterfaceIndexAny == interfaceIndex) return "any interface"; + // hm, on Windows, if_indextoname returns a name that is neither the AdapterName nor the FriendlyName from GetAdaptersAddresses + if (0 != if_indextoname(interfaceIndex, if_name)) return if_name; + return "unknown interface"; + } + + struct if_name_manip + { + std::uint32_t i = kDNSServiceInterfaceIndexAny; + explicit if_name_manip(std::uint32_t i) : i(i) {} + friend std::ostream& operator<<(std::ostream& os, const if_name_manip& manip) { return os << manip.i << " (" << get_interface_name(manip.i) << ")"; } + }; + static inline if_name_manip put_interface_id(std::uint32_t interface_id) { return if_name_manip{ interface_id }; } + struct browse_context { // browse in-flight state @@ -52,7 +77,7 @@ namespace mdns_details { const browse_result result{ serviceName, regtype, replyDomain, make_interface_id(interfaceIndex) }; - slog::log(impl->gate, SLOG_FLF) << "After DNSServiceBrowse, DNSServiceBrowseReply got service: " << result.name << " for regtype: " << result.type << " domain: " << result.domain << " on interface: " << result.interface_id; + slog::log(impl->gate, SLOG_FLF) << "After DNSServiceBrowse, DNSServiceBrowseReply got service: " << result.name << " for regtype: " << result.type << " domain: " << result.domain << " on interface: " << put_interface_id(result.interface_id); impl->had_enough = impl->handler(result); } @@ -81,8 +106,7 @@ namespace mdns_details const auto latest_timeout = now + latest_timeout_; const auto earliest_timeout = now + earliest_timeout_; - // could use if_indextoname to get a name for the interface (remembering that 0 means "do the right thing", i.e. usually any interface, and there are some other special values too; see dns_sd.h) - slog::log(gate, SLOG_FLF) << "DNSServiceBrowse for regtype: " << type << " domain: " << domain << " on interface: " << interface_id; + slog::log(gate, SLOG_FLF) << "DNSServiceBrowse for regtype: " << type << " domain: " << domain << " on interface: " << put_interface_id(interface_id); browse_context context{ handler, had_enough, more_coming, gate }; DNSServiceErrorType errorCode = DNSServiceBrowse(&client, 0, interface_id, type.c_str(), !domain.empty() ? domain.c_str() : NULL, browse_reply, &context); @@ -200,19 +224,6 @@ namespace mdns_details } } - struct address_result - { - address_result(const std::string& host_name, const std::string& ip_address, std::uint32_t ttl = 0, std::uint32_t interface_id = 0) : host_name(host_name), ip_address(ip_address), ttl(ttl), interface_id(interface_id) {} - - std::string host_name; - std::string ip_address; - std::uint32_t ttl; - std::uint32_t interface_id; - }; - - // return true from the address result callback if the operation should be ended before its specified timeout once no more results are "imminent" - typedef std::function address_handler; - #ifdef HAVE_DNSSERVICEGETADDRINFO struct getaddrinfo_context { @@ -287,8 +298,7 @@ namespace mdns_details static bool resolve(const resolve_handler& handler, const std::string& name, const std::string& type, const std::string& domain, std::uint32_t interface_id, const std::chrono::steady_clock::duration& latest_timeout_, DNSServiceCancellationToken cancel, slog::base_gate& gate) { - // apply a minimum timeout when the interface id isn't known e.g. from the result of a browse - const auto earliest_timeout_ = std::chrono::seconds(0 == interface_id ? 1 : 0); + const auto earliest_timeout_ = std::chrono::seconds(0); bool had_enough = false; bool more_coming = true; @@ -299,8 +309,7 @@ namespace mdns_details const auto latest_timeout = now + latest_timeout_; const auto earliest_timeout = now + earliest_timeout_; - // could use if_indextoname to get a name for the interface (remembering that 0 means "do the right thing", i.e. usually any interface, and there are some other special values too; see dns_sd.h) - slog::log(gate, SLOG_FLF) << "DNSServiceResolve for name: " << name << " regtype: " << type << " domain: " << domain << " on interface: " << interface_id; + slog::log(gate, SLOG_FLF) << "DNSServiceResolve for name: " << name << " regtype: " << type << " domain: " << domain << " on interface: " << put_interface_id(interface_id); resolve_context context{ handler, had_enough, more_coming, gate }; DNSServiceErrorType errorCode = DNSServiceResolve(&client, 0, interface_id, name.c_str(), type.c_str(), domain.c_str(), (DNSServiceResolveReply)resolve_reply, &context); @@ -347,8 +356,7 @@ namespace mdns_details static bool getaddrinfo(const address_handler& handler, const std::string& host_name, std::uint32_t interface_id, const std::chrono::steady_clock::duration& latest_timeout_, DNSServiceCancellationToken cancel, slog::base_gate& gate) { - // apply a minimum timeout when the interface id isn't known e.g. from the result of a browse - const auto earliest_timeout_ = std::chrono::seconds(0 == interface_id ? 1 : 0); + const auto earliest_timeout_ = std::chrono::seconds(0); bool had_enough = false; #ifdef HAVE_DNSSERVICEGETADDRINFO @@ -367,12 +375,12 @@ namespace mdns_details if (protocol == kDNSServiceProtocol_IPv4 && interface_id >= kIPv6IfIndexBase) { // no point trying in this case! - slog::log(gate, SLOG_FLF) << "DNSServiceGetAddrInfo not tried for hostname: " << host_name << " on interface: " << interface_id; + slog::log(gate, SLOG_FLF) << "DNSServiceGetAddrInfo not tried for hostname: " << host_name << " on interface: " << put_interface_id(interface_id); } else #endif { - slog::log(gate, SLOG_FLF) << "DNSServiceGetAddrInfo for hostname: " << host_name << " on interface: " << interface_id; + slog::log(gate, SLOG_FLF) << "DNSServiceGetAddrInfo for hostname: " << host_name << " on interface: " << put_interface_id(interface_id); getaddrinfo_context context{ handler, had_enough, more_coming, gate }; DNSServiceErrorType errorCode = DNSServiceGetAddrInfo(&client, 0, interface_id, protocol, host_name.c_str(), getaddrinfo_reply, &context); @@ -522,6 +530,20 @@ namespace mdns }, token); } + pplx::task getaddrinfo(const address_handler& handler, const std::string& host_name, std::uint32_t interface_id, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) override + { + auto gate_ = &this->gate; + return pplx::create_task([=] + { + cancellation_guard guard(token); + auto result = mdns_details::getaddrinfo(handler, host_name, interface_id, timeout, guard.target, *gate_); + // when this task is cancelled, make sure it doesn't just return an empty/partial result + if (token.is_canceled()) pplx::cancel_current_task(); + // hmm, perhaps should throw an exception on timeout, rather than returning an empty result? + return result; + }, token); + } + private: slog::base_gate& gate; }; @@ -564,4 +586,9 @@ namespace mdns { return impl->resolve(handler, name, type, domain, interface_id, timeout, token); } + + pplx::task service_discovery::getaddrinfo(const address_handler& handler, const std::string& host_name, std::uint32_t interface_id, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) + { + return impl->getaddrinfo(handler, host_name, interface_id, timeout, token); + } } diff --git a/Development/mdns/service_discovery_impl.h b/Development/mdns/service_discovery_impl.h index a6050bf66..e1406795c 100644 --- a/Development/mdns/service_discovery_impl.h +++ b/Development/mdns/service_discovery_impl.h @@ -14,6 +14,7 @@ namespace mdns virtual pplx::task browse(const browse_handler& handler, const std::string& type, const std::string& domain, std::uint32_t interface_id, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) = 0; virtual pplx::task resolve(const resolve_handler& handler, const std::string& name, const std::string& type, const std::string& domain, std::uint32_t interface_id, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) = 0; + virtual pplx::task getaddrinfo(const address_handler& handler, const std::string& host_name, std::uint32_t interface_id, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) = 0; }; } } diff --git a/Development/mdns/test/mdns_test.cpp b/Development/mdns/test/mdns_test.cpp index afda730e5..473a07d3a 100644 --- a/Development/mdns/test/mdns_test.cpp +++ b/Development/mdns/test/mdns_test.cpp @@ -392,6 +392,10 @@ namespace { return pplx::task_from_result(false); } + pplx::task getaddrinfo(const mdns::address_handler& handler, const std::string& host_name, std::uint32_t interface_id, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) override + { + return pplx::task_from_result(false); + } slog::base_gate& gate; }; @@ -415,3 +419,14 @@ BST_TEST_CASE(testMdnsImpl) advertiser.close().wait(); BST_REQUIRE(gate.hasLogMessage("Close")); } + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testDnsGetAddrInfo) +{ + test_gate gate; + + mdns::service_discovery discover(gate); + auto results = discover.getaddrinfo("google-public-dns-a.google.com").get(); + BST_REQUIRE(!results.empty()); + BST_REQUIRE_EQUAL("8.8.8.8", results[0].ip_address); +} diff --git a/Development/nmos-cpp-node/config.json b/Development/nmos-cpp-node/config.json index e504ab645..f7e5216e6 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -4,14 +4,56 @@ { // Custom settings for the example node implementation + // node_tags, device_tags: used in resource tags fields + // "Each tag has a single key, but MAY have multiple values." + // See https://specs.amwa.tv/is-04/releases/v1.3.2/docs/APIs_-_Common_Keys.html#tags + // { + // "tag_1": [ "tag_1_value_1", "tag_1_value_2" ], + // "tag_2": [ "tag_2_value_1" ] + // } + //"node_tags": {}, + //"device_tags": {}, + // how_many: provides for very basic testing of a node with many sub-resources of each type - //"how_many" : 7, + //"how_many": 4, + + // activate_senders: controls whether to activate senders on start up (true, default) or not (false) + //"activate_senders": false, + + // senders, receivers: controls which kinds of sender and receiver are instantiated by the example node + // the values must be an array of unique strings identifying the kinds of 'port', like ["v", "a", "d"], see impl::ports + // when omitted, all ports are instantiated + //"senders": ["v", "a"], + //"receivers": [], // frame_rate: controls the grain_rate of video, audio and ancillary data sources and flows // and the equivalent parameter constraint on video receivers // the value must be an object like { "numerator": 25, "denominator": 1 } //"frame_rate": { "numerator": 60000, "denominator": 1001 }, + // frame_width, frame_height: control the frame_width and frame_height of video flows + //"frame_width": 3840, + //"frame_height": 2160, + + // interlace_mode: controls the interlace_mode of video flows, see nmos::interlace_mode + // when omitted, a default of "progressive" or "interlaced_tff" is used based on the frame_rate, etc. + //"interlace_mode": "progressive", + + // colorspace: controls the colorspace of video flows, see nmos::colorspace + //"colorspace": "BT709", + + // transfer_characteristic: controls the transfer characteristic system of video flows, see nmos::transfer_characteristic + //"transfer_characteristic": "SDR", + + // color_sampling: controls the color (sub-)sampling mode of video flows, see sdp::sampling + //"color_sampling": "YCbCr-4:2:2", + + // component_depth: controls the bits per component sample of video flows + //"component_depth": 10, + + // video_type: media type of video flows, e.g. "video/raw" or "video/jxsv", see nmos::media_types + //"video_type": "video/jxsv", + // channel_count: controls the number of channels in audio sources //"channel_count": 8, @@ -45,10 +87,10 @@ //"host_addresses": array-of-ip-address-strings, // is04_versions [registry, node]: used to specify the enabled API versions (advertised via 'api_ver') for a version-locked configuration - //"is04_versions": ["v1.1", "v1.2"], + //"is04_versions": ["v1.2", "v1.3"], // is05_versions [node]: used to specify the enabled API versions for a version-locked configuration - //"is05_versions": ["v1.0"], + //"is05_versions": ["v1.0", "v1.1"], // is07_versions [node]: used to specify the enabled API versions for a version-locked configuration //"is07_versions": ["v1.0"], @@ -59,18 +101,32 @@ // is09_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration //"is09_versions": ["v1.0"], + // is10_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration + //"is10_versions": ["v1.0"], + + // is12_versions [node]: used to specify the enabled API versions for a version-locked configuration + //"is12_versions": ["v1.0"], + // pri [registry, node]: used for the 'pri' TXT record; specifying nmos::service_priorities::no_priority (maximum value) disables advertisement completely //"pri": 100, - // highest_pri, lowest_pri [node]: used to specify the (inclusive) range of suitable 'pri' values of discovered APIs, to avoid development and live systems colliding + // highest_pri, lowest_pri [node]: used to specify the (inclusive) range of suitable 'pri' values of discovered Registration and System APIs, to avoid development and live systems colliding //"highest_pri": 0, //"lowest_pri": 2147483647, - // discovery_backoff_min/discovery_backoff_max/discovery_backoff_factor [node]: used to back-off after errors interacting with all discoverable Registration APIs or System APIs + // authorization_highest_pri, authorization_lowest_pri [registry, node]: used to specify the (inclusive) range of suitable 'pri' values of discovered Authorization APIs, to avoid development and live systems colliding + //"authorization_highest_pri": 0, + //"authorization_lowest_pri": 2147483647, + + // discovery_backoff_min/discovery_backoff_max/discovery_backoff_factor [registry, node]: used to back-off after errors interacting with all discoverable service instances + // e.g. Registration APIs, System APIs, Authorization APIs or OCSP servers //"discovery_backoff_min": 1, //"discovery_backoff_max": 30, //"discovery_backoff_factor": 1.5, + // service_name_prefix [registry, node]: used as a prefix in the advertised service names ("__:", e.g. "nmos-cpp_node_127-0-0-1:3212") + //"service_name_prefix": "nmos-cpp" + // registry_address [node]: IP address or host name used to construct request URLs for registry APIs (if not discovered via DNS-SD) //"registry_address": ip-address-string, @@ -79,7 +135,7 @@ // port numbers [registry, node]: ports to which clients should connect for each API - // http_port [registry, node]: if specified, used in preference to the individual defaults for each HTTP API + // http_port [registry, node]: if specified, this becomes the default port for each HTTP API and the next higher port becomes the default for each WebSocket API //"http_port": 0, // registration_port [node]: used to construct request URLs for the registry's Registration API (if not discovered via DNS-SD) @@ -91,13 +147,15 @@ //"channelmapping_port": 3215, // system_port [node]: used to construct request URLs for the System API (if not discovered via DNS-SD) //"system_port": 10641, + // control_protocol_ws_port [node]: used to construct request URLs for the Control Protocol websocket, or negative to disable the control protocol features + //"control_protocol_ws_port": 3218, // listen_backlog [registry, node]: the maximum length of the queue of pending connections, or zero for the implementation default (the implementation may not honour this value) //"listen_backlog": 0, - // registration_heartbeat_interval [node]: + // registration_heartbeat_interval [registry, node]: // "Nodes are expected to peform a heartbeat every 5 seconds by default." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md#heartbeating + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#heartbeating //"registration_heartbeat_interval": 5, // registration_request_max [node]: timeout for interactions with the Registration API /resource endpoint @@ -113,13 +171,13 @@ // events_heartbeat_interval [node, client]: // "Upon connection, the client is required to report its health every 5 seconds in order to maintain its session and subscription." - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md#41-heartbeats + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#41-heartbeats //"events_heartbeat_interval": 5, // events_expiry_interval [node]: // "The server is expected to check health commands and after a 12 seconds timeout (2 consecutive missed health commands plus 2 seconds to allow for latencies) // it should clear the subscriptions for that particular client and close the websocket connection." - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md#41-heartbeats + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#41-heartbeats //"events_expiry_interval": 12, // system_address [node]: IP address or host name used to construct request URLs for the System API (if not discovered via DNS-SD) @@ -136,20 +194,32 @@ // seed id [registry, node]: optional, used to generate repeatable id values when running with the same configuration //"seed_id": uuid-string, - // label [registry, node]: used in resource description/label fields + // label [registry, node]: used in resource label field //"label": "", + // description [registry, node]: used in resource description field + //"description": "", + // port numbers [registry, node]: ports to which clients should connect for each API // see http_port //"settings_port": 3209, //"logging_port": 5106, - // addresses [registry, node]: addresses on which to listen for each API, or empty string for the wildcard address + // addresses [registry, node]: IP addresses on which to listen for each API, or empty string for the wildcard address + + // server_address [registry, node]: if specified, this becomes the default address on which to listen for each API instead of the wildcard address + //"server_address": "", + + // addresses [registry, node]: IP addresses on which to listen for specific APIs //"settings_address": "127.0.0.1", //"logging_address": "", + // client_address [registry, node]: IP address of the network interface to bind client connections + // for now, only supporting HTTP/HTTPS client connections on Linux + //"client_address": "", + // logging_limit [registry, node]: maximum number of log events cached for the Logging API //"logging_limit": 1234, @@ -187,14 +257,15 @@ // when true, server certificates etc. must also be configured //"server_secure": false, - // private_key_files [registry, node]: full paths of private key files in PEM format - //"private_key_files": ["server-ecdsa-key.pem", "server-rsa-key.pem"], - - // certificate_chain_files [registry, node]: full paths of server certificate chain files which must be in PEM format and must be sorted + // server_certificates [registry, node]: an array of server certificate objects, each has the name of the key algorithm, the full paths of private key file and certificate chain file + // each value must be an object like { "key_algorithm": "ECDSA", "private_key_file": "server-ecdsa-key.pem", "certificate_chain_file": "server-ecdsa-chain.pem" } + // key_algorithm (attribute of server_certificates objects): name of the key algorithm for the certificate, see nmos::key_algorithm + // private_key_file (attribute of server_certificates objects): full path of private key file in PEM format + // certificate_chain_file (attribute of server_certificates object): full path of certificate chain file in PEM format, which must be sorted // starting with the server's certificate, followed by any intermediate CA certificates, and ending with the highest level (root) CA // on Windows, if C++ REST SDK is built with CPPREST_HTTP_LISTENER_IMPL=httpsys (reported as "listener=httpsys" by nmos::get_build_settings_info) // one of the certificates must also be bound to each port e.g. using 'netsh add sslcert' - //"certificate_chain_files": ["server-ecdsa-chain.pem", "server-rsa-chain.pem"], + //"server_certificates": [{"key_algorithm": "ECDSA", "private_key_file": "server-ecdsa-key.pem", "certificate_chain_file": "server-ecdsa-chain.pem"}, {"key_algorithm": "RSA", "private_key_file": "server-rsa-key.pem", "certificate_chain_file": "server-rsa-chain.pem"}], // validate_certificates [registry, node]: boolean value, false (ignore all server certificate validation errors), or true (do not ignore, the default behaviour) //"validate_certificates": true, @@ -206,5 +277,111 @@ //"system_interval_min": 3600, //"system_interval_max": 3660, + // hsts_max_age [registry, node]: the HTTP Strict-Transport-Security response header's max-age value; default is approximately 365 days + // (the header is omitted if server_secure is false, or hsts_max_age is negative) + // See https://tools.ietf.org/html/rfc6797#section-6.1.1 + //"hsts_max_age": 31536000, + + // hsts_include_sub_domains [registry, node]: the HTTP Strict-Transport-Security HTTP response header's includeSubDomains value + // See https://tools.ietf.org/html/rfc6797#section-6.1.2 + //"hsts_include_sub_domains": false, + + // ocsp_interval_min/ocsp_interval_max [registry, node]: used to poll for certificate status (OCSP) changes; default is about one hour + // Note that if half of the server certificate expiry time is shorter, then the ocsp_interval_min/max will be overridden by it + //"ocsp_interval_min": 3600, + //"ocsp_interval_max": 3660, + + // ocsp_request_max [registry, node]: timeout for interactions with the OCSP server + //"ocsp_request_max": 30, + + // authorization_address [registry, node]: IP address or host name used to construct request URLs for the Authorization API (if not discovered via DNS-SD) + //"authorization_address": ip-address-string, + + // authorization_port [registry, node]: used to construct request URLs for the authorization server's Authorization API (if not discovered via DNS-SD) + //"authorization_port" 443, + + // authorization_version [registry, node]: used to construct request URLs for Authorization API (if not discovered via DNS-SD) + //"authorization_version": "v1.0", + + // authorization_selector [registry, node]: used to construct request URLs for the authorization API (if not discovered via DNS-SD) + //"authorization_selector", "", + + // authorization_request_max [registry, node]: timeout for interactions with the Authorization API /certs & /token endpoints + //"authorization_request_max": 30, + + // fetch_authorization_public_keys_interval_min/fetch_authorization_public_keys_interval_max [registry, node]: used to poll for Authorization API public keys changes; default is about one hour + // "Resource Servers (Nodes) SHOULD seek to fetch public keys from the Authorization Server at least once every hour. Resource Servers MUST vary their retrieval + // interval at random by up to at least one minute to avoid overloading the Authorization Server due to Resource Servers synchronising their retrieval time." + // See https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.1._Behaviour_-_Authorization_Servers.html#authorization-server-public-keys + //"fetch_authorization_public_keys_interval_min": 3600, + //"fetch_authorization_public_keys_interval_max": 3660, + + // access_token_refresh_interval [node]: time interval (in seconds) to refresh access token from Authorization Server + // It specified the access token refresh period otherwise Bearer token's expires_in is used instead. + // See https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#access-token-lifetime + //"access_token_refresh_interval": -1, + + // client_authorization [node]: whether clients should use authorization to access protected APIs + //"client_authorization": false, + + // server_authorization [registry, node]: whether server should use authorization to protect its APIs + //"server_authorization": false, + + // authorization_code_flow_max [node]: timeout for the authorization code flow (in seconds) + // No timeout if value is set to -1, default to 30 seconds + //"authorization_code_flow_max": 30, + + // authorization_flow [node]: used to specify the authorization flow for the registered scopes + // supported flow are authorization_code and client_credentials + // client_credentials SHOULD only be used for NO user interface node, otherwise authorization_code MUST be used + //"authorization_flow": "authorization_code", + + // authorization_redirect_port [node]: redirect URL port for listening authorization code, used for client registration + //"authorization_redirect_port": 3218, + + // initial_access_token [node]: initial access token giving access to the client registration endpoint for non-opened registration + //"initial_access_token", "", + + // authorization_scopes [node]: used to specify the supported scopes for client registration + // supported scopes are registration, query, node, connection, events and channelmapping + //"authorization_scopes": [ "registration" ], + + // token_endpoint_auth_method [node]: String indicator of the requested authentication method for the token endpoint + // supported methods are none, client_secret_basic and private_key_jwt, default to client_secret_basic, where none is used for public client + // when using private_key_jwt, the JWT is created and signed by the node's private key + //"token_endpoint_auth_method": "client_secret_basic", + + // jwks_uri_port [node]: JWKs URL port for providing JSON Web Key Set (public keys) to Authorization Server for verifing client_assertion, used for client registration + //"jwks_uri_port": 3218, + + // validate_openid_client [node]: boolean value, false (bypass openid connect client validation), or true (do not bypass, the default behaviour) + //"validate_openid_client": true, + + // no_trailing_dot_for_authorization_callback_uri [node]: used to specify whether no trailing dot FQDN should be used to construct the URL for the authorization server callbacks + // as it is because not all Authorization server can cope with URL with trailing dot, default to true + //"no_trailing_dot_for_authorization_callback_uri": true, + + // retry_after [registry, node]: used to specify the HTTP Retry-After header to indicate the number of seconds when the client may retry its request again, default to 5 seconds + // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss + // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it + // MAY temporarily respond with an HTTP 503 code in order to avoid blocking the incoming authorized request. When a HTTP 503 code is used, the Resource + // Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request. + // If the Resource Server fails to verify a token using all public keys available it MUST reject the token." + //"service_unavailable_retry_after": 5, + + // manufacturer_name [node]: the manufacturer name of the NcDeviceManager used for NMOS Control Protocol + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicemanager + //"manufacturer_name": "", + + // product_name/product_key/product_revision_level [node]: the product description of the NcDeviceManager used for NMOS Control Protocol + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncproduct + //"product_name": "", + //"product_key": "", + //"product_revision_level": "", + + // serial_number [node]: the serial number of the NcDeviceManager used for NMOS Control Protocol + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicemanager + //"serial_number": "", + "don't worry": "about trailing commas" } diff --git a/Development/nmos-cpp-node/main.cpp b/Development/nmos-cpp-node/main.cpp index 9b9298948..e4b420fa2 100644 --- a/Development/nmos-cpp-node/main.cpp +++ b/Development/nmos-cpp-node/main.cpp @@ -1,10 +1,22 @@ #include #include +#include "cpprest/grant_type.h" +#include "cpprest/token_endpoint_auth_method.h" +#include "nmos/api_utils.h" // for make_api_listener +#include "nmos/authorization_behaviour.h" +#include "nmos/authorization_redirect_api.h" +#include "nmos/authorization_state.h" +#include "nmos/control_protocol_state.h" +#include "nmos/jwks_uri_api.h" #include "nmos/log_gate.h" #include "nmos/model.h" #include "nmos/node_server.h" +#include "nmos/ocsp_behaviour.h" +#include "nmos/ocsp_response_handler.h" +#include "nmos/ocsp_state.h" #include "nmos/process_utils.h" #include "nmos/server.h" +#include "nmos/server_utils.h" // for make_http_listener_config #include "node_implementation.h" int main(int argc, char* argv[]) @@ -47,12 +59,10 @@ int main(int argc, char* argv[]) if (error) { std::ifstream file(argv[1]); - node_model.settings = web::json::value::parse(file, error); - } - if (error || !node_model.settings.is_object()) - { - slog::log(gate, SLOG_FLF) << "Bad command-line settings [" << error << "]"; - return -1; + // check the file can be opened, and is parsed to an object + file.exceptions(std::ios_base::failbit); + node_model.settings = web::json::value::parse(file); + node_model.settings.as_object(); } } @@ -95,10 +105,118 @@ int main(int argc, char* argv[]) auto node_implementation = make_node_implementation(node_model, gate); +// only implement communication with OCSP server if http_listener supports OCSP stapling +// cf. preprocessor conditions in nmos::make_http_listener_config +// Note: the get_ocsp_response callback must be set up before executing the make_node_server where make_http_listener_config is set up +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) + nmos::experimental::ocsp_state ocsp_state; + if (nmos::experimental::fields::server_secure(node_model.settings)) + { + node_implementation.on_get_ocsp_response(nmos::make_ocsp_response_handler(ocsp_state, gate)); + } +#endif + + // only configure communication with Authorization server if IS-10/BCP-003-02 is required + // Note: + // the validate_authorization callback must be set up before executing the make_node_server where make_node_api, make_connection_api, make_events_api, and make_channelmapping_api are set up + // the ws_validate_authorization callback must be set up before executing the make_node_server where make_events_ws_validate_handler is set up + // the get_authorization_bearer_token callback must be set up before executing the make_node_server where make_http_client_config is set up + nmos::experimental::authorization_state authorization_state; + if (nmos::experimental::fields::server_authorization(node_model.settings)) + { + node_implementation + .on_validate_authorization(nmos::experimental::make_validate_authorization_handler(node_model, authorization_state, nmos::experimental::make_validate_authorization_token_handler(authorization_state, gate), gate)) + .on_ws_validate_authorization(nmos::experimental::make_ws_validate_authorization_handler(node_model, authorization_state, nmos::experimental::make_validate_authorization_token_handler(authorization_state, gate), gate)); + } + if (nmos::experimental::fields::client_authorization(node_model.settings)) + { + node_implementation + .on_get_authorization_bearer_token(nmos::experimental::make_get_authorization_bearer_token_handler(authorization_state, gate)) + .on_load_authorization_clients(nmos::experimental::make_load_authorization_clients_handler(node_model.settings, gate)) + .on_save_authorization_client(nmos::experimental::make_save_authorization_client_handler(node_model.settings, gate)) + .on_load_rsa_private_keys(nmos::make_load_rsa_private_keys_handler(node_model.settings, gate)) // may be omitted, only required for OAuth client which is using Private Key JWT as the requested authentication method for the token endpoint + .on_request_authorization_code(nmos::experimental::make_request_authorization_code_handler(gate)); // may be omitted, only required for OAuth client which is using the Authorization Code Flow to obtain the access token + } + + nmos::experimental::control_protocol_state control_protocol_state(node_implementation.control_protocol_property_changed); + if (0 <= nmos::fields::control_protocol_ws_port(node_model.settings)) + { + node_implementation + .on_get_control_class_descriptor(nmos::make_get_control_protocol_class_descriptor_handler(control_protocol_state)) + .on_get_control_datatype_descriptor(nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state)) + .on_get_control_protocol_method_descriptor(nmos::make_get_control_protocol_method_descriptor_handler(control_protocol_state)); + } + // Set up the node server auto node_server = nmos::experimental::make_node_server(node_model, node_implementation, log_model, gate); + // Add the underlying implementation, which will set up the node resources, etc. + + node_server.thread_functions.push_back([&] { node_implementation_thread(node_model, control_protocol_state, gate); }); + +// only implement communication with OCSP server if http_listener supports OCSP stapling +// cf. preprocessor conditions in nmos::make_http_listener_config +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) + if (nmos::experimental::fields::server_secure(node_model.settings)) + { + auto load_ca_certificates = node_implementation.load_ca_certificates; + auto load_server_certificates = node_implementation.load_server_certificates; + node_server.thread_functions.push_back([&, load_ca_certificates, load_server_certificates] { nmos::ocsp_behaviour_thread(node_model, ocsp_state, load_ca_certificates, load_server_certificates, gate); }); + } +#endif + + // only configure communication with Authorization server if IS-10/BCP-003-02 is required + if (nmos::experimental::fields::client_authorization(node_model.settings)) + { + std::map api_routers; + + // Configure the authorization_redirect API (require for Authorization Code Flow support) + + if (web::http::oauth2::experimental::grant_types::authorization_code.name == nmos::experimental::fields::authorization_flow(node_model.settings)) + { + auto load_ca_certificates = node_implementation.load_ca_certificates; + auto load_rsa_private_keys = node_implementation.load_rsa_private_keys; + api_routers[{ {}, nmos::experimental::fields::authorization_redirect_port(node_model.settings) }].mount({}, nmos::experimental::make_authorization_redirect_api(node_model, authorization_state, load_ca_certificates, load_rsa_private_keys, gate)); + } + + // Configure the jwks_uri API (require for Private Key JWK support) + + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == nmos::experimental::fields::token_endpoint_auth_method(node_model.settings)) + { + auto load_rsa_private_keys = node_implementation.load_rsa_private_keys; + api_routers[{ {}, nmos::experimental::fields::jwks_uri_port(node_model.settings) }].mount({}, nmos::experimental::make_jwk_uri_api(node_model, load_rsa_private_keys, gate)); + } + + auto http_config = nmos::make_http_listener_config(node_model.settings, node_implementation.load_server_certificates, node_implementation.load_dh_param, node_implementation.get_ocsp_response, gate); + const auto server_secure = nmos::experimental::fields::server_secure(node_model.settings); + const auto hsts = nmos::experimental::get_hsts(node_model.settings); + for (auto& api_router : api_routers) + { + auto found = node_server.api_routers.find(api_router.first); + + const auto& host = !api_router.first.first.empty() ? api_router.first.first : web::http::experimental::listener::host_wildcard; + const auto& port = nmos::experimental::server_port(api_router.first.second, node_model.settings); + + if (node_server.api_routers.end() != found) + { + const auto uri = web::http::experimental::listener::make_listener_uri(server_secure, host, port); + auto listener = std::find_if(node_server.http_listeners.begin(), node_server.http_listeners.end(), [&](const web::http::experimental::listener::http_listener& listener) { return listener.uri() == uri; }); + if (node_server.http_listeners.end() != listener) + { + found->second.pop_back(); // remove the api_finally_handler which was previously added in the make_node_server, the api_finally_handler will be re-inserted in the make_api_listener + node_server.http_listeners.erase(listener); + } + found->second.mount({}, api_router.second); + node_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, port, found->second, http_config, hsts, gate)); + } + else + { + node_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, port, api_router.second, http_config, hsts, gate)); + } + } + } + if (!nmos::experimental::fields::http_trace(node_model.settings)) { // Disable TRACE method @@ -109,9 +227,28 @@ int main(int argc, char* argv[]) } } - // Add the underlying implementation, which will set up the node resources, etc. - - node_server.thread_functions.push_back([&] { node_implementation_thread(node_model, gate); }); + // only configure communication with Authorization server if IS-10/BCP-003-02 is required + if (nmos::experimental::fields::client_authorization(node_model.settings) || nmos::experimental::fields::server_authorization(node_model.settings)) + { + // IS-10 client registration, fetch access token, and fetch authorization server token public key + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html + // and https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + auto load_ca_certificates = node_implementation.load_ca_certificates; + auto load_rsa_private_keys = node_implementation.load_rsa_private_keys; + auto load_authorization_clients = node_implementation.load_authorization_clients; + auto save_authorization_client = node_implementation.save_authorization_client; + auto request_authorization_code = node_implementation.request_authorization_code; + node_server.thread_functions.push_back([&, load_ca_certificates, load_rsa_private_keys, load_authorization_clients, save_authorization_client, request_authorization_code] { nmos::experimental::authorization_behaviour_thread(node_model, authorization_state, load_ca_certificates, load_rsa_private_keys, load_authorization_clients, save_authorization_client, request_authorization_code, gate); }); + + if (nmos::experimental::fields::server_authorization(node_model.settings)) + { + // When no matching public key for a given access token, it SHOULD attempt to obtain the missing public key + // via the the token iss claim as specified in RFC 8414 section 3. + // see https://tools.ietf.org/html/rfc8414#section-3 + // and https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + node_server.thread_functions.push_back([&, load_ca_certificates] { nmos::experimental::authorization_token_issuer_thread(node_model, authorization_state, load_ca_certificates, gate); }); + } + } // Open the API ports and start up node operation (including the DNS-SD advertisements) @@ -128,32 +265,45 @@ int main(int argc, char* argv[]) } catch (const web::json::json_exception& e) { - // most likely from incorrect types in the command line settings + // most likely from incorrect syntax or incorrect value types in the command line settings slog::log(gate, SLOG_FLF) << "JSON error: " << e.what(); + return 1; } catch (const web::http::http_exception& e) { slog::log(gate, SLOG_FLF) << "HTTP error: " << e.what() << " [" << e.error_code() << "]"; + return 1; } catch (const web::websockets::websocket_exception& e) { slog::log(gate, SLOG_FLF) << "WebSocket error: " << e.what() << " [" << e.error_code() << "]"; + return 1; + } + catch (const std::ios_base::failure& e) + { + // most likely from failing to open the command line settings file + slog::log(gate, SLOG_FLF) << "File error: " << e.what(); + return 1; } catch (const std::system_error& e) { slog::log(gate, SLOG_FLF) << "System error: " << e.what() << " [" << e.code() << "]"; + return 1; } catch (const std::runtime_error& e) { slog::log(gate, SLOG_FLF) << "Implementation error: " << e.what(); + return 1; } catch (const std::exception& e) { slog::log(gate, SLOG_FLF) << "Unexpected exception: " << e.what(); + return 1; } catch (...) { slog::log(gate, SLOG_FLF) << "Unexpected unknown exception"; + return 1; } slog::log(gate, SLOG_FLF) << "Stopping nmos-cpp node"; diff --git a/Development/nmos-cpp-node/node_implementation.cpp b/Development/nmos-cpp-node/node_implementation.cpp index 9ea87cf09..6450c1fb6 100644 --- a/Development/nmos-cpp-node/node_implementation.cpp +++ b/Development/nmos-cpp-node/node_implementation.cpp @@ -1,5 +1,6 @@ #include "node_implementation.h" +#include #include #include #include @@ -12,17 +13,23 @@ #ifdef HAVE_LLDP #include "lldp/lldp_manager.h" #endif +#include "nmos/activation_mode.h" #include "nmos/capabilities.h" #include "nmos/channels.h" #include "nmos/channelmapping_resources.h" #include "nmos/clock_name.h" #include "nmos/colorspace.h" -#include "nmos/components.h" // for nmos::chroma_subsampling #include "nmos/connection_resources.h" #include "nmos/connection_events_activation.h" +#include "nmos/control_protocol_resources.h" +#include "nmos/control_protocol_resource.h" +#include "nmos/control_protocol_state.h" +#include "nmos/control_protocol_utils.h" #include "nmos/events_resources.h" +#include "nmos/format.h" #include "nmos/group_hint.h" #include "nmos/interlace_mode.h" +#include "nmos/is12_versions.h" // for IS-12 gain control #ifdef HAVE_LLDP #include "nmos/lldp_manager.h" #endif @@ -35,9 +42,11 @@ #include "nmos/random.h" #include "nmos/sdp_utils.h" #include "nmos/slog.h" +#include "nmos/st2110_21_sender_type.h" #include "nmos/system_resources.h" #include "nmos/transfer_characteristic.h" #include "nmos/transport.h" +#include "nmos/video_jxsv.h" #include "sdp/sdp.h" // example node implementation details @@ -52,9 +61,28 @@ namespace impl // custom settings for the example node implementation namespace fields { + // node_tags, device_tags: used in resource tags fields + // "Each tag has a single key, but MAY have multiple values." + // See https://specs.amwa.tv/is-04/releases/v1.3.2/docs/APIs_-_Common_Keys.html#tags + // { + // "tag_1": [ "tag_1_value_1", "tag_1_value_2" ], + // "tag_2": [ "tag_2_value_1" ] + // } + const web::json::field_as_value_or node_tags{ U("node_tags"), web::json::value::object() }; + const web::json::field_as_value_or device_tags{ U("device_tags"), web::json::value::object() }; + // how_many: provides for very basic testing of a node with many sub-resources of each type const web::json::field_as_integer_or how_many{ U("how_many"), 1 }; + // activate_senders: controls whether to activate senders on start up (true, default) or not (false) + const web::json::field_as_bool_or activate_senders{ U("activate_senders"), true }; + + // senders, receivers: controls which kinds of sender and receiver are instantiated by the example node + // the values must be an array of unique strings identifying the kinds of 'port', like ["v", "a", "d"], see impl::ports + // when omitted, all ports are instantiated + const web::json::field_as_value_or senders{ U("senders"), {} }; + const web::json::field_as_value_or receivers{ U("receivers"), {} }; + // frame_rate: controls the grain_rate of video, audio and ancillary data sources and flows // and the equivalent parameter constraint on video receivers // the value must be an object like { "numerator": 25, "denominator": 1 } @@ -64,8 +92,28 @@ namespace impl { nmos::fields::denominator, 1 } }) }; - // hm, could add custom settings for e.g. frame_width, frame_height to allow 720p and UHD, - // and interlace_mode to allow 1080p25 and 1080p29.97 as well as 1080i50 and 1080i59.94? + // frame_width, frame_height: control the frame_width and frame_height of video flows + const web::json::field_as_integer_or frame_width{ U("frame_width"), 1920 }; + const web::json::field_as_integer_or frame_height{ U("frame_height"), 1080 }; + + // interlace_mode: controls the interlace_mode of video flows, see nmos::interlace_mode + // when omitted, a default of "progressive" or "interlaced_tff" is used based on the frame_rate, etc. + const web::json::field_as_string interlace_mode{ U("interlace_mode") }; + + // colorspace: controls the colorspace of video flows, see nmos::colorspace + const web::json::field_as_string_or colorspace{ U("colorspace"), U("BT709") }; + + // transfer_characteristic: controls the transfer characteristic system of video flows, see nmos::transfer_characteristic + const web::json::field_as_string_or transfer_characteristic{ U("transfer_characteristic"), U("SDR") }; + + // color_sampling: controls the color (sub-)sampling mode of video flows, see sdp::sampling + const web::json::field_as_string_or color_sampling{ U("color_sampling"), U("YCbCr-4:2:2") }; + + // component_depth: controls the bits per component sample of video flows + const web::json::field_as_integer_or component_depth{ U("component_depth"), 10 }; + + // video_type: media type of video flows, e.g. "video/raw" or "video/jxsv", see nmos::media_types + const web::json::field_as_string_or video_type{ U("video_type"), U("video/raw") }; // channel_count: controls the number of channels in audio sources const web::json::field_as_integer_or channel_count{ U("channel_count"), 4 }; @@ -74,12 +122,14 @@ namespace impl const web::json::field_as_bool_or smpte2022_7{ U("smpte2022_7"), true }; } + nmos::interlace_mode get_interlace_mode(const nmos::settings& settings); + // the different kinds of 'port' (standing for the format/media type/event type) implemented by the example node - // each 'port' of the example node has a source, flow, sender and compatible receiver + // each 'port' of the example node has a source, flow, sender and/or compatible receiver DEFINE_STRING_ENUM(port) namespace ports { - // video/raw + // video/raw, video/jxsv, etc. const port video{ U("v") }; // audio/L24 const port audio{ U("a") }; @@ -102,6 +152,10 @@ namespace impl const std::vector all{ boost::copy_range>(boost::range::join(rtp, ws)) }; } + bool is_rtp_port(const port& port); + bool is_ws_port(const port& port); + std::vector parse_ports(const web::json::value& value); + const std::vector channels_repeat{ { U("Left Channel"), nmos::channel_symbols::L }, { U("Right Channel"), nmos::channel_symbols::R }, @@ -109,13 +163,23 @@ namespace impl { U("Low Frequency Effects Channel"), nmos::channel_symbols::LFE } }; + // find interface with the specified address + std::vector::const_iterator find_interface(const std::vector& interfaces, const utility::string_t& address); + // generate repeatable ids for the example node's resources nmos::id make_id(const nmos::id& seed_id, const nmos::type& type, const port& port = {}, int index = 0); - std::vector make_ids(const nmos::id& seed_id, const nmos::type& type, const port& port, int how_many); - std::vector make_ids(const nmos::id& seed_id, const nmos::type& type, const std::vector& ports, int how_many); + std::vector make_ids(const nmos::id& seed_id, const nmos::type& type, const port& port, int how_many = 1); + std::vector make_ids(const nmos::id& seed_id, const nmos::type& type, const std::vector& ports, int how_many = 1); + std::vector make_ids(const nmos::id& seed_id, const std::vector& types, const std::vector& ports, int how_many = 1); + + // generate a repeatable source-specific multicast address for each leg of a sender + utility::string_t make_source_specific_multicast_address_v4(const nmos::id& id, int leg = 0); + + // add a selection of parents to a source or flow + void insert_parents(nmos::resource& resource, const nmos::id& seed_id, const port& port, int index); // add a helpful suffix to the label of a sub-resource for the example node - void set_label(nmos::resource& resource, const port& port, int index); + void set_label_description(nmos::resource& resource, const port& port, int index); // add an example "natural grouping" hint to a sender or receiver void insert_group_hint(nmos::resource& resource, const port& port, int index); @@ -127,16 +191,54 @@ namespace impl } // forward declarations for node_implementation_thread +void node_implementation_init(nmos::node_model& model, nmos::experimental::control_protocol_state& control_protocol_state, slog::base_gate& gate); +void node_implementation_run(nmos::node_model& model, slog::base_gate& gate); nmos::connection_resource_auto_resolver make_node_implementation_auto_resolver(const nmos::settings& settings); nmos::connection_sender_transportfile_setter make_node_implementation_transportfile_setter(const nmos::resources& node_resources, const nmos::settings& settings); +struct node_implementation_init_exception {}; + // This is an example of how to integrate the nmos-cpp library with a device-specific underlying implementation. // It constructs and inserts a node resource and some sub-resources into the model, based on the model settings, -// starts background tasks to emit regular events from the temperature event source and then waits for shutdown. -void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) +// starts background tasks to emit regular events from the temperature event source, and then waits for shutdown. +void node_implementation_thread(nmos::node_model& model, nmos::experimental::control_protocol_state& control_protocol_state, slog::base_gate& gate_) { nmos::details::omanip_gate gate{ gate_, nmos::stash_category(impl::categories::node_implementation) }; + try + { + node_implementation_init(model, control_protocol_state, gate); + node_implementation_run(model, gate); + } + catch (const node_implementation_init_exception&) + { + // node_implementation_init writes the log message + } + catch (const web::json::json_exception& e) + { + // most likely from incorrect value types in the command line settings + slog::log(gate, SLOG_FLF) << "JSON error: " << e.what(); + } + catch (const std::system_error& e) + { + slog::log(gate, SLOG_FLF) << "System error: " << e.what() << " [" << e.code() << "]"; + } + catch (const std::runtime_error& e) + { + slog::log(gate, SLOG_FLF) << "Implementation error: " << e.what(); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Unexpected exception: " << e.what(); + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Unexpected unknown exception"; + } +} + +void node_implementation_init(nmos::node_model& model, nmos::experimental::control_protocol_state& control_protocol_state, slog::base_gate& gate) +{ using web::json::value; using web::json::value_from_elements; using web::json::value_of; @@ -147,13 +249,35 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) const auto node_id = impl::make_id(seed_id, nmos::types::node); const auto device_id = impl::make_id(seed_id, nmos::types::device); const auto how_many = impl::fields::how_many(model.settings); + const auto sender_ports = impl::parse_ports(impl::fields::senders(model.settings)); + const auto rtp_sender_ports = boost::copy_range>(sender_ports | boost::adaptors::filtered(impl::is_rtp_port)); + const auto ws_sender_ports = boost::copy_range>(sender_ports | boost::adaptors::filtered(impl::is_ws_port)); + const auto receiver_ports = impl::parse_ports(impl::fields::receivers(model.settings)); + const auto rtp_receiver_ports = boost::copy_range>(receiver_ports | boost::adaptors::filtered(impl::is_rtp_port)); + const auto ws_receiver_ports = boost::copy_range>(receiver_ports | boost::adaptors::filtered(impl::is_ws_port)); const auto frame_rate = nmos::parse_rational(impl::fields::frame_rate(model.settings)); + const auto frame_width = impl::fields::frame_width(model.settings); + const auto frame_height = impl::fields::frame_height(model.settings); + const auto interlace_mode = impl::get_interlace_mode(model.settings); + const auto colorspace = nmos::colorspace{ impl::fields::colorspace(model.settings) }; + const auto transfer_characteristic = nmos::transfer_characteristic{ impl::fields::transfer_characteristic(model.settings) }; + const auto sampling = sdp::sampling{ impl::fields::color_sampling(model.settings) }; + const auto bit_depth = impl::fields::component_depth(model.settings); + const auto video_type = nmos::media_type{ impl::fields::video_type(model.settings) }; const auto channel_count = impl::fields::channel_count(model.settings); const auto smpte2022_7 = impl::fields::smpte2022_7(model.settings); - // any delay between updates to the model resources is unnecessary - // this just serves as a slightly more realistic example! - const unsigned int delay_millis{ 10 }; + // for now, some typical values for video/jxsv, based on VSF TR-08:2022 + // see https://vsf.tv/download/technical_recommendations/VSF_TR-08_2022-04-20.pdf + const auto profile = nmos::profiles::High444_12; + const auto level = nmos::get_video_jxsv_level(frame_rate, frame_width, frame_height); + const auto sublevel = nmos::sublevels::Sublev3bpp; + const auto max_bits_per_pixel = 4.0; // min coding efficiency + const auto bits_per_pixel = 2.0; + const auto transport_bit_rate_factor = 1.05; + + // any delay between updates to the model resources is unnecessary unless for debugging purposes + const unsigned int delay_millis{ 0 }; // it is important that the model be locked before inserting, updating or deleting a resource // and that the the node behaviour thread be notified after doing so @@ -175,6 +299,27 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) return success; }; + // it is important that the model be locked before inserting, updating or deleting a resource + // and that the the node behaviour thread be notified after doing so + const auto insert_root_after = [&model, insert_resource_after](unsigned int milliseconds, nmos::control_protocol_resource& root, slog::base_gate& gate) + { + std::function insert_resources; + + insert_resources = [&milliseconds, insert_resource_after, &insert_resources, &gate](nmos::resources& resources, nmos::control_protocol_resource& resource) + { + for (auto& resource_ : resource.resources) + { + insert_resources(resources, resource_); + if (!insert_resource_after(milliseconds, resources, std::move(resource_), gate)) throw node_implementation_init_exception(); + } + }; + + auto& resources = model.control_protocol_resources; + + insert_resources(resources, root); + if (!insert_resource_after(milliseconds, resources, std::move(root), gate)) throw node_implementation_init_exception(); + }; + const auto resolve_auto = make_node_implementation_auto_resolver(model.settings); const auto set_transportfile = make_node_implementation_transportfile_setter(model.node_resources, model.settings); @@ -186,7 +331,8 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) // example node { auto node = nmos::make_node(node_id, clocks, nmos::make_node_interfaces(interfaces), model.settings); - if (!insert_resource_after(delay_millis, model.node_resources, std::move(node), gate)) return; + node.data[nmos::fields::tags] = impl::fields::node_tags(model.settings); + if (!insert_resource_after(delay_millis, model.node_resources, std::move(node), gate)) throw node_implementation_init_exception(); } #ifdef HAVE_LLDP @@ -198,35 +344,46 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) #endif // prepare interface bindings for all senders and receivers - const auto host_interface_ = boost::range::find_if(host_interfaces, [&](const web::hosts::experimental::host_interface& interface) - { - return interface.addresses.end() != boost::range::find(interface.addresses, nmos::fields::host_address(model.settings)); - }); + const auto& host_address = nmos::fields::host_address(model.settings); + // the interface corresponding to the host address is used for the example node's WebSocket senders and receivers + const auto host_interface_ = impl::find_interface(host_interfaces, host_address); if (host_interfaces.end() == host_interface_) { slog::log(gate, SLOG_FLF) << "No network interface corresponding to host_address?"; - return; + throw node_implementation_init_exception(); } const auto& host_interface = *host_interface_; - // hmm, should probably add a custom setting to control the primary and secondary interfaces for the example node's senders and receivers - const auto& primary_interface = host_interfaces.front(); - const auto& secondary_interface = host_interfaces.back(); + // hmm, should probably add a custom setting to control the primary and secondary interfaces for the example node's RTP senders and receivers + // rather than just picking the one(s) corresponding to the first and last of the specified host addresses + const auto& primary_address = model.settings.has_field(nmos::fields::host_addresses) ? web::json::front(nmos::fields::host_addresses(model.settings)).as_string() : host_address; + const auto& secondary_address = model.settings.has_field(nmos::fields::host_addresses) ? web::json::back(nmos::fields::host_addresses(model.settings)).as_string() : host_address; + const auto primary_interface_ = impl::find_interface(host_interfaces, primary_address); + const auto secondary_interface_ = impl::find_interface(host_interfaces, secondary_address); + if (host_interfaces.end() == primary_interface_ || host_interfaces.end() == secondary_interface_) + { + slog::log(gate, SLOG_FLF) << "No network interface corresponding to one of the host_addresses?"; + throw node_implementation_init_exception(); + } + const auto& primary_interface = *primary_interface_; + const auto& secondary_interface = *secondary_interface_; const auto interface_names = smpte2022_7 ? std::vector{ primary_interface.name, secondary_interface.name } : std::vector{ primary_interface.name }; // example device { - auto sender_ids = impl::make_ids(seed_id, nmos::types::sender, impl::ports::rtp, how_many); - if (0 <= nmos::fields::events_port(model.settings)) boost::range::push_back(sender_ids, impl::make_ids(seed_id, nmos::types::sender, impl::ports::ws, how_many)); - auto receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, impl::ports::all, how_many); - if (!insert_resource_after(delay_millis, model.node_resources, nmos::make_device(device_id, node_id, sender_ids, receiver_ids, model.settings), gate)) return; + auto sender_ids = impl::make_ids(seed_id, nmos::types::sender, rtp_sender_ports, how_many); + if (0 <= nmos::fields::events_port(model.settings)) boost::range::push_back(sender_ids, impl::make_ids(seed_id, nmos::types::sender, ws_sender_ports, how_many)); + auto receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, receiver_ports, how_many); + auto device = nmos::make_device(device_id, node_id, sender_ids, receiver_ids, model.settings); + device.data[nmos::fields::tags] = impl::fields::device_tags(model.settings); + if (!insert_resource_after(delay_millis, model.node_resources, std::move(device), gate)) throw node_implementation_init_exception(); } // example sources, flows and senders for (int index = 0; index < how_many; ++index) { - for (const auto& port : impl::ports::rtp) + for (const auto& port : rtp_sender_ports) { const auto source_id = impl::make_id(seed_id, nmos::types::source, port, index); const auto flow_id = impl::make_id(seed_id, nmos::types::flow, port, index); @@ -243,7 +400,7 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) { return impl::channels_repeat[index % (int)impl::channels_repeat.size()]; })); - + source = nmos::make_audio_source(source_id, device_id, nmos::clock_names::clk0, frame_rate, channels, model.settings); } else if (impl::ports::data == port) @@ -254,24 +411,44 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) { source = nmos::make_mux_source(source_id, device_id, nmos::clock_names::clk0, frame_rate, model.settings); } - impl::set_label(source, port, index); + impl::insert_parents(source, seed_id, port, index); + impl::set_label_description(source, port, index); nmos::resource flow; if (impl::ports::video == port) { - // for 1080i formats, ST 2110-20 says that "the fields of an interlaced image are transmitted in time order, - // first field first [and] the sample rows of the temporally second field are displaced vertically 'below' the - // like-numbered sample rows of the temporally first field." - const auto interlace_mode = nmos::rates::rate25 == frame_rate || nmos::rates::rate29_97 == frame_rate - ? nmos::interlace_modes::interlaced_tff - : nmos::interlace_modes::progressive; - flow = nmos::make_raw_video_flow( - flow_id, source_id, device_id, - frame_rate, - 1920, 1080, interlace_mode, - nmos::colorspaces::BT709, nmos::transfer_characteristics::SDR, nmos::chroma_subsampling::YCbCr422, 10, - model.settings - ); + if (nmos::media_types::video_raw == video_type) + { + flow = nmos::make_raw_video_flow( + flow_id, source_id, device_id, + frame_rate, + frame_width, frame_height, interlace_mode, + colorspace, transfer_characteristic, sampling, bit_depth, + model.settings + ); + } + else if (nmos::media_types::video_jxsv == video_type) + { + flow = nmos::make_video_jxsv_flow( + flow_id, source_id, device_id, + frame_rate, + frame_width, frame_height, interlace_mode, + colorspace, transfer_characteristic, sampling, bit_depth, + profile, level, sublevel, bits_per_pixel, + model.settings + ); + } + else + { + flow = nmos::make_coded_video_flow( + flow_id, source_id, device_id, + frame_rate, + frame_width, frame_height, interlace_mode, + colorspace, transfer_characteristic, sampling, bit_depth, + video_type, + model.settings + ); + } } else if (impl::ports::audio == port) { @@ -292,14 +469,27 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) // add optional grain_rate flow.data[nmos::fields::grain_rate] = nmos::make_rational(frame_rate); } - impl::set_label(flow, port, index); + impl::insert_parents(flow, seed_id, port, index); + impl::set_label_description(flow, port, index); // set_transportfile needs to find the matching source and flow for the sender, so insert these first - if (!insert_resource_after(delay_millis, model.node_resources, std::move(source), gate)) return; - if (!insert_resource_after(delay_millis, model.node_resources, std::move(flow), gate)) return; + if (!insert_resource_after(delay_millis, model.node_resources, std::move(source), gate)) throw node_implementation_init_exception(); + if (!insert_resource_after(delay_millis, model.node_resources, std::move(flow), gate)) throw node_implementation_init_exception(); - auto sender = nmos::make_sender(sender_id, flow_id, device_id, interface_names, model.settings); - impl::set_label(sender, port, index); + const auto manifest_href = nmos::experimental::make_manifest_api_manifest(sender_id, model.settings); + auto sender = nmos::make_sender(sender_id, flow_id, nmos::transports::rtp, device_id, manifest_href.to_string(), interface_names, model.settings); + // hm, could add nmos::make_video_jxsv_sender to encapsulate this? + if (impl::ports::video == port && nmos::media_types::video_jxsv == video_type) + { + // additional attributes required by BCP-006-01 + // see https://specs.amwa.tv/bcp-006-01/branches/v1.0-dev/docs/NMOS_With_JPEG_XS.html#senders + const auto format_bit_rate = nmos::get_video_jxsv_bit_rate(frame_rate, frame_width, frame_height, bits_per_pixel); + // round to nearest Megabit/second per examples in VSF TR-08:2022 + const auto transport_bit_rate = uint64_t(transport_bit_rate_factor * format_bit_rate / 1e3 + 0.5) * 1000; + sender.data[nmos::fields::bit_rate] = value(transport_bit_rate); + sender.data[nmos::fields::st2110_21_sender_type] = value(nmos::st2110_21_sender_types::type_N.name); + } + impl::set_label_description(sender, port, index); impl::insert_group_hint(sender, port, index); auto connection_sender = nmos::make_connection_rtp_sender(sender_id, smpte2022_7); @@ -311,46 +501,74 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) { nmos::fields::constraint_enum, value_from_elements(secondary_interface.addresses) } }); - // initialize this sender enabled, just to enable the IS-05-01 test suite to run immediately - connection_sender.data[nmos::fields::endpoint_active][nmos::fields::master_enable] = connection_sender.data[nmos::fields::endpoint_staged][nmos::fields::master_enable] = value::boolean(true); - resolve_auto(sender, connection_sender, connection_sender.data[nmos::fields::endpoint_active][nmos::fields::transport_params]); - set_transportfile(sender, connection_sender, connection_sender.data[nmos::fields::endpoint_transportfile]); - nmos::set_resource_subscription(sender, nmos::fields::master_enable(connection_sender.data[nmos::fields::endpoint_active]), {}, nmos::tai_now()); + if (impl::fields::activate_senders(model.settings)) + { + // initialize this sender with a scheduled activation, e.g. to enable the IS-05-01 test suite to run immediately + auto& staged = connection_sender.data[nmos::fields::endpoint_staged]; + staged[nmos::fields::master_enable] = value::boolean(true); + staged[nmos::fields::activation] = value_of({ + { nmos::fields::mode, nmos::activation_modes::activate_scheduled_relative.name }, + { nmos::fields::requested_time, U("0:0") }, + { nmos::fields::activation_time, nmos::make_version() } + }); + } - if (!insert_resource_after(delay_millis, model.node_resources, std::move(sender), gate)) return; - if (!insert_resource_after(delay_millis, model.connection_resources, std::move(connection_sender), gate)) return; + if (!insert_resource_after(delay_millis, model.node_resources, std::move(sender), gate)) throw node_implementation_init_exception(); + if (!insert_resource_after(delay_millis, model.connection_resources, std::move(connection_sender), gate)) throw node_implementation_init_exception(); } } // example receivers for (int index = 0; index < how_many; ++index) { - for (const auto& port : impl::ports::rtp) + for (const auto& port : rtp_receiver_ports) { const auto receiver_id = impl::make_id(seed_id, nmos::types::receiver, port, index); nmos::resource receiver; if (impl::ports::video == port) { - receiver = nmos::make_video_receiver(receiver_id, device_id, nmos::transports::rtp_mcast, interface_names, model.settings); + receiver = nmos::make_receiver(receiver_id, device_id, nmos::transports::rtp, interface_names, nmos::formats::video, { video_type }, model.settings); // add an example constraint set; these should be completed fully! - const auto interlace_modes = nmos::rates::rate25 == frame_rate || nmos::rates::rate29_97 == frame_rate - ? std::vector{ nmos::interlace_modes::interlaced_bff.name, nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name } - : std::vector{ nmos::interlace_modes::progressive.name }; - receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({ - value_of({ - { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ frame_rate }) }, - { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, - { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, - { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint(interlace_modes) }, - { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) } - }) - }); + if (nmos::media_types::video_raw == video_type) + { + const auto interlace_modes = nmos::interlace_modes::progressive != interlace_mode + ? std::vector{ nmos::interlace_modes::interlaced_bff.name, nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name } + : std::vector{ nmos::interlace_modes::progressive.name }; + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({ + value_of({ + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ frame_rate }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ frame_width }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ frame_height }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint(interlace_modes) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sampling.name }) } + }) + }); + } + else if (nmos::media_types::video_jxsv == video_type) + { + // some of the parameter constraints recommended by BCP-006-01 + // see https://specs.amwa.tv/bcp-006-01/branches/v1.0-dev/docs/NMOS_With_JPEG_XS.html#receivers + const auto max_format_bit_rate = nmos::get_video_jxsv_bit_rate(frame_rate, frame_width, frame_height, max_bits_per_pixel); + // round to nearest Megabit/second per examples in VSF TR-08:2022 + const auto max_transport_bit_rate = uint64_t(transport_bit_rate_factor * max_format_bit_rate / 1e3 + 0.5) * 1000; + + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({ + value_of({ + { nmos::caps::format::profile, nmos::make_caps_string_constraint({ profile.name }) }, + { nmos::caps::format::level, nmos::make_caps_string_constraint({ level.name }) }, + { nmos::caps::format::sublevel, nmos::make_caps_string_constraint({ nmos::sublevels::Sublev3bpp.name, nmos::sublevels::Sublev4bpp.name }) }, + { nmos::caps::format::bit_rate, nmos::make_caps_integer_constraint({}, nmos::no_minimum(), (int64_t)max_format_bit_rate) }, + { nmos::caps::transport::bit_rate, nmos::make_caps_integer_constraint({}, nmos::no_minimum(), (int64_t)max_transport_bit_rate) }, + { nmos::caps::transport::packet_transmission_mode, nmos::make_caps_string_constraint({ nmos::packet_transmission_modes::codestream.name }) } + }) + }); + } receiver.data[nmos::fields::version] = receiver.data[nmos::fields::caps][nmos::fields::version] = value(nmos::make_version()); } else if (impl::ports::audio == port) { - receiver = nmos::make_audio_receiver(receiver_id, device_id, nmos::transports::rtp_mcast, interface_names, 24, model.settings); + receiver = nmos::make_audio_receiver(receiver_id, device_id, nmos::transports::rtp, interface_names, 24, model.settings); // add some example constraint sets; these should be completed fully! receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({ value_of({ @@ -371,7 +589,7 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) } else if (impl::ports::data == port) { - receiver = nmos::make_sdianc_data_receiver(receiver_id, device_id, nmos::transports::rtp_mcast, interface_names, model.settings); + receiver = nmos::make_sdianc_data_receiver(receiver_id, device_id, nmos::transports::rtp, interface_names, model.settings); // add an example constraint set; these should be completed fully! receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({ value_of({ @@ -382,7 +600,7 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) } else if (impl::ports::mux == port) { - receiver = nmos::make_mux_receiver(receiver_id, device_id, nmos::transports::rtp_mcast, interface_names, model.settings); + receiver = nmos::make_mux_receiver(receiver_id, device_id, nmos::transports::rtp, interface_names, model.settings); // add an example constraint set; these should be completed fully! receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({ value_of({ @@ -391,7 +609,7 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) }); receiver.data[nmos::fields::version] = receiver.data[nmos::fields::caps][nmos::fields::version] = value(nmos::make_version()); } - impl::set_label(receiver, port, index); + impl::set_label_description(receiver, port, index); impl::insert_group_hint(receiver, port, index); auto connection_receiver = nmos::make_connection_rtp_receiver(receiver_id, smpte2022_7); @@ -405,15 +623,15 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) resolve_auto(receiver, connection_receiver, connection_receiver.data[nmos::fields::endpoint_active][nmos::fields::transport_params]); - if (!insert_resource_after(delay_millis, model.node_resources, std::move(receiver), gate)) return; - if (!insert_resource_after(delay_millis, model.connection_resources, std::move(connection_receiver), gate)) return; + if (!insert_resource_after(delay_millis, model.node_resources, std::move(receiver), gate)) throw node_implementation_init_exception(); + if (!insert_resource_after(delay_millis, model.connection_resources, std::move(connection_receiver), gate)) throw node_implementation_init_exception(); } } - // example event sources, senders, flows + // example event sources, flows and senders for (int index = 0; 0 <= nmos::fields::events_port(model.settings) && index < how_many; ++index) { - for (const auto& port : impl::ports::ws) + for (const auto& port : ws_sender_ports) { const auto source_id = impl::make_id(seed_id, nmos::types::source, port, index); const auto flow_id = impl::make_id(seed_id, nmos::types::flow, port, index); @@ -426,9 +644,9 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) { event_type = impl::temperature_Celsius; - // see https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/docs/3.0.%20Event%20types.md#231-measurements - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/examples/eventsapi-type-number-measurement-get-200.json - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/examples/eventsapi-state-number-measurement-get-200.json + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#231-measurements + // and https://specs.amwa.tv/is-07/releases/v1.0.1/examples/eventsapi-type-number-measurement-get-200.html + // and https://specs.amwa.tv/is-07/releases/v1.0.1/examples/eventsapi-state-number-measurement-get-200.html events_type = nmos::make_events_number_type({ -200, 10 }, { 1000, 10 }, { 1, 10 }, U("C")); events_state = nmos::make_events_number_state({ source_id, flow_id }, { 201, 10 }, event_type); } @@ -436,7 +654,7 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) { event_type = nmos::event_types::boolean; - // see https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/docs/3.0.%20Event%20types.md#21-boolean + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#21-boolean events_type = nmos::make_events_boolean_type(); events_state = nmos::make_events_boolean_state({ source_id, flow_id }, false); } @@ -444,7 +662,7 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) { event_type = nmos::event_types::string; - // see https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/docs/3.0.%20Event%20types.md#22-string + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#22-string // and of course, https://en.wikipedia.org/wiki/Metasyntactic_variable events_type = nmos::make_events_string_type(0, 0, U("^foo|bar|baz|qu+x$")); events_state = nmos::make_events_string_state({ source_id, flow_id }, U("foo")); @@ -453,7 +671,7 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) { event_type = impl::catcall; - // see https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/docs/3.0.%20Event%20types.md#3-enum + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#3-enum events_type = nmos::make_events_number_enum_type({ { 1, { U("meow"), U("chatty") } }, { 2, { U("purr"), U("happy") } }, @@ -465,15 +683,15 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) // grain_rate is not set because these events are aperiodic auto source = nmos::make_data_source(source_id, device_id, {}, event_type, model.settings); - impl::set_label(source, port, index); + impl::set_label_description(source, port, index); auto events_source = nmos::make_events_source(source_id, events_state, events_type); auto flow = nmos::make_json_data_flow(flow_id, source_id, device_id, event_type, model.settings); - impl::set_label(flow, port, index); + impl::set_label_description(flow, port, index); auto sender = nmos::make_sender(sender_id, flow_id, nmos::transports::websocket, device_id, {}, { host_interface.name }, model.settings); - impl::set_label(sender, port, index); + impl::set_label_description(sender, port, index); impl::insert_group_hint(sender, port, index); // initialize this sender enabled, just to enable the IS-07-02 test suite to run immediately @@ -482,18 +700,18 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) resolve_auto(sender, connection_sender, connection_sender.data[nmos::fields::endpoint_active][nmos::fields::transport_params]); nmos::set_resource_subscription(sender, nmos::fields::master_enable(connection_sender.data[nmos::fields::endpoint_active]), {}, nmos::tai_now()); - if (!insert_resource_after(delay_millis, model.node_resources, std::move(source), gate)) return; - if (!insert_resource_after(delay_millis, model.node_resources, std::move(flow), gate)) return; - if (!insert_resource_after(delay_millis, model.node_resources, std::move(sender), gate)) return; - if (!insert_resource_after(delay_millis, model.connection_resources, std::move(connection_sender), gate)) return; - if (!insert_resource_after(delay_millis, model.events_resources, std::move(events_source), gate)) return; + if (!insert_resource_after(delay_millis, model.node_resources, std::move(source), gate)) throw node_implementation_init_exception(); + if (!insert_resource_after(delay_millis, model.node_resources, std::move(flow), gate)) throw node_implementation_init_exception(); + if (!insert_resource_after(delay_millis, model.node_resources, std::move(sender), gate)) throw node_implementation_init_exception(); + if (!insert_resource_after(delay_millis, model.connection_resources, std::move(connection_sender), gate)) throw node_implementation_init_exception(); + if (!insert_resource_after(delay_millis, model.events_resources, std::move(events_source), gate)) throw node_implementation_init_exception(); } } // example event receivers for (int index = 0; index < how_many; ++index) { - for (const auto& port : impl::ports::ws) + for (const auto& port : ws_receiver_ports) { const auto receiver_id = impl::make_id(seed_id, nmos::types::receiver, port, index); @@ -520,20 +738,23 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) } auto receiver = nmos::make_data_receiver(receiver_id, device_id, nmos::transports::websocket, { host_interface.name }, nmos::media_types::application_json, { event_type }, model.settings); - impl::set_label(receiver, port, index); + impl::set_label_description(receiver, port, index); impl::insert_group_hint(receiver, port, index); auto connection_receiver = nmos::make_connection_events_websocket_receiver(receiver_id, model.settings); resolve_auto(receiver, connection_receiver, connection_receiver.data[nmos::fields::endpoint_active][nmos::fields::transport_params]); - if (!insert_resource_after(delay_millis, model.node_resources, std::move(receiver), gate)) return; - if (!insert_resource_after(delay_millis, model.connection_resources, std::move(connection_receiver), gate)) return; + if (!insert_resource_after(delay_millis, model.node_resources, std::move(receiver), gate)) throw node_implementation_init_exception(); + if (!insert_resource_after(delay_millis, model.connection_resources, std::move(connection_receiver), gate)) throw node_implementation_init_exception(); } } - // example audio inputs + // example channelmapping resources demonstrating a range of input/output capabilities + // see https://github.com/sony/nmos-cpp/issues/111#issuecomment-740613137 - for (int index = 0; index < how_many; ++index) + // example audio inputs + const bool channelmapping_receivers = 0 <= nmos::fields::channelmapping_port(model.settings) && rtp_receiver_ports.end() != boost::range::find(rtp_receiver_ports, impl::ports::audio); + for (int index = 0; channelmapping_receivers && index < how_many; ++index) { const auto stri = utility::conversions::details::to_string_t(index); @@ -552,12 +773,12 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) // use default input capabilities to indicate no constraints auto channelmapping_input = nmos::make_channelmapping_input(id, name, description, parent, channel_labels); - if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_input), gate)) return; + if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_input), gate)) throw node_implementation_init_exception(); } // example audio outputs - - for (int index = 0; index < how_many; ++index) + const bool channelmapping_senders = 0 <= nmos::fields::channelmapping_port(model.settings) && rtp_sender_ports.end() != boost::range::find(rtp_sender_ports, impl::ports::audio); + for (int index = 0; channelmapping_senders && index < how_many; ++index) { const auto stri = utility::conversions::details::to_string_t(index); @@ -575,12 +796,14 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) // omit routable inputs to indicate no restrictions auto channelmapping_output = nmos::make_channelmapping_output(id, name, description, source_id, channel_labels); - if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_output), gate)) return; + if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_output), gate)) throw node_implementation_init_exception(); } - // example non-IP audio input const int input_block_size = 8; const int input_block_count = 8; + + // example non-IP audio input + if (0 <= nmos::fields::channelmapping_port(model.settings)) { const auto id = U("inputA"); @@ -600,11 +823,11 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) const auto block_size = input_block_size; auto channelmapping_input = nmos::make_channelmapping_input(id, name, description, parent, channel_labels, reordering, block_size); - if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_input), gate)) return; + if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_input), gate)) throw node_implementation_init_exception(); } // example outputs to some audio gizmo - + if (0 <= nmos::fields::channelmapping_port(model.settings)) { const auto id = U("outputX"); @@ -629,11 +852,11 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) })); auto channelmapping_output = nmos::make_channelmapping_output(id, name, description, source_id, channel_labels, routable_inputs, active_map); - if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_output), gate)) return; + if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_output), gate)) throw node_implementation_init_exception(); } // example source for some audio gizmo - + if (0 <= nmos::fields::channelmapping_port(model.settings)) { const auto source_id = impl::make_id(seed_id, nmos::types::source, impl::ports::audio, how_many); @@ -643,13 +866,13 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) })); auto source = nmos::make_audio_source(source_id, device_id, nmos::clock_names::clk0, frame_rate, channels, model.settings); - impl::set_label(source, impl::ports::audio, how_many); + impl::set_label_description(source, impl::ports::audio, how_many); - if (!insert_resource_after(delay_millis, model.node_resources, std::move(source), gate)) return; + if (!insert_resource_after(delay_millis, model.node_resources, std::move(source), gate)) throw node_implementation_init_exception(); } // example inputs from some audio gizmo - + if (0 <= nmos::fields::channelmapping_port(model.settings)) { const auto id = U("inputX"); @@ -670,11 +893,11 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) const auto block_size = 2; auto channelmapping_input = nmos::make_channelmapping_input(id, name, description, parent, channel_labels, reordering, block_size); - if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_input), gate)) return; + if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_input), gate)) throw node_implementation_init_exception(); } // example non-ST 2110-30 audio output - + if (0 <= nmos::fields::channelmapping_port(model.settings)) { const auto id = U("outputB"); @@ -695,8 +918,386 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) routable_inputs.push_back({}); auto channelmapping_output = nmos::make_channelmapping_output(id, name, description, source_id, channel_labels, routable_inputs); - if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_output), gate)) return; + if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_output), gate)) throw node_implementation_init_exception(); + } + + // examples of using IS-12 control protocol + // they are based on the NC-DEVICE-MOCK + // See https://specs.amwa.tv/nmos-device-control-mock/#about-nc-device-mock + // See https://github.com/AMWA-TV/nmos-device-control-mock/blob/main/code/src/NCModel/Features.ts + if (0 <= nmos::fields::control_protocol_ws_port(model.settings)) + { + // example to create a non-standard Gain control class + const auto gain_control_class_id = nmos::make_nc_class_id(nmos::nc_worker_class_id, 0, { 1 }); + const web::json::field_as_number gain_value{ U("gainValue") }; + { + // Gain control class property descriptors + std::vector gain_control_property_descriptors = { nmos::experimental::make_control_class_property_descriptor(U("Gain value"), { 3, 1 }, gain_value, U("NcFloat32")) }; + + // create Gain control class descriptor + auto gain_control_class_descriptor = nmos::experimental::make_control_class_descriptor(U("Gain control class descriptor"), gain_control_class_id, U("GainControl"), gain_control_property_descriptors); + + // insert Gain control class descriptor to global state, which will be used by the control_protocol_ws_message_handler to process incoming ws message + control_protocol_state.insert(gain_control_class_descriptor); + } + // helper function to create Gain control instance + auto make_gain_control = [&gain_value, &gain_control_class_id](nmos::nc_oid oid, nmos::nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, float gain) + { + auto data = nmos::details::make_nc_worker(gain_control_class_id, oid, true, owner, role, value::string(user_label), description, touchpoints, runtime_property_constraints, true); + data[gain_value] = value::number(gain); + + return nmos::control_protocol_resource{ nmos::is12_versions::v1_0, nmos::types::nc_worker, std::move(data), true }; + }; + + // example to create a non-standard Example control class + const auto example_control_class_id = nmos::make_nc_class_id(nmos::nc_worker_class_id, 0, { 2 }); + const web::json::field_as_number enum_property{ U("enumProperty") }; + const web::json::field_as_string string_property{ U("stringProperty") }; + const web::json::field_as_number number_property{ U("numberProperty") }; + const web::json::field_as_number deprecated_number_property{ U("deprecatedNumberProperty") }; + const web::json::field_as_bool boolean_property{ U("booleanProperty") }; + const web::json::field_as_value object_property{ U("objectProperty") }; + const web::json::field_as_number method_no_args_count{ U("methodNoArgsCount") }; + const web::json::field_as_number method_simple_args_count{ U("methodSimpleArgsCount") }; + const web::json::field_as_number method_object_arg_count{ U("methodObjectArgCount") }; + const web::json::field_as_array string_sequence{ U("stringSequence") }; + const web::json::field_as_array boolean_sequence{ U("booleanSequence") }; + const web::json::field_as_array enum_sequence{ U("enumSequence") }; + const web::json::field_as_array number_sequence{ U("numberSequence") }; + const web::json::field_as_array object_sequence{ U("objectSequence") }; + const web::json::field_as_number enum_arg{ U("enumArg") }; + const web::json::field_as_string string_arg{ U("stringArg") }; + const web::json::field_as_number number_arg{ U("numberArg") }; + const web::json::field_as_bool boolean_arg{ U("booleanArg") }; + const web::json::field_as_value obj_arg{ U("objArg") }; + enum example_enum + { + Undefined = 0, + Alpha = 1, + Beta = 2, + Gamma = 3 + }; + { + // following constraints are used for the example control class level 0 datatype, level 1 property constraints and the method parameters constraints + auto make_string_example_argument_constraints = []() {return nmos::details::make_nc_parameter_constraints_string(10, U("^[a-z]+$")); }; + auto make_number_example_argument_constraints = []() {return nmos::details::make_nc_parameter_constraints_number(0, 1000, 1); }; + + // Example control class property descriptors + std::vector example_control_property_descriptors = { + nmos::experimental::make_control_class_property_descriptor(U("Example enum property"), { 3, 1 }, enum_property, U("ExampleEnum")), + // create "Example string property" with level 1: property constraints, See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Constraints.html + // use nmos::details::make_nc_parameter_constraints_string to create property constraints + nmos::experimental::make_control_class_property_descriptor(U("Example string property"), { 3, 2 }, string_property, U("NcString"), false, false, false, false, make_string_example_argument_constraints()), + // create "Example numeric property" with level 1: property constraints, See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Constraints.html + // use nmos::details::make_nc_parameter_constraints_number to create property constraints + nmos::experimental::make_control_class_property_descriptor(U("Example numeric property"), { 3, 3 }, number_property, U("NcUint64"), false, false, false, false, make_number_example_argument_constraints()), + nmos::experimental::make_control_class_property_descriptor(U("Example deprecated numeric property"), { 3, 4 }, deprecated_number_property, U("NcUint64"), false, false, false, true, make_number_example_argument_constraints()), + nmos::experimental::make_control_class_property_descriptor(U("Example boolean property"), { 3, 5 }, boolean_property, U("NcBoolean")), + nmos::experimental::make_control_class_property_descriptor(U("Example object property"), { 3, 6 }, object_property, U("ExampleDataType")), + nmos::experimental::make_control_class_property_descriptor(U("Example method no args invoke counter"), { 3, 7 }, method_no_args_count, U("NcUint64"), true), + nmos::experimental::make_control_class_property_descriptor(U("Example method simple args invoke counter"), { 3, 8 }, method_simple_args_count, U("NcUint64"), true), + nmos::experimental::make_control_class_property_descriptor(U("Example method obj arg invoke counter"), { 3, 9 }, method_object_arg_count, U("NcUint64"), true), + // create "Example sequence string property" with level 1: property constraints, See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Constraints.html + // use nmos::details::make_nc_parameter_constraints_string to create sequence property constraints + nmos::experimental::make_control_class_property_descriptor(U("Example string sequence property"), { 3, 10 }, string_sequence, U("NcString"), false, false, true, false, make_string_example_argument_constraints()), + nmos::experimental::make_control_class_property_descriptor(U("Example boolean sequence property"), { 3, 11 }, boolean_sequence, U("NcBoolean"), false, false, true), + nmos::experimental::make_control_class_property_descriptor(U("Example enum sequence property"), { 3, 12 }, enum_sequence, U("ExampleEnum"), false, false, true), + // create "Example sequence numeric property" with level 1: property constraints, See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Constraints.html + // use nmos::details::make_nc_parameter_constraints_number to create sequence property constraints + nmos::experimental::make_control_class_property_descriptor(U("Example number sequence property"), { 3, 13 }, number_sequence, U("NcUint64"), false, false, true, false, make_number_example_argument_constraints()), + nmos::experimental::make_control_class_property_descriptor(U("Example object sequence property"), { 3, 14 }, object_sequence, U("ExampleDataType"), false, false, true) + }; + + auto example_method_with_no_args = [](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + slog::log(gate, SLOG_FLF) << "Executing the example method with no arguments"; + + return nmos::details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nmos::nc_method_status::ok }); + }; + auto example_method_with_simple_args = [](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + // and the method parameters constriants has already been validated by the outer function + + slog::log(gate, SLOG_FLF) << "Executing the example method with simple arguments: " << arguments.serialize(); + + return nmos::details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nmos::nc_method_status::ok }); + }; + auto example_method_with_object_args = [](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + // and the method parameters constriants has already been validated by the outer function + + slog::log(gate, SLOG_FLF) << "Executing the example method with object argument: " << arguments.serialize(); + + return nmos::details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nmos::nc_method_status::ok }); + }; + // Example control class method descriptors + std::vector example_control_method_descriptors = + { + { nmos::experimental::make_control_class_method_descriptor(U("Example method with no arguments"), { 3, 1 }, U("MethodNoArgs"), U("NcMethodResult"), {}, false, example_method_with_no_args) }, + { nmos::experimental::make_control_class_method_descriptor(U("Example deprecated method with no arguments"), { 3, 2 }, U("MethodNoArgs"), U("NcMethodResult"), {}, true, example_method_with_no_args) }, + { nmos::experimental::make_control_class_method_descriptor(U("Example method with simple arguments"), { 3, 3 }, U("MethodSimpleArgs"), U("NcMethodResult"), + { + nmos::experimental::make_control_class_method_parameter_descriptor(U("Enum example argument"), enum_arg, U("ExampleEnum")), + nmos::experimental::make_control_class_method_parameter_descriptor(U("String example argument"), string_arg, U("NcString"), false, false, make_string_example_argument_constraints()), // e.g. include method property constraints + nmos::experimental::make_control_class_method_parameter_descriptor(U("Number example argument"), number_arg, U("NcUint64"), false, false, make_number_example_argument_constraints()), // e.g. include method property constraints + nmos::experimental::make_control_class_method_parameter_descriptor(U("Boolean example argument"), boolean_arg, U("NcBoolean")) + }, + false, example_method_with_simple_args) + }, + { nmos::experimental::make_control_class_method_descriptor(U("Example method with object argument"), { 3, 4 }, U("MethodObjectArg"), U("NcMethodResult"), + { + nmos::experimental::make_control_class_method_parameter_descriptor(U("Object example argument"), obj_arg, U("ExampleDataType")) + }, + false, example_method_with_object_args) + } + }; + + // create Example control class descriptor + auto example_control_class_descriptor = nmos::experimental::make_control_class_descriptor(U("Example control class descriptor"), example_control_class_id, U("ExampleControl"), example_control_property_descriptors, example_control_method_descriptors); + + // insert Example control class descriptor to global state, which will be used by the control_protocol_ws_message_handler to process incoming ws message + control_protocol_state.insert(example_control_class_descriptor); + + // create/insert Example datatypes to global state, which will be used by the control_protocol_ws_message_handler to process incoming ws message + auto make_example_enum_datatype = [&]() + { + using web::json::value; + + auto items = value::array(); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("Undefined"), U("Undefined"), example_enum::Undefined)); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("Alpha"), U("Alpha"), example_enum::Alpha)); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("Beta"), U("Beta"), example_enum::Beta)); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("Gamma"), U("Gamma"), example_enum::Gamma)); + return nmos::details::make_nc_datatype_descriptor_enum(U("Example enum datatype"), U("ExampleEnum"), items, value::null()); + }; + auto make_example_datatype_datatype = [&]() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Enum property example"), enum_property, U("ExampleEnum"), false, false, value::null())); + { + // level 0: datatype constraints, See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Constraints.html + // use nmos::details::make_nc_parameter_constraints_string to create datatype constraints + value datatype_constraints = make_string_example_argument_constraints(); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("String property example"), string_property, U("NcString"), false, false, datatype_constraints)); + } + { + // level 0: datatype constraints, See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Constraints.html + // use nmos::details::make_nc_parameter_constraints_number to create datatype constraints + value datatype_constraints = make_number_example_argument_constraints(); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Number property example"), number_property, U("NcUint64"), false, false, datatype_constraints)); + } + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Boolean property example"), boolean_property, U("NcBoolean"), false, false, value::null())); + return nmos::details::make_nc_datatype_descriptor_struct(U("Example data type"), U("ExampleDataType"), fields, value::null()); + }; + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ make_example_enum_datatype() }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ make_example_datatype_datatype() }); + } + // helper function to create Example datatype + auto make_example_datatype = [&](example_enum enum_property_, const utility::string_t& string_property_, uint64_t number_property_, bool boolean_property_) + { + using web::json::value_of; + + return value_of({ + { enum_property, enum_property_ }, + { string_property, string_property_ }, + { number_property, number_property_ }, + { boolean_property, boolean_property_ } + }); + }; + // helper function to create Example control instance + auto make_example_control = [&](nmos::nc_oid oid, nmos::nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const value& touchpoints, + const value& runtime_property_constraints, // level 2: runtime constraints. See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Constraints.html + // use of make_nc_property_constraints_string and make_nc_property_constraints_number to create runtime constraints + example_enum enum_property_, + const utility::string_t& string_property_, + uint64_t number_property_, + uint64_t deprecated_number_property_, + bool boolean_property_, + const value& object_property_, + uint64_t method_no_args_count_, + uint64_t method_simple_args_count_, + uint64_t method_object_arg_count_, + std::vector string_sequence_, + std::vector boolean_sequence_, + std::vector enum_sequence_, + std::vector number_sequence_, + std::vector object_sequence_) + { + auto data = nmos::details::make_nc_worker(example_control_class_id, oid, true, owner, role, value::string(user_label), description, touchpoints, runtime_property_constraints, true); + data[enum_property] = value::number(enum_property_); + data[string_property] = value::string(string_property_); + data[number_property] = value::number(number_property_); + data[deprecated_number_property] = value::number(deprecated_number_property_); + data[boolean_property] = value::boolean(boolean_property_); + data[object_property] = object_property_; + data[method_no_args_count] = value::number(method_no_args_count_); + data[method_simple_args_count] = value::number(method_simple_args_count_); + data[method_object_arg_count] = value::number(method_object_arg_count_); + { + value sequence; + for (const auto& value_ : string_sequence_) { web::json::push_back(sequence, value::string(value_)); } + data[string_sequence] = sequence; + } + { + value sequence; + for (const auto& value_ : boolean_sequence_) { web::json::push_back(sequence, value::boolean(value_)); } + data[boolean_sequence] = sequence; + } + { + value sequence; + for (const auto& value_ : enum_sequence_) { web::json::push_back(sequence, value_); } + data[enum_sequence] = sequence; + } + { + value sequence; + for (const auto& value_ : number_sequence_) { web::json::push_back(sequence, value_); } + data[number_sequence] = sequence; + } + { + value sequence; + for (const auto& value_ : object_sequence_) { web::json::push_back(sequence, value_); } + data[object_sequence] = sequence; + } + + return nmos::control_protocol_resource{ nmos::is12_versions::v1_0, nmos::types::nc_worker, std::move(data), true }; + }; + + // example to create a non-standard Temperature Sensor control class + const auto temperature_sensor_control_class_id = nmos::make_nc_class_id(nmos::nc_worker_class_id, 0, { 3 }); + const web::json::field_as_number temperature{ U("temperature") }; + const web::json::field_as_string unit{ U("uint") }; + { + // Temperature Sensor control class property descriptors + std::vector temperature_sensor_property_descriptors = { + nmos::experimental::make_control_class_property_descriptor(U("Temperature"), { 3, 1 }, temperature, U("NcFloat32"), true), + nmos::experimental::make_control_class_property_descriptor(U("Unit"), { 3, 2 }, unit, U("NcString"), true) + }; + + // create Temperature Sensor control class descriptor + auto temperature_sensor_control_class_descriptor = nmos::experimental::make_control_class_descriptor(U("Temperature Sensor control class descriptor"), temperature_sensor_control_class_id, U("TemperatureSensor"), temperature_sensor_property_descriptors); + + // insert Temperature Sensor control class descriptor to global state, which will be used by the control_protocol_ws_message_handler to process incoming ws message + control_protocol_state.insert(temperature_sensor_control_class_descriptor); + } + // helper function to create Temperature Sensor control instance + auto make_temperature_sensor = [&temperature, &unit, temperature_sensor_control_class_id](nmos::nc_oid oid, nmos::nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, float temperature_, const utility::string_t& unit_) + { + auto data = nmos::details::make_nc_worker(temperature_sensor_control_class_id, oid, true, owner, role, value::string(user_label), description, touchpoints, runtime_property_constraints, true); + data[temperature] = value::number(temperature_); + data[unit] = value::string(unit_); + + return nmos::control_protocol_resource{ nmos::is12_versions::v1_0, nmos::types::nc_worker, std::move(data), true }; + }; + + // example root block + auto root_block = nmos::make_root_block(); + + nmos::nc_oid oid = nmos::root_block_oid; + + // example device manager + auto device_manager = nmos::make_device_manager(++oid, model.settings); + + // example class manager + auto class_manager = nmos::make_class_manager(++oid, control_protocol_state); + + // example stereo gain + const auto stereo_gain_oid = ++oid; + auto stereo_gain = nmos::make_block(stereo_gain_oid, nmos::root_block_oid, U("stereo-gain"), U("Stereo gain"), U("Stereo gain block")); + + // example channel gain + const auto channel_gain_oid = ++oid; + auto channel_gain = nmos::make_block(channel_gain_oid, stereo_gain_oid, U("channel-gain"), U("Channel gain"), U("Channel gain block")); + // example left/right gains + auto left_gain = make_gain_control(++oid, channel_gain_oid, U("left-gain"), U("Left gain"), U("Left channel gain"), value::null(), value::null(), 0.0); + auto right_gain = make_gain_control(++oid, channel_gain_oid, U("right-gain"), U("Right gain"), U("Right channel gain"), value::null(), value::null(), 0.0); + // add left-gain and right-gain to channel gain + nmos::push_back(channel_gain, left_gain); + nmos::push_back(channel_gain, right_gain); + + // example master-gain + auto master_gain = make_gain_control(++oid, channel_gain_oid, U("master-gain"), U("Master gain"), U("Master gain block"), value::null(), value::null(), 0.0); + // add channel-gain and master-gain to stereo-gain + nmos::push_back(stereo_gain, channel_gain); + nmos::push_back(stereo_gain, master_gain); + + // example example-control + auto example_control = make_example_control(++oid, nmos::root_block_oid, U("ExampleControl"), U("Example control worker"), U("Example control worker"), + value::null(), + // specify the level 2: runtime constraints, see https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Constraints.html + // use of make_nc_property_constraints_string and make_nc_property_constraints_number to create runtime constraints + value_of({ + { nmos::details::make_nc_property_constraints_string({3, 2}, 5, U("^[a-z]+$")) }, + { nmos::details::make_nc_property_constraints_number({3, 3}, 10, 100, 2) } + }), + example_enum::Undefined, + U("test"), + 30, + 10, + false, + make_example_datatype(example_enum::Undefined, U("default"), 5, false), + 0, + 0, + 0, + { U("red"), U("blue"), U("green") }, + { true, false }, + { example_enum::Alpha, example_enum::Gamma }, + { 0, 50, 80 }, + { make_example_datatype(example_enum::Alpha, U("example"), 50, false), make_example_datatype(example_enum::Gamma, U("different"), 75, true) } + ); + + // example receiver-monitor(s) + { + int count = 0; + for (int index = 0; index < how_many; ++index) + { + for (const auto& port : rtp_receiver_ports) + { + const auto receiver_id = impl::make_id(seed_id, nmos::types::receiver, port, index); + + utility::ostringstream_t role; + role << U("monitor-") << ++count; + const auto& receiver = nmos::find_resource(model.node_resources, receiver_id); + const auto receiver_monitor = nmos::make_receiver_monitor(++oid, true, nmos::root_block_oid, role.str(), nmos::fields::label(receiver->data), nmos::fields::description(receiver->data), value_of({ { nmos::details::make_nc_touchpoint_nmos({nmos::ncp_nmos_resource_types::receiver, receiver_id}) } })); + + // add receiver-monitor to root-block + nmos::push_back(root_block, receiver_monitor); + } + } + } + + // example temperature-sensor + const auto temperature_sensor = make_temperature_sensor(++oid, nmos::root_block_oid, U("temperature-sensor"), U("Temperature Sensor"), U("Temperature Sensor block"), value::null(), value::null(), 0.0, U("Celsius")); + + // add temperature-sensor to root-block + nmos::push_back(root_block, temperature_sensor); + // add example-control to root-block + nmos::push_back(root_block, example_control); + // add stereo-gain to root-block + nmos::push_back(root_block, stereo_gain); + // add class-manager to root-block + nmos::push_back(root_block, class_manager); + // add device-manager to root-block + nmos::push_back(root_block, device_manager); + + // insert control protocol resources to model + insert_root_after(delay_millis, root_block, gate); } +} + +void node_implementation_run(nmos::node_model& model, slog::base_gate& gate) +{ + auto lock = model.read_lock(); + + const auto seed_id = nmos::experimental::fields::seed_id(model.settings); + const auto how_many = impl::fields::how_many(model.settings); + const auto sender_ports = impl::parse_ports(impl::fields::senders(model.settings)); + const auto ws_sender_ports = boost::copy_range>(sender_ports | boost::adaptors::filtered(impl::is_ws_port)); // start background tasks to intermittently update the state of the event sources, to cause events to be emitted to connected receivers @@ -704,11 +1305,12 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) std::shared_ptr events_engine(new std::default_random_engine(events_seeder)); auto cancellation_source = pplx::cancellation_token_source(); + auto token = cancellation_source.get_token(); - auto events = pplx::do_while([&model, seed_id, how_many, events_engine, &gate, token] + auto events = pplx::do_while([&model, seed_id, how_many, ws_sender_ports, events_engine, &gate, token] { const auto event_interval = std::uniform_real_distribution<>(0.5, 5.0)(*events_engine); - return pplx::complete_after(std::chrono::milliseconds(std::chrono::milliseconds::rep(1000 * event_interval)), token).then([&model, seed_id, how_many, events_engine, &gate] + return pplx::complete_after(std::chrono::milliseconds(std::chrono::milliseconds::rep(1000 * event_interval)), token).then([&model, seed_id, how_many, ws_sender_ports, events_engine, &gate] { auto lock = model.write_lock(); @@ -716,9 +1318,9 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) const nmos::events_number temp(175.0 + std::abs(nmos::tai_now().seconds % 100 - 50), 10); // i.e. 17.5-22.5 C - for (int index = 0; index < how_many; ++index) + for (int index = 0; 0 <= nmos::fields::events_port(model.settings) && index < how_many; ++index) { - for (const auto& port : impl::ports::ws) + for (const auto& port : ws_sender_ports) { const auto source_id = impl::make_id(seed_id, nmos::types::source, port, index); const auto flow_id = impl::make_id(seed_id, nmos::types::flow, port, index); @@ -749,6 +1351,33 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) } } + // update temperature sensor + { + const auto temperature_sensor_control_class_id = nmos::make_nc_class_id(nmos::nc_worker_class_id, 0, { 3 }); + const web::json::field_as_number temperature{ U("temperature") }; + + auto& resources = model.control_protocol_resources; + + auto found = nmos::find_resource_if(resources, nmos::types::nc_worker, [&temperature_sensor_control_class_id](const nmos::resource& resource) + { + return temperature_sensor_control_class_id == nmos::details::parse_nc_class_id(nmos::fields::nc::class_id(resource.data)); + }); + + if (resources.end() != found) + { + const auto property_changed_event = nmos::make_property_changed_event(nmos::fields::nc::oid(found->data), + { + { {3, 1}, nmos::nc_property_change_type::type::value_changed, web::json::value(temp.scaled_value()) } + }); + + nmos::modify_control_protocol_resource(model.control_protocol_resources, found->id, [&](nmos::resource& resource) + { + resource.data[temperature] = temp.scaled_value(); + + }, property_changed_event); + } + } + slog::log(gate, SLOG_FLF) << "Temperature updated: " << temp.scaled_value() << " (" << impl::temperature_Celsius.name << ")"; model.notify(); @@ -762,7 +1391,8 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) cancellation_source.cancel(); // wait without the lock since it is also used by the background tasks - nmos::details::reverse_lock_guard unlock{ lock }; + nmos::details::reverse_lock_guard unlock{ lock }; + events.wait(); } @@ -781,7 +1411,8 @@ nmos::system_global_handler make_node_implementation_system_global_handler(nmos: // in either Registration API behaviour or the senders' /transportfile endpoints until // an update to these is forced by other circumstances - web::json::merge_patch(model.settings, nmos::parse_system_global_data(system_global).second, true); + auto system_global_settings = nmos::parse_system_global_data(system_global).second; + web::json::merge_patch(model.settings, system_global_settings, true); } else { @@ -809,9 +1440,25 @@ nmos::registration_handler make_node_implementation_registration_handler(slog::b // Example Connection API callback to parse "transport_file" during a PATCH /staged request nmos::transport_file_parser make_node_implementation_transport_file_parser() { - // this example uses the default transport file parser explicitly + // this example uses a custom transport file parser to handle video/jxsv in addition to the core media types + // otherwise, it could simply return &nmos::parse_rtp_transport_file // (if this callback is specified, an 'empty' std::function is not allowed) - return &nmos::parse_rtp_transport_file; + return [](const nmos::resource& receiver, const nmos::resource& connection_receiver, const utility::string_t& transport_file_type, const utility::string_t& transport_file_data, slog::base_gate& gate) + { + const auto validate_sdp_parameters = [](const web::json::value& receiver, const nmos::sdp_parameters& sdp_params) + { + if (nmos::media_types::video_jxsv == nmos::get_media_type(sdp_params)) + { + nmos::validate_video_jxsv_sdp_parameters(receiver, sdp_params); + } + else + { + // validate core media types, i.e., "video/raw", "audio/L", "video/smpte291" and "video/SMPTE2022-6" + nmos::validate_sdp_parameters(receiver, sdp_params); + } + }; + return nmos::details::parse_rtp_transport_file(validate_sdp_parameters, receiver, connection_receiver, transport_file_type, transport_file_data, gate); + }; } // Example Connection API callback to perform application-specific validation of the merged /staged endpoint during a PATCH /staged request @@ -830,29 +1477,33 @@ nmos::connection_resource_auto_resolver make_node_implementation_auto_resolver(c const auto seed_id = nmos::experimental::fields::seed_id(settings); const auto device_id = impl::make_id(seed_id, nmos::types::device); const auto how_many = impl::fields::how_many(settings); - const auto rtp_sender_ids = impl::make_ids(seed_id, nmos::types::sender, impl::ports::rtp, how_many); - const auto ws_sender_ids = impl::make_ids(seed_id, nmos::types::sender, impl::ports::ws, how_many); + const auto rtp_sender_ports = boost::copy_range>(impl::parse_ports(impl::fields::senders(settings)) | boost::adaptors::filtered(impl::is_rtp_port)); + const auto rtp_sender_ids = impl::make_ids(seed_id, nmos::types::sender, rtp_sender_ports, how_many); + const auto ws_sender_ports = boost::copy_range>(impl::parse_ports(impl::fields::senders(settings)) | boost::adaptors::filtered(impl::is_ws_port)); + const auto ws_sender_ids = impl::make_ids(seed_id, nmos::types::sender, ws_sender_ports, how_many); const auto ws_sender_uri = nmos::make_events_ws_api_connection_uri(device_id, settings); - const auto rtp_receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, impl::ports::rtp, how_many); - const auto ws_receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, impl::ports::ws, how_many); + const auto rtp_receiver_ports = boost::copy_range>(impl::parse_ports(impl::fields::receivers(settings)) | boost::adaptors::filtered(impl::is_rtp_port)); + const auto rtp_receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, rtp_receiver_ports, how_many); + const auto ws_receiver_ports = boost::copy_range>(impl::parse_ports(impl::fields::receivers(settings)) | boost::adaptors::filtered(impl::is_ws_port)); + const auto ws_receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, ws_receiver_ports, how_many); // although which properties may need to be defaulted depends on the resource type, // the default value will almost always be different for each resource return [rtp_sender_ids, rtp_receiver_ids, ws_sender_ids, ws_sender_uri, ws_receiver_ids](const nmos::resource& resource, const nmos::resource& connection_resource, value& transport_params) { const std::pair id_type{ connection_resource.id, connection_resource.type }; - // this code relies on the specific constraints added by nmos_implementation_thread + // this code relies on the specific constraints added by node_implementation_thread const auto& constraints = nmos::fields::endpoint_constraints(connection_resource.data); // "In some cases the behaviour is more complex, and may be determined by the vendor." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/docs/2.2.%20APIs%20-%20Server%20Side%20Implementation.md#use-of-auto + // See https://specs.amwa.tv/is-05/releases/v1.0.0/docs/2.2._APIs_-_Server_Side_Implementation.html#use-of-auto if (rtp_sender_ids.end() != boost::range::find(rtp_sender_ids, id_type.first)) { const bool smpte2022_7 = 1 < transport_params.size(); nmos::details::resolve_auto(transport_params[0], nmos::fields::source_ip, [&] { return web::json::front(nmos::fields::constraint_enum(constraints.at(0).at(nmos::fields::source_ip))); }); if (smpte2022_7) nmos::details::resolve_auto(transport_params[1], nmos::fields::source_ip, [&] { return web::json::back(nmos::fields::constraint_enum(constraints.at(1).at(nmos::fields::source_ip))); }); - nmos::details::resolve_auto(transport_params[0], nmos::fields::destination_ip, [] { return value::string(U("239.255.255.0")); }); - if (smpte2022_7) nmos::details::resolve_auto(transport_params[1], nmos::fields::destination_ip, [] { return value::string(U("239.255.255.1")); }); + nmos::details::resolve_auto(transport_params[0], nmos::fields::destination_ip, [&] { return value::string(impl::make_source_specific_multicast_address_v4(id_type.first, 0)); }); + if (smpte2022_7) nmos::details::resolve_auto(transport_params[1], nmos::fields::destination_ip, [&] { return value::string(impl::make_source_specific_multicast_address_v4(id_type.first, 1)); }); // lastly, apply the specification defaults for any properties not handled above nmos::resolve_rtp_auto(id_type.second, transport_params); } @@ -884,9 +1535,11 @@ nmos::connection_sender_transportfile_setter make_node_implementation_transportf const auto seed_id = nmos::experimental::fields::seed_id(settings); const auto node_id = impl::make_id(seed_id, nmos::types::node); const auto how_many = impl::fields::how_many(settings); - const auto rtp_source_ids = impl::make_ids(seed_id, nmos::types::source, impl::ports::rtp, how_many); - const auto rtp_flow_ids = impl::make_ids(seed_id, nmos::types::flow, impl::ports::rtp, how_many); - const auto rtp_sender_ids = impl::make_ids(seed_id, nmos::types::sender, impl::ports::rtp, how_many); + const auto sender_ports = impl::parse_ports(impl::fields::senders(settings)); + const auto rtp_sender_ports = boost::copy_range>(sender_ports | boost::adaptors::filtered(impl::is_rtp_port)); + const auto rtp_source_ids = impl::make_ids(seed_id, nmos::types::source, rtp_sender_ports, how_many); + const auto rtp_flow_ids = impl::make_ids(seed_id, nmos::types::flow, rtp_sender_ports, how_many); + const auto rtp_sender_ids = impl::make_ids(seed_id, nmos::types::sender, rtp_sender_ports, how_many); // as part of activation, the example sender /transportfile should be updated based on the active transport parameters return [&node_resources, node_id, rtp_source_ids, rtp_flow_ids, rtp_sender_ids](const nmos::resource& sender, const nmos::resource& connection_sender, value& endpoint_transportfile) @@ -907,11 +1560,54 @@ nmos::connection_sender_transportfile_setter make_node_implementation_transportf throw std::logic_error("matching IS-04 node, source or flow not found"); } - auto sdp_params = nmos::make_sdp_parameters(node->data, source->data, flow->data, sender.data, { U("PRIMARY"), U("SECONDARY") }); - if (sdp_params.audio.channel_count != 0) + // the nmos::make_sdp_parameters overload from the IS-04 resources provides a high-level interface + // for common "video/raw", "audio/L", "video/smpte291" and "video/SMPTE2022-6" use cases + //auto sdp_params = nmos::make_sdp_parameters(node->data, source->data, flow->data, sender.data, { U("PRIMARY"), U("SECONDARY") }); + + // nmos::make_{video,audio,data,mux}_sdp_parameters provide a little more flexibility for those four media types + // and the combination of nmos::make_{video_raw,audio_L,video_smpte291,video_SMPTE2022_6}_parameters + // with the related make_sdp_parameters overloads provides the most flexible and extensible approach + auto sdp_params = [&] { - sdp_params.audio.packet_time = sdp_params.audio.channel_count > 8 ? 0.125 : 1; - } + const std::vector mids{ U("PRIMARY"), U("SECONDARY") }; + const nmos::format format{ nmos::fields::format(flow->data) }; + if (nmos::formats::video == format) + { + const nmos::media_type video_type{ nmos::fields::media_type(flow->data) }; + if (nmos::media_types::video_raw == video_type) + { + return nmos::make_video_sdp_parameters(node->data, source->data, flow->data, sender.data, nmos::details::payload_type_video_default, mids, {}, sdp::type_parameters::type_N); + } + else if (nmos::media_types::video_jxsv == video_type) + { + const auto params = nmos::make_video_jxsv_parameters(node->data, source->data, flow->data, sender.data); + const auto ts_refclk = nmos::details::make_ts_refclk(node->data, source->data, sender.data, {}); + return nmos::make_sdp_parameters(nmos::fields::label(sender.data), params, nmos::details::payload_type_video_default, mids, ts_refclk); + } + else + { + throw std::logic_error("unexpected flow media_type"); + } + } + else if (nmos::formats::audio == format) + { + // this example application doesn't actually stream, so just indicate a sensible value for packet time + const double packet_time = nmos::fields::channels(source->data).size() > 8 ? 0.125 : 1; + return nmos::make_audio_sdp_parameters(node->data, source->data, flow->data, sender.data, nmos::details::payload_type_audio_default, mids, {}, packet_time); + } + else if (nmos::formats::data == format) + { + return nmos::make_data_sdp_parameters(node->data, source->data, flow->data, sender.data, nmos::details::payload_type_data_default, mids, {}, {}); + } + else if (nmos::formats::mux == format) + { + return nmos::make_mux_sdp_parameters(node->data, source->data, flow->data, sender.data, nmos::details::payload_type_mux_default, mids, {}, sdp::type_parameters::type_N); + } + else + { + throw std::logic_error("unexpected flow format"); + } + }(); auto& transport_params = nmos::fields::transport_params(nmos::fields::endpoint_active(connection_sender.data)); auto session_description = nmos::make_session_description(sdp_params, transport_params); @@ -926,15 +1622,17 @@ nmos::events_ws_message_handler make_node_implementation_events_ws_message_handl { const auto seed_id = nmos::experimental::fields::seed_id(model.settings); const auto how_many = impl::fields::how_many(model.settings); - const auto receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, impl::ports::ws, how_many); + const auto receiver_ports = impl::parse_ports(impl::fields::receivers(model.settings)); + const auto ws_receiver_ports = boost::copy_range>(receiver_ports | boost::adaptors::filtered(impl::is_ws_port)); + const auto ws_receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, ws_receiver_ports, how_many); // the message handler will be used for all Events WebSocket connections, and each connection may potentially // have subscriptions to a number of sources, for multiple receivers, so this example uses a handler adaptor // that enables simple processing of "state" messages (events) per receiver - return nmos::experimental::make_events_ws_message_handler(model, [receiver_ids, &gate](const nmos::resource& receiver, const nmos::resource& connection_receiver, const web::json::value& message) + return nmos::experimental::make_events_ws_message_handler(model, [ws_receiver_ids, &gate](const nmos::resource& receiver, const nmos::resource& connection_receiver, const web::json::value& message) { - const auto found = boost::range::find(receiver_ids, connection_receiver.id); - if (receiver_ids.end() != found) + const auto found = boost::range::find(ws_receiver_ids, connection_receiver.id); + if (ws_receiver_ids.end() != found) { const auto event_type = nmos::event_type(nmos::fields::state_event_type(message)); const auto& payload = nmos::fields::state_payload(message); @@ -959,19 +1657,24 @@ nmos::events_ws_message_handler make_node_implementation_events_ws_message_handl // Example Connection API activation callback to perform application-specific operations to complete activation nmos::connection_activation_handler make_node_implementation_connection_activation_handler(nmos::node_model& model, slog::base_gate& gate) { + auto handle_load_ca_certificates = nmos::make_load_ca_certificates_handler(model.settings, gate); // this example uses this callback to (un)subscribe a IS-07 Events WebSocket receiver when it is activated // and, in addition to the message handler, specifies the optional close handler in order that any subsequent // connection errors are reflected into the /active endpoint by setting master_enable to false auto handle_events_ws_message = make_node_implementation_events_ws_message_handler(model, gate); auto handle_close = nmos::experimental::make_events_ws_close_handler(model, gate); - auto connection_events_activation_handler = nmos::make_connection_events_websocket_activation_handler(handle_events_ws_message, handle_close, model.settings, gate); + auto connection_events_activation_handler = nmos::make_connection_events_websocket_activation_handler(handle_load_ca_certificates, handle_events_ws_message, handle_close, model.settings, gate); + // this example uses this callback to update IS-12 Receiver-Monitor connection status + auto receiver_monitor_connection_activation_handler = nmos::make_receiver_monitor_connection_activation_handler(model.control_protocol_resources); - return [connection_events_activation_handler, &gate](const nmos::resource& resource, const nmos::resource& connection_resource) + return [connection_events_activation_handler, receiver_monitor_connection_activation_handler, &gate](const nmos::resource& resource, const nmos::resource& connection_resource) { const std::pair id_type{ resource.id, resource.type }; slog::log(gate, SLOG_FLF) << nmos::stash_category(impl::categories::node_implementation) << "Activating " << id_type; connection_events_activation_handler(resource, connection_resource); + + receiver_monitor_connection_activation_handler(connection_resource); }; } @@ -993,8 +1696,71 @@ nmos::channelmapping_activation_handler make_node_implementation_channelmapping_ }; } +// Example Control Protocol WebSocket API property changed callback to perform application-specific operations to complete the property changed +nmos::control_protocol_property_changed_handler make_node_implementation_control_protocol_property_changed_handler(slog::base_gate& gate) +{ + return [&gate](const nmos::resource& resource, const utility::string_t& property_name, int index) + { + if (index >= 0) + { + // sequence property + slog::log(gate, SLOG_FLF) << nmos::stash_category(impl::categories::node_implementation) << "Property: " << property_name << " index " << index << " has value changed to " << resource.data.at(property_name).at(index).serialize(); + } + else + { + // non-sequence property + slog::log(gate, SLOG_FLF) << nmos::stash_category(impl::categories::node_implementation) << "Property: " << property_name << " has value changed to " << resource.data.at(property_name).serialize(); + } + }; +} + namespace impl { + nmos::interlace_mode get_interlace_mode(const nmos::settings& settings) + { + if (settings.has_field(impl::fields::interlace_mode)) + { + return nmos::interlace_mode{ impl::fields::interlace_mode(settings) }; + } + // for the default, 1080i50 and 1080i59.94 are arbitrarily preferred to 1080p25 and 1080p29.97 + // for 1080i formats, ST 2110-20 says that "the fields of an interlaced image are transmitted in time order, + // first field first [and] the sample rows of the temporally second field are displaced vertically 'below' the + // like-numbered sample rows of the temporally first field." + const auto frame_rate = nmos::parse_rational(impl::fields::frame_rate(settings)); + const auto frame_height = impl::fields::frame_height(settings); + return (nmos::rates::rate25 == frame_rate || nmos::rates::rate29_97 == frame_rate) && 1080 == frame_height + ? nmos::interlace_modes::interlaced_tff + : nmos::interlace_modes::progressive; + } + + bool is_rtp_port(const impl::port& port) + { + return impl::ports::rtp.end() != boost::range::find(impl::ports::rtp, port); + } + + bool is_ws_port(const impl::port& port) + { + return impl::ports::ws.end() != boost::range::find(impl::ports::ws, port); + } + + std::vector parse_ports(const web::json::value& value) + { + if (value.is_null()) return impl::ports::all; + return boost::copy_range>(value.as_array() | boost::adaptors::transformed([&](const web::json::value& value) + { + return port{ value.as_string() }; + })); + } + + // find interface with the specified address + std::vector::const_iterator find_interface(const std::vector& interfaces, const utility::string_t& address) + { + return boost::range::find_if(interfaces, [&](const web::hosts::experimental::host_interface& interface) + { + return interface.addresses.end() != boost::range::find(interface.addresses, address); + }); + } + // generate repeatable ids for the example node's resources nmos::id make_id(const nmos::id& seed_id, const nmos::type& type, const impl::port& port, int index) { @@ -1020,15 +1786,62 @@ namespace impl return ids; } + std::vector make_ids(const nmos::id& seed_id, const std::vector& types, const std::vector& ports, int how_many) + { + // hm, boost::range::combine arrived in Boost 1.56.0 + std::vector ids; + for (const auto& type : types) + { + boost::range::push_back(ids, make_ids(seed_id, type, ports, how_many)); + } + return ids; + } + + // generate a repeatable source-specific multicast address for each leg of a sender + utility::string_t make_source_specific_multicast_address_v4(const nmos::id& id, int leg) + { + // hash the pseudo-random id and leg to generate the address + const auto s = id + U('/') + utility::conversions::details::to_string_t(leg); + const auto h = std::hash{}(s); + auto a = boost::asio::ip::address_v4(uint32_t(h)).to_bytes(); + // ensure the address is in the source-specific multicast block reserved for local host allocation, 232.0.1.0-232.255.255.255 + // see https://www.iana.org/assignments/multicast-addresses/multicast-addresses.xhtml#multicast-addresses-10 + a[0] = 232; + a[2] |= 1; + return utility::s2us(boost::asio::ip::address_v4(a).to_string()); + } + + // add a selection of parents to a source or flow + void insert_parents(nmos::resource& resource, const nmos::id& seed_id, const port& port, int index) + { + // algorithm to produce signal ancestry with a range of depths and breadths + // see https://github.com/sony/nmos-cpp/issues/312#issuecomment-1335641637 + int b = 0; + while (index & (1 << b)) ++b; + if (!b) return; + index &= ~(1 << (b - 1)); + do + { + index &= ~(1 << b); + web::json::push_back(resource.data[nmos::fields::parents], impl::make_id(seed_id, resource.type, port, index)); + ++b; + } while (index & (1 << b)); + } + // add a helpful suffix to the label of a sub-resource for the example node - void set_label(nmos::resource& resource, const impl::port& port, int index) + void set_label_description(nmos::resource& resource, const impl::port& port, int index) { using web::json::value; auto label = nmos::fields::label(resource.data); if (!label.empty()) label += U('/'); label += resource.type.name + U('/') + port.name + utility::conversions::details::to_string_t(index); - resource.data[nmos::fields::label] = resource.data[nmos::fields::description] = value::string(label); + resource.data[nmos::fields::label] = value::string(label); + + auto description = nmos::fields::description(resource.data); + if (!description.empty()) description += U('/'); + description += resource.type.name + U('/') + port.name + utility::conversions::details::to_string_t(index); + resource.data[nmos::fields::description] = value::string(description); } // add an example "natural grouping" hint to a sender or receiver @@ -1043,6 +1856,9 @@ namespace impl nmos::experimental::node_implementation make_node_implementation(nmos::node_model& model, slog::base_gate& gate) { return nmos::experimental::node_implementation() + .on_load_server_certificates(nmos::make_load_server_certificates_handler(model.settings, gate)) + .on_load_dh_param(nmos::make_load_dh_param_handler(model.settings, gate)) + .on_load_ca_certificates(nmos::make_load_ca_certificates_handler(model.settings, gate)) .on_system_changed(make_node_implementation_system_global_handler(model, gate)) // may be omitted if not required .on_registration_changed(make_node_implementation_registration_handler(gate)) // may be omitted if not required .on_parse_transport_file(make_node_implementation_transport_file_parser()) // may be omitted if the default is sufficient @@ -1051,5 +1867,6 @@ nmos::experimental::node_implementation make_node_implementation(nmos::node_mode .on_set_transportfile(make_node_implementation_transportfile_setter(model.node_resources, model.settings)) .on_connection_activated(make_node_implementation_connection_activation_handler(model, gate)) .on_validate_channelmapping_output_map(make_node_implementation_map_validator()) // may be omitted if not required - .on_channelmapping_activated(make_node_implementation_channelmapping_activation_handler(gate)); + .on_channelmapping_activated(make_node_implementation_channelmapping_activation_handler(gate)) + .on_control_protocol_property_changed(make_node_implementation_control_protocol_property_changed_handler(gate)); // may be omitted if IS-12 not required } diff --git a/Development/nmos-cpp-node/node_implementation.h b/Development/nmos-cpp-node/node_implementation.h index 781a81e88..3c6b295c3 100644 --- a/Development/nmos-cpp-node/node_implementation.h +++ b/Development/nmos-cpp-node/node_implementation.h @@ -13,13 +13,14 @@ namespace nmos namespace experimental { struct node_implementation; + struct control_protocol_state; } } // This is an example of how to integrate the nmos-cpp library with a device-specific underlying implementation. // It constructs and inserts a node resource and some sub-resources into the model, based on the model settings, -// starts background tasks to emit regular events from the temperature event source and then waits for shutdown. -void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate); +// starts background tasks to emit regular events from the temperature event source, and then waits for shutdown. +void node_implementation_thread(nmos::node_model& model, nmos::experimental::control_protocol_state& control_protocol_state, slog::base_gate& gate); // This constructs all the callbacks used to integrate the example device-specific underlying implementation // into the server instance for the NMOS Node. diff --git a/Development/nmos-cpp-registry/config.json b/Development/nmos-cpp-registry/config.json index 154f7cbb5..4b4a3e89b 100644 --- a/Development/nmos-cpp-registry/config.json +++ b/Development/nmos-cpp-registry/config.json @@ -29,17 +29,33 @@ //"host_addresses": array-of-ip-address-strings, // is04_versions [registry, node]: used to specify the enabled API versions (advertised via 'api_ver') for a version-locked configuration - //"is04_versions": ["v1.1", "v1.2"], + //"is04_versions": ["v1.2", "v1.3"], // is09_versions [registry, node]: used to specify the enabled API versions (advertised via 'api_ver') for a version-locked configuration //"is09_versions": ["v1.0"], + // is10_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration + //"is10_versions": ["v1.0"], + // pri [registry, node]: used for the 'pri' TXT record; specifying nmos::service_priorities::no_priority (maximum value) disables advertisement completely //"pri": 100, + // authorization_highest_pri, authorization_lowest_pri [registry, node]: used to specify the (inclusive) range of suitable 'pri' values of discovered Authorization APIs, to avoid development and live systems colliding + //"authorization_highest_pri": 0, + //"authorization_lowest_pri": 2147483647, + + // discovery_backoff_min/discovery_backoff_max/discovery_backoff_factor [registry, node]: used to back-off after errors interacting with all discoverable service instances + // e.g. Registration APIs, System APIs, Authorization APIs, or OCSP servers + //"discovery_backoff_min": 1, + //"discovery_backoff_max": 30, + //"discovery_backoff_factor": 1.5, + + // service_name_prefix [registry, node]: used as a prefix in the advertised service names ("__:", e.g. "nmos-cpp_node_127-0-0-1:3212") + //"service_name_prefix": "nmos-cpp" + // port numbers [registry, node]: ports to which clients should connect for each API - // http_port [registry, node]: if specified, used in preference to the individual defaults for each HTTP API + // http_port [registry, node]: if specified, this becomes the default port for each HTTP API and the next higher port becomes the default for each WebSocket API //"http_port": 0, //"query_port": 3211, @@ -51,9 +67,16 @@ // listen_backlog [registry, node]: the maximum length of the queue of pending connections, or zero for the implementation default (the implementation may not honour this value) //"listen_backlog": 0, + // registration_heartbeat_interval [registry, node]: + // [registry]: used in System API resource is04 object's heartbeat_interval field + // "Constants related to the AMWA IS-04 Discovery and Registration Specification are contained in the is04 object. + // heartbeat_interval defines how often Nodes should perform a heartbeat to maintain their resources in the Registration API." + // See https://specs.amwa.tv/is-09/releases/v1.0.0/docs/4.2._Behaviour_-_Global_Configuration_Parameters.html#amwa-is-04-nmos-discovery-and-registration-parameters + //"registration_heartbeat_interval": 5, + // registration_expiry_interval [registry]: // "Registration APIs should use a garbage collection interval of 12 seconds by default (triggered just after two failed heartbeats at the default 5 second interval)." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md#heartbeating + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#heartbeating //"registration_expiry_interval": 12, // query_paging_default/query_paging_limit [registry]: default/maximum number of results per "page" when using the Query API (a client may request a lower limit) @@ -71,9 +94,12 @@ // seed id [registry, node]: optional, used to generate repeatable id values when running with the same configuration //"seed_id": uuid-string, - // label [registry, node]: used in resource description/label fields + // label [registry, node]: used in resource label field //"label": "", + // description [registry, node]: used in resource description field + //"description": "", + // registration_available [registry]: used to flag the Registration API as temporarily unavailable //"registration_available": true, @@ -93,17 +119,26 @@ //"mdns_port": 3208, //"schemas_port": 3208, - // addresses [registry, node]: addresses on which to listen for each API, or empty string for the wildcard address + // addresses [registry, node]: IP addresses on which to listen for each API, or empty string for the wildcard address + + // server_address [registry, node]: if specified, this becomes the default address on which to listen for each API instead of the wildcard address + //"server_address": "", + + // addresses [registry, node]: IP addresses on which to listen for specific APIs //"settings_address": "127.0.0.1", //"logging_address": "", - // addresses [registry]: addresses on which to listen for each API, or empty string for the wildcard address + // addresses [registry]: IP addresses on which to listen for specific APIs //"admin_address": "", //"mdns_address": "", //"schemas_address": "", + // client_address [registry, node]: IP address of the network interface to bind client connections + // for now, only supporting HTTP/HTTPS client connections on Linux + //"client_address": "", + // query_ws_paging_default/query_ws_paging_limit [registry]: default/maximum number of events per message when using the Query WebSocket API (a client may request a lower limit) //"query_ws_paging_default": 10, //"query_ws_paging_limit": 100, @@ -145,14 +180,15 @@ // when true, server certificates etc. must also be configured //"server_secure": false, - // private_key_files [registry, node]: full paths of private key files in PEM format - //"private_key_files": ["server-ecdsa-key.pem", "server-rsa-key.pem"], - - // certificate_chain_files [registry, node]: full paths of server certificate chain files which must be in PEM format and must be sorted + // server_certificates [registry, node]: an array of server certificate objects, each has the name of the key algorithm, the full paths of private key file and certificate chain file + // each value must be an object like { "key_algorithm": "ECDSA", "private_key_file": "server-ecdsa-key.pem", "certificate_chain_file": "server-ecdsa-chain.pem" } + // key_algorithm (attribute of server_certificates objects): name of the key algorithm for the certificate, see nmos::key_algorithm + // private_key_file (attribute of server_certificates objects): full path of private key file in PEM format + // certificate_chain_file (attribute of server_certificates object): full path of certificate chain file in PEM format, which must be sorted // starting with the server's certificate, followed by any intermediate CA certificates, and ending with the highest level (root) CA // on Windows, if C++ REST SDK is built with CPPREST_HTTP_LISTENER_IMPL=httpsys (reported as "listener=httpsys" by nmos::get_build_settings_info) // one of the certificates must also be bound to each port e.g. using 'netsh add sslcert' - //"certificate_chain_files": ["server-ecdsa-chain.pem", "server-rsa-chain.pem"], + //"server_certificates": [{"key_algorithm": "ECDSA", "private_key_file": "server-ecdsa-key.pem", "certificate_chain_file": "server-ecdsa-chain.pem"}, {"key_algorithm": "RSA", "private_key_file": "server-rsa-key.pem", "certificate_chain_file": "server-rsa-chain.pem"}], // validate_certificates [registry, node]: boolean value, false (ignore all server certificate validation errors), or true (do not ignore, the default behaviour) //"validate_certificates": true, @@ -160,5 +196,88 @@ // dh_param_file [registry, node]: Diffie-Hellman parameters file in PEM format for ephemeral key exchange support, or empty string for no support //"dh_param_file": "dhparam.pem", + // system_label [registry]: used in System API resource label field + //"system_label": "", + + // system_description [registry]: used in System API resource description field + //"system_description": "", + + // system_tags [registry]: used in System API resource tags field + // "Each tag has a single key, but MAY have multiple values." + // { + // "tag_1": [ "tag_1_value_1", "tag_1_value_2" ], + // "tag_2": [ "tag_2_value_1" ] + // } + // See https://specs.amwa.tv/is-09/releases/v1.0.0/docs/2.1._APIs_-_Common_Keys.html#tags + //"system_tags": {}, + + // "syslog contains hostname and port for the system's syslog "version 1" server using the UDP transport (IETF RFC 5246)" + // See https://specs.amwa.tv/is-09/releases/v1.0.0/docs/4.2._Behaviour_-_Global_Configuration_Parameters.html#syslog-parameters + + // system_syslog_hostname [registry]: the fully-qualified host name or the IP address of the system's syslog "version 1" server + //"system_syslog_hostname": "", + + // system_syslog_port [registry]: the port number for the system's syslog "version 1" server + //"system_syslog_port": 514, + + // "syslogv2 contains hostname and port for the system's syslog "version 2" server using the TLS transport (IETF RFC 5245)" + // See https://specs.amwa.tv/is-09/releases/v1.0.0/docs/4.2._Behaviour_-_Global_Configuration_Parameters.html#syslog-parameters + + // system_syslogv2_hostname [registry]: the fully-qualified host name or the IP address of the system's syslog "version 2" server + //"system_syslogv2_hostname": "", + + // system_syslogv2_port [registry]: the port number for the system's syslog "version 2" server + //"system_syslogv2_port": 6514, + + // hsts_max_age [registry, node]: the HTTP Strict-Transport-Security response header's max-age value; default is approximately 365 days + // (the header is omitted if server_secure is false, or hsts_max_age is negative) + // See https://tools.ietf.org/html/rfc6797#section-6.1.1 + //"hsts_max_age": 31536000, + + // hsts_include_sub_domains [registry, node]: the HTTP Strict-Transport-Security HTTP response header's includeSubDomains value + // See https://tools.ietf.org/html/rfc6797#section-6.1.2 + //"hsts_include_sub_domains": false, + + // ocsp_interval_min/ocsp_interval_max [registry, node]: used to poll for certificate status (OCSP) changes; default is about one hour + // Note that if half of the server certificate expiry time is shorter, then the ocsp_interval_min/max will be overridden by it + //"ocsp_interval_min": 3600, + //"ocsp_interval_max": 3660, + + // ocsp_request_max [registry, node]: timeout for interactions with the OCSP server + //"ocsp_request_max": 30, + + // authorization_address [registry, node]: IP address or host name used to construct request URLs for the Authorization API (if not discovered via DNS-SD) + //"authorization_address": ip-address-string, + + // authorization_port [registry, node]: used to construct request URLs for the authorization server's Authorization API (if not discovered via DNS-SD) + //"authorization_port" 443, + + // authorization_version [registry, node]: used to construct request URLs for Authorization API (if not discovered via DNS-SD) + //"authorization_version": "v1.0", + + // authorization_selector [registry, node]: used to construct request URLs for the authorization API (if not discovered via DNS-SD) + //"authorization_selector", "", + + // authorization_request_max [registry, node]: timeout for interactions with the Authorization API /certs & /token endpoints + //"authorization_request_max": 30, + + // fetch_authorization_public_keys_interval_min/fetch_authorization_public_keys_interval_max [registry, node]: used to poll for Authorization API public keys changes; default is about one hour + // "Resource Servers (Nodes) SHOULD seek to fetch public keys from the Authorization Server at least once every hour. Resource Servers MUST vary their retrieval + // interval at random by up to at least one minute to avoid overloading the Authorization Server due to Resource Servers synchronising their retrieval time." + // See https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.1._Behaviour_-_Authorization_Servers.html#authorization-server-public-keys + //"fetch_authorization_public_keys_interval_min": 3600, + //"fetch_authorization_public_keys_interval_max": 3660, + + // server_authorization [registry, node]: whether server should use authorization to protect its APIs + //"server_authorization": false, + + // retry_after [registry, node]: used to specify the HTTP Retry-After header to indicate the number of seconds when the client may retry its request again, default to 5 seconds + // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss + // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it + // MAY temporarily respond with an HTTP 503 code in order to avoid blocking the incoming authorized request. When a HTTP 503 code is used, the Resource + // Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request. + // If the Resource Server fails to verify a token using all public keys available it MUST reject the token." + //"service_unavailable_retry_after": 5, + "don't worry": "about trailing commas" } diff --git a/Development/nmos-cpp-registry/main.cpp b/Development/nmos-cpp-registry/main.cpp index 052ed235c..a98b253b7 100644 --- a/Development/nmos-cpp-registry/main.cpp +++ b/Development/nmos-cpp-registry/main.cpp @@ -1,10 +1,16 @@ #include #include +#include "nmos/authorization_behaviour.h" +#include "nmos/authorization_state.h" #include "nmos/log_gate.h" #include "nmos/model.h" +#include "nmos/ocsp_behaviour.h" +#include "nmos/ocsp_response_handler.h" +#include "nmos/ocsp_state.h" #include "nmos/process_utils.h" #include "nmos/registry_server.h" #include "nmos/server.h" +#include "registry_implementation.h" int main(int argc, char* argv[]) { @@ -89,9 +95,37 @@ int main(int argc, char* argv[]) slog::log(gate, SLOG_FLF) << "Build settings: " << nmos::get_build_settings_info(); slog::log(gate, SLOG_FLF) << "Initial settings: " << registry_model.settings.serialize(); + // Set up the callbacks between the registry server and the underlying implementation + + auto registry_implementation = make_registry_implementation(registry_model, gate); + +// only implement communication with OCSP server if http_listener supports OCSP stapling +// cf. preprocessor conditions in nmos::make_http_listener_config +// Note: the get_ocsp_response callback must be set up before executing the make_registry_server where make_http_listener_config is set up +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) + nmos::experimental::ocsp_state ocsp_state; + if (nmos::experimental::fields::server_secure(registry_model.settings)) + { + registry_implementation + .on_get_ocsp_response(nmos::make_ocsp_response_handler(ocsp_state, gate)); + } +#endif + + // only configure communication with Authorization server if IS-10/BCP-003-02 is required + // Note: + // the validate_authorization callback must be set up before executing the make_node_server where make_node_api, make_connection_api, make_events_api, and make_channelmapping_api are set up + // the ws_validate_authorization callback must be set up before executing the make_node_server where make_events_ws_validate_handler is set up + nmos::experimental::authorization_state authorization_state; + if (nmos::experimental::fields::server_authorization(registry_model.settings)) + { + registry_implementation + .on_validate_authorization(nmos::experimental::make_validate_authorization_handler(registry_model, authorization_state, nmos::experimental::make_validate_authorization_token_handler(authorization_state, gate), gate)) + .on_ws_validate_authorization(nmos::experimental::make_ws_validate_authorization_handler(registry_model, authorization_state, nmos::experimental::make_validate_authorization_token_handler(authorization_state, gate), gate)); + } + // Set up the registry server - auto registry_server = nmos::experimental::make_registry_server(registry_model, log_model, gate); + auto registry_server = nmos::experimental::make_registry_server(registry_model, registry_implementation, log_model, gate); if (!nmos::experimental::fields::http_trace(registry_model.settings)) { @@ -103,6 +137,27 @@ int main(int argc, char* argv[]) } } + // Add the underlying implementation + +// only implement communication with OCSP server if http_listener supports OCSP stapling +// cf. preprocessor conditions in nmos::make_http_listener_config +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) + if (nmos::experimental::fields::server_secure(registry_model.settings)) + { + auto load_ca_certificates = registry_implementation.load_ca_certificates; + auto load_server_certificates = registry_implementation.load_server_certificates; + registry_server.thread_functions.push_back([&, load_ca_certificates, load_server_certificates] { nmos::ocsp_behaviour_thread(registry_model, ocsp_state, load_ca_certificates, load_server_certificates, gate); }); + } +#endif + + // only configure communication with Authorization server if IS-10/BCP-003-02 is required + if (nmos::experimental::fields::server_authorization(registry_model.settings)) + { + auto load_ca_certificates = registry_implementation.load_ca_certificates; + registry_server.thread_functions.push_back([&, load_ca_certificates] { authorization_behaviour_thread(registry_model, authorization_state, load_ca_certificates, {}, {}, {}, {}, gate); }); + registry_server.thread_functions.push_back([&, load_ca_certificates] { authorization_token_issuer_thread(registry_model, authorization_state, load_ca_certificates, gate); }); + } + // Open the API ports and start up registry management slog::log(gate, SLOG_FLF) << "Preparing for connections"; diff --git a/Development/nmos-cpp-registry/registry_implementation.cpp b/Development/nmos-cpp-registry/registry_implementation.cpp new file mode 100644 index 000000000..91e0f89de --- /dev/null +++ b/Development/nmos-cpp-registry/registry_implementation.cpp @@ -0,0 +1,25 @@ +#include "registry_implementation.h" + +#include "nmos/model.h" +#include "nmos/registry_server.h" +#include "nmos/slog.h" + +// example registry implementation details +namespace impl +{ + // custom logging category for the example registry implementation thread + namespace categories + { + const nmos::category registry_implementation{ "registry_implementation" }; + } +} + +// This constructs all the callbacks used to integrate the example device-specific underlying implementation +// into the server instance for the NMOS Registry. +nmos::experimental::registry_implementation make_registry_implementation(nmos::registry_model& model, slog::base_gate& gate) +{ + return nmos::experimental::registry_implementation() + .on_load_server_certificates(nmos::make_load_server_certificates_handler(model.settings, gate)) + .on_load_dh_param(nmos::make_load_dh_param_handler(model.settings, gate)) + .on_load_ca_certificates(nmos::make_load_ca_certificates_handler(model.settings, gate)); +} diff --git a/Development/nmos-cpp-registry/registry_implementation.h b/Development/nmos-cpp-registry/registry_implementation.h new file mode 100644 index 000000000..d4aaf1c0b --- /dev/null +++ b/Development/nmos-cpp-registry/registry_implementation.h @@ -0,0 +1,23 @@ +#ifndef NMOS_CPP_REGISTRY_REGISTRY_IMPLEMENTATION_H +#define NMOS_CPP_REGISTRY_REGISTRY_IMPLEMENTATION_H + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + struct registry_model; + + namespace experimental + { + struct registry_implementation; + } +} + +// This constructs all the callbacks used to integrate the example device-specific underlying implementation +// into the server instance for the NMOS Registry. +nmos::experimental::registry_implementation make_registry_implementation(nmos::registry_model& model, slog::base_gate& gate); + +#endif diff --git a/Development/nmos/activation_mode.h b/Development/nmos/activation_mode.h index 0133ef724..88ebcd39c 100644 --- a/Development/nmos/activation_mode.h +++ b/Development/nmos/activation_mode.h @@ -6,7 +6,7 @@ namespace nmos { // Connection API activation mode - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/schemas/v1.0-activation-schema.json + // See https://specs.amwa.tv/is-05/releases/v1.0.0/APIs/schemas/with-refs/v1.0-activation-schema.html DEFINE_STRING_ENUM(activation_mode) namespace activation_modes { diff --git a/Development/nmos/activation_utils.cpp b/Development/nmos/activation_utils.cpp index 37fa3fe6e..8f91f5922 100644 --- a/Development/nmos/activation_utils.cpp +++ b/Development/nmos/activation_utils.cpp @@ -83,7 +83,7 @@ namespace nmos activation[nmos::fields::requested_time] = value::null(); // "If no activation was requested in the PATCH `activation_time` will be set `null`." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/ConnectionAPI.raml + // See https://specs.amwa.tv/is-05/releases/v1.0.0/APIs/ConnectionAPI.html activation[nmos::fields::activation_time] = value::null(); break; @@ -93,8 +93,8 @@ namespace nmos activation[nmos::fields::mode] = value::null(); // Each of these fields "returns to null [...] when the resource is unlocked by setting the activation mode to null." - // See https://github.com/amwa-tv/nmos-device-connection-management/blob/v1.0/APIs/schemas/v1.0-activation-response-schema.json - // and https://github.com/amwa-tv/nmos-device-connection-management/blob/v1.1/APIs/schemas/activation-response-schema.json + // See https://specs.amwa.tv/is-05/releases/v1.0.0/APIs/schemas/with-refs/v1.0-activation-response-schema.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/activation-response-schema.html activation[nmos::fields::requested_time] = value::null(); activation[nmos::fields::activation_time] = value::null(); @@ -104,7 +104,7 @@ namespace nmos // "For immediate activations, in the response to the PATCH request this field // will be set to 'activate_immediate'" - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/schemas/v1.0-activation-response-schema.json + // See https://specs.amwa.tv/is-05/releases/v1.0.0/APIs/schemas/with-refs/v1.0-activation-response-schema.html activation[nmos::fields::mode] = value::string(nmos::activation_modes::activate_immediate.name); // "For an immediate activation this field will always be null on the staged endpoint, @@ -125,7 +125,7 @@ namespace nmos activation[nmos::fields::requested_time] = request_activation.at(nmos::fields::requested_time); // "For scheduled activations `activation_time` should be the absolute TAI time the parameters will actually transition." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/ConnectionAPI.raml + // See https://specs.amwa.tv/is-05/releases/v1.0.0/APIs/ConnectionAPI.html auto absolute_requested_time = get_absolute_requested_time(activation, request_time); activation[nmos::fields::activation_time] = value::string(nmos::make_version(absolute_requested_time)); diff --git a/Development/nmos/api_downgrade.cpp b/Development/nmos/api_downgrade.cpp index a722b2b6d..1047d78fd 100644 --- a/Development/nmos/api_downgrade.cpp +++ b/Development/nmos/api_downgrade.cpp @@ -6,7 +6,7 @@ namespace nmos { - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.5.%20APIs%20-%20Query%20Parameters.md#downgrade-queries + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.5._APIs_-_Query_Parameters.html#downgrade-queries bool is_permitted_downgrade(const nmos::resource& resource, const nmos::api_version& version) { @@ -148,10 +148,10 @@ namespace nmos // sub-object of a receiver. However, the schema, "subscription" sub-object does not have "additionalProperties": false // so downgrading from v1.2 to v1.1 and keeping the "active" property doesn't cause a schema violation, though this // could be an oversight... - // See nmos-discovery-registration/APIs/schemas/receiver_core.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_core.html // Further examples of this are the proposed addition in v1.3 of "attached_network_device" in the "interfaces" // sub-objects of a node and of "authorization" in node "api.endpoints" and "services" and device "controls". - // See https://github.com/AMWA-TV/nmos-discovery-registration/pull/109/files#diff-251d9acc57a6ffaeed673153c6409f5f + // See https://github.com/AMWA-TV/is-04/pull/109/files#diff-251d9acc57a6ffaeed673153c6409f5f auto& resource_versions = resources_versions().at(resource_type); auto version_first = resource_versions.cbegin(); diff --git a/Development/nmos/api_downgrade.h b/Development/nmos/api_downgrade.h index f5640b8d7..114b11b1e 100644 --- a/Development/nmos/api_downgrade.h +++ b/Development/nmos/api_downgrade.h @@ -5,7 +5,7 @@ // "Downgrade queries permit old-versioned responses to be provided to clients which are confident // that they can handle any missing attributes between the specified API versions." -// See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.5.%20APIs%20-%20Query%20Parameters.md#downgrade-queries +// See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.5._APIs_-_Query_Parameters.html#downgrade-queries namespace nmos { struct api_version; diff --git a/Development/nmos/api_utils.cpp b/Development/nmos/api_utils.cpp index b2f064e16..899d759d6 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -5,9 +5,16 @@ #include #include #include "cpprest/json_visit.h" +#include "cpprest/resource_server_error.h" #include "cpprest/uri_schemes.h" #include "cpprest/ws_utils.h" #include "nmos/api_version.h" +#include "nmos/authorization.h" +#include "nmos/authorization_state.h" +#include "nmos/authorization_utils.h" +#include "nmos/media_type.h" +#include "nmos/model.h" +#include "nmos/scope.h" #include "nmos/slog.h" #include "nmos/type.h" #include "nmos/version.h" @@ -155,7 +162,15 @@ namespace nmos { U("receivers"), nmos::types::receiver }, { U("subscriptions"), nmos::types::subscription }, { U("inputs"), nmos::types::input }, - { U("outputs"), nmos::types::output } + { U("outputs"), nmos::types::output }, + { U("nc_block"), nmos::types::nc_block }, + { U("nc_worker"), nmos::types::nc_worker }, + { U("nc_manager"), nmos::types::nc_manager }, + { U("nc_device_manager"), nmos::types::nc_device_manager }, + { U("nc_class_manager"), nmos::types::nc_class_manager }, + { U("nc_receiver_monitor"), nmos::types::nc_receiver_monitor }, + { U("nc_receiver_monitor_protected"), nmos::types::nc_receiver_monitor_protected }, + { U("nc_ident_beacon"), nmos::types::nc_ident_beacon } }; return types_from_resourceType.at(resourceType); } @@ -174,7 +189,15 @@ namespace nmos { nmos::types::subscription, U("subscriptions") }, { nmos::types::grain, {} }, // subscription websocket grains aren't exposed via the Query API { nmos::types::input, U("inputs") }, - { nmos::types::output, U("outputs") } + { nmos::types::output, U("outputs") }, + { nmos::types::nc_block, U("nc_block") }, + { nmos::types::nc_worker, U("nc_worker") }, + { nmos::types::nc_manager, U("nc_manager") }, + { nmos::types::nc_device_manager, U("nc_device_manager") }, + { nmos::types::nc_class_manager, U("nc_class_manager") }, + { nmos::types::nc_receiver_monitor, U("nc_receiver_monitor") }, + { nmos::types::nc_receiver_monitor_protected, U("nc_receiver_monitor_protected") }, + { nmos::types::nc_ident_beacon, U("nc_ident_beacon") } }; return resourceTypes_from_type.at(type); } @@ -190,7 +213,7 @@ namespace nmos const auto accept = req.headers().find(web::http::header_names::accept); return req.headers().end() != accept && !boost::algorithm::contains(accept->second, mime_type) - && boost::algorithm::contains(accept->second, U("text/html")); + && boost::algorithm::contains(accept->second, nmos::media_types::text_html.name); } web::json::value make_html_response_a_tag(const web::uri& href, const web::json::value& value) @@ -213,7 +236,7 @@ namespace nmos // construct a standard NMOS "child resources" response, from the specified sub-routes // merging with ones from an existing response - // see https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.0.%20APIs.md#api-paths + // see https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.0._APIs.html#api-paths web::json::value make_sub_routes_body(std::set sub_routes, const web::http::http_request& req, web::http::http_response res) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -466,15 +489,18 @@ namespace nmos }; } - static const utility::string_t received_time{ U("X-Received-Time") }; - static const utility::string_t actual_method{ U("X-Actual-Method") }; + // make handler to set appropriate response headers, and error response body if indicated + web::http::experimental::listener::route_handler make_api_finally_handler(slog::base_gate& gate) + { + return make_api_finally_handler({}, gate); + } // make handler to set appropriate response headers, and error response body if indicated - web::http::experimental::listener::route_handler make_api_finally_handler(slog::base_gate& gate_) + web::http::experimental::listener::route_handler make_api_finally_handler(const bst::optional& hsts, slog::base_gate& gate_) { using namespace web::http::experimental::listener::api_router_using_declarations; - return [&gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + return [hsts, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) { nmos::api_gate gate(gate_, req, parameters); @@ -491,6 +517,11 @@ namespace nmos res.headers().add(web::http::experimental::header_names::timing_allow_origin, U("*")); } + if (hsts) + { + res.headers().add(web::http::experimental::header_names::strict_transport_security, web::http::experimental::make_hsts_header(*hsts)); + } + // if it was a HEAD request, restore that and discard any response body // since RFC 7231 says "the server MUST NOT send a message body in the response" // see https://tools.ietf.org/html/rfc7231#section-4.3.2 @@ -563,11 +594,13 @@ namespace nmos if (web::http::details::is_mime_type_json(mime_type) && experimental::details::is_html_response_preferred(req, mime_type)) { res.set_body(nmos::experimental::make_html_response_body(res, gate)); - res.headers().set_content_type(U("text/html; charset=utf-8")); + res.headers().set_content_type(nmos::media_types::text_html.name + U("; charset=utf-8")); } slog::detail::logw(gate, slog::severities::more_info, SLOG_FLF) << nmos::stash_categories({ nmos::categories::access }) << nmos::common_log_stash(req, res) << "Sending response after " << processing_dur << "ms"; + // the task returned by reply() silently 'observes' any exception thrown from the underlying server + // reply() itself can throw http_exception if a response has already been sent, but that would indicate a programming error req.reply(res); return pplx::task_from_result(false); // don't continue matching routes }; @@ -589,18 +622,24 @@ namespace nmos } // add handler to set appropriate response headers, and error response body if indicated - call this only after adding all others! - void add_api_finally_handler(web::http::experimental::listener::api_router& api, slog::base_gate& gate_) + void add_api_finally_handler(web::http::experimental::listener::api_router& api, slog::base_gate& gate) + { + add_api_finally_handler(api, {}, gate); + } + + // add handler to set appropriate response headers, and error response body if indicated - call this only after adding all others! + void add_api_finally_handler(web::http::experimental::listener::api_router& api, const bst::optional& hsts, slog::base_gate& gate_) { using namespace web::http::experimental::listener::api_router_using_declarations; - api.support(U(".*"), details::make_api_finally_handler(gate_)); + api.support(U(".*"), details::make_api_finally_handler(hsts, gate_)); api.set_exception_handler([&gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) { nmos::api_gate gate(gate_, req, parameters); try { - std::rethrow_exception(std::current_exception()); + throw; } // assume a JSON error indicates a bad request catch (const web::json::json_exception& e) @@ -650,11 +689,17 @@ namespace nmos }); } - // modify the specified API to handle all requests (including CORS preflight requests via "OPTIONS") and attach it to the specified listener - captures api by reference! - void support_api(web::http::experimental::listener::http_listener& listener, web::http::experimental::listener::api_router& api_, slog::base_gate& gate) + // modify the specified API to handle all requests (including CORS preflight requests via "OPTIONS") and attach it to the specified listener + void support_api(web::http::experimental::listener::http_listener& listener, web::http::experimental::listener::api_router api, slog::base_gate& gate) { - add_api_finally_handler(api_, gate); - auto api = [&api_, &gate](web::http::http_request req) + support_api(listener, api, {}, gate); + } + + // modify the specified API to handle all requests (including CORS preflight requests via "OPTIONS") and attach it to the specified listener + void support_api(web::http::experimental::listener::http_listener& listener, web::http::experimental::listener::api_router api_, const bst::optional& hsts, slog::base_gate& gate) + { + add_api_finally_handler(api_, hsts, gate); + auto api = [api_, &gate](web::http::http_request req) mutable { req.headers().add(details::received_time, nmos::make_version()); slog::log(gate, SLOG_FLF) @@ -667,7 +712,7 @@ namespace nmos }; listener.support(api); listener.support(web::http::methods::OPTIONS, api); // to handle CORS preflight requests - listener.support(web::http::methods::HEAD, [api](web::http::http_request req) // to handle HEAD requests + listener.support(web::http::methods::HEAD, [api](web::http::http_request req) mutable // to handle HEAD requests { // this naive approach means that the API may well generate a response body req.headers().add(details::actual_method, web::http::methods::HEAD); @@ -677,11 +722,18 @@ namespace nmos } // construct an http_listener on the specified address and port, modifying the specified API to handle all requests - // (including CORS preflight requests via "OPTIONS") - captures api by reference! - web::http::experimental::listener::http_listener make_api_listener(bool secure, const utility::string_t& host_address, int port, web::http::experimental::listener::api_router& api, web::http::experimental::listener::http_listener_config config, slog::base_gate& gate) + // (including CORS preflight requests via "OPTIONS") + web::http::experimental::listener::http_listener make_api_listener(bool secure, const utility::string_t& host_address, int port, web::http::experimental::listener::api_router api, web::http::experimental::listener::http_listener_config config, slog::base_gate& gate) + { + return make_api_listener(secure, host_address, port, api, config, {}, gate); + } + + // construct an http_listener on the specified address and port, modifying the specified API to handle all requests + // (including CORS preflight requests via "OPTIONS") + web::http::experimental::listener::http_listener make_api_listener(bool secure, const utility::string_t& host_address, int port, web::http::experimental::listener::api_router api, web::http::experimental::listener::http_listener_config config, const bst::optional& hsts, slog::base_gate& gate) { web::http::experimental::listener::http_listener api_listener(web::http::experimental::listener::make_listener_uri(secure, host_address, port), std::move(config)); - nmos::support_api(api_listener, api, gate); + nmos::support_api(api_listener, api, secure ? hsts : bst::optional{}, gate); return api_listener; } @@ -721,6 +773,103 @@ namespace nmos { return mqtt_scheme(nmos::experimental::fields::client_secure(settings)); } + + namespace experimental + { + namespace details + { + // JWT validation to confirm authentication credentials and an access token that allows access to the protected resource + // see https://tools.ietf.org/html/rfc6750#section-3 + web::http::experimental::listener::route_handler make_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, const nmos::experimental::scope& scope, validate_authorization_token_handler access_token_validation, slog::base_gate& gate_) + { + using namespace web::http::experimental::listener::api_router_using_declarations; + + return [&model, &authorization_state, scope, access_token_validation, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + + if (methods::OPTIONS == req.method()) return pplx::task_from_result(true); + + web::uri token_issuer; + const auto audience = with_read_lock(model.mutex, [&] { return nmos::get_host_name(model.settings); }); + // note: the validate_authorization returns the token_issuer via function parameter + const auto result = nmos::experimental::validate_authorization(req, scope, audience, token_issuer, access_token_validation, gate_); + if (!result) + { + // set error repsonse + auto realm = web::http::get_host_port(req).first; + if (realm.empty()) { realm = with_read_lock(model.mutex, [&] { return nmos::get_host(model.settings); }); } + const auto retry_after = with_read_lock(model.mutex, [&] { return nmos::experimental::fields::service_unavailable_retry_after(model.settings); }); + set_error_reply(res, realm, retry_after, result); + + // if no matching public keys caused the error, trigger a re-fetch to obtain public keys from the token issuer (authorization_state.token_issuer) + if (result.value == authorization_error::no_matching_keys) + { + slog::log(gate, SLOG_FLF) << "Authorization warning: " << result.message; + + with_write_lock(authorization_state.mutex, [&authorization_state, token_issuer] + { + authorization_state.fetch_token_issuer_pubkeys = true; + authorization_state.token_issuer = token_issuer; + }); + + auto lock = model.write_lock(); + model.notify(); + } + else + { + slog::log(gate, SLOG_FLF) << "Authorization error: " << result.message; + } + + throw nmos::details::to_api_finally_handler{}; // in order to skip other route handlers and then send the response + } + + return pplx::task_from_result(true); + }; + } + + void set_error_reply(web::http::http_response& res, const utility::string_t& realm, int retry_after, const nmos::experimental::authorization_error& error) + { + using namespace web::http; + + // WWW-Authenticate Response Header Field definition + // see https://tools.ietf.org/html/rfc6750#section-3 + utility::string_t auth_params{ U("Bearer realm=") + realm }; + utility::string_t error_description{}; + // If the request lacks any authentication information (e.g., the client + // was unaware that authentication is necessary or attempted using an + // unsupported authentication method), the resource server SHOULD NOT + // include an error code or other error information. + // + // For example : + // + // HTTP / 1.1 401 Unauthorized + // WWW - Authenticate : Bearer realm = "example" + // see https://tools.ietf.org/html/rfc6750#section-3.1 + if (error.value != nmos::experimental::authorization_error::without_authentication) + { + utility::string_t error_string = { (error.value == nmos::experimental::authorization_error::insufficient_scope) ? web::http::oauth2::experimental::resource_server_errors::insufficient_scope.name : web::http::oauth2::experimental::resource_server_errors::invalid_token.name }; + error_description = utility::s2us(error.message); + auth_params += U(",error=") + error_string + U(",error_description=") + error_description; + } + + res.headers().add(web::http::header_names::www_authenticate, auth_params); + + auto status_code = status_codes::Unauthorized; + if (error.value == nmos::experimental::authorization_error::insufficient_scope) + { + status_code = status_codes::Forbidden; + } + else if (error.value == nmos::experimental::authorization_error::no_matching_keys) + { + status_code = status_codes::ServiceUnavailable; + res.headers().add(web::http::header_names::retry_after, retry_after); + } + + nmos::set_error_reply(res, status_code, utility::s2us(error.message)); + } + } + } } #if 0 diff --git a/Development/nmos/api_utils.h b/Development/nmos/api_utils.h index 7d045110c..0f8b132a3 100644 --- a/Development/nmos/api_utils.h +++ b/Development/nmos/api_utils.h @@ -3,10 +3,12 @@ #include #include +#include "bst/optional.h" #include "cpprest/api_router.h" #include "cpprest/http_listener.h" // for web::http::experimental::listener::http_listener_config #include "cpprest/regex_utils.h" #include "cpprest/ws_listener.h" // for web::websockets::experimental::listener::websocket_listener_config +#include "nmos/authorization_handlers.h" // for nmos::experimental::validate_authorization_token_handler #include "nmos/settings.h" // just a forward declaration of nmos::settings namespace slog @@ -18,8 +20,14 @@ namespace slog namespace nmos { struct api_version; + struct base_model; struct type; + namespace experimental + { + struct authorization_state; + } + // Patterns are used to form parameterised route paths // (could be moved to cpprest/api_router.h or cpprest/route_pattern.h?) @@ -45,8 +53,8 @@ namespace nmos const route_pattern connection_api = make_route_pattern(U("api"), U("connection")); // IS-07 Events API const route_pattern events_api = make_route_pattern(U("api"), U("events")); - // IS-08 Channel Mapping API - const route_pattern channelmapping_api = make_route_pattern(U("api"), U("channelmapping")); + // IS-08 Channel Mapping API + const route_pattern channelmapping_api = make_route_pattern(U("api"), U("channelmapping")); // IS-09 System API (originally specified in JT-NM TR-1001-1:2018 Annex A) const route_pattern system_api = make_route_pattern(U("api"), U("system")); @@ -108,7 +116,7 @@ namespace nmos // construct a standard NMOS "child resources" response, from the specified sub-routes // merging with ones from an existing response - // see https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.0.%20APIs.md#api-paths + // see https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.0._APIs.html#api-paths web::json::value make_sub_routes_body(std::set sub_routes, const web::http::http_request& req, web::http::http_response res); // construct sub-routes for the specified API versions @@ -125,19 +133,22 @@ namespace nmos // add handler to set appropriate response headers, and error response body if indicated - call this only after adding all others! void add_api_finally_handler(web::http::experimental::listener::api_router& api, slog::base_gate& gate); + void add_api_finally_handler(web::http::experimental::listener::api_router& api, const bst::optional& hsts, slog::base_gate& gate); - // modify the specified API to handle all requests (including CORS preflight requests via "OPTIONS") and attach it to the specified listener - captures api by reference! - void support_api(web::http::experimental::listener::http_listener& listener, web::http::experimental::listener::api_router& api, slog::base_gate& gate); + // modify the specified API to handle all requests (including CORS preflight requests via "OPTIONS") and attach it to the specified listener + void support_api(web::http::experimental::listener::http_listener& listener, web::http::experimental::listener::api_router api, slog::base_gate& gate); + void support_api(web::http::experimental::listener::http_listener& listener, web::http::experimental::listener::api_router api, const bst::optional& hsts, slog::base_gate& gate); // construct an http_listener on the specified address and port, modifying the specified API to handle all requests - // (including CORS preflight requests via "OPTIONS") - captures api by reference! - web::http::experimental::listener::http_listener make_api_listener(bool secure, const utility::string_t& host_address, int port, web::http::experimental::listener::api_router& api, web::http::experimental::listener::http_listener_config config, slog::base_gate& gate); + // (including CORS preflight requests via "OPTIONS") + web::http::experimental::listener::http_listener make_api_listener(bool secure, const utility::string_t& host_address, int port, web::http::experimental::listener::api_router api, web::http::experimental::listener::http_listener_config config, slog::base_gate& gate); + web::http::experimental::listener::http_listener make_api_listener(bool secure, const utility::string_t& host_address, int port, web::http::experimental::listener::api_router api, web::http::experimental::listener::http_listener_config config, const bst::optional& hsts, slog::base_gate& gate); // construct an http_listener on the specified port, modifying the specified API to handle all requests - // (including CORS preflight requests via "OPTIONS") - captures api by reference! - inline web::http::experimental::listener::http_listener make_api_listener(int port, web::http::experimental::listener::api_router& api, web::http::experimental::listener::http_listener_config config, slog::base_gate& gate) + // (including CORS preflight requests via "OPTIONS") + inline web::http::experimental::listener::http_listener make_api_listener(int port, web::http::experimental::listener::api_router api, web::http::experimental::listener::http_listener_config config, slog::base_gate& gate) { - return make_api_listener(false, web::http::experimental::listener::host_wildcard, port, api, config, gate); + return make_api_listener(false, web::http::experimental::listener::host_wildcard, port, api, config, {}, gate); } // construct a websocket_listener on the specified address and port - captures handlers by reference! @@ -157,6 +168,9 @@ namespace nmos namespace details { + const utility::string_t received_time{ U("X-Received-Time") }; + const utility::string_t actual_method{ U("X-Actual-Method") }; + // exception to skip other route handlers and then send the response (see add_api_finally_handler) struct to_api_finally_handler {}; @@ -184,6 +198,24 @@ namespace nmos // make handler to set appropriate response headers, and error response body if indicated web::http::experimental::listener::route_handler make_api_finally_handler(slog::base_gate& gate); + web::http::experimental::listener::route_handler make_api_finally_handler(const bst::optional& hsts, slog::base_gate& gate); + } + + // experimental extension, for BCP-003-02 Authorization + namespace experimental + { + struct authorization_error; + struct scope; + + namespace details + { + // JWT validation to confirm authentication credentials and an access token that allows access to the protected resource + // see https://tools.ietf.org/html/rfc6750#section-3 + web::http::experimental::listener::route_handler make_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, const nmos::experimental::scope& scope, validate_authorization_token_handler access_token_validation, slog::base_gate& gate); + + // set error response + void set_error_reply(web::http::http_response& res, const utility::string_t& realm, int retry_after, const nmos::experimental::authorization_error& error); + } } } diff --git a/Development/nmos/asset.h b/Development/nmos/asset.h new file mode 100644 index 000000000..fafd2c834 --- /dev/null +++ b/Development/nmos/asset.h @@ -0,0 +1,20 @@ +#ifndef NMOS_ASSET_H +#define NMOS_ASSET_H + +#include "cpprest/json_utils.h" + +// Asset Distinguishing Information +// See https://specs.amwa.tv/bcp-002-02/ +// and https://specs.amwa.tv/nmos-parameter-registers/branches/main/tags/ +namespace nmos +{ + namespace fields + { + const web::json::field_as_value_or asset_manufacturer{ U("urn:x-nmos:tag:asset:manufacturer/v1.0"), web::json::value::array() }; + const web::json::field_as_value_or asset_product_name{ U("urn:x-nmos:tag:asset:product/v1.0"), web::json::value::array() }; + const web::json::field_as_value_or asset_instance_id{ U("urn:x-nmos:tag:asset:instance-id/v1.0"), web::json::value::array() }; + const web::json::field_as_value_or asset_function{ U("urn:x-nmos:tag:asset:function/v1.0"), web::json::value::array() }; + } +} + +#endif diff --git a/Development/nmos/authorization.cpp b/Development/nmos/authorization.cpp new file mode 100644 index 000000000..0a3510d03 --- /dev/null +++ b/Development/nmos/authorization.cpp @@ -0,0 +1,203 @@ +#include "nmos/authorization.h" + +#include +#include "nmos/authorization_utils.h" +#include "nmos/jwt_validator.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace experimental + { + struct without_authentication_exception : std::runtime_error + { + without_authentication_exception(const std::string& message) : std::runtime_error(message) {} + }; + + bool is_access_token_expired(const utility::string_t& access_token, const issuers& issuers, const web::uri& expected_issuer, slog::base_gate& gate) + { + if (access_token.empty()) + { + // no access token, treat it as expired + return true; + } + + try + { + const auto& token_issuer = nmos::experimental::jwt_validator::get_token_issuer(access_token); + + // is token from the expected issuer + if (token_issuer == expected_issuer) + { + // is token expired + const auto& issuer = issuers.find(token_issuer); + if (issuers.end() != issuer) + { + issuer->second.jwt_validator.basic_validation(access_token); + return false; + } + } + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Test token expiry error: " << e.what(); + } + + // reaching here indicates token validation has failed so treat it as expired + return true; + } + + utility::string_t get_client_id(const web::http::http_headers& headers, slog::base_gate& gate) + { + try + { + const auto header = headers.find(web::http::header_names::authorization); + if (headers.end() == header) + { + throw without_authentication_exception{ "missing Authorization header" }; + } + + const auto& token = header->second; + const utility::string_t scheme{ U("Bearer ") }; + if (!boost::algorithm::starts_with(token, scheme)) + { + throw without_authentication_exception{ "unsupported authentication scheme" }; + } + + const auto access_token = token.substr(scheme.length()); + return jwt_validator::get_client_id(access_token); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Failed to get client_id from header: " << e.what(); + } + return{}; + } + + namespace details + { + authorization_error validate_authorization(const utility::string_t& access_token, const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, validate_authorization_token_handler access_token_validation, slog::base_gate& gate) + { + if (access_token.empty()) + { + slog::log(gate, SLOG_FLF) << "Missing access token"; + return{ authorization_error::without_authentication, "Missing access token" }; + } + + try + { + // extract the token issuer from the token + token_issuer = nmos::experimental::jwt_validator::get_token_issuer(access_token); + } + catch (const std::exception& e) + { +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Unable to extract token issuer from access token: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "Unable to extract token issuer from access token: " << e.what() << "; access_token: " << access_token; +#endif + return{ authorization_error::failed, e.what() }; + } + + if (access_token_validation) + { + try + { + // do basic access token token validation + const auto result = access_token_validation(access_token); + + if (result) + { + // do AMWA IS-10 registered claims validation + nmos::experimental::jwt_validator::registered_claims_validation(access_token, request.method(), request.relative_uri(), scope, audience); + + return authorization_error{ authorization_error::succeeded }; + } + return result; + } + catch (const insufficient_scope_exception& e) + { + // validator can decode the token, but insufficient scope +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Insufficient scope error: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "Insufficient scope error: " << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::insufficient_scope, e.what() }; + } + catch (const std::exception& e) + { + // validator can decode the token, with general failure +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Unexpected exception: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "Unexpected exception: " << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::failed, e.what() }; + } + } + else + { + std::string error{ "Access token validation callback is not set up to validate the access token" }; + slog::log(gate, SLOG_FLF) << error; + return{ authorization_error::failed, error }; + } + } + } + + authorization_error validate_authorization(const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, validate_authorization_token_handler access_token_validation, slog::base_gate& gate) + { + try + { + const auto& headers = request.headers(); + + const auto header = headers.find(web::http::header_names::authorization); + if (headers.end() == header) + { + throw without_authentication_exception{ "missing Authorization header" }; + } + + const auto& token = header->second; + const utility::string_t scheme{ U("Bearer ") }; + if (!boost::algorithm::starts_with(token, scheme)) + { + throw without_authentication_exception{ "unsupported authentication scheme" }; + } + + const auto access_token = token.substr(scheme.length()); + return details::validate_authorization(access_token, request, scope, audience, token_issuer, access_token_validation, gate); + } + catch (const without_authentication_exception& e) + { + return{ authorization_error::without_authentication, e.what() }; + } + } + + // RFC 6750 defines two methods of sending bearer access tokens which are applicable to WebSocket + // Clients SHOULD use the "Authorization Request Header Field" method. + // Clients MAY use "URI Query Parameter". + // See https://tools.ietf.org/html/rfc6750#section-2 + authorization_error ws_validate_authorization(const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, validate_authorization_token_handler access_token_validation, slog::base_gate& gate) + { + auto result = validate_authorization(request, scope, audience, token_issuer, access_token_validation, gate); + + if (!result) + { + result = { authorization_error::without_authentication, "missing access token" }; + + // test "URI Query Parameter" + const auto& query = request.request_uri().query(); + if (!query.empty()) + { + auto querys = web::uri::split_query(query); + auto found = querys.find(U("access_token")); + if (querys.end() != found) + { + result = details::validate_authorization(found->second, request, scope, audience, token_issuer, access_token_validation, gate); + } + } + } + return result; + } + } +} diff --git a/Development/nmos/authorization.h b/Development/nmos/authorization.h new file mode 100644 index 000000000..9a331f01d --- /dev/null +++ b/Development/nmos/authorization.h @@ -0,0 +1,41 @@ +#ifndef NMOS_AUTHORIZATION_H +#define NMOS_AUTHORIZATION_H + +#include "nmos/authorization_handlers.h" // for nmos::experimental::validate_authorization_token_handler, nmos::experimental::authorization_error, and nmos::experimental::scope +#include "nmos/issuers.h" + +namespace web +{ + class uri; + + namespace http + { + class http_headers; + class http_request; + } +} + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + namespace experimental + { + bool is_access_token_expired(const utility::string_t& access_token, const issuers& issuers, const web::uri& expected_issuer, slog::base_gate& gate); + + utility::string_t get_client_id(const web::http::http_headers& headers, slog::base_gate& gate); + + authorization_error validate_authorization(const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, validate_authorization_token_handler access_token_validation, slog::base_gate& gate); + + // RFC 6750 defines two methods of sending bearer access tokens which are applicable to WebSocket + // Clients SHOULD use the "Authorization Request Header Field" method. + // Clients MAY use "URI Query Parameter". + // See https://tools.ietf.org/html/rfc6750#section-2 + authorization_error ws_validate_authorization(const web::http::http_request& request, const scope& scope, const utility::string_t& audience, web::uri& token_issuer, validate_authorization_token_handler access_token_validation, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/nmos/authorization_behaviour.cpp b/Development/nmos/authorization_behaviour.cpp new file mode 100644 index 000000000..d5e77cdd8 --- /dev/null +++ b/Development/nmos/authorization_behaviour.cpp @@ -0,0 +1,528 @@ +#include "nmos/authorization_behaviour.h" + +#include "cpprest/response_type.h" +#include "mdns/service_discovery.h" +#include "nmos/api_utils.h" +#include "nmos/authorization.h" +#include "nmos/authorization_operation.h" +#include "nmos/authorization_scopes.h" +#include "nmos/authorization_state.h" +#include "nmos/authorization_utils.h" +#include "nmos/is10_versions.h" +#include "nmos/model.h" +#include "nmos/random.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace experimental + { + namespace fields + { + const web::json::field_as_string_or ver{ U("ver"),{} }; + //const web::json::field_as_integer_or pri{ U("pri"), nmos::service_priorities::no_priority }; already defined in settings.h + const web::json::field_as_string_or uri{ U("uri"),{} }; + } + + namespace details + { + // thread to fetch token and public keys from service + void authorization_behaviour_thread(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, mdns::service_discovery& discovery, slog::base_gate& gate); + // thread to fetch public keys from token issuer + void authorization_token_issuer_thread(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + + // background service discovery + void authorization_services_background_discovery(nmos::base_model& model, mdns::service_discovery& discovery, slog::base_gate& gate); + + // service discovery + bool discover_authorization_services(nmos::base_model& model, mdns::service_discovery& discovery, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + bool has_discovered_authorization_services(const nmos::base_model& model); + } + + // uses the default DNS-SD implementation + // callbacks from this function are called with the model locked, and may read or write directly to the model + void authorization_behaviour_thread(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, slog::base_gate& gate_) + { + nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::authorization_behaviour)); + + mdns::service_discovery discovery(gate); + + details::authorization_behaviour_thread(model, authorization_state, std::move(load_ca_certificates), std::move(load_rsa_private_keys), std::move(load_authorization_clients), std::move(save_authorization_client), std::move(request_authorization_code), discovery, gate); + } + + // uses the specified DNS-SD implementation + // callbacks from this function are called with the model locked, and may read or write directly to the model + void authorization_behaviour_thread(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, mdns::service_discovery& discovery, slog::base_gate& gate_) + { + nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::authorization_behaviour)); + + details::authorization_behaviour_thread(model, authorization_state, std::move(load_ca_certificates), std::move(load_rsa_private_keys), std::move(load_authorization_clients), std::move(save_authorization_client), std::move(request_authorization_code), discovery, gate); + } + + void details::authorization_behaviour_thread(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, mdns::service_discovery& discovery, slog::base_gate& gate) + { + enum + { + initial_discovery, + request_authorization_server_metadata, + client_registration, + authorization_code_flow, + authorization_operation, + authorization_operation_with_immediate_token_fetch, + rediscovery, + background_discovery + } mode = initial_discovery; + + // If the chosen Authorization API does not respond correctly at any time, another Authorization API should be selected from the discovered list. + with_write_lock(model.mutex, [&model] { model.settings[nmos::experimental::fields::authorization_services] = web::json::value::array(); }); + + nmos::details::seed_generator discovery_backoff_seeder; + std::default_random_engine discovery_backoff_engine(discovery_backoff_seeder); + double discovery_backoff = 0; + + // load authorization client's metadata to cache + if (load_authorization_clients) + { + const auto auth_clients = load_authorization_clients(); + + if (!auth_clients.is_null() && auth_clients.is_array()) + { + slog::log(gate, SLOG_FLF) << "Retrieved authorization clients: " << utility::us2s(auth_clients.serialize()) << " from non-volatile memory"; + + for (const auto& auth_client : auth_clients.as_array()) + { + nmos::experimental::update_client_metadata(authorization_state, auth_client.at(nmos::experimental::fields::authorization_server_uri).as_string(), nmos::experimental::fields::client_metadata(auth_client)); + } + } + } + + bool authorization_service_error{ false }; + + // continue until the server is being shut down + for (;;) + { + if (with_read_lock(model.mutex, [&] { return model.shutdown; })) break; + + switch (mode) + { + case initial_discovery: + case rediscovery: + if (0 != discovery_backoff) + { + auto lock = model.read_lock(); + const auto random_backoff = std::uniform_real_distribution<>(0, discovery_backoff)(discovery_backoff_engine); + slog::log(gate, SLOG_FLF) << "Waiting to retry Authorization API discovery for about " << std::fixed << std::setprecision(3) << random_backoff << " seconds (current backoff limit: " << discovery_backoff << " seconds)"; + model.wait_for(lock, std::chrono::milliseconds(std::chrono::milliseconds::rep(1000 * random_backoff)), [&] { return model.shutdown; }); + if (model.shutdown) break; + } + + // The Node performs a DNS-SD browse for services of type '_nmos-auth._tcp' as specified. + if (details::discover_authorization_services(model, discovery, gate)) + { + mode = request_authorization_server_metadata; + + // If unable to contact the Authorization server, we MUST implement a + // random back-off mechanism to avoid overloading the Authorization server in the event of a system restart. + auto lock = model.read_lock(); + discovery_backoff = (std::min)((std::max)((double)nmos::fields::discovery_backoff_min(model.settings), discovery_backoff * nmos::fields::discovery_backoff_factor(model.settings)), (double)nmos::fields::discovery_backoff_max(model.settings)); + } + else + { + mode = background_discovery; + } + break; + + case request_authorization_server_metadata: + if (details::request_authorization_server_metadata(model, authorization_state, authorization_service_error, load_ca_certificates, gate)) + { + // reterive client metadata from cache + const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); + + // does the client have a scope? A client without a scope is one that doesn't access any protected APIs (i.e. client isn't required to register with Authorization server). + if (with_read_lock(model.mutex, [&] { return details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)).size(); })) + { + // is the client already registered to Authorization server? (i.e. found it in cache). + if (!client_metadata.is_null()) + { + // no token or token expired + auto is_access_token_bad = [&] + { + auto lock = authorization_state.read_lock(); + + const auto& bearer_token = authorization_state.bearer_token; + return (!bearer_token.is_valid_access_token() || is_access_token_expired(bearer_token.access_token(), authorization_state.issuers, authorization_state.authorization_server_uri, gate)); + }; + + auto is_client_expired = [&] + { + // Time at which the client_secret will expire. If time is 0, it will never expire + // The time is represented as the number of seconds from 1970-01-01T0:0:0Z as measured in UTC + const auto expires_at = nmos::experimental::fields::client_secret_expires_at(client_metadata); + if (expires_at == 0) + { + return false; + } + const auto now = std::chrono::system_clock::now(); + const auto exp = std::chrono::system_clock::from_time_t(expires_at); + return (now > exp); + }; + + utility::string_t authorization_flow; + auto validate_openid_client = false; + with_read_lock(model.mutex, [&] + { + authorization_flow = nmos::experimental::fields::authorization_flow(model.settings); + validate_openid_client = nmos::experimental::fields::validate_openid_client(model.settings); + }); + + // if using OpenID Connect Authorization server, update the cache client metadata, in case it has been changed (e.g. changed by the system admin) + if (validate_openid_client) + { + // if OpenID Connect Authorization server is used, client status can be obtained via the Client Configuration Endpoint + // "The Client Configuration Endpoint is an OAuth 2.0 Protected Resource that MAY be provisioned by the server for a + // specific Client to be able to view and update its registered information." + // see https://openid.net/specs/openid-connect-registration-1_0.html#ClientConfigurationEndpoint + // registration_access_token + // OPTIONAL. Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the + // Client registration. + // registration_client_uri + // OPTIONAL. Location of the Client Configuration Endpoint where the Registration Access Token can be used to perform subsequent operations + // upon the resulting Client registration. + // Implementations MUST either return both a Client Configuration Endpoint and a Registration Access Token or neither of them. + if (client_metadata.has_string_field(nmos::experimental::fields::registration_access_token) && client_metadata.has_string_field(nmos::experimental::fields::registration_client_uri)) + { + // fetch client metadata from Authorization server in case it has been changed (e.g. changed by the system admin) + if (details::request_client_metadata_from_openid_connect(model, authorization_state, load_ca_certificates, save_authorization_client, gate)) + { + mode = (web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation // client credentials flow + : (is_access_token_bad() ? authorization_code_flow : authorization_operation_with_immediate_token_fetch); // bad access token must start from authorization code flow, otherise do token refresh + } + else + { + // remove client metadata from cache + nmos::experimental::erase_client_metadata(authorization_state); + + // client not known by the Authorization server, trigger client registration process + mode = client_registration; + } + } + else + { + // no registration_access_token and registration_client_uri found, treat it as if connected with a non-OpenID Connect server + // start grant flow based on what been defined in the settings + // hmm, maybe use of the OpenID API to extend the client lifespan instead of re-registration + mode = is_client_expired() ? client_registration // client registration + : ((web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation // client credentials flow + : (is_access_token_bad() ? authorization_code_flow : authorization_operation_with_immediate_token_fetch)); // bad access token must start from authorization code flow, otherise do token refresh + } + } + else + { + // start grant flow based on what been defined in the settings + // hmm, maybe use of the OpenID API to extend the client lifespan instead of re-registration + mode = is_client_expired() ? client_registration // client registration + : ((web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation // client credentials flow + : (is_access_token_bad() ? authorization_code_flow : authorization_operation_with_immediate_token_fetch)); // bad access token must start from authorization code flow, otherise do token refresh + } + } + else + { + // client has not been registered with the Authorization server yet + mode = client_registration; + } + } + else + { + // client does not have a scope therefore not require to obtain access token + mode = authorization_operation; + } + } + else + { + // Should no further Authorization APIs be available or TTLs on advertised services expired, a re-query may be performed. + mode = rediscovery; + } + break; + + case client_registration: + // register to the Authorization server to obtain client_id and client_secret (they can be found inside the client metadata) + if (details::client_registration(model, authorization_state, load_ca_certificates, save_authorization_client, gate)) + { + // client registered + mode = with_read_lock(model.mutex, [&] + { + const auto& authorization_flow = nmos::experimental::fields::authorization_flow(model.settings); + return (web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? authorization_operation : authorization_code_flow; + }); + } + else + { + // client registration failure, start authorization sequence again on next available Authorization server + authorization_service_error = true; + mode = request_authorization_server_metadata; + } + break; + + case authorization_code_flow: + if (details::authorization_code_flow(model, authorization_state, request_authorization_code, gate)) + { + mode = authorization_operation; + } + else + { + // authorization code flow failure, start authorization sequence again on next available Authorization server + authorization_service_error = true; + mode = request_authorization_server_metadata; + } + break; + + case authorization_operation: + // fetch public keys + // fetch access token within 1/2 token life time interval. + // authorization_operation will block until an error occurs, or shutdown + // on shutdown, enclosing for loop will exit + details::authorization_operation(model, authorization_state, load_ca_certificates, load_rsa_private_keys, false, gate); + + // reaching here indicates there has been a failure within the authorization operation, + // start the authorization sequence again on next available Authorization server + authorization_service_error = true; + mode = request_authorization_server_metadata; + break; + + case authorization_operation_with_immediate_token_fetch: + // fetch public keys + // immediately fetch access token + // authorization_operation will block until an error occurs, or shutdown + // on shutdown, enclosing for loop will exit + + details::authorization_operation(model, authorization_state, load_ca_certificates, load_rsa_private_keys, true, gate); + + // reaching here indicates there has been a failure within the authorization operation, + // start the authorization sequence again on next available Authorization server + authorization_service_error = true; + mode = request_authorization_server_metadata; + break; + + case background_discovery: + details::authorization_services_background_discovery(model, discovery, gate); + + if (details::has_discovered_authorization_services(model)) + { + mode = request_authorization_server_metadata; + } + } + } + } + + void authorization_token_issuer_thread(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate_) + { + nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::authorization_behaviour)); + + details::authorization_token_issuer_thread(model, authorization_state, load_ca_certificates, gate); + } + + void details::authorization_token_issuer_thread(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + enum + { + fetch_issuer_metadata, + fetch_issuer_pubkeys, + } mode = fetch_issuer_metadata; + + // continue until the server is being shut down + for (;;) + { + if (with_read_lock(model.mutex, [&] { return model.shutdown; })) break; + + switch (mode) + { + case fetch_issuer_metadata: + // fetch token issuer metadata + if (details::request_token_issuer_metadata(model, authorization_state, load_ca_certificates, gate)) + { + mode = fetch_issuer_pubkeys; + } + break; + + case fetch_issuer_pubkeys: + // fetch token issuer public keys + details::request_token_issuer_public_keys(model, authorization_state, load_ca_certificates, gate); + mode = fetch_issuer_metadata; + break; + } + } + } + + // service discovery + namespace details + { + static web::json::value make_service(const resolved_service& service) + { + using web::json::value; + + return web::json::value_of({ + { nmos::experimental::fields::ver, value::string(make_api_version(service.first.first)) }, + { nmos::fields::pri, service.first.second }, + { nmos::experimental::fields::uri, value::string(service.second.to_string()) } + }); + } + + static resolved_service parse_service(const web::json::value& data) + { + + return { + {parse_api_version(nmos::experimental::fields::ver(data)), nmos::fields::pri(data)}, + web::uri(nmos::experimental::fields::uri(data)) + }; + } + + // get the fallback authorization service from settings (if present) + resolved_service get_authorization_service(const nmos::settings& settings) + { + if (settings.has_field(nmos::experimental::fields::authorization_address)) + { + const auto api_selector = nmos::experimental::fields::authorization_selector(settings); + + return { { parse_api_version(nmos::experimental::fields::authorization_version(settings)), 0 }, + web::uri_builder() + .set_scheme(nmos::http_scheme(settings)) + .set_host(nmos::experimental::fields::authorization_address(settings)) + .set_port(nmos::experimental::fields::authorization_port(settings)) + .set_path(U("/.well-known/oauth-authorization-server")).append_path(!api_selector.empty() ? U("/") + api_selector : U("")) + .to_uri() }; + } + return {}; + } + + // query DNS Service Discovery for any Authorization API based on settings + bool discover_authorization_services(nmos::base_model& model, mdns::service_discovery& discovery, slog::base_gate& gate, const pplx::cancellation_token& token) + { + slog::log(gate, SLOG_FLF) << "Trying Authorization API discovery"; + + // lock to read settings, then unlock to wait for the discovery task to complete + auto authorization_services = with_read_lock(model.mutex, [&] + { + auto& settings = model.settings; + + if (nmos::service_priorities::no_priority != nmos::fields::authorization_highest_pri(settings)) + { + slog::log(gate, SLOG_FLF) << "Attempting discovery of a Authorization API in domain: " << nmos::get_domain(settings); + + return nmos::experimental::resolve_service_(discovery, nmos::service_types::authorization, settings, token); + } + else + { + return pplx::task_from_result(std::list{}); + } + }).get(); + + with_write_lock(model.mutex, [&] + { + if (!authorization_services.empty()) + { + slog::log(gate, SLOG_FLF) << "Discovered " << authorization_services.size() << " Authorization API(s)"; + } + else + { + slog::log(gate, SLOG_FLF) << "Did not discover a suitable Authorization API via DNS-SD"; + + auto fallback_authorization_service = get_authorization_service(model.settings); + if (!fallback_authorization_service.second.is_empty()) + { + authorization_services.push_back(fallback_authorization_service); + } + } + + if (!authorization_services.empty()) slog::log(gate, SLOG_FLF) << "Using the Authorization API(s):" << slog::log_manip([&](slog::log_statement& s) + { + for (auto& authorization_service : authorization_services) + { + s << '\n' << authorization_service.second.to_string(); + } + }); + + model.settings[nmos::experimental::fields::authorization_services] = web::json::value_from_elements(authorization_services | boost::adaptors::transformed([](const resolved_service& authorization_service) { return make_service(authorization_service); })); + + model.notify(); + }); + + return !authorization_services.empty(); + } + + bool empty_authorization_services(const nmos::settings& settings) + { + return web::json::empty(nmos::experimental::fields::authorization_services(settings)); + } + + bool has_discovered_authorization_services(const nmos::base_model& model) + { + return with_read_lock(model.mutex, [&] { return !empty_authorization_services(model.settings); }); + } + + // "The Node selects an Authorization API to use based on the priority" + resolved_service top_authorization_service(const nmos::settings& settings) + { + const auto value = web::json::front(nmos::experimental::fields::authorization_services(settings)); + return parse_service(value); + } + + // "If the chosen Authorization API does not respond correctly at any time, + // another Authorization API should be selected from the discovered list." + void pop_authorization_service(nmos::settings& settings) + { + web::json::pop_front(nmos::experimental::fields::authorization_services(settings)); + // "TTLs on advertised services" may have expired too, so should cache time-to-live values + // using DNSServiceQueryRecord instead of DNSServiceResolve? + } + } + + // service discovery operation + namespace details + { + void authorization_services_background_discovery(nmos::base_model& model, mdns::service_discovery& discovery, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Adopting background discovery of an Authorization API"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool authorization_services_discovered(false); + + // background tasks may read/write the above local state by reference + pplx::cancellation_token_source cancellation_source; + auto token = cancellation_source.get_token(); + pplx::task background_discovery = pplx::do_while([&] + { + // add a short delay since initial discovery or rediscovery must have only just failed + // (this also prevents a tight loop in the case that the underlying DNS-SD implementation is just refusing to co-operate + // though that would be better indicated by an exception from discover_authorization_services) + return pplx::complete_after(std::chrono::seconds(1), token).then([&] + { + return !discover_authorization_services(model, discovery, gate, token); + }); + }, token).then([&] + { + auto lock = model.write_lock(); // in order to update local state + + authorization_services_discovered = true; // since discovery must have succeeded + + model.notify(); + }); + + for (;;) + { + // wait for the thread to be interrupted because an Authorization API has been discovered + // or because the server is being shut down + condition.wait(lock, [&] { return shutdown || authorization_services_discovered; }); + if (shutdown || authorization_services_discovered) break; + } + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + background_discovery.wait(); + } + } + } +} diff --git a/Development/nmos/authorization_behaviour.h b/Development/nmos/authorization_behaviour.h new file mode 100644 index 000000000..fce1f72f5 --- /dev/null +++ b/Development/nmos/authorization_behaviour.h @@ -0,0 +1,50 @@ +#ifndef NMOS_AUTHORIZATION_BEHAVIOUR_H +#define NMOS_AUTHORIZATION_BEHAVIOUR_H + +#include +#include "cpprest/http_client.h" +#include "nmos/authorization_handlers.h" +#include "nmos/certificate_handlers.h" +#include "nmos/mdns.h" +#include "nmos/settings.h" // just a forward declaration of nmos::settings + +namespace slog +{ + class base_gate; +} + +namespace mdns +{ + class service_discovery; +} + +namespace nmos +{ + struct base_model; + + namespace experimental + { + struct authorization_state; + + // uses the default DNS-SD implementation + // callbacks from this function are called with the model locked, and may read or write directly to the model + void authorization_behaviour_thread(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, slog::base_gate& gate); + + // uses the specified DNS-SD implementation + // callbacks from this function are called with the model locked, and may read or write directly to the model + void authorization_behaviour_thread(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, mdns::service_discovery& discovery, slog::base_gate& gate); + + // callbacks from this function are called with the model locked, and may read or write directly to the model and the authorization settings + void authorization_token_issuer_thread(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + + namespace details + { + // services functions which are used by authorization operation + bool empty_authorization_services(const nmos::settings& settings); + resolved_service top_authorization_service(const nmos::settings& settings); + void pop_authorization_service(nmos::settings& settings); + } + } +} + +#endif diff --git a/Development/nmos/authorization_handlers.cpp b/Development/nmos/authorization_handlers.cpp new file mode 100644 index 000000000..6b155b4b8 --- /dev/null +++ b/Development/nmos/authorization_handlers.cpp @@ -0,0 +1,331 @@ +#include "nmos/authorization_handlers.h" + +#include +#include "cpprest/basic_utils.h" +#include "cpprest/json_validator.h" +#include "cpprest/response_type.h" +#include "nmos/api_utils.h" // for nmos::experimental::details::make_validate_authorization_handler +#include "nmos/authorization_state.h" +#include "nmos/is10_versions.h" +#include "nmos/json_schema.h" +#include "nmos/json_fields.h" +#include "nmos/slog.h" +#if defined(_WIN32) && !defined(__cplusplus_winrt) +#include +#include +#endif + +namespace nmos +{ + namespace experimental + { + namespace details + { + static const web::json::experimental::json_validator& auth_clients_schema_validator() + { + static const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_register_client_response_uri)) + }; + + return validator; + } + } + + // helper function to load the authorization clients file + // example of the file + // [ + // { + // "authorization_server_uri": "https://example.com" + // }, + // { + // "client_metadata": { + // "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "client_name" : "example client", + // "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ], + // "jwks_uri" : "https://example_client/jwks", + // "redirect_uris" : [ "https://example_client/callback" ], + // "registration_access_token" : "eyJhbGci....", + // "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "response_types" : [ "code" ], + // "scope" : "registration", + // "subject_type" : "public", + // "tls_client_certificate_bound_access_tokens" : false, + // "token_endpoint_auth_method" : "private_key_jwt" + // } + // } + // ] + web::json::value load_authorization_clients_file(const utility::string_t& filename, slog::base_gate& gate) + { + using web::json::value; + + try + { + utility::ifstream_t is(filename); + if (is.is_open()) + { + const auto authorization_clients = value::parse(is); + + if (!authorization_clients.is_null() && authorization_clients.is_array() && authorization_clients.as_array().size()) + { + for (auto& authorization_client : authorization_clients.as_array()) + { + if (authorization_client.has_field(nmos::experimental::fields::authorization_server_uri) && + !authorization_client.at(nmos::experimental::fields::authorization_server_uri).as_string().empty() && + authorization_client.has_field(nmos::experimental::fields::client_metadata)) + { + // validate client metadata + const auto& client_metadata = authorization_client.at(nmos::experimental::fields::client_metadata); + details::auth_clients_schema_validator().validate(client_metadata, experimental::make_authapi_register_client_response_uri(is10_versions::v1_0)); // may throw json_exception + } + } + } + + return authorization_clients; + } + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Unable to load authorization clients from non-volatile memory: " << filename << ": " << e.what(); + } + return web::json::value::array(); + } + + // helper function to update the authorization clients file + // example of authorization_client + // { + // { + // "authorization_server_uri": "https://example.com" + // }, + // { + // "client_metadata": { + // "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "client_name" : "example client", + // "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ], + // "issuer" : "https://example.com", + // "jwks_uri" : "https://example_client/jwks", + // "redirect_uris" : [ "https://example_client/callback" ], + // "registration_access_token" : "eyJhbGci....", + // "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "response_types" : [ "code" ], + // "scope" : "registration", + // "subject_type" : "public", + // "tls_client_certificate_bound_access_tokens" : false, + // "token_endpoint_auth_method" : "private_key_jwt" + // } + // } + // } + void update_authorization_clients_file(const utility::string_t& filename, const web::json::value& authorization_client, slog::base_gate& gate) + { + // load authorization_clients from file + auto authorization_clients = load_authorization_clients_file(filename, gate); + + // update/append to the authorization_clients + bool updated{ false }; + if (authorization_clients.as_array().size()) + { + for (auto& auth_client : authorization_clients.as_array()) + { + const auto& authorization_server_uri = auth_client.at(nmos::experimental::fields::authorization_server_uri); + if (authorization_server_uri == authorization_client.at(nmos::experimental::fields::authorization_server_uri)) + { + auth_client[nmos::experimental::fields::client_metadata] = authorization_client.at(nmos::experimental::fields::client_metadata); + updated = true; + break; + } + } + } + if (!updated) + { + web::json::push_back(authorization_clients, authorization_client); + } + + // save the updated authorization_clients to file + utility::ofstream_t os(filename, std::ios::out | std::ios::trunc); + if (os.is_open()) + { + os << authorization_clients.serialize(); + os.close(); + } + } + + // construct callback to load a table of authorization server uri vs authorization client metadata from file based on settings seed_id + // it is not required for scopeless OAuth 2.0 client (client not require to access any protected APIs) + load_authorization_clients_handler make_load_authorization_clients_handler(const nmos::settings& settings, slog::base_gate& gate) + { + return [&]() + { + // obtain client metadata from the safe, permission-restricted, location in the non-volatile memory, e.g. a file + // Client metadata SHOULD consist of the client_id, client_secret, client_secret_expires_at, client_uri, grant_types, redirect_uris, response_types, scope, token_endpoint_auth_method + auto filename = nmos::experimental::fields::seed_id(settings) + U(".json"); + slog::log(gate, SLOG_FLF) << "Load authorization clients from non-volatile memory: " << filename; + + return load_authorization_clients_file(filename, gate); + }; + } + + // construct callback to save the authorization server uri vs authorization client metadata table to file, using seed_id for the filename + // it is not required for scopeless OAuth 2.0 client (client not require to access any protected APIs) + save_authorization_client_handler make_save_authorization_client_handler(const nmos::settings& settings, slog::base_gate& gate) + { + return [&](const web::json::value& authorization_client) + { + // Client metadata SHOULD be stored in a safe, permission-restricted, location in non-volatile memory in case of a device restart to prevent re-registration. + // Client secrets SHOULD be encrypted before being stored to reduce the chance of client secret leaking. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#client-credentials + const auto filename = nmos::experimental::fields::seed_id(settings) + U(".json"); + slog::log(gate, SLOG_FLF) << "Save authorization clients to non-volatile memory: " << filename; + + update_authorization_clients_file(filename, authorization_client, gate); + }; + } + + // construct callback to start the authorization code flow request on a browser + // this is required for OAuth clients which use Authorization Code Flow to obtain the access token + // note: as it is not easy to specify the 'content-type' used in the browser programmatically, this can be easily + // fixed by installing a browser header modifier + // extensions such as ModHeader can be used to add the missing 'content-type' header: + // for Windows https://chrome.google.com/webstore/detail/modheader-modify-http-hea/idgpnmonknjnojddfkpgkljpfnnfcklj + // for Linux https://addons.mozilla.org/en-GB/firefox/addon/modheader-firefox/ + request_authorization_code_handler make_request_authorization_code_handler(slog::base_gate& gate) + { + return[&gate](const web::uri& authorization_code_uri) + { + slog::log(gate, SLOG_FLF) << "Open a browser to start the authorization code flow: " << authorization_code_uri.to_string(); + + std::string browser_cmd; +#if defined(_WIN32) && !defined(__cplusplus_winrt) + browser_cmd = "start \"\" \"" + utility::us2s(authorization_code_uri.to_string()) + "\""; +#else + browser_cmd = "xdg-open \"" + utility::us2s(authorization_code_uri.to_string()) + "\""; +#endif + std::ignore = system(browser_cmd.c_str()); + + // hmm, notify authorization_code_flow in the authorization_behaviour thread + // in the event of user cancels the authorization code flow process + }; + } + + // construct callback to validate OAuth 2.0 authorization access token + validate_authorization_token_handler make_validate_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate) + { + return[&](const utility::string_t& access_token) + { + try + { + // extract the token issuer from the token + const auto token_issuer = nmos::experimental::jwt_validator::get_token_issuer(access_token); + + auto lock = authorization_state.read_lock(); + + std::string error; + auto issuer = authorization_state.issuers.find(token_issuer); + if (authorization_state.issuers.end() != issuer) + { + slog::log(gate, SLOG_FLF) << "Validate access token against " << utility::us2s(issuer->first.to_string()) << " public keys"; + + try + { + // if jwt_validator is not already set up, assume no public keys to validate token + if (issuer->second.jwt_validator.is_initialized()) + { + // do access token basic validation, including token schema validation and token issuer public keys validation + issuer->second.jwt_validator.basic_validation(access_token); + + return authorization_error{ authorization_error::succeeded }; + } + else + { + std::stringstream ss; + ss << "No " << utility::us2s(issuer->first.to_string()) << " public keys to validate access token"; + error = ss.str(); + slog::log(gate, SLOG_FLF) << error; + + return authorization_error{ authorization_error::no_matching_keys, error }; + } + } + catch (const web::json::json_exception& e) + { +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "JSON error: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "JSON error: " << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::failed, e.what() }; + } + catch (const jwt::error::token_verification_exception& e) + { +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Token verification error: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "Token verification error: " << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::failed, e.what() }; + } + catch (const no_matching_keys_exception& e) + { +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "No matching public keys error: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "No matching public keys error: " << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::no_matching_keys, e.what() }; + } + catch (const std::exception& e) + { +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Unexpected exception: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "Unexpected exception: " << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::failed, e.what() }; + } + } + else + { + std::stringstream ss; + ss << "No " << utility::us2s(token_issuer.to_string()) << " public keys to validate access token"; + error = ss.str(); + slog::log(gate, SLOG_FLF) << error; + + // no public keys to validate token + return authorization_error{ authorization_error::no_matching_keys, error }; + } + } + catch (const std::exception& e) + { +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Unable to extract token issuer from access token: " << e.what(); +#else + slog::log(gate, SLOG_FLF) << "Unable to extract token issuer from access token: " << e.what() << "; access_token: " << access_token; +#endif + return authorization_error{ authorization_error::failed, e.what() }; + } + }; + } + + // construct callback to validate OAuth 2.0 authorization + validate_authorization_handler make_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, validate_authorization_token_handler access_token_validation, slog::base_gate& gate) + { + return[&, access_token_validation](const nmos::experimental::scope& scope) + { + slog::log(gate, SLOG_FLF) << "Make authorization validation"; + + return nmos::experimental::details::make_validate_authorization_handler(model, authorization_state, scope, access_token_validation, gate); + }; + } + + // construct callback to retrieve OAuth 2.0 authorization bearer token + get_authorization_bearer_token_handler make_get_authorization_bearer_token_handler(authorization_state& authorization_state, slog::base_gate& gate) + { + return[&]() + { + slog::log(gate, SLOG_FLF) << "Retrieve bearer token from cache"; + + auto lock = authorization_state.read_lock(); + return authorization_state.bearer_token; + }; + } + } +} diff --git a/Development/nmos/authorization_handlers.h b/Development/nmos/authorization_handlers.h new file mode 100644 index 000000000..b21a27dd7 --- /dev/null +++ b/Development/nmos/authorization_handlers.h @@ -0,0 +1,159 @@ +#ifndef NMOS_AUTHORIZATION_HANDLERS_H +#define NMOS_AUTHORIZATION_HANDLERS_H + +#include +#include +#include "cpprest/api_router.h" +#include "nmos/scope.h" +#include "nmos/settings.h" + +namespace slog +{ + class base_gate; +} + +namespace web +{ + class uri; + + namespace json + { + class value; + } +} + +namespace nmos +{ + struct base_model; + + namespace experimental + { + struct authorization_state; + + struct authorization_error + { + enum status_t + { + succeeded, + without_authentication, // failure: access protected resource request without authentication + insufficient_scope, // failure: access protected resource request requires higher privileges + no_matching_keys, // failure: no matching keys for the token validation + failed // failure: access protected resource request with authentication but failed + }; + + authorization_error() : value(without_authentication) {} + authorization_error(status_t value, const std::string& message = {}) : value(value), message(message) {} + + status_t value; + std::string message; + + operator bool() const { return succeeded == value; } + }; + + namespace fields + { + // authorization_server_uri: the uri of the authorization server, where the client is registered + const web::json::field_as_string_or authorization_server_uri{ U("authorization_server_uri"), U("") }; + + // client_metadata: the registered client metadata + // already defined in nmos/json_fields.h + //const web::json::field_as_value client_metadata{ U("client_metadata") }; + } + + // callback to supply a list of authorization clients + // callbacks from this function are called with the model locked, and may read or write directly to the model + // this callback should not throw exceptions + // example JSON of the authorization client list + // [ + // { + // "authorization_server_uri": "https://example.com" + // }, + // { + // "client_metadata": { + // "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "client_name" : "example client", + // "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ], + // "jwks_uri" : "https://example_client/jwks", + // "redirect_uris" : [ "https://example_client/callback" ], + // "registration_access_token" : "eyJhbGci....", + // "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "response_types" : [ "code" ], + // "scope" : "registration", + // "subject_type" : "public", + // "tls_client_certificate_bound_access_tokens" : false, + // "token_endpoint_auth_method" : "private_key_jwt" + // } + // } + // ] + typedef std::function load_authorization_clients_handler; + + // callback after authorization client has registered + // callbacks from this function are called with the model locked, and may read or write directly to the model + // this callback should not throw exceptions + // example JSON of the client_metadata + // { + // { + // "authorization_server_uri": "https://example.com" + // }, + // { + // "client_metadata": { + // "client_id": "acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "client_name" : "example client", + // "grant_types" : [ "authorization_code", "client_credentials","refresh_token" ], + // "issuer" : "https://example.com", + // "jwks_uri" : "https://example_client/jwks", + // "redirect_uris" : [ "https://example_client/callback" ], + // "registration_access_token" : "eyJhbGci....", + // "registration_client_uri" : "https://example.com/openid-connect/acc8fd35-327d-4486-a02f-9a8fdc25a609", + // "response_types" : [ "code" ], + // "scope" : "registration", + // "subject_type" : "public", + // "tls_client_certificate_bound_access_tokens" : false, + // "token_endpoint_auth_method" : "private_key_jwt" + // } + // } + // } + typedef std::function save_authorization_client_handler; + + // callback on requesting to start off the authorization code grant flow + // callbacks from this function are called with the model locked, and may read or write directly to the model + // this callback should not throw exceptions + typedef std::function request_authorization_code_handler; + + // helper function to load from the authorization clients file + web::json::value load_authorization_clients_file(const utility::string_t& filename, slog::base_gate& gate); + + // helper function to update the authorization clients file + void update_authorization_clients_file(const utility::string_t& filename, const web::json::value& authorization_client, slog::base_gate& gate); + + // construct callback to load a table of authorization server uri vs authorization clients metadata from file based on settings seed_id + load_authorization_clients_handler make_load_authorization_clients_handler(const nmos::settings& settings, slog::base_gate& gate); + + // construct callback to save authorization client metadata to file based on seed_id from settings + save_authorization_client_handler make_save_authorization_client_handler(const nmos::settings& settings, slog::base_gate& gate); + + // construct callback to start the authorization code flow request on a browser + request_authorization_code_handler make_request_authorization_code_handler(slog::base_gate& gate); + + // callback to validate OAuth 2.0 authorization access token + // this callback should not throw exceptions + typedef std::function validate_authorization_token_handler; + // construct callback to validate OAuth 2.0 authorization access token + validate_authorization_token_handler make_validate_authorization_token_handler(authorization_state& authorization_state, slog::base_gate& gate); + + // callback to return the OAuth 2.0 validation route handler + // this callback is executed at the beginning while walking the supported API routes + typedef std::function validate_authorization_handler; + // construct callback to validate OAuth 2.0 authorization + validate_authorization_handler make_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, validate_authorization_token_handler access_token_validation, slog::base_gate& gate); + + // callback to return OAuth 2.0 authorization bearer token + // this callback is execute while create http_client + // this callback should not throw exceptions + typedef std::function get_authorization_bearer_token_handler; + // construct callback to retrieve OAuth 2.0 authorization bearer token + get_authorization_bearer_token_handler make_get_authorization_bearer_token_handler(authorization_state& authorization_state, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/nmos/authorization_operation.cpp b/Development/nmos/authorization_operation.cpp new file mode 100644 index 000000000..b9a6a230e --- /dev/null +++ b/Development/nmos/authorization_operation.cpp @@ -0,0 +1,2019 @@ +#include "nmos/authorization_operation.h" + +#include +#include +#include "cpprest/code_challenge_method.h" +#include "cpprest/json_validator.h" +#include "cpprest/response_type.h" +#include "cpprest/token_endpoint_auth_method.h" +#include "nmos/api_utils.h" +#include "nmos/authorization.h" +#include "nmos/authorization_scopes.h" +#include "nmos/authorization_state.h" +#include "nmos/authorization_utils.h" +#include "nmos/client_utils.h" +#include "nmos/is10_versions.h" +#include "nmos/json_schema.h" +#include "nmos/jwt_generator.h" +#include "nmos/jwk_utils.h" +#include "nmos/model.h" +#include "nmos/random.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace experimental + { + // authorization operation + namespace details + { + static const web::json::experimental::json_validator& authapi_validator() + { + static const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(boost::join(boost::join(boost::join(boost::join(boost::join( + is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_auth_metadata_schema_uri), + is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_jwks_response_schema_uri)), + is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_register_client_response_uri)), + is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_token_error_response_uri)), + is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_token_response_schema_uri)), + is10_versions::all | boost::adaptors::transformed(experimental::make_authapi_token_schema_schema_uri))) + }; + return validator; + } + + // build the scope string with given list of scopes + utility::string_t make_scope(const std::set& scopes_) + { + utility::string_t scopes; + for (const auto& scope : scopes_) + { + if (!scopes.empty()) { scopes += U(" "); } + scopes += scope.name; + } + return scopes; + } + + // build grant array with given list of grants + web::json::value make_grant_types(const std::set& grants) + { + auto grant_types = web::json::value::array(); + for (const auto& grant : grants) + { + web::json::push_back(grant_types, grant.name); + } + return grant_types; + } + + // generate SHA256 with the given string + std::vector sha256(const std::string& text) + { +#if OPENSSL_VERSION_NUMBER < 0x30000000L + uint8_t hash[SHA256_DIGEST_LENGTH]; + SHA256_CTX ctx; + if (SHA256_Init(&ctx) && SHA256_Update(&ctx, text.c_str(), text.size()) && SHA256_Final(hash, &ctx)) + { + return{ hash, hash + SHA256_DIGEST_LENGTH }; + } +#else + typedef std::unique_ptr EVP_MD_CTX_ptr; + uint8_t hash[EVP_MAX_MD_SIZE]; + uint32_t md_len{ 0 }; + EVP_MD_CTX_ptr mdctx(EVP_MD_CTX_new(), &EVP_MD_CTX_free); + if (EVP_DigestInit_ex(mdctx.get(), EVP_sha256(), NULL) && EVP_DigestUpdate(mdctx.get(), text.c_str(), text.size()) && EVP_DigestFinal_ex(mdctx.get(), hash, &md_len)) + { + return{ hash, hash + md_len }; + } +#endif + return{}; + } + + // use the authorization URI on a web browser to start the authorization code flow + web::uri make_authorization_code_uri(const web::uri& authorization_endpoint, const utility::string_t& client_id, const web::uri& redirect_uri, const web::http::oauth2::experimental::response_type& response_type, const std::set& scopes, const web::json::array& code_challenge_methods_supported, utility::string_t& state, utility::string_t& code_verifier) + { + using web::http::oauth2::details::oauth2_strings; + + web::uri_builder ub(authorization_endpoint); + ub.append_query(oauth2_strings::client_id, client_id); + ub.append_query(oauth2_strings::redirect_uri, redirect_uri.to_string()); + ub.append_query(oauth2_strings::response_type, response_type.name); + + // using PKCE? + if (code_challenge_methods_supported.size()) + { + const auto found = std::find_if(code_challenge_methods_supported.begin(), code_challenge_methods_supported.end(), [&](const web::json::value& code_challenge_method) + { + return web::http::oauth2::experimental::code_challenge_methods::S256.name == code_challenge_method.as_string(); + }); + + const auto code_challenge_method = (code_challenge_methods_supported.end() != found) ? web::http::oauth2::experimental::code_challenge_methods::S256 : web::http::oauth2::experimental::code_challenge_methods::plain; + + // code_verifier = high-entropy cryptographic random STRING using the + // unreserved characters[A - Z] / [a - z] / [0 - 9] / "-" / "." / "_" / "~" + // from Section 2.3 of[RFC3986], with a minimum length of 43 characters + // and a maximum length of 128 characters + // see https://tools.ietf.org/html/rfc7636#section-4.1 + { + utility::nonce_generator generator(128); + code_verifier = generator.generate(); + } + + // creates code challenge from code verifier + // see https://tools.ietf.org/html/rfc7636#section-4.2 + utility::string_t code_challenge{}; + if (web::http::oauth2::experimental::code_challenge_methods::plain == code_challenge_method) + { + code_challenge = code_verifier; + } + else + { + const auto sha256 = nmos::experimental::details::sha256(utility::us2s(code_verifier)); + code_challenge = utility::conversions::to_base64url(sha256); + } + ub.append_query(U("code_challenge"), code_challenge); + ub.append_query(U("code_challenge_method"), code_challenge_method.name); + } + + utility::nonce_generator generator; + state = generator.generate(); + ub.append_query(oauth2_strings::state, state); + + if (scopes.size()) + { + ub.append_query(oauth2_strings::scope, make_scope(scopes)); + } + + return ub.to_uri(); + } + + // used to strip the trailing dot of the FQDN if it is presented + utility::string_t strip_trailing_dot(const utility::string_t& host_) + { + auto host = host_; + if (!host.empty() && U('.') == host.back()) + { + host.pop_back(); + } + return host; + } + + // construct the redirect URI from settings + // format of the authorization_redirect_uri "://:/x-authorization/callback/" + web::uri make_authorization_redirect_uri(const nmos::settings& settings) + { + return web::uri_builder() + .set_scheme(web::http_scheme(nmos::experimental::fields::client_secure(settings))) + .set_host(nmos::experimental::fields::no_trailing_dot_for_authorization_callback_uri(settings) ? strip_trailing_dot(get_host(settings)) : get_host(settings)) + .set_port(nmos::experimental::fields::authorization_redirect_port(settings)) + .set_path(U("/x-authorization/callback")) + .to_uri(); + } + + // construct the jwks URI from settings + // format of the jwks_uri "://:/x-authorization/jwks/" + web::uri make_jwks_uri(const nmos::settings& settings) + { + return web::uri_builder() + .set_scheme(web::http_scheme(nmos::experimental::fields::client_secure(settings))) + .set_host(nmos::experimental::fields::no_trailing_dot_for_authorization_callback_uri(settings) ? strip_trailing_dot(get_host(settings)) : get_host(settings)) + .set_port(nmos::experimental::fields::jwks_uri_port(settings)) + .set_path(U("/x-authorization/jwks")) + .to_uri(); + } + + // construct the authorization server URI using the given URI authority + // format of the authorization_service_uri "://:/.well-known/oauth-authorization-server[/]" + web::uri make_authorization_service_uri(const web::uri& uri, const utility::string_t& api_selector = {}) + { + return web::uri_builder(uri.authority()).set_path(U("/.well-known/oauth-authorization-server")).append_path(!api_selector.empty() ? U("/") + api_selector : U("")).to_uri(); + } + + // construct authorization client config based on settings + // with the remaining options defaulted, e.g. authorization request timeout + web::http::client::http_client_config make_authorization_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) + { + auto config = nmos::make_http_client_config(settings, load_ca_certificates, bearer_token, gate); + config.set_timeout(std::chrono::seconds(nmos::experimental::fields::authorization_request_max(settings))); + + return config; + } + + struct authorization_exception {}; + + // parse the given json to obtain bearer token + // this function is based on the oauth2_config::_parse_token_from_json(const json::value& token_json) from cpprestsdk's oauth2.cpp + web::http::oauth2::experimental::oauth2_token parse_token_from_json(const web::json::value& token_json) + { + using web::http::oauth2::details::oauth2_strings; + using web::http::oauth2::experimental::oauth2_token; + using web::http::oauth2::experimental::oauth2_exception; + + oauth2_token result; + + if (token_json.has_string_field(oauth2_strings::access_token)) + { + result.set_access_token(token_json.at(oauth2_strings::access_token).as_string()); + } + else + { +#if defined (NDEBUG) + throw oauth2_exception(U("response json contains no 'access_token'")); +#else + throw oauth2_exception(U("response json contains no 'access_token': ") + token_json.serialize()); +#endif + } + + if (token_json.has_string_field(oauth2_strings::token_type)) + { + result.set_token_type(token_json.at(oauth2_strings::token_type).as_string()); + } + else + { + // Some services don't return 'token_type' even though it's required by the OAuth 2.0 spec: + // http://tools.ietf.org/html/rfc6749#section-5.1 + // As workaround we act as if 'token_type=bearer' was received. + result.set_token_type(oauth2_strings::bearer); + } + if (!utility::details::str_iequal(result.token_type(), oauth2_strings::bearer)) + { +#if defined (NDEBUG) + throw oauth2_exception(U("only bearer tokens are currently supported")); +#else + throw oauth2_exception(U("only bearer tokens are currently supported: ") + token_json.serialize()); +#endif + } + + if (token_json.has_string_field(oauth2_strings::refresh_token)) + { + result.set_refresh_token(token_json.at(oauth2_strings::refresh_token).as_string()); + } + else + { + // Do nothing. Preserves the old refresh token + } + + if (token_json.has_field(oauth2_strings::expires_in)) + { + const auto& json_expires_in_val = token_json.at(oauth2_strings::expires_in); + + if (json_expires_in_val.is_number()) + { + result.set_expires_in(json_expires_in_val.as_number().to_int64()); + } + else + { + // Handle the case of a number as a JSON "string" + int64_t expires; + utility::istringstream_t iss(json_expires_in_val.as_string()); + iss.exceptions(std::ios::badbit | std::ios::failbit); + iss >> expires; + result.set_expires_in(expires); + } + } + else + { + result.set_expires_in(oauth2_token::undefined_expiration); + } + + if (token_json.has_string_field(oauth2_strings::scope)) + { + // The authorization server may return different scope from the one requested + // This however doesn't necessarily mean the token authorization scope is different + // See: http://tools.ietf.org/html/rfc6749#section-3.3 + result.set_scope(token_json.at(oauth2_strings::scope).as_string()); + } + + return result; + } + + // make an asynchronously GET request on the Authorization API to fetch authorization server metadata + pplx::task request_authorization_server_metadata(web::http::client::http_client client, const std::set& scopes, const std::set& grants, const web::http::oauth2::experimental::token_endpoint_auth_method& token_endpoint_auth_method, const nmos::api_version& version, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting authorization server metadata at " << client.base_uri().to_string(); + + using namespace web::http; + + // ://:/.well-known/oauth-authorization-server[/] + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/3.0._Discovery.html#authorization-server-metadata-endpoint + return nmos::api_request(client, methods::GET, gate, token).then([=, &gate](pplx::task response_task) + { + namespace response_types = web::http::oauth2::experimental::response_types; + namespace grant_types = web::http::oauth2::experimental::grant_types; + + auto response = response_task.get(); // may throw http_exception + + if (status_codes::OK == response.status_code()) + { + if (response.body()) + { + return response.extract_json().then([=, &gate](web::json::value metadata) + { + // validate server metadata + authapi_validator().validate(metadata, experimental::make_authapi_auth_metadata_schema_uri(version)); // may throw json_exception + + // hmm, verify Authorization server meets the minimum client requirement. + + // are the required response_types supported by the Authorization server? + std::set response_types = { response_types::code }; + if (grants.end() != std::find_if(grants.begin(), grants.end(), [](const web::http::oauth2::experimental::grant_type& grant) { return grant_types::implicit == grant; })) + { + response_types.insert(response_types::token); + } + if (response_types.size()) + { + const auto supported = std::all_of(response_types.begin(), response_types.end(), [&](const web::http::oauth2::experimental::response_type& response_type) + { + const auto& response_types_supported = nmos::experimental::fields::response_types_supported(metadata); + const auto found = std::find_if(response_types_supported.begin(), response_types_supported.end(), [&response_type](const web::json::value& response_type_) { return response_type_.as_string() == response_type.name; }); + return response_types_supported.end() != found; + }); + if (!supported) + { + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: server does not supporting all the required response types"; + throw authorization_exception(); + } + } + + // scopes_supported is optional + if (scopes.size() && metadata.has_array_field(nmos::experimental::fields::scopes_supported)) + { + // are the required scopes supported by the Authorization server? + const auto supported = std::all_of(scopes.begin(), scopes.end(), [&](const nmos::experimental::scope& scope) + { + const auto& scopes_supported = nmos::experimental::fields::scopes_supported(metadata); + const auto found = std::find_if(scopes_supported.begin(), scopes_supported.end(), [&scope](const web::json::value& scope_) { return scope_.as_string() == scope.name; }); + return scopes_supported.end() != found; + }); + if (!supported) + { + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: server does not support all the required scopes: " << [&scopes]() { std::stringstream ss; for (auto scope : scopes) ss << utility::us2s(scope.name) << " "; return ss.str(); }(); + throw authorization_exception(); + } + } + + // grant_types_supported is optional + if (grants.size() && metadata.has_array_field(nmos::experimental::fields::grant_types_supported)) + { + // are the required grants supported by the Authorization server? + const auto supported = std::all_of(grants.begin(), grants.end(), [&](const web::http::oauth2::experimental::grant_type& grant) + { + const auto& grants_supported = nmos::experimental::fields::grant_types_supported(metadata); + const auto found = std::find_if(grants_supported.begin(), grants_supported.end(), [&grant](const web::json::value& grant_) { return grant_.as_string() == grant.name; }); + return grants_supported.end() != found; + }); + if (!supported) + { + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: server does not support all the required grants: " << [&grants]() { std::stringstream ss; for (auto grant : grants) ss << utility::us2s(grant.name) << " "; return ss.str(); }(); + throw authorization_exception(); + } + } + + // token_endpoint_auth_methods_supported is optional + if (metadata.has_array_field(nmos::experimental::fields::token_endpoint_auth_methods_supported)) + { + // is the required token_endpoint_auth_method supported by the Authorization server? + const auto& supported = nmos::experimental::fields::token_endpoint_auth_methods_supported(metadata); + const auto found = std::find_if(supported.begin(), supported.end(), [&token_endpoint_auth_method](const web::json::value& token_endpoint_auth_method_) { return token_endpoint_auth_method_.as_string() == token_endpoint_auth_method.name; }); + if (supported.end() == found) + { + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: server does not supporting the required token_endpoint_auth_method:" << token_endpoint_auth_method.name; + throw authorization_exception(); + } + } + + slog::log(gate, SLOG_FLF) << "Received authorization server metadata: " << utility::us2s(metadata.serialize()); + return metadata; + }, token); + } + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: no response body"; + } + else + { + slog::log(gate, SLOG_FLF) << "Request authorization server metadata error: " << response.status_code() << " " << response.reason_phrase(); + } + throw authorization_exception(); + + }, token); + } + + // make an asynchronously POST request on the Authorization API to register a client + // see https://tools.ietf.org/html/rfc6749#section-2 + // see https://tools.ietf.org/html/rfc7591#section-3.1 + // e.g. curl -X POST "https://authorization.server.example.com/register" -H "Content-Type: application/json" -d "{\"redirect_uris\": [\"https://client.example.com/callback/\"],\"client_name\": \"My Example Client\",\"client_uri\": \"https://client.example.com/details.html\",\"token_endpoint_auth_method\": \"client_secret_basic\",\"response_types\": [\"code\",\"token\"],\"scope\": \"registration query node connection\",\"grant_types\": [\"authorization_code\",\"refresh_token\",\"client_credentials\"],\"token_endpoint_auth_method\": \"client_secret_basic\"}" + pplx::task request_client_registration(web::http::client::http_client client, const utility::string_t& client_name, const std::vector& redirect_uris, const web::uri& client_uri, const std::set& response_types, const std::set& scopes, const std::set& grants, const web::http::oauth2::experimental::token_endpoint_auth_method& token_endpoint_auth_method, const web::json::value& jwk, const web::uri& jwks_uri, const nmos::api_version& version, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting authorization client registration at " << client.base_uri().to_string(); + + using namespace web; + using namespace web::http; + using web::json::value; + using web::json::value_of; + + const auto make_uris = [](const std::vector& uris) + { + auto result = value::array(); + for (const auto& uri : uris) { web::json::push_back(result, uri.to_string()); } + return result; + }; + + const auto make_response_types = [](const std::set& response_types) + { + auto result = value::array(); + for (const auto& response_type : response_types) { web::json::push_back(result, response_type.name); } + return result; + }; + + const auto make_scope = [](const std::set& scopes) + { + std::ostringstream os; + int idx{ 0 }; + for (const auto& scope : scopes) + { + if (idx++) { os << " "; } + os << utility::us2s(scope.name); + } + return value(utility::s2us(os.str())); + }; + + const auto make_grant_type = [](const std::set& grants) + { + auto result = value::array(); + for (const auto& grant : grants) { web::json::push_back(result, grant.name); } + return result; + }; + + // required + auto metadata = value_of({ + { nmos::experimental::fields::client_name, client_name } + }); + + // optional + if (grants.end() != std::find_if(grants.begin(), grants.end(), [](const web::http::oauth2::experimental::grant_type& grant) { return web::http::oauth2::experimental::grant_types::authorization_code == grant; })) + { + metadata[nmos::experimental::fields::redirect_uris] = make_uris(redirect_uris); + } + if (!client_uri.is_empty()) + { + metadata[nmos::experimental::fields::client_uri] = value::string(client_uri.to_string()); + } + if (response_types.size()) + { + metadata[nmos::experimental::fields::response_types] = make_response_types(response_types); + } + if (scopes.size()) + { + metadata[nmos::experimental::fields::scope] = make_scope(scopes); + } + if (grants.size()) + { + metadata[nmos::experimental::fields::grant_types] = make_grant_type(grants); + } + + metadata[nmos::experimental::fields::token_endpoint_auth_method] = value::string(token_endpoint_auth_method.name); + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt == token_endpoint_auth_method) + { + if (!jwks_uri.is_empty()) + { + metadata[nmos::experimental::fields::jwks_uri] = value::string(jwks_uri.to_string()); + } + else + { + metadata[nmos::experimental::fields::jwks] = value_of({ + { nmos::experimental::fields::keys, value_of({ jwk }) } + }); + } + } + + slog::log(gate, SLOG_FLF) << "Request to register client metadata: " << utility::us2s(metadata.serialize()) << " at " << client.base_uri().to_string(); + + return nmos::api_request(client, methods::POST, {}, metadata, gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (response.body()) + { + return response.extract_json().then([=, &gate](web::json::value client_metadata) + { + if (status_codes::Created == response.status_code()) + { + slog::log(gate, SLOG_FLF) << "Registered client metadata: " << utility::us2s(client_metadata.serialize()); + + // validate client metadata + authapi_validator().validate(client_metadata, experimental::make_authapi_register_client_response_uri(version)); // may throw json_exception + + return client_metadata; + } + else + { + slog::log(gate, SLOG_FLF) << "Request client registration error: " << response.status_code() << " " << response.reason_phrase() << " " << utility::us2s(client_metadata.serialize()); + throw authorization_exception(); + } + }, token); + } + slog::log(gate, SLOG_FLF) << "Request client registration error: " << response.status_code() << " " << response.reason_phrase(); + throw authorization_exception(); + + }, token); + } + + // make an asynchronously GET request on the Authorization API to fetch the authorization JSON Web Keys (public keys) + pplx::task request_jwks(web::http::client::http_client client, const nmos::api_version& version, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting authorization jwks at " << client.base_uri().to_string(); + + using namespace web::http; + using oauth2::experimental::oauth2_exception; + + return nmos::api_request(client, methods::GET, gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (status_codes::OK == response.status_code()) + { + if (response.body()) + { + return nmos::details::extract_json(response, gate).then([version, &gate](web::json::value body) + { + // validate jwks JSON + authapi_validator().validate(body, experimental::make_authapi_jwks_response_schema_uri(version)); // may throw json_exception + + // MUST have a "keys" member! + // see https://tools.ietf.org/html/rfc7517#section-5 + if (!body.has_array_field(U("keys"))) throw web::http::http_exception(U("jwks contains no 'keys': ") + body.serialize()); + + const auto jwks = body.at(U("keys")); + + jwks.as_array().size() ? slog::log(gate, SLOG_FLF) << "Received authorization jwks: " << utility::us2s(jwks.serialize()) : + slog::log(gate, SLOG_FLF) << "Request authorization jwks: no jwk"; + + return jwks; + + }, token); + } + else + { + slog::log(gate, SLOG_FLF) << "Request authorization jwks error: no response body"; + } + } + else + { + slog::log(gate, SLOG_FLF) << "Request authorization jwks error: " << response.status_code() << " " << response.reason_phrase(); + } + throw authorization_exception(); + + }, token); + } + + // make an asynchronously GET request on the OpenID Connect Authorization API to fetch the client metdadata + pplx::task request_client_metadata_from_openid_connect(web::http::client::http_client client, const nmos::api_version& version, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting OpenID Connect client metadata at " << client.base_uri().to_string(); + + using namespace web::http; + + return api_request(client, methods::GET, gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (response.body()) + { + return nmos::details::extract_json(response, gate).then([=, &gate](web::json::value body) + { + if (status_codes::OK == response.status_code()) + { + slog::log(gate, SLOG_FLF) << "Received OpenID Connect client metadata: " << utility::us2s(body.serialize()); + + // validate client metadata JSON + authapi_validator().validate(body, experimental::make_authapi_register_client_response_uri(version)); // may throw json_exception + + return body; + } + else + { + slog::log(gate, SLOG_FLF) << "Requesting OpenID Connect client metadata error: " << response.status_code() << " " << response.reason_phrase() << " " << utility::us2s(body.serialize()); + throw authorization_exception(); + } + }); + } + slog::log(gate, SLOG_FLF) << "Requesting OpenID Connect client metadata error: no response json: no client metadata"; + throw authorization_exception(); + + }, token); + } + + // make an asynchronously POST request on the Authorization API to fetch the bearer token, + // this is a helper function which is used by the request_token_from_client_credentials and request_token_from_refresh_token + // see https://medium.com/@software_factotum/pkce-public-clients-and-refresh-token-d1faa4ef6965#:~:text=Refresh%20Token%20are%20credentials%20that,application%20needs%20additional%20access%20tokens.&text=Authorization%20Server%20may%20issue%20a,Client%20it%20was%20issued%20to. + pplx::task request_token(web::http::client::http_client client, const nmos::api_version& version, web::uri_builder& request_body_ub, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token at " << client.base_uri().to_string(); + + using namespace web::http; + using oauth2::details::oauth2_strings; + using oauth2::experimental::oauth2_exception; + using oauth2::experimental::oauth2_token; + using web::http::details::mime_types; + + if (!scope.empty()) + { + request_body_ub.append_query(oauth2_strings::scope, uri::encode_data_string(scope), false); + } + + http_request req(methods::POST); + + if (client_secret.empty()) + { + if (!client_id.empty()) + { + // for Public Client or using private_key_jwt just append the client_id to query + request_body_ub.append_query(oauth2_strings::client_id, client_id, false); + } + } + else + { + // for Confidential Client and not using private_key_jwt + // Build HTTP Basic authorization header with 'client_id' and 'client_secret' + const std::string creds_utf8(utility::conversions::to_utf8string(uri::encode_data_string(client_id) + U(":") + uri::encode_data_string(client_secret))); + req.headers().add(header_names::authorization, U("Basic ") + utility::conversions::to_base64(std::vector(creds_utf8.begin(), creds_utf8.end()))); + } + + req.set_body(request_body_ub.query(), mime_types::application_x_www_form_urlencoded); + + return nmos::api_request(client, req, gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (response.body()) + { + return nmos::details::extract_json(response, gate).then([=, &gate](web::json::value body) + { + if (status_codes::OK == response.status_code()) + { +#if defined (NDEBUG) + slog::log(gate, SLOG_FLF) << "Received bearer token"; +#else + slog::log(gate, SLOG_FLF) << "Received bearer token: " << utility::us2s(body.serialize()); +#endif + // validate bearer token JSON + authapi_validator().validate(body, experimental::make_authapi_token_response_schema_uri(version)); // may throw json_exception + + return parse_token_from_json(body); // may throw oauth2_exception + } + else + { + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token error: " << response.status_code() << " " << utility::us2s(body.serialize()); + + // validate token error response JSON + authapi_validator().validate(body, experimental::make_authapi_token_error_response_uri(version)); // may throw json_exception + + throw authorization_exception(); + } + }); + } + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token error: no response json: no bearer token"; + throw authorization_exception(); + + }, token); + } + + // make an asynchronously POST request on the Authorization API to fetch the bearer token using client_credentials grant + pplx::task request_token_from_client_credentials(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using client_credentials grant at " << client.base_uri().to_string(); + + using web::http::oauth2::details::oauth2_strings; + + web::uri_builder ub; + ub.append_query(oauth2_strings::grant_type, U("client_credentials"), false); + + return request_token(client, version, ub, client_id, client_secret, scope, gate, token); + } + + // make an asynchronously POST request on the Authorization API to fetch the bearer token using client_credentials grant with private_key_jwt for client authentication + pplx::task request_token_from_client_credentials_using_private_key_jwt(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using client_credentials grant with private_key_jwt at " << client.base_uri().to_string(); + + using web::http::oauth2::details::oauth2_strings; + + web::uri_builder ub; + ub.append_query(oauth2_strings::grant_type, U("client_credentials"), false); + + // use private_key_jwt client authentication + // see https://tools.ietf.org/html/rfc7523#section-2.2 + ub.append_query(U("client_assertion_type"), U("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), false); + ub.append_query(U("client_assertion"), client_assertion, false); + + return request_token(client, version, ub, client_id, {}, scope, gate, token); + } + + web::uri_builder make_request_token_base_query(const utility::string_t& code, const utility::string_t& redirect_uri, const utility::string_t& code_verifier) + { + using web::http::oauth2::details::oauth2_strings; + + web::uri_builder ub; + ub.append_query(oauth2_strings::grant_type, oauth2_strings::authorization_code, false); + ub.append_query(oauth2_strings::code, web::uri::encode_data_string(code), false); + ub.append_query(oauth2_strings::redirect_uri, web::uri::encode_data_string(redirect_uri), false); + ub.append_query(U("code_verifier"), code_verifier, false); + return ub; + } + + // make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token + pplx::task request_token_from_authorization_code(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& code, const utility::string_t& redirect_uri, const utility::string_t& code_verifier, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Exchanging authorization code: " << utility::us2s(code) << " for bearer token with code_verifier: " << utility::us2s(code_verifier) << " at " << client.base_uri().to_string(); + + auto ub = make_request_token_base_query(code, redirect_uri, code_verifier); + + return request_token(client, version, ub, client_id, client_secret, scope, gate, token); + } + + // make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token with private_key_jwt for client authentication + pplx::task request_token_from_authorization_code_with_private_key_jwt(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& code, const utility::string_t& redirect_uri, const utility::string_t& code_verifier, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Exchanging authorization code: " << utility::us2s(code) << " for bearer token with private_key_jwt and code_verifier: " << utility::us2s(code_verifier) << " and client_assertion: " << utility::us2s(client_assertion) << " at " << client.base_uri().to_string(); + + auto ub = make_request_token_base_query(code, redirect_uri, code_verifier); + // use private_key_jwt client authentication + // see https://tools.ietf.org/html/rfc7523#section-2.2 + ub.append_query(U("client_assertion_type"), U("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), false); + ub.append_query(U("client_assertion"), client_assertion, false); + + return request_token(client, version, ub, client_id, {}, scope, gate, token); + } + + web::uri_builder make_request_token_base_query(const utility::string_t& refresh_token) + { + using web::http::oauth2::details::oauth2_strings; + + web::uri_builder ub; + ub.append_query(oauth2_strings::grant_type, oauth2_strings::refresh_token, false); + ub.append_query(oauth2_strings::refresh_token, web::uri::encode_data_string(refresh_token), false); + return ub; + } + + // make an asynchronously POST request on the Authorization API to fetch the bearer token using refresh_token grant + pplx::task request_token_from_refresh_token(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& refresh_token, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using refresh_token grant at " << client.base_uri().to_string(); + + auto ub = make_request_token_base_query(refresh_token); + + return request_token(client, version, ub, client_id, client_secret, scope, gate, token); + } + + // make an asynchronously POST request on the Authorization API to fetch the bearer token using refresh_token grant with private_key_jwt for client authentication + pplx::task request_token_from_refresh_token_using_private_key_jwt(web::http::client::http_client client, const nmos::api_version& version, const utility::string_t& client_id, const utility::string_t& scope, const utility::string_t& refresh_token, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token using refresh_token grant with private_key_jwt at " << client.base_uri().to_string(); + + using web::http::oauth2::details::oauth2_strings; + + auto ub = make_request_token_base_query(refresh_token); + + // use private_key_jwt client authentication + // see https://tools.ietf.org/html/rfc7523#section-2.2 + ub.append_query(U("client_assertion_type"), U("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"), false); + ub.append_query(U("client_assertion"), client_assertion, false); + + return request_token(client, version, ub, client_id, {}, scope, gate, token); + } + + // verify the redirect URI and make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token with private_key_jwt for client authentication + // this function is based on the oauth2_config::token_from_redirected_uri + pplx::task request_token_from_redirected_uri(web::http::client::http_client client, const nmos::api_version& version, const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& redirect_uri, const utility::string_t& state, const utility::string_t& code_verifier, const web::http::oauth2::experimental::token_endpoint_auth_method& token_endpoint_auth_method, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token) + { + using web::http::oauth2::experimental::oauth2_exception; + using web::http::oauth2::details::oauth2_strings; + namespace response_types = web::http::oauth2::experimental::response_types; + + std::map query; + + // for Authorization Code Grant Type Response (response_type = code) + // "If the resource owner grants the access request, the authorization + // server issues an authorization codeand delivers it to the client by + // adding the following parameters to the query component of the + // redirection URI using the "application/x-www-form-urlencoded" format + // + // For example, the authorization server redirects the user-agent by + // sending the following HTTP response : + // HTTP / 1.1 302 Found + // Location : https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz + // see https://tools.ietf.org/html/rfc6749#section-4.1.2 + if (response_type == response_types::code.name) + { + query = web::uri::split_query(redirected_uri.query()); + } + // for Implicit Grant Type Response (response_type = token) + // "If the resource owner grants the access request, the authorization + // server issues an access tokenand delivers it to the client by adding + // the following parameters to the fragment component of the redirection + // URI using the "application/x-www-form-urlencoded" format" + // + // For example, the authorization server redirects the user-agent by + // sending the following HTTP response + // HTTP / 1.1 302 Found + // Location : http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA&state=xyz&token_type=example&expires_in=3600 + else if (response_type == response_types::token.name) + { + query = web::uri::split_query(redirected_uri.fragment()); + } + else + { + throw oauth2_exception(U("response_type: '") + response_type + U("' is not supported")); + } + + auto state_param = query.find(oauth2_strings::state); + if (state_param == query.end()) + { + throw oauth2_exception(U("parameter 'state' missing from redirected URI")); + } + + if (state != state_param->second) + { + throw oauth2_exception(U("parameter 'state': '") + state_param->second + U("' does not match with the expected 'state': '") + state + U("'")); + } + + // for Authorization Code Grant Type Response (response_type = code) + // do request_token_from_authorization_code + if (response_type == response_types::code.name) + { + auto code_param = query.find(oauth2_strings::code); + if (code_param == query.end()) + { + throw oauth2_exception(U("parameter 'code' missing from redirected URI")); + } + + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt == token_endpoint_auth_method) + { + return request_token_from_authorization_code_with_private_key_jwt(client, version, client_id, scope, code_param->second, redirect_uri, code_verifier, client_assertion, gate, token); + } + else if (web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic == token_endpoint_auth_method) + { + return request_token_from_authorization_code(client, version, client_id, client_secret, scope, code_param->second, redirect_uri, code_verifier, gate, token); + } + else + { + throw oauth2_exception(U("token_endpoint_auth_method: '") + token_endpoint_auth_method.name + U("' is not curently supported")); + } + } + + // for Implicit Grant Type Response (response_type = token) + // extract access token from query parameters + auto token_type_param = query.find(oauth2_strings::token_type); + if (token_type_param == query.end()) + { + throw oauth2_exception(U("parameter 'token_type' missing from redirected URI")); + } + + if (boost::algorithm::to_lower_copy(token_type_param->second) != U("bearer")) + { + throw oauth2_exception(U("invalid parameter 'token_type': '") + token_type_param->second + U("', expecting 'bearer'")); + } + + auto token_param = query.find(oauth2_strings::access_token); + if (token_param == query.end()) + { + throw oauth2_exception(U("parameter 'access_token' missing from redirected URI")); + } + + return pplx::task_from_result(web::http::oauth2::experimental::oauth2_token(token_param->second)); + } + + struct token_shared_state + { + web::http::oauth2::experimental::grant_type grant_type; + web::http::oauth2::experimental::oauth2_token bearer_token; + std::unique_ptr client; + nmos::api_version version; // issuer version + load_rsa_private_keys_handler load_rsa_private_keys; + bool immediate; // true = do an immediate fetch; false = loop based on time interval + + explicit token_shared_state(web::http::oauth2::experimental::grant_type grant_type, web::http::oauth2::experimental::oauth2_token bearer_token, web::http::client::http_client client, nmos::api_version version, load_rsa_private_keys_handler load_rsa_private_keys, bool immediate) + : grant_type(std::move(grant_type)) + , bearer_token(std::move(bearer_token)) + , client(std::unique_ptr(new web::http::client::http_client(client))) + , version(std::move(version)) + , load_rsa_private_keys(std::move(load_rsa_private_keys)) + , immediate(immediate) {} + }; + + // task to continuously fetch the bearer token on a time interval until failure or cancellation + pplx::task do_token_requests(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, token_shared_state& token_state, bool& authorization_service_error, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + const auto access_token_refresh_interval = nmos::experimental::fields::access_token_refresh_interval(model.settings); + const auto authorization_server_metadata = nmos::experimental::get_authorization_server_metadata(authorization_state); + const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); + const auto client_id = nmos::experimental::fields::client_id(client_metadata); + const auto client_secret = client_metadata.has_string_field(nmos::experimental::fields::client_secret) ? nmos::experimental::fields::client_secret(client_metadata) : U(""); + const auto scope = nmos::experimental::fields::scope(client_metadata); + const auto token_endpoint_auth_method = client_metadata.has_string_field(nmos::experimental::fields::token_endpoint_auth_method) ? + web::http::oauth2::experimental::to_token_endpoint_auth_method(nmos::experimental::fields::token_endpoint_auth_method(client_metadata)) : web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic; + const auto token_endpoint = nmos::experimental::fields::token_endpoint(authorization_server_metadata); + const auto client_assertion_lifespan = std::chrono::seconds(nmos::experimental::fields::authorization_request_max(model.settings)); + + // start a background task for continuous fetching bearer token in a time interval + return pplx::do_while([=, &model, &authorization_state, &token_state, &gate] + { + auto fetch_interval = std::chrono::seconds(0); + if (!token_state.immediate && token_state.bearer_token.is_valid_access_token()) + { + // RECOMMENDED to attempt a refresh at least 15 seconds before expiry (i.e the half-life of the shortest-lived token possible) + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#refreshing-a-token + fetch_interval = access_token_refresh_interval < 0 ? std::chrono::seconds(token_state.bearer_token.expires_in() / 2) : std::chrono::seconds(access_token_refresh_interval); + } + token_state.immediate = false; + + slog::log(gate, SLOG_FLF) << "Requesting '" << utility::us2s(scope) << "' bearer token for about " << fetch_interval.count() << " seconds"; + + auto fetch_time = std::chrono::steady_clock::now(); + return pplx::complete_at(fetch_time + fetch_interval, token).then([=, &model, &token_state, &gate]() + { + // create client assertion using private key jwt + utility::string_t client_assertion; + with_read_lock(model.mutex, [&] + { + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt == token_endpoint_auth_method) + { + // use the 1st RSA private key from RSA private keys list to create the client_assertion + if (!token_state.load_rsa_private_keys) + { + throw web::http::oauth2::experimental::oauth2_exception(U("missing RSA private key loader to extract RSA private key")); + } + auto rsa_private_keys = token_state.load_rsa_private_keys(); + if (rsa_private_keys.empty() || rsa_private_keys[0].empty()) + { + throw web::http::oauth2::experimental::oauth2_exception(U("no RSA key to create client assertion")); + } + client_assertion = jwt_generator::create_client_assertion(client_id, client_id, token_endpoint, client_assertion_lifespan, rsa_private_keys[0], U("1")); + } + }); + + if (web::http::oauth2::experimental::grant_types::authorization_code == token_state.grant_type) + { + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt == token_endpoint_auth_method) + { + return request_token_from_refresh_token_using_private_key_jwt(*token_state.client, token_state.version, client_id, scope, token_state.bearer_token.refresh_token(), client_assertion, gate, token); + } + else + { + return request_token_from_refresh_token(*token_state.client, token_state.version, client_id, client_secret, scope, token_state.bearer_token.refresh_token(), gate, token); + } + } + else if (web::http::oauth2::experimental::grant_types::client_credentials == token_state.grant_type) + { + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt == token_endpoint_auth_method) + { + return request_token_from_client_credentials_using_private_key_jwt(*token_state.client, token_state.version, client_id, scope, client_assertion, gate, token); + } + else + { + return request_token_from_client_credentials(*token_state.client, token_state.version, client_id, client_secret, scope, gate, token); + } + } + else + { + throw web::http::oauth2::experimental::oauth2_exception(U("Unsupported grant: ") + token_state.grant_type.name); + } + + }).then([=, &authorization_state, &token_state, &gate](const web::http::oauth2::experimental::oauth2_token& bearer_token) + { + token_state.bearer_token = bearer_token; + + // update token in authorization settings + auto lock = authorization_state.write_lock(); + authorization_state.bearer_token = token_state.bearer_token; + slog::log(gate, SLOG_FLF) << "'" << utility::us2s(scope) << "' bearer token updated"; + + return true; + }); + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request JSON error: " << e.what(); + } + catch (const web::http::oauth2::experimental::oauth2_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request OAuth 2.0 error: " << e.what(); + } + catch (const nmos::experimental::jwk_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request JWK error: " << e.what(); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request error: " << e.what(); + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request error"; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization API Bearer token request unexpected unknown exception"; + } + + // reaching here indicates something has gone wrong with the Authorization Server + // so let's select the next available Authorization server + authorization_service_error = true; + + model.notify(); + }); + } + + // task to continuously fetch the authorization server public keys on a time interval until failure or cancellation + pplx::task do_public_keys_requests(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, pubkeys_shared_state& pubkeys_state, bool& authorization_service_error, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + const auto fetch_interval_min(nmos::experimental::fields::fetch_authorization_public_keys_interval_min(model.settings)); + const auto fetch_interval_max(nmos::experimental::fields::fetch_authorization_public_keys_interval_max(model.settings)); + + // start a background task to fetch public keys on a time interval + return pplx::do_while([=, &model, &authorization_state, &pubkeys_state, &gate] + { + auto fetch_interval = std::chrono::seconds(0); + if (nmos::with_read_lock(authorization_state.mutex, [&] + { + auto issuer = authorization_state.issuers.find(pubkeys_state.issuer.to_string()); + return ((authorization_state.issuers.end() != issuer) && !pubkeys_state.immediate); + })) + { + fetch_interval = std::chrono::seconds((int)(std::uniform_real_distribution<>(fetch_interval_min, fetch_interval_max)(pubkeys_state.engine))); + } + nmos::with_write_lock(authorization_state.mutex, [&] { pubkeys_state.immediate = false; }); + slog::log(gate, SLOG_FLF) << "Requesting authorization public keys (jwks) for about " << fetch_interval.count() << " seconds"; + + auto fetch_time = std::chrono::steady_clock::now(); + return pplx::complete_at(fetch_time + fetch_interval, token).then([=, &authorization_state, &pubkeys_state, &gate]() + { + return details::request_jwks(*pubkeys_state.client, pubkeys_state.version, gate, token); + + }).then([&authorization_state, &pubkeys_state, &gate](web::json::value jwks_) + { + const auto jwks = nmos::experimental::get_jwks(authorization_state, pubkeys_state.issuer); + + // are changes found in new set of jwks? + if(jwks != jwks_) + { + // convert jwks to array of public keys + auto pems = web::json::value::array(); + for (const auto& jwk : jwks_.as_array()) + { + try + { + const auto pem = jwk_to_rsa_public_key(jwk); // can throw jwk_exception + + web::json::push_back(pems, web::json::value_of({ + { U("jwk"), jwk }, + { U("pem"), pem } + })); + } + catch (const jwk_exception& e) + { + slog::log(gate, SLOG_FLF) << "Invalid jwk from " << utility::us2s(pubkeys_state.issuer.to_string()) << " JWK error: " << e.what(); + } + } + + // update jwks and jwt validator cache + if (pems.as_array().size()) + { + nmos::experimental::update_jwks(authorization_state, pubkeys_state.issuer, jwks_, nmos::experimental::jwt_validator(pems, [&pubkeys_state](const web::json::value& payload) + { + // validate access token payload JSON + authapi_validator().validate(payload, experimental::make_authapi_token_schema_schema_uri(pubkeys_state.version)); // may throw json_exception + })); + + slog::log(gate, SLOG_FLF) << "JSON Web Token validator updated using an new set of public keys for " << utility::us2s(pubkeys_state.issuer.to_string()); + } + else + { + nmos::experimental::erase_jwks(authorization_state, pubkeys_state.issuer); + + slog::log(gate, SLOG_FLF) << "Clear JSON Web Token validator due to receiving an empty public key list for " << utility::us2s(pubkeys_state.issuer.to_string()); + } + } + else + { + slog::log(gate, SLOG_FLF) << "No public keys changes found for " << utility::us2s(pubkeys_state.issuer.to_string()); + } + + return true; + }); + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + + nmos::with_write_lock(authorization_state.mutex, [&] { pubkeys_state.received = true; }); + + authorization_service_error = false; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + + authorization_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request JSON error: " << e.what(); + + authorization_service_error = true; + } + catch (const web::http::oauth2::experimental::oauth2_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request OAuth 2.0 error: " << e.what(); + + authorization_service_error = true; + } + catch (const jwk_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request JWK error: " << e.what(); + + authorization_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request error: " << e.what(); + + authorization_service_error = true; + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request error"; + + authorization_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request unexpected unknown exception"; + + authorization_service_error = true; + } + + model.notify(); + }); + } + + // fetch authorization server metadata, such as endpoints used for client registration, token fetches and public keys fetches + bool request_authorization_server_metadata(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, bool& authorization_service_error, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting authorization server metadata fetch"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + std::unique_ptr client; + bool metadata_received(false); + + pplx::cancellation_token_source cancellation_source; + pplx::task request = pplx::task_from_result(); + + const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); + const auto scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); + const auto grants = grant_types(client_metadata, grant_types_from_settings(model.settings)); + const auto token_endpoint_auth_method = nmos::experimental::details::token_endpoint_auth_method(client_metadata, token_endpoint_auth_method_from_settings(model.settings)); + + for (;;) + { + // wait for the thread to be interrupted because an error has been encountered with the selected authorization service + // or because the server is being shut down + condition.wait(lock, [&] { return shutdown || authorization_service_error || metadata_received || !client; }); + if (authorization_service_error) + { + pop_authorization_service(model.settings); + model.notify(); + authorization_service_error = false; + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + + request.wait(); + + client.reset(); + cancellation_source = pplx::cancellation_token_source(); + } + if (shutdown || empty_authorization_services(model.settings) || metadata_received) break; + + const auto service = top_authorization_service(model.settings); + + const auto auth_uri = service.second; + client = nmos::details::make_http_client(auth_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, gate)); + + auto token = cancellation_source.get_token(); + + const auto auth_version = service.first.first; + request = details::request_authorization_server_metadata(*client, scopes, grants, token_endpoint_auth_method, auth_version, gate, token).then([&authorization_state](web::json::value metadata) + { + // record the current connected authorization server uri + with_write_lock(authorization_state.mutex, [&] + { + authorization_state.authorization_server_uri = nmos::experimental::fields::issuer(metadata); + }); + + // cache the authorization server metadata + nmos::experimental::update_authorization_server_metadata(authorization_state, metadata); + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + + metadata_received = true; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + + authorization_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request JSON error: " << e.what(); + + authorization_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request error: " << e.what(); + + authorization_service_error = true; + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request error"; + + authorization_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata unexpected unknown exception"; + + authorization_service_error = true; + } + }); + request.then([&] + { + condition.notify_all(); + }); + + // wait for the request because interactions with the Authorization API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || authorization_service_error || metadata_received; }); + } + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + request.wait(); + + return !authorization_service_error && metadata_received; + } + + // fetch client metadata via OpenID Connect server + bool request_client_metadata_from_openid_connect(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::experimental::save_authorization_client_handler save_authorization_client, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting OpenID Connect client metadata fetch"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool authorization_service_error(false); + + pplx::cancellation_token_source cancellation_source; + auto token = cancellation_source.get_token(); + pplx::task request = pplx::task_from_result(); + + bool registered(false); + const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); + const auto authorization_server_metadata = nmos::experimental::get_authorization_server_metadata(authorization_state); + + // is client already registered to the Authorization server + if(client_metadata.is_null()) + { + slog::log(gate, SLOG_FLF) << "Missing client_metadata from cache"; + return false; + } + + const auto& auth_version = version(nmos::experimental::fields::issuer(client_metadata)); + + // See https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse + // registration_access_token + // OPTIONAL. Registration Access Token that can be used at the Client Configuration Endpoint to perform subsequent operations upon the + // Client registration. + // registration_client_uri + // OPTIONAL. Location of the Client Configuration Endpoint where the Registration Access Token can be used to perform subsequent operations + // upon the resulting Client registration. + // Implementations MUST either return both a Client Configuration Endpoint and a Registration Access Token or neither of them. + if (!client_metadata.has_string_field(nmos::experimental::fields::registration_access_token)) + { + slog::log(gate, SLOG_FLF) << "No registration_access_token from client_metadata"; + return false; + } + const auto& registration_access_token = nmos::experimental::fields::registration_access_token(client_metadata); + if (!client_metadata.has_string_field(nmos::experimental::fields::registration_client_uri)) + { + slog::log(gate, SLOG_FLF) << "No registration_client_uri from client_metadata"; + return false; + } + const auto& registration_client_uri = nmos::experimental::fields::registration_client_uri(client_metadata); + const auto& issuer = nmos::experimental::fields::issuer(authorization_server_metadata); + + request = request_client_metadata_from_openid_connect(web::http::client::http_client(registration_client_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, { registration_access_token }, gate)), + auth_version, gate, token).then([&model, &authorization_state, issuer, save_authorization_client, &gate](web::json::value client_metadata) + { + auto lock = model.write_lock(); + + // check client_secret existence for confidential client + if (((nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic.name) + || (nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_post.name) + || (nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_jwt.name)) + && (!client_metadata.has_string_field(nmos::experimental::fields::client_secret))) + { + slog::log(gate, SLOG_FLF) << "Missing client_secret"; + throw authorization_exception(); + } + + // scope is optional. If one has not be returned by the Authorization server, + // insert one as it is required by authorization functionality. + if (!client_metadata.has_field(nmos::experimental::fields::scope)) + { + client_metadata[nmos::experimental::fields::scope] = web::json::value::string(make_scope(nmos::experimental::authorization_scopes::from_settings(model.settings))); + } + // grant_types is optional. If it has not been returned by the Authorization server + // insert it as it is required by authorization functionality. + if (!client_metadata.has_field(nmos::experimental::fields::grant_types)) + { + client_metadata[nmos::experimental::fields::grant_types] = make_grant_types(grant_types_from_settings(model.settings)); + } + // token_endpoint_auth_method is optional. If it has not been returned by the Authorization server + // insert it as it is required by the authorization functionality. + if (!client_metadata.has_field(nmos::experimental::fields::token_endpoint_auth_method)) + { + client_metadata[nmos::experimental::fields::token_endpoint_auth_method] = web::json::value::string(token_endpoint_auth_method_from_settings(model.settings).name); + } + + // store client metadata to settings + // hmm, may store only the required fields + nmos::experimental::update_client_metadata(authorization_state, client_metadata); + + // do callback to safely store the client metadata + // Client metadata SHOULD be stored by the client in a safe, permission-restricted, location in non-volatile memory in case of a device restart to prevent duplicate registrations. + // Client secrets SHOULD be encrypted before being stored to reduce the chance of client secret leaking. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#client-credentials + if (save_authorization_client) + { + save_authorization_client(web::json::value_of({ + { nmos::experimental::fields::authorization_server_uri, issuer }, + { nmos::experimental::fields::client_metadata, client_metadata } + })); + } + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + + registered = true; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "OpenID Connect Authorization API client metadata retreieve HTTP error: " << e.what() << " [" << e.error_code() << "]"; + + authorization_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "OpenID Connect Authorization API client metadata retreieve JSON error: " << e.what(); + + authorization_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "OpenID Connect Authorization API client metadata retreieve error: " << e.what(); + + authorization_service_error = true; + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "OpenID Connect Authorization API client metadata retreieve error"; + + authorization_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "OpenID Connect Authorization API client metadata retreieve unexpected unknown exception"; + + authorization_service_error = true; + } + + model.notify(); + }); + + // wait for the request because interactions with the Authorization API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || authorization_service_error || registered; }); + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + request.wait(); + return !authorization_service_error && registered; + } + + // register client with the Authorization server + bool client_registration(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::experimental::save_authorization_client_handler save_authorization_client, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting authorization client registration"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool authorization_service_error(false); + + pplx::cancellation_token_source cancellation_source; + auto token = cancellation_source.get_token(); + pplx::task request = pplx::task_from_result(); + + bool registered(false); + + const auto auth_version = top_authorization_service(model.settings).first.first; + + // create client metadata from settings + // see https://tools.ietf.org/html/rfc7591#section-2 + const auto client_name = model.settings.has_field(nmos::fields::label) ? nmos::fields::label(model.settings) : U(""); + const std::vector redirect_uris = { make_authorization_redirect_uri(model.settings) }; + const auto scopes = nmos::experimental::authorization_scopes::from_settings(model.settings); + const auto grants = grant_types_from_settings(model.settings); + + std::set response_types; + if (grants.end() != std::find_if(grants.begin(), grants.end(), [](const web::http::oauth2::experimental::grant_type& grant) { return web::http::oauth2::experimental::grant_types::authorization_code == grant; })) + { + response_types.insert(web::http::oauth2::experimental::response_types::code); + } + if (grants.end() != std::find_if(grants.begin(), grants.end(), [](const web::http::oauth2::experimental::grant_type& grant) { return web::http::oauth2::experimental::grant_types::implicit == grant; })) + { + response_types.insert(web::http::oauth2::experimental::response_types::token); + } + if (response_types.empty()) + { + response_types.insert(web::http::oauth2::experimental::response_types::none); + } + + const auto token_endpoint_auth_method = token_endpoint_auth_method_from_settings(model.settings); + const auto authorization_server_metadata = get_authorization_server_metadata(authorization_state); + const auto& registration_endpoint = web::uri(nmos::experimental::fields::registration_endpoint(authorization_server_metadata)); + const auto& issuer = nmos::experimental::fields::issuer(authorization_server_metadata); + const auto jwks_uri = make_jwks_uri(model.settings); + const auto& initial_access_token = nmos::experimental::fields::initial_access_token(model.settings); + + request = request_client_registration(web::http::client::http_client(registration_endpoint, make_authorization_http_client_config(model.settings, load_ca_certificates, { initial_access_token }, gate)), + client_name, redirect_uris, {}, response_types, scopes, grants, token_endpoint_auth_method, {}, jwks_uri, auth_version, gate, token).then([&model, &authorization_state, issuer, token_endpoint_auth_method, save_authorization_client, &gate](web::json::value client_metadata) + { + auto lock = model.write_lock(); + + // check client_secret exists for confidential client + if (client_metadata.has_string_field(nmos::experimental::fields::token_endpoint_auth_method)) + { + if (((nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic.name) + || (nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_post.name) + || (nmos::experimental::fields::token_endpoint_auth_method(client_metadata) == web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_jwt.name)) + && (!client_metadata.has_string_field(nmos::experimental::fields::client_secret))) + { + slog::log(gate, SLOG_FLF) << "Missing client_secret"; + throw authorization_exception(); + } + } + else + { + if (((web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic == token_endpoint_auth_method) + || (web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_post == token_endpoint_auth_method) + || (web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_jwt == token_endpoint_auth_method)) + && (!client_metadata.has_string_field(nmos::experimental::fields::client_secret))) + { + slog::log(gate, SLOG_FLF) << "Missing client_secret"; + throw authorization_exception(); + } + } + + // scope is optional. If one has not be returned by the Authorization server, + // insert one as it is required by authorization functionality. + if (!client_metadata.has_field(nmos::experimental::fields::scope)) + { + client_metadata[nmos::experimental::fields::scope] = web::json::value::string(make_scope(nmos::experimental::authorization_scopes::from_settings(model.settings))); + } + // grant_types is optional. If it has not been returned by the Authorization server + // insert it as it is required by authorization functionality. + if (!client_metadata.has_field(nmos::experimental::fields::grant_types)) + { + client_metadata[nmos::experimental::fields::grant_types] = make_grant_types(grant_types_from_settings(model.settings)); + } + // token_endpoint_auth_method is optional. If it has not been returned by the Authorization server + // insert it as it is required by the authorization functionality. + if (!client_metadata.has_field(nmos::experimental::fields::token_endpoint_auth_method)) + { + client_metadata[nmos::experimental::fields::token_endpoint_auth_method] = web::json::value::string(token_endpoint_auth_method_from_settings(model.settings).name); + } + + // store client metadata to settings + // hmm, may store only the required fields + nmos::experimental::update_client_metadata(authorization_state, client_metadata); + + // hmm, do a callback allowing user to store the client credentials + // Client credentials SHOULD be stored by the client in a safe, permission-restricted, location in non-volatile memory in case of a device restart to prevent duplicate registrations. Client secrets SHOULD be encrypted before being stored to reduce the chance of client secret leaking. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#client-credentials + if (save_authorization_client) + { + save_authorization_client(web::json::value_of({ + { nmos::experimental::fields::authorization_server_uri, issuer }, + { nmos::experimental::fields::client_metadata, client_metadata } + })); + } + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + + registered = true; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API client registration HTTP error: " << e.what() << " [" << e.error_code() << "]"; + + authorization_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API client registration JSON error: " << e.what(); + + authorization_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API client registration error: " << e.what(); + + authorization_service_error = true; + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "Authorization API client registration error"; + + authorization_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization API client registration unexpected unknown exception"; + + authorization_service_error = true; + } + + model.notify(); + }); + + // wait for the request because interactions with the Authorization API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || authorization_service_error || registered; }); + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + request.wait(); + return !authorization_service_error && registered; + } + + // start authorization code flow + // see https://tools.ietf.org/html/rfc8252#section-4.1 + bool authorization_code_flow(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::experimental::request_authorization_code_handler request_authorization_code, slog::base_gate& gate) + { + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool authorization_service_error(false); + + const auto& settings = model.settings; + + const auto authorization_server_metadata = get_authorization_server_metadata(authorization_state); + const web::uri authorization_endpoint(nmos::experimental::fields::authorization_endpoint(authorization_server_metadata)); + const auto code_challenge_methods_supported(nmos::experimental::fields::code_challenge_methods_supported(authorization_server_metadata)); + const auto client_metadata = get_client_metadata(authorization_state); + const auto client_id(nmos::experimental::fields::client_id(client_metadata)); + const web::uri redirct_uri(nmos::experimental::fields::redirect_uris(client_metadata).size() ? nmos::experimental::fields::redirect_uris(client_metadata).at(0).as_string() : U("")); + const auto scopes = nmos::experimental::details::scopes(nmos::experimental::fields::scope(client_metadata)); + + slog::log(gate, SLOG_FLF) << "Attempting authorization code flow for scope: '" << nmos::experimental::details::make_scope(scopes) << "'"; + + auto access_token_received = false; + auto authorization_flow = nmos::experimental::authorization_state::request_code; + + // start the authorization code flow, the authorization URI is required to + // be loaded in the web browser to kick start the authorization code grant flow + if (request_authorization_code) + { + nmos::with_write_lock(authorization_state.mutex, [&] + { + authorization_state.authorization_flow = nmos::experimental::authorization_state::request_code; + request_authorization_code(make_authorization_code_uri(authorization_endpoint, client_id, redirct_uri, web::http::oauth2::experimental::response_types::code, scopes, code_challenge_methods_supported, authorization_state.state, authorization_state.code_verifier)); + }); + + // wait for the access token + const auto& authorization_code_flow_max = nmos::experimental::fields::authorization_code_flow_max(settings); + if (authorization_code_flow_max > -1) + { + // wait for access token with timeout + if (!model.wait_for(lock, std::chrono::seconds(authorization_code_flow_max), [&] { + authorization_flow = with_read_lock(authorization_state.mutex, [&] { return authorization_state.authorization_flow; }); + return shutdown || nmos::experimental::authorization_state::failed == authorization_flow || nmos::experimental::authorization_state::access_token_received == authorization_flow; })) + { + // authorization code flow timeout + authorization_service_error = true; + slog::log(gate, SLOG_FLF) << "Authorization code flow timeout"; + } + else if (nmos::experimental::authorization_state::access_token_received == authorization_flow) + { + // access token received + access_token_received = true; + slog::log(gate, SLOG_FLF) << "Acess token received"; + } + else + { + // authorization code flow failure + authorization_service_error = true; + slog::log(gate, SLOG_FLF) << "Authorization code flow failure"; + } + } + else + { + // wait for access token without timeout + condition.wait(lock, [&] { + authorization_flow = with_read_lock(authorization_state.mutex, [&] { return authorization_state.authorization_flow; }); + return shutdown || nmos::experimental::authorization_state::failed == authorization_flow || nmos::experimental::authorization_state::access_token_received == authorization_flow; }); + + if (nmos::experimental::authorization_state::access_token_received == authorization_flow) + { + // access token received + access_token_received = true; + slog::log(gate, SLOG_FLF) << "Access token received"; + } + else + { + // authorization code flow failure + authorization_service_error = true; + slog::log(gate, SLOG_FLF) << "Authorization code flow failure"; + } + } + } + else + { + // no handler to start the authorization code grant flow + authorization_service_error = true; + slog::log(gate, SLOG_FLF) << "No authorization code flow handler"; + } + + model.notify(); + + return !authorization_service_error && access_token_received; + } + + // fetch the bearer access token for the required scope(s) to access the protected APIs + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#requesting-a-token + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#accessing-protected-resources + // fetch the token issuer(authorization server)'s public keys for validating the incoming bearer access token + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + void authorization_operation(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, load_ca_certificates_handler load_ca_certificates, load_rsa_private_keys_handler load_rsa_private_keys, bool immediate_token_fetch, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting authorization operation: " << (immediate_token_fetch ? "immediate fetch token" : "fetch token at next interval"); + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + const auto authorization_server_metadata = get_authorization_server_metadata(authorization_state); + const web::uri jwks_uri(nmos::experimental::fields::jwks_uri(authorization_server_metadata)); + const web::uri token_endpoint(nmos::experimental::fields::token_endpoint(authorization_server_metadata)); + const auto& authorization_flow = nmos::experimental::fields::authorization_flow(model.settings); + const auto& grant = (web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? web::http::oauth2::experimental::grant_types::client_credentials : web::http::oauth2::experimental::grant_types::authorization_code; + const auto authorization_version = top_authorization_service(model.settings).first.first; + + bool authorization_service_error(false); + + pplx::cancellation_token_source cancellation_source; + + auto pubkeys_requests(pplx::task_from_result()); + pubkeys_shared_state pubkeys_state + { + { jwks_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, gate) }, + authorization_version, + nmos::experimental::fields::issuer(authorization_server_metadata) + }; + + auto bearer_token_requests(pplx::task_from_result()); + const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); + const auto scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); + const auto bearer_token = nmos::with_read_lock(authorization_state.mutex, [&] { return authorization_state.bearer_token.is_valid_access_token() ? authorization_state.bearer_token : web::http::oauth2::experimental::oauth2_token{}; }); + token_shared_state token_state( + grant, + bearer_token, + { token_endpoint, make_authorization_http_client_config(model.settings, load_ca_certificates, gate) }, + authorization_version, + std::move(load_rsa_private_keys), + immediate_token_fetch + ); + + auto token = cancellation_source.get_token(); + + // start a background task to fetch public keys from authorization server + if (nmos::experimental::fields::server_authorization(model.settings)) + { + pubkeys_requests = do_public_keys_requests(model, authorization_state, pubkeys_state, authorization_service_error, gate, token); + } + + // start a background task to fetch bearer access token from authorization server + if (nmos::experimental::fields::client_authorization(model.settings) && scopes.size()) + { + bearer_token_requests = do_token_requests(model, authorization_state, token_state, authorization_service_error, gate, token); + } + + // wait for the request because interactions with the Authorization API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || authorization_service_error; }); + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + pubkeys_requests.wait(); + bearer_token_requests.wait(); + } + + // make an asynchronously GET request over the Token Issuer(authorization server) to fetch issuer metadata + bool request_token_issuer_metadata(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool authorization_service_error(false); + + bool metadata_received(false); + + pplx::cancellation_token_source cancellation_source; + + // wait for the thread to be interrupted because of no matching public keys from the received token or because the server is being shut down + condition.wait(lock, [&] { return shutdown || nmos::with_read_lock(authorization_state.mutex, [&] { return authorization_state.fetch_token_issuer_pubkeys; }); }); + + slog::log(gate, SLOG_FLF) << "Attempting authorization token issuer metadata fetch"; + + if (shutdown) return false; + + const auto token_issuer = nmos::with_write_lock(authorization_state.mutex, [&] + { + authorization_state.fetch_token_issuer_pubkeys = false; + return authorization_state.token_issuer; + }); + if (token_issuer.is_empty()) + { + slog::log(gate, SLOG_FLF) << "No authorization token's issuer to fetch server metadata"; + return false; + } + web::http::client::http_client client(make_authorization_service_uri(token_issuer), make_authorization_http_client_config(model.settings, load_ca_certificates, gate)); + + const auto client_metadata = nmos::experimental::get_client_metadata(authorization_state); + const auto scopes = nmos::experimental::details::scopes(client_metadata, nmos::experimental::authorization_scopes::from_settings(model.settings)); + const auto grants = grant_types(client_metadata, grant_types_from_settings(model.settings)); + const auto token_endpoint_auth_method = nmos::experimental::details::token_endpoint_auth_method(client_metadata, token_endpoint_auth_method_from_settings(model.settings)); + + auto token = cancellation_source.get_token(); + + auto request = details::request_authorization_server_metadata(client, scopes, grants, token_endpoint_auth_method, version(token_issuer), gate, token).then([&authorization_state, token_issuer](web::json::value metadata) + { + // cache the token issuer(authorization server) metadata + nmos::experimental::update_authorization_server_metadata(authorization_state, token_issuer, metadata); + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + + metadata_received = true; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + + authorization_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request JSON error: " << e.what(); + + authorization_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request error: " << e.what(); + + authorization_service_error = true; + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata request error"; + + authorization_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization API metadata unexpected unknown exception"; + + authorization_service_error = true; + } + }); + request.then([&] + { + condition.notify_all(); + }); + + // wait for the request because interactions with the Authorization API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || authorization_service_error || metadata_received; }); + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + request.wait(); + + return metadata_received; + } + + // make an asynchronously GET request over the Token Issuer(authorization server) to fetch public keys + bool request_token_issuer_public_keys(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting authorization token issuer's public keys fetch"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool authorization_service_error(false); + + bool jwks_received(false); + + pplx::cancellation_token_source cancellation_source; + + const auto token_issuer = with_read_lock(authorization_state.mutex, [&] { return authorization_state.token_issuer; }); + const auto jwks_uri = nmos::experimental::fields::jwks_uri(get_authorization_server_metadata(authorization_state, token_issuer)); + + auto authorization_version = version(token_issuer); + pubkeys_shared_state pubkeys_state( + { jwks_uri, make_authorization_http_client_config(model.settings, load_ca_certificates, gate) }, + authorization_version, + token_issuer + ); + + auto token = cancellation_source.get_token(); + + auto request = details::request_jwks(*pubkeys_state.client, pubkeys_state.version, gate, token).then([&authorization_state, &pubkeys_state, &gate](web::json::value jwks_) + { + const auto jwks = nmos::experimental::get_jwks(authorization_state, pubkeys_state.issuer); + + // are changes found in new set of jwks? + if (jwks != jwks_) + { + // convert jwks to array of public keys + auto pems = web::json::value::array(); + for (const auto& jwk : jwks_.as_array()) + { + try + { + const auto& pem = jwk_to_rsa_public_key(jwk); // can throw jwk_exception + + web::json::push_back(pems, web::json::value_of({ + { U("jwk"), jwk }, + { U("pem"), pem } + })); + } + catch (const jwk_exception& e) + { + slog::log(gate, SLOG_FLF) << "Invalid jwk from " << utility::us2s(pubkeys_state.issuer.to_string()) << " JWK error: " << e.what(); + } + } + + // update jwks and jwt validator cache + if (pems.as_array().size()) + { + nmos::experimental::update_jwks(authorization_state, pubkeys_state.issuer, jwks_, nmos::experimental::jwt_validator(pems, [&pubkeys_state](const web::json::value& payload) + { + // validate access token payload JSON + authapi_validator().validate(payload, experimental::make_authapi_token_schema_schema_uri(pubkeys_state.version)); // may throw json_exception + })); + + slog::log(gate, SLOG_FLF) << "JSON Web Token validator updated using an new set of public keys for " << utility::us2s(pubkeys_state.issuer.to_string()); + } + else + { + nmos::experimental::erase_jwks(authorization_state, pubkeys_state.issuer); + + slog::log(gate, SLOG_FLF) << "Clear JSON Web Token validator due to receiving an empty public key list for " << utility::us2s(pubkeys_state.issuer.to_string()); + } + } + else + { + slog::log(gate, SLOG_FLF) << "No public keys changes found for " << utility::us2s(pubkeys_state.issuer.to_string()); + } + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + + jwks_received = true; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + + authorization_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request JSON error: " << e.what(); + + authorization_service_error = true; + } + catch (const web::http::oauth2::experimental::oauth2_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request OAuth 2.0 error: " << e.what(); + + authorization_service_error = true; + } + catch (const jwk_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request JWK error: " << e.what(); + + authorization_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request error: " << e.what(); + + authorization_service_error = true; + } + catch (const authorization_exception&) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request error"; + + authorization_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization API jwks request unexpected unknown exception"; + + authorization_service_error = true; + } + }); + request.then([&] + { + condition.notify_all(); + }); + + // wait for the request because interactions with the Authorization API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || authorization_service_error || jwks_received; }); + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + request.wait(); + + return jwks_received; + } + } + } +} diff --git a/Development/nmos/authorization_operation.h b/Development/nmos/authorization_operation.h new file mode 100644 index 000000000..9bc1dacec --- /dev/null +++ b/Development/nmos/authorization_operation.h @@ -0,0 +1,81 @@ +#ifndef NMOS_AUTHORIZATION_OPERATION_H +#define NMOS_AUTHORIZATION_OPERATION_H + +#include "nmos/authorization_behaviour.h" + +namespace slog +{ + class base_gate; +} + +namespace web +{ + namespace http + { + namespace oauth2 + { + namespace experimental + { + struct token_endpoint_auth_method; + } + } + } +} + +namespace nmos +{ + struct base_model; + + namespace experimental + { + struct authorization_state; + + namespace details + { + + // construct authorization client config based on settings + // with the remaining options defaulted, e.g. authorization request timeout + web::http::client::http_client_config make_authorization_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate); + inline web::http::client::http_client_config make_authorization_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + return make_authorization_http_client_config(settings, load_ca_certificates, {}, gate); + } + + // verify the redirect URI and make an asynchronously POST request on the Authorization API to exchange authorization code for bearer token + // this function is based on the oauth2::token_from_redirected_uri + pplx::task request_token_from_redirected_uri(web::http::client::http_client client, const nmos::api_version& version, const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& client_id, const utility::string_t& client_secret, const utility::string_t& scope, const utility::string_t& redirect_uri, const utility::string_t& state, const utility::string_t& code_verifier, const web::http::oauth2::experimental::token_endpoint_auth_method& token_endpoint_auth_method, const utility::string_t& client_assertion, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + + // make an asynchronously GET request on the Authorization API to fetch authorization server metadata + bool request_authorization_server_metadata(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, bool& authorization_service_error, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + + // make an asynchronously GET request on the OpenID Authorization API to fetch client metadata + bool request_client_metadata_from_openid_connect(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::experimental::save_authorization_client_handler client_registered, slog::base_gate& gate); + + // make an asynchronously POST request on the Authorization API to register a client + // see https://tools.ietf.org/html/rfc6749#section-2 + // see https://tools.ietf.org/html/rfc7591#section-3.1 + bool client_registration(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::experimental::save_authorization_client_handler client_registered, slog::base_gate& gate); + + // start authorization code flow + // see https://tools.ietf.org/html/rfc8252#section-4.1 + bool authorization_code_flow(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::experimental::request_authorization_code_handler request_authorization_code, slog::base_gate& gate); + + // The bearer token is used for accessing protected APIs + // The pems are used for validating incoming access token + // fetch the bearer access token for the required scope(s) to access the protected APIs + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#requesting-a-token + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html#accessing-protected-resources + // fetch the Token Issuer(authorization server)'s public keys for validating the incoming bearer access token + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + void authorization_operation(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, load_rsa_private_keys_handler load_rsa_private_keys, bool immediate_token_fetch, slog::base_gate& gate); + + // make an asynchronously GET request over the Token Issuer(authorization server) to fetch issuer metadata + bool request_token_issuer_metadata(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + + // make an asynchronously GET request over the Token Issuer(authorization server) to fetch public keys + bool request_token_issuer_public_keys(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + } + } +} + +#endif diff --git a/Development/nmos/authorization_redirect_api.cpp b/Development/nmos/authorization_redirect_api.cpp new file mode 100644 index 000000000..22290985f --- /dev/null +++ b/Development/nmos/authorization_redirect_api.cpp @@ -0,0 +1,354 @@ +#include "nmos/authorization_redirect_api.h" + +#include "cpprest/access_token_error.h" +#include "cpprest/response_type.h" +#include "nmos/api_utils.h" +#include "nmos/authorization_behaviour.h" // for top_authorization_service +#include "nmos/authorization_operation.h" // for request_token_from_redirected_uri +#include "nmos/authorization_state.h" +#include "nmos/authorization_utils.h" +#include "nmos/certificate_settings.h" +#include "nmos/client_utils.h" // for make_http_client_config +#include "nmos/jwt_generator.h" +#include "nmos/jwk_utils.h" +#include "nmos/model.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace experimental + { + struct authorization_flow_exception : std::runtime_error + { + web::http::oauth2::experimental::access_token_error error; + utility::string_t description; + + explicit authorization_flow_exception(web::http::oauth2::experimental::access_token_error error) + : std::runtime_error(utility::us2s(error.name)) + , error(std::move(error)) {} + + explicit authorization_flow_exception(web::http::oauth2::experimental::access_token_error error, utility::string_t description) + : std::runtime_error(utility::us2s(error.name)) + , error(std::move(error)) + , description(std::move(description)) {} + }; + + namespace details + { + typedef std::pair authorization_flow_response; + + inline authorization_flow_response make_authorization_flow_error_response(web::http::status_code code, const utility::string_t& error = {}, const utility::string_t& debug = {}) + { + return{ code, make_error_response_body(code, error, debug) }; + } + + inline authorization_flow_response make_authorization_flow_error_response(web::http::status_code code, const std::exception& debug) + { + return make_authorization_flow_error_response(code, {}, utility::s2us(debug.what())); + } + + void process_error_response(const web::uri& redirected_uri, const utility::string_t& response_type, const utility::string_t& state) + { + using web::http::oauth2::experimental::oauth2_exception; + using web::http::oauth2::details::oauth2_strings; + namespace response_types = web::http::oauth2::experimental::response_types; + namespace access_token_errors = web::http::oauth2::experimental::access_token_errors; + + std::map query; + + // for Authorization Code Grant + // "If the resource owner denies the access request or if the request + // fails for reasons other than a missing or invalid redirection URI, + // the authorization server informs the client by adding the following + // parameters to the query component of the redirection URI using the + // "application/x-www-form-urlencoded" format" + // + // For example, the authorization server redirects the user-agent by + // sending the following HTTP response + // HTTP/1.1 302 Found + // Location: https://client.example.com/cb?error=access_denied&state=xyz + // see https://tools.ietf.org/html/rfc6749#section-4.1.2.1 + if (response_type == response_types::code.name) + { + query = web::uri::split_query(redirected_uri.query()); + } + // for Implicit Grant + // "If the resource owner denies the access request or if the request + // fails for reasons other than a missing or invalid redirection URI, + // the authorization server informs the client by adding the following + // parameters to the fragment component of the redirection URI using the + // "application/x-www-form-urlencoded" format" + // + // For example, the authorization server redirects the user-agent by + // sending the following HTTP response + // HTTP/1.1 302 Found + // Location: https://client.example.com/cb#error=access_denied&state=xyz + // see https://tools.ietf.org/html/rfc6749#section-4.2.2.1 + else if (response_type == response_types::token.name) + { + query = web::uri::split_query(redirected_uri.fragment()); + } + else + { + throw oauth2_exception(U("response_type: '") + response_type + U("' is not supported")); + } + + auto state_param = query.find(oauth2_strings::state); + if (state_param == query.end()) + { + throw oauth2_exception(U("parameter 'state' missing from redirected URI")); + } + + if (state != state_param->second) + { + throw oauth2_exception(U("parameter 'state': '") + state_param->second + U("' does not match with the expected 'state': '") + state + U("'")); + } + + auto error_param = query.find(U("error")); + if (error_param != query.end()) + { + const auto error = web::http::oauth2::experimental::to_access_token_error(error_param->second); + if (error.empty()) + { + throw oauth2_exception(U("invalid 'error' parameter")); + } + + auto error_description_param = query.find(U("error_description")); + if (error_description_param != query.end()) + { + auto error_description = web::uri::decode(error_description_param->second); + std::replace(error_description.begin(), error_description.end(), '+', ' '); + throw authorization_flow_exception(error, error_description); + } + else + { + throw authorization_flow_exception(error); + } + + // hmm, error_uri is ignored for now + } + } + } + + web::http::experimental::listener::api_router make_authorization_redirect_api(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, slog::base_gate& gate_) + { + using namespace web::http::experimental::listener::api_router_using_declarations; + + api_router authorization_api; + + authorization_api.support(U("/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("x-authorization/") }, req, res)); + return pplx::task_from_result(true); + }); + + authorization_api.support(U("/x-authorization/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("callback/") }, req, res)); + return pplx::task_from_result(true); + }); + + authorization_api.support(U("/x-authorization/callback/?"), methods::GET, [&model, &authorization_state, load_ca_certificates, load_rsa_private_keys, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + + details::authorization_flow_response result{ status_codes::BadRequest, {} }; + + utility::string_t state; + utility::string_t code_verifier; + with_write_lock(authorization_state.mutex, [&] + { + state = authorization_state.state; + code_verifier = authorization_state.code_verifier; + authorization_state.authorization_flow = authorization_state::request_code; + }); + const auto authorization_server_metadata = get_authorization_server_metadata(authorization_state); + const auto client_metadata = get_client_metadata(authorization_state); + + web::uri token_endpoint; + web::http::client::http_client_config config; + utility::string_t response_type; + nmos::api_version version; + utility::string_t client_id; + utility::string_t client_secret; + utility::string_t scope; + utility::string_t redirect_uri; + utility::string_t token_endpoint_auth_method; + utility::string_t rsa_private_key; + utility::string_t keyid; + auto client_assertion_lifespan(std::chrono::seconds(30)); + with_read_lock(model.mutex, [&, load_ca_certificates, load_rsa_private_keys] + { + const auto& settings = model.settings; + token_endpoint = nmos::experimental::fields::token_endpoint(authorization_server_metadata); + config = nmos::make_http_client_config(settings, load_ca_certificates, gate); + response_type = web::http::oauth2::experimental::response_types::code.name; + client_id = nmos::experimental::fields::client_id(client_metadata); + client_secret = client_metadata.has_string_field(nmos::experimental::fields::client_secret) ? nmos::experimental::fields::client_secret(client_metadata) : U(""); + scope = nmos::experimental::fields::scope(client_metadata); + redirect_uri = nmos::experimental::fields::redirect_uris(client_metadata).at(0).as_string(); + version = details::top_authorization_service(model.settings).first.first; + token_endpoint_auth_method = client_metadata.has_string_field(nmos::experimental::fields::token_endpoint_auth_method) ? nmos::experimental::fields::token_endpoint_auth_method(client_metadata) : web::http::oauth2::experimental::token_endpoint_auth_methods::client_secret_basic.name; + + if (load_rsa_private_keys) + { + // use the 1st RSA private key from RSA private keys list to create the client_assertion + auto rsa_private_keys = load_rsa_private_keys(); + if (!rsa_private_keys.empty()) + { + rsa_private_key = rsa_private_keys[0]; + keyid = U("1"); + } + } + client_assertion_lifespan = std::chrono::seconds(nmos::experimental::fields::authorization_request_max(settings)); + }); + + // The authorization server may redirect an error back to this endpoint due to error conditions + // such as resource owner rejecting the request, or invalid authorization request + { + auto lock = authorization_state.write_lock(); // in order to update shared state + try + { + details::process_error_response(req.request_uri(), response_type, state); + } + catch (const web::http::oauth2::experimental::oauth2_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request OAuth 2.0 error: " << e.what(); + result = details::make_authorization_flow_error_response(status_codes::InternalError, e); + authorization_state.authorization_flow = authorization_state::failed; + } + catch (const authorization_flow_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request authorization flow error: " << utility::us2s(e.error.name) << " description: " << utility::us2s(e.description); + result = details::make_authorization_flow_error_response(status_codes::BadRequest, e.error.name, e.description); + authorization_state.authorization_flow = authorization_state::failed; + } + + if (authorization_state::failed == authorization_state.authorization_flow) + { + with_write_lock(model.mutex, [&] + { + model.notify(); + }); + + set_reply(res, result.first, !result.second.is_null() ? result.second : nmos::make_error_response_body(result.first)); + + return pplx::task_from_result(true); + } + } + + web::http::client::http_client client(token_endpoint, config); + + auto request_token = pplx::task_from_result(web::http::oauth2::experimental::oauth2_token()); + + const auto token_endpoint_auth_meth = web::http::oauth2::experimental::to_token_endpoint_auth_method(token_endpoint_auth_method); + + // create client assertion for private_key_jwt + utility::string_t client_assertion; + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt == token_endpoint_auth_meth) + { + auto lock = authorization_state.write_lock(); // in order to update shared state + try + { + client_assertion = jwt_generator::create_client_assertion(client_id, client_id, token_endpoint, client_assertion_lifespan, rsa_private_key, keyid); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request Create Client Assertion error: " << e.what(); + result = details::make_authorization_flow_error_response(status_codes::InternalError, e); + authorization_state.authorization_flow = authorization_state::failed; + } + + if (authorization_state::failed == authorization_state.authorization_flow) + { + with_write_lock(model.mutex, [&] + { + model.notify(); + }); + + set_reply(res, result.first, !result.second.is_null() ? result.second : nmos::make_error_response_body(result.first)); + + return pplx::task_from_result(true); + } + } + + // exchange authorization code for bearer token + request_token = details::request_token_from_redirected_uri(client, version, req.request_uri(), response_type, client_id, client_secret, scope, redirect_uri, state, code_verifier, token_endpoint_auth_meth, client_assertion, gate); + + auto request = request_token.then([&model, &authorization_state, &scope, &gate](const web::http::oauth2::experimental::oauth2_token& bearer_token) + { + auto lock = authorization_state.write_lock(); + + // signal authorization_flow that bearer token has just been received + authorization_state.authorization_flow = authorization_state::access_token_received; + + // update bearer token cache, which will be used for accessing protected APIs + authorization_state.bearer_token = bearer_token; + + slog::log(gate, SLOG_FLF) << utility::us2s(bearer_token.scope()) << " bearer token updated"; + + }).then([&](pplx::task finally) + { + auto lock = authorization_state.write_lock(); // in order to update shared state + + try + { + finally.get(); + result = { status_codes::OK, web::json::value_of({{ U("status"), U("Bearer token received") }}, true) }; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + result = details::make_authorization_flow_error_response(status_codes::BadRequest, e); + authorization_state.authorization_flow = authorization_state::failed; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request JSON error: " << e.what(); + result = details::make_authorization_flow_error_response(status_codes::InternalError, e); + authorization_state.authorization_flow = authorization_state::failed; + } + catch (const web::http::oauth2::experimental::oauth2_exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request OAuth 2.0 error: " << e.what(); + result = details::make_authorization_flow_error_response(status_codes::InternalError, e); + authorization_state.authorization_flow = authorization_state::failed; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request error: " << e.what(); + result = details::make_authorization_flow_error_response(status_codes::InternalError, e); + authorization_state.authorization_flow = authorization_state::failed; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Authorization flow token request error"; + result = details::make_authorization_flow_error_response(status_codes::InternalError); + authorization_state.authorization_flow = authorization_state::failed; + } + + with_write_lock(model.mutex, [&] + { + model.notify(); + }); + }); + + // hmm, perhaps wait with timeout? + request.wait(); + + if (web::http::is_success_status_code(result.first)) + { + set_reply(res, result.first, result.second); + } + else + { + set_reply(res, result.first, !result.second.is_null() ? result.second : nmos::make_error_response_body(result.first)); + } + + return pplx::task_from_result(true); + }); + + return authorization_api; + } + } +} diff --git a/Development/nmos/authorization_redirect_api.h b/Development/nmos/authorization_redirect_api.h new file mode 100644 index 000000000..780a77650 --- /dev/null +++ b/Development/nmos/authorization_redirect_api.h @@ -0,0 +1,28 @@ +#ifndef NMOS_AUTHORIZATION_REDIRECT_API_H +#define NMOS_AUTHORIZATION_REDIRECT_API_H + +#include "cpprest/api_router.h" +#include "nmos/certificate_handlers.h" + +namespace slog +{ + class base_gate; +} + +// This is an experimental extension to support authorization code via a REST API +namespace nmos +{ + namespace experimental + { + struct authorization_state; + } + + struct base_model; + + namespace experimental + { + web::http::experimental::listener::api_router make_authorization_redirect_api(nmos::base_model& model, authorization_state& authorization_state, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_rsa_private_keys_handler load_rsa_private_keys, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/nmos/authorization_scopes.h b/Development/nmos/authorization_scopes.h new file mode 100644 index 000000000..98bf172e7 --- /dev/null +++ b/Development/nmos/authorization_scopes.h @@ -0,0 +1,26 @@ +#ifndef NMOS_AUTHORIZATION_SCOPES_H +#define NMOS_AUTHORIZATION_SCOPES_H + +#include +#include +#include "nmos/scope.h" +#include "nmos/settings.h" + +namespace nmos +{ + namespace experimental + { + namespace authorization_scopes + { + // get scope set from settings + inline std::set from_settings(const nmos::settings& settings) + { + return settings.has_field(nmos::experimental::fields::authorization_scopes) + ? boost::copy_range>(nmos::experimental::fields::authorization_scopes(settings) | boost::adaptors::transformed([](const web::json::value& v) { return nmos::experimental::parse_scope(v.as_string()); })) + : std::set{}; + } + } + } +} + +#endif diff --git a/Development/nmos/authorization_state.cpp b/Development/nmos/authorization_state.cpp new file mode 100644 index 000000000..297c06a30 --- /dev/null +++ b/Development/nmos/authorization_state.cpp @@ -0,0 +1,181 @@ +#include "nmos/authorization_state.h" + +#include "nmos/json_fields.h" + +namespace nmos +{ + namespace experimental + { + web::json::value get_authorization_server_metadata(const authorization_state& authorization_state, const web::uri& authorization_server_uri) + { + auto lock = authorization_state.read_lock(); + + const auto& issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + return nmos::experimental::fields::authorization_server_metadata(issuer->second.settings); + } + return{}; + } + web::json::value get_authorization_server_metadata(const authorization_state& authorization_state) + { + return get_authorization_server_metadata(authorization_state, authorization_state.authorization_server_uri); + } + + web::json::value get_client_metadata(const authorization_state& authorization_state, const web::uri& authorization_server_uri) + { + auto lock = authorization_state.read_lock(); + + const auto& issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + return nmos::experimental::fields::client_metadata(issuer->second.settings); + } + return{}; + } + web::json::value get_client_metadata(const authorization_state& authorization_state) + { + return get_client_metadata(authorization_state, authorization_state.authorization_server_uri); + } + + web::json::value get_jwks(const authorization_state& authorization_state, const web::uri& authorization_server_uri) + { + auto lock = authorization_state.read_lock(); + + const auto& issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + return nmos::experimental::fields::jwks(issuer->second.settings); + } + return{}; + } + web::json::value get_jwks(const authorization_state& authorization_state) + { + return get_jwks(authorization_state, authorization_state.authorization_server_uri); + } + + void update_authorization_server_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& authorization_server_metadata) + { + auto lock = authorization_state.write_lock(); + + auto issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + // update the relevant issuer's metadata + auto& settings = issuer->second.settings; + settings[nmos::experimental::fields::authorization_server_metadata] = authorization_server_metadata; + } + else + { + // insert a new issuer with metadata + authorization_state.issuers.insert(std::make_pair( + authorization_server_uri.to_string(), + { web::json::value_of({ + { nmos::experimental::fields::authorization_server_metadata, authorization_server_metadata }, + { nmos::experimental::fields::jwks, {} }, + { nmos::experimental::fields::client_metadata, {} } + }), nmos::experimental::jwt_validator{} } + )); + } + } + void update_authorization_server_metadata(authorization_state& authorization_state, const web::json::value& authorization_server_metadata) + { + update_authorization_server_metadata(authorization_state, authorization_state.authorization_server_uri, authorization_server_metadata); + } + + void update_client_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& client_metadata) + { + auto lock = authorization_state.write_lock(); + + auto issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + // update the relevant issuer's client_metadata + auto& settings = issuer->second.settings; + settings[nmos::experimental::fields::client_metadata] = client_metadata; + } + else + { + // insert a new issuer with client_metadata + authorization_state.issuers.insert(std::make_pair( + authorization_server_uri.to_string(), + { web::json::value_of({ + { nmos::experimental::fields::authorization_server_metadata, {} }, + { nmos::experimental::fields::jwks, {} }, + { nmos::experimental::fields::client_metadata, client_metadata } + }), nmos::experimental::jwt_validator{} } + )); + } + } + void update_client_metadata(authorization_state& authorization_state, const web::json::value& client_metadata) + { + update_client_metadata(authorization_state, authorization_state.authorization_server_uri, client_metadata); + } + + void update_jwks(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& jwks, const nmos::experimental::jwt_validator& jwt_validator) + { + auto lock = authorization_state.write_lock(); + + auto issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + // update the relevant issuer's jwks + auto& settings = issuer->second.settings; + settings[nmos::experimental::fields::jwks] = jwks; + // update relevant issuer's jwt_validator, which was constructed by the jwks + issuer->second.jwt_validator = jwt_validator; + } + else + { + // insert a new issuer with issuer's jwks and issuer's jwt_validator + authorization_state.issuers.insert(std::make_pair( + authorization_server_uri.to_string(), + { web::json::value_of({ + { nmos::experimental::fields::authorization_server_metadata,{} }, + { nmos::experimental::fields::jwks, jwks }, + { nmos::experimental::fields::client_metadata,{} } + }), jwt_validator })); + } + } + void update_jwks(authorization_state& authorization_state, const web::json::value& jwks, const nmos::experimental::jwt_validator& jwt_validator) + { + update_jwks(authorization_state, authorization_state.authorization_server_uri, jwks, jwt_validator); + } + + void erase_client_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri) + { + auto lock = authorization_state.write_lock(); + + auto issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + // erase + auto& settings = issuer->second.settings; + settings[nmos::experimental::fields::client_metadata] = {}; + } + } + void erase_client_metadata(authorization_state& authorization_state) + { + erase_client_metadata(authorization_state, authorization_state.authorization_server_uri); + } + + void erase_jwks(authorization_state& authorization_state, const web::uri& authorization_server_uri) + { + auto lock = authorization_state.write_lock(); + + auto issuer = authorization_state.issuers.find(authorization_server_uri); + if (authorization_state.issuers.end() != issuer) + { + // erase + auto& settings = issuer->second.settings; + settings[nmos::experimental::fields::jwks] = {}; + issuer->second.jwt_validator = {}; + } + } + void erase_jwks(authorization_state& authorization_state) + { + erase_jwks(authorization_state, authorization_state.authorization_server_uri); + } + } +} + diff --git a/Development/nmos/authorization_state.h b/Development/nmos/authorization_state.h new file mode 100644 index 000000000..74f704ad4 --- /dev/null +++ b/Development/nmos/authorization_state.h @@ -0,0 +1,112 @@ +#ifndef NMOS_AUTHORIZATION_STATE_H +#define NMOS_AUTHORIZATION_STATE_H + +#include +#include "cpprest/http_client.h" +#include "cpprest/oauth2.h" +#include "nmos/api_version.h" +#include "nmos/issuers.h" +#include "nmos/mutex.h" +#include "nmos/random.h" + +namespace nmos +{ + namespace experimental + { + struct pubkeys_shared_state + { + nmos::details::seed_generator seeder; + std::default_random_engine engine; + std::unique_ptr client; + nmos::api_version version; + web::uri issuer; + bool immediate; // true = do an immediate fetch; false = do time interval fetch + bool received; + + pubkeys_shared_state() + : engine(seeder) + , immediate(true) + , received(false) {} + + pubkeys_shared_state(web::http::client::http_client client, nmos::api_version version, web::uri issuer) + : engine(seeder) + , client(std::unique_ptr(new web::http::client::http_client(client))) + , version(std::move(version)) + , issuer(std::move(issuer)) + , immediate(true) + , received(false) {} + }; + + struct authorization_state + { + // mutex to be used to protect the members of the authorization_state from simultaneous access by multiple threads + mutable nmos::mutex mutex; + + // authorization code flow settings + utility::string_t state; + utility::string_t code_verifier; + + enum authorization_flow_type + { + request_code, + exchange_code_for_access_token, + fetch_access_token, + access_token_received, + failed + }; + // current status of the authorization flow + authorization_flow_type authorization_flow; + + // fetch public keys from token issuer(Authorization server), in event when no matching keys in cache to validate token + // it is used for triggering the authorization_token_issuer_thread to fetch the token issuer metadata follow by fetching the issuer public keys + bool fetch_token_issuer_pubkeys; + web::uri token_issuer; + + // map of issuer (authorization server) to jwt_validator set for access token validation + nmos::experimental::issuers issuers; + // currently connected authorization server + web::uri authorization_server_uri; + + // OAuth 2.0 bearer token to access authorizaton protected APIs + web::http::oauth2::experimental::oauth2_token bearer_token; + + nmos::read_lock read_lock() const { return nmos::read_lock{ mutex }; } + nmos::write_lock write_lock() const { return nmos::write_lock{ mutex }; } + + authorization_state() + : state{} + , code_verifier{} + , authorization_flow(request_code) + , fetch_token_issuer_pubkeys{ false } + , token_issuer{} + , authorization_server_uri{} + {} + }; + + web::json::value get_authorization_server_metadata(const authorization_state& authorization_state, const web::uri& authorization_server_uri); + web::json::value get_authorization_server_metadata(const authorization_state& authorization_state); + + web::json::value get_client_metadata(const authorization_state& authorization_state, const web::uri& authorization_server_uri); + web::json::value get_client_metadata(const authorization_state& authorization_state); + + web::json::value get_jwks(const authorization_state& authorization_state, const web::uri& authorization_server_uri); + web::json::value get_jwks(const authorization_state& authorization_state); + + void update_authorization_server_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& authorization_server_metadata); + void update_authorization_server_metadata(authorization_state& authorization_state, const web::json::value& authorization_server_metadata); + + void update_client_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& client_metadata); + void update_client_metadata(authorization_state& authorization_state, const web::json::value& client_metadata); + + void update_jwks(authorization_state& authorization_state, const web::uri& authorization_server_uri, const web::json::value& jwks, const nmos::experimental::jwt_validator& jwt_validator); + void update_jwks(authorization_state& authorization_state, const web::json::value& jwks, const nmos::experimental::jwt_validator& jwt_validator); + + void erase_client_metadata(authorization_state& authorization_state, const web::uri& authorization_server_uri); + void erase_client_metadata(authorization_state& authorization_state); + + void erase_jwks(authorization_state& authorization_state, const web::uri& authorization_server_uri); + void erase_jwks(authorization_state& authorization_state); + } +} + +#endif diff --git a/Development/nmos/authorization_utils.cpp b/Development/nmos/authorization_utils.cpp new file mode 100644 index 000000000..82b385d33 --- /dev/null +++ b/Development/nmos/authorization_utils.cpp @@ -0,0 +1,90 @@ +#include "nmos/authorization_utils.h" + +#include // for boost::is_any_of +#include // for boost::split +#include "cpprest/client_type.h" +#include "cpprest/base_uri.h" +#include "nmos/authorization_scopes.h" +#include "nmos/is10_versions.h" +#include "nmos/json_fields.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + // get grant type set from json array + std::set grant_types(const web::json::array& grants) + { + return boost::copy_range>(grants | boost::adaptors::transformed([](const web::json::value& v) { return web::http::oauth2::experimental::to_grant_type(v.as_string()); })); + } + + // get grant type set from settings + std::set grant_types_from_settings(const nmos::settings& settings) + { + const auto& authorization_flow = nmos::experimental::fields::authorization_flow(settings); + return (web::http::oauth2::experimental::grant_types::client_credentials.name == authorization_flow) ? std::set{ web::http::oauth2::experimental::grant_types::client_credentials } : std::set{ web::http::oauth2::experimental::grant_types::authorization_code, web::http::oauth2::experimental::grant_types::refresh_token }; + } + + // get grant type set from client metadata if presented, otherwise return default grant types + std::set grant_types(const web::json::value& client_metadata, const std::set& default_grant_types) + { + if (!client_metadata.is_null() && client_metadata.has_array_field(nmos::experimental::fields::grant_types)) + { + return details::grant_types(nmos::experimental::fields::grant_types(client_metadata)); + } + return default_grant_types; + } + + // get scope set from a spare delimiter scope string + std::set scopes(const utility::string_t& scope) + { + std::vector tokens; + boost::split(tokens, scope, boost::is_any_of(U(" "))); + return boost::copy_range>(tokens | boost::adaptors::transformed([](const utility::string_t& v) { return parse_scope(v); })); + } + + // get scope set from client metadata if presented, otherwise return default scope set + std::set scopes(const web::json::value& client_metadata, const std::set& default_scopes) + { + if (!client_metadata.is_null() && client_metadata.has_string_field(nmos::experimental::fields::scope)) + { + return scopes(nmos::experimental::fields::scope(client_metadata)); + } + return default_scopes; + } + + // get token_endpoint_auth_method from settings + web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method_from_settings(const nmos::settings& settings) + { + return web::http::oauth2::experimental::to_token_endpoint_auth_method(nmos::experimental::fields::token_endpoint_auth_method(settings)); + } + + // get token_endpoint_auth_method from client metadata if presented, otherwise return default_token_endpoint_auth_method + web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method(const web::json::value& client_metadata, const web::http::oauth2::experimental::token_endpoint_auth_method& default_token_endpoint_auth_method) + { + namespace token_endpoint_auth_methods = web::http::oauth2::experimental::token_endpoint_auth_methods; + + auto token_endpoint_auth_method = default_token_endpoint_auth_method; + if (!client_metadata.is_null() && client_metadata.has_string_field(nmos::experimental::fields::token_endpoint_auth_method)) + { + token_endpoint_auth_method = web::http::oauth2::experimental::token_endpoint_auth_method{ nmos::experimental::fields::token_endpoint_auth_method(client_metadata) }; + } + return token_endpoint_auth_method; + } + + // get issuer version + api_version version(const web::uri& issuer) + { + // issuer uri should be of the form "https://server.example.com/{version}" + api_version ver{ api_version{} }; + if (!issuer.is_path_empty()) + { + ver = parse_api_version(web::uri::split_path(issuer.path()).back()); + } + return (api_version{} == ver) ? is10_versions::v1_0 : ver; + } + } + } +} diff --git a/Development/nmos/authorization_utils.h b/Development/nmos/authorization_utils.h new file mode 100644 index 000000000..dfce4cd74 --- /dev/null +++ b/Development/nmos/authorization_utils.h @@ -0,0 +1,61 @@ +#ifndef NMOS_AUTHORIZATION_UTILS_H +#define NMOS_AUTHORIZATION_UTILS_H + +#include +#include "cpprest/grant_type.h" +#include "cpprest/token_endpoint_auth_method.h" +#include "nmos/api_version.h" +#include "nmos/scope.h" +#include "nmos/settings.h" // just a forward declaration of nmos::settings + +namespace web +{ + class uri; +} + +namespace nmos +{ + namespace experimental + { + namespace details + { + // get client's grant types + std::set grant_types(const web::json::array& grants); + std::set grant_types_from_settings(const nmos::settings& settings); + std::set grant_types(const web::json::value& client_metadata, const std::set& default_grant_types); + // get client's scopes + std::set scopes(const utility::string_t& scope); + std::set scopes(const web::json::value& client_metadata, const std::set& default_scopes); + // get client's token_endpoint_auth_method + web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method_from_settings(const nmos::settings& settings); + web::http::oauth2::experimental::token_endpoint_auth_method token_endpoint_auth_method(const web::json::value& client_metadata, const web::http::oauth2::experimental::token_endpoint_auth_method& default_token_endpoint_auth_method); + // get issuer version + api_version version(const web::uri& issuer); + + // is subsets found in given set + template + inline bool find_all(const std::set& sub, const std::set& full) + { + if (sub.size() == 0 || full.size() == 0) { return false; } + + for (auto s : sub) + { + bool found{ false }; + for (auto f : full) + { + if (f == s) + { + found = true; + break; + } + } + + if (!found) { return false; } + } + return true; + } + } + } +} + +#endif diff --git a/Development/nmos/capabilities.cpp b/Development/nmos/capabilities.cpp index 4ba723e2c..adeeedad7 100644 --- a/Development/nmos/capabilities.cpp +++ b/Development/nmos/capabilities.cpp @@ -1,16 +1,18 @@ #include "nmos/capabilities.h" #include +#include "cpprest/regex_utils.h" #include "nmos/json_fields.h" namespace nmos { - web::json::value make_caps_string_constraint(const std::vector& enum_values) + web::json::value make_caps_string_constraint(const std::vector& enum_values, const utility::string_t& pattern) { using web::json::value_of; using web::json::value_from_elements; return value_of({ - { !enum_values.empty() ? nmos::fields::constraint_enum.key : U(""), value_from_elements(enum_values) } + { !enum_values.empty() ? nmos::fields::constraint_enum.key : U(""), value_from_elements(enum_values) }, + { !pattern.empty() ? nmos::fields::constraint_pattern.key : U(""), pattern } }); } @@ -58,9 +60,8 @@ namespace nmos namespace details { - // cf. nmos::details::make_constraints_schema in nmos/connection_api.cpp - template - bool match_constraint(const T& value, const web::json::value& constraint, Parse parse) + template > + bool match_enum_constraint(const T& value, const web::json::value& constraint, Parse parse = {}) { if (constraint.has_field(nmos::fields::constraint_enum)) { @@ -73,6 +74,12 @@ namespace nmos return false; } } + return true; + } + + template > + bool match_minimum_maximum_constraint(const T& value, const web::json::value& constraint, Parse parse = {}) + { if (constraint.has_field(nmos::fields::constraint_minimum)) { const auto& minimum = nmos::fields::constraint_minimum(constraint); @@ -91,45 +98,77 @@ namespace nmos } return true; } + + bool match_pattern_constraint(const utility::string_t& value, const web::json::value& constraint) + { + if (constraint.has_field(nmos::fields::constraint_pattern)) + { + const utility::regex_t regex(nmos::fields::constraint_pattern(constraint)); + utility::smatch_t match; + // throws bst::regex_error if pattern is invalid + if (!bst::regex_search(value, match, regex)) + { + return false; + } + } + return true; + } } bool match_string_constraint(const utility::string_t& value, const web::json::value& constraint) { - return details::match_constraint(value, constraint, [](const web::json::value& enum_value) - { - return enum_value.as_string(); - }); + return details::match_enum_constraint(value, constraint) + && details::match_pattern_constraint(value, constraint); } bool match_integer_constraint(int64_t value, const web::json::value& constraint) { - return details::match_constraint(value, constraint, [&value](const web::json::value& enum_value) - { - return enum_value.as_integer(); - }); + return details::match_enum_constraint(value, constraint) + && details::match_minimum_maximum_constraint(value, constraint); } bool match_number_constraint(double value, const web::json::value& constraint) { - return details::match_constraint(value, constraint, [&value](const web::json::value& enum_value) - { - return enum_value.as_double(); - }); + return details::match_enum_constraint(value, constraint) + && details::match_minimum_maximum_constraint(value, constraint); } bool match_boolean_constraint(bool value, const web::json::value& constraint) { - return details::match_constraint(value, constraint, [&value](const web::json::value& enum_value) - { - return enum_value.as_bool(); - }); + return details::match_enum_constraint(value, constraint); } bool match_rational_constraint(const nmos::rational& value, const web::json::value& constraint) { - return details::match_constraint(value, constraint, [&value](const web::json::value& enum_value) + return details::match_enum_constraint(value, constraint, &nmos::parse_rational) + && details::match_minimum_maximum_constraint(value, constraint, &nmos::parse_rational); + } + + bool match_constraint(const web::json::value& value, const web::json::value& constraint) + { + if (value.is_string()) { - return nmos::parse_rational(enum_value); - }); + return match_string_constraint(value.as_string(), constraint); + } + else if (value.is_integer()) + { + return match_integer_constraint(value.as_number().to_int64(), constraint); + } + else if (value.is_double()) + { + return match_number_constraint(value.as_double(), constraint); + } + else if (value.is_boolean()) + { + return match_boolean_constraint(value.as_bool(), constraint); + } + else if (nmos::is_rational(value)) + { + return match_rational_constraint(nmos::parse_rational(value), constraint); + } + else + { + throw web::json::json_exception("not a valid constraint target type"); + } } } diff --git a/Development/nmos/capabilities.h b/Development/nmos/capabilities.h index e57cabafa..c931df792 100644 --- a/Development/nmos/capabilities.h +++ b/Development/nmos/capabilities.h @@ -7,7 +7,7 @@ namespace nmos { // BCP-004-01 Receiver Capabilities - // See https://github.com/AMWA-TV/nmos-receiver-capabilities/blob/v1.0-dev/docs/1.0.%20Receiver%20Capabilities.md + // See https://specs.amwa.tv/bcp-004-01/releases/v1.0.0/docs/1.0._Receiver_Capabilities.html namespace fields { const web::json::field_as_value_or constraint_sets{ U("constraint_sets"), {} }; @@ -19,19 +19,19 @@ namespace nmos template <> nmos::rational inline no_minimum() { return (std::numeric_limits::max)(); } template <> nmos::rational inline no_maximum() { return 0; } - // See https://github.com/AMWA-TV/nmos-receiver-capabilities/blob/v1.0-dev/docs/1.0.%20Receiver%20Capabilities.md#string-constraint-keywords - web::json::value make_caps_string_constraint(const std::vector& enum_values = {}); + // See https://specs.amwa.tv/bcp-004-01/releases/v1.0.0/docs/1.0._Receiver_Capabilities.html#string-constraint-keywords + web::json::value make_caps_string_constraint(const std::vector& enum_values = {}, const utility::string_t& pattern = {}); - // See https://github.com/AMWA-TV/nmos-receiver-capabilities/blob/v1.0-dev/docs/1.0.%20Receiver%20Capabilities.md#integer-and-number-constraint-keywords + // See https://specs.amwa.tv/bcp-004-01/releases/v1.0.0/docs/1.0._Receiver_Capabilities.html#integer-and-number-constraint-keywords web::json::value make_caps_integer_constraint(const std::vector& enum_values = {}, int64_t minimum = no_minimum(), int64_t maximum = no_maximum()); - // See https://github.com/AMWA-TV/nmos-receiver-capabilities/blob/v1.0-dev/docs/1.0.%20Receiver%20Capabilities.md#integer-and-number-constraint-keywords + // See https://specs.amwa.tv/bcp-004-01/releases/v1.0.0/docs/1.0._Receiver_Capabilities.html#integer-and-number-constraint-keywords web::json::value make_caps_number_constraint(const std::vector& enum_values = {}, double minimum = no_minimum(), double maximum = no_maximum()); - // See https://github.com/AMWA-TV/nmos-receiver-capabilities/blob/v1.0-dev/docs/1.0.%20Receiver%20Capabilities.md#boolean-constraint-keywords + // See https://specs.amwa.tv/bcp-004-01/releases/v1.0.0/docs/1.0._Receiver_Capabilities.html#boolean-constraint-keywords web::json::value make_caps_boolean_constraint(const std::vector& enum_values = {}); - // See https://github.com/AMWA-TV/nmos-receiver-capabilities/blob/v1.0-dev/docs/1.0.%20Receiver%20Capabilities.md#rational-constraint-keywords + // See https://specs.amwa.tv/bcp-004-01/releases/v1.0.0/docs/1.0._Receiver_Capabilities.html#rational-constraint-keywords web::json::value make_caps_rational_constraint(const std::vector& enum_values = {}, const nmos::rational& minimum = no_minimum(), const nmos::rational& maximum = no_maximum()); bool match_string_constraint(const utility::string_t& value, const web::json::value& constraint); @@ -39,9 +39,10 @@ namespace nmos bool match_number_constraint(double value, const web::json::value& constraint); bool match_boolean_constraint(bool value, const web::json::value& constraint); bool match_rational_constraint(const nmos::rational& value, const web::json::value& constraint); + bool match_constraint(const web::json::value& value, const web::json::value& constraint); // NMOS Parameter Registers - Capabilities register - // See https://github.com/AMWA-TV/nmos-parameter-registers/blob/capabilities/capabilities/README.md + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/ namespace caps { namespace meta diff --git a/Development/nmos/certificate_handlers.cpp b/Development/nmos/certificate_handlers.cpp new file mode 100644 index 000000000..f34491dcd --- /dev/null +++ b/Development/nmos/certificate_handlers.cpp @@ -0,0 +1,181 @@ +#include "nmos/certificate_handlers.h" + +#include "cpprest/basic_utils.h" +#include "nmos/certificate_settings.h" +#include "nmos/slog.h" + +namespace nmos +{ + // construct callback to load certification authorities from file based on settings, see nmos/certificate_settings.h + load_ca_certificates_handler make_load_ca_certificates_handler(const nmos::settings& settings, slog::base_gate& gate) + { + const auto ca_certificate_file = nmos::experimental::fields::ca_certificate_file(settings); + + return [&, ca_certificate_file]() + { + slog::log(gate, SLOG_FLF) << "Load certification authorities"; + + if (ca_certificate_file.empty()) + { + slog::log(gate, SLOG_FLF) << "Missing certification authorities file"; + } + else + { + utility::ifstream_t ca_file(ca_certificate_file); + utility::ostringstream_t cacerts; + cacerts << ca_file.rdbuf(); + return cacerts.str(); + } + return utility::string_t{}; + }; + } + + // construct callback to load server certificates from files based on settings, see nmos/certificate_settings.h + load_server_certificates_handler make_load_server_certificates_handler(const nmos::settings& settings, slog::base_gate& gate) + { + // load the server private keys and certificate chains from files + auto server_certificates = nmos::experimental::fields::server_certificates(settings); + if (0 == server_certificates.size()) + { + // (deprecated, replaced by server_certificates) + const auto private_key_files = nmos::experimental::fields::private_key_files(settings); + const auto certificate_chain_files = nmos::experimental::fields::certificate_chain_files(settings); + + const auto size = (std::min)(private_key_files.size(), certificate_chain_files.size()); + for (size_t i = 0; i < size; ++i) + { + web::json::push_back(server_certificates, + web::json::value_of({ + { nmos::experimental::fields::private_key_file, private_key_files.at(i) }, + { nmos::experimental::fields::certificate_chain_file, certificate_chain_files.at(i) } + }) + ); + } + } + + return [&, server_certificates]() + { + slog::log(gate, SLOG_FLF) << "Load server private keys and certificate chains"; + + auto data = std::vector(); + + if (0 == server_certificates.size()) + { + slog::log(gate, SLOG_FLF) << "Missing server certificates"; + } + + for (const auto& server_certificate : server_certificates.as_array()) + { + const auto key_algorithm = nmos::experimental::fields::key_algorithm(server_certificate); + const auto private_key_file = nmos::experimental::fields::private_key_file(server_certificate); + const auto certificate_chain_file = nmos::experimental::fields::certificate_chain_file(server_certificate); + + utility::ostringstream_t pkey; + if (private_key_file.empty()) + { + slog::log(gate, SLOG_FLF) << "Missing server private key file"; + } + else + { + utility::ifstream_t pkey_file(private_key_file); + pkey << pkey_file.rdbuf(); + } + + utility::ostringstream_t cert_chain; + if (certificate_chain_file.empty()) + { + slog::log(gate, SLOG_FLF) << "Missing server certificate chain file"; + } + else + { + utility::ifstream_t cert_chain_file(certificate_chain_file); + cert_chain << cert_chain_file.rdbuf(); + } + + data.push_back(nmos::certificate(nmos::key_algorithm{ key_algorithm }, pkey.str(), cert_chain.str())); + } + return data; + }; + } + + // construct callback to load Diffie-Hellman parameters for ephemeral key exchange support from file based on settings, see nmos/certificate_settings.h + load_dh_param_handler make_load_dh_param_handler(const nmos::settings& settings, slog::base_gate& gate) + { + const auto dh_param_file = nmos::experimental::fields::dh_param_file(settings); + + return[&, dh_param_file]() + { + slog::log(gate, SLOG_FLF) << "Load DH parameters"; + + if (dh_param_file.empty()) + { + slog::log(gate, SLOG_FLF) << "Missing DH parameters file"; + } + else + { + utility::ifstream_t dh_file(dh_param_file); + utility::ostringstream_t dh_param; + dh_param << dh_file.rdbuf(); + return dh_param.str(); + } + return utility::string_t{}; + }; + } + + // construct callback to load RSA private keys from file based on settings, see nmos/certificate_settings.h + // required for OAuth client which is using Private Key JWT as the requested authentication method for the token endpoint + load_rsa_private_keys_handler make_load_rsa_private_keys_handler(const nmos::settings& settings, slog::base_gate& gate) + { + // load the server private keys from files + auto server_certificates = nmos::experimental::fields::server_certificates(settings); + if (0 == server_certificates.size()) + { + // (deprecated, replaced by server_certificates) + const auto private_key_files = nmos::experimental::fields::private_key_files(settings); + + for (const auto& private_key_file : private_key_files.as_array()) + { + web::json::push_back(server_certificates, + web::json::value_of({ + { nmos::experimental::fields::private_key_file, private_key_file }, + { nmos::experimental::fields::certificate_chain_file, {} } + }) + ); + } + } + + return [&, server_certificates]() + { + slog::log(gate, SLOG_FLF) << "Load server private keys"; + + auto data = std::vector(); + auto private_keys = std::vector(); + if (0 == server_certificates.size()) + { + slog::log(gate, SLOG_FLF) << "Missing server certificates"; + } + + for (const auto& server_certificate : server_certificates.as_array()) + { + const auto key_algorithm = nmos::experimental::fields::key_algorithm(server_certificate); + const auto private_key_file = nmos::experimental::fields::private_key_file(server_certificate); + + utility::ostringstream_t pkey; + if (private_key_file.empty()) + { + slog::log(gate, SLOG_FLF) << "Missing private key file"; + } + else + { + if (key_algorithm.empty() || key_algorithms::RSA.name == key_algorithm) + { + utility::ifstream_t pkey_file(private_key_file); + pkey << pkey_file.rdbuf(); + private_keys.push_back(pkey.str()); + } + } + } + return private_keys; + }; + } +} diff --git a/Development/nmos/certificate_handlers.h b/Development/nmos/certificate_handlers.h new file mode 100644 index 000000000..043a50e40 --- /dev/null +++ b/Development/nmos/certificate_handlers.h @@ -0,0 +1,84 @@ +#ifndef NMOS_CERTIFICATE_HANDLERS_H +#define NMOS_CERTIFICATE_HANDLERS_H + +#include +#include +#include "cpprest/details/basic_types.h" +#include "nmos/settings.h" +#include "nmos/string_enum.h" + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + // callback to supply trusted root CA certificate(s) in PEM format + // this callback is executed when opening the HTTP or WebSocket client + // this callback should not throw exceptions + // on Windows, if C++ REST SDK is built with CPPREST_HTTP_CLIENT_IMPL=winhttp (reported as "client=winhttp" by nmos::get_build_settings_info) + // the trusted root CA certificates must also be imported into the certificate store + typedef std::function load_ca_certificates_handler; + + // common key algorithms + DEFINE_STRING_ENUM(key_algorithm) + namespace key_algorithms + { + const key_algorithm ECDSA{ U("ECDSA") }; + const key_algorithm RSA{ U("RSA") }; + } + + // certificate details including the private key and the certificate chain in PEM format + // the key algorithm may also be specified + struct certificate + { + certificate() {} + + certificate(utility::string_t private_key, utility::string_t certificate_chain) + : private_key(std::move(private_key)) + , certificate_chain(std::move(certificate_chain)) + {} + + certificate(nmos::key_algorithm key_algorithm, utility::string_t private_key, utility::string_t certificate_chain) + : key_algorithm(std::move(key_algorithm)) + , private_key(std::move(private_key)) + , certificate_chain(std::move(certificate_chain)) + {} + + nmos::key_algorithm key_algorithm; + utility::string_t private_key; + // the chain should be sorted starting with the end entity's certificate, followed by any intermediate CA certificates, and ending with the highest level (root) CA + utility::string_t certificate_chain; + }; + + // callback to supply a list of server certificates + // this callback is executed when a connection is accepted by the HTTP or WebSocket listener + // this callback should not throw exceptions + // on Windows, if C++ REST SDK is built with CPPREST_HTTP_LISTENER_IMPL=httpsys (reported as "listener=httpsys" by nmos::get_build_settings_info) + // one of the certificates must also be bound to each port e.g. using 'netsh add sslcert' + typedef std::function()> load_server_certificates_handler; + + // callback to supply Diffie-Hellman parameters for ephemeral key exchange support, in PEM format or empty string for no support + // see e.g. https://wiki.openssl.org/index.php/Diffie-Hellman_parameters + // this callback is executed when a connection is accepted by the HTTP or WebSocket listener + // this callback should not throw exceptions + typedef std::function load_dh_param_handler; + + // callback to supply a list of RSA private keys + typedef std::function()> load_rsa_private_keys_handler; + + // construct callback to load certification authorities from file based on settings, see nmos/certificate_settings.h + load_ca_certificates_handler make_load_ca_certificates_handler(const nmos::settings& settings, slog::base_gate& gate); + + // construct callback to load server certificates from files based on settings, see nmos/certificate_settings.h + load_server_certificates_handler make_load_server_certificates_handler(const nmos::settings& settings, slog::base_gate& gate); + + // construct callback to load Diffie-Hellman parameters for ephemeral key exchange support from file based on settings, see nmos/certificate_settings.h + load_dh_param_handler make_load_dh_param_handler(const nmos::settings& settings, slog::base_gate& gate); + + // construct callback to load server RSA private key files based on settings, see nmos/certificate_settings.h + load_rsa_private_keys_handler make_load_rsa_private_keys_handler(const nmos::settings& settings, slog::base_gate& gate); +} + +#endif diff --git a/Development/nmos/certificate_settings.h b/Development/nmos/certificate_settings.h new file mode 100644 index 000000000..a01fc90f0 --- /dev/null +++ b/Development/nmos/certificate_settings.h @@ -0,0 +1,56 @@ +#ifndef NMOS_CERTIFICATE_SETTINGS_H +#define NMOS_CERTIFICATE_SETTINGS_H + +#include "cpprest/json_utils.h" + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + namespace experimental + { + namespace fields + { + // ca_certificate_file: full path of certification authorities file in PEM format + // on Windows, if C++ REST SDK is built with CPPREST_HTTP_CLIENT_IMPL=winhttp (reported as "client=winhttp" by nmos::get_build_settings_info) + // the trusted root CA certificates must also be imported into the certificate store + const web::json::field_as_string_or ca_certificate_file{ U("ca_certificate_file"), U("") }; + + // server_certificates [registry, node]: an array of server certificate objects, each has the name of the key algorithm, the full paths of private key file and certificate chain file + // each value must be an object like { "key_algorithm": "ECDSA", "private_key_file": "server-key.pem, "certificate_chain_file": "server-chain.pem" } + // see key_algorithm, private_key_file and certificate_chain_file below + const web::json::field_as_value_or server_certificates{ U("server_certificates"), web::json::value::array() }; + + // key_algorithm (attribute of server_certificates objects): name of the key algorithm for the certificate, see nmos::key_algorithm + const web::json::field_as_string_or key_algorithm{ U("key_algorithm"), U("") }; + + // private_key_file (attribute of server_certificates objects): full path of private key file in PEM format + const web::json::field_as_string_or private_key_file{ U("private_key_file"), U("") }; + + // certificate_chain_file (attribute of server_certificates objects): full path of certificate chain file in PEM format, which must be sorted + // starting with the server's certificate, followed by any intermediate CA certificates, and ending with the highest level (root) CA + // on Windows, if C++ REST SDK is built with CPPREST_HTTP_LISTENER_IMPL=httpsys (reported as "listener=httpsys" by nmos::get_build_settings_info) + // one of the certificates must also be bound to each port e.g. using 'netsh add sslcert' + const web::json::field_as_string_or certificate_chain_file{ U("certificate_chain_file"), U("") }; + + // dh_param_file [registry, node]: Diffie-Hellman parameters file in PEM format for ephemeral key exchange support, or empty string for no support + const web::json::field_as_string_or dh_param_file{ U("dh_param_file"), U("") }; + + // (deprecated, replaced by server_certificates) + // private_key_files [registry, node]: full paths of private key files in PEM format + const web::json::field_as_value_or private_key_files{ U("private_key_files"), web::json::value::array() }; + + // (deprecated, replaced by server_certificates) + // certificate_chain_files [registry, node]: full paths of server certificate chain files which must be in PEM format and must be sorted + // starting with the server's certificate, followed by any intermediate CA certificates, and ending with the highest level (root) CA + // on Windows, if C++ REST SDK is built with CPPREST_HTTP_LISTENER_IMPL=httpsys (reported as "listener=httpsys" by nmos::get_build_settings_info) + // one of the certificates must also be bound to each port e.g. using 'netsh add sslcert' + const web::json::field_as_value_or certificate_chain_files{ U("certificate_chain_files"), web::json::value::array() }; + } + } +} + +#endif diff --git a/Development/nmos/channelmapping_activation.cpp b/Development/nmos/channelmapping_activation.cpp index 94122c146..c86a97f85 100644 --- a/Development/nmos/channelmapping_activation.cpp +++ b/Development/nmos/channelmapping_activation.cpp @@ -178,7 +178,7 @@ namespace nmos // At the moment, it doesn't seem necessary to enable support multiple API instances via the API selector mechanism // so therefore just a single Channel Mapping API instance is mounted directly at /x-nmos/channelmapping/{version}/ // If it becomes necessary, each device could associated with a specific API selector - // See https://github.com/AMWA-TV/nmos-audio-channel-mapping/blob/v1.0.x/docs/2.0.%20APIs.md#api-paths + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/2.0._APIs.html#api-paths // hmm, should all devices get the same activation time or not? const auto activation_time = nmos::tai_now(); diff --git a/Development/nmos/channelmapping_api.cpp b/Development/nmos/channelmapping_api.cpp index 0191a52a3..0439d8a12 100644 --- a/Development/nmos/channelmapping_api.cpp +++ b/Development/nmos/channelmapping_api.cpp @@ -16,7 +16,7 @@ namespace nmos { web::http::experimental::listener::api_router make_unmounted_channelmapping_api(nmos::node_model& model, details::channelmapping_output_map_validator validate_merged, slog::base_gate& gate); - web::http::experimental::listener::api_router make_channelmapping_api(nmos::node_model& model, details::channelmapping_output_map_validator validate_merged, slog::base_gate& gate) + web::http::experimental::listener::api_router make_channelmapping_api(nmos::node_model& model, details::channelmapping_output_map_validator validate_merged, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -34,6 +34,12 @@ namespace nmos return pplx::task_from_result(true); }); + if (validate_authorization) + { + channelmapping_api.support(U("/x-nmos/") + nmos::patterns::channelmapping_api.pattern + U("/?"), validate_authorization); + channelmapping_api.support(U("/x-nmos/") + nmos::patterns::channelmapping_api.pattern + U("/.*"), validate_authorization); + } + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is08_versions::from_settings(model.settings); }); channelmapping_api.support(U("/x-nmos/") + nmos::patterns::channelmapping_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { @@ -45,7 +51,7 @@ namespace nmos // so therefore just mount the Channel Mapping API instance directly at /x-nmos/channelmapping/{version}/ // If it becomes necessary, nmos::node_mode::channelmapping_resources could become a map from {selector} to nmos::resources // and the 'unmounted' API instance handler also mounted after a "child resources" handler based on the API selectors - // See https://github.com/AMWA-TV/nmos-audio-channel-mapping/blob/v1.0.x/docs/2.0.%20APIs.md#api-paths + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/2.0._APIs.html#api-paths channelmapping_api.mount(U("/x-nmos/") + nmos::patterns::channelmapping_api.pattern + U("/") + nmos::patterns::version.pattern, make_unmounted_channelmapping_api(model, validate_merged, gate)); return channelmapping_api; @@ -154,14 +160,14 @@ namespace nmos { // When "one field of Output channel object is null but other is not", the entire request must be rejected // with the 400 Bad Request response. - // See https://nmos.amwa.tv/nmos-audio-channel-mapping/branches/v1.0.x/docs/4.0._Behaviour.html#activation-responses + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/4.0._Behaviour.html#activation-responses if (!input_channel_index_or_null.is_null()) { throw web::json::json_exception("invalid channel_index when input is null"); } // "If no [routing] restrictions exist, the routable_inputs field MUST be set to null." - // See https://nmos.amwa.tv/nmos-audio-channel-mapping/branches/v1.0.x/docs/4.0._Behaviour.html#output-routing-constraints + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/4.0._Behaviour.html#output-routing-constraints if (!routable_inputs_or_null.is_null()) { // "If the Device allows the Output to have unrouted channels, the list SHOULD also include null." @@ -178,7 +184,7 @@ namespace nmos // When "one field of Output channel object is null but other is not", the entire request must be rejected // with the 400 Bad Request response. - // See https://nmos.amwa.tv/nmos-audio-channel-mapping/branches/v1.0.x/docs/4.0._Behaviour.html#activation-responses + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/4.0._Behaviour.html#activation-responses if (input_channel_index_or_null.is_null()) { throw web::json::json_exception("invalid channel_index when input is not null"); @@ -186,7 +192,7 @@ namespace nmos const auto input_channel_index = input_channel_index_or_null.as_integer(); // "If no [routing] restrictions exist, the routable_inputs field MUST be set to null." - // See https://nmos.amwa.tv/nmos-audio-channel-mapping/branches/v1.0.x/docs/4.0._Behaviour.html#output-routing-constraints + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/4.0._Behaviour.html#output-routing-constraints if (!routable_inputs_or_null.is_null()) { // "If an Input is listed in an Output's routable input list then channels from that input can be routed @@ -223,7 +229,7 @@ namespace nmos // When an "Output is modified by an already scheduled activation", the entire request must be rejected // with the 423 Locked response. - // See https://nmos.amwa.tv/nmos-audio-channel-mapping/branches/v1.0.x/docs/4.0._Behaviour.html#activation-responses + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/4.0._Behaviour.html#activation-responses const auto& mode = nmos::fields::mode(nmos::fields::activation(nmos::fields::endpoint_staged(output.data))); @@ -245,13 +251,13 @@ namespace nmos // "If the Input [...] cannot perform re-ordering, [...] there MUST be a fixed offset // for all Input and Output channel indexes in the mapping." - // See https://nmos.amwa.tv/nmos-audio-channel-mapping/branches/v1.0.x/docs/4.0._Behaviour.html#re-ordering + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/4.0._Behaviour.html#re-ordering bool current_reorderable = false; std::map channel_offsets; // "It MUST be that either all Input channels in a block are routed to an Output, or none are routed. // All Input channels in a block MUST be routed to the same Output when routed." - // See https://nmos.amwa.tv/nmos-audio-channel-mapping/branches/v1.0.x/docs/4.0._Behaviour.html#channel-block-sizes + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/4.0._Behaviour.html#channel-block-sizes unsigned int current_block_size = 0; std::set channels_in_current_block; @@ -312,7 +318,7 @@ namespace nmos if (input_channel_index % current_block_size != 0) { // "All blocks MUST start with an input channel where channel_index is zero or a multiple of the block_size parameter." - // See https://nmos.amwa.tv/nmos-audio-channel-mapping/branches/v1.0.x/docs/4.0._Behaviour.html#channel-block-sizes + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/4.0._Behaviour.html#channel-block-sizes return make_channelmapping_activation_post_error_response(status_codes::BadRequest, U("Bad Request; ") + make_channelmapping_input_block_not_reorderable_error(input_id)); } } diff --git a/Development/nmos/channelmapping_api.h b/Development/nmos/channelmapping_api.h index b48df4c40..04cad01df 100644 --- a/Development/nmos/channelmapping_api.h +++ b/Development/nmos/channelmapping_api.h @@ -11,7 +11,7 @@ namespace slog } // Channel Mapping API implementation -// See https://github.com/AMWA-TV/nmos-audio-channel-mapping/blob/v1.0.x/APIs/ChannelMappingAPI.raml +// See https://specs.amwa.tv/is-08/releases/v1.0.1/APIs/ChannelMappingAPI.html namespace nmos { struct node_model; @@ -29,7 +29,12 @@ namespace nmos // Channel Mapping API factory functions // callbacks from this function are called with the model locked, and may read but should not write directly to the model - web::http::experimental::listener::api_router make_channelmapping_api(nmos::node_model& model, details::channelmapping_output_map_validator validate_merged, slog::base_gate& gate); + web::http::experimental::listener::api_router make_channelmapping_api(nmos::node_model& model, details::channelmapping_output_map_validator validate_merged, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); + + inline web::http::experimental::listener::api_router make_channelmapping_api(nmos::node_model& model, details::channelmapping_output_map_validator validate_merged, slog::base_gate& gate) + { + return make_channelmapping_api(model, std::move(validate_merged), {}, gate); + } inline web::http::experimental::listener::api_router make_channelmapping_api(nmos::node_model& model, slog::base_gate& gate) { diff --git a/Development/nmos/channelmapping_resources.cpp b/Development/nmos/channelmapping_resources.cpp index e2a6cee5a..7e738c98c 100644 --- a/Development/nmos/channelmapping_resources.cpp +++ b/Development/nmos/channelmapping_resources.cpp @@ -122,7 +122,7 @@ namespace nmos using web::json::value; using web::json::value_of; - const bool empty = id.empty() || type.name.empty(); + const bool empty = id.empty() || type.empty(); return value_of({ { nmos::fields::id, empty ? value::null() : value::string(id) }, { nmos::fields::type, empty ? value::null() : value::string(type.name) } diff --git a/Development/nmos/channelmapping_resources.h b/Development/nmos/channelmapping_resources.h index c54160690..6febd0270 100644 --- a/Development/nmos/channelmapping_resources.h +++ b/Development/nmos/channelmapping_resources.h @@ -11,23 +11,23 @@ namespace nmos struct resource; // IS-08 Channel Mapping API resources - // See https://github.com/AMWA-TV/nmos-audio-channel-mapping/blob/v1.0.x/docs/1.0.%20Overview.md#api-structure + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/1.0._Overview.html#api-structure // Each IS-08 input and output's data are json objects with an identifier field // and a field for the resource's view in the /io endpoint, also used for // the individual endpoints, "properties", "caps" and so on - // See https://github.com/AMWA-TV/nmos-audio-channel-mapping/blob/v1.0.x/APIs/schemas/io-response-schema.json + // See https://specs.amwa.tv/is-08/releases/v1.0.1/APIs/schemas/with-refs/io-response-schema.html // The output resource type also has a field for that output's /map/active endpoint // and a field that represents a 'staged' endpoint which contains the output-specific // "action" when there is a scheduled or in-flight immediate activation for that // output (an "activation_id" and the "activation" object are also included so that // the /map/activations endpoints can be implemented similarly to the /io endpoint) - // See https://github.com/AMWA-TV/nmos-audio-channel-mapping/blob/v1.0.x/APIs/schemas/map-active-output-response-schema.json - // and https://github.com/AMWA-TV/nmos-audio-channel-mapping/blob/v1.0.x/APIs/schemas/map-activations-activation-get-response-schema.json + // See https://specs.amwa.tv/is-08/releases/v1.0.1/APIs/schemas/with-refs/map-active-output-response-schema.html + // and https://specs.amwa.tv/is-08/releases/v1.0.1/APIs/schemas/with-refs/map-activations-activation-get-response-schema.html // Note that the input/output identifiers used in the Channel Mapping API are not universally unique // and one input and one output in an API instance may even share the same identifier // so these need to be prefixed with the resource type to make the nmos::resource::id locally unique - // See https://github.com/AMWA-TV/nmos-audio-channel-mapping/blob/v1.0.x/docs/4.0.%20Behaviour.md#identifiers + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/4.0._Behaviour.html#identifiers typedef utility::string_t channelmapping_id; nmos::id make_channelmapping_resource_id(const std::pair& id_type); diff --git a/Development/nmos/channels.cpp b/Development/nmos/channels.cpp index 611e22f3d..e9f596099 100644 --- a/Development/nmos/channels.cpp +++ b/Development/nmos/channels.cpp @@ -155,4 +155,52 @@ namespace nmos return channel_order.str(); } + + // See SMPTE ST 2110-30:2017 Section 6.2.2 Channel Order Convention + std::vector parse_fmtp_channel_order(const utility::string_t& channel_order) + { + std::vector channels; + + const auto first = channel_order.data(); + const auto last = first + channel_order.size(); + auto it = first; + + // check prefix + + static const auto prefix = U("SMPTE2110.("); + auto pit = &prefix[0]; + while (it != last && *pit != U('\0') && *it == *pit) ++it, ++pit; + if (*pit != U('\0')) return {}; + + // parse comma-separated channel group symbols + + while (true) + { + const auto git = it; + while (it != last && *it != U(')') && *it != U(',')) ++it; + if (it == last) return {}; + + const channel_group_symbol symbol(utility::string_t(git, it)); + + // hm, does not handle 22.2 Surround ('222'), SDI audio group ('SGRP') or Undefined ('U01' to 'U64') + auto group = std::find_if(details::channel_groups.begin(), details::channel_groups.end(), + [&](const std::pair, channel_group_symbol>& group) + { + return symbol == group.second; + }); + if (details::channel_groups.end() == group) return {}; + channels.insert(channels.end(), group->first.begin(), group->first.end()); + + if (*it == U(')')) break; + ++it; + } + + // check suffix + + if (it == last) return {}; + ++it; + if (it != last) return {}; + + return channels; + } } diff --git a/Development/nmos/channels.h b/Development/nmos/channels.h index d8950f130..df0522b12 100644 --- a/Development/nmos/channels.h +++ b/Development/nmos/channels.h @@ -15,7 +15,7 @@ namespace web namespace nmos { // Audio channel symbols (used in audio sources) from VSF TR-03 Appendix A - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_audio.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_audio.html // and http://www.videoservicesforum.org/download/technical_recommendations/VSF_TR-03_2015-11-12.pdf DEFINE_STRING_ENUM(channel_symbol) namespace channel_symbols @@ -68,7 +68,7 @@ namespace nmos // Numbered Source Channel (001..127) // "due to original regex [in source_audio.json] allowing NSC000, but NSC001-NSC128 possibly // being preferable for consistency with U01-U64", it's OK to use NSC000 and NSC128 also! - // see https://github.com/AMWA-TV/nmos-discovery-registration/pull/145 + // see https://github.com/AMWA-TV/is-04/pull/145 const channel_symbol NSC(unsigned int channel_number); // Undefined channel (01..64) @@ -113,6 +113,7 @@ namespace nmos // See SMPTE ST 2110-30:2017 Section 6.2.2 Channel Order Convention utility::string_t make_fmtp_channel_order(const std::vector& channels); + std::vector parse_fmtp_channel_order(const utility::string_t& channel_order); } #endif diff --git a/Development/nmos/client_utils.cpp b/Development/nmos/client_utils.cpp index 5d0b9adf8..bce7d9588 100644 --- a/Development/nmos/client_utils.cpp +++ b/Development/nmos/client_utils.cpp @@ -1,12 +1,20 @@ #include "nmos/client_utils.h" +// cf. preprocessor conditions in nmos::details::make_client_ssl_context_callback and nmos::details::make_client_nativehandle_options #if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) +#if defined(__linux__) +#include +#endif #include "boost/asio/ssl/set_cipher_list.hpp" +#include "cpprest/host_utils.h" #endif #include "cpprest/basic_utils.h" #include "cpprest/details/system_error.h" #include "cpprest/http_utils.h" +#include "cpprest/response_type.h" #include "cpprest/ws_client.h" +#include "nmos/certificate_settings.h" +#include "nmos/json_fields.h" #include "nmos/slog.h" #include "nmos/ssl_context_options.h" @@ -26,17 +34,25 @@ namespace nmos namespace details { +// cf. preprocessor conditions in nmos::make_http_client_config and nmos::make_websocket_client_config #if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) template - inline std::function make_client_ssl_context_callback(const nmos::settings& settings) + inline std::function make_client_ssl_context_callback(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { - const auto ca_certificate_file = utility::us2s(nmos::experimental::fields::ca_certificate_file(settings)); - return [ca_certificate_file](boost::asio::ssl::context& ctx) + if (!load_ca_certificates) + { + load_ca_certificates = make_load_ca_certificates_handler(settings, gate); + } + + return [load_ca_certificates](boost::asio::ssl::context& ctx) { try { ctx.set_options(nmos::details::ssl_context_options); - ctx.load_verify_file(ca_certificate_file); + + const auto cacerts = utility::us2s(load_ca_certificates()); + ctx.add_certificate_authority(boost::asio::buffer(cacerts.data(), cacerts.size())); + set_cipher_list(ctx, nmos::details::ssl_cipher_list); } catch (const boost::system::system_error& e) @@ -45,48 +61,245 @@ namespace nmos } }; } +#endif + +#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) + // bind socket to a specific network interface + // for now, only supporting Linux because SO_BINDTODEVICE is not defined on Windows and Mac + inline void bind_to_device(const utility::string_t& interface_name, bool secure, void* native_handle) + { +#if defined(__linux__) + int socket_fd; + // hmm, frustrating that native_handle type has been erased so we need secure flag + if (secure) + { + auto socket = (boost::asio::ssl::stream*)native_handle; + if (!socket->lowest_layer().is_open()) + { + // for now, limited to IPv4 + socket->lowest_layer().open(boost::asio::ip::tcp::v4()); + } + socket_fd = socket->lowest_layer().native_handle(); + } + else + { + auto socket = (boost::asio::ip::tcp::socket*)native_handle; + if (!socket->is_open()) + { + // for now, limited to IPv4 + socket->open(boost::asio::ip::tcp::v4()); + } + socket_fd = socket->lowest_layer().native_handle(); + } + const auto interface_name_ = utility::us2s(interface_name); + if (0 != setsockopt(socket_fd, SOL_SOCKET, SO_BINDTODEVICE, interface_name_.data(), interface_name_.length())) + { + char error[1024]; + throw std::runtime_error(strerror_r(errno, error, sizeof(error))); + } +#else + throw std::logic_error("unsupported"); +#endif + } + + inline std::function make_client_nativehandle_options(bool secure, const utility::string_t& client_address, slog::base_gate& gate) + { + if (client_address.empty()) return {}; + // get the associated network interface name from IP address + const auto interface_name = web::hosts::experimental::get_interface_name(client_address); + if (interface_name.empty()) + { + slog::log(gate, SLOG_FLF) << "No network interface found for " << client_address << " to bind for the HTTP client connection"; + return {}; + } + + return [interface_name, secure, &gate](web::http::client::native_handle native_handle) + { + try + { + bind_to_device(interface_name, secure, native_handle); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Unable to bind HTTP client connection to " << interface_name << ": " << e.what(); + } + }; + } + +#ifdef CPPRESTSDK_ENABLE_BIND_WEBSOCKET_CLIENT + // The current version of the C++ REST SDK 2.10.19 does not provide the callback to enable the custom websocket setting + inline std::function make_ws_client_nativehandle_options(bool secure, const utility::string_t& client_address, slog::base_gate& gate) + { + if (client_address.empty()) return {}; + // get the associated network interface name from IP address + const auto interface_name = web::hosts::experimental::get_interface_name(client_address); + if (interface_name.empty()) + { + slog::log(gate, SLOG_FLF) << "No network interface found for " << client_address << " to bind for the websocket client connection"; + return {}; + } + + return [interface_name, secure, &gate](web::websockets::client::native_handle native_handle) + { + try + { + bind_to_device(interface_name, secure, native_handle); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Unable to bind websocket client connection to " << interface_name << ": " << e.what(); + } + }; + } +#endif + #endif } - // construct client config based on settings, e.g. using the specified proxy + // construct client config based on specified secure flag and settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. request timeout - web::http::client::http_client_config make_http_client_config(const nmos::settings& settings) + web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { web::http::client::http_client_config config; const auto proxy = proxy_uri(settings); if (!proxy.is_empty()) config.set_proxy(proxy); - config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings)); + if (secure) config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings)); #if !defined(_WIN32) && !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) - config.set_ssl_context_callback(details::make_client_ssl_context_callback(settings)); + if (secure) config.set_ssl_context_callback(details::make_client_ssl_context_callback(settings, load_ca_certificates, gate)); + config.set_nativehandle_options(details::make_client_nativehandle_options(secure, nmos::experimental::fields::client_address(settings), gate)); #endif return config; } - // construct client config based on settings, e.g. using the specified proxy + // construct client config based on settings, e.g. using the specified proxy and OCSP config + // with the remaining options defaulted, e.g. request timeout + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + return make_http_client_config(nmos::experimental::fields::client_secure(settings), settings, load_ca_certificates, gate); + } + + // construct oauth2 config with the bearer token + web::http::oauth2::experimental::oauth2_config make_oauth2_config(const web::http::oauth2::experimental::oauth2_token& bearer_token) + { + web::http::oauth2::experimental::oauth2_config config(U(""), U(""), U(""), U(""), U(""), U("")); + config.set_token(bearer_token); + + return config; + } + + // construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy and OCSP config + // with the remaining options defaulted, e.g. authorization request timeout + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) + { + auto config = make_http_client_config(settings, load_ca_certificates, gate); + + if (bearer_token.is_valid_access_token()) + { + config.set_oauth2(make_oauth2_config(bearer_token)); + } + + return config; + } + + // construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy and OCSP config + // with the remaining options defaulted, e.g. authorization request timeout + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate) + { + web::http::oauth2::experimental::oauth2_token bearer_token; + + if (get_authorization_bearer_token) + { + bearer_token = get_authorization_bearer_token(); + } + + return make_http_client_config(settings, load_ca_certificates, bearer_token, gate); + } + + // construct client config based on specified secure flag and settings, e.g. using the specified proxy // with the remaining options defaulted - web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings) + web::websockets::client::websocket_client_config make_websocket_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { web::websockets::client::websocket_client_config config; const auto proxy = proxy_uri(settings); if (!proxy.is_empty()) config.set_proxy(proxy); - config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings)); + if (secure) config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings)); #if !defined(_WIN32) || !defined(__cplusplus_winrt) - config.set_ssl_context_callback(details::make_client_ssl_context_callback(settings)); + if (secure) config.set_ssl_context_callback(details::make_client_ssl_context_callback(settings, load_ca_certificates, gate)); +#ifdef CPPRESTSDK_ENABLE_BIND_WEBSOCKET_CLIENT + config.set_nativehandle_options(details::make_ws_client_nativehandle_options(secure, nmos::experimental::fields::client_address(settings), gate)); +#endif #endif return config; } + // construct client config based on settings, e.g. using the specified proxy + // with the remaining options defaulted + web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + return make_websocket_client_config(nmos::experimental::fields::client_secure(settings), settings, load_ca_certificates, gate); + } + + // construct client config based on settings and access token, e.g. using the specified proxy + // with the remaining options defaulted + web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate) + { + auto config = make_websocket_client_config(settings, std::move(load_ca_certificates), gate); + + if (get_authorization_bearer_token) + { + const auto bearer_token = get_authorization_bearer_token(); + config.headers().add(web::http::header_names::authorization, U("Bearer ") + bearer_token.access_token()); + } + + return config; + } + + namespace details + { + // make a client for the specified base_uri and config, with host name for the Host header sneakily stashed in user info + std::unique_ptr make_http_client(const web::uri& base_uri, const web::http::client::http_client_config& client_config) + { + // unstash the host name for the Host header + // cf. nmos::details::resolve_service + // don't bother clearing user_info since http_client makes no use of it + // see https://github.com/microsoft/cpprestsdk/issues/3 + std::unique_ptr client(new web::http::client::http_client(base_uri, client_config)); + if (!base_uri.user_info().empty()) + { + auto host = base_uri.user_info(); + + // hmm, in secure mode, don't append the port to the Host header + // because both calc_cn_host in cpprestsdk/Release/src/http/client/http_client_asio.cpp + // and winhttp_client::send_request in cpprestsdk/Release/src/http/client/http_client_winhttp.cpp + // compare the entire Host header value with the certificate Common Name + // which causes an SSL handshake error + // see https://github.com/microsoft/cpprestsdk/issues/1790 + if (base_uri.port() > 0 && !web::is_secure_uri_scheme(base_uri.scheme())) + { + host.append(U(":")).append(utility::conversions::details::to_string_t(base_uri.port())); + } + + client->add_handler([host](web::http::http_request request, std::shared_ptr next_stage) -> pplx::task + { + request.headers().add(web::http::header_names::host, host); + return next_stage->propagate(request); + }); + } + return client; + } + } + // make a request with logging pplx::task api_request(web::http::client::http_client client, web::http::http_request request, slog::base_gate& gate, const pplx::cancellation_token& token) { slog::log(gate, SLOG_FLF) << "Sending request"; // see https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API#Resource_loading_timestamps - const auto start_time = std::chrono::system_clock::now(); + const auto start_time = std::chrono::steady_clock::now(); return client.request(request, token).then([start_time, &gate](web::http::http_response res) { - const auto response_start = std::chrono::system_clock::now(); + const auto response_start = std::chrono::steady_clock::now(); const auto request_dur = std::chrono::duration_cast(response_start - start_time).count() / 1000.0; // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing diff --git a/Development/nmos/client_utils.h b/Development/nmos/client_utils.h index e9007e62e..c1f83e3f7 100644 --- a/Development/nmos/client_utils.h +++ b/Development/nmos/client_utils.h @@ -2,6 +2,8 @@ #define NMOS_CLIENT_UTILS_H #include "cpprest/http_client.h" // for http_client, http_client_config, http_response, etc. +#include "nmos/authorization_handlers.h" +#include "nmos/certificate_handlers.h" #include "nmos/settings.h" namespace web { namespace websockets { namespace client { class websocket_client_config; } } } @@ -10,13 +12,30 @@ namespace slog { class base_gate; } // Utility types, constants and functions for implementing NMOS REST API clients namespace nmos { - // construct client config based on settings, e.g. using the specified proxy + // construct client config based on specified secure flag and settings, e.g. using the specified proxy and OCSP config + // with the remaining options defaulted, e.g. request timeout + web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + // construct client config based on settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. request timeout - web::http::client::http_client_config make_http_client_config(const nmos::settings& settings); + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + // construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy and OCSP config + // with the remaining options defaulted, e.g. authorization request timeout + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate); + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate); + // construct client config based on specified secure flag and settings, e.g. using the specified proxy + // with the remaining options defaulted + web::websockets::client::websocket_client_config make_websocket_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); // construct client config based on settings, e.g. using the specified proxy // with the remaining options defaulted - web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings); + web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate); + web::websockets::client::websocket_client_config make_websocket_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); + + namespace details + { + // make a client for the specified base_uri and config, with host name for the Host header sneakily stashed in user info + std::unique_ptr make_http_client(const web::uri& base_uri_with_host_name_in_user_info, const web::http::client::http_client_config& client_config); + } // make an API request with logging pplx::task api_request(web::http::client::http_client client, web::http::http_request request, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()); diff --git a/Development/nmos/clock_name.h b/Development/nmos/clock_name.h index 83065d1ce..791054a36 100644 --- a/Development/nmos/clock_name.h +++ b/Development/nmos/clock_name.h @@ -7,9 +7,9 @@ namespace nmos { // Clock name - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/clock_internal.json - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/clock_ptp.json - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_core.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/clock_internal.html + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/clock_ptp.html + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_core.html DEFINE_STRING_ENUM(clock_name) namespace clock_names { diff --git a/Development/nmos/clock_ref_type.h b/Development/nmos/clock_ref_type.h index 7a32bf23a..e9ee59d71 100644 --- a/Development/nmos/clock_ref_type.h +++ b/Development/nmos/clock_ref_type.h @@ -6,8 +6,8 @@ namespace nmos { // Clock reference type - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/clock_internal.json - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/clock_ptp.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/clock_internal.html + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/clock_ptp.html DEFINE_STRING_ENUM(clock_ref_type) namespace clock_ref_types { diff --git a/Development/nmos/colorspace.h b/Development/nmos/colorspace.h index 8ec98d561..fe4015161 100644 --- a/Development/nmos/colorspace.h +++ b/Development/nmos/colorspace.h @@ -6,14 +6,28 @@ namespace nmos { // Colorspace (used in video flows) - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_video.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_video.html + // and https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#colorspace DEFINE_STRING_ENUM(colorspace) namespace colorspaces { + // Recommendation ITU-R BT.601-7 const colorspace BT601{ U("BT601") }; + // Recommendation ITU-R BT.709-6 const colorspace BT709{ U("BT709") }; + // Recommendation ITU-R BT.2020-2 const colorspace BT2020{ U("BT2020") }; + // Recommendation ITU-R BT.2100 Table 2 titled "System colorimetry" const colorspace BT2100{ U("BT2100") }; + + // Since IS-04 v1.3, colorspace values may be defined in the Flow Attributes register of the NMOS Parameter Registers + + // SMPTE ST 2065-1 Academy Color Encoding Specification (ACES) + const colorspace ST2065_1{ U("ST2065-1") }; + // SMPTE ST 2065-3 Academy Density Exchange Encoding (ADX) + const colorspace ST2065_3{ U("ST2065-3") }; + // ISO 11664-1 CIE 1931 standard colorimetric system + const colorspace XYZ{ U("XYZ") }; } } diff --git a/Development/nmos/components.cpp b/Development/nmos/components.cpp index a98995999..d3c2d3065 100644 --- a/Development/nmos/components.cpp +++ b/Development/nmos/components.cpp @@ -14,6 +14,7 @@ namespace nmos }); } + // deprecated, see overload with sdp::sampling in nmos/sdp_utils.h web::json::value make_components(chroma_subsampling chroma_subsampling, unsigned int frame_width, unsigned int frame_height, unsigned int bit_depth) { using web::json::value; @@ -28,7 +29,7 @@ namespace nmos make_component(component_names::B, frame_width, frame_height, bit_depth) }); case YCbCr422: - return value_of({ + return value_of({ make_component(component_names::Y, frame_width, frame_height, bit_depth), make_component(component_names::Cb, frame_width / 2, frame_height, bit_depth), make_component(component_names::Cr, frame_width / 2, frame_height, bit_depth) diff --git a/Development/nmos/components.h b/Development/nmos/components.h index a1fb31ed0..59907acf2 100644 --- a/Development/nmos/components.h +++ b/Development/nmos/components.h @@ -6,8 +6,9 @@ namespace nmos { - // Components (used in raw video flows) - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_video_raw.json + // Components (for raw video flows since IS-04 v1.1, extended to coded video Flows since v1.3 by the entry in the Flow Attributes register) + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_video_raw.html + // and https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#components DEFINE_STRING_ENUM(component_name) namespace component_names { @@ -22,11 +23,22 @@ namespace nmos const component_name G{ U("G") }; const component_name B{ U("B") }; const component_name DepthMap{ U("DepthMap") }; + + // Since IS-04 v1.3, component names may be defined in the Flow Attributes register of the NMOS Parameter Registers + // The following values support CLYCbCr, XYZ, and KEY signal formats, see sdp::samplings + + const component_name Yc{ U("Yc") }; + const component_name Cbc{ U("Cbc") }; + const component_name Crc{ U("Crc") }; + const component_name X{ U("X") }; + const component_name Z{ U("Z") }; + const component_name Key{ U("Key") }; } web::json::value make_component(const nmos::component_name& name, unsigned int width, unsigned int height, unsigned int bit_depth); enum chroma_subsampling : int { YCbCr422, RGB444 }; + // deprecated, see overload with sdp::sampling in nmos/sdp_utils.h web::json::value make_components(chroma_subsampling chroma_subsampling = YCbCr422, unsigned int frame_width = 1920, unsigned int frame_height = 1080, unsigned int bit_depth = 10); } diff --git a/Development/nmos/connection_activation.cpp b/Development/nmos/connection_activation.cpp index 1931abbc7..5781eed31 100644 --- a/Development/nmos/connection_activation.cpp +++ b/Development/nmos/connection_activation.cpp @@ -147,7 +147,7 @@ namespace nmos // the resolve_auto callback may throw exceptions, which will prevent activation in order that // "if there is an error condition that means `auto` cannot be resolved, the active transport parameters // must not change, and the underlying sender [or receiver] must continue as before." - // see https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/ConnectionAPI.raml#L308-L309 + // see https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/ConnectionAPI.html#single_senders__senderid__active_get resolve_auto(*matching_resource, connection_resource, nmos::fields::transport_params(endpoint_active)); active = nmos::fields::master_enable(endpoint_active); diff --git a/Development/nmos/connection_api.cpp b/Development/nmos/connection_api.cpp index 164586002..eb63bcafb 100644 --- a/Development/nmos/connection_api.cpp +++ b/Development/nmos/connection_api.cpp @@ -11,6 +11,7 @@ #include "nmos/is04_versions.h" #include "nmos/is05_versions.h" #include "nmos/json_schema.h" +#include "nmos/media_type.h" #include "nmos/model.h" #include "nmos/sdp_utils.h" #include "nmos/slog.h" @@ -23,7 +24,7 @@ namespace nmos { web::http::experimental::listener::api_router make_unmounted_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, slog::base_gate& gate); - web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, slog::base_gate& gate) + web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -41,6 +42,12 @@ namespace nmos return pplx::task_from_result(true); }); + if (validate_authorization) + { + connection_api.support(U("/x-nmos/") + nmos::patterns::connection_api.pattern + U("/?"), validate_authorization); + connection_api.support(U("/x-nmos/") + nmos::patterns::connection_api.pattern + U("/.*"), validate_authorization); + } + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is05_versions::from_settings(model.settings); }); connection_api.support(U("/x-nmos/") + nmos::patterns::connection_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { @@ -53,9 +60,14 @@ namespace nmos return connection_api; } + web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, slog::base_gate& gate) + { + return make_connection_api(model, parse_transport_file, validate_merged, {}, gate); + } + web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, slog::base_gate& gate) { - return make_connection_api(model, &parse_rtp_transport_file, {}, gate); + return make_connection_api(model, &parse_rtp_transport_file, {}, {}, gate); } inline bool is_connection_api_permitted_downgrade(const nmos::resource& resource, const nmos::resource& connection_resource, const nmos::api_version& version) @@ -64,7 +76,7 @@ namespace nmos if (resource.version.minor <= version.minor) return true; // "Where a transport type is added in a new version of the Connection Management API specification, earlier versioned APIs must not list any Senders or Receivers which make use of this new transport type." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/5.0.%20Upgrade%20Path.md#requirements-for-connection-management-apis + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/5.0._Upgrade_Path.html#requirements-for-connection-management-apis typedef const std::map> versions_transport_bases_t; versions_transport_bases_t versions_transport_bases @@ -137,8 +149,8 @@ namespace nmos static const std::map>& rtp_auto_constraints() { // These are the constraints that support "auto" in /staged; cf. resolve_rtp_auto - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/sender_transport_params_rtp.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver_transport_params_rtp.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/sender_transport_params_rtp.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver_transport_params_rtp.html static const std::map> auto_constraints { { @@ -178,8 +190,8 @@ namespace nmos static const std::map>& websocket_auto_constraints() { // These are the constraints that support "auto" in /staged - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/sender_transport_params_websocket.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver_transport_params_websocket.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/sender_transport_params_websocket.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver_transport_params_websocket.html static const std::map> auto_constraints { { @@ -202,8 +214,8 @@ namespace nmos static const std::map>& mqtt_auto_constraints() { // These are the constraints that support "auto" in /staged - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/sender_transport_params_mqtt.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver_transport_params_mqtt.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/sender_transport_params_mqtt.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver_transport_params_mqtt.html static const std::map> auto_constraints { { @@ -304,7 +316,7 @@ namespace nmos } // Apparently, the response to errors in the transport file should be 500 Internal Error rather than 400 Bad Request - // See https://github.com/AMWA-TV/nmos-device-connection-management/issues/40 + // See https://github.com/AMWA-TV/is-05/issues/40 inline std::logic_error transport_file_error(const std::string& message) { return std::logic_error("Transport file error - " + message); @@ -313,8 +325,8 @@ namespace nmos std::pair get_transport_type_data(const web::json::value& transport_file) { // "'data' and 'type' must both be strings or both be null" - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0.2/APIs/schemas/v1.0-receiver-response-schema.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver-transport-file.json + // See https://specs.amwa.tv/is-05/releases/v1.0.2/APIs/schemas/with-refs/v1.0-receiver-response-schema.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver-transport-file.html if (!transport_file.has_field(nmos::fields::data)) throw transport_file_error("data is required"); @@ -436,7 +448,7 @@ namespace nmos // "If a 'bulk' request includes multiple sets of parameters for the same Sender or Receiver ID the behaviour is defined by the implementation. // In order to maximise interoperability clients are encouraged not to include the same Sender or Receiver ID multiple times in the same 'bulk' request." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0.2/docs/4.0.%20Behaviour.md#salvo-operation + // See https://specs.amwa.tv/is-05/releases/v1.0.2/docs/4.0._Behaviour.html#salvo-operation if (!requested_time_or_null.is_null() && request_time == web::json::as(requested_time_or_null)) { slog::log(gate, SLOG_FLF) << "Rejecting PATCH request for " << id_type << " due to a pending immediate activation from the same bulk request"; @@ -444,7 +456,7 @@ namespace nmos return details::make_connection_resource_patch_error_response(status_codes::BadRequest); } // "If an API implementation receives a new PATCH request to the /staged resource while an activation is in progress it SHOULD block the request until the previous activation is complete." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/4.0.%20Behaviour.md#in-progress-activations + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.0._Behaviour.html#in-progress-activations else if (!details::wait_immediate_activation_not_pending(model, lock, id_type) || model.shutdown) { slog::log(gate, SLOG_FLF) << "Rejecting PATCH request for " << id_type << " due to a pending immediate activation"; @@ -480,12 +492,12 @@ namespace nmos // "In the case where the transport file and transport parameters are updated in the same PATCH request // transport parameters specified in the request object take precedence over those in the transport file." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/ConnectionAPI.raml#L369 + // See https://specs.amwa.tv/is-05/releases/v1.0.0/APIs/ConnectionAPI.html#single_receivers__receiverid__staged_patch // "In all other cases the most recently received PATCH request takes priority." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/docs/4.1.%20Behaviour%20-%20RTP%20Transport%20Type.md#interpretation-of-sdp-files + // See https://specs.amwa.tv/is-05/releases/v1.0.0/docs/4.1._Behaviour_-_RTP_Transport_Type.html#interpretation-of-sdp-files // First, validate and merge the transport file (this resource must be a receiver) - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/ConnectionAPI.raml#L344-L363 + // See https://specs.amwa.tv/is-05/releases/v1.0.0/APIs/ConnectionAPI.html#single_receivers__receiverid__staged_patch auto& transport_file = nmos::fields::transport_file(patch); if (!transport_file.is_null() && !transport_file.as_object().empty()) @@ -634,21 +646,27 @@ namespace nmos { slog::log(gate, SLOG_FLF) << "Returning transport file for " << id_type; + const auto transportfile_type = nmos::fields::transportfile_type(transportfile); + // hmm, parsing of the Accept header could be much better and should take account of quality values - if (!accept.empty() && web::http::details::mime_types::application_json == web::http::details::get_mime_type(accept) && U("application/sdp") == nmos::fields::transportfile_type(transportfile)) + const auto accept_type = web::http::details::get_mime_type(accept); + // allow application/json as well as the more precise application/sdp+json + const std::set sdp_json_types{ nmos::media_types::application_json.name, nmos::media_types::application_sdp_json.name }; + if (nmos::media_types::application_sdp.name == transportfile_type && 0 != sdp_json_types.count(accept_type)) { // Experimental extension - SDP as JSON + res.headers().set_content_type(accept_type); set_reply(res, status_codes::OK, sdp::parse_session_description(utility::us2s(data.as_string()))); } else { // This automatically performs conversion to UTF-8 if required (i.e. on Windows) - set_reply(res, status_codes::OK, data.as_string(), nmos::fields::transportfile_type(transportfile)); + set_reply(res, status_codes::OK, data.as_string(), transportfile_type); } // "It is strongly recommended that the following caching headers are included via the /transportfile endpoint (or whatever this endpoint redirects to). // This is important to ensure that connection management clients do not cache the contents of transport files which are liable to change." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/docs/4.0.%20Behaviour.md#transport-files--caching + // See https://specs.amwa.tv/is-05/releases/v1.0.0/docs/4.0._Behaviour.html#transport-files--caching res.headers().set_cache_control(U("no-cache")); } else @@ -666,16 +684,9 @@ namespace nmos // (or this is an internal server error, but since a 5xx response is not defined, assume one of the former cases) slog::log(gate, SLOG_FLF) << "Transport file requested for " << id_type << " which does not have one"; - // An HTTP 404 response may be returned if "the transport type does not require a transport file". - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/ConnectionAPI.raml#L339-L340 - // "When the `master_enable` parameter is false [...] the `/transportfile` endpoint should return an HTTP 404 response." - // In other words an HTTP 404 response is returned "if the sender is not currently configured". - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/ConnectionAPI.raml#L163-L165 - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/ConnectionAPI.raml#L277 - // Those statements are going to be combined and adjusted slightly in the next specification patch release to say: // An HTTP 404 response is returned if "the transport type does not require a transport file, or if the sender is not currently configured" - // and "may also be returned when the `master_enable` parameter is false in /active, if the sender only maintains a transport file when transmitting." - // See https://github.com/AMWA-TV/nmos-device-connection-management/pull/111 + // and "may also be returned when the `master_enable` parameter is `false` in /active, if the sender only maintains a transport file when transmitting." + // See https://specs.amwa.tv/is-05/releases/v1.1.1/APIs/ConnectionAPI.html#http-status-code-404-6 set_error_reply(res, status_codes::NotFound, U("Sender is not configured with a transport file")); } } @@ -722,19 +733,19 @@ namespace nmos // transport parameters must not change, and the underlying sender must continue as before." // Therefore, in case it throws an exception, resolve_auto is called on a copy of the /staged resource data, // before making any changes to the /active resource data. - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/ConnectionAPI.raml#L300-L309 - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/2.2.%20APIs%20-%20Server%20Side%20Implementation.md#use-of-auto + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/ConnectionAPI.html#single_senders__senderid__active_get + // and https://specs.amwa.tv/is-05/releases/v1.1.0/docs/2.2._APIs_-_Server_Side_Implementation.html#use-of-auto auto activating = staged; resolve_auto(activating); // "When a set of 'staged' settings is activated, these settings transition into the 'active' resource." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/docs/1.0.%20Overview.md#active + // See https://specs.amwa.tv/is-05/releases/v1.0.0/docs/1.0._Overview.html#active active = activating; // Unclear whether the activation in the active endpoint should have values for mode, requested_time // (and even activation_time?) or whether they should be null? The examples have them with values. - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/examples/v1.0-receiver-active-get-200.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/examples/v1.0-sender-active-get.json + // See https://specs.amwa.tv/is-05/releases/v1.0.0/examples/v1.0-receiver-active-get-200.html + // and https://specs.amwa.tv/is-05/releases/v1.0.0/examples/v1.0-sender-active-get.html if (nmos::activation_modes::activate_scheduled_absolute == staged_mode || nmos::activation_modes::activate_scheduled_relative == staged_mode) @@ -755,10 +766,10 @@ namespace nmos // "This parameter returns to null on the staged endpoint once an activation is completed." // "This field returns to null once the activation is completed on the staged endpoint." // "On the staged endpoint this field returns to null once the activation is completed." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/schemas/v1.0-activation-response-schema.json + // See https://specs.amwa.tv/is-05/releases/v1.0.0/APIs/schemas/with-refs/v1.0-activation-response-schema.html // "A resource may be unlocked by setting `mode` in `activation` to `null`, which will cancel the pending activation." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/ConnectionAPI.raml#L244 + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/ConnectionAPI.html#http-status-code-423 staged_activation = nmos::make_activation(); } @@ -775,12 +786,12 @@ namespace nmos // "The 'receiver_id' key MUST be set to `null` in all cases except where a unicast push-based Sender is configured to transmit to an NMOS Receiver, and the 'active' key is set to 'true'." // "The 'sender_id' key MUST be set to `null` in all cases except where the Receiver is currently configured to receive from an NMOS Sender, and the 'active' key is set to 'true'. - // See https://github.com/amwa-tv/nmos-discovery-registration/blob/v1.2.2/docs/4.3.%20Behaviour%20-%20Nodes.md#api-resources + // See https://specs.amwa.tv/is-04/releases/v1.2.2/docs/4.3._Behaviour_-_Nodes.html#api-resources const auto ci = active && !connected_id.empty() ? value::string(connected_id) : value::null(); // "When the 'active' parameters of a Sender or Receiver are modified, or when a re-activation of the same parameters // is performed, the 'version' attribute of the relevant IS-04 Sender or Receiver must be incremented." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/docs/3.1.%20Interoperability%20-%20NMOS%20IS-04.md#version-increments + // See https://specs.amwa.tv/is-05/releases/v1.0.0/docs/3.1._Interoperability_-_NMOS_IS-04.html#version-increments resource.data[nmos::fields::version] = at; // Senders indicate the connected receiver_id, receivers indicate the connected sender_id @@ -800,10 +811,17 @@ namespace nmos } } - // Validate and parse the specified transport file for the specified receiver + // Parse and validate the specified transport file for the specified receiver using the default validator + // (this is the default transport file parser) web::json::value parse_rtp_transport_file(const nmos::resource& receiver, const nmos::resource& connection_receiver, const utility::string_t& transport_file_type, const utility::string_t& transport_file_data, slog::base_gate& gate) { - if (transport_file_type != U("application/sdp")) + return details::parse_rtp_transport_file(&validate_sdp_parameters, receiver, connection_receiver, transport_file_type, transport_file_data, gate); + } + + // Parse and validate the specified transport file for the specified receiver using the specified validator + web::json::value details::parse_rtp_transport_file(details::sdp_parameters_validator validate_sdp_parameters, const nmos::resource& receiver, const nmos::resource& connection_receiver, const utility::string_t& transport_file_type, const utility::string_t& transport_file_data, slog::base_gate& gate) + { + if (transport_file_type != nmos::media_types::application_sdp.name) { throw std::runtime_error("unexpected type: " + utility::us2s(transport_file_type)); } @@ -826,7 +844,7 @@ namespace nmos // "Where a Receiver supports SMPTE 2022-7 but is required to Receive a non-SMPTE 2022-7 stream, // only the first set of transport parameters should be used. rtp_enabled in the second set of parameters // must be set to false" - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/docs/4.1.%20Behaviour%20-%20RTP%20Transport%20Type.md#operation-with-smpte-2022-7 + // See https://specs.amwa.tv/is-05/releases/v1.0.0/docs/4.1._Behaviour_-_RTP_Transport_Type.html#operation-with-smpte-2022-7 if (2 == legs && 1 == sdp_transport_params.second.size()) { web::json::push_back(sdp_transport_params.second, web::json::value_of({ { U("rtp_enabled"), false } })); @@ -836,17 +854,17 @@ namespace nmos } // "On activation all instances of "auto" should be resolved into the actual values that will be used" - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/ConnectionAPI.raml#L300-L301 - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/sender_transport_params_rtp.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver_transport_params_rtp.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/ConnectionAPI.html#single_senders__senderid__active_get + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/sender_transport_params_rtp.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver_transport_params_rtp.html // "In many cases this is a simple operation, and the behaviour is very clearly defined in the relevant transport parameter schemas. // For example a port number may be offset from the RTP port number by a pre-determined value. The specification makes suggestions // of a sensible default value for "auto" to resolve to, but the Sender or Receiver may choose any value permitted by the schema // and constraints." // This function implements those sensible defaults for the RTP transport type. // "In some cases the behaviour is more complex, and may be determined by the vendor." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/2.2.%20APIs%20-%20Server%20Side%20Implementation.md#use-of-auto - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/4.1.%20Behaviour%20-%20RTP%20Transport%20Type.md#use-of-auto + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/2.2._APIs_-_Server_Side_Implementation.html#use-of-auto + // and https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.1._Behaviour_-_RTP_Transport_Type.html#use-of-auto // This function therefore does not select a value for e.g. sender "source_ip" or receiver "interface_ip". void resolve_rtp_auto(const nmos::type& type, web::json::value& transport_params, int auto_rtp_port) { @@ -908,8 +926,8 @@ namespace nmos // "The API should actively return an HTTP 405 if a GET is called on the [/bulk/senders and /bulk/receivers] endpoint[s]." // This is provided for free by the api_router which also identifies the handler for POST as a "near miss" - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/ConnectionAPI.raml#L39-L44 - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/ConnectionAPI.raml#L73-L78 + // See https://specs.amwa.tv/is-05/releases/v1.0.0/APIs/ConnectionAPI.html#bulk_senders_get + // and https://specs.amwa.tv/is-05/releases/v1.0.0/APIs/ConnectionAPI.html#bulk_receivers_get connection_api.support(U("/bulk/") + nmos::patterns::connectorType.pattern + U("/?"), methods::POST, [&model, parse_transport_file, validate_merged, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) { @@ -933,7 +951,7 @@ namespace nmos // underlying Senders and Receivers, it may choose to perform 'bulk' resource operations // in a parallel fashion internally. This is an implementation decision and is not a // requirement of this specification." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/docs/4.0.%20Behaviour.md + // See https://specs.amwa.tv/is-05/releases/v1.0.0/docs/4.0._Behaviour.html const auto type = nmos::type_from_resourceType(resourceType); @@ -1025,7 +1043,7 @@ namespace nmos if (web::http::is_success_status_code(result.first)) { // make a bulk response success item - // see https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/APIs/schemas/v1.0-bulk-response-schema.json + // see https://specs.amwa.tv/is-05/releases/v1.0.1/APIs/schemas/with-refs/v1.0-bulk-response-schema.html result.second = value_of({ { nmos::fields::id, id }, { U("code"), result.first } @@ -1185,12 +1203,14 @@ namespace nmos // hmm, parsing of the Accept header could be much better and should take account of quality values const auto accept = req.headers().find(web::http::header_names::accept); - if (req.headers().end() != accept && U("application/schema+json") == web::http::details::get_mime_type(accept->second)) + const auto accept_or_empty = req.headers().end() != accept ? accept->second : utility::string_t{}; + const auto accept_type = web::http::details::get_mime_type(accept_or_empty); + if (nmos::media_types::application_schema_json.name == accept_type) { // Experimental extension - constraints as JSON Schema const nmos::transport transport_subclassification(nmos::fields::transport(matching_resource->data)); - res.headers().set_content_type(U("application/schema+json")); + res.headers().set_content_type(accept_type); set_reply(res, status_codes::OK, nmos::details::make_constraints_schema(resource->type, nmos::fields::endpoint_constraints(resource->data), nmos::transport_base(transport_subclassification))); } else @@ -1266,7 +1286,7 @@ namespace nmos if (details::immediate_activation_pending == staged_state) { // "Any GET requests to `/staged` during this time [while an activation is in progress] MAY also be blocked until the activation is complete." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/4.0.%20Behaviour.md#in-progress-activations + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.0._Behaviour.html#in-progress-activations if (!details::wait_immediate_activation_not_pending(model, lock, id_type) || model.shutdown) { slog::log(gate, SLOG_FLF) << "Rejecting GET request for " << id_type << " due to a pending immediate activation"; @@ -1354,7 +1374,7 @@ namespace nmos if (nmos::is_connection_api_permitted_downgrade(*matching_resource, *resource, version)) { // "Returns the URN base for the transport type employed by this sender with any subclassifications or versions removed." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/ConnectionAPI.raml#L349 + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/ConnectionAPI.html#single_senders__senderid__transporttype_get const nmos::transport transport_subclassification(nmos::fields::transport(matching_resource->data)); set_reply(res, status_codes::OK, web::json::value::string(nmos::transport_base(transport_subclassification).name)); } diff --git a/Development/nmos/connection_api.h b/Development/nmos/connection_api.h index a5ed6788b..bc6b24182 100644 --- a/Development/nmos/connection_api.h +++ b/Development/nmos/connection_api.h @@ -10,7 +10,7 @@ namespace slog } // Connection API implementation -// See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/ConnectionAPI.raml +// See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/ConnectionAPI.html namespace nmos { struct api_version; @@ -37,11 +37,11 @@ namespace nmos // Connection API factory functions // callbacks from this function are called with the model locked, and may read but should not write directly to the model - web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, slog::base_gate& gate); + web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); inline web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, transport_file_parser parse_transport_file, slog::base_gate& gate) { - return make_connection_api(model, std::move(parse_transport_file), {}, gate); + return make_connection_api(model, std::move(parse_transport_file), {}, {}, gate); } web::http::experimental::listener::api_router make_connection_api(nmos::node_model& model, slog::base_gate& gate); @@ -69,22 +69,34 @@ namespace nmos // Helper functions for the Connection API callbacks - // Validate and parse the specified transport file for the specified receiver + struct sdp_parameters; + + namespace details + { + // an sdp_parameters_validator validates the specified SDP parameters for the specified IS-04 receiver + // it should throw std::runtime_error to indicate the parameters are not supported by the receiver + typedef std::function sdp_parameters_validator; + + // Parse and validate the specified transport file for the specified receiver using the specified validator + web::json::value parse_rtp_transport_file(sdp_parameters_validator validate_sdp_parameters, const nmos::resource& receiver, const nmos::resource& connection_receiver, const utility::string_t& transport_file_type, const utility::string_t& transport_file_data, slog::base_gate& gate); + } + + // Parse and validate the specified transport file for the specified receiver using the default validator // (this is the default transport file parser) web::json::value parse_rtp_transport_file(const nmos::resource& receiver, const nmos::resource& connection_receiver, const utility::string_t& transport_file_type, const utility::string_t& transport_file_data, slog::base_gate& gate); // "On activation all instances of "auto" must be resolved into the actual values that will be used by the sender, unless there is an error condition." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/ConnectionAPI.raml#L300-L301 - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/sender_transport_params_rtp.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver_transport_params_rtp.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/ConnectionAPI.html#single_senders__senderid__active_get + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/sender_transport_params_rtp.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver_transport_params_rtp.html // "In many cases this is a simple operation, and the behaviour is very clearly defined in the relevant transport parameter schemas. // For example a port number may be offset from the RTP port number by a pre-determined value. The specification makes suggestions // of a sensible default value for "auto" to resolve to, but the Sender or Receiver may choose any value permitted by the schema // and constraints." // This function implements those sensible defaults for the RTP transport type. // "In some cases the behaviour is more complex, and may be determined by the vendor." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/2.2.%20APIs%20-%20Server%20Side%20Implementation.md#use-of-auto - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/4.1.%20Behaviour%20-%20RTP%20Transport%20Type.md#use-of-auto + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/2.2._APIs_-_Server_Side_Implementation.html#use-of-auto + // and https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.1._Behaviour_-_RTP_Transport_Type.html#use-of-auto // This function therefore does not select a value for e.g. sender "source_ip" or receiver "interface_ip". void resolve_rtp_auto(const nmos::type& type, web::json::value& transport_params, int auto_rtp_port = 5004); diff --git a/Development/nmos/connection_events_activation.cpp b/Development/nmos/connection_events_activation.cpp index e452c58cb..8bea107e8 100644 --- a/Development/nmos/connection_events_activation.cpp +++ b/Development/nmos/connection_events_activation.cpp @@ -11,9 +11,9 @@ namespace nmos { // this handler can be used to (un)subscribe IS-07 Events WebSocket receivers with the specified handlers, when they are activated - nmos::connection_activation_handler make_connection_events_websocket_activation_handler(nmos::events_ws_message_handler message_handler, nmos::events_ws_close_handler close_handler, const nmos::settings& settings, slog::base_gate& gate) + nmos::connection_activation_handler make_connection_events_websocket_activation_handler(load_ca_certificates_handler load_ca_certificates, events_ws_message_handler message_handler, events_ws_close_handler close_handler, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, const nmos::settings& settings, slog::base_gate& gate) { - std::shared_ptr events_ws_client(new nmos::events_ws_client(nmos::make_websocket_client_config(settings), nmos::fields::events_heartbeat_interval(settings), gate)); + std::shared_ptr events_ws_client(new nmos::events_ws_client(nmos::make_websocket_client_config(settings, load_ca_certificates, get_authorization_bearer_token, gate), nmos::fields::events_heartbeat_interval(settings), gate)); events_ws_client->set_message_handler(message_handler); events_ws_client->set_close_handler(close_handler); @@ -34,12 +34,12 @@ namespace nmos if (active && !connection_uri_or_null.is_null() && !ext_is_07_source_id_or_null.is_null()) { events_ws_client->subscribe(connection_resource.id, connection_uri_or_null.as_string(), ext_is_07_source_id_or_null.as_string()) - .then(pplx::observe_exception()); + .then(pplx::observe_exception()); } else { events_ws_client->unsubscribe(connection_resource.id) - .then(pplx::observe_exception()); + .then(pplx::observe_exception()); } }; } @@ -119,7 +119,7 @@ namespace nmos else // "health", "shutdown" or "reboot" { // hmm, for "reboot", should probably try to re-make the connection, possibly with exponential back-off - // see https://github.com/AMWA-TV/nmos-device-connection-management/issues/96 + // see https://github.com/AMWA-TV/is-05/issues/96 // for now, just log all of these message types @@ -136,10 +136,10 @@ namespace nmos auto lock = model.write_lock(); // hmm, should probably try to re-make the connection, possibly with exponential back-off, for ephemeral error conditions - // see https://github.com/AMWA-TV/nmos-device-connection-management/issues/96 + // see https://github.com/AMWA-TV/is-05/issues/96 // for now, just reflect this into the /active endpoint of all associated receivers by setting master_enable to false - // see https://github.com/AMWA-TV/nmos-device-connection-management/pull/97 + // see https://github.com/AMWA-TV/is-05/pull/97 const auto activation_time = nmos::tai_now(); diff --git a/Development/nmos/connection_events_activation.h b/Development/nmos/connection_events_activation.h index 41b1136be..4367ab2de 100644 --- a/Development/nmos/connection_events_activation.h +++ b/Development/nmos/connection_events_activation.h @@ -1,6 +1,8 @@ #ifndef NMOS_CONNECTION_EVENTS_ACTIVATION_H #define NMOS_CONNECTION_EVENTS_ACTIVATION_H +#include "nmos/authorization_handlers.h" +#include "nmos/certificate_handlers.h" #include "nmos/connection_activation.h" #include "nmos/events_ws_client.h" // for nmos::events_ws_message_handler, etc. #include "nmos/settings.h" // just a forward declaration of nmos::settings @@ -10,7 +12,17 @@ namespace nmos struct node_model; // this handler can be used to (un)subscribe IS-07 Events WebSocket receivers with the specified handlers, when they are activated - nmos::connection_activation_handler make_connection_events_websocket_activation_handler(nmos::events_ws_message_handler message_handler, nmos::events_ws_close_handler close_handler, const nmos::settings& settings, slog::base_gate& gate); + nmos::connection_activation_handler make_connection_events_websocket_activation_handler(load_ca_certificates_handler load_ca_certificates, events_ws_message_handler message_handler, events_ws_close_handler close_handler, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, const nmos::settings& settings, slog::base_gate& gate); + + inline nmos::connection_activation_handler make_connection_events_websocket_activation_handler(nmos::load_ca_certificates_handler load_ca_certificates, nmos::events_ws_message_handler message_handler, nmos::events_ws_close_handler close_handler, const nmos::settings& settings, slog::base_gate& gate) + { + return make_connection_events_websocket_activation_handler(load_ca_certificates, std::move(message_handler), std::move(close_handler), {}, settings, gate); + } + + inline nmos::connection_activation_handler make_connection_events_websocket_activation_handler(nmos::events_ws_message_handler message_handler, nmos::events_ws_close_handler close_handler, const nmos::settings& settings, slog::base_gate& gate) + { + return make_connection_events_websocket_activation_handler({}, std::move(message_handler), std::move(close_handler), settings, gate); + } inline nmos::connection_activation_handler make_connection_events_websocket_activation_handler(nmos::events_ws_message_handler message_handler, const nmos::settings& settings, slog::base_gate& gate) { diff --git a/Development/nmos/connection_resources.cpp b/Development/nmos/connection_resources.cpp index 7dcc8765f..79d81a7e9 100644 --- a/Development/nmos/connection_resources.cpp +++ b/Development/nmos/connection_resources.cpp @@ -6,6 +6,7 @@ #include "nmos/api_utils.h" // for nmos::http_scheme #include "nmos/is05_versions.h" #include "nmos/is07_versions.h" +#include "nmos/media_type.h" // for nmos::media_types::application_sdp #include "nmos/resource.h" namespace nmos @@ -32,8 +33,8 @@ namespace nmos return redundant ? value_of({ value, value }) : value_of({ value }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/sender-response-schema.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver-response-schema.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/sender-response-schema.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver-response-schema.html web::json::value make_connection_resource_staging_core(bool redundant) { using web::json::value; @@ -50,7 +51,7 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver-transport-file.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver-transport-file.html web::json::value make_connection_receiver_staging_transport_file() { using web::json::value; @@ -76,8 +77,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/4.1.%20Behaviour%20-%20RTP%20Transport%20Type.md#sender-parameter-sets - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/constraints-schema-rtp.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.1._Behaviour_-_RTP_Transport_Type.html#sender-parameter-sets + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/constraints-schema-rtp.html web::json::value make_connection_rtp_sender_core_constraints() { using web::json::value; @@ -93,8 +94,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/4.1.%20Behaviour%20-%20RTP%20Transport%20Type.md#sender-parameter-sets - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/sender_transport_params_rtp.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.1._Behaviour_-_RTP_Transport_Type.html#sender-parameter-sets + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/sender_transport_params_rtp.html web::json::value make_connection_rtp_sender_staged_core_parameter_set() { using web::json::value; @@ -109,8 +110,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/4.1.%20Behaviour%20-%20RTP%20Transport%20Type.md#receiver-parameter-sets - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/constraints-schema-rtp.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.1._Behaviour_-_RTP_Transport_Type.html#receiver-parameter-sets + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/constraints-schema-rtp.html web::json::value make_connection_rtp_receiver_core_constraints() { using web::json::value; @@ -126,8 +127,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/4.1.%20Behaviour%20-%20RTP%20Transport%20Type.md#receiver-parameter-sets - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver_transport_params_rtp.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.1._Behaviour_-_RTP_Transport_Type.html#receiver-parameter-sets + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver_transport_params_rtp.html web::json::value make_connection_rtp_receiver_staged_core_parameter_set() { using web::json::value; @@ -171,7 +172,7 @@ namespace nmos return value_of({ { nmos::fields::transportfile_data, transportfile }, - { nmos::fields::transportfile_type, U("application/sdp") } + { nmos::fields::transportfile_type, nmos::media_types::application_sdp.name } }); } @@ -243,15 +244,15 @@ namespace nmos return indeterminate(v) ? web::json::value::string(U("auto")) : web::json::value::boolean(bool(v)); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/constraints-schema-websocket.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/sender_transport_params_websocket.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/constraints-schema-websocket.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/sender_transport_params_websocket.html web::json::value make_connection_websocket_sender_core_constraints(const web::uri& connection_uri, boost::tribool connection_authorization) { using web::json::value; using web::json::value_of; // connection_uri for a sender is currently fixed, basically read-only - // see https://github.com/AMWA-TV/nmos-device-connection-management/issues/70 + // see https://github.com/AMWA-TV/is-05/issues/70 return value_of({ { nmos::fields::connection_uri, value_of({ { nmos::fields::constraint_enum, value_of({ @@ -262,8 +263,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/4.3.%20Behaviour%20-%20WebSocket%20Transport%20Type.md#sender-parameter-sets - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/sender_transport_params_websocket.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.3._Behaviour_-_WebSocket_Transport_Type.html#sender-parameter-sets + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/sender_transport_params_websocket.html web::json::value make_connection_websocket_sender_staged_core_parameter_set(const web::uri& connection_uri, boost::tribool connection_authorization) { using web::json::value; @@ -275,8 +276,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/constraints-schema-websocket.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver_transport_params_websocket.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/constraints-schema-websocket.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver_transport_params_websocket.html web::json::value make_connection_websocket_receiver_core_constraints(boost::tribool connection_authorization) { using web::json::value; @@ -289,8 +290,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/4.3.%20Behaviour%20-%20WebSocket%20Transport%20Type.md#receiver-parameter-sets - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver_transport_params_websocket.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.3._Behaviour_-_WebSocket_Transport_Type.html#receiver-parameter-sets + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver_transport_params_websocket.html web::json::value make_connection_websocket_receiver_staged_core_parameter_set(boost::tribool connection_authorization) { using web::json::value; @@ -302,8 +303,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/constraints-schema-mqtt.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/sender_transport_params_mqtt.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/constraints-schema-mqtt.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/sender_transport_params_mqtt.html web::json::value make_connection_mqtt_sender_core_constraints(boost::tribool broker_secure, boost::tribool broker_authorization, const utility::string_t& broker_topic, const utility::string_t& connection_status_broker_topic) { using web::json::value; @@ -332,8 +333,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/4.2.%20Behaviour%20-%20MQTT%20Transport%20Type.md#sender-parameter-sets - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/sender_transport_params_mqtt.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.2._Behaviour_-_MQTT_Transport_Type.html#sender-parameter-sets + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/sender_transport_params_mqtt.html web::json::value make_connection_mqtt_sender_staged_core_parameter_set(boost::tribool broker_secure, boost::tribool broker_authorization, const utility::string_t& broker_topic, const utility::string_t& connection_status_broker_topic) { using web::json::value; @@ -349,8 +350,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/constraints-schema-mqtt.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver_transport_params_mqtt.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/constraints-schema-mqtt.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver_transport_params_mqtt.html web::json::value make_connection_mqtt_receiver_core_constraints(boost::tribool broker_secure, boost::tribool broker_authorization) { using web::json::value; @@ -371,8 +372,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/docs/4.2.%20Behaviour%20-%20MQTT%20Transport%20Type.md#receiver-parameter-sets - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver_transport_params_mqtt.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.2._Behaviour_-_MQTT_Transport_Type.html#receiver-parameter-sets + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver_transport_params_mqtt.html web::json::value make_connection_mqtt_receiver_staged_core_parameter_set(boost::tribool broker_secure, boost::tribool broker_authorization) { using web::json::value; @@ -388,8 +389,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md#3-connection-management - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/sender_transport_params_ext.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#3-connection-management + // and https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/sender_transport_params_ext.html web::json::value make_connection_events_websocket_sender_ext_constraints(const nmos::id& source_id, const web::uri& rest_api_url) { using web::json::value; @@ -409,8 +410,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md#3-connection-management - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/sender_transport_params_ext.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#3-connection-management + // and https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/sender_transport_params_ext.html web::json::value make_connection_events_websocket_sender_staged_ext_parameter_set(const nmos::id& source_id, const web::uri& rest_api_url) { using web::json::value; @@ -422,8 +423,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md#3-connection-management - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/receiver_transport_params_ext.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#3-connection-management + // and https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/receiver_transport_params_ext.html web::json::value make_connection_events_websocket_receiver_ext_constraints() { using web::json::value; @@ -436,8 +437,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md#3-connection-management - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/receiver_transport_params_ext.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#3-connection-management + // and https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/receiver_transport_params_ext.html web::json::value make_connection_events_websocket_receiver_staged_ext_parameter_set() { using web::json::value; @@ -449,8 +450,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.1.%20Transport%20-%20MQTT.md#3-connection-management - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/sender_transport_params_ext.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.1._Transport_-_MQTT.html#3-connection-management + // and https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/sender_transport_params_ext.html web::json::value make_connection_events_mqtt_sender_ext_constraints(const web::uri& rest_api_url) { using web::json::value; @@ -465,8 +466,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.1.%20Transport%20-%20MQTT.md#3-connection-management - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/sender_transport_params_ext.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.1._Transport_-_MQTT.html#3-connection-management + // and https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/sender_transport_params_ext.html web::json::value make_connection_events_mqtt_sender_staged_ext_parameter_set(const web::uri& rest_api_url) { using web::json::value; @@ -477,8 +478,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.1.%20Transport%20-%20MQTT.md#3-connection-management - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/receiver_transport_params_ext.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.1._Transport_-_MQTT.html#3-connection-management + // and https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/receiver_transport_params_ext.html web::json::value make_connection_events_mqtt_receiver_ext_constraints() { using web::json::value; @@ -490,8 +491,8 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.1.%20Transport%20-%20MQTT.md#3-connection-management - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/receiver_transport_params_ext.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.1._Transport_-_MQTT.html#3-connection-management + // and https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/receiver_transport_params_ext.html web::json::value make_connection_events_mqtt_receiver_staged_ext_parameter_set() { using web::json::value; @@ -505,7 +506,7 @@ namespace nmos // Although these functions make "connection" (IS-05) resources, the details are defined by IS-07 Event & Tally // so maybe these belong in nmos/events_resources.h or their own file, e.g. nmos/connection_events_resources.h? - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md#3-connection-management + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#3-connection-management nmos::resource make_connection_events_websocket_sender(const nmos::id& id, const nmos::id& device_id, const nmos::id& source_id, const nmos::settings& settings) { using web::json::value; @@ -583,7 +584,7 @@ namespace nmos // "The sender should append the relative path sources/{source_id}/" // I'd rather be consistent with the general guidance regarding trailing slashes - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/docs/4.0.%20Core%20models.md#ext_is_07_rest_api_url + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/4.0._Core_models.html#ext_is_07_rest_api_url return web::uri_builder() .set_scheme(nmos::http_scheme(settings)) .set_host(nmos::get_host(settings)) @@ -598,7 +599,7 @@ namespace nmos // "To facilitate filtering, the recommended format is x-nmos/events/{version}/sources/{sourceId}, // where {version} is the version of this specification, e.g. v1.0, and {sourceId} is the associated source id." - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/docs/5.1.%20Transport%20-%20MQTT.md#32-broker_topic + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.1._Transport_-_MQTT.html#32-broker_topic return U("x-nmos/events/") + make_api_version(version) + U("/sources/") + source_id; } @@ -608,7 +609,7 @@ namespace nmos // "The connection_status_broker_topic parameter holds the sender's MQTT connection status topic. // The recommended format is x-nmos/events/{version}/connections/{connectionId}." - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/docs/5.1.%20Transport%20-%20MQTT.md#33-connection_status_broker_topic + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.1._Transport_-_MQTT.html#33-connection_status_broker_topic return U("x-nmos/events/") + make_api_version(version) + U("/connections/") + connection_id; } } diff --git a/Development/nmos/connection_resources.h b/Development/nmos/connection_resources.h index 54f7ada9a..adb035d34 100644 --- a/Development/nmos/connection_resources.h +++ b/Development/nmos/connection_resources.h @@ -20,7 +20,7 @@ namespace nmos // IS-05 Connection API resources // "The UUIDs used to advertise Senders and Receivers in the Connection Management API must match // those used in a corresponding IS-04 implementation." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/docs/3.1.%20Interoperability%20-%20NMOS%20IS-04.md#sender--receiver-ids + // See https://specs.amwa.tv/is-05/releases/v1.0.0/docs/3.1._Interoperability_-_NMOS_IS-04.html#sender--receiver-ids // Whereas the data of the IS-04 resources corresponds to a particular Node API resource endpoint, // each IS-05 resource's data is a json object with an "id" field and a field for each Connection API // endpoint of that logical single resource @@ -29,9 +29,9 @@ namespace nmos // "staged" and "active" fields, which must each have a value conforming to the sender-response-schema or receiver-response-schema, // and for senders, also a "transportfile" field, the value of which must be an object, with either // "data" and "type" fields, or an "href" field - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/constraints-schema.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/sender-response-schema.json - // and https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1/APIs/schemas/receiver-response-schema.json + // See https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/constraints-schema.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/sender-response-schema.html + // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver-response-schema.html // The caller must resolve all instances of "auto" in the /active endpoint into the actual values that will be used! // See nmos::resolve_rtp_auto @@ -51,7 +51,7 @@ namespace nmos // Although these functions make "connection" (IS-05) resources, the details are defined by IS-07 Event & Tally // so maybe these belong in nmos/events_resources.h or their own file, e.g. nmos/connection_events_resources.h? - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md#3-connection-management + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#3-connection-management nmos::resource make_connection_events_websocket_sender(const nmos::id& id, const nmos::id& device_id, const nmos::id& source_id, const nmos::settings& settings); nmos::resource make_connection_events_websocket_receiver(const nmos::id& id, const nmos::settings& settings); diff --git a/Development/nmos/control_protocol_handlers.cpp b/Development/nmos/control_protocol_handlers.cpp new file mode 100644 index 000000000..522bcc964 --- /dev/null +++ b/Development/nmos/control_protocol_handlers.cpp @@ -0,0 +1,103 @@ +#include "nmos/control_protocol_handlers.h" + +#include "nmos/control_protocol_resource.h" +#include "nmos/control_protocol_state.h" +#include "nmos/control_protocol_utils.h" +#include "nmos/slog.h" + +namespace nmos +{ + get_control_protocol_class_descriptor_handler make_get_control_protocol_class_descriptor_handler(nmos::experimental::control_protocol_state& control_protocol_state) + { + return [&](const nc_class_id& class_id) + { + auto lock = control_protocol_state.read_lock(); + + auto& control_class_descriptors = control_protocol_state.control_class_descriptors; + auto found = control_class_descriptors.find(class_id); + if (control_class_descriptors.end() != found) + { + return found->second; + } + return nmos::experimental::control_class_descriptor{}; + }; + } + + get_control_protocol_datatype_descriptor_handler make_get_control_protocol_datatype_descriptor_handler(nmos::experimental::control_protocol_state& control_protocol_state) + { + return [&](const nmos::nc_name& name) + { + auto lock = control_protocol_state.read_lock(); + + auto found = control_protocol_state.datatype_descriptors.find(name); + if (control_protocol_state.datatype_descriptors.end() != found) + { + return found->second; + } + return nmos::experimental::datatype_descriptor{}; + }; + } + + get_control_protocol_method_descriptor_handler make_get_control_protocol_method_descriptor_handler(experimental::control_protocol_state& control_protocol_state) + { + return [&](const nc_class_id& class_id_, const nc_method_id& method_id) + { + auto class_id = class_id_; + + auto get_control_protocol_class_descriptor = make_get_control_protocol_class_descriptor_handler(control_protocol_state); + + auto lock = control_protocol_state.read_lock(); + + while (!class_id.empty()) + { + const auto& control_class_descriptor = get_control_protocol_class_descriptor(class_id); + + auto& method_descriptors = control_class_descriptor.method_descriptors; + auto found = std::find_if(method_descriptors.begin(), method_descriptors.end(), [&method_id](const experimental::method& method) + { + return method_id == details::parse_nc_method_id(nmos::fields::nc::id(std::get<0>(method))); + }); + if (method_descriptors.end() != found) + { + return *found; + } + + class_id.pop_back(); + } + + return experimental::method(); + }; + } + + // Example Receiver-Monitor Connection activation callback to perform application-specific operations to complete activation + control_protocol_connection_activation_handler make_receiver_monitor_connection_activation_handler(resources& resources) + { + return [&resources](const resource& connection_resource) + { + auto found = find_control_protocol_resource(resources, nmos::types::nc_receiver_monitor, connection_resource.id); + if (resources.end() != found && nc_receiver_monitor_class_id == details::parse_nc_class_id(nmos::fields::nc::class_id(found->data))) + { + // update receiver-monitor's connectionStatus and payloadStatus properties + + const auto active = nmos::fields::master_enable(nmos::fields::endpoint_active(connection_resource.data)); + const web::json::value connection_status = active ? nc_connection_status::connected : nc_connection_status::disconnected; + const web::json::value payload_status = active ? nc_payload_status::payload_ok : nc_payload_status::undefined; + + // hmm, maybe updating connectionStatusMessage and payloadStatusMessage too + + const auto property_changed_event = make_property_changed_event(nmos::fields::nc::oid(found->data), + { + { nc_receiver_monitor_connection_status_property_id, nc_property_change_type::type::value_changed, connection_status }, + { nc_receiver_monitor_payload_status_property_id, nc_property_change_type::type::value_changed, payload_status } + }); + + modify_control_protocol_resource(resources, found->id, [&](nmos::resource& resource) + { + resource.data[nmos::fields::nc::connection_status] = connection_status; + resource.data[nmos::fields::nc::payload_status] = payload_status; + + }, property_changed_event); + } + }; + } +} diff --git a/Development/nmos/control_protocol_handlers.h b/Development/nmos/control_protocol_handlers.h new file mode 100644 index 000000000..0dcd84787 --- /dev/null +++ b/Development/nmos/control_protocol_handlers.h @@ -0,0 +1,74 @@ +#ifndef NMOS_CONTROL_PROTOCOL_HANDLERS_H +#define NMOS_CONTROL_PROTOCOL_HANDLERS_H + +#include +#include "nmos/control_protocol_typedefs.h" +#include "nmos/resources.h" + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + namespace experimental + { + struct control_protocol_state; + struct control_class_descriptor; + struct datatype_descriptor; + } + + // callback to retrieve a specific control protocol class descriptor + // this callback should not throw exceptions + typedef std::function get_control_protocol_class_descriptor_handler; + + // callback to add user control protocol class descriptor + // this callback should not throw exceptions + typedef std::function add_control_protocol_class_descriptor_handler; + + // callback to retrieve a control protocol datatype descriptor + // this callback should not throw exceptions + typedef std::function get_control_protocol_datatype_descriptor_handler; + + // a control_protocol_property_changed_handler is a notification that the specified (IS-12) property has changed + // index is set to -1 for non-sequence property + // this callback should not throw exceptions, as the relevant property will already has been changed and those changes will not be rolled back + typedef std::function control_protocol_property_changed_handler; + + namespace experimental + { + // control method handler definition + typedef std::function control_protocol_method_handler; + + // method definition (NcMethodDescriptor vs method handler) + typedef std::pair method; + + inline method make_control_class_method(const web::json::value& nc_method_descriptor, control_protocol_method_handler method_handler) + { + return std::make_pair(nc_method_descriptor, method_handler); + } + } + + // callback to retrieve a specific method + // this callback should not throw exceptions + typedef std::function get_control_protocol_method_descriptor_handler; + + // construct callback to retrieve a specific control protocol class descriptor + get_control_protocol_class_descriptor_handler make_get_control_protocol_class_descriptor_handler(experimental::control_protocol_state& control_protocol_state); + + // construct callback to retrieve a specific datatype descriptor + get_control_protocol_datatype_descriptor_handler make_get_control_protocol_datatype_descriptor_handler(experimental::control_protocol_state& control_protocol_state); + + // construct callback to retrieve a specific method + get_control_protocol_method_descriptor_handler make_get_control_protocol_method_descriptor_handler(experimental::control_protocol_state& control_protocol_state); + + // a control_protocol_connection_activation_handler is a notification that the active parameters for the specified (IS-05) sender/connection_sender or receiver/connection_receiver have changed + // this callback should not throw exceptions + typedef std::function control_protocol_connection_activation_handler; + + // construct callback for receiver monitor to process connection (de)activation + control_protocol_connection_activation_handler make_receiver_monitor_connection_activation_handler(nmos::resources& resources); +} + +#endif diff --git a/Development/nmos/control_protocol_methods.cpp b/Development/nmos/control_protocol_methods.cpp new file mode 100644 index 000000000..153d953b5 --- /dev/null +++ b/Development/nmos/control_protocol_methods.cpp @@ -0,0 +1,666 @@ +#include "nmos/control_protocol_methods.h" + +#include "cpprest/json_utils.h" +#include "nmos/control_protocol_resource.h" +#include "nmos/control_protocol_resources.h" +#include "nmos/control_protocol_state.h" +#include "nmos/control_protocol_utils.h" +#include "nmos/json_fields.h" +#include "nmos/slog.h" + +namespace nmos +{ + // NcObject methods implementation + // Get property value + web::json::value get(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + const auto& property_id = nmos::fields::nc::id(arguments); + + slog::log(gate, SLOG_FLF) << "Get property: " << property_id.serialize(); + + // find the relevant nc_property_descriptor + const auto& property = find_property_descriptor(details::parse_nc_property_id(property_id), details::parse_nc_class_id(nmos::fields::nc::class_id(resource.data)), get_control_protocol_class_descriptor); + if (!property.is_null()) + { + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nmos::fields::nc::is_deprecated(property) ? nc_method_status::property_deprecated : nc_method_status::ok }, resource.data.at(nmos::fields::nc::name(property))); + } + + // unknown property + utility::ostringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << U(" to do Get"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::property_not_implemented }, ss.str()); + } + + // Set property value + web::json::value set(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, control_protocol_property_changed_handler property_changed, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + const auto& property_id = nmos::fields::nc::id(arguments); + const auto& val = nmos::fields::nc::value(arguments); + + slog::log(gate, SLOG_FLF) << "Set property: " << property_id.serialize() << " value: " << val.serialize(); + + // find the relevant nc_property_descriptor + const auto property_id_ = details::parse_nc_property_id(property_id); + const auto& property = find_property_descriptor(property_id_, details::parse_nc_class_id(nmos::fields::nc::class_id(resource.data)), get_control_protocol_class_descriptor); + if (!property.is_null()) + { + if (nmos::fields::nc::is_read_only(property)) + { + utility::ostringstream_t ss; + ss << U("can not set read only property: ") << property_id.serialize(); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::read_only }, ss.str()); + } + + if ((val.is_null() && !nmos::fields::nc::is_nullable(property)) + || (!val.is_array() && nmos::fields::nc::is_sequence(property)) + || (val.is_array() && !nmos::fields::nc::is_sequence(property))) + { + utility::ostringstream_t ss; + ss << U("parameter error: can not set value: ") << val.serialize() << U(" on property: ") << property_id.serialize(); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, ss.str()); + } + + try + { + // do property constraints validation + nmos::details::constraints_validation(val, details::get_runtime_property_constraints(property_id_, resource.data.at(nmos::fields::nc::runtime_property_constraints)), nmos::fields::nc::constraints(property), { details::get_datatype_descriptor(property.at(nmos::fields::nc::type_name), get_control_protocol_datatype_descriptor), get_control_protocol_datatype_descriptor }); + + // update property + modify_control_protocol_resource(resources, resource.id, [&](nmos::resource& resource) + { + resource.data[nmos::fields::nc::name(property)] = val; + + // do notification that the specified property has changed + if (property_changed) + { + property_changed(resource, nmos::fields::nc::name(property), -1); + } + + }, make_property_changed_event(nmos::fields::nc::oid(resource.data), { { property_id_, nc_property_change_type::type::value_changed, val } })); + + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nmos::fields::nc::is_deprecated(property) ? nc_method_status::property_deprecated : nc_method_status::ok }); + } + catch (const nmos::control_protocol_exception& e) + { + utility::ostringstream_t ss; + ss << "Set property: " << property_id.serialize() << " value: " << val.serialize() << " error: " << e.what(); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, ss.str()); + } + } + + // unknown property + utility::ostringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << " to do Set"; + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::property_not_implemented }, ss.str()); + } + + // Get sequence item + web::json::value get_sequence_item(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + const auto& property_id = nmos::fields::nc::id(arguments); + const auto& index = nmos::fields::nc::index(arguments); + + slog::log(gate, SLOG_FLF) << "Get sequence item: " << property_id.serialize() << " index: " << index; + + // find the relevant nc_property_descriptor + const auto& property = find_property_descriptor(details::parse_nc_property_id(property_id), details::parse_nc_class_id(nmos::fields::nc::class_id(resource.data)), get_control_protocol_class_descriptor); + if (!property.is_null()) + { + const auto& data = resource.data.at(nmos::fields::nc::name(property)); + + if (!nmos::fields::nc::is_sequence(property) || data.is_null() || !data.is_array()) + { + // property is not a sequence + utility::ostringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is not a sequence to do GetSequenceItem"); + return details::make_nc_method_result_error({ nc_method_status::invalid_request }, ss.str()); + } + + if (data.as_array().size() > (size_t)index) + { + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nmos::fields::nc::is_deprecated(property) ? nc_method_status::property_deprecated : nc_method_status::ok }, data.at(index)); + } + + // out of bound + utility::ostringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is outside the available range to do GetSequenceItem"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::index_out_of_bounds }, ss.str()); + } + + // unknown property + utility::ostringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << U(" to do GetSequenceItem"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::property_not_implemented }, ss.str()); + } + + // Set sequence item + web::json::value set_sequence_item(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, control_protocol_property_changed_handler property_changed, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + const auto& property_id = nmos::fields::nc::id(arguments); + const auto& index = nmos::fields::nc::index(arguments); + const auto& val = nmos::fields::nc::value(arguments); + + slog::log(gate, SLOG_FLF) << "Set sequence item: " << property_id.serialize() << " index: " << index << " value: " << val.serialize(); + + // find the relevant nc_property_descriptor + const auto property_id_ = details::parse_nc_property_id(property_id); + const auto& property = find_property_descriptor(property_id_, details::parse_nc_class_id(nmos::fields::nc::class_id(resource.data)), get_control_protocol_class_descriptor); + if (!property.is_null()) + { + if (nmos::fields::nc::is_read_only(property)) + { + return details::make_nc_method_result({ nc_method_status::read_only }); + } + + auto& data = resource.data.at(nmos::fields::nc::name(property)); + + if (!nmos::fields::nc::is_sequence(property) || data.is_null() || !data.is_array()) + { + // property is not a sequence + utility::ostringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is not a sequence to do SetSequenceItem"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::invalid_request }, ss.str()); + } + + if (data.as_array().size() > (size_t)index) + { + try + { + // do property constraints validation + nmos::details::constraints_validation(val, details::get_runtime_property_constraints(property_id_, resource.data.at(nmos::fields::nc::runtime_property_constraints)), nmos::fields::nc::constraints(property), { details::get_datatype_descriptor(property.at(nmos::fields::nc::type_name), get_control_protocol_datatype_descriptor), get_control_protocol_datatype_descriptor }); + + // update property + modify_control_protocol_resource(resources, resource.id, [&](nmos::resource& resource) + { + resource.data[nmos::fields::nc::name(property)][index] = val; + + // do notification that the specified property has changed + if (property_changed) + { + property_changed(resource, nmos::fields::nc::name(property), index); + } + + }, make_property_changed_event(nmos::fields::nc::oid(resource.data), { { property_id_, nc_property_change_type::type::sequence_item_changed, val, nc_id(index) } })); + + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nmos::fields::nc::is_deprecated(property) ? nc_method_status::property_deprecated : nc_method_status::ok }); + } + catch (const nmos::control_protocol_exception& e) + { + utility::ostringstream_t ss; + ss << "Set sequence item: " << property_id.serialize() << " index: " << index << " value: " << val.serialize() << " error: " << e.what(); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, ss.str()); + } + } + + // out of bound + utility::ostringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is outside the available range to do SetSequenceItem"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::index_out_of_bounds }, ss.str()); + } + + // unknown property + utility::ostringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << U(" to do SetSequenceItem"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::property_not_implemented }, ss.str()); + } + + // Add item to sequence + web::json::value add_sequence_item(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, control_protocol_property_changed_handler property_changed, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + using web::json::value; + + const auto& property_id = nmos::fields::nc::id(arguments); + const auto& val = nmos::fields::nc::value(arguments); + + slog::log(gate, SLOG_FLF) << "Add sequence item: " << property_id.serialize() << " value: " << val.serialize(); + + // find the relevant nc_property_descriptor + const auto property_id_ = details::parse_nc_property_id(property_id); + const auto& property = find_property_descriptor(property_id_, details::parse_nc_class_id(nmos::fields::nc::class_id(resource.data)), get_control_protocol_class_descriptor); + if (!property.is_null()) + { + if (nmos::fields::nc::is_read_only(property)) + { + return details::make_nc_method_result({ nc_method_status::read_only }); + } + + if (!nmos::fields::nc::is_sequence(property)) + { + // property is not a sequence + utility::ostringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is not a sequence to do AddSequenceItem"); + return details::make_nc_method_result_error({ nc_method_status::invalid_request }, ss.str()); + } + + auto& data = resource.data.at(nmos::fields::nc::name(property)); + + const nc_id sequence_item_index = data.is_null() ? 0 : nc_id(data.as_array().size()); + + try + { + // do property constraints validation + nmos::details::constraints_validation(val, details::get_runtime_property_constraints(property_id_, resource.data.at(nmos::fields::nc::runtime_property_constraints)), nmos::fields::nc::constraints(property), { details::get_datatype_descriptor(property.at(nmos::fields::nc::type_name), get_control_protocol_datatype_descriptor), get_control_protocol_datatype_descriptor }); + + // update property + modify_control_protocol_resource(resources, resource.id, [&](nmos::resource& resource) + { + auto& sequence = resource.data[nmos::fields::nc::name(property)]; + if (data.is_null()) { sequence = value::array(); } + web::json::push_back(sequence, val); + + // do notification that the specified property has changed + if (property_changed) + { + property_changed(resource, nmos::fields::nc::name(property), (int)sequence.as_array().size()-1); + } + + }, make_property_changed_event(nmos::fields::nc::oid(resource.data), { { property_id_, nc_property_change_type::type::sequence_item_added, val, sequence_item_index } })); + + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nmos::fields::nc::is_deprecated(property) ? nc_method_status::property_deprecated : nc_method_status::ok }, sequence_item_index); + } + catch (const nmos::control_protocol_exception& e) + { + utility::ostringstream_t ss; + ss << "Add sequence item: " << property_id.serialize() << " value: " << val.serialize() << " error: " << e.what(); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, ss.str()); + } + } + + // unknown property + utility::ostringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << U(" to do AddSequenceItem"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::property_not_implemented }, ss.str()); + } + + // Delete sequence item + web::json::value remove_sequence_item(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, control_protocol_property_changed_handler property_changed, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + const auto& property_id = nmos::fields::nc::id(arguments); + const auto& index = nmos::fields::nc::index(arguments); + + slog::log(gate, SLOG_FLF) << "Remove sequence item: " << property_id.serialize() << " index: " << index; + + // find the relevant nc_property_descriptor + const auto& property = find_property_descriptor(details::parse_nc_property_id(property_id), details::parse_nc_class_id(nmos::fields::nc::class_id(resource.data)), get_control_protocol_class_descriptor); + if (!property.is_null()) + { + if (nmos::fields::nc::is_read_only(property)) + { + return details::make_nc_method_result({ nc_method_status::read_only }); + } + + const auto& data = resource.data.at(nmos::fields::nc::name(property)); + + if (!nmos::fields::nc::is_sequence(property) || data.is_null() || !data.is_array()) + { + // property is not a sequence + utility::ostringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is not a sequence to do RemoveSequenceItem"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::invalid_request }, ss.str()); + } + + if (data.as_array().size() > (size_t)index) + { + modify_control_protocol_resource(resources, resource.id, [&](nmos::resource& resource) + { + auto& sequence = resource.data[nmos::fields::nc::name(property)].as_array(); + sequence.erase(index); + + // do notification that the specified property has changed + if (property_changed) + { + property_changed(resource, nmos::fields::nc::name(property), index); + } + + }, make_property_changed_event(nmos::fields::nc::oid(resource.data), { { details::parse_nc_property_id(property_id), nc_property_change_type::type::sequence_item_removed, nc_id(index) } })); + + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nmos::fields::nc::is_deprecated(property) ? nc_method_status::property_deprecated : nc_method_status::ok }); + } + + // out of bound + utility::ostringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is outside the available range to do RemoveSequenceItem"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::index_out_of_bounds }, ss.str()); + } + + // unknown property + utility::ostringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << U(" to do RemoveSequenceItem"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::property_not_implemented }, ss.str()); + } + + // Get sequence length + web::json::value get_sequence_length(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + using web::json::value; + + const auto& property_id = nmos::fields::nc::id(arguments); + + slog::log(gate, SLOG_FLF) << "Get sequence length: " << property_id.serialize(); + + // find the relevant nc_property_descriptor + const auto& property = find_property_descriptor(details::parse_nc_property_id(property_id), details::parse_nc_class_id(nmos::fields::nc::class_id(resource.data)), get_control_protocol_class_descriptor); + if (!property.is_null()) + { + if (!nmos::fields::nc::is_sequence(property)) + { + // property is not a sequence + utility::ostringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is not a sequence to do GetSequenceLength"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::invalid_request }, ss.str()); + } + + const auto& data = resource.data.at(nmos::fields::nc::name(property)); + + if (nmos::fields::nc::is_nullable(property)) + { + // can be null + if (data.is_null()) + { + // null + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nmos::fields::nc::is_deprecated(property) ? nc_method_status::property_deprecated : nc_method_status::ok }, value::null()); + } + } + else + { + // cannot be null + if (data.is_null()) + { + // null + utility::ostringstream_t ss; + ss << U("property: ") << property_id.serialize() << " is a null sequence to do GetSequenceLength"; + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::invalid_request }, ss.str()); + } + } + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nmos::fields::nc::is_deprecated(property) ? nc_method_status::property_deprecated : nc_method_status::ok }, value(uint32_t(data.as_array().size()))); + } + + // unknown property + utility::ostringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << " to do GetSequenceLength"; + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::property_not_implemented }, ss.str()); + } + + // NcBlock methods implementation + // Gets descriptors of members of the block + web::json::value get_member_descriptors(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + using web::json::value; + + const auto& recurse = nmos::fields::nc::recurse(arguments); // If recurse is set to true, nested members is to be retrieved + + slog::log(gate, SLOG_FLF) << "Get descriptors of members of the block: " << "recurse: " << recurse; + + auto descriptors = value::array(); + nmos::get_member_descriptors(resources, resource, recurse, descriptors.as_array()); + + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nc_method_status::ok }, descriptors); + } + + // Finds member(s) by path + web::json::value find_members_by_path(nmos::resources& resources, const nmos::resource& resource_, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + using web::json::value; + + // Relative path to search for (MUST not include the role of the block targeted by oid) + const auto& path = arguments.at(nmos::fields::nc::path); + + slog::log(gate, SLOG_FLF) << "Find member(s) by path: " << "path: " << path.serialize(); + + if (0 == path.size()) + { + // empty path + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, U("empty path to do FindMembersByPath")); + } + + auto descriptors = value::array(); + value descriptor; + + nmos::resource resource = resource_; + for (const auto& role : path.as_array()) + { + // look for the role in members + + if (resource.data.has_field(nmos::fields::nc::members)) + { + auto& members = nmos::fields::nc::members(resource.data); + auto member_found = std::find_if(members.begin(), members.end(), [&](const web::json::value& nc_block_member_descriptor) + { + return role.as_string() == nmos::fields::nc::role(nc_block_member_descriptor); + }); + + if (members.end() != member_found) + { + descriptor = *member_found; + + // use oid to look for the next resource + resource = *nmos::find_resource(resources, utility::s2us(std::to_string(nmos::fields::nc::oid(*member_found)))); + } + else + { + // no role + utility::ostringstream_t ss; + ss << U("role: ") << role.as_string() << U(" not found to do FindMembersByPath"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, ss.str()); + } + } + else + { + // no members + utility::ostringstream_t ss; + ss << U("role: ") << role.as_string() << U(" has no members to do FindMembersByPath"); + slog::log(gate, SLOG_FLF) << ss.str(); + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, ss.str()); + } + } + + web::json::push_back(descriptors, descriptor); + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nc_method_status::ok }, descriptors); + } + + // Finds members with given role name or fragment + web::json::value find_members_by_role(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + using web::json::value; + + const auto& role = nmos::fields::nc::role(arguments); // Role text to search for + const auto& case_sensitive = nmos::fields::nc::case_sensitive(arguments); // Signals if the comparison should be case sensitive + const auto& match_whole_string = nmos::fields::nc::match_whole_string(arguments); // TRUE to only return exact matches + const auto& recurse = nmos::fields::nc::recurse(arguments); // TRUE to search nested blocks + + slog::log(gate, SLOG_FLF) << "Find members with given role name or fragment: " << "role: " << role; + + if (role.empty()) + { + // empty role + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, U("empty role to do FindMembersByRole")); + } + + auto descriptors = value::array(); + nmos::find_members_by_role(resources, resource, role, match_whole_string, case_sensitive, recurse, descriptors.as_array()); + + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nc_method_status::ok }, descriptors); + } + + // Finds members with given class id + web::json::value find_members_by_class_id(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + using web::json::value; + + const auto& class_id = details::parse_nc_class_id(nmos::fields::nc::class_id(arguments)); // Class id to search for + const auto& include_derived = nmos::fields::nc::include_derived(arguments); // If TRUE it will also include derived class descriptors + const auto& recurse = nmos::fields::nc::recurse(arguments); // TRUE to search nested blocks + + slog::log(gate, SLOG_FLF) << "Find members with given class id: " << "class_id: " << nmos::details::make_nc_class_id(class_id).serialize(); + + if (class_id.empty()) + { + // empty class_id + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, U("empty classId to do FindMembersByClassId")); + } + + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + auto descriptors = value::array(); + nmos::find_members_by_class_id(resources, resource, class_id, include_derived, recurse, descriptors.as_array()); + + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nc_method_status::ok }, descriptors); + } + + // NcClassManager methods implementation + // Get a single class descriptor + web::json::value get_control_class(nmos::resources&, const nmos::resource&, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, slog::base_gate& gate) + { + using web::json::value; + + const auto& class_id = details::parse_nc_class_id(nmos::fields::nc::class_id(arguments)); // Class id to search for + const auto& include_inherited = nmos::fields::nc::include_inherited(arguments); // If set the descriptor would contain all inherited elements + + slog::log(gate, SLOG_FLF) << "Get a single class descriptor: " << "class_id: " << nmos::details::make_nc_class_id(class_id).serialize(); + + if (class_id.empty()) + { + // empty class_id + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, U("empty classId to do GetControlClass")); + } + + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + const auto& control_class = get_control_protocol_class_descriptor(class_id); + if (!control_class.class_id.empty()) + { + auto& description = control_class.description; + auto& name = control_class.name; + auto& fixed_role = control_class.fixed_role; + auto property_descriptors = control_class.property_descriptors; + auto method_descriptors = value::array(); + for (const auto& method_descriptor : control_class.method_descriptors) { web::json::push_back(method_descriptors, std::get<0>(method_descriptor)); } + auto event_descriptors = control_class.event_descriptors; + + if (include_inherited) + { + auto inherited_class_id = class_id; + inherited_class_id.pop_back(); + + while (!inherited_class_id.empty()) + { + const auto& inherited_control_class = get_control_protocol_class_descriptor(inherited_class_id); + { + for (const auto& property_descriptor : inherited_control_class.property_descriptors.as_array()) { web::json::push_back(property_descriptors, property_descriptor); } + for (const auto& method_descriptor : inherited_control_class.method_descriptors) { web::json::push_back(method_descriptors, std::get<0>(method_descriptor)); } + for (const auto& event_descriptor : inherited_control_class.event_descriptors.as_array()) { web::json::push_back(event_descriptors, event_descriptor); } + } + inherited_class_id.pop_back(); + } + } + const auto descriptor = fixed_role.is_null() + ? details::make_nc_class_descriptor(description, class_id, name, property_descriptors, method_descriptors, event_descriptors) + : details::make_nc_class_descriptor(description, class_id, name, fixed_role.as_string(), property_descriptors, method_descriptors, event_descriptors); + + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nc_method_status::ok }, descriptor); + } + + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, U("classId not found")); + } + + // Get a single datatype descriptor + web::json::value get_datatype(nmos::resources&, const nmos::resource&, const web::json::value& arguments, bool is_deprecated, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, slog::base_gate& gate) + { + // note, model mutex is already locked by the outer function, so access to control_protocol_resources is OK... + + const auto& name = nmos::fields::nc::name(arguments); // name of datatype + const auto& include_inherited = nmos::fields::nc::include_inherited(arguments); // If set the descriptor would contain all inherited elements + + slog::log(gate, SLOG_FLF) << "Get a single datatype descriptor: " << "name: " << name; + + if (name.empty()) + { + // empty name + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, U("empty name to do GetDatatype")); + } + + const auto& datatype = get_control_protocol_datatype_descriptor(name); + if (datatype.descriptor.size()) + { + auto descriptor = datatype.descriptor; + + if (include_inherited) + { + const auto& type = nmos::fields::nc::type(descriptor); + if (nc_datatype_type::Struct == type) + { + auto descriptor_ = descriptor; + + for (;;) + { + const auto& parent_type = descriptor_.at(nmos::fields::nc::parent_type); + if (!parent_type.is_null()) + { + const auto& parent_datatype = get_control_protocol_datatype_descriptor(parent_type.as_string()); + if (parent_datatype.descriptor.size()) + { + descriptor_ = parent_datatype.descriptor; + + const auto& fields = nmos::fields::nc::fields(descriptor_); + for (const auto& field : fields) + { + web::json::push_back(descriptor.at(nmos::fields::nc::fields), field); + } + } + } + else + { + break; + } + } + } + } + + return details::make_nc_method_result({ is_deprecated ? nmos::nc_method_status::method_deprecated : nc_method_status::ok }, descriptor); + } + + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, U("name not found")); + } +} diff --git a/Development/nmos/control_protocol_methods.h b/Development/nmos/control_protocol_methods.h new file mode 100644 index 000000000..69f7b7c41 --- /dev/null +++ b/Development/nmos/control_protocol_methods.h @@ -0,0 +1,47 @@ +#ifndef NMOS_CONTROL_PROTOCOL_METHODS_H +#define NMOS_CONTROL_PROTOCOL_METHODS_H + +#include "nmos/control_protocol_handlers.h" +#include "nmos/resources.h" + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + // NcObject methods implementation + // Get property value + web::json::value get(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, slog::base_gate& gate); + // Set property value + web::json::value set(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, control_protocol_property_changed_handler property_changed, slog::base_gate& gate); + // Get sequence item + web::json::value get_sequence_item(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, slog::base_gate& gate); + // Set sequence item + web::json::value set_sequence_item(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor_handler, control_protocol_property_changed_handler property_changed, slog::base_gate& gate); + // Add item to sequence + web::json::value add_sequence_item(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor_handler, control_protocol_property_changed_handler property_changed, slog::base_gate& gate); + // Delete sequence item + web::json::value remove_sequence_item(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, control_protocol_property_changed_handler property_changed, slog::base_gate& gate); + // Get sequence length + web::json::value get_sequence_length(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, slog::base_gate& gate); + + // NcBlock methods implementation + // Get descriptors of members of the block + web::json::value get_member_descriptors(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate); + // Finds member(s) by path + web::json::value find_members_by_path(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate); + // Finds members with given role name or fragment + web::json::value find_members_by_role(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate); + // Finds members with given class id + web::json::value find_members_by_class_id(nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate); + + // NcClassManager methods implementation + // Get a single class descriptor + web::json::value get_control_class(nmos::resources&, const nmos::resource&, const web::json::value& arguments, bool is_deprecated, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, slog::base_gate& gate); + // Get a single datatype descriptor + web::json::value get_datatype(nmos::resources&, const nmos::resource&, const web::json::value& arguments, bool is_deprecated, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype, slog::base_gate& gate); +} + +#endif diff --git a/Development/nmos/control_protocol_nmos_channel_mapping_resource_type.h b/Development/nmos/control_protocol_nmos_channel_mapping_resource_type.h new file mode 100644 index 000000000..8e31c96cf --- /dev/null +++ b/Development/nmos/control_protocol_nmos_channel_mapping_resource_type.h @@ -0,0 +1,19 @@ +#ifndef NMOS_CONTROL_PROTOCOL_NMOS_CHANNEL_MAPPING_RESOURCE_TYPE_H +#define NMOS_CONTROL_PROTOCOL_NMOS_CHANNEL_MAPPING_RESOURCE_TYPE_H + +#include "cpprest/basic_utils.h" +#include "nmos/string_enum.h" + +namespace nmos +{ + // resourceType + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpointresourcenmoschannelmapping + DEFINE_STRING_ENUM(ncp_nmos_channel_mapping_resource_type) + namespace ncp_nmos_channel_mapping_resource_types + { + const ncp_nmos_channel_mapping_resource_type input{ U("input") }; + const ncp_nmos_channel_mapping_resource_type output{ U("output") }; + } +} + +#endif diff --git a/Development/nmos/control_protocol_nmos_resource_type.h b/Development/nmos/control_protocol_nmos_resource_type.h new file mode 100644 index 000000000..436b72257 --- /dev/null +++ b/Development/nmos/control_protocol_nmos_resource_type.h @@ -0,0 +1,23 @@ +#ifndef NMOS_CONTROL_PROTOCOL_NMOS_RESOURCE_TYPE_H +#define NMOS_CONTROL_PROTOCOL_NMOS_RESOURCE_TYPE_H + +#include "cpprest/basic_utils.h" +#include "nmos/string_enum.h" + +namespace nmos +{ + // resourceType + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpointresourcenmos + DEFINE_STRING_ENUM(ncp_nmos_resource_type) + namespace ncp_nmos_resource_types + { + const ncp_nmos_resource_type node{ U("node") }; + const ncp_nmos_resource_type device{ U("device") }; + const ncp_nmos_resource_type source{ U("source") }; + const ncp_nmos_resource_type flow{ U("flow") }; + const ncp_nmos_resource_type sender{ U("sender") }; + const ncp_nmos_resource_type receiver{ U("receiver") }; + } +} + +#endif diff --git a/Development/nmos/control_protocol_resource.cpp b/Development/nmos/control_protocol_resource.cpp new file mode 100644 index 000000000..83ccaad08 --- /dev/null +++ b/Development/nmos/control_protocol_resource.cpp @@ -0,0 +1,2052 @@ +#include "nmos/control_protocol_resource.h" + +#include "cpprest/base_uri.h" +#include "nmos/control_protocol_state.h" // for nmos::experimental::control_classes definitions +#include "nmos/json_fields.h" + +namespace nmos +{ + namespace details + { + web::json::value make_nc_method_result(const nc_method_result& method_result) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::status, method_result.status } + }); + } + + web::json::value make_nc_method_result_error(const nc_method_result& method_result, const utility::string_t& error_message) + { + auto result = make_nc_method_result(method_result); + if (!error_message.empty()) { result[nmos::fields::nc::error_message] = web::json::value::string(error_message); } + return result; + } + + web::json::value make_nc_method_result(const nc_method_result& method_result, const web::json::value& value) + { + auto result = make_nc_method_result(method_result); + result[nmos::fields::nc::value] = value; + return result; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncelementid + web::json::value make_nc_element_id(uint16_t level, uint16_t index) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::level, level }, + { nmos::fields::nc::index, index } + }); + } + web::json::value make_nc_element_id(const nc_element_id& id) + { + return make_nc_element_id(id.level, id.index); + } + nc_element_id parse_nc_element_id(const web::json::value& id) + { + return { uint16_t(nmos::fields::nc::level(id)), uint16_t(nmos::fields::nc::index(id)) }; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nceventid + web::json::value make_nc_event_id(const nc_event_id& id) + { + return make_nc_element_id(id); + } + nc_event_id parse_nc_event_id(const web::json::value& id) + { + return parse_nc_element_id(id); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmethodid + web::json::value make_nc_method_id(const nc_method_id& id) + { + return make_nc_element_id(id); + } + nc_method_id parse_nc_method_id(const web::json::value& id) + { + return parse_nc_element_id(id); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertyid + web::json::value make_nc_property_id(const nc_property_id& id) + { + return make_nc_element_id(id); + } + nc_property_id parse_nc_property_id(const web::json::value& id) + { + return parse_nc_element_id(id); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassid + web::json::value make_nc_class_id(const nc_class_id& class_id) + { + using web::json::value; + + auto nc_class_id = value::array(); + for (const auto class_id_item : class_id) { web::json::push_back(nc_class_id, class_id_item); } + return nc_class_id; + } + nc_class_id parse_nc_class_id(const web::json::array& class_id_) + { + nc_class_id class_id; + for (auto& element : class_id_) + { + class_id.push_back(element.as_integer()); + } + return class_id; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmanufacturer + web::json::value make_nc_manufacturer(const utility::string_t& name, const web::json::value& organization_id, const web::json::value& website) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::name, name }, + { nmos::fields::nc::organization_id, organization_id }, + { nmos::fields::nc::website, website } + }); + } + web::json::value make_nc_manufacturer(const utility::string_t& name, nc_organization_id organization_id, const web::uri& website) + { + using web::json::value; + + return make_nc_manufacturer(name, organization_id, value::string(website.to_string())); + } + web::json::value make_nc_manufacturer(const utility::string_t& name, nc_organization_id organization_id) + { + using web::json::value; + + return make_nc_manufacturer(name, organization_id, value::null()); + } + web::json::value make_nc_manufacturer(const utility::string_t& name) + { + using web::json::value; + + return make_nc_manufacturer(name, value::null(), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncproduct + // brand_name can be null + // uuid can be null + // description can be null + web::json::value make_nc_product(const utility::string_t& name, const utility::string_t& key, const utility::string_t& revision_level, + const web::json::value& brand_name, const web::json::value& uuid, const web::json::value& description) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::name, name }, + { nmos::fields::nc::key, key }, + { nmos::fields::nc::revision_level, revision_level }, + { nmos::fields::nc::brand_name, brand_name }, + { nmos::fields::nc::uuid, uuid }, + { nmos::fields::nc::description, description } + }); + } + web::json::value make_nc_product(const utility::string_t& name, const utility::string_t& key, const utility::string_t& revision_level, + const utility::string_t& brand_name, const nc_uuid& uuid, const utility::string_t& description) + { + using web::json::value; + + return make_nc_product(name, key, revision_level, value::string(brand_name), value::string(uuid), value::string(description)); + } + web::json::value make_nc_product(const utility::string_t& name, const utility::string_t& key, const utility::string_t& revision_level, + const utility::string_t& brand_name, const nc_uuid& uuid) + { + using web::json::value; + + return make_nc_product(name, key, revision_level, value::string(brand_name), value::string(uuid), value::null()); + } + web::json::value make_nc_product(const utility::string_t& name, const utility::string_t& key, const utility::string_t& revision_level, + const utility::string_t& brand_name) + { + using web::json::value; + + return make_nc_product(name, key, revision_level, value::string(brand_name), value::null(), value::null()); + } + web::json::value make_nc_product(const utility::string_t& name, const utility::string_t& key, const utility::string_t& revision_level) + { + using web::json::value; + + return make_nc_product(name, key, revision_level, value::null(), value::null(), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdeviceoperationalstate + // device_specific_details can be null + web::json::value make_nc_device_operational_state(nc_device_generic_state::state generic_state, const web::json::value& device_specific_details) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::generic_state, generic_state }, + { nmos::fields::nc::device_specific_details, device_specific_details } + }); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdescriptor + // description can be null + web::json::value make_nc_descriptor(const web::json::value& description) + { + using web::json::value_of; + + return value_of({ { nmos::fields::nc::description, description } }); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncblockmemberdescriptor + // description can be null + // user_label can be null + web::json::value make_nc_block_member_descriptor(const web::json::value& description, const utility::string_t& role, nc_oid oid, bool constant_oid, const nc_class_id& class_id, const web::json::value& user_label, nc_oid owner) + { + using web::json::value; + + auto data = make_nc_descriptor(description); + data[nmos::fields::nc::role] = value::string(role); + data[nmos::fields::nc::oid] = oid; + data[nmos::fields::nc::constant_oid] = value::boolean(constant_oid); + data[nmos::fields::nc::class_id] = make_nc_class_id(class_id); + data[nmos::fields::nc::user_label] = user_label; + data[nmos::fields::nc::owner] = owner; + + return data; + } + web::json::value make_nc_block_member_descriptor(const utility::string_t& description, const utility::string_t& role, nc_oid oid, bool constant_oid, const nc_class_id& class_id, const utility::string_t& user_label, nc_oid owner) + { + using web::json::value; + + return make_nc_block_member_descriptor(value::string(description), role, oid, constant_oid, class_id, value::string(user_label), owner); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassdescriptor + // description can be null + // fixedRole can be null + web::json::value make_nc_class_descriptor(const web::json::value& description, const nc_class_id& class_id, const nc_name& name, const web::json::value& fixed_role, const web::json::value& properties, const web::json::value& methods, const web::json::value& events) + { + using web::json::value; + + auto data = make_nc_descriptor(description); + data[nmos::fields::nc::class_id] = make_nc_class_id(class_id); + data[nmos::fields::nc::name] = value::string(name); + data[nmos::fields::nc::fixed_role] = fixed_role; + data[nmos::fields::nc::properties] = properties; + data[nmos::fields::nc::methods] = methods; + data[nmos::fields::nc::events] = events; + + return data; + } + web::json::value make_nc_class_descriptor(const utility::string_t& description, const nc_class_id& class_id, const nc_name& name, const utility::string_t& fixed_role, const web::json::value& properties, const web::json::value& methods, const web::json::value& events) + { + using web::json::value; + + return make_nc_class_descriptor(value::string(description), class_id, name, value::string(fixed_role), properties, methods, events); + } + web::json::value make_nc_class_descriptor(const utility::string_t& description, const nc_class_id& class_id, const nc_name& name, const web::json::value& properties, const web::json::value& methods, const web::json::value& events) + { + using web::json::value; + + return make_nc_class_descriptor(value::string(description), class_id, name, value::null(), properties, methods, events); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncenumitemdescriptor + // description can be null + web::json::value make_nc_enum_item_descriptor(const web::json::value& description, const nc_name& name, uint16_t val) + { + using web::json::value; + + auto data = make_nc_descriptor(description); + data[nmos::fields::nc::name] = value::string(name); + data[nmos::fields::nc::value] = val; + + return data; + } + web::json::value make_nc_enum_item_descriptor(const utility::string_t& description, const nc_name& name, uint16_t val) + { + using web::json::value; + + return make_nc_enum_item_descriptor(value::string(description), name, val); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nceventdescriptor + // description can be null + // id = make_nc_event_id(level, index) + web::json::value make_nc_event_descriptor(const web::json::value& description, const nc_event_id& id, const nc_name& name, const utility::string_t& event_datatype, bool is_deprecated) + { + using web::json::value; + + auto data = make_nc_descriptor(description); + data[nmos::fields::nc::id] = make_nc_event_id(id); + data[nmos::fields::nc::name] = value::string(name); + data[nmos::fields::nc::event_datatype] = value::string(event_datatype); + data[nmos::fields::nc::is_deprecated] = value::boolean(is_deprecated); + + return data; + } + web::json::value make_nc_event_descriptor(const utility::string_t& description, const nc_event_id& id, const nc_name& name, const utility::string_t& event_datatype, bool is_deprecated) + { + using web::json::value; + + return make_nc_event_descriptor(value::string(description), id, name, event_datatype, is_deprecated); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncfielddescriptor + // description can be null + // type_name can be null + // constraints can be null + web::json::value make_nc_field_descriptor(const web::json::value& description, const nc_name& name, const web::json::value& type_name, bool is_nullable, bool is_sequence, const web::json::value& constraints) + { + using web::json::value; + + auto data = make_nc_descriptor(description); + data[nmos::fields::nc::name] = value::string(name); + data[nmos::fields::nc::type_name] = type_name; + data[nmos::fields::nc::is_nullable] = value::boolean(is_nullable); + data[nmos::fields::nc::is_sequence] = value::boolean(is_sequence); + data[nmos::fields::nc::constraints] = constraints; + + return data; + } + web::json::value make_nc_field_descriptor(const utility::string_t& description, const nc_name& name, const utility::string_t& type_name, bool is_nullable, bool is_sequence, const web::json::value& constraints) + { + using web::json::value; + + return make_nc_field_descriptor(value::string(description), name, value::string(type_name), is_nullable, is_sequence, constraints); + } + web::json::value make_nc_field_descriptor(const utility::string_t& description, const nc_name& name, bool is_nullable, bool is_sequence, const web::json::value& constraints) + { + using web::json::value; + + return make_nc_field_descriptor(value::string(description), name, value::null(), is_nullable, is_sequence, constraints); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmethoddescriptor + // description can be null + // id = make_nc_method_id(level, index) + // sequence parameters + web::json::value make_nc_method_descriptor(const web::json::value& description, const nc_method_id& id, const nc_name& name, const utility::string_t& result_datatype, const web::json::value& parameters, bool is_deprecated) + { + using web::json::value; + + auto data = make_nc_descriptor(description); + data[nmos::fields::nc::id] = make_nc_method_id(id); + data[nmos::fields::nc::name] = value::string(name); + data[nmos::fields::nc::result_datatype] = value::string(result_datatype); + data[nmos::fields::nc::parameters] = parameters; + data[nmos::fields::nc::is_deprecated] = value::boolean(is_deprecated); + + return data; + } + web::json::value make_nc_method_descriptor(const utility::string_t& description, const nc_method_id& id, const nc_name& name, const utility::string_t& result_datatype, const web::json::value& parameters, bool is_deprecated) + { + using web::json::value; + + return make_nc_method_descriptor(value::string(description), id, name, result_datatype, parameters, is_deprecated); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncparameterdescriptor + // description can be null + // type_name can be null + web::json::value make_nc_parameter_descriptor(const web::json::value& description, const nc_name& name, const web::json::value& type_name, bool is_nullable, bool is_sequence, const web::json::value& constraints) + { + using web::json::value; + + auto data = make_nc_descriptor(description); + data[nmos::fields::nc::name] = value::string(name); + data[nmos::fields::nc::type_name] = type_name; + data[nmos::fields::nc::is_nullable] = value::boolean(is_nullable); + data[nmos::fields::nc::is_sequence] = value::boolean(is_sequence); + data[nmos::fields::nc::constraints] = constraints; + + return data; + } + web::json::value make_nc_parameter_descriptor(const utility::string_t& description, const nc_name& name, bool is_nullable, bool is_sequence, const web::json::value& constraints) + { + using web::json::value; + + return make_nc_parameter_descriptor(value::string(description), name, value::null(), is_nullable, is_sequence, constraints); + } + web::json::value make_nc_parameter_descriptor(const utility::string_t& description, const nc_name& name, const utility::string_t& type_name, bool is_nullable, bool is_sequence, const web::json::value& constraints) + { + using web::json::value; + + return make_nc_parameter_descriptor(value::string(description), name, value::string(type_name), is_nullable, is_sequence, constraints); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertydescriptor + // description can be null + // constraints can be null + web::json::value make_nc_property_descriptor(const web::json::value& description, const nc_property_id& id, const nc_name& name, const web::json::value& type_name, + bool is_read_only, bool is_nullable, bool is_sequence, bool is_deprecated, const web::json::value& constraints) + { + using web::json::value; + + auto data = make_nc_descriptor(description); + data[nmos::fields::nc::id] = make_nc_property_id(id); + data[nmos::fields::nc::name] = value::string(name); + data[nmos::fields::nc::type_name] = type_name; + data[nmos::fields::nc::is_read_only] = value::boolean(is_read_only); + data[nmos::fields::nc::is_nullable] = value::boolean(is_nullable); + data[nmos::fields::nc::is_sequence] = value::boolean(is_sequence); + data[nmos::fields::nc::is_deprecated] = value::boolean(is_deprecated); + data[nmos::fields::nc::constraints] = constraints; + + return data; + } + web::json::value make_nc_property_descriptor(const utility::string_t& description, const nc_property_id& id, const nc_name& name, const utility::string_t& type_name, + bool is_read_only, bool is_nullable, bool is_sequence, bool is_deprecated, const web::json::value& constraints) + { + using web::json::value; + + return nmos::details::make_nc_property_descriptor(value::string(description), id, name, value::string(type_name), is_read_only, is_nullable, is_sequence, is_deprecated, constraints); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdatatypedescriptor + // description can be null + // constraints can be null + web::json::value make_nc_datatype_descriptor(const web::json::value& description, const nc_name& name, nc_datatype_type::type type, const web::json::value& constraints) + { + using web::json::value; + + auto data = make_nc_descriptor(description); + data[nmos::fields::nc::name] = value::string(name); + data[nmos::fields::nc::type] = type; + data[nmos::fields::nc::constraints] = constraints; + + return data; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdatatypedescriptorenum + // description can be null + // constraints can be null + // items: sequence + web::json::value make_nc_datatype_descriptor_enum(const web::json::value& description, const nc_name& name, const web::json::value& items, const web::json::value& constraints) + { + auto data = make_nc_datatype_descriptor(description, name, nc_datatype_type::Enum, constraints); + data[nmos::fields::nc::items] = items; + + return data; + } + web::json::value make_nc_datatype_descriptor_enum(const utility::string_t& description, const nc_name& name, const web::json::value& items, const web::json::value& constraints) + { + using web::json::value; + + return make_nc_datatype_descriptor_enum(value::string(description), name, items, constraints); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdatatypedescriptorprimitive + // description can be null + // constraints can be null + web::json::value make_nc_datatype_descriptor_primitive(const web::json::value& description, const nc_name& name, const web::json::value& constraints) + { + return make_nc_datatype_descriptor(description, name, nc_datatype_type::Primitive, constraints); + } + web::json::value make_nc_datatype_descriptor_primitive(const utility::string_t& description, const nc_name& name, const web::json::value& constraints) + { + using web::json::value; + + return make_nc_datatype_descriptor_primitive(value::string(description), name, constraints); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdatatypedescriptorstruct + // description can be null + // constraints can be null + // fields: sequence + // parent_type can be null + web::json::value make_nc_datatype_descriptor_struct(const web::json::value& description, const nc_name& name, const web::json::value& fields, const web::json::value& parent_type, const web::json::value& constraints) + { + auto data = make_nc_datatype_descriptor(description, name, nc_datatype_type::Struct, constraints); + data[nmos::fields::nc::fields] = fields; + data[nmos::fields::nc::parent_type] = parent_type; + + return data; + } + web::json::value make_nc_datatype_descriptor_struct(const utility::string_t& description, const nc_name& name, const web::json::value& fields, const utility::string_t& parent_type, const web::json::value& constraints) + { + using web::json::value; + + return make_nc_datatype_descriptor_struct(value::string(description), name, fields, value::string(parent_type), constraints); + } + web::json::value make_nc_datatype_descriptor_struct(const utility::string_t& description, const nc_name& name, const web::json::value& fields, const web::json::value& constraints) + { + using web::json::value; + + return make_nc_datatype_descriptor_struct(value::string(description), name, fields, value::null(), constraints); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdatatypedescriptortypedef + // description can be null + // constraints can be null + web::json::value make_nc_datatype_typedef(const web::json::value& description, const nc_name& name, bool is_sequence, const utility::string_t& parent_type, const web::json::value& constraints) + { + using web::json::value; + + auto data = make_nc_datatype_descriptor(description, name, nc_datatype_type::Typedef, constraints); + data[nmos::fields::nc::parent_type] = value::string(parent_type); + data[nmos::fields::nc::is_sequence] = value::boolean(is_sequence); + + return data; + } + web::json::value make_nc_datatype_typedef(const utility::string_t& description, const nc_name& name, bool is_sequence, const utility::string_t& parent_type, const web::json::value& constraints) + { + using web::json::value; + + return make_nc_datatype_typedef(value::string(description), name, is_sequence, parent_type, constraints); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertyconstraints + web::json::value make_nc_property_constraints(const nc_property_id& property_id, const web::json::value& default_value) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::property_id, make_nc_property_id(property_id) }, + { nmos::fields::nc::default_value, default_value } + }); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertyconstraintsnumber + web::json::value make_nc_property_constraints_number(const nc_property_id& property_id, const web::json::value& default_value, const web::json::value& minimum, const web::json::value& maximum, const web::json::value& step) + { + using web::json::value; + + auto data = make_nc_property_constraints(property_id, default_value); + data[nmos::fields::nc::minimum] = minimum; + data[nmos::fields::nc::maximum] = maximum; + data[nmos::fields::nc::step] = step; + + return data; + } + web::json::value make_nc_property_constraints_number(const nc_property_id& property_id, uint64_t default_value, uint64_t minimum, uint64_t maximum, uint64_t step) + { + using web::json::value; + + return make_nc_property_constraints_number(property_id, value(default_value), value(minimum), value(maximum), value(step)); + } + web::json::value make_nc_property_constraints_number(const nc_property_id& property_id, uint64_t minimum, uint64_t maximum, uint64_t step) + { + using web::json::value; + + return make_nc_property_constraints_number(property_id, value::null(), minimum, maximum, step); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertyconstraintsstring + web::json::value make_nc_property_constraints_string(const nc_property_id& property_id, const web::json::value& default_value, const web::json::value& max_characters, const web::json::value& pattern) + { + using web::json::value; + + auto data = make_nc_property_constraints(property_id, default_value); + data[nmos::fields::nc::max_characters] = max_characters; + data[nmos::fields::nc::pattern] = pattern; + + return data; + } + web::json::value make_nc_property_constraints_string(const nc_property_id& property_id, const utility::string_t& default_value, uint32_t max_characters, const nc_regex& pattern) + { + using web::json::value; + + return make_nc_property_constraints_string(property_id, value::string(default_value), max_characters, value::string(pattern)); + } + web::json::value make_nc_property_constraints_string(const nc_property_id& property_id, uint32_t max_characters, const nc_regex& pattern) + { + using web::json::value; + + return make_nc_property_constraints_string(property_id, value::null(), max_characters, value::string(pattern)); + } + web::json::value make_nc_property_constraints_string(const nc_property_id& property_id, uint32_t max_characters) + { + using web::json::value; + + return make_nc_property_constraints_string(property_id, value::null(), max_characters, value::null()); + } + web::json::value make_nc_property_constraints_string(const nc_property_id& property_id, const nc_regex& pattern) + { + using web::json::value; + + return make_nc_property_constraints_string(property_id, value::null(), value::null(), value::string(pattern)); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncparameterconstraints + web::json::value make_nc_parameter_constraints(const web::json::value& default_value) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::default_value, default_value } + }); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncparameterconstraintsnumber + web::json::value make_nc_parameter_constraints_number(const web::json::value& default_value, const web::json::value& minimum, const web::json::value& maximum, const web::json::value& step) + { + using web::json::value; + + auto data = make_nc_parameter_constraints(default_value); + data[nmos::fields::nc::minimum] = minimum; + data[nmos::fields::nc::maximum] = maximum; + data[nmos::fields::nc::step] = step; + + return data; + } + web::json::value make_nc_parameter_constraints_number(uint64_t default_value, uint64_t minimum, uint64_t maximum, uint64_t step) + { + using web::json::value; + + return make_nc_parameter_constraints_number(value(default_value), value(minimum), value(maximum), value(step)); + } + web::json::value make_nc_parameter_constraints_number(uint64_t minimum, uint64_t maximum, uint64_t step) + { + using web::json::value; + + return make_nc_parameter_constraints_number(value::null(), minimum, maximum, step); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncparameterconstraintsstring + web::json::value make_nc_parameter_constraints_string(const web::json::value& default_value, const web::json::value& max_characters, const web::json::value& pattern) + { + auto data = make_nc_parameter_constraints(default_value); + data[nmos::fields::nc::max_characters] = max_characters; + data[nmos::fields::nc::pattern] = pattern; + + return data; + } + web::json::value make_nc_parameter_constraints_string(const utility::string_t& default_value, uint32_t max_characters, const nc_regex& pattern) + { + using web::json::value; + + return make_nc_parameter_constraints_string(value::string(default_value), max_characters, value::string(pattern)); + } + web::json::value make_nc_parameter_constraints_string(uint32_t max_characters, const nc_regex& pattern) + { + using web::json::value; + + return make_nc_parameter_constraints_string(value::null(), max_characters, value::string(pattern)); + } + web::json::value make_nc_parameter_constraints_string(uint32_t max_characters) + { + using web::json::value; + + return make_nc_parameter_constraints_string(value::null(), max_characters, value::null()); + } + web::json::value make_nc_parameter_constraints_string(const nc_regex& pattern) + { + using web::json::value; + + return make_nc_parameter_constraints_string(value::null(), value::null(), value::string(pattern)); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpointresource + web::json::value make_nc_touchpoint_resource(const nc_touchpoint_resource& resource) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::resource_type, resource.resource_type } + }); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpointresourcenmos + web::json::value make_nc_touchpoint_resource_nmos(const nc_touchpoint_resource_nmos& resource) + { + using web::json::value; + + auto data = make_nc_touchpoint_resource(resource); + data[nmos::fields::nc::id] = value::string(resource.id); + + return data; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpointresourcenmoschannelmapping + web::json::value make_nc_touchpoint_resource_nmos_channel_mapping(const nc_touchpoint_resource_nmos_channel_mapping& resource) + { + using web::json::value; + + auto data = make_nc_touchpoint_resource_nmos(resource); + data[nmos::fields::nc::io_id] = value::string(resource.io_id); + + return data; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpoint + web::json::value make_nc_touchpoint(const utility::string_t& context_namespace) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::context_namespace, context_namespace } + }); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpointnmos + web::json::value make_nc_touchpoint_nmos(const nc_touchpoint_resource_nmos& resource) + { + auto data = make_nc_touchpoint(U("x-nmos")); + data[nmos::fields::nc::resource] = make_nc_touchpoint_resource_nmos(resource); + + return data; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpointnmoschannelmapping + web::json::value make_nc_touchpoint_nmos_channel_mapping(const nc_touchpoint_resource_nmos_channel_mapping& resource) + { + auto data = make_nc_touchpoint(U("x-nmos/channelmapping")); + data[nmos::fields::nc::resource] = make_nc_touchpoint_resource_nmos_channel_mapping(resource); + + return data; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncobject + web::json::value make_nc_object(const nc_class_id& class_id, nc_oid oid, bool constant_oid, const web::json::value& owner, const utility::string_t& role, const web::json::value& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints) + { + using web::json::value; + + const auto id = utility::conversions::details::to_string_t(oid); + auto data = make_resource_core(id, user_label.is_null() ? U("") : user_label.as_string(), description); // required for nmos::resource + data[nmos::fields::nc::class_id] = make_nc_class_id(class_id); + data[nmos::fields::nc::oid] = oid; + data[nmos::fields::nc::constant_oid] = value::boolean(constant_oid); + data[nmos::fields::nc::owner] = owner; + data[nmos::fields::nc::role] = value::string(role); + data[nmos::fields::nc::user_label] = user_label; + data[nmos::fields::nc::touchpoints] = touchpoints; + data[nmos::fields::nc::runtime_property_constraints] = runtime_property_constraints; // level 2 runtime constraints. See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Constraints.html + + return data; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncblock + web::json::value make_nc_block(const nc_class_id& class_id, nc_oid oid, bool constant_oid, const web::json::value& owner, const utility::string_t& role, const web::json::value& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, bool enabled, const web::json::value& members) + { + using web::json::value; + + auto data = details::make_nc_object(class_id, oid, constant_oid, owner, role, user_label, description, touchpoints, runtime_property_constraints); + data[nmos::fields::nc::enabled] = value::boolean(enabled); + data[nmos::fields::nc::members] = members; + + return data; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncworker + web::json::value make_nc_worker(const nc_class_id& class_id, nc_oid oid, bool constant_oid, nc_oid owner, const utility::string_t& role, const web::json::value& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, bool enabled) + { + using web::json::value; + + auto data = details::make_nc_object(class_id, oid, constant_oid, owner, role, user_label, description, touchpoints, runtime_property_constraints); + data[nmos::fields::nc::enabled] = value::boolean(enabled); + + return data; + } + + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitor + web::json::value make_receiver_monitor(const nc_class_id& class_id, nc_oid oid, bool constant_oid, nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, bool enabled, + nc_connection_status::status connection_status, const utility::string_t& connection_status_message, nc_payload_status::status payload_status, const utility::string_t& payload_status_message) + { + using web::json::value; + + auto data = make_nc_worker(class_id, oid, constant_oid, owner, role, value::string(user_label), description, touchpoints, runtime_property_constraints, enabled); + data[nmos::fields::nc::connection_status] = value::number(connection_status); + data[nmos::fields::nc::connection_status_message] = value::string(connection_status_message); + data[nmos::fields::nc::payload_status] = value::number(payload_status); + data[nmos::fields::nc::payload_status_message] = value::string(payload_status_message); + + return data; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmanager + web::json::value make_nc_manager(const nc_class_id& class_id, nc_oid oid, bool constant_oid, const web::json::value& owner, const utility::string_t& role, const web::json::value& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints) + { + return make_nc_object(class_id, oid, constant_oid, owner, role, user_label, description, touchpoints, runtime_property_constraints); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicemanager + web::json::value make_nc_device_manager(nc_oid oid, nc_oid owner, const web::json::value& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, + const web::json::value& manufacturer, const web::json::value& product, const utility::string_t& serial_number, + const web::json::value& user_inventory_code, const web::json::value& device_name, const web::json::value& device_role, const web::json::value& operational_state, nc_reset_cause::cause reset_cause) + { + using web::json::value; + + auto data = details::make_nc_manager(nc_device_manager_class_id, oid, true, owner, U("DeviceManager"), user_label, description, touchpoints, runtime_property_constraints); + data[nmos::fields::nc::nc_version] = value::string(U("v1.0.0")); + data[nmos::fields::nc::manufacturer] = manufacturer; + data[nmos::fields::nc::product] = product; + data[nmos::fields::nc::serial_number] = value::string(serial_number); + data[nmos::fields::nc::user_inventory_code] = user_inventory_code; + data[nmos::fields::nc::device_name] = device_name; + data[nmos::fields::nc::device_role] = device_role; + data[nmos::fields::nc::operational_state] = operational_state; + data[nmos::fields::nc::reset_cause] = reset_cause; + data[nmos::fields::nc::message] = value::null(); + + return data; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassmanager + web::json::value make_nc_class_manager(nc_oid oid, nc_oid owner, const web::json::value& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, const nmos::experimental::control_protocol_state& control_protocol_state) + { + using web::json::value; + + auto data = make_nc_manager(nc_class_manager_class_id, oid, true, owner, U("ClassManager"), user_label, description, touchpoints, runtime_property_constraints); + + auto lock = control_protocol_state.read_lock(); + + // add control classes + data[nmos::fields::nc::control_classes] = value::array(); + auto& control_classes = data[nmos::fields::nc::control_classes]; + for (const auto& control_class : control_protocol_state.control_class_descriptors) + { + auto& ctl_class = control_class.second; + + auto method_descriptors = value::array(); + for (const auto& method_descriptor : ctl_class.method_descriptors) { web::json::push_back(method_descriptors, std::get<0>(method_descriptor)); } + + const auto class_description = ctl_class.fixed_role.is_null() + ? make_nc_class_descriptor(ctl_class.description, ctl_class.class_id, ctl_class.name, ctl_class.property_descriptors, method_descriptors, ctl_class.event_descriptors) + : make_nc_class_descriptor(ctl_class.description, ctl_class.class_id, ctl_class.name, ctl_class.fixed_role.as_string(), ctl_class.property_descriptors, method_descriptors, ctl_class.event_descriptors); + web::json::push_back(control_classes, class_description); + } + + // add datatypes + data[nmos::fields::nc::datatypes] = value::array(); + auto& datatypes = data[nmos::fields::nc::datatypes]; + for (const auto& datatype_descriptor : control_protocol_state.datatype_descriptors) + { + web::json::push_back(datatypes, datatype_descriptor.second.descriptor); + } + + return data; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertychangedeventdata + web::json::value make_nc_property_changed_event_data(const nc_property_changed_event_data& property_changed_event_data) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::property_id, details::make_nc_property_id(property_changed_event_data.property_id) }, + { nmos::fields::nc::change_type, property_changed_event_data.change_type }, + { nmos::fields::nc::value, property_changed_event_data.value }, + { nmos::fields::nc::sequence_item_index, property_changed_event_data.sequence_item_index } + }); + } + } + + // command message response + // See https://specs.amwa.tv/is-12/branches/v1.0.x/docs/Protocol_messaging.html#command-response-message-type + web::json::value make_control_protocol_response(int32_t handle, const web::json::value& method_result) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::handle, handle }, + { nmos::fields::nc::result, method_result } + }); + } + web::json::value make_control_protocol_command_response(const web::json::value& responses) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::message_type, ncp_message_type::command_response }, + { nmos::fields::nc::responses, responses } + }); + } + + // subscription response + // See https://specs.amwa.tv/is-12/branches/v1.0.x/docs/Protocol_messaging.html#subscription-response-message-type + web::json::value make_control_protocol_subscription_response(const web::json::value& subscriptions) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::message_type, ncp_message_type::subscription_response }, + { nmos::fields::nc::subscriptions, subscriptions } + }); + } + + // notification + // See https://specs.amwa.tv/ms-05-01/branches/v1.0.x/docs/Core_Mechanisms.html#notification-messages + // See https://specs.amwa.tv/is-12/branches/v1.0.x/docs/Protocol_messaging.html#notification-message-type + web::json::value make_control_protocol_notification(nc_oid oid, const nc_event_id& event_id, const nc_property_changed_event_data& property_changed_event_data) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::oid, oid }, + { nmos::fields::nc::event_id, details::make_nc_event_id(event_id)}, + { nmos::fields::nc::event_data, details::make_nc_property_changed_event_data(property_changed_event_data) } + }); + } + web::json::value make_control_protocol_notification_message(const web::json::value& notifications) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::message_type, ncp_message_type::notification }, + { nmos::fields::nc::notifications, notifications } + }); + } + + // property changed notification event + // See https://specs.amwa.tv/ms-05-01/branches/v1.0.x/docs/Core_Mechanisms.html#the-propertychanged-event + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/NcObject.html#propertychanged-event + web::json::value make_property_changed_event(nc_oid oid, const std::vector& property_changed_event_data_list) + { + using web::json::value; + + auto notifications = value::array(); + for (auto& property_changed_event_data : property_changed_event_data_list) + { + web::json::push_back(notifications, make_control_protocol_notification(oid, nc_object_property_changed_event_id, property_changed_event_data)); + } + return make_control_protocol_notification_message(notifications); + } + + // error message + // See https://specs.amwa.tv/is-12/branches/v1.0.x/docs/Protocol_messaging.html#error-messages + web::json::value make_control_protocol_error_message(const nc_method_result& method_result, const utility::string_t& error_message) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::nc::message_type, ncp_message_type::error }, + { nmos::fields::nc::status, method_result.status}, + { nmos::fields::nc::error_message, error_message } + }); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncobject + web::json::value make_nc_object_properties() + { + using web::json::value; + + auto properties = value::array(); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Static value. All instances of the same class will have the same identity value"), nc_object_class_id_property_id, nmos::fields::nc::class_id, U("NcClassId"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Object identifier"), nc_object_oid_property_id, nmos::fields::nc::oid, U("NcOid"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("TRUE iff OID is hardwired into device"), nc_object_constant_oid_property_id, nmos::fields::nc::constant_oid, U("NcBoolean"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("OID of containing block. Can only ever be null for the root block"), nc_object_owner_property_id, nmos::fields::nc::owner, U("NcOid"), true, true, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Role of object in the containing block"), nc_object_role_property_id, nmos::fields::nc::role, U("NcString"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Scribble strip"), nc_object_user_label_property_id, nmos::fields::nc::user_label, U("NcString"), false, true, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Touchpoints to other contexts"), nc_object_touchpoints_property_id, nmos::fields::nc::touchpoints, U("NcTouchpoint"), true, true, true, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Runtime property constraints"), nc_object_runtime_property_constraints_property_id, nmos::fields::nc::runtime_property_constraints, U("NcPropertyConstraints"), true, true, true, false, value::null())); + + return properties; + } + web::json::value make_nc_object_methods() + { + using web::json::value; + + auto methods = value::array(); + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Get property value"), nc_object_get_method_id, U("Get"), U("NcMethodResultPropertyValue"), parameters, false)); + } + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Property value"), nmos::fields::nc::value, true, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Set property value"), nc_object_set_method_id, U("Set"), U("NcMethodResult"), parameters, false)); + } + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Index of item in the sequence"), nmos::fields::nc::index, U("NcId"), false, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Get sequence item"), nc_object_get_sequence_item_method_id, U("GetSequenceItem"), U("NcMethodResultPropertyValue"), parameters, false)); + } + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Index of item in the sequence"), nmos::fields::nc::index, U("NcId"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Value"), nmos::fields::nc::value, true, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Set sequence item value"), nc_object_set_sequence_item_method_id, U("SetSequenceItem"), U("NcMethodResult"), parameters, false)); + } + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id,U("NcPropertyId"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Value"), nmos::fields::nc::value, true, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Add item to sequence"), nc_object_add_sequence_item_method_id, U("AddSequenceItem"), U("NcMethodResultId"), parameters, false)); + } + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Index of item in the sequence"), nmos::fields::nc::index, U("NcId"), false, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Delete sequence item"), nc_object_remove_sequence_item_method_id, U("RemoveSequenceItem"), U("NcMethodResult"), parameters, false)); + } + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Get sequence length"), nc_object_get_sequence_length_method_id, U("GetSequenceLength"), U("NcMethodResultLength"), parameters, false)); + } + + return methods; + } + web::json::value make_nc_object_events() + { + using web::json::value; + + auto events = value::array(); + web::json::push_back(events, details::make_nc_event_descriptor(U("Property changed event"), nc_object_property_changed_event_id, U("PropertyChanged"), U("NcPropertyChangedEventData"), false)); + + return events; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncblock + web::json::value make_nc_block_properties() + { + using web::json::value; + + auto properties = value::array(); + web::json::push_back(properties, details::make_nc_property_descriptor(U("TRUE if block is functional"), nc_block_enabled_property_id, nmos::fields::nc::enabled, U("NcBoolean"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Descriptors of this block's members"), nc_block_members_property_id, nmos::fields::nc::members, U("NcBlockMemberDescriptor"), true, false, true, false, value::null())); + + return properties; + } + web::json::value make_nc_block_methods() + { + using web::json::value; + + auto methods = value::array(); + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("If recurse is set to true, nested members can be retrieved"), nmos::fields::nc::recurse, U("NcBoolean"), false, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Gets descriptors of members of the block"), nc_block_get_member_descriptors_method_id, U("GetMemberDescriptors"), U("NcMethodResultBlockMemberDescriptors"), parameters, false)); + } + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Relative path to search for (MUST not include the role of the block targeted by oid)"), nmos::fields::nc::path, U("NcRolePath"), false, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Finds member(s) by path"), nc_block_find_members_by_path_method_id, U("FindMembersByPath"), U("NcMethodResultBlockMemberDescriptors"), parameters, false)); + } + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Role text to search for"), nmos::fields::nc::role, U("NcString"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Signals if the comparison should be case sensitive"), nmos::fields::nc::case_sensitive, U("NcBoolean"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("TRUE to only return exact matches"), nmos::fields::nc::match_whole_string, U("NcBoolean"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("TRUE to search nested blocks"), nmos::fields::nc::recurse, U("NcBoolean"), false, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Finds members with given role name or fragment"), nc_block_find_members_by_role_method_id, U("FindMembersByRole"), U("NcMethodResultBlockMemberDescriptors"), parameters, false)); + } + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("Class id to search for"), nmos::fields::nc::class_id, U("NcClassId"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("If TRUE it will also include derived class descriptors"), nmos::fields::nc::include_derived, U("NcBoolean"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("TRUE to search nested blocks"), nmos::fields::nc::recurse,U("NcBoolean"), false, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Finds members with given class id"), nc_block_find_members_by_class_id_method_id, U("FindMembersByClassId"), U("NcMethodResultBlockMemberDescriptors"), parameters, false)); + } + + return methods; + } + web::json::value make_nc_block_events() + { + using web::json::value; + + return value::array(); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncworker + web::json::value make_nc_worker_properties() + { + using web::json::value; + + auto properties = value::array(); + web::json::push_back(properties, details::make_nc_property_descriptor(U("TRUE iff worker is enabled"), nc_worker_enabled_property_id, nmos::fields::nc::enabled, U("NcBoolean"), false, false, false, false, value::null())); + + return properties; + } + web::json::value make_nc_worker_methods() + { + using web::json::value; + + return value::array(); + } + web::json::value make_nc_worker_events() + { + using web::json::value; + + return value::array(); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmanager + web::json::value make_nc_manager_properties() + { + using web::json::value; + + return value::array(); + } + web::json::value make_nc_manager_methods() + { + using web::json::value; + + return value::array(); + } + web::json::value make_nc_manager_events() + { + using web::json::value; + + return value::array(); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicemanager + web::json::value make_nc_device_manager_properties() + { + using web::json::value; + + auto properties = value::array(); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Version of MS-05-02 that this device uses"), nc_device_manager_nc_version_property_id, nmos::fields::nc::nc_version, U("NcVersionCode"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Manufacturer descriptor"), nc_device_manager_manufacturer_property_id, nmos::fields::nc::manufacturer, U("NcManufacturer"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Product descriptor"), nc_device_manager_product_property_id, nmos::fields::nc::product, U("NcProduct"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Serial number"), nc_device_manager_serial_number_property_id, nmos::fields::nc::serial_number, U("NcString"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Asset tracking identifier (user specified)"), nc_device_manager_user_inventory_code_property_id, nmos::fields::nc::user_inventory_code, U("NcString"), false, true, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Name of this device in the application. Instance name, not product name"), nc_device_manager_device_name_property_id, nmos::fields::nc::device_name, U("NcString"), false, true, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Role of this device in the application"), nc_device_manager_device_role_property_id, nmos::fields::nc::device_role, U("NcString"), false, true, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Device operational state"), nc_device_manager_operational_state_property_id, nmos::fields::nc::operational_state, U("NcDeviceOperationalState"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Reason for most recent reset"), nc_device_manager_reset_cause_property_id, nmos::fields::nc::reset_cause, U("NcResetCause"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Arbitrary message from dev to controller"), nc_device_manager_message_property_id, nmos::fields::nc::message, U("NcString"), true, true, false, false, value::null())); + + return properties; + } + web::json::value make_nc_device_manager_methods() + { + using web::json::value; + + return value::array(); + } + web::json::value make_nc_device_manager_events() + { + using web::json::value; + + return value::array(); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassmanager + web::json::value make_nc_class_manager_properties() + { + using web::json::value; + + auto properties = value::array(); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Descriptions of all control classes in the device (descriptors do not contain inherited elements)"), nc_class_manager_control_classes_property_id, nmos::fields::nc::control_classes, U("NcClassDescriptor"), true, false, true, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Descriptions of all data types in the device (descriptors do not contain inherited elements)"), nc_class_manager_datatypes_property_id, nmos::fields::nc::datatypes, U("NcDatatypeDescriptor"), true, false, true, false, value::null())); + + return properties; + } + web::json::value make_nc_class_manager_methods() + { + using web::json::value; + + auto methods = value::array(); + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("class ID"), nmos::fields::nc::class_id, U("NcClassId"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("If set the descriptor would contain all inherited elements"), nmos::fields::nc::include_inherited, U("NcBoolean"), false, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Get a single class descriptor"), nc_class_manager_get_control_class_method_id, U("GetControlClass"), U("NcMethodResultClassDescriptor"), parameters, false)); + } + { + auto parameters = value::array(); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("name of datatype"), nmos::fields::nc::name, U("NcName"), false, false, value::null())); + web::json::push_back(parameters, details::make_nc_parameter_descriptor(U("If set the descriptor would contain all inherited elements"), nmos::fields::nc::include_inherited, U("NcBoolean"), false, false, value::null())); + web::json::push_back(methods, details::make_nc_method_descriptor(U("Get a single datatype descriptor"), nc_class_manager_get_datatype_method_id, U("GetDatatype"), U("NcMethodResultDatatypeDescriptor"), parameters, false)); + } + + return methods; + } + web::json::value make_nc_class_manager_events() + { + using web::json::value; + + return value::array(); + } + + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitor + web::json::value make_nc_receiver_monitor_properties() + { + using web::json::value; + + auto properties = value::array(); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Connection status property"), nc_receiver_monitor_connection_status_property_id, nmos::fields::nc::connection_status, U("NcConnectionStatus"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Connection status message property"), nc_receiver_monitor_connection_status_message_property_id, nmos::fields::nc::connection_status_message, U("NcString"), true, true, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Payload status property"), nc_receiver_monitor_payload_status_property_id, nmos::fields::nc::payload_status, U("NcPayloadStatus"), true, false, false, false, value::null())); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Payload status message property"), nc_receiver_monitor_payload_status_message_property_id, nmos::fields::nc::payload_status_message, U("NcString"), true, true, false, false, value::null())); + + return properties; + } + web::json::value make_nc_receiver_monitor_methods() + { + using web::json::value; + + return value::array(); + } + web::json::value make_nc_receiver_monitor_events() + { + using web::json::value; + + return value::array(); + } + + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitorprotected + web::json::value make_nc_receiver_monitor_protected_properties() + { + using web::json::value; + + auto properties = value::array(); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Indicates if signal protection is active"), nc_receiver_monitor_protected_signal_protection_status_property_id, nmos::fields::nc::signal_protection_status, U("NcBoolean"), true, false, false, false, value::null())); + + return properties; + } + web::json::value make_nc_receiver_monitor_protected_methods() + { + using web::json::value; + + return value::array(); + } + web::json::value make_nc_receiver_monitor_protected_events() + { + using web::json::value; + + return value::array(); + } + + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/identification/#ncidentbeacon + web::json::value make_nc_ident_beacon_properties() + { + using web::json::value; + + auto properties = value::array(); + web::json::push_back(properties, details::make_nc_property_descriptor(U("Indicator active state"), nc_ident_beacon_active_property_id, nmos::fields::nc::active, U("NcBoolean"), false, false, false, false, value::null())); + + return properties; + } + web::json::value make_nc_ident_beacon_methods() + { + using web::json::value; + + return value::array(); + } + web::json::value make_nc_ident_beacon_events() + { + using web::json::value; + + return value::array(); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.html + web::json::value make_nc_object_class() + { + using web::json::value; + + return details::make_nc_class_descriptor(U("NcObject class descriptor"), nc_object_class_id, U("NcObject"), make_nc_object_properties(), make_nc_object_methods(), make_nc_object_events()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.1.html + web::json::value make_nc_block_class() + { + using web::json::value; + + return details::make_nc_class_descriptor(U("NcBlock class descriptor"), nc_block_class_id, U("NcBlock"), make_nc_block_properties(), make_nc_block_methods(), make_nc_block_events()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.2.html + web::json::value make_nc_worker_class() + { + using web::json::value; + + return details::make_nc_class_descriptor(U("NcWorker class descriptor"), nc_worker_class_id, U("NcWorker"), make_nc_worker_properties(), make_nc_worker_methods(), make_nc_worker_events()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.3.html + web::json::value make_nc_manager_class() + { + using web::json::value; + + return details::make_nc_class_descriptor(U("NcManager class descriptor"), nc_manager_class_id, U("NcManager"), make_nc_manager_properties(), make_nc_manager_methods(), make_nc_manager_events()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.3.1.html + web::json::value make_nc_device_manager_class() + { + using web::json::value; + + return details::make_nc_class_descriptor(U("NcDeviceManager class descriptor"), nc_device_manager_class_id, U("NcDeviceManager"), U("DeviceManager"), make_nc_device_manager_properties(), make_nc_device_manager_methods(), make_nc_device_manager_events()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.3.2.html + web::json::value make_nc_class_manager_class() + { + using web::json::value; + + return details::make_nc_class_descriptor(U("NcClassManager class descriptor"), nc_class_manager_class_id, U("NcClassManager"), U("ClassManager"), make_nc_class_manager_properties(), make_nc_class_manager_methods(), make_nc_class_manager_events()); + } + + // Identification feature set control classes + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/identification/#ncidentbeacon + web::json::value make_nc_ident_beacon_class() + { + using web::json::value; + + return details::make_nc_class_descriptor(U("NcIdentBeacon class descriptor"), nc_ident_beacon_class_id, U("NcIdentBeacon"), make_nc_ident_beacon_properties(), make_nc_ident_beacon_methods(), make_nc_ident_beacon_events()); + } + + // Monitoring feature set control classes + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitor + web::json::value make_nc_receiver_monitor_class() + { + using web::json::value; + + return details::make_nc_class_descriptor(U("NcReceiverMonitor class descriptor"), nc_receiver_monitor_class_id, U("NcReceiverMonitor"), make_nc_receiver_monitor_properties(), make_nc_receiver_monitor_methods(), make_nc_receiver_monitor_events()); + } + + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitorprotected + web::json::value make_nc_receiver_monitor_protected_class() + { + using web::json::value; + + return details::make_nc_class_descriptor(U("NcReceiverMonitorProtected class descriptor"), nc_receiver_monitor_protected_class_id, U("NcReceiverMonitorProtected"), make_nc_receiver_monitor_protected_properties(), make_nc_receiver_monitor_protected_methods(), make_nc_receiver_monitor_protected_events()); + } + + // Primitive datatypes + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_boolean_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_primitive(U("Boolean primitive type"), U("NcBoolean"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_int16_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_primitive(U("short"), U("NcInt16"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_int32_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_primitive(U("long"), U("NcInt32"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_int64_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_primitive(U("longlong"), U("NcInt64"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_uint16_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_primitive(U("unsignedshort"), U("NcUint16"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_uint32_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_primitive(U("unsignedlong"), U("NcUint32"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_uint64_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_primitive(U("unsignedlonglong"), U("NcUint64"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_float32_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_primitive(U("unrestrictedfloat"), U("NcFloat32"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_float64_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_primitive(U("unrestricteddouble"), U("NcFloat64"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_string_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_primitive(U("UTF-8 string"), U("NcString"), value::null()); + } + + + // Standard datatypes + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcBlockMemberDescriptor.html + web::json::value make_nc_block_member_descriptor_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Role of member in its containing block"), nmos::fields::nc::role, U("NcString"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("OID of member"), nmos::fields::nc::oid, U("NcOid"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("TRUE iff member's OID is hardwired into device"), nmos::fields::nc::constant_oid, U("NcBoolean"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Class ID"), nmos::fields::nc::class_id, U("NcClassId"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("User label"), nmos::fields::nc::user_label, U("NcString"), true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Containing block's OID"), nmos::fields::nc::owner, U("NcOid"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Descriptor which is specific to a block member"), U("NcBlockMemberDescriptor"), fields, U("NcDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcClassDescriptor.html + web::json::value make_nc_class_descriptor_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Identity of the class"), nmos::fields::nc::class_id, U("NcClassId"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of the class"), nmos::fields::nc::name, U("NcName"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Role if the class has fixed role (manager classes)"), nmos::fields::nc::fixed_role, U("NcString"), true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Property descriptors"), nmos::fields::nc::properties, U("NcPropertyDescriptor"), false, true, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Method descriptors"), nmos::fields::nc::methods, U("NcMethodDescriptor"), false, true, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Event descriptors"), nmos::fields::nc::events, U("NcEventDescriptor"), false, true, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Descriptor of a class"), U("NcClassDescriptor"), fields, U("NcDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcClassId.html + web::json::value make_nc_class_id_datatype() + { + using web::json::value; + + return details::make_nc_datatype_typedef(U("Sequence of class ID fields"), U("NcClassId"), true, U("NcInt32"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDatatypeDescriptor.html + web::json::value make_nc_datatype_descriptor_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Datatype name"), nmos::fields::nc::name, U("NcName"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Type: Primitive, Typedef, Struct, Enum"), nmos::fields::nc::type, U("NcDatatypeType"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Optional constraints on top of the underlying data type"), nmos::fields::nc::constraints, U("NcParameterConstraints"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Base datatype descriptor"), U("NcDatatypeDescriptor"), fields, U("NcDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDatatypeDescriptorEnum.html + web::json::value make_nc_datatype_descriptor_enum_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("One item descriptor per enum option"), nmos::fields::nc::items, U("NcEnumItemDescriptor"), false, true, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Enum datatype descriptor"), U("NcDatatypeDescriptorEnum"), fields, U("NcDatatypeDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDatatypeDescriptorPrimitive.html + web::json::value make_nc_datatype_descriptor_primitive_datatype() + { + using web::json::value; + + auto fields = value::array(); + return details::make_nc_datatype_descriptor_struct(U("Primitive datatype descriptor"), U("NcDatatypeDescriptorPrimitive"), fields, U("NcDatatypeDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDatatypeDescriptorStruct.html + web::json::value make_nc_datatype_descriptor_struct_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("One item descriptor per field of the struct"), nmos::fields::nc::fields, U("NcFieldDescriptor"), false, true, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of the parent type if any or null if it has no parent"), nmos::fields::nc::parent_type, U("NcName"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Struct datatype descriptor"), U("NcDatatypeDescriptorStruct"), fields, U("NcDatatypeDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDatatypeDescriptorTypeDef.html + web::json::value make_nc_datatype_descriptor_type_def_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Original typedef datatype name"), nmos::fields::nc::parent_type, U("NcName"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("TRUE iff type is a typedef sequence of another type"), nmos::fields::nc::is_sequence, U("NcBoolean"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Type def datatype descriptor"), U("NcDatatypeDescriptorTypeDef"), fields, U("NcDatatypeDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDatatypeType.html + web::json::value make_nc_datatype_type_datatype() + { + using web::json::value; + + auto items = value::array(); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Primitive datatype"), U("Primitive"), 0)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Simple alias of another datatype"), U("Typedef"), 1)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Data structure"), U("Struct"), 2)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Enum datatype"), U("Enum"), 3)); + return details::make_nc_datatype_descriptor_enum(U("Datatype type"), U("NcDatatypeType"), items, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDescriptor.html + web::json::value make_nc_descriptor_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Optional user facing description"), nmos::fields::nc::description, U("NcString"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Base descriptor"), U("NcDescriptor"), fields, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDeviceGenericState.html + web::json::value make_nc_device_generic_state_datatype() + { + using web::json::value; + + auto items = value::array(); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Unknown"), U("Unknown"), 0)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Normal operation"), U("NormalOperation"), 1)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Device is initializing"), U("Initializing"), 2)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Device is performing a software or firmware update"), U("Updating"), 3)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Device is experiencing a licensing error"), U("LicensingError"), 4)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Device is experiencing an internal error"), U("InternalError"), 5)); + return details::make_nc_datatype_descriptor_enum(U("Device generic operational state"), U("NcDeviceGenericState"), items, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDeviceOperationalState.html + web::json::value make_nc_device_operational_state_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Generic operational state"), nmos::fields::nc::generic_state, U("NcDeviceGenericState"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Specific device details"), nmos::fields::nc::device_specific_details, U("NcString"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Device operational state"), U("NcDeviceOperationalState"), fields, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcElementId.html + web::json::value make_nc_element_id_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Level of the element"), nmos::fields::nc::level, U("NcUint16"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Index of the element"), nmos::fields::nc::index, U("NcUint16"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Class element id which contains the level and index"), U("NcElementId"), fields, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcEnumItemDescriptor.html + web::json::value make_nc_enum_item_descriptor_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of option"), nmos::fields::nc::name, U("NcName"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Enum item numerical value"), nmos::fields::nc::value, U("NcUint16"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Descriptor of an enum item"), U("NcEnumItemDescriptor"), fields, U("NcDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcEventDescriptor.html + web::json::value make_nc_event_descriptor_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Event id with level and index"), nmos::fields::nc::id, U("NcEventId"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of event"), nmos::fields::nc::name, U("NcName"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of event data's datatype"), nmos::fields::nc::event_datatype, U("NcName"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("TRUE iff property is marked as deprecated"), nmos::fields::nc::is_deprecated, U("NcBoolean"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Descriptor of a class event"), U("NcEventDescriptor"), fields, U("NcDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcEventId.html + web::json::value make_nc_event_id_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_struct(U("Event id which contains the level and index"), U("NcEventId"), value::array(), U("NcElementId"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcFieldDescriptor.html + web::json::value make_nc_field_descriptor_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of field"), nmos::fields::nc::name, U("NcName"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of field's datatype. Can only ever be null if the type is any"), nmos::fields::nc::type_name, U("NcName"), true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("TRUE iff field is nullable"), nmos::fields::nc::is_nullable, U("NcBoolean"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("TRUE iff field is a sequence"), nmos::fields::nc::is_sequence, U("NcBoolean"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Optional constraints on top of the underlying data type"), nmos::fields::nc::constraints, U("NcParameterConstraints"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Descriptor of a field of a struct"), U("NcFieldDescriptor"), fields, U("NcDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcId.html + web::json::value make_nc_id_datatype() + { + using web::json::value; + + return details::make_nc_datatype_typedef(U("Identity handler"), U("NcId"), false, U("NcUint32"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcManufacturer.html + web::json::value make_nc_manufacturer_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Manufacturer's name"), nmos::fields::nc::name, U("NcString"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("IEEE OUI or CID of manufacturer"), nmos::fields::nc::organization_id, U("NcOrganizationId"), true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("URL of the manufacturer's website"), nmos::fields::nc::website, U("NcUri"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Manufacturer descriptor"), U("NcManufacturer"), fields, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodDescriptor.html + web::json::value make_nc_method_descriptor_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Method id with level and index"), nmos::fields::nc::id, U("NcMethodId"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of method"), nmos::fields::nc::name, U("NcName"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of method result's datatype"), nmos::fields::nc::result_datatype, U("NcName"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Parameter descriptors if any"), nmos::fields::nc::parameters, U("NcParameterDescriptor"), false, true, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("TRUE iff property is marked as deprecated"), nmos::fields::nc::is_deprecated, U("NcBoolean"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Descriptor of a class method"), U("NcMethodDescriptor"), fields, U("NcDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodId.html + web::json::value make_nc_method_id_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_struct(U("Method id which contains the level and index"), U("NcMethodId"), value::array(), U("NcElementId"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResult.html + web::json::value make_nc_method_result_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Status for the invoked method"), nmos::fields::nc::status, U("NcMethodStatus"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Base result of the invoked method"), U("NcMethodResult"), fields, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultBlockMemberDescriptors.html + web::json::value make_nc_method_result_block_member_descriptors_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Block member descriptors method result value"), nmos::fields::nc::value, U("NcBlockMemberDescriptor"), false, true, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Method result containing block member descriptors as the value"), U("NcMethodResultBlockMemberDescriptors"), fields, U("NcMethodResult"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultClassDescriptor.html + web::json::value make_nc_method_result_class_descriptor_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Class descriptor method result value"), nmos::fields::nc::value, U("NcClassDescriptor"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Method result containing a class descriptor as the value"), U("NcMethodResultClassDescriptor"), fields, U("NcMethodResult"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultDatatypeDescriptor.html + web::json::value make_nc_method_result_datatype_descriptor_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Datatype descriptor method result value"), nmos::fields::nc::value, U("NcDatatypeDescriptor"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Method result containing a datatype descriptor as the value"), U("NcMethodResultDatatypeDescriptor"), fields, U("NcMethodResult"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultError.html + web::json::value make_nc_method_result_error_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Error message"), nmos::fields::nc::error_message, U("NcString"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Error result - to be used when the method call encounters an error"), U("NcMethodResultError"), fields, U("NcMethodResult"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultId.html + web::json::value make_nc_method_result_id_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Id result value"), nmos::fields::nc::value, U("NcId"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Id method result"), U("NcMethodResultId"), fields, U("NcMethodResult"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultLength.html + web::json::value make_nc_method_result_length_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Length result value"), nmos::fields::nc::value, U("NcUint32"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Length method result"), U("NcMethodResultLength"), fields, U("NcMethodResult"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultPropertyValue.html + web::json::value make_nc_method_result_property_value_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Getter method value for the associated property"), nmos::fields::nc::value, true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Result when invoking the getter method associated with a property"), U("NcMethodResultPropertyValue"), fields, U("NcMethodResult"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodStatus.html + web::json::value make_nc_method_status_datatype() + { + using web::json::value; + + auto items = value::array(); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Method call was successful"), U("Ok"), 200)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Method call was successful but targeted property is deprecated"), U("PropertyDeprecated"), 298)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Method call was successful but method is deprecated"), U("MethodDeprecated"), 299)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Badly-formed command (e.g. the incoming command has invalid message encoding and cannot be parsed by the underlying protocol)"), U("BadCommandFormat"), 400)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Client is not authorized"), U("Unauthorized"), 401)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Command addresses a nonexistent object"), U("BadOid"), 404)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Attempt to change read-only state"), U("Readonly"), 405)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Method call is invalid in current operating context (e.g. attempting to invoke a method when the object is disabled)"), U("InvalidRequest"), 406)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("There is a conflict with the current state of the device"), U("Conflict"), 409)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Something was too big"), U("BufferOverflow"), 413)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Index is outside the available range"), U("IndexOutOfBounds"), 414)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Method parameter does not meet expectations (e.g. attempting to invoke a method with an invalid type for one of its parameters)"), U("ParameterError"), 417)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Addressed object is locked"), U("Locked"), 423)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Internal device error"), U("DeviceError"), 500)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Addressed method is not implemented by the addressed object"), U("MethodNotImplemented"), 501)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Addressed property is not implemented by the addressed object"), U("PropertyNotImplemented"), 502)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("The device is not ready to handle any commands"), U("NotReady"), 503)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Method call did not finish within the allotted time"), U("Timeout"), 504)); + return details::make_nc_datatype_descriptor_enum(U("Method invokation status"), U("NcMethodStatus"), items, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcName.html + web::json::value make_nc_name_datatype() + { + using web::json::value; + + return details::make_nc_datatype_typedef(U("Programmatically significant name, alphanumerics + underscore, no spaces"), U("NcName"), false, U("NcString"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcOid.html + web::json::value make_nc_oid_datatype() + { + using web::json::value; + + return details::make_nc_datatype_typedef(U("Object id"), U("NcOid"), false, U("NcUint32"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcOrganizationId.html + web::json::value make_nc_organization_id_datatype() + { + using web::json::value; + + return details::make_nc_datatype_typedef(U("Unique 24-bit organization id"), U("NcOrganizationId"), false, U("NcInt32"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcParameterConstraints.html + web::json::value make_nc_parameter_constraints_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Default value"), nmos::fields::nc::default_value, true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Abstract parameter constraints class"), U("NcParameterConstraints"), fields, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcParameterConstraintsNumber.html + web::json::value make_nc_parameter_constraints_number_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Optional minimum"), nmos::fields::nc::minimum, true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Optional maximum"), nmos::fields::nc::maximum, true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Optional step"), nmos::fields::nc::step, true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Number parameter constraints class"), U("NcParameterConstraintsNumber"), fields, U("NcParameterConstraints"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcParameterConstraintsString.html + web::json::value make_nc_parameter_constraints_string_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Maximum characters allowed"), nmos::fields::nc::max_characters, U("NcUint32"), true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Regex pattern"), nmos::fields::nc::pattern, U("NcRegex"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("String parameter constraints class"), U("NcParameterConstraintsString"), fields, U("NcParameterConstraints"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcParameterDescriptor.html + web::json::value make_nc_parameter_descriptor_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of parameter"), nmos::fields::nc::name, U("NcName"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of parameter's datatype. Can only ever be null if the type is any"), nmos::fields::nc::type_name, U("NcName"), true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("TRUE iff property is nullable"), nmos::fields::nc::is_nullable, U("NcBoolean"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("TRUE iff property is a sequence"), nmos::fields::nc::is_sequence, U("NcBoolean"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Optional constraints on top of the underlying data type"), nmos::fields::nc::constraints, U("NcParameterConstraints"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Descriptor of a method parameter"), U("NcParameterDescriptor"), fields, U("NcDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcProduct.html + web::json::value make_nc_product_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Product name"), nmos::fields::nc::name, U("NcString"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Manufacturer's unique key to product - model number, SKU, etc"), nmos::fields::nc::key, U("NcString"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Manufacturer's product revision level code"), nmos::fields::nc::revision_level, U("NcString"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Brand name under which product is sold"), nmos::fields::nc::brand_name, U("NcString"), true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Unique UUID of product (not product instance)"), nmos::fields::nc::uuid, U("NcUuid"), true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Text description of product"), nmos::fields::nc::description, U("NcString"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Product descriptor"), U("NcProduct"), fields, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyChangeType.html + web::json::value make_nc_property_change_type_datatype() + { + using web::json::value; + + auto items = value::array(); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Current value changed"), U("ValueChanged"), 0)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Sequence item added"), U("SequenceItemAdded"), 1)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Sequence item changed"), U("SequenceItemChanged"), 2)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Sequence item removed"), U("SequenceItemRemoved"), 3)); + return details::make_nc_datatype_descriptor_enum(U("Type of property change"), U("NcPropertyChangeType"), items, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyChangedEventData.html + web::json::value make_nc_property_changed_event_data_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("The id of the property that changed"), nmos::fields::nc::property_id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Information regarding the change type"), nmos::fields::nc::change_type, U("NcPropertyChangeType"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Property-type specific value"), nmos::fields::nc::value, true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Index of sequence item if the property is a sequence"), nmos::fields::nc::sequence_item_index,U("NcId"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Payload of property-changed event"), U("NcPropertyChangedEventData"), fields, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyConstraints.html + web::json::value make_nc_property_contraints_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("The id of the property being constrained"), nmos::fields::nc::property_id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Optional default value"), nmos::fields::nc::default_value, true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Property constraints class"), U("NcPropertyConstraints"), fields, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyConstraintsNumber.html + web::json::value make_nc_property_constraints_number_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Optional minimum"), nmos::fields::nc::minimum, true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Optional maximum"), nmos::fields::nc::maximum, true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Optional step"), nmos::fields::nc::step, true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Number property constraints class"), U("NcPropertyConstraintsNumber"), fields, U("NcPropertyConstraints"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyConstraintsString.html + web::json::value make_nc_property_constraints_string_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Maximum characters allowed"), nmos::fields::nc::max_characters, U("NcUint32"), true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Regex pattern"), nmos::fields::nc::pattern, U("NcRegex"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("String property constraints class"), U("NcPropertyConstraintsString"), fields, U("NcPropertyConstraints"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyDescriptor.html + web::json::value make_nc_property_descriptor_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Property id with level and index"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of property"), nmos::fields::nc::name, U("NcName"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Name of property's datatype. Can only ever be null if the type is any"), nmos::fields::nc::type_name, U("NcName"), true, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("TRUE iff property is read-only"), nmos::fields::nc::is_read_only, U("NcBoolean"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("TRUE iff property is nullable"), nmos::fields::nc::is_nullable, U("NcBoolean"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("TRUE iff property is a sequence"), nmos::fields::nc::is_sequence, U("NcBoolean"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("TRUE iff property is marked as deprecated"), nmos::fields::nc::is_deprecated, U("NcBoolean"), false, false, value::null())); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Optional constraints on top of the underlying data type"), nmos::fields::nc::constraints, U("NcParameterConstraints"), true, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Descriptor of a class property"), U("NcPropertyDescriptor"), fields, U("NcDescriptor"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyId.html + web::json::value make_nc_property_id_datatype() + { + using web::json::value; + + return details::make_nc_datatype_descriptor_struct(U("Property id which contains the level and index"), U("NcPropertyId"), value::array(), U("NcElementId"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcRegex.html + web::json::value make_nc_regex_datatype() + { + using web::json::value; + + return details::make_nc_datatype_typedef(U("Regex pattern"), U("NcRegex"), false, U("NcString"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcResetCause.html + web::json::value make_nc_reset_cause_datatype() + { + using web::json::value; + + auto items = value::array(); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Unknown"), U("Unknown"), 0)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Power on"), U("PowerOn"), 1)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Internal error"), U("InternalError"), 2)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Upgrade"), U("Upgrade"), 3)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Controller request"), U("ControllerRequest"), 4)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Manual request from the front panel"), U("ManualReset"), 5)); + return details::make_nc_datatype_descriptor_enum(U("Reset cause enum"), U("NcResetCause"), items, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcRolePath.html + web::json::value make_nc_role_path_datatype() + { + using web::json::value; + + return details::make_nc_datatype_typedef(U("Role path"), U("NcRolePath"), true, U("NcString"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTimeInterval.html + web::json::value make_nc_time_interval_datatype() + { + using web::json::value; + + return details::make_nc_datatype_typedef(U("Time interval described in nanoseconds"), U("NcTimeInterval"), false, U("NcInt64"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTouchpoint.html + web::json::value make_nc_touchpoint_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Context namespace"), nmos::fields::nc::context_namespace, U("NcString"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Base touchpoint class"), U("NcTouchpoint"), fields, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTouchpointNmos.html + web::json::value make_nc_touchpoint_nmos_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Context NMOS resource"), nmos::fields::nc::resource, U("NcTouchpointResourceNmos"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Touchpoint class for NMOS resources"), U("NcTouchpointNmos"), fields, U("NcTouchpoint"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTouchpointNmosChannelMapping.html + web::json::value make_nc_touchpoint_nmos_channel_mapping_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("Context Channel Mapping resource"), nmos::fields::nc::resource,U("NcTouchpointResourceNmosChannelMapping"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Touchpoint class for NMOS IS-08 resources"), U("NcTouchpointNmosChannelMapping"), fields, U("NcTouchpoint"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTouchpointResource.html + web::json::value make_nc_touchpoint_resource_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("The type of the resource"), nmos::fields::nc::resource_type, U("NcString"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Touchpoint resource class"), U("NcTouchpointResource"), fields, value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTouchpointResourceNmos.html + web::json::value make_nc_touchpoint_resource_nmos_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("NMOS resource UUID"), nmos::fields::nc::id, U("NcUuid"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Touchpoint resource class for NMOS resources"), U("NcTouchpointResourceNmos"), fields, U("NcTouchpointResource"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTouchpointResourceNmosChannelMapping.html + web::json::value make_nc_touchpoint_resource_nmos_channel_mapping_datatype() + { + using web::json::value; + + auto fields = value::array(); + web::json::push_back(fields, details::make_nc_field_descriptor(U("IS-08 Audio Channel Mapping input or output id"), nmos::fields::nc::io_id, U("NcString"), false, false, value::null())); + return details::make_nc_datatype_descriptor_struct(U("Touchpoint resource class for NMOS resources"), U("NcTouchpointResourceNmosChannelMapping"), fields, U("NcTouchpointResourceNmos"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcUri.html + web::json::value make_nc_uri_datatype() + { + using web::json::value; + + return details::make_nc_datatype_typedef(U("Uniform resource identifier"), U("NcUri"), false, U("NcString"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcUuid.html + web::json::value make_nc_uuid_datatype() + { + using web::json::value; + + return details::make_nc_datatype_typedef(U("UUID"), U("NcUuid"), false, U("NcString"), value::null()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcVersionCode.html + web::json::value make_nc_version_code_datatype() + { + using web::json::value; + + return details::make_nc_datatype_typedef(U("Version code in semantic versioning format"), U("NcVersionCode"), false, U("NcString"), value::null()); + } + + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncconnectionstatus + web::json::value make_nc_connection_status_datatype() + { + using web::json::value; + + auto items = value::array(); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("This is the value when there is no receiver"), U("Undefined"), 0)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Connected to a stream"), U("Connected"), 1)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Not connected to a stream"), U("Disconnected"), 2)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("A connection error was encountered"), U("ConnectionError"), 3)); + return details::make_nc_datatype_descriptor_enum(U("Connection status enum data typee"), U("NcConnectionStatus"), items, value::null()); + } + + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncpayloadstatus + web::json::value make_nc_payload_status_datatype() + { + using web::json::value; + + auto items = value::array(); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("This is the value when there's no connection"), U("Undefined"), 0)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Payload is being received without errors and is the correct type"), U("PayloadOK"), 1)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("Payload is being received but is of an unsupported type"), U("PayloadFormatUnsupported"), 2)); + web::json::push_back(items, details::make_nc_enum_item_descriptor(U("A payload error was encountered"), U("PayloadError"), 3)); + return details::make_nc_datatype_descriptor_enum(U("Connection status enum data typee"), U("NcPayloadStatus"), items, value::null()); + } +} diff --git a/Development/nmos/control_protocol_resource.h b/Development/nmos/control_protocol_resource.h new file mode 100644 index 000000000..5f341949b --- /dev/null +++ b/Development/nmos/control_protocol_resource.h @@ -0,0 +1,433 @@ +#ifndef NMOS_CONTROL_PROTOCOL_RESOURCE_H +#define NMOS_CONTROL_PROTOCOL_RESOURCE_H + +#include "cpprest/json_utils.h" +#include "nmos/control_protocol_typedefs.h" +#include "nmos/resource.h" + +namespace web +{ + namespace json + { + class value; + } + class uri; +} + +namespace nmos +{ + struct control_protocol_resource : resource + { + control_protocol_resource(api_version version, nmos::type type, web::json::value&& data, nmos::id id, bool never_expire) + : resource(version, type, std::move(data), id, never_expire) + {} + + control_protocol_resource(api_version version, nmos::type type, web::json::value data, bool never_expire) + : resource(version, type, data, never_expire) + {} + + // temporary storage to hold the resources until they are moved to the model resources + std::vector resources; + }; +} + +namespace nmos +{ + namespace experimental + { + struct control_protocol_state; + } + + namespace details + { + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmethodresult + web::json::value make_nc_method_result(const nc_method_result& method_result); + web::json::value make_nc_method_result_error(const nc_method_result& method_result, const utility::string_t& error_message); + web::json::value make_nc_method_result(const nc_method_result& method_result, const web::json::value& value); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncelementid + web::json::value make_nc_element_id(const nc_element_id& element_id); + nc_element_id parse_nc_element_id(const web::json::value& element_id); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nceventid + web::json::value make_nc_event_id(const nc_event_id& event_id); + nc_event_id parse_nc_event_id(const web::json::value& event_id); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmethodid + web::json::value make_nc_method_id(const nc_method_id& method_id); + nc_method_id parse_nc_method_id(const web::json::value& method_id); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertyid + web::json::value make_nc_property_id(const nc_property_id& property_id); + nc_property_id parse_nc_property_id(const web::json::value& property_id); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassid + web::json::value make_nc_class_id(const nc_class_id& class_id); + nc_class_id parse_nc_class_id(const web::json::array& class_id); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmanufacturer + web::json::value make_nc_manufacturer(const utility::string_t& name, nc_organization_id organization_id, const web::uri& website); + web::json::value make_nc_manufacturer(const utility::string_t& name, nc_organization_id organization_id); + web::json::value make_nc_manufacturer(const utility::string_t& name); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncproduct + web::json::value make_nc_product(const utility::string_t& name, const utility::string_t& key, const utility::string_t& revision_level, + const utility::string_t& brand_name, const nc_uuid& uuid, const utility::string_t& description); + web::json::value make_nc_product(const utility::string_t& name, const utility::string_t& key, const utility::string_t& revision_level, + const utility::string_t& brand_name, const nc_uuid& uuid); + web::json::value make_nc_product(const utility::string_t& name, const utility::string_t& key, const utility::string_t& revision_level, + const utility::string_t& brand_name); + web::json::value make_nc_product(const utility::string_t& name, const utility::string_t& key, const utility::string_t& revision_level); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdeviceoperationalstate + // device_specific_details can be null + web::json::value make_nc_device_operational_state(nc_device_generic_state::state generic_state, const web::json::value& device_specific_details); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncblockmemberdescriptor + web::json::value make_nc_block_member_descriptor(const utility::string_t& description, const utility::string_t& role, nc_oid oid, bool constant_oid, const nc_class_id& class_id, const utility::string_t& user_label, nc_oid owner); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassdescriptor + web::json::value make_nc_class_descriptor(const utility::string_t& description, const nc_class_id& class_id, const nc_name& name, const utility::string_t& fixed_role, const web::json::value& properties, const web::json::value& methods, const web::json::value& events); + web::json::value make_nc_class_descriptor(const utility::string_t& description, const nc_class_id& class_id, const nc_name& name, const web::json::value& properties, const web::json::value& methods, const web::json::value& events); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncenumitemdescriptor + web::json::value make_nc_enum_item_descriptor(const utility::string_t& description, const nc_name& name, uint16_t val); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nceventdescriptor + web::json::value make_nc_event_descriptor(const utility::string_t& description, const nc_event_id& id, const nc_name& name, const utility::string_t& event_datatype, bool is_deprecated); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncfielddescriptor + // constraints can be null + web::json::value make_nc_field_descriptor(const utility::string_t& description, const nc_name& name, const utility::string_t& type_name, bool is_nullable, bool is_sequence, const web::json::value& constraints); + web::json::value make_nc_field_descriptor(const utility::string_t& description, const nc_name& name, bool is_nullable, bool is_sequence, const web::json::value& constraints); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmethoddescriptor + // sequence parameters + web::json::value make_nc_method_descriptor(const utility::string_t& description, const nc_method_id& id, const nc_name& name, const utility::string_t& result_datatype, const web::json::value& parameters, bool is_deprecated); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncparameterdescriptor + // constraints can be null + web::json::value make_nc_parameter_descriptor(const utility::string_t& description, const nc_name& name, bool is_nullable, bool is_sequence, const web::json::value& constraints); + web::json::value make_nc_parameter_descriptor(const utility::string_t& description, const nc_name& name, const utility::string_t& type_name, bool is_nullable, bool is_sequence, const web::json::value& constraints); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertydescriptor + // constraints can be null + web::json::value make_nc_property_descriptor(const utility::string_t& description, const nc_property_id& id, const nc_name& name, const utility::string_t& type_name, + bool is_read_only, bool is_nullable, bool is_sequence, bool is_deprecated, const web::json::value& constraints); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdatatypedescriptorenum + // constraints can be null + // items: sequence + web::json::value make_nc_datatype_descriptor_enum(const utility::string_t& description, const nc_name& name, const web::json::value& items, const web::json::value& constraints); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdatatypedescriptorprimitive + // constraints can be null + web::json::value make_nc_datatype_descriptor_primitive(const utility::string_t& description, const nc_name& name, const web::json::value& constraints); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdatatypedescriptorstruct + // constraints can be null + // fields: sequence + web::json::value make_nc_datatype_descriptor_struct(const utility::string_t& description, const nc_name& name, const web::json::value& fields, const utility::string_t& parent_type, const web::json::value& constraints); + web::json::value make_nc_datatype_descriptor_struct(const utility::string_t& description, const nc_name& name, const web::json::value& fields, const web::json::value& constraints); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdatatypedescriptortypedef + web::json::value make_nc_datatype_typedef(const utility::string_t& description, const nc_name& name, bool is_sequence, const utility::string_t& parent_type, const web::json::value& constraints); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertyconstraints + web::json::value make_nc_property_constraints(const nc_property_id& property_id, const web::json::value& default_value); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertyconstraintsnumber + web::json::value make_nc_property_constraints_number(const nc_property_id& property_id, uint64_t default_value, uint64_t minimum, uint64_t maximum, uint64_t step); + web::json::value make_nc_property_constraints_number(const nc_property_id& property_id, uint64_t minimum, uint64_t maximum, uint64_t step); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertyconstraintsstring + web::json::value make_nc_property_constraints_string(const nc_property_id& property_id, const utility::string_t& default_value, uint32_t max_characters, const nc_regex& pattern); + web::json::value make_nc_property_constraints_string(const nc_property_id& property_id, uint32_t max_characters, const nc_regex& pattern); + web::json::value make_nc_property_constraints_string(const nc_property_id& property_id, uint32_t max_characters); + web::json::value make_nc_property_constraints_string(const nc_property_id& property_id, const nc_regex& pattern); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncparameterconstraints + web::json::value make_nc_parameter_constraints(const web::json::value& default_value); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncparameterconstraintsnumber + web::json::value make_nc_parameter_constraints_number(uint64_t default_value, uint64_t minimum, uint64_t maximum, uint64_t step); + web::json::value make_nc_parameter_constraints_number(uint64_t minimum, uint64_t maximum, uint64_t step); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncparameterconstraintsstring + web::json::value make_nc_parameter_constraints_string(const utility::string_t& default_value, uint32_t max_characters, const nc_regex& pattern); + web::json::value make_nc_parameter_constraints_string(uint32_t max_characters, const nc_regex& pattern); + web::json::value make_nc_parameter_constraints_string(uint32_t max_characters); + web::json::value make_nc_parameter_constraints_string(const nc_regex& pattern); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpoint + web::json::value make_nc_touchpoint(const utility::string_t& context_namespace); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpointnmos + web::json::value make_nc_touchpoint_nmos(const nc_touchpoint_resource_nmos& resource); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpointnmoschannelmapping + web::json::value make_nc_touchpoint_nmos_channel_mapping(const nc_touchpoint_resource_nmos_channel_mapping& resource); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncobject + web::json::value make_nc_object(const nc_class_id& class_id, nc_oid oid, bool constant_oid, const web::json::value& owner, const utility::string_t& role, const web::json::value& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncblock + web::json::value make_nc_block(const nc_class_id& class_id, nc_oid oid, bool constant_oid, const web::json::value& owner, const utility::string_t& role, const web::json::value& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, bool enabled, const web::json::value& members); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncworker + web::json::value make_nc_worker(const nc_class_id& class_id, nc_oid oid, bool constant_oid, nc_oid owner, const utility::string_t& role, const web::json::value& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, bool enabled); + + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitor + web::json::value make_receiver_monitor(const nc_class_id& class_id, nc_oid oid, bool constant_oid, nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, bool enabled, + nc_connection_status::status connection_status, const utility::string_t& connection_status_message, nc_payload_status::status payload_status, const utility::string_t& payload_status_message); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmanager + web::json::value make_nc_manager(const nc_class_id& class_id, nc_oid oid, bool constant_oid, const web::json::value& owner, const utility::string_t& role, const web::json::value& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicemanager + web::json::value make_nc_device_manager(nc_oid oid, nc_oid owner, const web::json::value& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, + const web::json::value& manufacturer, const web::json::value& product, const utility::string_t& serial_number, + const web::json::value& user_inventory_code, const web::json::value& device_name, const web::json::value& device_role, const web::json::value& operational_state, nc_reset_cause::cause reset_cause); + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassmanager + web::json::value make_nc_class_manager(nc_oid oid, nc_oid owner, const web::json::value& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, const nmos::experimental::control_protocol_state& control_protocol_state); + } + + // command message response + // See https://specs.amwa.tv/is-12/branches/v1.0.x/docs/Protocol_messaging.html#command-response-message-type + web::json::value make_control_protocol_response(int32_t handle, const web::json::value& method_result); + web::json::value make_control_protocol_command_response(const web::json::value& responses); + + // subscription response + // See https://specs.amwa.tv/is-12/branches/v1.0.x/docs/Protocol_messaging.html#subscription-response-message-type + web::json::value make_control_protocol_subscription_response(const web::json::value& subscriptions); + + // notification + // See https://specs.amwa.tv/ms-05-01/branches/v1.0.x/docs/Core_Mechanisms.html#notification-messages + // See https://specs.amwa.tv/is-12/branches/v1.0.x/docs/Protocol_messaging.html#notification-message-type + web::json::value make_control_protocol_notification(nc_oid oid, const nc_event_id& event_id, const nc_property_changed_event_data& property_changed_event_data); + web::json::value make_control_protocol_notification_message(const web::json::value& notifications); + + // property changed notification event + // See https://specs.amwa.tv/ms-05-01/branches/v1.0.x/docs/Core_Mechanisms.html#the-propertychanged-event + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/NcObject.html#propertychanged-event + web::json::value make_property_changed_event(nc_oid oid, const std::vector& property_changed_event_data_list); + + // error message + // See https://specs.amwa.tv/is-12/branches/v1.0.x/docs/Protocol_messaging.html#error-messages + web::json::value make_control_protocol_error_message(const nc_method_result& method_result, const utility::string_t& error_message); + + // Control class models + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/#control-class-models-for-branch-v10-dev + // + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.html + web::json::value make_nc_object_class(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.1.html + web::json::value make_nc_block_class(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.2.html + web::json::value make_nc_worker_class(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.3.html + web::json::value make_nc_manager_class(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.3.1.html + web::json::value make_nc_device_manager_class(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.3.2.html + web::json::value make_nc_class_manager_class(); + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/identification/#ncidentbeacon + web::json::value make_nc_ident_beacon_class(); + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitor + web::json::value make_nc_receiver_monitor_class(); + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitorprotected + web::json::value make_nc_receiver_monitor_protected_class(); + + // control classes proprties/methods/events + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncobject + web::json::value make_nc_object_properties(); + web::json::value make_nc_object_methods(); + web::json::value make_nc_object_events(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncblock + web::json::value make_nc_block_properties(); + web::json::value make_nc_block_methods(); + web::json::value make_nc_block_events(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncworker + web::json::value make_nc_worker_properties(); + web::json::value make_nc_worker_methods(); + web::json::value make_nc_worker_events(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmanager + web::json::value make_nc_manager_properties(); + web::json::value make_nc_manager_methods(); + web::json::value make_nc_manager_events(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicemanager + web::json::value make_nc_device_manager_properties(); + web::json::value make_nc_device_manager_methods(); + web::json::value make_nc_device_manager_events(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassmanager + web::json::value make_nc_class_manager_properties(); + web::json::value make_nc_class_manager_methods(); + web::json::value make_nc_class_manager_events(); + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitor + web::json::value make_nc_receiver_monitor_properties(); + web::json::value make_nc_receiver_monitor_methods(); + web::json::value make_nc_receiver_monitor_events(); + + // Monitoring feature set control classes + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitorprotected + web::json::value make_nc_receiver_monitor_protected_properties(); + web::json::value make_nc_receiver_monitor_protected_methods(); + web::json::value make_nc_receiver_monitor_protected_events(); + + // Identification feature set control classes + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/identification/#ncidentbeacon + web::json::value make_nc_ident_beacon_properties(); + web::json::value make_nc_ident_beacon_methods(); + web::json::value make_nc_ident_beacon_events(); + + // Datatype models + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/#datatype-models-for-branch-v10-dev + // + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_boolean_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_int16_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_int32_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_int64_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_uint16_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_uint32_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_uint64_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_float32_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_float64_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#primitives + web::json::value make_nc_string_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcBlockMemberDescriptor.html + web::json::value make_nc_block_member_descriptor_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcClassDescriptor.html + web::json::value make_nc_class_descriptor_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcClassId.html + web::json::value make_nc_class_id_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDatatypeDescriptor.html + web::json::value make_nc_datatype_descriptor_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDatatypeDescriptorEnum.html + web::json::value make_nc_datatype_descriptor_enum_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDatatypeDescriptorPrimitive.html + web::json::value make_nc_datatype_descriptor_primitive_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDatatypeDescriptorStruct.html + web::json::value make_nc_datatype_descriptor_struct_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDatatypeDescriptorTypeDef.html + web::json::value make_nc_datatype_descriptor_type_def_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDatatypeType.html + web::json::value make_nc_datatype_type_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDescriptor.html + web::json::value make_nc_descriptor_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDeviceGenericState.html + web::json::value make_nc_device_generic_state_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDeviceOperationalState.html + web::json::value make_nc_device_operational_state_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcElementId.html + web::json::value make_nc_element_id_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcEnumItemDescriptor.html + web::json::value make_nc_enum_item_descriptor_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcEventDescriptor.html + web::json::value make_nc_event_descriptor_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcEventId.html + web::json::value make_nc_event_id_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcFieldDescriptor.html + web::json::value make_nc_field_descriptor_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcId.html + web::json::value make_nc_id_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcManufacturer.html + web::json::value make_nc_manufacturer_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodDescriptor.html + web::json::value make_nc_method_descriptor_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodId.html + web::json::value make_nc_method_id_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResult.html + web::json::value make_nc_method_result_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultBlockMemberDescriptors.html + web::json::value make_nc_method_result_block_member_descriptors_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultClassDescriptor.html + web::json::value make_nc_method_result_class_descriptor_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultDatatypeDescriptor.html + web::json::value make_nc_method_result_datatype_descriptor_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultError.html + web::json::value make_nc_method_result_error_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultId.html + web::json::value make_nc_method_result_id_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultLength.html + web::json::value make_nc_method_result_length_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodResultPropertyValue.html + web::json::value make_nc_method_result_property_value_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcMethodStatus.html + web::json::value make_nc_method_status_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcName.html + web::json::value make_nc_name_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcOid.html + web::json::value make_nc_oid_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcOrganizationId.html + web::json::value make_nc_organization_id_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcParameterConstraints.html + web::json::value make_nc_parameter_constraints_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcParameterConstraintsNumber.html + web::json::value make_nc_parameter_constraints_number_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcParameterConstraintsString.html + web::json::value make_nc_parameter_constraints_string_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcParameterDescriptor.html + web::json::value make_nc_parameter_descriptor_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcProduct.html + web::json::value make_nc_product_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyChangeType.html + web::json::value make_nc_property_change_type_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyChangedEventData.html + web::json::value make_nc_property_changed_event_data_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyConstraints.html + web::json::value make_nc_property_contraints_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyConstraintsNumber.html + web::json::value make_nc_property_constraints_number_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyConstraintsString.html + web::json::value make_nc_property_constraints_string_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyDescriptor.html + web::json::value make_nc_property_descriptor_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcPropertyId.html + web::json::value make_nc_property_id_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcRegex.html + web::json::value make_nc_regex_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcResetCause.html + web::json::value make_nc_reset_cause_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcRolePath.html + web::json::value make_nc_role_path_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTimeInterval.html + web::json::value make_nc_time_interval_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTouchpoint.html + web::json::value make_nc_touchpoint_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTouchpointNmos.html + web::json::value make_nc_touchpoint_nmos_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTouchpointNmosChannelMapping.html + web::json::value make_nc_touchpoint_nmos_channel_mapping_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTouchpointResource.html + web::json::value make_nc_touchpoint_resource_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTouchpointResourceNmos.html + web::json::value make_nc_touchpoint_resource_nmos_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcTouchpointResourceNmosChannelMapping.html + web::json::value make_nc_touchpoint_resource_nmos_channel_mapping_datatype(); + // See // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcUri.html + web::json::value make_nc_uri_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcUuid.html + web::json::value make_nc_uuid_datatype(); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcVersionCode.html + web::json::value make_nc_version_code_datatype(); + + // Monitoring feature set datatypes + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#datatypes + // + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncconnectionstatus + web::json::value make_nc_connection_status_datatype(); + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncpayloadstatus + web::json::value make_nc_payload_status_datatype(); +} + +#endif diff --git a/Development/nmos/control_protocol_resources.cpp b/Development/nmos/control_protocol_resources.cpp new file mode 100644 index 000000000..0fde54c33 --- /dev/null +++ b/Development/nmos/control_protocol_resources.cpp @@ -0,0 +1,101 @@ +#include "nmos/control_protocol_resources.h" + +#include "nmos/control_protocol_resource.h" +#include "nmos/control_protocol_utils.h" +#include "nmos/is12_versions.h" + +namespace nmos +{ + namespace details + { + // create block resource + control_protocol_resource make_block(nmos::nc_oid oid, const web::json::value& owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, const web::json::value& members) + { + using web::json::value; + + auto data = details::make_nc_block(nc_block_class_id, oid, true, owner, role, value::string(user_label), description, touchpoints, runtime_property_constraints, true, members); + + return{ is12_versions::v1_0, types::nc_block, std::move(data), true }; + } + } + + // create block resource + control_protocol_resource make_block(nc_oid oid, nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, const web::json::value& members) + { + using web::json::value; + + return details::make_block(oid, value(owner), role, user_label, description, touchpoints, runtime_property_constraints, members); + } + + // create Root block resource + control_protocol_resource make_root_block() + { + using web::json::value; + + return details::make_block(1, value::null(), U("root"), U("Root"), U("Root block"), value::null(), value::null(), value::array()); + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicemanager + control_protocol_resource make_device_manager(nc_oid oid, const nmos::settings& settings) + { + using web::json::value; + + const auto& manufacturer = details::make_nc_manufacturer(nmos::experimental::fields::manufacturer_name(settings)); + const auto& product = details::make_nc_product(nmos::experimental::fields::product_name(settings), nmos::experimental::fields::product_key(settings), nmos::experimental::fields::product_key(settings)); + const auto& serial_number = nmos::experimental::fields::serial_number(settings); + const auto device_name = value::null(); + const auto device_role = value::null(); + const auto& operational_state = details::make_nc_device_operational_state(nc_device_generic_state::normal_operation, value::null()); + + auto data = details::make_nc_device_manager(oid, root_block_oid, value::string(U("Device manager")), U("The device manager offers information about the product this device is representing"), value::null(), value::null(), + manufacturer, product, serial_number, value::null(), device_name, device_role, operational_state, nc_reset_cause::unknown); + + return{ is12_versions::v1_0, types::nc_device_manager, std::move(data), true }; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassmanager + control_protocol_resource make_class_manager(nc_oid oid, const nmos::experimental::control_protocol_state& control_protocol_state) + { + using web::json::value; + + auto data = details::make_nc_class_manager(oid, root_block_oid, value::string(U("Class manager")), U("The class manager offers access to control class and data type descriptors"), value::null(), value::null(), control_protocol_state); + + return{ is12_versions::v1_0, types::nc_class_manager, std::move(data), true }; + } + + // Monitoring feature set control classes + // + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitor + control_protocol_resource make_receiver_monitor(nc_oid oid, bool constant_oid, nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, bool enabled, + nc_connection_status::status connection_status, const utility::string_t& connection_status_message, nc_payload_status::status payload_status, const utility::string_t& payload_status_message) + { + auto data = details::make_receiver_monitor(nc_receiver_monitor_class_id, oid, constant_oid, owner, role, user_label, description, touchpoints, runtime_property_constraints, enabled, connection_status, connection_status_message, payload_status, payload_status_message); + + return{ is12_versions::v1_0, types::nc_receiver_monitor, std::move(data), true }; + } + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitorprotected + control_protocol_resource make_receiver_monitor_protected(nc_oid oid, bool constant_oid, nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, bool enabled, + nc_connection_status::status connection_status, const utility::string_t& connection_status_message, nc_payload_status::status payload_status, const utility::string_t& payload_status_message, bool signal_protection_status) + { + using web::json::value; + + auto data = details::make_receiver_monitor(nc_receiver_monitor_protected_class_id, oid, constant_oid, owner, role, user_label, description, touchpoints, runtime_property_constraints, enabled, connection_status, connection_status_message, payload_status, payload_status_message); + data[nmos::fields::nc::signal_protection_status] = value::boolean(signal_protection_status); + + return{ is12_versions::v1_0, types::nc_receiver_monitor_protected, std::move(data), true }; + } + + // Identification feature set control classes + // + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/identification/#ncidentbeacon + control_protocol_resource make_ident_beacon(nc_oid oid, bool constant_oid, nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints, bool enabled, + bool active) + { + using web::json::value; + + auto data = nmos::details::make_nc_worker(nc_ident_beacon_class_id, oid, constant_oid, owner, role, value::string(user_label), description, touchpoints, runtime_property_constraints, enabled); + data[nmos::fields::nc::active] = value::boolean(active); + + return{ is12_versions::v1_0, types::nc_ident_beacon, std::move(data), true }; + } +} diff --git a/Development/nmos/control_protocol_resources.h b/Development/nmos/control_protocol_resources.h new file mode 100644 index 000000000..4ad7d85da --- /dev/null +++ b/Development/nmos/control_protocol_resources.h @@ -0,0 +1,54 @@ +#ifndef NMOS_CONTROL_PROTOCOL_RESOURCES_H +#define NMOS_CONTROL_PROTOCOL_RESOURCES_H + +#include "nmos/control_protocol_typedefs.h" // for details::nc_oid definition +#include "nmos/settings.h" + +namespace nmos +{ + namespace experimental + { + struct control_protocol_state; + } + + struct control_protocol_resource; + + // create block resource + control_protocol_resource make_block(nc_oid oid, nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints = web::json::value::null(), const web::json::value& runtime_property_constraints = web::json::value::null(), const web::json::value& members = web::json::value::array()); + + // create Root block resource + control_protocol_resource make_root_block(); + + // create Device manager resource + control_protocol_resource make_device_manager(nc_oid oid, const nmos::settings& settings); + + // create Class manager resource + control_protocol_resource make_class_manager(nc_oid oid, const nmos::experimental::control_protocol_state& control_protocol_state); + + // Monitoring feature set control classes + // + // create Receiver Monitor resource + control_protocol_resource make_receiver_monitor(nc_oid oid, bool constant_oid, nmos::nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints = web::json::value::null(), const web::json::value& runtime_property_constraints = web::json::value::null(), bool enabled = true, + nc_connection_status::status connection_status = nc_connection_status::status::undefined, + const utility::string_t& connection_status_message = U(""), + nc_payload_status::status payload_status = nc_payload_status::status::undefined, + const utility::string_t& payload_status_message = U("") + ); + // create Receiver Monitor Protected resource + control_protocol_resource make_receiver_monitor_protected(nc_oid oid, bool constant_oid, nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints, const web::json::value& runtime_property_constraints = web::json::value::null(), bool enabled = true, + nc_connection_status::status connection_status = nc_connection_status::status::undefined, + const utility::string_t& connection_status_message = U(""), + nc_payload_status::status payload_status = nc_payload_status::status::undefined, + const utility::string_t& payload_status_message = U(""), + bool signal_protection_status = true + ); + + // Identification feature set control classes + // + // create Ident Beacon resource + control_protocol_resource make_ident_beacon(nc_oid oid, bool constant_oid, nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description, const web::json::value& touchpoints = web::json::value::null(), const web::json::value& runtime_property_constraints = web::json::value::null(), bool enabled = true, + bool active = false + ); +} + +#endif diff --git a/Development/nmos/control_protocol_state.cpp b/Development/nmos/control_protocol_state.cpp new file mode 100644 index 000000000..11f91662b --- /dev/null +++ b/Development/nmos/control_protocol_state.cpp @@ -0,0 +1,454 @@ +#include "nmos/control_protocol_state.h" + +#include "nmos/control_protocol_methods.h" +#include "nmos/control_protocol_resource.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + // create control class descriptor + // where + // properties: vector of NcPropertyDescriptor where NcPropertyDescriptor can be constructed using make_control_class_property + // methods: vector of NcMethodDescriptor vs assoicated method handler where NcMethodDescriptor can be constructed using make_nc_method_descriptor + // events: vector of NcEventDescriptor where NcEventDescriptor can be constructed using make_nc_event_descriptor + control_class_descriptor make_control_class_descriptor(const utility::string_t& description, const nc_class_id& class_id, const nc_name& name, const web::json::value& fixed_role, const std::vector& properties_, const std::vector& methods_, const std::vector& events_) + { + using web::json::value; + + web::json::value properties = value::array(); + for (const auto& property : properties_) { web::json::push_back(properties, property); } + web::json::value events = value::array(); + for (const auto& event : events_) { web::json::push_back(events, event); } + + return { description, class_id, name, fixed_role, properties, methods_, events }; + } + } + // create control class descriptor with fixed role + // where + // properties: vector of NcPropertyDescriptor where NcPropertyDescriptor can be constructed using make_control_class_property + // methods: vector of NcMethodDescriptor where NcMethodDescriptor can be constructed using make_nc_method_descriptor and the assoicated method handler + // events: vector of NcEventDescriptor where NcEventDescriptor can be constructed using make_nc_event_descriptor + control_class_descriptor make_control_class_descriptor(const utility::string_t& description, const nc_class_id& class_id, const nc_name& name, const utility::string_t& fixed_role, const std::vector& properties, const std::vector& methods, const std::vector& events) + { + using web::json::value; + + return details::make_control_class_descriptor(description, class_id, name, value::string(fixed_role), properties, methods, events); + } + // create control class descriptor without fixed role + // where + // properties: vector of NcPropertyDescriptor where NcPropertyDescriptor can be constructed using make_control_class_property + // methods: vector of NcMethodDescriptor where NcMethodDescriptor can be constructed using make_nc_method_descriptor and the assoicated method handler + // events: vector of NcEventDescriptor where NcEventDescriptor can be constructed using make_nc_event_descriptor + control_class_descriptor make_control_class_descriptor(const utility::string_t& description, const nc_class_id& class_id, const nc_name& name, const std::vector& properties, const std::vector& methods, const std::vector& events) + { + using web::json::value; + + return details::make_control_class_descriptor(description, class_id, name, value::null(), properties, methods, events); + } + + // create control class property descriptor + web::json::value make_control_class_property_descriptor(const utility::string_t& description, const nc_property_id& id, const nc_name& name, const utility::string_t& type_name, bool is_read_only, bool is_nullable, bool is_sequence, bool is_deprecated, const web::json::value& constraints) + { + return nmos::details::make_nc_property_descriptor(description, id, name, type_name, is_read_only, is_nullable, is_sequence, is_deprecated, constraints); + } + + // create control class method parameter descriptor + web::json::value make_control_class_method_parameter_descriptor(const utility::string_t& description, const nc_name& name, const utility::string_t& type_name, bool is_nullable, bool is_sequence, const web::json::value& constraints) + { + return nmos::details::make_nc_parameter_descriptor(description, name, type_name, is_nullable, is_sequence, constraints); + } + + namespace details + { + web::json::value make_control_class_method_descriptor(const utility::string_t& description, const nc_method_id& id, const nc_name& name, const utility::string_t& result_datatype, const std::vector& parameters_, bool is_deprecated) + { + using web::json::value; + + value parameters = value::array(); + for (const auto& parameter : parameters_) { web::json::push_back(parameters, parameter); } + + return nmos::details::make_nc_method_descriptor(description, id, name, result_datatype, parameters, is_deprecated); + } + } + // create control class method descriptor + method make_control_class_method_descriptor(const utility::string_t& description, const nc_method_id& id, const nc_name& name, const utility::string_t& result_datatype, const std::vector& parameters, bool is_deprecated, control_protocol_method_handler method_handler) + { + return make_control_class_method(details::make_control_class_method_descriptor(description, id, name, result_datatype, parameters, is_deprecated), method_handler); + } + + // create control class event descriptor + web::json::value make_control_class_event_descriptor(const utility::string_t& description, const nc_event_id& id, const nc_name& name, const utility::string_t& event_datatype, bool is_deprecated) + { + return nmos::details::make_nc_event_descriptor(description, id, name, event_datatype, is_deprecated); + } + + namespace details + { + nmos::experimental::control_protocol_method_handler make_nc_get_handler(get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor) + { + return [get_control_protocol_class_descriptor](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return get(resources, resource, arguments, is_deprecated, get_control_protocol_class_descriptor, gate); + }; + } + nmos::experimental::control_protocol_method_handler make_nc_set_handler(get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, control_protocol_property_changed_handler property_changed) + { + return [get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor, property_changed](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return set(resources, resource, arguments, is_deprecated, get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor, property_changed, gate); + }; + } + nmos::experimental::control_protocol_method_handler make_nc_get_sequence_item_handler(get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor) + { + return [get_control_protocol_class_descriptor](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return get_sequence_item(resources, resource, arguments, is_deprecated, get_control_protocol_class_descriptor, gate); + }; + } + nmos::experimental::control_protocol_method_handler make_nc_set_sequence_item_handler(get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, control_protocol_property_changed_handler property_changed) + { + return [get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor, property_changed](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return set_sequence_item(resources, resource, arguments, is_deprecated, get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor, property_changed, gate); + }; + } + nmos::experimental::control_protocol_method_handler make_nc_add_sequence_item_handler(get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, control_protocol_property_changed_handler property_changed) + { + return [get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor, property_changed](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return add_sequence_item(resources, resource, arguments, is_deprecated, get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor, property_changed, gate); + }; + } + nmos::experimental::control_protocol_method_handler make_nc_remove_sequence_item_handler(get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, control_protocol_property_changed_handler property_changed) + { + return [get_control_protocol_class_descriptor, property_changed](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return remove_sequence_item(resources, resource, arguments, is_deprecated, get_control_protocol_class_descriptor, property_changed, gate); + }; + } + nmos::experimental::control_protocol_method_handler make_nc_get_sequence_length_handler(get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor) + { + return [get_control_protocol_class_descriptor](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return get_sequence_length(resources, resource, arguments, is_deprecated, get_control_protocol_class_descriptor, gate); + }; + } + nmos::experimental::control_protocol_method_handler make_nc_get_member_descriptors_handler() + { + return [](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return get_member_descriptors(resources, resource, arguments, is_deprecated, gate); + }; + } + nmos::experimental::control_protocol_method_handler make_nc_find_members_by_path_handler() + { + return [](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return find_members_by_path(resources, resource, arguments, is_deprecated, gate); + }; + } + nmos::experimental::control_protocol_method_handler make_nc_find_members_by_role_handler() + { + return [](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return find_members_by_role(resources, resource, arguments, is_deprecated, gate); + }; + } + nmos::experimental::control_protocol_method_handler make_nc_find_members_by_class_id_handler() + { + return [](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return find_members_by_class_id(resources, resource, arguments, is_deprecated, gate); + }; + } + nmos::experimental::control_protocol_method_handler make_nc_get_control_class_handler(get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor) + { + return [get_control_protocol_class_descriptor](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return get_control_class(resources, resource, arguments, is_deprecated, get_control_protocol_class_descriptor, gate); + }; + } + nmos::experimental::control_protocol_method_handler make_nc_get_datatype_handler(get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor) + { + return [get_control_protocol_datatype_descriptor](nmos::resources& resources, const nmos::resource& resource, const web::json::value& arguments, bool is_deprecated, slog::base_gate& gate) + { + return get_datatype(resources, resource, arguments, is_deprecated, get_control_protocol_datatype_descriptor, gate); + }; + } + } + + control_protocol_state::control_protocol_state(control_protocol_property_changed_handler property_changed) + { + using web::json::value; + + auto to_vector = [](const web::json::value& data) + { + if (!data.is_null()) + { + return std::vector(data.as_array().begin(), data.as_array().end()); + } + return std::vector{}; + }; + + auto to_methods_vector = [](const web::json::value& nc_method_descriptors, const std::map& method_handlers) + { + // NcMethodDescriptor method handler array + std::vector methods; + + if (!nc_method_descriptors.is_null()) + { + for (const auto& nc_method_descriptor : nc_method_descriptors.as_array()) + { + methods.push_back(make_control_class_method(nc_method_descriptor, method_handlers.at(nmos::details::parse_nc_method_id(nmos::fields::nc::id(nc_method_descriptor))))); + } + } + return methods; + }; + + auto get_control_protocol_class_descriptor = make_get_control_protocol_class_descriptor_handler(*this); + auto get_control_protocol_datatype_descriptor = make_get_control_protocol_datatype_descriptor_handler(*this); + + // setup the standard control classes + control_class_descriptors = + { + // Control class models + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/ + + // NcObject + { nc_object_class_id, make_control_class_descriptor(U("NcObject class descriptor"), nc_object_class_id, U("NcObject"), + // NcObject properties + to_vector(make_nc_object_properties()), + // NcObject methods + to_methods_vector(make_nc_object_methods(), + { + // link NcObject method_ids with method functions + { nc_object_get_method_id, details::make_nc_get_handler(get_control_protocol_class_descriptor) }, + { nc_object_set_method_id, details::make_nc_set_handler(get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor, property_changed) }, + { nc_object_get_sequence_item_method_id, details::make_nc_get_sequence_item_handler(get_control_protocol_class_descriptor) }, + { nc_object_set_sequence_item_method_id, details::make_nc_set_sequence_item_handler(get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor, property_changed) }, + { nc_object_add_sequence_item_method_id, details::make_nc_add_sequence_item_handler(get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor, property_changed) }, + { nc_object_remove_sequence_item_method_id, details::make_nc_remove_sequence_item_handler(get_control_protocol_class_descriptor, property_changed) }, + { nc_object_get_sequence_length_method_id, details::make_nc_get_sequence_length_handler(get_control_protocol_class_descriptor) } + }), + // NcObject events + to_vector(make_nc_object_events())) }, + // NcBlock + { nc_block_class_id, make_control_class_descriptor(U("NcBlock class descriptor"), nc_block_class_id, U("NcBlock"), + // NcBlock properties + to_vector(make_nc_block_properties()), + // NcBlock methods + to_methods_vector(make_nc_block_methods(), + { + // link NcBlock method_ids with method functions + { nc_block_get_member_descriptors_method_id, details::make_nc_get_member_descriptors_handler() }, + { nc_block_find_members_by_path_method_id, details::make_nc_find_members_by_path_handler() }, + { nc_block_find_members_by_role_method_id, details::make_nc_find_members_by_role_handler() }, + { nc_block_find_members_by_class_id_method_id, details::make_nc_find_members_by_class_id_handler() } + }), + // NcBlock events + to_vector(make_nc_block_events())) }, + // NcWorker + { nc_worker_class_id, make_control_class_descriptor(U("NcWorker class descriptor"), nc_worker_class_id, U("NcWorker"), + // NcWorker properties + to_vector(make_nc_worker_properties()), + // NcWorker methods + to_methods_vector(make_nc_worker_methods(), {}), + // NcWorker events + to_vector(make_nc_worker_events())) }, + // NcManager + { nc_manager_class_id, make_control_class_descriptor(U("NcManager class descriptor"), nc_manager_class_id, U("NcManager"), + // NcManager properties + to_vector(make_nc_manager_properties()), + // NcManager methods + to_methods_vector(make_nc_manager_methods(), {}), + // NcManager events + to_vector(make_nc_manager_events())) }, + // NcDeviceManager + { nc_device_manager_class_id, make_control_class_descriptor(U("NcDeviceManager class descriptor"), nc_device_manager_class_id, U("NcDeviceManager"), U("DeviceManager"), + // NcDeviceManager properties + to_vector(make_nc_device_manager_properties()), + // NcDeviceManager methods + to_methods_vector(make_nc_device_manager_methods(), {}), + // NcDeviceManager events + to_vector(make_nc_device_manager_events())) }, + // NcClassManager + { nc_class_manager_class_id, make_control_class_descriptor(U("NcClassManager class descriptor"), nc_class_manager_class_id, U("NcClassManager"), U("ClassManager"), + // NcClassManager properties + to_vector(make_nc_class_manager_properties()), + // NcClassManager methods + to_methods_vector(make_nc_class_manager_methods(), + { + // link NcClassManager method_ids with method functions + { nc_class_manager_get_control_class_method_id, details::make_nc_get_control_class_handler(get_control_protocol_class_descriptor) }, + { nc_class_manager_get_datatype_method_id, details::make_nc_get_datatype_handler(get_control_protocol_datatype_descriptor) } + }), + // NcClassManager events + to_vector(make_nc_class_manager_events())) }, + // Identification feature set + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/identification/#control-classes + // NcIdentBeacon + { nc_ident_beacon_class_id, make_control_class_descriptor(U("NcIdentBeacon class descriptor"), nc_ident_beacon_class_id, U("NcIdentBeacon"), + // NcIdentBeacon properties + to_vector(make_nc_ident_beacon_properties()), + // NcIdentBeacon methods + to_methods_vector(make_nc_ident_beacon_methods(), {}), + // NcIdentBeacon events + to_vector(make_nc_ident_beacon_events())) }, + // Monitoring feature set + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#control-classes + // NcReceiverMonitor + { nc_receiver_monitor_class_id, make_control_class_descriptor(U("NcReceiverMonitor class descriptor"), nc_receiver_monitor_class_id, U("NcReceiverMonitor"), + // NcReceiverMonitor properties + to_vector(make_nc_receiver_monitor_properties()), + // NcReceiverMonitor methods + to_methods_vector(make_nc_receiver_monitor_methods(), {}), + // NcReceiverMonitor events + to_vector(make_nc_receiver_monitor_events())) }, + // NcReceiverMonitorProtected + { nc_receiver_monitor_protected_class_id, make_control_class_descriptor(U("NcReceiverMonitorProtected class descriptor"), nc_receiver_monitor_protected_class_id, U("NcReceiverMonitorProtected"), + // NcReceiverMonitorProtected properties + to_vector(make_nc_receiver_monitor_protected_properties()), + // NcReceiverMonitorProtected methods + to_methods_vector(make_nc_receiver_monitor_protected_methods(), {}), + // NcReceiverMonitorProtected events + to_vector(make_nc_receiver_monitor_protected_events())) } + }; + + // setup the standard datatypes + datatype_descriptors = + { + // Datatype models + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/ + { U("NcBoolean"), {make_nc_boolean_datatype()} }, + { U("NcInt16"), {make_nc_int16_datatype()} }, + { U("NcInt32"), {make_nc_int32_datatype()} }, + { U("NcInt64"), {make_nc_int64_datatype()} }, + { U("NcUint16"), {make_nc_uint16_datatype()} }, + { U("NcUint32"), {make_nc_uint32_datatype()} }, + { U("NcUint64"), {make_nc_uint64_datatype()} }, + { U("NcFloat32"), {make_nc_float32_datatype()} }, + { U("NcFloat64"), {make_nc_float64_datatype()} }, + { U("NcString"), {make_nc_string_datatype()} }, + { U("NcClassId"), {make_nc_class_id_datatype()} }, + { U("NcOid"), {make_nc_oid_datatype()} }, + { U("NcTouchpoint"), {make_nc_touchpoint_datatype()} }, + { U("NcElementId"), {make_nc_element_id_datatype()} }, + { U("NcPropertyId"), {make_nc_property_id_datatype()} }, + { U("NcPropertyConstraints"), {make_nc_property_contraints_datatype()} }, + { U("NcMethodResultPropertyValue"), {make_nc_method_result_property_value_datatype()} }, + { U("NcMethodStatus"), {make_nc_method_status_datatype()} }, + { U("NcMethodResult"), {make_nc_method_result_datatype()} }, + { U("NcId"), {make_nc_id_datatype()} }, + { U("NcMethodResultId"), {make_nc_method_result_id_datatype()} }, + { U("NcMethodResultLength"), {make_nc_method_result_length_datatype()} }, + { U("NcPropertyChangeType"), {make_nc_property_change_type_datatype()} }, + { U("NcPropertyChangedEventData"), {make_nc_property_changed_event_data_datatype()} }, + { U("NcDescriptor"), {make_nc_descriptor_datatype()} }, + { U("NcBlockMemberDescriptor"), {make_nc_block_member_descriptor_datatype()} }, + { U("NcMethodResultBlockMemberDescriptors"), {make_nc_method_result_block_member_descriptors_datatype()} }, + { U("NcVersionCode"), {make_nc_version_code_datatype()} }, + { U("NcOrganizationId"), {make_nc_organization_id_datatype()} }, + { U("NcUri"), {make_nc_uri_datatype()} }, + { U("NcManufacturer"), {make_nc_manufacturer_datatype()} }, + { U("NcUuid"), {make_nc_uuid_datatype()} }, + { U("NcProduct"), {make_nc_product_datatype()} }, + { U("NcDeviceGenericState"), {make_nc_device_generic_state_datatype()} }, + { U("NcDeviceOperationalState"), {make_nc_device_operational_state_datatype()} }, + { U("NcResetCause"), {make_nc_reset_cause_datatype()} }, + { U("NcName"), {make_nc_name_datatype()} }, + { U("NcPropertyDescriptor"), {make_nc_property_descriptor_datatype()} }, + { U("NcParameterDescriptor"), {make_nc_parameter_descriptor_datatype()} }, + { U("NcMethodId"), {make_nc_method_id_datatype()} }, + { U("NcMethodDescriptor"), {make_nc_method_descriptor_datatype()} }, + { U("NcEventId"), {make_nc_event_id_datatype()} }, + { U("NcEventDescriptor"), {make_nc_event_descriptor_datatype()} }, + { U("NcClassDescriptor"), {make_nc_class_descriptor_datatype()} }, + { U("NcParameterConstraints"), {make_nc_parameter_constraints_datatype()} }, + { U("NcDatatypeType"), {make_nc_datatype_type_datatype()} }, + { U("NcDatatypeDescriptor"), {make_nc_datatype_descriptor_datatype()} }, + { U("NcMethodResultClassDescriptor"), {make_nc_method_result_class_descriptor_datatype()} }, + { U("NcMethodResultDatatypeDescriptor"), {make_nc_method_result_datatype_descriptor_datatype()} }, + { U("NcMethodResultError"), {make_nc_method_result_error_datatype()} }, + { U("NcDatatypeDescriptorEnum"), {make_nc_datatype_descriptor_enum_datatype()} }, + { U("NcDatatypeDescriptorPrimitive"), {make_nc_datatype_descriptor_primitive_datatype()} }, + { U("NcDatatypeDescriptorStruct"), {make_nc_datatype_descriptor_struct_datatype()} }, + { U("NcDatatypeDescriptorTypeDef"), {make_nc_datatype_descriptor_type_def_datatype()} }, + { U("NcEnumItemDescriptor"), {make_nc_enum_item_descriptor_datatype()} }, + { U("NcFieldDescriptor"), {make_nc_field_descriptor_datatype()} }, + { U("NcPropertyConstraintsNumber"), {make_nc_property_constraints_number_datatype()} }, + { U("NcPropertyConstraintsString"), {make_nc_property_constraints_string_datatype()} }, + { U("NcRegex"), {make_nc_regex_datatype()} }, + { U("NcRolePath"), {make_nc_role_path_datatype()} }, + { U("NcParameterConstraintsNumber"), {make_nc_parameter_constraints_number_datatype()} }, + { U("NcParameterConstraintsString"), {make_nc_parameter_constraints_string_datatype()} }, + { U("NcTimeInterval"), {make_nc_time_interval_datatype()} }, + { U("NcTouchpointNmos"), {make_nc_touchpoint_nmos_datatype()} }, + { U("NcTouchpointNmosChannelMapping"), {make_nc_touchpoint_nmos_channel_mapping_datatype()} }, + { U("NcTouchpointResource"), {make_nc_touchpoint_resource_datatype()} }, + { U("NcTouchpointResourceNmos"), {make_nc_touchpoint_resource_nmos_datatype()} }, + { U("NcTouchpointResourceNmosChannelMapping"), {make_nc_touchpoint_resource_nmos_channel_mapping_datatype()} }, + // Monitoring feature set + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#datatypes + { U("NcConnectionStatus"), {make_nc_connection_status_datatype()} }, + { U("NcPayloadStatus"), {make_nc_payload_status_datatype()} } + }; + } + + // insert control class descriptor, false if class descriptor already inserted + bool control_protocol_state::insert(const experimental::control_class_descriptor& control_class_descriptor) + { + auto lock = write_lock(); + + if (control_class_descriptors.end() == control_class_descriptors.find(control_class_descriptor.class_id)) + { + control_class_descriptors[control_class_descriptor.class_id] = control_class_descriptor; + return true; + } + return false; + } + + // erase control class descriptor of the given class id, false if the required class descriptor not found + bool control_protocol_state::erase(nc_class_id class_id) + { + auto lock = write_lock(); + + if (control_class_descriptors.end() != control_class_descriptors.find(class_id)) + { + control_class_descriptors.erase(class_id); + return true; + } + return false; + } + + // insert datatype descriptor, false if datatype descriptor already inserted + bool control_protocol_state::insert(const experimental::datatype_descriptor& datatype_descriptor) + { + const auto& name = nmos::fields::nc::name(datatype_descriptor.descriptor); + + auto lock = write_lock(); + + if (datatype_descriptors.end() == datatype_descriptors.find(name)) + { + datatype_descriptors[name] = datatype_descriptor; + return true; + } + return false; + } + + // erase datatype descriptor of the given datatype name, false if the required datatype descriptor not found + bool control_protocol_state::erase(const utility::string_t& datatype_name) + { + auto lock = write_lock(); + + if (datatype_descriptors.end() != datatype_descriptors.find(datatype_name)) + { + datatype_descriptors.erase(datatype_name); + return true; + } + return false; + } + } +} \ No newline at end of file diff --git a/Development/nmos/control_protocol_state.h b/Development/nmos/control_protocol_state.h new file mode 100644 index 000000000..b49d4e502 --- /dev/null +++ b/Development/nmos/control_protocol_state.h @@ -0,0 +1,99 @@ +#ifndef NMOS_CONTROL_PROTOCOL_STATE_H +#define NMOS_CONTROL_PROTOCOL_STATE_H + +#include +#include "cpprest/json_utils.h" +#include "nmos/control_protocol_handlers.h" +#include "nmos/control_protocol_typedefs.h" +#include "nmos/mutex.h" + +namespace slog { class base_gate; } + +namespace nmos +{ + namespace experimental + { + struct control_class_descriptor // NcClassDescriptor + { + utility::string_t description; + nmos::nc_class_id class_id; + nmos::nc_name name; + web::json::value fixed_role; + + web::json::value property_descriptors = web::json::value::array(); // NcPropertyDescriptor array + std::vector method_descriptors; // NcMethodDescriptor method handler array + web::json::value event_descriptors = web::json::value::array(); // NcEventDescriptor array + + control_class_descriptor() + : class_id({ 0 }) + {} + + control_class_descriptor(utility::string_t description, nmos::nc_class_id class_id, nmos::nc_name name, web::json::value fixed_role, web::json::value property_descriptors, std::vector method_descriptors, web::json::value event_descriptors) + : description(std::move(description)) + , class_id(std::move(class_id)) + , name(std::move(name)) + , fixed_role(std::move(fixed_role)) + , property_descriptors(std::move(property_descriptors)) + , method_descriptors(std::move(method_descriptors)) + , event_descriptors(std::move(event_descriptors)) + {} + }; + + struct datatype_descriptor // NcDatatypeDescriptorEnum/NcDatatypeDescriptorPrimitive/NcDatatypeDescriptorStruct/NcDatatypeDescriptorTypeDef + { + web::json::value descriptor; + }; + + typedef std::map control_class_descriptors; + typedef std::map datatype_descriptors; + + struct control_protocol_state + { + // mutex to be used to protect the members from simultaneous access by multiple threads + mutable nmos::mutex mutex; + + experimental::control_class_descriptors control_class_descriptors; + experimental::datatype_descriptors datatype_descriptors; + + nmos::read_lock read_lock() const { return nmos::read_lock{ mutex }; } + nmos::write_lock write_lock() const { return nmos::write_lock{ mutex }; } + + control_protocol_state(control_protocol_property_changed_handler property_changed); + + // insert control class descriptor, false if class descriptor already inserted + bool insert(const experimental::control_class_descriptor& control_class_descriptor); + // erase control class of the given class id, false if the required class not found + bool erase(nc_class_id class_id); + + // insert datatype descriptor, false if datatype descriptor already inserted + bool insert(const experimental::datatype_descriptor& datatype_descriptor); + // erase datatype descriptor of the given datatype name, false if the required datatype descriptor not found + bool erase(const utility::string_t& datatype_name); + }; + + // helper functions to create non-standard control class + // + + // create control class descriptor with fixed role + control_class_descriptor make_control_class_descriptor(const utility::string_t& description, const nc_class_id& class_id, const nc_name& name, const utility::string_t& fixed_role, const std::vector& properties = {}, const std::vector& methods = {}, const std::vector& events = {}); + // create control class descriptor with no fixed role + control_class_descriptor make_control_class_descriptor(const utility::string_t& description, const nc_class_id& class_id, const nc_name& name, const std::vector& properties = {}, const std::vector& methods = {}, const std::vector& events = {}); + + // create control class property descriptor + web::json::value make_control_class_property_descriptor(const utility::string_t& description, const nc_property_id& id, const nc_name& name, const utility::string_t& type_name, + bool is_read_only = false, bool is_nullable = false, bool is_sequence = false, bool is_deprecated = false, const web::json::value& constraints = web::json::value::null()); + + // create control class method parameter descriptor + web::json::value make_control_class_method_parameter_descriptor(const utility::string_t& description, const nc_name& name, const utility::string_t& type_name, + bool is_nullable = false, bool is_sequence = false, const web::json::value& constraints = web::json::value::null()); + // create control class method descriptor + method make_control_class_method_descriptor(const utility::string_t& description, const nc_method_id& id, const nc_name& name, const utility::string_t& result_datatype, + const std::vector& parameters, bool is_deprecated, control_protocol_method_handler method_handler); + + // create control class event descriptor + web::json::value make_control_class_event_descriptor(const utility::string_t& description, const nc_event_id& id, const nc_name& name, const utility::string_t& event_datatype, + bool is_deprecated = false); + } +} + +#endif diff --git a/Development/nmos/control_protocol_typedefs.h b/Development/nmos/control_protocol_typedefs.h new file mode 100644 index 000000000..7afcefedb --- /dev/null +++ b/Development/nmos/control_protocol_typedefs.h @@ -0,0 +1,386 @@ +#ifndef NMOS_CONTROL_PROTOCOL_TYPEDEFS_H +#define NMOS_CONTROL_PROTOCOL_TYPEDEFS_H + +#include "cpprest/basic_utils.h" +#include "cpprest/json_utils.h" +#include "nmos/control_protocol_nmos_channel_mapping_resource_type.h" +#include "nmos/control_protocol_nmos_resource_type.h" + +namespace nmos +{ + // See https://specs.amwa.tv/is-12/branches/v1.0.x/docs/Protocol_messaging.html + namespace ncp_message_type + { + enum type + { + command = 0, + command_response = 1, + notification = 2, + subscription = 3, + subscription_response = 4, + error = 5 + }; + } + + // Method invokation status + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmethodstatus + namespace nc_method_status + { + enum status + { + ok = 200, // Method call was successful + property_deprecated = 298, // Method call was successful but targeted property is deprecated + method_deprecated = 299, // Method call was successful but method is deprecated + bad_command_format = 400, // Badly-formed command + unauthorized = 401, // Client is not authorized + bad_oid = 404, // Command addresses a nonexistent object + read_only = 405, // Attempt to change read-only state + invalid_request = 406, // Method call is invalid in current operating context + conflict = 409, // There is a conflict with the current state of the device + buffer_overflow = 413, // Something was too big + index_out_of_bounds = 414, // Index is outside the available range + parameter_error = 417, // Method parameter does not meet expectations + locked = 423, // Addressed object is locked + device_error = 500, // Internal device error + method_not_implemented = 501, // Addressed method is not implemented by the addressed object + property_not_implemented = 502, // Addressed property is not implemented by the addressed object + not_ready = 503, // The device is not ready to handle any commands + timeout = 504, // Method call did not finish within the allotted time + }; + } + + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmethodresult + struct nc_method_result + { + nc_method_status::status status; + }; + + // Datatype type + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdatatypetype + namespace nc_datatype_type + { + enum type + { + Primitive = 0, + Typedef = 1, + Struct = 2, + Enum = 3 + }; + } + + // Device generic operational state + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicegenericstate + namespace nc_device_generic_state + { + enum state + { + unknown = 0, // Unknown + normal_operation = 1, // Normal operation + initializing = 2, // Device is initializing + updating = 3, // Device is performing a software or firmware update + licensing_error = 4, // Device is experiencing a licensing error + internal_error = 5 // Device is experiencing an internal error + }; + } + + // Reset cause enum + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncresetcause + namespace nc_reset_cause + { + enum cause + { + unknown = 0, // Unknown + power_on = 1, // Power on + internal_error = 2, // Internal error + upgrade = 3, // Upgrade + controller_request = 4, // Controller request + manual_reset = 5 // Manual request from the front panel + }; + } + + // NcConnectionStatus + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncconnectionstatus + namespace nc_connection_status + { + enum status + { + undefined = 0, // This is the value when there is no receiver + connected = 1, // Connected to a stream + disconnected = 2, // Not connected to a stream + connection_error = 3 // A connection error was encountered + }; + } + + // NcPayloadStatus + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncpayloadstatus + namespace nc_payload_status + { + enum status + { + undefined = 0, // This is the value when there's no connection. + payload_ok = 1, // Payload is being received without errors and is the correct type + payload_format_unsupported = 2, // Payload is being received but is of an unsupported type + payloadError = 3 // A payload error was encountered + }; + } + + // NcElementId + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncelementid + struct nc_element_id + { + uint16_t level; + uint16_t index; + + nc_element_id(uint16_t level, uint16_t index) + : level(level) + , index(index) + {} + + auto tied() const -> decltype(std::tie(level, index)) { return std::tie(level, index); } + friend bool operator==(const nc_element_id& lhs, const nc_element_id& rhs) { return lhs.tied() == rhs.tied(); } + friend bool operator!=(const nc_element_id& lhs, const nc_element_id& rhs) { return !(lhs == rhs); } + friend bool operator<(const nc_element_id& lhs, const nc_element_id& rhs) { return lhs.tied() < rhs.tied(); } + }; + + // NcEventId + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nceventid + typedef nc_element_id nc_event_id; + // NcEventIds for NcObject + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncobject + const nc_event_id nc_object_property_changed_event_id(1, 1); + + // NcMethodId + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmethodid + typedef nc_element_id nc_method_id; + // NcMethodIds for NcObject + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncobject + const nc_method_id nc_object_get_method_id(1, 1); + const nc_method_id nc_object_set_method_id(1, 2); + const nc_method_id nc_object_get_sequence_item_method_id(1, 3); + const nc_method_id nc_object_set_sequence_item_method_id(1, 4); + const nc_method_id nc_object_add_sequence_item_method_id(1, 5); + const nc_method_id nc_object_remove_sequence_item_method_id(1, 6); + const nc_method_id nc_object_get_sequence_length_method_id(1, 7); + // NcMethodIds for NcBlock + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncblock + const nc_method_id nc_block_get_member_descriptors_method_id(2, 1); + const nc_method_id nc_block_find_members_by_path_method_id(2, 2); + const nc_method_id nc_block_find_members_by_role_method_id(2, 3); + const nc_method_id nc_block_find_members_by_class_id_method_id(2, 4); + // NcMethodIds for NcClassManager + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassmanager + const nc_method_id nc_class_manager_get_control_class_method_id(3, 1); + const nc_method_id nc_class_manager_get_datatype_method_id(3, 2); + + // NcPropertyId + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertyid + typedef nc_element_id nc_property_id; + // NcPropertyIds for NcObject + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncobject + const nc_property_id nc_object_class_id_property_id(1, 1); + const nc_property_id nc_object_oid_property_id(1, 2); + const nc_property_id nc_object_constant_oid_property_id(1, 3); + const nc_property_id nc_object_owner_property_id(1, 4); + const nc_property_id nc_object_role_property_id(1, 5); + const nc_property_id nc_object_user_label_property_id(1, 6); + const nc_property_id nc_object_touchpoints_property_id(1, 7); + const nc_property_id nc_object_runtime_property_constraints_property_id(1, 8); + // NcPropertyIds for NcBlock + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncblock + const nc_property_id nc_block_enabled_property_id(2, 1); + const nc_property_id nc_block_members_property_id(2, 2); + // NcPropertyIds for NcWorker + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncworker + const nc_property_id nc_worker_enabled_property_id(2, 1); + // NcPropertyIds for NcDeviceManager + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicemanager + const nc_property_id nc_device_manager_nc_version_property_id(3, 1); + const nc_property_id nc_device_manager_manufacturer_property_id(3, 2); + const nc_property_id nc_device_manager_product_property_id(3, 3); + const nc_property_id nc_device_manager_serial_number_property_id(3, 4); + const nc_property_id nc_device_manager_user_inventory_code_property_id(3, 5); + const nc_property_id nc_device_manager_device_name_property_id(3, 6); + const nc_property_id nc_device_manager_device_role_property_id(3, 7); + const nc_property_id nc_device_manager_operational_state_property_id(3, 8); + const nc_property_id nc_device_manager_reset_cause_property_id(3, 9); + const nc_property_id nc_device_manager_message_property_id(3, 10); + // NcPropertyIds for NcClassManager + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassmanager + const nc_property_id nc_class_manager_control_classes_property_id(3, 1); + const nc_property_id nc_class_manager_datatypes_property_id(3, 2); + // NcPropertyids for NcReceiverMonitor + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitor + const nc_property_id nc_receiver_monitor_connection_status_property_id(3, 1); + const nc_property_id nc_receiver_monitor_connection_status_message_property_id(3, 2); + const nc_property_id nc_receiver_monitor_payload_status_property_id(3, 3); + const nc_property_id nc_receiver_monitor_payload_status_message_property_id(3, 4); + // NcPropertyids for NcReceiverMonitorProtected + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitorprotected + const nc_property_id nc_receiver_monitor_protected_signal_protection_status_property_id(4, 1); + // NcPropertyids for NcIdentBeacon + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/identification/#ncidentbeacon + const nc_property_id nc_ident_beacon_active_property_id(3, 1); + + // NcId + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncid + typedef uint32_t nc_id; + + // NcName + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncname + typedef utility::string_t nc_name; + + // NcOid + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncoid + typedef uint32_t nc_oid; + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Blocks.html + const nc_oid root_block_oid{ 1 }; + + // NcUri + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncuri + typedef utility::string_t nc_uri; + + // NcUuid + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncuuid + typedef utility::string_t nc_uuid; + + // NcRegex + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncregex + typedef utility::string_t nc_regex; + + // NcOrganizationId + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncorganizationid + typedef int32_t nc_organization_id; + + // NcClassId + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassid + typedef std::vector nc_class_id; + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncobject + const nc_class_id nc_object_class_id({ 1 }); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncblock + const nc_class_id nc_block_class_id({ 1, 1 }); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncworker + const nc_class_id nc_worker_class_id({ 1, 2 }); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncmanager + const nc_class_id nc_manager_class_id({ 1, 3 }); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicemanager + const nc_class_id nc_device_manager_class_id({ 1, 3, 1 }); + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncclassmanager + const nc_class_id nc_class_manager_class_id({ 1, 3, 2 }); + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/identification/#ncidentbeacon + const nc_class_id nc_ident_beacon_class_id({ 1, 2, 2 }); + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitor + const nc_class_id nc_receiver_monitor_class_id({ 1, 2, 3 }); + // See https://specs.amwa.tv/nmos-control-feature-sets/branches/main/monitoring/#ncreceivermonitorprotected + const nc_class_id nc_receiver_monitor_protected_class_id({ 1, 2, 3, 1 }); + + // NcTouchpoint + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpoint + typedef utility::string_t nc_touch_point; + + // NcPropertyChangeType + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertychangetype + namespace nc_property_change_type + { + enum type + { + value_changed = 0, // Current value changed + sequence_item_added = 1, // Sequence item added + sequence_item_changed = 2, // Sequence item changed + sequence_item_removed = 3 // Sequence item removed + }; + } + + // NcPropertyChangedEventData + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncpropertychangedeventdata + struct nc_property_changed_event_data + { + nc_property_id property_id; + nc_property_change_type::type change_type; + web::json::value value; + web::json::value sequence_item_index; // nc_id, can be null + + nc_property_changed_event_data(nc_property_id property_id, nc_property_change_type::type change_type, web::json::value value, nc_id sequence_item_index) + : property_id(std::move(property_id)) + , change_type(change_type) + , value(std::move(value)) + , sequence_item_index(sequence_item_index) + {} + + nc_property_changed_event_data(nc_property_id property_id, nc_property_change_type::type change_type, web::json::value value) + : property_id(std::move(property_id)) + , change_type(change_type) + , value(std::move(value)) + , sequence_item_index(web::json::value::null()) + {} + + nc_property_changed_event_data(nc_property_id property_id, nc_property_change_type::type change_type, nc_id sequence_item_index) + : property_id(std::move(property_id)) + , change_type(change_type) + , value(web::json::value::null()) + , sequence_item_index(sequence_item_index) + {} + + auto tied() const -> decltype(std::tie(property_id, change_type, value, sequence_item_index)) { return std::tie(property_id, change_type, value, sequence_item_index); } + friend bool operator==(const nc_property_changed_event_data& lhs, const nc_property_changed_event_data& rhs) { return lhs.tied() == rhs.tied(); } + friend bool operator!=(const nc_property_changed_event_data& lhs, const nc_property_changed_event_data& rhs) { return !(lhs == rhs); } + friend bool operator<(const nc_property_changed_event_data& lhs, const nc_property_changed_event_data& rhs) { return lhs.tied() < rhs.tied(); } + }; + + // NcTouchpointResource + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpointresource + struct nc_touchpoint_resource + { + utility::string_t resource_type; + + nc_touchpoint_resource(const utility::string_t& resource_type) + : resource_type(resource_type) + {} + + auto tied() const -> decltype(std::tie(resource_type)) { return std::tie(resource_type); } + friend bool operator==(const nc_touchpoint_resource& lhs, const nc_touchpoint_resource& rhs) { return lhs.tied() == rhs.tied(); } + friend bool operator!=(const nc_touchpoint_resource& lhs, const nc_touchpoint_resource& rhs) { return !(lhs == rhs); } + friend bool operator<(const nc_touchpoint_resource& lhs, const nc_touchpoint_resource& rhs) { return lhs.tied() < rhs.tied(); } + }; + + // NcTouchpointResourceNmos + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpointresourcenmos + struct nc_touchpoint_resource_nmos : nc_touchpoint_resource + { + nc_uuid id; + + nc_touchpoint_resource_nmos(const utility::string_t& resource_type, nc_uuid id) + : nc_touchpoint_resource(resource_type) + , id(id) + {} + + nc_touchpoint_resource_nmos(const ncp_nmos_resource_type& resource_type, nc_uuid id) + : nc_touchpoint_resource(resource_type.name) + , id(id) + {} + + auto tied() const -> decltype(std::tie(resource_type, id)) { return std::tie(resource_type, id); } + friend bool operator==(const nc_touchpoint_resource_nmos& lhs, const nc_touchpoint_resource_nmos& rhs) { return lhs.tied() == rhs.tied(); } + friend bool operator!=(const nc_touchpoint_resource_nmos& lhs, const nc_touchpoint_resource_nmos& rhs) { return !(lhs == rhs); } + friend bool operator<(const nc_touchpoint_resource_nmos& lhs, const nc_touchpoint_resource_nmos& rhs) { return lhs.tied() < rhs.tied(); } + }; + + // NcTouchpointResourceNmosChannelMapping + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#nctouchpointresourcenmoschannelmapping + struct nc_touchpoint_resource_nmos_channel_mapping : nc_touchpoint_resource_nmos + { + //ncp_nmos_channel_mapping_resource_type resource_type; + nc_uuid io_id; + + nc_touchpoint_resource_nmos_channel_mapping(const ncp_nmos_channel_mapping_resource_type& resource_type, nc_uuid id, const utility::string_t& io_id) + : nc_touchpoint_resource_nmos(resource_type.name, id) + , io_id(io_id) + {} + + auto tied() const -> decltype(std::tie(resource_type, id, io_id)) { return std::tie(resource_type, id, io_id); } + friend bool operator==(const nc_touchpoint_resource_nmos_channel_mapping& lhs, const nc_touchpoint_resource_nmos_channel_mapping& rhs) { return lhs.tied() == rhs.tied(); } + friend bool operator!=(const nc_touchpoint_resource_nmos_channel_mapping& lhs, const nc_touchpoint_resource_nmos_channel_mapping& rhs) { return !(lhs == rhs); } + friend bool operator<(const nc_touchpoint_resource_nmos_channel_mapping& lhs, const nc_touchpoint_resource_nmos_channel_mapping& rhs) { return lhs.tied() < rhs.tied(); } + }; +} + +#endif diff --git a/Development/nmos/control_protocol_utils.cpp b/Development/nmos/control_protocol_utils.cpp new file mode 100644 index 000000000..9d17c0967 --- /dev/null +++ b/Development/nmos/control_protocol_utils.cpp @@ -0,0 +1,641 @@ +#include "nmos/control_protocol_utils.h" + +#include +#include +#include +#include "bst/regex.h" +#include "cpprest/json_utils.h" +#include "nmos/control_protocol_resource.h" +#include "nmos/control_protocol_state.h" +#include "nmos/json_fields.h" +#include "nmos/query_utils.h" +#include "nmos/resources.h" + +namespace nmos +{ + namespace details + { + bool is_control_class(const nc_class_id& control_class_id, const nc_class_id& class_id_) + { + nc_class_id class_id{ class_id_ }; + if (control_class_id.size() < class_id.size()) + { + // truncate test class_id to relevant class_id + class_id.resize(control_class_id.size()); + } + return control_class_id == class_id; + } + + // get the runtime property constraints of a specific property_id + web::json::value get_runtime_property_constraints(const nc_property_id& property_id, const web::json::value& runtime_property_constraints) + { + using web::json::value; + + if (!runtime_property_constraints.is_null()) + { + auto& runtime_prop_constraints = runtime_property_constraints.as_array(); + auto found_constraints = std::find_if(runtime_prop_constraints.begin(), runtime_prop_constraints.end(), [&property_id](const web::json::value& constraints) + { + return property_id == parse_nc_property_id(nmos::fields::nc::property_id(constraints)); + }); + + if (runtime_prop_constraints.end() != found_constraints) + { + return *found_constraints; + } + } + return value::null(); + } + + // get the datatype descriptor of a specific type_name + web::json::value get_datatype_descriptor(const web::json::value& type_name, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor) + { + using web::json::value; + + if (!type_name.is_null()) + { + return get_control_protocol_datatype_descriptor(type_name.as_string()).descriptor; + } + return value::null(); + } + + // get the datatype property constraints of a specific type_name + web::json::value get_datatype_constraints(const web::json::value& type_name, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor) + { + using web::json::value; + + // NcDatatypeDescriptor + const auto& datatype_descriptor = get_datatype_descriptor(type_name, get_control_protocol_datatype_descriptor); + if (!datatype_descriptor.is_null()) + { + return nmos::fields::nc::constraints(datatype_descriptor); + } + return value::null(); + } + + // constraints validation, may throw nmos::control_protocol_exception + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncparameterconstraintsnumber + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncparameterconstraintsstring + void constraints_validation(const web::json::value& data, const web::json::value& constraints) + { + auto parameter_constraints_validation = [&constraints](const web::json::value& value) + { + // is numeric constraints + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncparameterconstraintsnumber + if (constraints.has_field(nmos::fields::nc::step) && !nmos::fields::nc::step(constraints).is_null()) + { + if (value.is_null()) { throw control_protocol_exception("value is null"); } + + if (!value.is_integer()) { throw control_protocol_exception("value is not an integer"); } + + const auto step = nmos::fields::nc::step(constraints).as_double(); + if (step <= 0) { throw control_protocol_exception("step is not a positive integer"); } + + const auto value_double = value.as_double(); + if (constraints.has_field(nmos::fields::nc::minimum) && !nmos::fields::nc::minimum(constraints).is_null()) + { + auto min = nmos::fields::nc::minimum(constraints).as_double(); + if (0 != std::fmod(value_double - min, step)) { throw control_protocol_exception("value is not divisible by step"); } + } + else if (constraints.has_field(nmos::fields::nc::maximum) && !nmos::fields::nc::maximum(constraints).is_null()) + { + auto max = nmos::fields::nc::maximum(constraints).as_double(); + if (0 != std::fmod(max - value_double, step)) { throw control_protocol_exception("value is not divisible by step"); } + } + else + { + if (0 != std::fmod(value_double, step)) { throw control_protocol_exception("value is not divisible by step"); } + } + } + if (constraints.has_field(nmos::fields::nc::minimum) && !nmos::fields::nc::minimum(constraints).is_null()) + { + if (value.is_null()) { throw control_protocol_exception("value is null"); } + + if (!value.is_integer() || value.as_double() < nmos::fields::nc::minimum(constraints).as_double()) { throw control_protocol_exception("value is less than minimum"); } + } + if (constraints.has_field(nmos::fields::nc::maximum) && !nmos::fields::nc::maximum(constraints).is_null()) + { + if (value.is_null()) { throw control_protocol_exception("value is null"); } + + if (!value.is_integer() || value.as_double() > nmos::fields::nc::maximum(constraints).as_double()) { throw control_protocol_exception("value is greater than maximum"); } + } + + // is string constraints + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncparameterconstraintsstring + if (constraints.has_field(nmos::fields::nc::max_characters) && !constraints.at(nmos::fields::nc::max_characters).is_null()) + { + if (value.is_null()) { throw control_protocol_exception("value is null"); } + + const size_t max_characters = nmos::fields::nc::max_characters(constraints); + if (!value.is_string() || value.as_string().length() > max_characters) { throw control_protocol_exception("value is longer than maximum characters"); } + } + if (constraints.has_field(nmos::fields::nc::pattern) && !constraints.at(nmos::fields::nc::pattern).is_null()) + { + if (value.is_null()) { throw control_protocol_exception("value is null"); } + + if (!value.is_string()) { throw control_protocol_exception("value is not a string"); } + const auto value_string = utility::us2s(value.as_string()); + bst::regex pattern(utility::us2s(nmos::fields::nc::pattern(constraints))); + if (!bst::regex_match(value_string, pattern)) { throw control_protocol_exception("value dose not match the pattern"); } + } + + // reaching here, parameter validation successfully + }; + + if (data.is_array()) + { + for (const auto& value : data.as_array()) + { + parameter_constraints_validation(value); + } + } + else + { + parameter_constraints_validation(data); + } + } + + // level 0 datatype constraints validation, may throw nmos::control_protocol_exception + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Constraints.html + void datatype_constraints_validation(const web::json::value& data, const datatype_constraints_validation_parameters& params) + { + auto parameter_constraints_validation = [¶ms](const web::json::value& value_) + { + // no constraints validation required + if (params.datatype_descriptor.is_null()) { return; } + + const auto& datatype_type = nmos::fields::nc::type(params.datatype_descriptor); + + // do NcDatatypeDescriptorPrimitive constraints validation + if (nc_datatype_type::Primitive == datatype_type) + { + // hmm, for the primitive type, it should not have datatype constraints specified via the datatype_descriptor but just in case + const auto& datatype_constraints = nmos::fields::nc::constraints(params.datatype_descriptor); + if (datatype_constraints.is_null()) + { + auto primitive_validation = [](const nc_name& name, const web::json::value& value) + { + auto is_int16 = [](int32_t value) + { + return value >= (std::numeric_limits::min)() + && value <= (std::numeric_limits::max)(); + }; + auto is_uint16 = [](uint32_t value) + { + return value >= (std::numeric_limits::min)() + && value <= (std::numeric_limits::max)(); + }; + auto is_float32 = [](double value) + { + return value >= (std::numeric_limits::lowest)() + && value <= (std::numeric_limits::max)(); + }; + + if (U("NcBoolean") == name) { return value.is_boolean(); } + if (U("NcInt16") == name && value.is_number()) { return is_int16(value.as_number().to_int32()); } + if (U("NcInt32") == name && value.is_number()) { return value.as_number().is_int32(); } + if (U("NcInt64") == name && value.is_number()) { return value.as_number().is_int64(); } + if (U("NcUint16") == name && value.is_number()) { return is_uint16(value.as_number().to_uint32()); } + if (U("NcUint32") == name && value.is_number()) { return value.as_number().is_uint32(); } + if (U("NcUint64") == name && value.is_number()) { return value.as_number().is_uint64(); } + if (U("NcFloat32") == name && value.is_number()) { return is_float32(value.as_number().to_double()); } + if (U("NcFloat64") == name && value.is_number()) { return !value.as_number().is_integral(); } + if (U("NcString") == name) { return value.is_string(); } + + // invalid primitive type + return false; + }; + + // do primitive type constraints validation + const auto& name = nmos::fields::nc::name(params.datatype_descriptor); + if (!primitive_validation(name, value_)) + { + throw control_protocol_exception("value is not a " + utility::us2s(name) + " type");; + } + } + else + { + constraints_validation(value_, datatype_constraints); + } + + return; + } + + // do NcDatatypeDescriptorTypeDef constraints validation + if (nc_datatype_type::Typedef == datatype_type) + { + // do the datatype constraints specified via the datatype_descriptor if presented + const auto& datatype_constraints = nmos::fields::nc::constraints(params.datatype_descriptor); + if (datatype_constraints.is_null()) + { + // do parent typename constraints validation + const auto& type_name = params.datatype_descriptor.at(nmos::fields::nc::parent_type); // parent type_name + datatype_constraints_validation(value_, { details::get_datatype_descriptor(type_name, params.get_control_protocol_datatype_descriptor), params.get_control_protocol_datatype_descriptor }); + } + else + { + constraints_validation(value_, datatype_constraints); + } + + return; + } + + // do NcDatatypeDescriptorEnum constraints validation + if (nc_datatype_type::Enum == datatype_type) + { + const auto& items = nmos::fields::nc::items(params.datatype_descriptor); + if (items.end() == std::find_if(items.begin(), items.end(), [&](const web::json::value& nc_enum_item_descriptor) { return nmos::fields::nc::value(nc_enum_item_descriptor) == value_; })) + { + const auto& name = nmos::fields::nc::name(params.datatype_descriptor); + throw control_protocol_exception("value is not an enum " + utility::us2s(name) + " type"); + } + + return; + } + + // do NcDatatypeDescriptorStruct constraints validation + if (nc_datatype_type::Struct == datatype_type) + { + const auto& datatype_name = nmos::fields::nc::name(params.datatype_descriptor); + const auto& fields = nmos::fields::nc::fields(params.datatype_descriptor); + // NcFieldDescriptor + for (const web::json::value& nc_field_descriptor : fields) + { + const auto& field_name = nmos::fields::nc::name(nc_field_descriptor); + // is field in strurcture + if (!value_.has_field(field_name)) { throw control_protocol_exception("missing " + utility::us2s(field_name) + " in " + utility::us2s(datatype_name)); } + + // is field nullable + if (!nmos::fields::nc::is_nullable(nc_field_descriptor) && value_.at(field_name).is_null()) { throw control_protocol_exception(utility::us2s(field_name) + " is not nullable"); } + + // if field value is null continue to next field + if (value_.at(field_name).is_null()) continue; + + // is field sequenceable + if (nmos::fields::nc::is_sequence(nc_field_descriptor) != value_.at(field_name).is_array()) { throw control_protocol_exception(utility::us2s(field_name) + " is not sequenceable"); } + + // check constraints of its typeName + const auto& field_type_name = nc_field_descriptor.at(nmos::fields::nc::type_name); + + if (!field_type_name.is_null()) + { + auto value = value_.at(field_name); + + // do typename constraints validation + datatype_constraints_validation(value, { details::get_datatype_descriptor(field_type_name, params.get_control_protocol_datatype_descriptor), params.get_control_protocol_datatype_descriptor }); + } + + // check against field constraints if present + const auto& constraints = nmos::fields::nc::constraints(nc_field_descriptor); + if (!constraints.is_null()) + { + // do field constraints validation + const auto& value = value_.at(field_name); + constraints_validation(value, constraints); + } + } + // unsupported datatype_type, no validation is required + return; + } + }; + + if (data.is_array()) + { + for (const auto& value : data.as_array()) + { + parameter_constraints_validation(value); + } + } + else + { + parameter_constraints_validation(data); + } + } + + // multiple levels of constraints validation, may throw nmos::control_protocol_exception + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Constraints.html + void constraints_validation(const web::json::value& data, const web::json::value& runtime_property_constraints, const web::json::value& property_constraints, const datatype_constraints_validation_parameters& params) + { + // do level 2 runtime property constraints validation + if (!runtime_property_constraints.is_null()) { constraints_validation(data, runtime_property_constraints); return; } + + // do level 1 property constraints validation + if (!property_constraints.is_null()) { constraints_validation(data, property_constraints); return; } + + // do level 0 datatype constraints validation + datatype_constraints_validation(data, params); + } + + // method parameter constraints validation, may throw nmos::control_protocol_exception + void method_parameter_constraints_validation(const web::json::value& data, const web::json::value& property_constraints, const datatype_constraints_validation_parameters& params) + { + using web::json::value; + + // do level 1 property constraints & level 0 datatype constraints validation + constraints_validation(data, value::null(), property_constraints, params); + } + } + + // is the given class_id a NcBlock + bool is_nc_block(const nc_class_id& class_id) + { + return details::is_control_class(nc_block_class_id, class_id); + } + + // is the given class_id a NcWorker + bool is_nc_worker(const nc_class_id& class_id) + { + return details::is_control_class(nc_worker_class_id, class_id); + } + + // is the given class_id a NcManager + bool is_nc_manager(const nc_class_id& class_id) + { + return details::is_control_class(nc_manager_class_id, class_id); + } + + // is the given class_id a NcDeviceManager + bool is_nc_device_manager(const nc_class_id& class_id) + { + return details::is_control_class(nc_device_manager_class_id, class_id); + } + + // is the given class_id a NcClassManager + bool is_nc_class_manager(const nc_class_id& class_id) + { + return details::is_control_class(nc_class_manager_class_id, class_id); + } + + // construct NcClassId + nc_class_id make_nc_class_id(const nc_class_id& prefix, int32_t authority_key, const std::vector& suffix) + { + nc_class_id class_id = prefix; + class_id.push_back(authority_key); + class_id.insert(class_id.end(), suffix.begin(), suffix.end()); + return class_id; + } + nc_class_id make_nc_class_id(const nc_class_id& prefix, const std::vector& suffix) + { + return make_nc_class_id(prefix, 0, suffix); + } + + // find control class property descriptor (NcPropertyDescriptor) + web::json::value find_property_descriptor(const nc_property_id& property_id, const nc_class_id& class_id_, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor) + { + using web::json::value; + + auto class_id = class_id_; + + while (!class_id.empty()) + { + const auto& control_class = get_control_protocol_class_descriptor(class_id); + auto& property_descriptors = control_class.property_descriptors.as_array(); + auto found = std::find_if(property_descriptors.begin(), property_descriptors.end(), [&property_id](const web::json::value& property_descriptor) + { + return (property_id == nmos::details::parse_nc_property_id(nmos::fields::nc::id(property_descriptor))); + }); + if (property_descriptors.end() != found) { return *found; } + + class_id.pop_back(); + } + + return value::null(); + } + + // get block member descriptors + void get_member_descriptors(const resources& resources, const resource& resource, bool recurse, web::json::array& descriptors) + { + if (resource.data.has_field(nmos::fields::nc::members)) + { + const auto& members = nmos::fields::nc::members(resource.data); + + for (const auto& member : members) + { + web::json::push_back(descriptors, member); + } + + if (recurse) + { + // get members on all NcBlock(s) + for (const auto& member : members) + { + if (is_nc_block(nmos::details::parse_nc_class_id(nmos::fields::nc::class_id(member)))) + { + // get resource based on the oid + const auto& oid = nmos::fields::nc::oid(member); + const auto& found = find_resource(resources, utility::s2us(std::to_string(oid))); + if (resources.end() != found) + { + get_member_descriptors(resources, *found, recurse, descriptors); + } + } + } + } + } + } + + // find members with given role name or fragment + void find_members_by_role(const resources& resources, const resource& resource, const utility::string_t& role, bool match_whole_string, bool case_sensitive, bool recurse, web::json::array& descriptors) + { + auto find_members_by_matching_role = [&](const web::json::array& members) + { + using web::json::value; + + auto match = [&](const web::json::value& descriptor) + { + if (match_whole_string) + { + if (case_sensitive) { return role == nmos::fields::nc::role(descriptor); } + else { return boost::algorithm::to_upper_copy(role) == boost::algorithm::to_upper_copy(nmos::fields::nc::role(descriptor)); } + } + else + { + if (case_sensitive) { return !boost::find_first(nmos::fields::nc::role(descriptor), role).empty(); } + else { return !boost::ifind_first(nmos::fields::nc::role(descriptor), role).empty(); } + } + }; + + return boost::make_iterator_range(boost::make_filter_iterator(match, members.begin(), members.end()), boost::make_filter_iterator(match, members.end(), members.end())); + }; + + if (resource.data.has_field(nmos::fields::nc::members)) + { + const auto& members = nmos::fields::nc::members(resource.data); + + auto members_found = find_members_by_matching_role(members); + for (const auto& member : members_found) + { + web::json::push_back(descriptors, member); + } + + if (recurse) + { + // do role match on all NcBlock(s) + for (const auto& member : members) + { + if (is_nc_block(nmos::details::parse_nc_class_id(nmos::fields::nc::class_id(member)))) + { + // get resource based on the oid + const auto& oid = nmos::fields::nc::oid(member); + const auto& found = find_resource(resources, utility::s2us(std::to_string(oid))); + if (resources.end() != found) + { + find_members_by_role(resources, *found, role, match_whole_string, case_sensitive, recurse, descriptors); + } + } + } + } + } + } + + // find members with given class id + void find_members_by_class_id(const resources& resources, const nmos::resource& resource, const nc_class_id& class_id_, bool include_derived, bool recurse, web::json::array& descriptors) + { + auto find_members_by_matching_class_id = [&](const web::json::array& members) + { + using web::json::value; + + auto match = [&](const web::json::value& descriptor) + { + const auto& class_id = nmos::details::parse_nc_class_id(nmos::fields::nc::class_id(descriptor)); + + if (include_derived) { return !boost::find_first(class_id, class_id_).empty(); } + else { return class_id == class_id_; } + }; + + return boost::make_iterator_range(boost::make_filter_iterator(match, members.begin(), members.end()), boost::make_filter_iterator(match, members.end(), members.end())); + }; + + if (resource.data.has_field(nmos::fields::nc::members)) + { + auto& members = nmos::fields::nc::members(resource.data); + + auto members_found = find_members_by_matching_class_id(members); + for (const auto& member : members_found) + { + web::json::push_back(descriptors, member); + } + + if (recurse) + { + // do class_id match on all NcBlock(s) + for (const auto& member : members) + { + if (is_nc_block(nmos::details::parse_nc_class_id(nmos::fields::nc::class_id(member)))) + { + // get resource based on the oid + const auto& oid = nmos::fields::nc::oid(member); + const auto& found = find_resource(resources, utility::s2us(std::to_string(oid))); + if (resources.end() != found) + { + find_members_by_class_id(resources, *found, class_id_, include_derived, recurse, descriptors); + } + } + } + } + } + } + + // push a control protocol resource into other control protocol NcBlock resource + void push_back(control_protocol_resource& nc_block_resource, const control_protocol_resource& resource) + { + // note, model write lock should aleady be applied by the outer function, so access to control_protocol_resources is OK... + + using web::json::value; + + auto& parent = nc_block_resource.data; + const auto& child = resource.data; + + if (!is_nc_block(details::parse_nc_class_id(nmos::fields::nc::class_id(parent)))) throw std::logic_error("non-NcBlock cannot be nested"); + + web::json::push_back(parent[nmos::fields::nc::members], + details::make_nc_block_member_descriptor(nmos::fields::description(child), nmos::fields::nc::role(child), nmos::fields::nc::oid(child), nmos::fields::nc::constant_oid(child), details::parse_nc_class_id(nmos::fields::nc::class_id(child)), nmos::fields::nc::user_label(child), nmos::fields::nc::oid(parent))); + + nc_block_resource.resources.push_back(resource); + } + + // modify a control protocol resource, and insert notification event to all subscriptions + bool modify_control_protocol_resource(resources& resources, const id& id, std::function modifier, const web::json::value& notification_event) + { + // note, model write lock should aleady be applied by the outer function, so access to control_protocol_resources is OK... + + auto found = resources.find(id); + if (resources.end() == found || !found->has_data()) return false; + + auto pre = found->data; + + // "If an exception is thrown by some user-provided operation, then the element pointed to by position is erased." + // This seems too surprising, despite the fact that it means that a modification may have been partially completed, + // so capture and rethrow. + // See https://www.boost.org/doc/libs/1_68_0/libs/multi_index/doc/reference/ord_indices.html#modify + std::exception_ptr modifier_exception; + + auto resource_updated = nmos::strictly_increasing_update(resources); + auto result = resources.modify(found, [&resource_updated, &modifier, &modifier_exception](resource& resource) + { + try + { + modifier(resource); + } + catch (...) + { + modifier_exception = std::current_exception(); + } + + // set the update timestamp + resource.updated = resource_updated; + }); + + if (result) + { + auto& modified = *found; + + insert_notification_events(resources, modified.version, modified.downgrade_version, modified.type, pre, modified.data, notification_event); + } + + if (modifier_exception) + { + std::rethrow_exception(modifier_exception); + } + + return result; + } + + // find the control protocol resource which is assoicated with the given IS-04/IS-05/IS-08 resource id + resources::const_iterator find_control_protocol_resource(resources& resources, type type, const id& resource_id) + { + return find_resource_if(resources, type, [resource_id](const nmos::resource& resource) + { + auto& touchpoints = resource.data.at(nmos::fields::nc::touchpoints); + if (!touchpoints.is_null() && touchpoints.is_array()) + { + auto& tps = touchpoints.as_array(); + auto found_tp = std::find_if(tps.begin(), tps.end(), [resource_id](const web::json::value& touchpoint) + { + auto& resource = nmos::fields::nc::resource(touchpoint); + return (resource_id == nmos::fields::nc::id(resource).as_string() + && nmos::ncp_nmos_resource_types::receiver.name == nmos::fields::nc::resource_type(resource)); + }); + return (tps.end() != found_tp); + } + return false; + }); + } + + // method parameters constraints validation + void method_parameters_contraints_validation(const web::json::value& arguments, const web::json::value& nc_method_descriptor, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor) + { + for (const auto& param : nmos::fields::nc::parameters(nc_method_descriptor)) + { + const auto& name = nmos::fields::nc::name(param); + const auto& constraints = nmos::fields::nc::constraints(param); + const auto& type_name = param.at(nmos::fields::nc::type_name); + if (arguments.is_null() || !arguments.has_field(name)) + { + // missing argument parameter + throw control_protocol_exception("missing argument parameter " + utility::us2s(name)); + } + details::method_parameter_constraints_validation(arguments.at(name), constraints, { nmos::details::get_datatype_descriptor(type_name, get_control_protocol_datatype_descriptor), get_control_protocol_datatype_descriptor }); + } + } +} diff --git a/Development/nmos/control_protocol_utils.h b/Development/nmos/control_protocol_utils.h new file mode 100644 index 000000000..6d7060851 --- /dev/null +++ b/Development/nmos/control_protocol_utils.h @@ -0,0 +1,84 @@ +#ifndef NMOS_CONTROL_PROTOCOL_UTILS_H +#define NMOS_CONTROL_PROTOCOL_UTILS_H + +#include "cpprest/basic_utils.h" +#include "nmos/control_protocol_handlers.h" + +namespace nmos +{ + struct control_protocol_resource; + + struct control_protocol_exception : std::runtime_error + { + control_protocol_exception(const std::string& message) : std::runtime_error(message) {} + }; + + namespace details + { + // get the runtime property constraints of a given property_id + web::json::value get_runtime_property_constraints(const nc_property_id& property_id, const web::json::value& runtime_property_constraints_list); + + // get the datatype descriptor of a specific type_name + web::json::value get_datatype_descriptor(const web::json::value& type_name, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype); + + // get the datatype property constraints of a given type_name + web::json::value get_datatype_constraints(const web::json::value& type_name, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype); + + struct datatype_constraints_validation_parameters + { + web::json::value datatype_descriptor; + get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor; + }; + // multiple levels of constraints validation, may throw nmos::control_protocol_exception + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Constraints.html + void constraints_validation(const web::json::value& value, const web::json::value& runtime_property_constraints, const web::json::value& property_constraints, const datatype_constraints_validation_parameters& params); + + // method parameter constraints validation, may throw nmos::control_protocol_exception + void method_parameter_constraints_validation(const web::json::value& data, const web::json::value& property_constraints, const datatype_constraints_validation_parameters& params); + } + + // is the given class_id a NcBlock + bool is_nc_block(const nc_class_id& class_id); + + // is the given class_id a NcWorker + bool is_nc_worker(const nc_class_id& class_id); + + // is the given class_id a NcManager + bool is_nc_manager(const nc_class_id& class_id); + + // is the given class_id a NcDeviceManager + bool is_nc_device_manager(const nc_class_id& class_id); + + // is the given class_id a NcClassManager + bool is_nc_class_manager(const nc_class_id& class_id); + + // construct NcClassId + nc_class_id make_nc_class_id(const nc_class_id& prefix, int32_t authority_key, const std::vector& suffix); + nc_class_id make_nc_class_id(const nc_class_id& prefix, const std::vector& suffix); // using default authority_key 0 + + // find control class property descriptor (NcPropertyDescriptor) + web::json::value find_property_descriptor(const nc_property_id& property_id, const nc_class_id& class_id, get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor); + + // get block memeber descriptors + void get_member_descriptors(const resources& resources, const resource& resource, bool recurse, web::json::array& descriptors); + + // find members with given role name or fragment + void find_members_by_role(const resources& resources, const resource& resource, const utility::string_t& role, bool match_whole_string, bool case_sensitive, bool recurse, web::json::array& nc_block_member_descriptors); + + // find members with given class id + void find_members_by_class_id(const resources& resources, const resource& resource, const nc_class_id& class_id, bool include_derived, bool recurse, web::json::array& descriptors); + + // push control protocol resource into other control protocol NcBlock resource + void push_back(control_protocol_resource& nc_block_resource, const control_protocol_resource& resource); + + // modify a control protocol resource, and insert notification event to all subscriptions + bool modify_control_protocol_resource(resources& resources, const id& id, std::function modifier, const web::json::value& notification_event); + + // find the control protocol resource which is assoicated with the given IS-04/IS-05/IS-08 resource id + resources::const_iterator find_control_protocol_resource(resources& resources, type type, const id& id); + + // method parameters constraints validation, may throw nmos::control_protocol_exception + void method_parameters_contraints_validation(const web::json::value& arguments, const web::json::value& nc_method_descriptor, get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor); +} + +#endif diff --git a/Development/nmos/control_protocol_ws_api.cpp b/Development/nmos/control_protocol_ws_api.cpp new file mode 100644 index 000000000..cf1f35a0e --- /dev/null +++ b/Development/nmos/control_protocol_ws_api.cpp @@ -0,0 +1,537 @@ +#include "nmos/control_protocol_ws_api.h" + +#include +#include +#include "cpprest/json_validator.h" +#include "nmos/api_utils.h" +#include "nmos/control_protocol_resources.h" +#include "nmos/control_protocol_resource.h" +#include "nmos/control_protocol_utils.h" +#include "nmos/is12_versions.h" +#include "nmos/json_schema.h" +#include "nmos/model.h" +#include "nmos/query_utils.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace details + { + static const web::json::experimental::json_validator& controlprotocol_validator() + { + // hmm, could be based on supported API versions from settings, like other APIs' validators? + static const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(boost::range::join(boost::range::join( + is12_versions::all | boost::adaptors::transformed(experimental::make_controlprotocolapi_base_message_schema_uri), + is12_versions::all | boost::adaptors::transformed(experimental::make_controlprotocolapi_command_message_schema_uri)), + is12_versions::all | boost::adaptors::transformed(experimental::make_controlprotocolapi_subscription_message_schema_uri) + )) + }; + return validator; + } + + // Validate against specification schema + // throws web::json::json_exception on failure, which results in a 400 Badly-formed command + void validate_controlprotocolapi_base_message_schema(const nmos::api_version& version, const web::json::value& request_data) + { + controlprotocol_validator().validate(request_data, experimental::make_controlprotocolapi_base_message_schema_uri(version)); + } + void validate_controlprotocolapi_command_message_schema(const nmos::api_version& version, const web::json::value& request_data) + { + controlprotocol_validator().validate(request_data, experimental::make_controlprotocolapi_command_message_schema_uri(version)); + } + void validate_controlprotocolapi_subscription_message_schema(const nmos::api_version& version, const web::json::value& request_data) + { + controlprotocol_validator().validate(request_data, experimental::make_controlprotocolapi_subscription_message_schema_uri(version)); + } + } + + // IS-12 Control Protocol WebSocket API + + web::websockets::experimental::listener::validate_handler make_control_protocol_ws_validate_handler(nmos::node_model& model, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate_) + { + return [&model, ws_validate_authorization, &gate_](web::http::http_request req) + { + nmos::ws_api_gate gate(gate_, req.request_uri()); + + // RFC 6750 defines two methods of sending bearer access tokens which are applicable to WebSocket + // Clients SHOULD use the "Authorization Request Header Field" method. + // Clients MAY use a "URI Query Parameter". + // See https://tools.ietf.org/html/rfc6750#section-2 + if (ws_validate_authorization) + { + if (!ws_validate_authorization(req, nmos::experimental::scopes::ncp)) { return false; } + } + + // For now just return true + const auto& ws_ncp_path = req.request_uri().path(); + slog::log(gate, SLOG_FLF) << "Validating websocket connection to: " << ws_ncp_path; + + return true; + }; + } + + web::websockets::experimental::listener::open_handler make_control_protocol_ws_open_handler(nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate_) + { + using web::json::value; + using web::json::value_of; + + return [&model, &websockets, &gate_](const web::uri& connection_uri, const web::websockets::experimental::listener::connection_id& connection_id) + { + nmos::ws_api_gate gate(gate_, connection_uri); + auto lock = model.write_lock(); + auto& resources = model.control_protocol_resources; + + const auto& ws_ncp_path = connection_uri.path(); + slog::log(gate, SLOG_FLF) << "Opening websocket connection to: " << ws_ncp_path; + + // create a subscription (1-1 relationship with the connection) + resources::const_iterator subscription; + + { + const bool secure = nmos::experimental::fields::client_secure(model.settings); + + const auto ws_href = web::uri_builder() + .set_scheme(web::ws_scheme(secure)) + .set_host(nmos::get_host(model.settings)) + .set_port(nmos::fields::control_protocol_ws_port(model.settings)) + .set_path(ws_ncp_path) + .to_uri(); + + const utility::string_t control_protocol_resource_path; + + const bool non_persistent = false; + value data = value_of({ + { nmos::fields::id, nmos::make_id() }, + { nmos::fields::max_update_rate_ms, 0 }, + { nmos::fields::resource_path, control_protocol_resource_path }, + { nmos::fields::params, value_of({ { U("query.rql"), U("in(id,())") } }) }, + { nmos::fields::persist, non_persistent }, + { nmos::fields::secure, secure }, + { nmos::fields::ws_href, ws_href.to_string() } + }, true); + + // hm, could version be determined from ws_resource_path? + nmos::resource subscription_{ is12_versions::v1_0, nmos::types::subscription, std::move(data), non_persistent }; + + subscription = insert_resource(resources, std::move(subscription_)).first; + } + + { + // create a websocket connection resource + + value data; + nmos::id id = nmos::make_id(); + data[nmos::fields::id] = value::string(id); + data[nmos::fields::subscription_id] = value::string(subscription->id); + + // create an initial websocket message with no data + + const auto resource_path = nmos::fields::resource_path(subscription->data); + const auto topic = resource_path + U('/'); + data[nmos::fields::message] = details::make_grain({}, {}, topic); + + resource grain{ is12_versions::v1_0, nmos::types::grain, std::move(data), false }; + insert_resource(resources, std::move(grain)); + + websockets.insert({ id, connection_id }); + + slog::log(gate, SLOG_FLF) << "Creating websocket connection: " << id << " to subscription: " << subscription->id; + + slog::log(gate, SLOG_FLF) << "Notifying control protocol websockets thread"; // and anyone else who cares... + model.notify(); + } + }; + } + + web::websockets::experimental::listener::close_handler make_control_protocol_ws_close_handler(nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate_) + { + return [&model, &websockets, &gate_](const web::uri& connection_uri, const web::websockets::experimental::listener::connection_id& connection_id, web::websockets::websocket_close_status close_status, const utility::string_t& close_reason) + { + nmos::ws_api_gate gate(gate_, connection_uri); + auto lock = model.write_lock(); + auto& resources = model.control_protocol_resources; + + const auto& ws_ncp_path = connection_uri.path(); + slog::log(gate, SLOG_FLF) << "Closing websocket connection to: " << ws_ncp_path << " [" << (int)close_status << ": " << close_reason << "]"; + + auto websocket = websockets.right.find(connection_id); + if (websockets.right.end() != websocket) + { + auto grain = find_resource(resources, { websocket->second, nmos::types::grain }); + + if (resources.end() != grain) + { + slog::log(gate, SLOG_FLF) << "Deleting websocket connection: " << grain->id; + + // subscriptions have a 1-1 relationship with the websocket connection and both should now be erased immediately + auto subscription = find_resource(resources, { nmos::fields::subscription_id(grain->data), nmos::types::subscription }); + + if (resources.end() != subscription) + { + // this should erase grain too, as a subscription's subresource + erase_resource(resources, subscription->id); + } + else + { + // a grain without a subscription shouldn't be possible, but let's be tidy + erase_resource(resources, grain->id); + } + } + + websockets.right.erase(websocket); + + model.notify(); + } + }; + } + + web::websockets::experimental::listener::message_handler make_control_protocol_ws_message_handler(nmos::node_model& model, nmos::websockets& websockets, nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, nmos::get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, nmos::get_control_protocol_method_descriptor_handler get_control_protocol_method_descriptor, nmos::control_protocol_property_changed_handler property_changed, slog::base_gate& gate_) + { + using web::json::value; + using web::json::value_of; + + return [&model, &websockets, get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor, get_control_protocol_method_descriptor, property_changed, &gate_](const web::uri& connection_uri, const web::websockets::experimental::listener::connection_id& connection_id, const web::websockets::websocket_incoming_message& msg_) + { + nmos::ws_api_gate gate(gate_, connection_uri); + + auto lock = model.write_lock(); + auto& resources = model.control_protocol_resources; + + // theoretically blocking, but in fact not + auto msg = msg_.extract_string().get(); + + const auto& ws_ncp_path = connection_uri.path(); + slog::log(gate, SLOG_FLF) << "Received websocket message: " << msg << " on connection: " << ws_ncp_path; + + auto websocket = websockets.right.find(connection_id); + if (websockets.right.end() != websocket) + { + auto grain = find_resource(resources, { websocket->second, nmos::types::grain }); + + if (resources.end() != grain) + { + auto subscription = find_resource(resources, { nmos::fields::subscription_id(grain->data), nmos::types::subscription }); + + if (resources.end() != subscription) + { + try + { + // extract the control protocol api version from the ws_ncp_path + if (web::uri::split_path(ws_ncp_path).empty()) { throw std::invalid_argument("empty URL"); } + const auto version = nmos::parse_api_version(web::uri::split_path(ws_ncp_path).back()); + + // convert message to JSON + const auto message = value::parse(utility::conversions::to_string_t(msg)); + + // validate the base-message + details::validate_controlprotocolapi_base_message_schema(version, message); + + const auto msg_type = nmos::fields::nc::message_type(message); + switch (msg_type) + { + // See https://specs.amwa.tv/is-12/branches/v1.0.x/docs/Protocol_messaging.html#command-message-type + case ncp_message_type::command: + { + // validate command-message + details::validate_controlprotocolapi_command_message_schema(version, message); + + auto responses = value::array(); + auto& commands = nmos::fields::nc::commands(message); + for (const auto& cmd : commands) + { + const auto handle = nmos::fields::nc::handle(cmd); + const auto oid = nmos::fields::nc::oid(cmd); + + // get methodId + const auto& method_id = nmos::details::parse_nc_method_id(nmos::fields::nc::method_id(cmd)); + + // get arguments + const auto& arguments = nmos::fields::nc::arguments(cmd); + + value nc_method_result; + + auto resource = nmos::find_resource(resources, utility::s2us(std::to_string(oid))); + if (resources.end() != resource) + { + const auto& class_id = nmos::details::parse_nc_class_id(nmos::fields::nc::class_id(resource->data)); + + // find the relevent method handler to execute + // method tuple definition described in control_protocol_handlers.h + auto method = get_control_protocol_method_descriptor(class_id, method_id); + auto& nc_method_descriptor = method.first; + auto& control_method_handler = method.second; + if (control_method_handler) + { + try + { + // do method arguments constraints validation + method_parameters_contraints_validation(arguments, nc_method_descriptor, get_control_protocol_datatype_descriptor); + + // execute the relevant control method handler, then accumulating up their response to reponses + // wrap the NcMethodResuls here + nc_method_result = control_method_handler(resources, *resource, arguments, nmos::fields::nc::is_deprecated(nc_method_descriptor), gate); + } + catch (const nmos::control_protocol_exception& e) + { + // invalid arguments + utility::ostringstream_t ss; + ss << "invalid argument: " << arguments.serialize() << " error: " << e.what(); + slog::log(gate, SLOG_FLF) << ss.str(); + nc_method_result = details::make_nc_method_result_error({ nmos::nc_method_status::parameter_error }, ss.str()); + } + } + else + { + // unknown methodId + utility::ostringstream_t ss; + ss << U("unsupported method_id: ") << nmos::fields::nc::method_id(cmd).serialize() + << U(" for control class class_id: ") << resource->data.at(nmos::fields::nc::class_id).serialize(); + slog::log(gate, SLOG_FLF) << ss.str(); + nc_method_result = details::make_nc_method_result_error({ nc_method_status::method_not_implemented }, ss.str()); + } + } + else + { + // resource not found for the given oid + utility::ostringstream_t ss; + ss << U("unknown oid: ") << oid; + slog::log(gate, SLOG_FLF) << ss.str(); + nc_method_result = details::make_nc_method_result_error({ nc_method_status::bad_oid }, ss.str()); + } + // accumulating up response + auto response = make_control_protocol_response(handle, nc_method_result); + + web::json::push_back(responses, response); + } + + // add command_response to the grain ready to transfer to the client in nmos::send_control_protocol_ws_messages_thread + resources.modify(grain, [&](nmos::resource& grain) + { + web::json::push_back(nmos::fields::message_grain_data(grain.data), make_control_protocol_command_response(responses)); + + grain.updated = strictly_increasing_update(resources); + }); + } + break; + // See https://specs.amwa.tv/is-12/branches/v1.0.x/docs/Protocol_messaging.html#subscription-message-type + case ncp_message_type::subscription: + { + // validate subscription-message + details::validate_controlprotocolapi_subscription_message_schema(version, message); + + // subscribing to multiple OIDs, and filtering out invalid OIDs which cannot be subscribed to + auto& subscriptions = nmos::fields::nc::subscriptions(message); + value valid_subscriptions = value::array(); + for (const auto& subscription : subscriptions) + { + const auto oid = subscription.as_integer(); + auto resource = nmos::find_resource(resources, utility::s2us(std::to_string(oid))); + if (resources.end() != resource) + { + // only add the valid OIDs which can be subscribed to + web::json::push_back(valid_subscriptions, subscription); + } + } + + // update the subscription + modify_resource(resources, subscription->id, [&valid_subscriptions](nmos::resource& resource) + { + auto rql_query = U("in(id,(") + boost::algorithm::join(valid_subscriptions.as_array() | boost::adaptors::transformed([](const value& v) { return U("string:") + utility::s2us(std::to_string(v.as_integer())); }), U(",")) + U("))"); + + resource.data[nmos::fields::params] = value_of({ { U("query.rql"), rql_query } }); + }); + + // add subscription_response to the grain ready to transfer to the client in nmos::send_control_protocol_ws_messages_thread + resources.modify(grain, [&](nmos::resource& grain) + { + web::json::push_back(nmos::fields::message_grain_data(grain.data), make_control_protocol_subscription_response(valid_subscriptions)); + + grain.updated = strictly_increasing_update(resources); + }); + + slog::log(gate, SLOG_FLF) << "Received subscription command for " << valid_subscriptions.serialize(); + model.notify(); + } + break; + default: + // ignore unexpected message type + break; + } + + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "JSON error: " << e.what(); + + resources.modify(grain, [&](nmos::resource& grain) + { + web::json::push_back(nmos::fields::message_grain_data(grain.data), + make_control_protocol_error_message({ nc_method_status::bad_command_format }, utility::s2us(e.what()))); + + grain.updated = strictly_increasing_update(resources); + }); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Unexpected exception while handing control protocol command: " << e.what(); + + resources.modify(grain, [&](nmos::resource& grain) + { + web::json::push_back(nmos::fields::message_grain_data(grain.data), + make_control_protocol_error_message({ nc_method_status::bad_command_format }, utility::s2us(std::string("Unexpected exception while handing control protocol command : ") + e.what()))); + + grain.updated = strictly_increasing_update(resources); + }); + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Unexpected unknown exception for handing control protocol command"; + + resources.modify(grain, [&](nmos::resource& grain) + { + web::json::push_back(nmos::fields::message_grain_data(grain.data), + make_control_protocol_error_message({ nc_method_status::bad_command_format }, U("Unexpected unknown exception while handing control protocol command"))); + + grain.updated = strictly_increasing_update(resources); + }); + } + model.notify(); + } + } + } + }; + } + + // observe_websocket_exception is the same as the one defined in events_ws_api + namespace details + { + struct observe_websocket_exception + { + observe_websocket_exception(slog::base_gate& gate) : gate(gate) {} + + void operator()(pplx::task finally) + { + try + { + finally.get(); + } + catch (const web::websockets::websocket_exception& e) + { + slog::log(gate, SLOG_FLF) << "WebSocket error: " << e.what() << " [" << e.error_code() << "]"; + } + } + + slog::base_gate& gate; + }; + } + + void send_control_protocol_ws_messages_thread(web::websockets::experimental::listener::websocket_listener& listener, nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate_) + { + nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::send_control_protocol_ws_messages)); + + using web::json::value; + using web::json::value_of; + + // could start out as a shared/read lock, only upgraded to an exclusive/write lock when a grain in the resources is actually modified + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + auto& resources = model.control_protocol_resources; + + tai most_recent_message{}; + auto earliest_necessary_update = (tai_clock::time_point::max)(); + + for (;;) + { + // wait for the thread to be interrupted either because there are resource changes, or because the server is being shut down + // or because message sending was throttled earlier + details::wait_until(condition, lock, earliest_necessary_update, [&] { return shutdown || most_recent_message < most_recent_update(resources); }); + if (shutdown) break; + most_recent_message = most_recent_update(resources); + + slog::log(gate, SLOG_FLF) << "Got notification on control protocol websockets thread"; + + earliest_necessary_update = (tai_clock::time_point::max)(); + + std::vector> outgoing_messages; + + for (auto wit = websockets.left.begin(); websockets.left.end() != wit;) + { + const auto& websocket = *wit; + + // for each websocket connection that has valid grain and subscription resources + const auto grain = find_resource(resources, { websocket.first, nmos::types::grain }); + if (resources.end() == grain) + { + auto close = listener.close(websocket.second, web::websockets::websocket_close_status::server_terminate, U("Expired")) + .then(details::observe_websocket_exception(gate)); + // theoretically blocking, but in fact not + close.wait(); + + wit = websockets.left.erase(wit); + continue; + } + const auto subscription = find_resource(resources, { nmos::fields::subscription_id(grain->data), nmos::types::subscription }); + if (resources.end() == subscription) + { + // a grain without a subscription shouldn't be possible, but let's be tidy + erase_resource(resources, grain->id); + + auto close = listener.close(websocket.second, web::websockets::websocket_close_status::server_terminate, U("Expired")) + .then(details::observe_websocket_exception(gate)); + // theoretically blocking, but in fact not + close.wait(); + + wit = websockets.left.erase(wit); + continue; + } + // and has events to send + if (0 == nmos::fields::message_grain_data(grain->data).size()) + { + ++wit; + continue; + } + + slog::log(gate, SLOG_FLF) << "Preparing to send " << nmos::fields::message_grain_data(grain->data).size() << " events on websocket connection: " << grain->id; + + for (const auto& event : nmos::fields::message_grain_data(grain->data).as_array()) + { + web::websockets::websocket_outgoing_message message; + + slog::log(gate, SLOG_FLF) << "outgoing_message: " << event.serialize(); + message.set_utf8_message(utility::us2s(event.serialize())); + outgoing_messages.push_back({ websocket.second, message }); + } + + // reset the grain for next time + resources.modify(grain, [&resources](nmos::resource& grain) + { + // all messages have now been prepared + nmos::fields::message_grain_data(grain.data) = value::array(); + grain.updated = strictly_increasing_update(resources); + }); + + ++wit; + } + + // send the messages without the lock on resources + details::reverse_lock_guard unlock{ lock }; + + if (!outgoing_messages.empty()) slog::log(gate, SLOG_FLF) << "Sending " << outgoing_messages.size() << " websocket messages"; + + for (auto& outgoing_message : outgoing_messages) + { + // hmmm, no way to cancel this currently... + + auto send = listener.send(outgoing_message.first, outgoing_message.second) + .then(details::observe_websocket_exception(gate)); + // current websocket_listener implementation is synchronous in any case, but just to make clear... + // for now, wait for the message to be sent + send.wait(); + } + } + } +} diff --git a/Development/nmos/control_protocol_ws_api.h b/Development/nmos/control_protocol_ws_api.h new file mode 100644 index 000000000..23cd2ec35 --- /dev/null +++ b/Development/nmos/control_protocol_ws_api.h @@ -0,0 +1,35 @@ +#ifndef NMOS_CONTROL_PROTOCOL_WS_API_H +#define NMOS_CONTROL_PROTOCOL_WS_API_H + +#include "nmos/control_protocol_handlers.h" +#include "nmos/websockets.h" +#include "nmos/ws_api_utils.h" + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + struct node_model; + + web::websockets::experimental::listener::validate_handler make_control_protocol_ws_validate_handler(nmos::node_model& model, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate); + web::websockets::experimental::listener::open_handler make_control_protocol_ws_open_handler(nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate); + web::websockets::experimental::listener::close_handler make_control_protocol_ws_close_handler(nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate); + web::websockets::experimental::listener::message_handler make_control_protocol_ws_message_handler(nmos::node_model& model, nmos::websockets& websockets, nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, nmos::get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, nmos::get_control_protocol_method_descriptor_handler get_control_protocol_method_descriptor, nmos::control_protocol_property_changed_handler property_changed, slog::base_gate& gate); + + inline web::websockets::experimental::listener::websocket_listener_handlers make_control_protocol_ws_api(nmos::node_model& model, nmos::websockets& websockets, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, nmos::get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, nmos::get_control_protocol_method_descriptor_handler get_control_protocol_method_descriptor, nmos::control_protocol_property_changed_handler property_changed, slog::base_gate& gate) + { + return{ + nmos::make_control_protocol_ws_validate_handler(model, ws_validate_authorization, gate), + nmos::make_control_protocol_ws_open_handler(model, websockets, gate), + nmos::make_control_protocol_ws_close_handler(model, websockets, gate), + nmos::make_control_protocol_ws_message_handler(model, websockets, get_control_protocol_class_descriptor, get_control_protocol_datatype_descriptor, get_control_protocol_method_descriptor, property_changed, gate) + }; + } + + void send_control_protocol_ws_messages_thread(web::websockets::experimental::listener::websocket_listener& listener, nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate); +} + +#endif diff --git a/Development/nmos/device_type.h b/Development/nmos/device_type.h index e8bc356e1..3c611de65 100644 --- a/Development/nmos/device_type.h +++ b/Development/nmos/device_type.h @@ -6,8 +6,8 @@ namespace nmos { // Device types - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.1.%20APIs%20-%20Common%20Keys.md#type-devices - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/device.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.1._APIs_-_Common_Keys.html#type-devices + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/device.html DEFINE_STRING_ENUM(device_type) namespace device_types { diff --git a/Development/nmos/did_sdid.cpp b/Development/nmos/did_sdid.cpp index 26d0eff34..18ac0dda0 100644 --- a/Development/nmos/did_sdid.cpp +++ b/Development/nmos/did_sdid.cpp @@ -19,7 +19,7 @@ namespace nmos } // Data identification and Secondary data identification words - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_sdianc_data.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_sdianc_data.html utility::string_t make_did_or_sdid(const uint8_t& did_or_sdid) { utility::ostringstream_t os; @@ -38,7 +38,7 @@ namespace nmos } // Data identification and Secondary data identification words - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_sdianc_data.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_sdianc_data.html web::json::value make_did_sdid(const nmos::did_sdid& did_sdid) { return web::json::value_of({ diff --git a/Development/nmos/did_sdid.h b/Development/nmos/did_sdid.h index d7cbdd40f..04d0fc6dd 100644 --- a/Development/nmos/did_sdid.h +++ b/Development/nmos/did_sdid.h @@ -41,12 +41,12 @@ namespace nmos } // Data identification and Secondary data identification words - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_sdianc_data.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_sdianc_data.html utility::string_t make_did_or_sdid(const uint8_t& did_or_sdid); uint8_t parse_did_or_sdid(const utility::string_t& did_or_sdid); // Data identification and Secondary data identification words - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_sdianc_data.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_sdianc_data.html web::json::value make_did_sdid(const nmos::did_sdid& did_sdid); nmos::did_sdid parse_did_sdid(const web::json::value& did_sdid); diff --git a/Development/nmos/event_type.h b/Development/nmos/event_type.h index 224583bab..f3c3e6c8c 100644 --- a/Development/nmos/event_type.h +++ b/Development/nmos/event_type.h @@ -6,21 +6,21 @@ namespace nmos { // IS-07 Event & Tally event types - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/3.0.%20Event%20types.md + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html DEFINE_STRING_ENUM(event_type) namespace event_types { - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/3.0.%20Event%20types.md#21-boolean + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#21-boolean const event_type boolean{ U("boolean") }; - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/3.0.%20Event%20types.md#22-string + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#22-string const event_type string{ U("string") }; - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/3.0.%20Event%20types.md#23-number + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#23-number const event_type number{ U("number") }; - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/3.0.%20Event%20types.md#4-object + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#4-object-out-of-scope-for-version-10-of-this-specification // "The usage of the object event type is out of scope of this specification for version 1.0" const event_type object{ U("object") }; - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/3.0.%20Event%20types.md#231-measurements + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#231-measurements inline const event_type measurement(const utility::string_t& name, const utility::string_t& unit) { // specific measurement types are always "number/{Name}/{Unit}" @@ -28,13 +28,13 @@ namespace nmos return event_type{ number.name + U('/') + name + U('/') + unit }; } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/3.0.%20Event%20types.md#3-enum + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#3-enum inline const event_type named_enum(const event_type& base_type, const utility::string_t& name) { return event_type{ base_type.name + U("/enum/") + name }; } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/3.0.%20Event%20types.md#event-types-capability-management + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#event-types-capability-management // "A wildcard (*) must replace a whole word and can only be used at the end of an event_type definition." struct wildcard_type { @@ -76,11 +76,11 @@ namespace nmos } } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/3.0.%20Event%20types.md#event-types-capability-management + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#event-types-capability-management inline bool is_matching_event_type(const event_type& capability, const event_type& type) { // "Comparisons between event_type values must be case sensitive." - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/docs/3.0.%20Event%20types.md#1-introduction + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#1-introduction auto& c = capability.name; auto& t = type.name; // The wildcard in a partial event type matches zero or more 'levels', e.g. "number/*" matches both "number" and "number/temperature/C". diff --git a/Development/nmos/events_api.cpp b/Development/nmos/events_api.cpp index 9f357b205..ccef80e62 100644 --- a/Development/nmos/events_api.cpp +++ b/Development/nmos/events_api.cpp @@ -10,7 +10,7 @@ namespace nmos { web::http::experimental::listener::api_router make_unmounted_events_api(const nmos::node_model& model, slog::base_gate& gate); - web::http::experimental::listener::api_router make_events_api(const nmos::node_model& model, slog::base_gate& gate) + web::http::experimental::listener::api_router make_events_api(nmos::node_model& model, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -28,6 +28,12 @@ namespace nmos return pplx::task_from_result(true); }); + if (validate_authorization) + { + events_api.support(U("/x-nmos/") + nmos::patterns::events_api.pattern + U("/?"), validate_authorization); + events_api.support(U("/x-nmos/") + nmos::patterns::events_api.pattern + U("/.*"), validate_authorization); + } + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is07_versions::from_settings(model.settings); }); events_api.support(U("/x-nmos/") + nmos::patterns::events_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { @@ -141,8 +147,8 @@ namespace nmos // specified transports because it will pass from the source through a flow and out on the network through the sender." // Therefore, since the stored data in the event resources is also used to generate the messages on the transport, it // *should* include the flow id. It will be removed to generate the Events API /state response. - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/2.0.%20Message%20types.md#11-the-state-message-type - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/4.0.%20Core%20models.md#1-introduction + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/2.0._Message_types.html#11-the-state-message-type + // and https://specs.amwa.tv/is-07/releases/v1.0.1/docs/4.0._Core_models.html#1-introduction auto state = endpoint(resource->data); auto& identity = nmos::fields::identity(state); if (identity.has_field(nmos::fields::flow_id)) diff --git a/Development/nmos/events_api.h b/Development/nmos/events_api.h index 495309b6e..810b6563c 100644 --- a/Development/nmos/events_api.h +++ b/Development/nmos/events_api.h @@ -9,12 +9,17 @@ namespace slog } // Events API implementation -// See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/EventsAPI.raml +// See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/EventsAPI.html namespace nmos { struct node_model; - web::http::experimental::listener::api_router make_events_api(const nmos::node_model& model, slog::base_gate& gate); + web::http::experimental::listener::api_router make_events_api(nmos::node_model& model, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); + + inline web::http::experimental::listener::api_router make_events_api(nmos::node_model& model, slog::base_gate& gate) + { + return make_events_api(model, {}, gate); + } } #endif diff --git a/Development/nmos/events_resources.cpp b/Development/nmos/events_resources.cpp index be5a3cb95..53e714dba 100644 --- a/Development/nmos/events_resources.cpp +++ b/Development/nmos/events_resources.cpp @@ -22,14 +22,14 @@ namespace nmos namespace details { - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_core.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_core.html // "The flow_id will NOT be included in the response to a [REST API query for the] state because the state is held by // the source which has no dependency on a flow. It will, however, appear when being sent through one of the two // specified transports because it will pass from the source through a flow and out on the network through the sender." // Therefore, since the stored data in the event resources is also used to generate the messages on the transport, it // *should* include the flow id. It will be removed to generate the Events API /state response. - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/2.0.%20Message%20types.md#11-the-state-message-type - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/4.0.%20Core%20models.md#1-introduction + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/2.0._Message_types.html#11-the-state-message-type + // and https://specs.amwa.tv/is-07/releases/v1.0.1/docs/4.0._Core_models.html#1-introduction web::json::value make_events_state_identity(const nmos::details::events_state_identity& identity) { using web::json::value_of; @@ -40,7 +40,7 @@ namespace nmos }, true); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_core.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_core.html web::json::value make_events_state_timing(const nmos::details::events_state_timing& timing) { using web::json::value_of; @@ -52,7 +52,7 @@ namespace nmos }, true); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_core.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_core.html web::json::value make_events_state(const nmos::details::events_state_identity& identity, web::json::value payload, const nmos::event_type& type, const nmos::details::events_state_timing& timing) { using web::json::value_of; @@ -91,28 +91,28 @@ namespace nmos } } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_boolean.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_boolean.html web::json::value make_events_boolean_state(const nmos::details::events_state_identity& identity, bool payload_value, const nmos::event_type& type, const nmos::details::events_state_timing& timing) { // should check type is nmos::event_types::boolean or a derived type return details::make_events_state(identity, details::make_payload(payload_value), type, timing); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_number.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_number.html web::json::value make_events_number_state(const nmos::details::events_state_identity& identity, const events_number& payload, const nmos::event_type& type, const nmos::details::events_state_timing& timing) { // should check type is nmos::event_types::number or a derived type return details::make_events_state(identity, details::make_payload(payload), type, timing); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_string.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_string.html web::json::value make_events_string_state(const nmos::details::events_state_identity& identity, const utility::string_t& payload_value, const nmos::event_type& type, const nmos::details::events_state_timing& timing) { // should check type is nmos::event_types::string or a derived type return details::make_events_state(identity, details::make_payload(payload_value), type, timing); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_object.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_object.html // (out of scope for version 1.0 of this specification) web::json::value make_events_object_state(const nmos::details::events_state_identity& identity, const web::json::value& payload, const nmos::event_type& type, const nmos::details::events_state_timing& timing) { @@ -120,7 +120,7 @@ namespace nmos return details::make_events_state(identity, payload, type, timing); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type_boolean.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type_boolean.html web::json::value make_events_boolean_type() { using web::json::value_of; @@ -128,7 +128,7 @@ namespace nmos return value_of({ { U("type"), nmos::event_types::boolean.name } }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type_number.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type_number.html web::json::value make_events_number_type(const events_number& min, const events_number& max, const events_number& step, const utility::string_t& unit, int64_t scale) { using web::json::value_of; @@ -143,7 +143,7 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type_string.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type_string.html web::json::value make_events_string_type(int64_t min_length, int64_t max_length, const utility::string_t& pattern) { using web::json::value_of; @@ -172,7 +172,7 @@ namespace nmos } } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type_boolean_enum.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type_boolean_enum.html // hmm, map or vector-of-pair? web::json::value make_events_boolean_enum_type(const std::vector>& values) { @@ -185,7 +185,7 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type_number_enum.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type_number_enum.html web::json::value make_events_number_enum_type(const std::vector>& values) { // hmm, web::json::number rather than double? @@ -199,7 +199,7 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type_string_enum.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type_string_enum.html web::json::value make_events_string_enum_type(const std::vector>& values) { using web::json::value_of; @@ -219,7 +219,7 @@ namespace nmos return value_of({ { U("type"), nmos::event_types::object.name } }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/APIs/schemas/command_subscription.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/command_subscription.html web::json::value make_events_subscription_command(const std::vector& sources) { using web::json::value_of; @@ -231,7 +231,7 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/command_health.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/command_health.html web::json::value make_events_health_command(const nmos::tai& timestamp) { using web::json::value_of; diff --git a/Development/nmos/events_resources.h b/Development/nmos/events_resources.h index 4506672cd..5be71b6ec 100644 --- a/Development/nmos/events_resources.h +++ b/Development/nmos/events_resources.h @@ -11,20 +11,20 @@ namespace nmos struct resource; // IS-07 Events API resources - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/6.0.%20Event%20and%20tally%20rest%20api.md#1-introduction + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/6.0._Event_and_tally_rest_api.html#1-introduction // Each IS-07 source's data is a json object with an "id" field // and a field for the Events API endpoints of that logical single resource // i.e. // a "type" field, which must have a value conforming to the type schema, // and a "state" field, which must have a value conforming to the event schema - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type.json - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type.html + // and https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event.html // For WebSocket connections, subscription and grain resources will also be added nmos::resource make_events_source(const nmos::id& id, const web::json::value& state, const web::json::value& type); // Events API source state - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/6.0.%20Event%20and%20tally%20rest%20api.md#3-usage + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/6.0._Event_and_tally_rest_api.html#3-usage namespace details { @@ -52,21 +52,21 @@ namespace nmos nmos::tai action_timestamp; }; - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_core.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_core.html // "The flow_id will NOT be included in the response to a [REST API query for the] state because the state is held by // the source which has no dependency on a flow. It will, however, appear when being sent through one of the two // specified transports because it will pass from the source through a flow and out on the network through the sender." // Therefore, since the stored data in the event resources is also used to generate the messages on the transport, it // *should* include the flow id. It will be removed to generate the Events API /state response. - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/2.0.%20Message%20types.md#11-the-state-message-type - // and https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/4.0.%20Core%20models.md#1-introduction + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/2.0._Message_types.html#11-the-state-message-type + // and https://specs.amwa.tv/is-07/releases/v1.0.1/docs/4.0._Core_models.html#1-introduction web::json::value make_events_state_identity(const nmos::details::events_state_identity& identity); - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_core.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_core.html web::json::value make_events_state_timing(const nmos::details::events_state_timing& timing); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/number.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/number.html struct events_number { events_number() : value(), scale() {} @@ -81,28 +81,28 @@ namespace nmos int64_t scale; }; - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_boolean.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_boolean.html web::json::value make_events_boolean_state(const nmos::details::events_state_identity& identity, bool payload_value, const nmos::event_type& type = nmos::event_types::boolean, const nmos::details::events_state_timing& timing = {}); inline web::json::value make_events_boolean_state(const nmos::details::events_state_identity& identity, bool payload_value, const nmos::details::events_state_timing& timing) { return make_events_boolean_state(identity, payload_value, nmos::event_types::boolean, timing); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_number.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_number.html web::json::value make_events_number_state(const nmos::details::events_state_identity& identity, const events_number& payload, const nmos::event_type& type = nmos::event_types::number, const nmos::details::events_state_timing& timing = {}); inline web::json::value make_events_number_state(const nmos::details::events_state_identity& identity, const events_number& payload, const nmos::details::events_state_timing& timing) { return make_events_number_state(identity, payload, nmos::event_types::number, timing); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_string.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_string.html web::json::value make_events_string_state(const nmos::details::events_state_identity& identity, const utility::string_t& payload_value, const nmos::event_type& type = nmos::event_types::string, const nmos::details::events_state_timing& timing = {}); inline web::json::value make_events_string_state(const nmos::details::events_state_identity& identity, const utility::string_t& payload_value, const nmos::details::events_state_timing& timing) { return make_events_string_state(identity, payload_value, nmos::event_types::string, timing); } - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/event_object.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/event_object.html // (out of scope for version 1.0 of this specification) web::json::value make_events_object_state(const nmos::details::events_state_identity& identity, const web::json::value& payload, const nmos::event_type& type = nmos::event_types::object, const nmos::details::events_state_timing& timing = {}); inline web::json::value make_events_object_state(const nmos::details::events_state_identity& identity, const web::json::value& payload, const nmos::details::events_state_timing& timing) @@ -111,15 +111,15 @@ namespace nmos } // Events API source type - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/6.0.%20Event%20and%20tally%20rest%20api.md#3-usage + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/6.0._Event_and_tally_rest_api.html#3-usage - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type_boolean.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type_boolean.html web::json::value make_events_boolean_type(); - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type_number.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type_number.html web::json::value make_events_number_type(const events_number& min, const events_number& max, const events_number& step = {}, const utility::string_t& unit = {}, int64_t scale = {}); - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type_string.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type_string.html web::json::value make_events_string_type(int64_t min_length = {}, int64_t max_length = {}, const utility::string_t& pattern = {}); struct events_enum_element_details @@ -132,14 +132,14 @@ namespace nmos utility::string_t description; }; - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type_boolean_enum.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type_boolean_enum.html // hmm, map or vector-of-pair? web::json::value make_events_boolean_enum_type(const std::vector>& values); - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type_number_enum.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type_number_enum.html web::json::value make_events_number_enum_type(const std::vector>& values); - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/type_string_enum.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/type_string_enum.html web::json::value make_events_string_enum_type(const std::vector>& values); // (out of scope for version 1.0 of this specification) @@ -147,12 +147,12 @@ namespace nmos // Events commands // These are not resources, so maybe these belong in their own file, e.g. nmos/events_commands.h? - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/docs/5.2.%20Transport%20-%20Websocket.md#4-subscriptions-strategy + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#4-subscriptions-strategy - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/APIs/schemas/command_subscription.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/command_subscription.html web::json::value make_events_subscription_command(const std::vector& sources); - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/APIs/schemas/command_health.json + // See https://specs.amwa.tv/is-07/releases/v1.0.1/APIs/schemas/with-refs/command_health.html web::json::value make_events_health_command(const nmos::tai& timestamp = tai_now()); } diff --git a/Development/nmos/events_ws_api.cpp b/Development/nmos/events_ws_api.cpp index 008522ff4..1fbbefa97 100644 --- a/Development/nmos/events_ws_api.cpp +++ b/Development/nmos/events_ws_api.cpp @@ -3,12 +3,15 @@ #include #include "cpprest/json_storage.h" #include "nmos/api_utils.h" +#include "nmos/authorization.h" +#include "nmos/authorization_state.h" #include "nmos/is07_versions.h" #include "nmos/log_manip.h" #include "nmos/model.h" #include "nmos/query_utils.h" #include "nmos/rational.h" #include "nmos/thread_utils.h" // for wait_until +#include "nmos/scope.h" #include "nmos/slog.h" #include "nmos/version.h" @@ -23,18 +26,22 @@ namespace nmos // by the IS-04 Registration API, so this implementation also shares much commonality. // See nmos/query_ws_api.cpp and nmos/registration_api.cpp - web::websockets::experimental::listener::validate_handler make_events_ws_validate_handler(nmos::node_model& model, slog::base_gate& gate_) + web::websockets::experimental::listener::validate_handler make_events_ws_validate_handler(nmos::node_model& model, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate_) { - return [&model, &gate_](web::http::http_request req) + return [&model, ws_validate_authorization, &gate_](web::http::http_request req) { nmos::ws_api_gate gate(gate_, req.request_uri()); - auto lock = model.read_lock(); + auto lock = model.write_lock(); auto& resources = model.connection_resources; // RFC 6750 defines two methods of sending bearer access tokens which are applicable to WebSocket // Clients SHOULD use the "Authorization Request Header Field" method. // Clients MAY use a "URI Query Parameter". // See https://tools.ietf.org/html/rfc6750#section-2 + if (ws_validate_authorization) + { + if (!ws_validate_authorization(req, nmos::experimental::scopes::events)) { return false; } + } // For now, to determine whether the "resource name" is valid, only look at the path, and ignore any query parameters const auto& ws_resource_path = req.request_uri().path(); @@ -286,7 +293,7 @@ namespace nmos } // reboot message - // see https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/2.0.%20Message%20types.md#12-the-reboot-message-type + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/2.0._Message_types.html#12-the-reboot-message-type web::json::value make_events_reboot_message(const nmos::details::events_state_identity& identity, const nmos::details::events_state_timing& timing) { using web::json::value_of; @@ -299,7 +306,7 @@ namespace nmos } // shutdown message - // see https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/2.0.%20Message%20types.md#13-the-shutdown-message-type + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/2.0._Message_types.html#13-the-shutdown-message-type web::json::value make_events_shutdown_message(const nmos::details::events_state_identity& identity, const nmos::details::events_state_timing& timing) { using web::json::value_of; @@ -312,7 +319,7 @@ namespace nmos } // health message - // see https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/2.0.%20Message%20types.md#15-the-health-message + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/2.0._Message_types.html#15-the-health-message-type web::json::value make_events_health_message(const nmos::details::events_state_timing& timing) { using web::json::value_of; @@ -427,7 +434,7 @@ namespace nmos else if (event.has_field(U("post"))) { // state message - // see https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/2.0.%20Message%20types.md#11-the-state-message-type + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/2.0._Message_types.html#11-the-state-message-type // and nmos::make_events_boolean_state, nmos::make_events_number_state, etc. // and nmos::details::make_resource_event const web::json::value& state = nmos::fields::endpoint_state(event.at(U("post"))); diff --git a/Development/nmos/events_ws_api.h b/Development/nmos/events_ws_api.h index d4f27cf05..09f12679c 100644 --- a/Development/nmos/events_ws_api.h +++ b/Development/nmos/events_ws_api.h @@ -1,8 +1,10 @@ #ifndef NMOS_EVENTS_WS_API_H #define NMOS_EVENTS_WS_API_H +#include "nmos/authorization_handlers.h" #include "nmos/events_resources.h" #include "nmos/websockets.h" +#include "nmos/ws_api_utils.h" namespace slog { @@ -10,20 +12,20 @@ namespace slog } // Events API websocket implementation -// See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md +// See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html namespace nmos { struct node_model; - web::websockets::experimental::listener::validate_handler make_events_ws_validate_handler(nmos::node_model& model, slog::base_gate& gate); + web::websockets::experimental::listener::validate_handler make_events_ws_validate_handler(nmos::node_model& model, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate); web::websockets::experimental::listener::open_handler make_events_ws_open_handler(nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate); web::websockets::experimental::listener::close_handler make_events_ws_close_handler(nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate); web::websockets::experimental::listener::message_handler make_events_ws_message_handler(nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate); - inline web::websockets::experimental::listener::websocket_listener_handlers make_events_ws_api(nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate) + inline web::websockets::experimental::listener::websocket_listener_handlers make_events_ws_api(nmos::node_model& model, nmos::websockets& websockets, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate) { return{ - nmos::make_events_ws_validate_handler(model, gate), + nmos::make_events_ws_validate_handler(model, ws_validate_authorization, gate), nmos::make_events_ws_open_handler(model, websockets, gate), nmos::make_events_ws_close_handler(model, websockets, gate), nmos::make_events_ws_message_handler(model, websockets, gate) @@ -34,15 +36,15 @@ namespace nmos // Maybe these belong in their own file, e.g. nmos/events_messages.h? // reboot message - // see https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/docs/2.0.%20Message%20types.md#12-the-reboot-message-type + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/2.0._Message_types.html#12-the-reboot-message-type web::json::value make_events_reboot_message(const nmos::details::events_state_identity& identity, const nmos::details::events_state_timing& timing = {}); // shutdown message - // see https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/docs/2.0.%20Message%20types.md#13-the-shutdown-message-type + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/2.0._Message_types.html#13-the-shutdown-message-type web::json::value make_events_shutdown_message(const nmos::details::events_state_identity& identity, const nmos::details::events_state_timing& timing = {}); // health message - // see https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/2.0.%20Message%20types.md#15-the-health-message + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/2.0._Message_types.html#15-the-health-message-type web::json::value make_events_health_message(const nmos::details::events_state_timing& timing); void send_events_ws_messages_thread(web::websockets::experimental::listener::websocket_listener& listener, nmos::node_model& model, nmos::websockets& websockets, slog::base_gate& gate); diff --git a/Development/nmos/events_ws_client.cpp b/Development/nmos/events_ws_client.cpp index 8ca4275c5..6e0486bb2 100644 --- a/Development/nmos/events_ws_client.cpp +++ b/Development/nmos/events_ws_client.cpp @@ -91,8 +91,6 @@ namespace nmos nmos::read_lock read_lock() const { return nmos::read_lock{ mutex }; } nmos::write_lock write_lock() const { return nmos::write_lock{ mutex }; } - static void wait_nothrow(pplx::task t) { try { t.wait(); } catch (...) {} } - static nmos::details::omanip_gate make_gate(slog::base_gate& gate) { return{ gate, nmos::stash_category(nmos::categories::send_events_ws_commands) }; } }; @@ -150,7 +148,7 @@ namespace nmos // "A disconnection IS-05 PATCH request should always trigger the client to remove the associated source id // from the current WebSocket subscriptions list. If the source is the last item in the subscriptions list, // then it is recommended for the client to close the underlying WebSocket connection." - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0.1/docs/5.2.%20Transport%20-%20Websocket.md#35-disconnectingparking + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#35-disconnectingparking // Doesn't seem much point in sending an empty subscription command, so just close the connection in that case... auto& by_connection_uri = subscriptions.get(); @@ -244,7 +242,7 @@ namespace nmos } // hmm, should probably try to re-make the connection, possibly with exponential back-off, for ephemeral error conditions - // see https://github.com/AMWA-TV/nmos-device-connection-management/issues/96 + // see https://github.com/AMWA-TV/is-05/issues/96 // for now, just signal via external handler @@ -268,7 +266,7 @@ namespace nmos auto heartbeats = result.then([this, client, token]() mutable { // "Upon connection, the client is required to report its health every 5 seconds in order to maintain its session and subscription." - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md#41-heartbeats + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#41-heartbeats return pplx::do_while([this, client, token]() mutable { @@ -283,7 +281,7 @@ namespace nmos .then([] { return true; }); }); }, token); - }).then(pplx::observe_exception()); + }).then(pplx::observe_exception()); connection = connections.insert({ connection_uri, client }).first; } @@ -335,11 +333,7 @@ namespace nmos subscriptions.clear(); connections.clear(); - return pplx::when_all(tasks.begin(), tasks.end()).then([tasks](pplx::task finally) - { - for (auto& task : tasks) wait_nothrow(task); - finally.wait(); - }); + return pplx::ranges::when_all(tasks).then(pplx::observe_exceptions(tasks)); } } diff --git a/Development/nmos/events_ws_client.h b/Development/nmos/events_ws_client.h index 030bfa916..2437c3a79 100644 --- a/Development/nmos/events_ws_client.h +++ b/Development/nmos/events_ws_client.h @@ -18,7 +18,7 @@ namespace slog } // Events API websocket implementation -// See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md +// See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html namespace nmos { namespace details diff --git a/Development/nmos/format.h b/Development/nmos/format.h index 46f6d6ec8..eaa455c20 100644 --- a/Development/nmos/format.h +++ b/Development/nmos/format.h @@ -6,10 +6,10 @@ namespace nmos { // Formats (used in sources, flows and receivers) - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.1.%20APIs%20-%20Common%20Keys.md#format - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_generic.json - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_audio.json - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_video.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.1._APIs_-_Common_Keys.html#format + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_generic.html + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_audio.html + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_video.html // etc. DEFINE_STRING_ENUM(format) namespace formats diff --git a/Development/nmos/group_hint.cpp b/Development/nmos/group_hint.cpp index cbd264f58..79373ded1 100644 --- a/Development/nmos/group_hint.cpp +++ b/Development/nmos/group_hint.cpp @@ -6,7 +6,7 @@ namespace nmos { utility::string_t make_group_hint(const group_hint& group_hint) { - return group_hint.optional_group_scope.name.empty() + return group_hint.optional_group_scope.empty() ? group_hint.group_name + U(':') + group_hint.role_in_group : group_hint.group_name + U(':') + group_hint.role_in_group + U(':') + group_hint.optional_group_scope.name; } diff --git a/Development/nmos/group_hint.h b/Development/nmos/group_hint.h index 67b4d724a..7047321d1 100644 --- a/Development/nmos/group_hint.h +++ b/Development/nmos/group_hint.h @@ -5,7 +5,7 @@ #include "nmos/string_enum.h" // Group Hint -// See https://github.com/AMWA-TV/nmos-parameter-registers/blob/master/tags/grouphint.md +// See https://specs.amwa.tv/nmos-parameter-registers/branches/main/tags/grouphint.html namespace nmos { namespace fields diff --git a/Development/nmos/id.h b/Development/nmos/id.h index 3a67ee870..bb06d58c5 100644 --- a/Development/nmos/id.h +++ b/Development/nmos/id.h @@ -7,7 +7,7 @@ namespace nmos { // "Each logical entity is identified by a UUID" - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/5.1.%20Data%20Model%20-%20Identifier%20Mapping.md + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/5.1._Data_Model_-_Identifier_Mapping.html // Since identifiers are passed as strings in the APIs, and the formatting of identifiers has been a little // inconsistent between implementations in the past, they are currently stored simply as strings... diff --git a/Development/nmos/interlace_mode.h b/Development/nmos/interlace_mode.h index a9c466b4e..647d0f31c 100644 --- a/Development/nmos/interlace_mode.h +++ b/Development/nmos/interlace_mode.h @@ -6,7 +6,7 @@ namespace nmos { // Interlace modes (used in video flows) - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_video.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_video.html DEFINE_STRING_ENUM(interlace_mode) namespace interlace_modes { diff --git a/Development/nmos/is10_schemas/is10_schemas.h b/Development/nmos/is10_schemas/is10_schemas.h new file mode 100644 index 000000000..a250b5631 --- /dev/null +++ b/Development/nmos/is10_schemas/is10_schemas.h @@ -0,0 +1,27 @@ +#ifndef NMOS_IS10_SCHEMAS_H +#define NMOS_IS10_SCHEMAS_H + +// Extern declarations for auto-generated constants +// could be auto-generated, but isn't currently! +namespace nmos +{ + namespace is10_schemas + { + namespace v1_0_x + { + extern const char* auth_clients_schema; + + extern const char* auth_metadata; + extern const char* jwks_response; + extern const char* jwks_schema; + extern const char* register_client_error_response; + extern const char* register_client_request; + extern const char* register_client_response; + extern const char* token_error_response; + extern const char* token_response; + extern const char* token_schema; + } + } +} + +#endif diff --git a/Development/nmos/is10_versions.h b/Development/nmos/is10_versions.h new file mode 100644 index 000000000..89e991afc --- /dev/null +++ b/Development/nmos/is10_versions.h @@ -0,0 +1,26 @@ +#ifndef NMOS_IS10_VERSIONS_H +#define NMOS_IS10_VERSIONS_H + +#include +#include +#include "nmos/api_version.h" +#include "nmos/settings.h" + +namespace nmos +{ + namespace is10_versions + { + const api_version v1_0{ 1, 0 }; + + const std::set all{ nmos::is10_versions::v1_0 }; + + inline std::set from_settings(const nmos::settings& settings) + { + return settings.has_field(nmos::fields::is10_versions) + ? boost::copy_range>(nmos::fields::is10_versions(settings) | boost::adaptors::transformed([](const web::json::value& v) { return nmos::parse_api_version(v.as_string()); })) + : nmos::is10_versions::all; + } + } +} + +#endif diff --git a/Development/nmos/is12_schemas/is12_schemas.h b/Development/nmos/is12_schemas/is12_schemas.h new file mode 100644 index 000000000..392b57648 --- /dev/null +++ b/Development/nmos/is12_schemas/is12_schemas.h @@ -0,0 +1,25 @@ +#ifndef NMOS_IS12_SCHEMAS_H +#define NMOS_IS12_SCHEMAS_H + +// Extern declarations for auto-generated constants +// could be auto-generated, but isn't currently! +namespace nmos +{ + namespace is12_schemas + { + namespace v1_0_x + { + extern const char* base_message; + extern const char* command_message; + extern const char* command_response_message; + extern const char* error_message; + extern const char* event_data; + extern const char* notification_message; + extern const char* property_changed_event_data; + extern const char* subscription_message; + extern const char* subscription_response_message; + } + } +} + +#endif diff --git a/Development/nmos/is12_versions.h b/Development/nmos/is12_versions.h new file mode 100644 index 000000000..06dfc1d59 --- /dev/null +++ b/Development/nmos/is12_versions.h @@ -0,0 +1,26 @@ +#ifndef NMOS_IS12_VERSIONS_H +#define NMOS_IS12_VERSIONS_H + +#include +#include +#include "nmos/api_version.h" +#include "nmos/settings.h" + +namespace nmos +{ + namespace is12_versions + { + const api_version v1_0{ 1, 0 }; + + const std::set all{ nmos::is12_versions::v1_0 }; + + inline std::set from_settings(const nmos::settings& settings) + { + return settings.has_field(nmos::fields::is12_versions) + ? boost::copy_range>(nmos::fields::is12_versions(settings) | boost::adaptors::transformed([](const web::json::value& v) { return nmos::parse_api_version(v.as_string()); })) + : nmos::is12_versions::all; + } + } +} + +#endif diff --git a/Development/nmos/issuers.h b/Development/nmos/issuers.h new file mode 100644 index 000000000..bc1002462 --- /dev/null +++ b/Development/nmos/issuers.h @@ -0,0 +1,25 @@ +#ifndef NMOS_ISSUERS_H +#define NMOS_ISSUERS_H + +#include "cpprest/json.h" +#include "nmos/jwt_validator.h" + +namespace nmos +{ + namespace experimental + { + struct issuer + { + web::json::value settings; // [U("authorization_server_metadata")], [U("jwks")], [U("client_metadata")], + // where: + // "authorization_server_metadata": issuer (authorization server) metadata + // "jwks": issuer jwks + // "client_metadata": client (Node/Registry) metadata + nmos::experimental::jwt_validator jwt_validator; + }; + + typedef std::map issuers; // where uri: issuer (authorization server) uri + } +} + +#endif diff --git a/Development/nmos/json_fields.h b/Development/nmos/json_fields.h index 445806597..7374c5a90 100644 --- a/Development/nmos/json_fields.h +++ b/Development/nmos/json_fields.h @@ -30,13 +30,13 @@ namespace nmos // Field accessors simplify access to fields in json request bodies namespace fields { - const web::json::field_as_string id{ U("id") }; + const web::json::field_as_string id{ U("id") }; // see nmos::id const web::json::field version{ U("version") }; // IS-04 Discovery and Registration // (mostly) for node_api and registration_api - const web::json::field_as_string type{ U("type") }; + const web::json::field_as_string type{ U("type") }; // see nmos::type const web::json::field_as_value data{ U("data") }; // resource_core const web::json::field_as_string label{ U("label") }; @@ -50,59 +50,60 @@ namespace nmos const web::json::field_as_string port_id{ U("port_id") }; const web::json::field_as_value_or attached_network_device{ U("attached_network_device"), {} }; // object const web::json::field_as_array clocks{ U("clocks") }; - const web::json::field_as_string ref_type{ U("ref_type") }; + const web::json::field_as_string ref_type{ U("ref_type") }; // see nmos::clock_ref_type const web::json::field_as_string ptp_version{ U("version") }; const web::json::field_as_string gmid{ U("gmid") }; const web::json::field_as_bool traceable{ U("traceable") }; const web::json::field_as_bool locked{ U("locked") }; // device - const web::json::field_as_string node_id{ U("node_id") }; - const web::json::field_as_array senders{ U("senders") }; // deprecated - const web::json::field_as_array receivers{ U("receivers") }; // deprecated + const web::json::field_as_string node_id{ U("node_id") }; // see nmos::id + const web::json::field_as_array senders{ U("senders") }; // deprecated, see nmos::id + const web::json::field_as_array receivers{ U("receivers") }; // deprecated, see nmos::id // source_core - const web::json::field_as_string device_id{ U("device_id") }; // also used in also used in sender, receiver, and flow from v1.1 - const web::json::field_as_array parents{ U("parents") }; // also used in flow - const web::json::field_as_value clock_name{ U("clock_name") }; // string or null - const web::json::field_as_string format{ U("format") }; // also used in flow + const web::json::field_as_string device_id{ U("device_id") }; // also used in also used in sender, receiver, and flow from v1.1, see nmos::id + const web::json::field_as_array parents{ U("parents") }; // also used in flow, see nmos::id + const web::json::field_as_value clock_name{ U("clock_name") }; // string or null, see nmos::clock_name + const web::json::field_as_string format{ U("format") }; // also used in flow, see nmos::format // source_audio const web::json::field_as_array channels{ U("channels") }; - const web::json::field_as_string_or symbol{ U("symbol"), U("") }; // or nmos::channel_symbol? + const web::json::field_as_string_or symbol{ U("symbol"), U("") }; // see nmos::channel_symbol // flow_core - const web::json::field_as_string source_id{ U("source_id") }; - const web::json::field_as_value grain_rate{ U("grain_rate") }; // or field with a bit of work! + const web::json::field_as_string source_id{ U("source_id") }; // see nmos::id + const web::json::field_as_value grain_rate{ U("grain_rate") }; // see nmos::make_rational, etc. const web::json::field_as_integer numerator{ U("numerator") }; const web::json::field_as_integer_or denominator{ U("denominator"), 1 }; + const web::json::field_as_string media_type{ U("media_type") }; // strictly speaking, defined in flow_video_raw, flow_video_coded, etc., see nmos::media_type // flow_video const web::json::field_as_integer frame_width{ U("frame_width") }; const web::json::field_as_integer frame_height{ U("frame_height") }; - const web::json::field_as_string colorspace{ U("colorspace") }; + const web::json::field_as_string colorspace{ U("colorspace") }; // see nmos::colorspace // flow_video_raw - const web::json::field_as_array components{ U("components") }; - const web::json::field_as_string_or transfer_characteristic{ U("transfer_characteristic"), U("") }; // or "SDR"? - const web::json::field_as_string_or interlace_mode{ U("interlace_mode"), U("") }; // or "progressive"? + const web::json::field_as_array components{ U("components") }; // see nmos::make_components, etc. + const web::json::field_as_string_or transfer_characteristic{ U("transfer_characteristic"), U("") }; // if omitted (empty), assume "SDR", see nmos::transfer_characteristic + const web::json::field_as_string_or interlace_mode{ U("interlace_mode"), U("") }; // if omitted (empty), assume "progressive", see nmos::interlace_mode const web::json::field_as_integer width{ U("width") }; const web::json::field_as_integer height{ U("height") }; const web::json::field_as_integer bit_depth{ U("bit_depth") }; // also used in flow_audio_raw // flow_audio - const web::json::field_as_value sample_rate{ U("sample_rate") }; + const web::json::field_as_value sample_rate{ U("sample_rate") }; // see nmos::rational // flow_sdianc_data - const web::json::field_as_value_or DID_SDID{ U("DID_SDID"), web::json::value::array() }; + const web::json::field_as_value_or DID_SDID{ U("DID_SDID"), web::json::value::array() }; // see nmos::did_sdid const web::json::field_as_string_or DID{ U("DID"), U("0x00") }; // probably an oversight this isn't required const web::json::field_as_string_or SDID{ U("SDID"), U("0x00") }; // sender const web::json::field_as_array interface_bindings{ U("interface_bindings") }; // also used in receiver const web::json::field_as_string transport{ U("transport") }; // also used in receiver - const web::json::field_as_value flow_id{ U("flow_id") }; + const web::json::field_as_value flow_id{ U("flow_id") }; // see nmos::id const web::json::field_as_value_or manifest_href{ U("manifest_href"), {} }; // string, or null from v1.3 const web::json::field_as_value subscription{ U("subscription") }; // from v1.2; also used in receiver from v1.0 - const web::json::field_as_value receiver_id{ U("receiver_id") }; // used in sender subscription + const web::json::field_as_value receiver_id{ U("receiver_id") }; // used in sender subscription, see nmos::id const web::json::field_as_bool active{ U("active") }; // used in sender subscription; also used in receiver subscription from v1.2 // receiver_core - const web::json::field_as_value sender_id{ U("sender_id") }; // used in receiver subscription + const web::json::field_as_value sender_id{ U("sender_id") }; // used in receiver subscription, see nmos::id // receiver_audio, receiver_data, receiver_mux, receiver_video - const web::json::field_as_value_or media_types{ U("media_types"), {} }; // array of string; used in receiver caps + const web::json::field_as_value_or media_types{ U("media_types"), {} }; // array of string; used in receiver caps, see nmos::media_type // receiver_data - const web::json::field_as_value_or event_types{ U("event_types"), {} }; // array of string; used in receiver caps + const web::json::field_as_value_or event_types{ U("event_types"), {} }; // array of string; used in receiver caps, see nmos::event_type // (mostly) for query_api const web::json::field_as_bool persist{ U("persist") }; @@ -123,8 +124,8 @@ namespace nmos // for connection_api const web::json::field_as_value endpoint_constraints{ U("constraints") }; // array - const web::json::field constraint_maximum{ U("maximum") }; // integer, number - const web::json::field constraint_minimum{ U("minimum") }; // integer, number + const web::json::field_as_value constraint_maximum{ U("maximum") }; // integer, number or rational + const web::json::field_as_value constraint_minimum{ U("minimum") }; // integer, number or rational const web::json::field_as_value constraint_enum{ U("enum") }; // array const web::json::field_as_string constraint_pattern{ U("pattern") }; // regex const web::json::field_as_string_or constraint_description{ U("description"), {} }; // string @@ -136,9 +137,9 @@ namespace nmos const web::json::field_as_string transportfile_href{ U("href") }; const web::json::field_as_bool master_enable{ U("master_enable") }; const web::json::field_as_value_or activation{ U("activation"), {} }; // object - const web::json::field_as_value_or mode{ U("mode"), {} }; // string or null - const web::json::field_as_value_or requested_time{ U("requested_time"), {} }; // string or null - const web::json::field_as_value_or activation_time{ U("activation_time"), {} }; // string or null + const web::json::field_as_value_or mode{ U("mode"), {} }; // string or null, see nmos::activation_mode + const web::json::field_as_value_or requested_time{ U("requested_time"), {} }; // string or null, see nmos::tai + const web::json::field_as_value_or activation_time{ U("activation_time"), {} }; // string or null, see nmos::tai const web::json::field_as_value_or transport_file{ U("transport_file"), {} }; // object const web::json::field_as_value transport_params{ U("transport_params") }; // array @@ -190,7 +191,7 @@ namespace nmos // for events_ws_api messages const web::json::field_as_string message_type{ U("message_type") }; // for "state" messages - const web::json::field_as_string state_event_type{ U("event_type") }; + const web::json::field_as_string state_event_type{ U("event_type") }; // see nmos::event_type const web::json::field_as_value state_payload{ U("payload") }; const web::json::field_as_bool payload_boolean_value{ U("value") }; const web::json::field_as_number payload_number_value{ U("value") }; @@ -224,8 +225,192 @@ namespace nmos const web::json::field_as_value ptp{ U("ptp") }; const web::json::field_as_integer announce_receipt_timeout{ U("announce_receipt_timeout") }; // 2..10, typically 3 const web::json::field_as_integer domain_number{ U("domain_number") }; // 0..127 + const web::json::field_as_value_or syslog{ U("syslog"), {} }; + const web::json::field_as_value_or syslogv2{ U("syslogv2"), {} }; + const web::json::field_as_string hostname{ U("hostname") }; // hostname, ipv4 or ipv6 + const web::json::field_as_integer port{ U("port") }; // 1..65535 + + // IS-12 Control Protocol and MS-05 model definitions + namespace nc + { + // for control_protocol_ws_api + const web::json::field_as_integer message_type{ U("messageType") }; + + // for control_protocol_ws_api commands + const web::json::field_as_array commands{ U("commands") }; + const web::json::field_as_array subscriptions{ U("subscriptions") }; + const web::json::field_as_integer oid{ U("oid") }; + const web::json::field_as_value method_id{ U("methodId") }; + const web::json::field_as_value_or arguments{ U("arguments"), {} }; + const web::json::field_as_value id{ U("id") }; + const web::json::field_as_integer level{ U("level") }; + const web::json::field_as_integer index{ U("index") }; + + // for control_protocol_ws_api responses & errors + const web::json::field_as_value responses{ U("responses") }; + const web::json::field_as_value result{ U("result") }; + const web::json::field_as_integer status{ U("status") }; + const web::json::field_as_value value{ U("value") }; + const web::json::field_as_string error_message{ U("errorMessage") }; + + // for control_protocol_ws_api commands & responses + const web::json::field_as_integer handle{ U("handle") }; + + // for cntrol_protocol_ws_api notifications + const web::json::field_as_array notifications{ U("notifications") }; + const web::json::field_as_value event_data{ U("eventData") }; + const web::json::field_as_value event_id{ U("eventId") }; + + const web::json::field_as_array class_id{ U("classId") }; + const web::json::field_as_bool constant_oid{ U("constantOid") }; + const web::json::field_as_integer owner{ U("owner") }; + const web::json::field_as_string role{ U("role") }; + const web::json::field_as_string user_label{ U("userLabel") }; + const web::json::field_as_array touchpoints{ U("touchpoints") }; + const web::json::field_as_array runtime_property_constraints{ U("runtimePropertyConstraints") }; + const web::json::field_as_bool recurse{ U("recurse") }; + const web::json::field_as_bool enabled{ U("enabled") }; + const web::json::field_as_array members{ U("members") }; + const web::json::field_as_string description{ U("description") }; + const web::json::field_as_string nc_version{ U("ncVersion") }; // NcVersionCode + const web::json::field_as_value manufacturer{ U("manufacturer") }; // NcManufacturer + const web::json::field_as_value product{ U("product") }; // NcProduct + const web::json::field_as_string serial_number{ U("serialNumber") }; + const web::json::field_as_string user_inventory_code{ U("userInventoryCode") }; + const web::json::field_as_string device_name{ U("deviceName") }; + const web::json::field_as_string device_role{ U("deviceRole") }; + const web::json::field_as_value operational_state{ U("operationalState") }; // NcDeviceOperationalState + const web::json::field_as_integer reset_cause{ U("resetCause") }; // NcResetCause + const web::json::field_as_string message{ U("message") }; + const web::json::field_as_array control_classes{ U("controlClasses") }; // sequence + const web::json::field_as_array datatypes{ U("datatypes") }; // sequence + const web::json::field_as_string name{ U("name")}; + const web::json::field_as_string fixed_role{ U("fixedRole") }; + const web::json::field_as_array properties{ U("properties") }; // sequence + const web::json::field_as_array methods{ U("methods") }; // sequence + const web::json::field_as_array events{ U("events") }; // sequence + const web::json::field_as_integer type{ U("type") }; // NcDatatypeType + const web::json::field_as_value constraints{ U("constraints") }; // NcParameterConstraints + const web::json::field_as_integer organization_id{ U("organizationId") }; + const web::json::field_as_string website{ U("website") }; + const web::json::field_as_string key{ U("key") }; + const web::json::field_as_string revision_level{ U("revisionLevel") }; + const web::json::field_as_string brand_name{ U("brandName") }; + const web::json::field_as_string uuid{ U("uuid") }; + const web::json::field_as_string type_name{ U("typeName") }; + const web::json::field_as_bool is_read_only{ U("isReadOnly") }; + const web::json::field_as_bool is_persistent{ U("isPersistent") }; + const web::json::field_as_bool is_nullable{ U("isNullable") }; + const web::json::field_as_bool is_sequence{ U("isSequence") }; + const web::json::field_as_bool is_deprecated{ U("isDeprecated") }; + const web::json::field_as_bool is_constant{ U("isConstant") }; + const web::json::field_as_string parent_type{ U("parentType") }; + const web::json::field_as_string event_datatype{ U("eventDatatype") }; + const web::json::field_as_string result_datatype{ U("resultDatatype") }; + const web::json::field_as_array parameters{ U("parameters") }; + const web::json::field_as_array items{ U("items") }; // sequence + const web::json::field_as_array fields{ U("fields") }; // sequence + const web::json::field_as_integer generic_state{ U("generic") }; // NcDeviceGenericState + const web::json::field_as_string device_specific_details{ U("deviceSpecificDetails") }; + const web::json::field_as_array path{ U("path") }; // NcRolePath + const web::json::field_as_bool case_sensitive{ U("caseSensitive") }; + const web::json::field_as_bool match_whole_string{ U("matchWholeString") }; + const web::json::field_as_bool include_derived{ U("includeDerived") }; + const web::json::field_as_bool include_inherited{ U("includeInherited") }; + const web::json::field_as_string context_namespace{ U("contextNamespace") }; + const web::json::field_as_value default_value{ U("defaultValue") }; + const web::json::field_as_integer change_type{ U("changeType") }; // NcPropertyChangeType + const web::json::field_as_integer sequence_item_index{ U("sequenceItemIndex") }; // NcId + const web::json::field_as_value property_id{ U("propertyId") }; + const web::json::field_as_value maximum{ U("maximum") }; + const web::json::field_as_value minimum{ U("minimum") }; + const web::json::field_as_value step{ U("step") }; + const web::json::field_as_integer max_characters{ U("maxCharacters") }; + const web::json::field_as_string pattern{ U("pattern") }; + const web::json::field_as_value resource{ U("resource") }; + const web::json::field_as_string resource_type{ U("resourceType") }; + const web::json::field_as_string io_id{ U("ioId") }; + const web::json::field_as_integer connection_status{ U("connectionStatus") }; // NcConnectionStatus + const web::json::field_as_string connection_status_message{ U("connectionStatusMessage") }; + const web::json::field_as_integer payload_status{ U("payloadStatus") }; // NcPayloadStatus + const web::json::field_as_string payload_status_message{ U("payloadStatusMessage") }; + const web::json::field_as_bool signal_protection_status{ U("signalProtectionStatus") }; + const web::json::field_as_bool active{ U("active") }; + } + + // NMOS Parameter Registers + + // Sender Attributes Register + + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/sender-attributes/#st-2110-21-sender-type + const web::json::field_as_string st2110_21_sender_type{ U("st2110_21_sender_type") }; // see nmos::st2110_21_sender_type } + // IS-10 Authorization + namespace experimental + { + namespace fields + { + // Authorization Server Metadata + const web::json::field_as_value authorization_server_metadata{ U("authorization_server_metadata") }; + // see https://tools.ietf.org/html/rfc8414#section-2 + const web::json::field_as_string_or issuer{ U("issuer"),{} }; + const web::json::field_as_string_or authorization_endpoint{ U("authorization_endpoint"),{} }; + const web::json::field_as_string_or token_endpoint{ U("token_endpoint"),{} }; + const web::json::field_as_string_or registration_endpoint{ U("registration_endpoint"),{} }; + const web::json::field_as_array scopes_supported{ U("scopes_supported") }; // OPTIONAL + const web::json::field_as_array response_types_supported{ U("response_types_supported") }; + const web::json::field_as_array response_modes_supported{ U("response_modes_supported") }; // OPTIONAL + const web::json::field_as_array grant_types_supported{ U("grant_types_supported") }; // OPTIONAL + const web::json::field_as_array token_endpoint_auth_methods_supported{ U("token_endpoint_auth_methods_supported") }; // OPTIONAL + const web::json::field_as_array token_endpoint_auth_signing_alg_values_supported{ U("token_endpoint_auth_signing_alg_values_supported") }; // OPTIONAL + const web::json::field_as_string service_documentation{ U("service_documentation") }; // OPTIONAL + const web::json::field_as_array ui_locales_supported{ U("ui_locales_supported") }; // OPTIONAL + const web::json::field_as_string op_policy_uri{ U("op_policy_uri") }; // OPTIONAL + const web::json::field_as_string op_tos_uri{ U("op_tos_uri") }; // OPTIONAL + const web::json::field_as_string revocation_endpoint{ U("revocation_endpoint") }; // OPTIONAL + const web::json::field_as_array revocation_endpoint_auth_methods_supported{ U("revocation_endpoint_auth_methods_supported") }; // OPTIONAL + const web::json::field_as_array revocation_endpoint_auth_signing_alg_values_supported{ U("revocation_endpoint_auth_signing_alg_values_supported") }; // OPTIONAL + const web::json::field_as_string introspection_endpoint{ U("introspection_endpoint") }; // OPTIONAL + const web::json::field_as_array introspection_endpoint_auth_methods_supported{ U("introspection_endpoint_auth_methods_supported") }; // OPTIONAL + const web::json::field_as_array introspection_endpoint_auth_signing_alg_values_supported{ U("introspection_endpoint_auth_signing_alg_values_supported") }; // OPTIONAL + const web::json::field_as_array code_challenge_methods_supported{ U("code_challenge_methods_supported") }; + + // Client Metadata + const web::json::field_as_value client_metadata{ U("client_metadata") }; + // see https://tools.ietf.org/html/rfc7591#section-2 + // see https://tools.ietf.org/html/rfc7591#section-3.1 + // see https://tools.ietf.org/html/rfc7591#section-3.2 + const web::json::field_as_array redirect_uris{ U("redirect_uris") }; + //const web::json::field_as_string token_endpoint_auth_method{ U("token_endpoint_auth_method") }; // OPTIONAL already defined in settings + const web::json::field_as_array grant_types{ U("grant_types") }; // OPTIONAL + const web::json::field_as_array response_types{ U("response_types") }; // OPTIONAL + const web::json::field_as_string client_name{ U("client_name") }; // OPTIONAL + const web::json::field_as_string client_uri{ U("client_uri") }; // OPTIONAL + const web::json::field_as_string logo_uri{ U("logo_uri") }; // OPTIONAL + const web::json::field_as_string scope{ U("scope") }; // OPTIONAL + const web::json::field_as_array contacts{ U("contacts") }; // OPTIONAL + const web::json::field_as_string tos_uri{ U("tos_uri") }; // OPTIONAL + const web::json::field_as_string policy_uri{ U("policy_uri") }; // OPTIONAL + const web::json::field_as_value jwks{ U("jwks") }; // OPTIONAL + const web::json::field_as_array keys{ U("keys") }; // use inside jwks + const web::json::field_as_string software_id{ U("software_id") }; // OPTIONAL + const web::json::field_as_string software_version{ U("software_version") }; // OPTIONAL + const web::json::field_as_string_or client_id{ U("client_id"), {} }; + const web::json::field_as_string client_secret{ U("client_secret") }; // OPTIONAL + const web::json::field_as_integer client_id_issued_at{ U("client_id_issued_at") }; // OPTIONAL + const web::json::field_as_integer_or client_secret_expires_at{ U("client_secret_expires_at"),0 }; + const web::json::field_as_string azp{ U("azp") }; // OPTIONAL + // OpenID Connect extension + const web::json::field_as_string registration_client_uri{ U("registration_client_uri") }; // OPTIONAL + const web::json::field_as_string registration_access_token{ U("registration_access_token") }; // OPTIONAL + + // use for Authorization Server Metadata & Client Metadata + const web::json::field_as_string_or jwks_uri{ U("jwks_uri"),{} }; + } + } + + // Fields for experimental extensions namespace experimental { diff --git a/Development/nmos/json_schema.cpp b/Development/nmos/json_schema.cpp index 1d35c56cf..d24f2a5df 100644 --- a/Development/nmos/json_schema.cpp +++ b/Development/nmos/json_schema.cpp @@ -9,6 +9,9 @@ #include "nmos/is08_schemas/is08_schemas.h" #include "nmos/is09_versions.h" #include "nmos/is09_schemas/is09_schemas.h" +#include "nmos/is10_schemas/is10_schemas.h" +#include "nmos/is12_versions.h" +#include "nmos/is12_schemas/is12_schemas.h" #include "nmos/type.h" namespace nmos @@ -17,10 +20,10 @@ namespace nmos { web::uri make_schema_uri(const utility::string_t& tag, const utility::string_t& ref = {}) { - return{ _XPLATSTR("https://github.com/AMWA-TV/nmos-discovery-registration/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; + return{ _XPLATSTR("https://github.com/AMWA-TV/is-04/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.3.x/APIs/schemas/ + // See https://github.com/AMWA-TV/is-04/blob/v1.3.x/APIs/schemas/ namespace v1_3 { using namespace nmos::is04_schemas::v1_3_x; @@ -31,7 +34,7 @@ namespace nmos const web::uri nodeapi_receiver_target_put_request_uri = make_schema_uri(tag, _XPLATSTR("nodeapi-receiver-target.json")); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2.x/APIs/schemas/ + // See https://github.com/AMWA-TV/is-04/blob/v1.2.x/APIs/schemas/ namespace v1_2 { using namespace nmos::is04_schemas::v1_2_x; @@ -42,7 +45,7 @@ namespace nmos const web::uri nodeapi_receiver_target_put_request_uri = make_schema_uri(tag, _XPLATSTR("nodeapi-receiver-target.json")); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.1.x/APIs/schemas/ + // See https://github.com/AMWA-TV/is-04/blob/v1.1.x/APIs/schemas/ namespace v1_1 { using namespace nmos::is04_schemas::v1_1_x; @@ -53,7 +56,7 @@ namespace nmos const web::uri nodeapi_receiver_target_put_request_uri = make_schema_uri(tag, _XPLATSTR("nodeapi-receiver-target.json")); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.0.x/APIs/schemas/ + // See https://github.com/AMWA-TV/is-04/blob/v1.0.x/APIs/schemas/ namespace v1_0 { using namespace nmos::is04_schemas::v1_0_x; @@ -69,10 +72,10 @@ namespace nmos { web::uri make_schema_uri(const utility::string_t& tag, const utility::string_t& ref = {}) { - return{ _XPLATSTR("https://github.com/AMWA-TV/nmos-device-connection-management/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; + return{ _XPLATSTR("https://github.com/AMWA-TV/is-05/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.1.x/APIs/schemas/ + // See https://github.com/AMWA-TV/is-05/blob/v1.1.x/APIs/schemas/ namespace v1_1 { using namespace nmos::is05_schemas::v1_1_x; @@ -82,7 +85,7 @@ namespace nmos const web::uri connectionapi_receiver_staged_patch_request_uri = make_schema_uri(tag, _XPLATSTR("receiver-stage-schema.json")); } - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0.x/APIs/schemas/ + // See https://github.com/AMWA-TV/is-05/blob/v1.0.x/APIs/schemas/ namespace v1_0 { using namespace nmos::is05_schemas::v1_0_x; @@ -97,9 +100,10 @@ namespace nmos { web::uri make_schema_uri(const utility::string_t& tag, const utility::string_t& ref = {}) { - return{ _XPLATSTR("https://github.com/AMWA-TV/nmos-audio-channel-mapping/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; + return{ _XPLATSTR("https://github.com/AMWA-TV/is-08/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; } + // See https://github.com/AMWA-TV/is-08/blob/v1.0.x/APIs/schemas/ namespace v1_0 { using namespace nmos::is08_schemas::v1_0_x; @@ -113,10 +117,10 @@ namespace nmos { web::uri make_schema_uri(const utility::string_t& tag, const utility::string_t& ref = {}) { - return{ _XPLATSTR("https://github.com/AMWA-TV/nmos-system/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; + return{ _XPLATSTR("https://github.com/AMWA-TV/is-09/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; } - // See https://github.com/AMWA-TV/nmos-system/blob/v1.0.x/APIs/schemas/ + // See https://github.com/AMWA-TV/is-09/blob/v1.0.x/APIs/schemas/ namespace v1_0 { using namespace nmos::is09_schemas::v1_0_x; @@ -125,6 +129,47 @@ namespace nmos const web::uri systemapi_global_schema_uri = make_schema_uri(tag, _XPLATSTR("global.json")); } } + + namespace is10_schemas + { + web::uri make_schema_uri(const utility::string_t& tag, const utility::string_t& ref = {}) + { + return{ _XPLATSTR("https://github.com/AMWA-TV/nmos-authorization/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; + } + + namespace v1_0 + { + using namespace nmos::is10_schemas::v1_0_x; + const utility::string_t tag(_XPLATSTR("v1.0.x")); + + const web::uri authapi_auth_metadata_schema_uri = make_schema_uri(tag, _XPLATSTR("auth_metadata.json")); + const web::uri authapi_jwks_response_schema_uri = make_schema_uri(tag, _XPLATSTR("jwks_response.json")); + const web::uri authapi_register_client_error_response_uri = make_schema_uri(tag, _XPLATSTR("register_client_error_response.json")); + const web::uri authapi_register_client_response_uri = make_schema_uri(tag, _XPLATSTR("register_client_response.json")); + const web::uri authapi_token_error_response_uri = make_schema_uri(tag, _XPLATSTR("token_error_response.json")); + const web::uri authapi_token_response_schema_uri = make_schema_uri(tag, _XPLATSTR("token_response.json")); + const web::uri authapi_token_schema_schema_uri = make_schema_uri(tag, _XPLATSTR("token_schema.json")); + } + } + + namespace is12_schemas + { + web::uri make_schema_uri(const utility::string_t& tag, const utility::string_t& ref = {}) + { + return{ _XPLATSTR("https://github.com/AMWA-TV/is-12/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; + } + + // See https://github.com/AMWA-TV/is-12/tree/v1.0-dev/APIs/schemas/ + namespace v1_0 + { + using namespace nmos::is12_schemas::v1_0_x; + const utility::string_t tag(_XPLATSTR("v1.0.x")); + + const web::uri controlprotocolapi_base_message_schema_uri = make_schema_uri(tag, _XPLATSTR("base-message.json")); + const web::uri controlprotocolapi_command_message_schema_uri = make_schema_uri(tag, _XPLATSTR("command-message.json")); + const web::uri controlprotocolapi_subscription_message_schema_uri = make_schema_uri(tag, _XPLATSTR("subscription-message.json")); + } + } } namespace nmos @@ -309,6 +354,43 @@ namespace nmos }; } + static std::map make_is10_schemas() + { + using namespace nmos::is10_schemas; + + return + { + // v1.0 + { make_schema_uri(v1_0::tag, _XPLATSTR("auth_metadata.json")), make_schema(v1_0::auth_metadata) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("jwks_response.json")), make_schema(v1_0::jwks_response) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("jwks_schema.json")), make_schema(v1_0::jwks_schema) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("register_client_error_response.json")), make_schema(v1_0::register_client_error_response) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("register_client_response.json")), make_schema(v1_0::register_client_response) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("token_error_response.json")), make_schema(v1_0::token_error_response) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("token_response.json")), make_schema(v1_0::token_response) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("token_schema.json")), make_schema(v1_0::token_schema) } + }; + } + + static std::map make_is12_schemas() + { + using namespace nmos::is12_schemas; + + return + { + // v1.0 + { make_schema_uri(v1_0::tag, _XPLATSTR("base-message.json")), make_schema(v1_0::base_message) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("command-message.json")), make_schema(v1_0::command_message) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("command-response-message.json")), make_schema(v1_0::command_response_message) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("error-message.json")), make_schema(v1_0::error_message) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("event-data.json")), make_schema(v1_0::event_data) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("notification-message.json")), make_schema(v1_0::notification_message) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("property-changed-event-data.json")), make_schema(v1_0::property_changed_event_data) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("subscription-message.json")), make_schema(v1_0::subscription_message) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("subscription-response-message.json")), make_schema(v1_0::subscription_response_message) } + }; + } + inline void merge(std::map& to, std::map&& from) { to.insert(from.begin(), from.end()); // std::map::merge in C++17 @@ -320,6 +402,8 @@ namespace nmos merge(result, make_is05_schemas()); merge(result, make_is08_schemas()); merge(result, make_is09_schemas()); + merge(result, make_is10_schemas()); + merge(result, make_is12_schemas()); return result; } @@ -381,6 +465,51 @@ namespace nmos return is08_schemas::v1_0::map_activations_post_request_uri; } + web::uri make_authapi_auth_metadata_schema_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::authapi_auth_metadata_schema_uri; + } + + web::uri make_authapi_jwks_response_schema_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::authapi_jwks_response_schema_uri; + } + + web::uri make_authapi_register_client_response_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::authapi_register_client_response_uri; + } + + web::uri make_authapi_token_error_response_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::authapi_token_error_response_uri; + } + + web::uri make_authapi_token_schema_schema_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::authapi_token_schema_schema_uri; + } + + web::uri make_authapi_token_response_schema_uri(const nmos::api_version& version) + { + return is10_schemas::v1_0::authapi_token_response_schema_uri; + } + + web::uri make_controlprotocolapi_base_message_schema_uri(const nmos::api_version& version) + { + return is12_schemas::v1_0::controlprotocolapi_base_message_schema_uri; + } + + web::uri make_controlprotocolapi_command_message_schema_uri(const nmos::api_version& version) + { + return is12_schemas::v1_0::controlprotocolapi_command_message_schema_uri; + } + + web::uri make_controlprotocolapi_subscription_message_schema_uri(const nmos::api_version& version) + { + return is12_schemas::v1_0::controlprotocolapi_subscription_message_schema_uri; + } + // load the json schema for the specified base URI web::json::value load_json_schema(const web::uri& id) { diff --git a/Development/nmos/json_schema.h b/Development/nmos/json_schema.h index e938a513e..57cb0996b 100644 --- a/Development/nmos/json_schema.h +++ b/Development/nmos/json_schema.h @@ -29,6 +29,17 @@ namespace nmos web::uri make_channelmappingapi_map_activations_post_request_schema_uri(const nmos::api_version& version); + web::uri make_authapi_auth_metadata_schema_uri(const nmos::api_version& version); + web::uri make_authapi_jwks_response_schema_uri(const nmos::api_version& version); + web::uri make_authapi_register_client_response_uri(const nmos::api_version& version); + web::uri make_authapi_token_error_response_uri(const nmos::api_version& version); + web::uri make_authapi_token_schema_schema_uri(const nmos::api_version& version); + web::uri make_authapi_token_response_schema_uri(const nmos::api_version& version); + + web::uri make_controlprotocolapi_base_message_schema_uri(const nmos::api_version& version); + web::uri make_controlprotocolapi_command_message_schema_uri(const nmos::api_version& version); + web::uri make_controlprotocolapi_subscription_message_schema_uri(const nmos::api_version& version); + // load the json schema for the specified base URI web::json::value load_json_schema(const web::uri& id); } diff --git a/Development/nmos/jwk_utils.cpp b/Development/nmos/jwk_utils.cpp new file mode 100644 index 000000000..bc0c5f1ed --- /dev/null +++ b/Development/nmos/jwk_utils.cpp @@ -0,0 +1,353 @@ +#include "nmos/jwk_utils.h" + +#include +#include + +#if OPENSSL_VERSION_NUMBER < 0x30000000L +#include +#else +#include +#include +#endif + +#include "cpprest/basic_utils.h" +#include "ssl/ssl_utils.h" + +namespace nmos +{ + namespace experimental + { + typedef std::unique_ptr BIGNUM_ptr; + typedef std::unique_ptr EVP_PKEY_ptr; + typedef std::unique_ptr EVP_PKEY_CTX_ptr; +#if OPENSSL_VERSION_NUMBER < 0x30000000L + typedef std::unique_ptr RSA_ptr; +#else + typedef std::unique_ptr OSSL_PARAM_ptr; + typedef std::unique_ptr OSSL_PARAM_BLD_ptr; +#endif + + namespace details + { +#if OPENSSL_VERSION_NUMBER < 0x10100000L + int RSA_set0_key(RSA* r, BIGNUM* n, BIGNUM* e, BIGNUM* d) + { + /* If the fields n and e in r are NULL, the corresponding input + * parameters MUST be non-NULL for n and e. d may be + * left NULL (in case only the public key is used). + */ + if ((r->n == NULL && n == NULL) + || (r->e == NULL && e == NULL)) + return 0; + + if (n != NULL) { + BN_free(r->n); + r->n = n; + } + if (e != NULL) { + BN_free(r->e); + r->e = e; + } + if (d != NULL) { + BN_free(r->d); + r->d = d; + } + + return 1; + } + + void RSA_get0_key(const RSA* r, const BIGNUM** n, const BIGNUM** e, const BIGNUM** d) + { + if (n != NULL) + *n = r->n; + if (e != NULL) + *e = r->e; + if (d != NULL) + *d = r->d; + } +#endif + // convert JSON Web Key to RSA Public Key + // The "n" (modulus) parameter contains the modulus value for the RSA public key + // It is represented as a Base64urlUInt - encoded value + // The "e" (exponent) parameter contains the exponent value for the RSA public key + // It is represented as a Base64urlUInt - encoded value + // see https://tools.ietf.org/html/rfc7518#section-6.3.1 + // this function is based on https://stackoverflow.com/questions/57217529/how-to-convert-jwk-public-key-to-pem-format-in-c + utility::string_t jwk_to_rsa_public_key(const utility::string_t& base64_n, const utility::string_t& base64_e) + { +#if OPENSSL_VERSION_NUMBER < 0x30000000L + using ssl::experimental::BIO_ptr; + + auto n = utility::conversions::from_base64url(base64_n); + auto e = utility::conversions::from_base64url(base64_e); + + BIGNUM_ptr modulus(BN_bin2bn(n.data(), (int)n.size(), NULL), &BN_free); + BIGNUM_ptr exponent(BN_bin2bn(e.data(), (int)e.size(), NULL), &BN_free); + + RSA_ptr rsa(RSA_new(), &RSA_free); + if (!rsa) + { + throw jwk_exception("convert jwk to pem error: failed to create RSA"); + } + + // "Calling this function transfers the memory management of the values to the RSA object, + // and therefore the values that have been passed in should not be freed by the caller after + // this function has been called." + // see https://www.openssl.org/docs/man1.1.1/man3/RSA_set0_key.html + if (RSA_set0_key(rsa.get(), modulus.get(), exponent.get(), NULL)) + { + modulus.release(); + exponent.release(); + } + else + { + throw jwk_exception("convert jwk to pem error: failed to initialise RSA"); + } + + BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if (!bio) + { + throw jwk_exception("convert jwk to pem error: failed to create BIO memory"); + } + if (PEM_write_bio_RSA_PUBKEY(bio.get(), rsa.get())) + { + BUF_MEM* buf; + BIO_get_mem_ptr(bio.get(), &buf); + std::string pem(size_t(buf->length), 0); + BIO_read(bio.get(), (void*)pem.data(), (int)pem.length()); + return utility::s2us(pem); + } + else + { + throw jwk_exception("convert jwk to pem error: failed to write RSA public key to BIO memory"); + } +#else + using ssl::experimental::BIO_ptr; + + auto n = utility::conversions::from_base64url(base64_n); + auto e = utility::conversions::from_base64url(base64_e); + + BIGNUM_ptr modulus(BN_bin2bn(n.data(), (int)n.size(), NULL), &BN_free); + BIGNUM_ptr exponent(BN_bin2bn(e.data(), (int)e.size(), NULL), &BN_free); + + OSSL_PARAM_BLD_ptr param_bld(OSSL_PARAM_BLD_new(), &OSSL_PARAM_BLD_free); + if (OSSL_PARAM_BLD_push_BN(param_bld.get(), OSSL_PKEY_PARAM_RSA_N, modulus.get())) + { + modulus.release(); + } + if (OSSL_PARAM_BLD_push_BN(param_bld.get(), OSSL_PKEY_PARAM_RSA_E, exponent.get())) + { + exponent.release(); + } + + OSSL_PARAM_ptr params(OSSL_PARAM_BLD_to_param(param_bld.get()), &OSSL_PARAM_free); + EVP_PKEY_CTX_ptr ctx(EVP_PKEY_CTX_new_from_name(NULL, "RSA", NULL), &EVP_PKEY_CTX_free); + + struct evp_pkey_cleanup + { + EVP_PKEY* p; + ~evp_pkey_cleanup() { if (p) { EVP_PKEY_free(p); } } + }; + + evp_pkey_cleanup pkey = { 0 }; + if ((1 != EVP_PKEY_fromdata_init(ctx.get())) || (1 != EVP_PKEY_fromdata(ctx.get(), &pkey.p, EVP_PKEY_PUBLIC_KEY, params.get()))) + { + throw jwk_exception("convert jwk to pem error: failed to create EVP_PKEY-RSA public key from OSSL parameters"); + } + + BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if (!bio) + { + throw jwk_exception("convert jwk to pem error: failed to create BIO memory"); + } + if (PEM_write_bio_PUBKEY(bio.get(), pkey.p)) + { + BUF_MEM* buf; + BIO_get_mem_ptr(bio.get(), &buf); + std::string pem(size_t(buf->length), 0); + BIO_read(bio.get(), (void*)pem.data(), (int)pem.length()); + return utility::s2us(pem); + } + else + { + throw jwk_exception("convert jwk to pem error: failed to write RSA public key to BIO memory"); + } +#endif + } + + // convert Bignum to base64url string + utility::string_t to_base64url(const BIGNUM* bignum) + { + if (bignum) + { + const auto size = BN_num_bytes(bignum); + std::vector data(size); + if (BN_bn2bin(bignum, data.data())) + { + return utility::conversions::to_base64url(data); + } + } + return utility::string_t{}; + } + + // convert RSA to JSON Web Key + web::json::value rsa_to_jwk(const EVP_PKEY_ptr& pkey, const utility::string_t& keyid, const jwk::public_key_use& pubkey_use, const jwk::algorithm& alg) + { +#if OPENSSL_VERSION_NUMBER < 0x30000000L + RSA_ptr rsa(EVP_PKEY_get1_RSA(pkey.get()), &RSA_free); + + // The n, e and d parameters can be obtained by calling RSA_get0_key(). + // If they have not been set yet, then *n, *e and *d will be set to NULL. + // Otherwise, they are set to pointers to their respective values. + // These point directly to the internal representations of the values and + // therefore should not be freed by the caller. + // see https://manpages.debian.org/unstable/libssl-doc/RSA_get0_key.3ssl.en.html#DESCRIPTION + const BIGNUM* modulus = nullptr; + const BIGNUM* exponent = nullptr; + RSA_get0_key(rsa.get(), &modulus, &exponent, nullptr); + + const auto base64_n = to_base64url(modulus); + const auto base64_e = to_base64url(exponent); +#else + BIGNUM* modulus = nullptr; + BIGNUM* exponent = nullptr; + + utility::string_t base64_n; + if (EVP_PKEY_get_bn_param(pkey.get(), OSSL_PKEY_PARAM_RSA_N, &modulus)) + { + base64_n = to_base64url(modulus); + BN_clear_free(modulus); + } + + utility::string_t base64_e; + if (EVP_PKEY_get_bn_param(pkey.get(), OSSL_PKEY_PARAM_RSA_E, &exponent)) + { + base64_e = to_base64url(exponent); + BN_clear_free(exponent); + } +#endif + // construct jwk + return web::json::value_of({ + { U("kid"), keyid }, + { U("kty"), U("RSA") }, + { U("n"), base64_n }, + { U("e"), base64_e }, + { U("alg"), alg.name }, + { U("use"), pubkey_use.name } + }); + } + } + + // extract RSA public key from RSA private key + utility::string_t rsa_public_key(const utility::string_t& rsa_private_key) + { + using ssl::experimental::BIO_ptr; + + const std::string private_key_buffer{ utility::us2s(rsa_private_key) }; + BIO_ptr private_key_bio(BIO_new_mem_buf((void*)private_key_buffer.c_str(), (int)private_key_buffer.length()), &BIO_free); + if (!private_key_bio) + { + throw jwk_exception("extract public key error: failed to create BIO memory from PEM private key"); + } + + EVP_PKEY_ptr private_key(PEM_read_bio_PrivateKey(private_key_bio.get(), NULL, NULL, NULL), &EVP_PKEY_free); + + if (!private_key) + { + throw jwk_exception("extract public key error: failed to create EVP_PKEY-RSA from BIO private key"); + } + + BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + + if (bio && PEM_write_bio_PUBKEY(bio.get(), private_key.get())) + { + BUF_MEM* buf; + BIO_get_mem_ptr(bio.get(), &buf); + std::string public_key(size_t(buf->length), 0); + BIO_read(bio.get(), (void*)public_key.data(), (int)public_key.length()); + return utility::s2us(public_key); + } + else + { + throw jwk_exception("extract public key error: failed to write EVP_PKEY-RSA public key to BIO memory"); + } + } + + // convert JSON Web Key to RSA public key + utility::string_t jwk_to_rsa_public_key(const web::json::value& jwk) + { + // Key Type (kty) + // see https://tools.ietf.org/html/rfc7517#section-4.1 + + // RSA Public Keys + // see https://tools.ietf.org/html/rfc7518#section-6.3.1 + if (U("RSA") == jwk.at(U("kty")).as_string()) + { + // Public Key Use (use), optional! + // see https://tools.ietf.org/html/rfc7517#section-4.2 + if (jwk.has_field(U("use"))) + { + if (U("sig") != jwk.at(U("use")).as_string()) throw jwk_exception("jwk contains invalid 'use': " + utility::us2s(jwk.serialize())); + } + + // is n presented? + // Base64 URL encoded string representing the modulus of the RSA Key + // see https://tools.ietf.org/html/rfc7518#section-6.3.1.1 + if (!jwk.has_field(U("n"))) throw jwk_exception("jwk does not contain 'n': " + utility::us2s(jwk.serialize())); + + // is e presented? + // Base64 URL encoded string representing the public exponent of the RSA Key + // see https://tools.ietf.org/html/rfc7518#section-6.3.1.2 + if (!jwk.has_field(U("e"))) throw jwk_exception("jwk does not contain 'e': " + utility::us2s(jwk.serialize())); + + // using n & e to convert Json Web Key to RSA Public Key + return details::jwk_to_rsa_public_key(jwk.at(U("n")).as_string(), jwk.at(U("e")).as_string()); // may throw jwk_exception + } + throw jwk_exception("unsupported non-RSA jwk: " + utility::us2s(jwk.serialize())); + } + + // convert RSA public key to JSON Web Key + web::json::value rsa_public_key_to_jwk(const utility::string_t& rsa_public_key, const utility::string_t& keyid, const jwk::public_key_use& pubkey_use, const jwk::algorithm& alg) + { + using ssl::experimental::BIO_ptr; + + const std::string public_key{ utility::us2s(rsa_public_key) }; + BIO_ptr bio(BIO_new_mem_buf((void*)public_key.c_str(), (int)public_key.length()), &BIO_free); + if (!bio) + { + throw jwk_exception("convert pem to jwk error: failed to create BIO memory from PEM public key"); + } + + // create EVP_PKEY-RSA from BIO public key + EVP_PKEY_ptr key(PEM_read_bio_PUBKEY(bio.get(), NULL, NULL, NULL), &EVP_PKEY_free); + if (key) + { + // create JWK + return details::rsa_to_jwk(key, keyid, pubkey_use, alg); + } + throw jwk_exception("convert pem to jwk error: failed to create EVP_PKEY-RSA from BIO public key"); + } + + // convert RSA private key to JSON Web Key + web::json::value rsa_private_key_to_jwk(const utility::string_t& rsa_private_key, const utility::string_t& keyid, const jwk::public_key_use& pubkey_use, const jwk::algorithm& alg) + { + using ssl::experimental::BIO_ptr; + + const std::string buffer{ utility::us2s(rsa_private_key) }; + BIO_ptr bio(BIO_new_mem_buf((void*)buffer.c_str(), (int)buffer.length()), &BIO_free); + if (!bio) + { + throw jwk_exception("convert pem to jwk error: failed to create BIO memory from PEM private key"); + } + + // create EVP_PKEY-RSA from BIO private key + EVP_PKEY_ptr private_key(PEM_read_bio_PrivateKey(bio.get(), NULL, NULL, NULL), &EVP_PKEY_free); + if (private_key) + { + // create JWK + return details::rsa_to_jwk(private_key, keyid, pubkey_use, alg); + } + throw jwk_exception("convert pem to jwk error: failed to create EVP_PKEY-RSA from BIO private key"); + } + } +} diff --git a/Development/nmos/jwk_utils.h b/Development/nmos/jwk_utils.h new file mode 100644 index 000000000..436a05350 --- /dev/null +++ b/Development/nmos/jwk_utils.h @@ -0,0 +1,31 @@ +#ifndef NMOS_JWK_UTILS_H +#define NMOS_JWK_UTILS_H + +#include "cpprest/json_utils.h" +#include "jwk/algorithm.h" +#include "jwk/public_key_use.h" + +namespace nmos +{ + namespace experimental + { + struct jwk_exception : std::runtime_error + { + jwk_exception(const std::string& message) : std::runtime_error(message) {} + }; + + // extract RSA public key from RSA private key + utility::string_t rsa_public_key(const utility::string_t& rsa_private_key); + + // convert JSON Web Key to RSA public key + utility::string_t jwk_to_rsa_public_key(const web::json::value& jwk); + + // convert RSA public key to JSON Web Key + web::json::value rsa_public_key_to_jwk(const utility::string_t& rsa_public_key, const utility::string_t& keyid, const jwk::public_key_use& pubkey_use = jwk::public_key_uses::signing, const jwk::algorithm& alg = jwk::algorithms::RS256); + + // convert RSA private key to JSON Web Key + web::json::value rsa_private_key_to_jwk(const utility::string_t& rsa_private_key, const utility::string_t& keyid, const jwk::public_key_use& pubkey_use = jwk::public_key_uses::signing, const jwk::algorithm& alg = jwk::algorithms::RS256); + } +} + +#endif diff --git a/Development/nmos/jwks_uri_api.cpp b/Development/nmos/jwks_uri_api.cpp new file mode 100644 index 000000000..23e53661c --- /dev/null +++ b/Development/nmos/jwks_uri_api.cpp @@ -0,0 +1,62 @@ +#include "nmos/jwks_uri_api.h" + +#include "cpprest/response_type.h" +#include "nmos/api_utils.h" +#include "nmos/authorization_utils.h" +#include "nmos/jwk_utils.h" +#include "nmos/model.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace experimental + { + web::http::experimental::listener::api_router make_jwk_uri_api(nmos::base_model& model, load_rsa_private_keys_handler load_rsa_private_keys, slog::base_gate& /*gate_*/) + { + using namespace web::http::experimental::listener::api_router_using_declarations; + + api_router jwks_api; + + jwks_api.support(U("/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("x-authorization/") }, req, res)); + return pplx::task_from_result(true); + }); + + jwks_api.support(U("/x-authorization/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("jwks/") }, req, res)); + return pplx::task_from_result(true); + }); + + jwks_api.support(U("/x-authorization/jwks/?"), methods::GET, [&model, load_rsa_private_keys](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + using web::json::array; + + auto keys = value::array(); + std::vector rsa_private_keys; + with_read_lock(model.mutex, [&model, &rsa_private_keys, load_rsa_private_keys] + { + rsa_private_keys = load_rsa_private_keys(); + }); + + int idx = 0; + for (const auto& rsa_private_key : rsa_private_keys) + { + const auto keyid = std::to_string(++idx); + const auto jwk = rsa_private_key_to_jwk(rsa_private_key, utility::s2us(keyid)); + web::json::push_back(keys, jwk); + } + + const auto jwks = value_of({ + { nmos::experimental::fields::keys, keys } + }); + + set_reply(res, status_codes::OK, jwks); + return pplx::task_from_result(true); + }); + + return jwks_api; + } + } +} diff --git a/Development/nmos/jwks_uri_api.h b/Development/nmos/jwks_uri_api.h new file mode 100644 index 000000000..b2e7d39cf --- /dev/null +++ b/Development/nmos/jwks_uri_api.h @@ -0,0 +1,23 @@ +#ifndef NMOS_JWK_URI_API_H +#define NMOS_JWK_URI_API_H + +#include "cpprest/api_router.h" +#include "nmos/certificate_handlers.h" + +namespace slog +{ + class base_gate; +} + +// This is an experimental extension to support authorization code via a REST API +namespace nmos +{ + struct base_model; + + namespace experimental + { + web::http::experimental::listener::api_router make_jwk_uri_api(nmos::base_model& model, load_rsa_private_keys_handler load_rsa_private_keys, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/nmos/jwt_generator.h b/Development/nmos/jwt_generator.h new file mode 100644 index 000000000..d5f8dd38e --- /dev/null +++ b/Development/nmos/jwt_generator.h @@ -0,0 +1,18 @@ +#ifndef NMOS_JWT_GENERATOR_H +#define NMOS_JWT_GENERATOR_H + +#include "cpprest/base_uri.h" + +namespace nmos +{ + namespace experimental + { + class jwt_generator + { + public: + static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key, const utility::string_t& keyid); + }; + } +} + +#endif diff --git a/Development/nmos/jwt_generator_impl.cpp b/Development/nmos/jwt_generator_impl.cpp new file mode 100644 index 000000000..300c82f8f --- /dev/null +++ b/Development/nmos/jwt_generator_impl.cpp @@ -0,0 +1,64 @@ +#include "nmos/jwt_generator.h" + +#include "cpprest/basic_utils.h" +#include "jwt-cpp/traits/nlohmann-json/traits.h" +#include "nmos/id.h" +#include "nmos/jwk_utils.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + class jwt_generator_impl + { + public: + static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& public_key, const utility::string_t& private_key, const utility::string_t& keyid) + { + using namespace jwt::traits; + + const auto now = std::chrono::system_clock::now(); + + // use server private key to create client_assertion (JWT) + // where client_assertion MUST including iss, sub, aud, exp, and may including jti + // see https://tools.ietf.org/html/rfc7523#section-2.2 + // see https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication + return utility::s2us(jwt::create() + .set_issuer(utility::us2s(issuer)) + .set_subject(utility::us2s(subject)) + .set_audience(utility::us2s(audience.to_string())) + .set_issued_at(now) + .set_expires_at(now + token_lifetime) + .set_id(utility::us2s(nmos::make_id())) + .set_key_id(utility::us2s(keyid)) + .set_type("JWT") + .sign(jwt::algorithm::rs256(utility::us2s(public_key), utility::us2s(private_key)))); + } + + static utility::string_t create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key, const utility::string_t& keyid) + { + // see https://tools.ietf.org/html/rfc7523#section-3 + if (issuer.empty()) + { + throw jwk_exception("empty issuer"); + } + if (subject.empty()) + { + throw jwk_exception("empty subject"); + } + if (audience.is_empty()) + { + throw jwk_exception("empty audience"); + } + return create_client_assertion(issuer, subject, audience, token_lifetime, rsa_public_key(private_key), private_key, keyid); + } + }; + } + + utility::string_t jwt_generator::create_client_assertion(const utility::string_t& issuer, const utility::string_t& subject, const web::uri& audience, const std::chrono::seconds& token_lifetime, const utility::string_t& private_key, const utility::string_t& keyid) + { + return details::jwt_generator_impl::create_client_assertion(issuer, subject, audience, token_lifetime, private_key, keyid); + } + } +} diff --git a/Development/nmos/jwt_validator.h b/Development/nmos/jwt_validator.h new file mode 100644 index 000000000..1cea62f47 --- /dev/null +++ b/Development/nmos/jwt_validator.h @@ -0,0 +1,81 @@ +#ifndef NMOS_JWT_VALIDATOR_H +#define NMOS_JWT_VALIDATOR_H + +#include +#include "cpprest/base_uri.h" + +namespace web +{ + namespace json + { + class value; + } + namespace http + { + class http_request; + } +} + +namespace nmos +{ + namespace experimental + { + struct insufficient_scope_exception : std::runtime_error + { + insufficient_scope_exception(const std::string& message) : std::runtime_error(message) {} + }; + + struct no_matching_keys_exception : std::runtime_error + { + web::uri issuer; + no_matching_keys_exception(const web::uri& issuer, const std::string& message) + : std::runtime_error(message) + , issuer(issuer) {} + }; + + struct scope; + + namespace details + { + class jwt_validator_impl; + } + + // callback for JSON validating access token + typedef std::function token_json_validator; + + class jwt_validator + { + public: + jwt_validator() {} + jwt_validator(const web::json::value& pub_keys, token_json_validator token_validation); + + // is JWT validator initialised + bool is_initialized() const; + + // Token JSON validation + // may throw + void json_validation(const utility::string_t& token) const; + + // Basic token validation, including token schema validation and token issuer public keys validation + // may throw + void basic_validation(const utility::string_t& token) const; + + // Registered claims validation + // may throw + static void registered_claims_validation(const utility::string_t& token, const web::http::method& method, const web::uri& relative_uri, const scope& scope, const utility::string_t& audience); + + // Get token client Id + // may throw + static utility::string_t get_client_id(const utility::string_t& token); + + // Get token issuer + // may throw + static web::uri get_token_issuer(const utility::string_t& token); + + private: + std::shared_ptr impl; + }; + } +} + +#endif diff --git a/Development/nmos/jwt_validator_impl.cpp b/Development/nmos/jwt_validator_impl.cpp new file mode 100644 index 000000000..8d3499e98 --- /dev/null +++ b/Development/nmos/jwt_validator_impl.cpp @@ -0,0 +1,493 @@ +#include "nmos/jwt_validator.h" + +#include +#include +#include "cpprest/basic_utils.h" +#include "cpprest/json.h" +#include "cpprest/regex_utils.h" +#include "cpprest/uri_schemes.h" +#include "nmos/authorization_utils.h" +#include "nmos/json_fields.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + class jwt_validator_impl + { + public: + explicit jwt_validator_impl(const web::json::value& pubkeys, token_json_validator token_validation) + : token_validation(token_validation) + { + using namespace jwt::traits; + + if (pubkeys.is_array()) + { + // empty out all jwt verifiers + validators.clear(); + + // create jwt verifier for each public key + + // preload JWT verifiers with authorization server publc keys (pems), should perform faster on token validation rather than load the public key then validation at runtime + + // "The access token MUST be a JSON Web Signature (JWS) as defined by RFC 7515. JSON Web Algorithms (JWA) MUST NOT be used. + // The JWS MUST be signed with RSASSA-PKCS1-v1_5 using SHA-512, meaning the value of the alg field in the token's JOSE (JSON Object Signing and Encryption) header (see RFC 7515) + // MUST be set to RS512 as defined in RFC 7518." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#behaviour-access-tokens + for (const auto& pubkey : pubkeys.as_array()) + { + const auto& jwk = pubkey.at(U("jwk")); + + // Key Type (kty) + // see https://tools.ietf.org/html/rfc7517#section-4.1 + if (U("RSA") != jwk.at(U("kty")).as_string()) continue; + + // Public Key Use (use), optional! + // see https://tools.ietf.org/html/rfc7517#section-4.2 + if (jwk.has_field(U("use"))) + { + if (U("sig") != jwk.at(U("use")).as_string()) continue; + } + + // Algorithm (alg), optional! + // see https://tools.ietf.org/html/rfc7517#section-4.4 + if (jwk.has_field(U("alg"))) + { + if (U("RS512") != jwk.at(U("alg")).as_string()) continue; + } + + auto validator = jwt::verify({}); + try + { + validator.allow_algorithm(jwt::algorithm::rs512(utility::us2s(pubkey.at(U("pem")).as_string()))); + validators.push_back(validator); + } + catch (const jwt::error::rsa_exception&) + { + // hmm, maybe log the error? + } + } + } + } + + // Basic token validation + // may throw + void basic_validation(const utility::string_t& token) const + { + using namespace jwt::traits; + + const auto decoded_token = jwt::decode(utility::us2s(token)); + + // do token JSON validation + if (token_validation) { token_validation(web::json::value::parse(utility::s2us(decoded_token.get_payload()))); } + else { throw web::json::json_exception("No JOSN token valiation callback to validate access token"); } + + std::vector errors; + + // is JWT validator set up + if (0 == validators.size()) { errors.push_back("no JWT validator to perform access token validation"); } + + // do basic token validation + for (const auto& validator : validators) + { + try + { + // verify the signature & some of the common claims, such as exp, iat, nbf etc + validator.verify(decoded_token); + + // basic token validation successfully + return; + } + catch (const jwt::error::signature_verification_exception& e) + { + // ignore, try next validator + errors.push_back(e.what()); + } + } + + // reaching here indicates there is no matching public key to validate the access token + + // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss + // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it + // MAY temporarily respond with an HTTP 503 code in order to avoid blocking the incoming authorized request. When a HTTP 503 code is used, the Resource + // Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request. + // If the Resource Server fails to verify a token using all public keys available it MUST reject the token." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + + const auto token_issuer = web::uri{ utility::s2us(decoded_token.get_issuer()) }; + // no matching public keys for the token, re-fetch public keys from token issuer + throw no_matching_keys_exception(token_issuer, format_errors(errors)); + } + + // Registered claims validation + // may throw + static void registered_claims_validation(const utility::string_t& token, const web::http::method& method, const web::uri& relative_uri_, const scope& scope, const utility::string_t& audience) + { + using namespace jwt::traits; + + const auto decoded_token = jwt::decode(utility::us2s(token)); + + // verify Registered Claims + + // iss (Identifies principal that issued the JWT) + // The "iss" value is a case-sensitive string containing a StringOrURI value. + // see https://tools.ietf.org/html/rfc7519#section-4.1.1 + // iss is not needed to validate as this token may be coming from an alternative Authorization server, which would have a different iss then the current in used Authorization server. + + // sub (Identifies the subject of the JWT) + // hmm, not sure how to verify sub as it could be anything + // see https://tools.ietf.org/html/rfc7519#section-4.1.2 + + // aud (Identifies the recipients of the JWT) + // This claim MUST be a JSON array containing the fully resolved domain names of the intended recipients, or a domain name containing + // wild - card characters in order to target a subset of devices on a network. Such wild-carding of domain names is documented in RFC 4592. + // If aud claim does not match the fully resolved domain name of the resource server, the Resource Server MUST reject the token. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#aud + // see https://tools.ietf.org/html/rfc7519#section-4.1.3 + + auto verify_aud = [&decoded_token](const utility::string_t& audience_) + { + auto strip_trailing_dot = [](const std::string& audience_) + { + auto audience = audience_; + if (!audience.empty() && U('.') == audience.back()) + { + audience.pop_back(); + } + return audience; + }; + + auto audience = strip_trailing_dot(utility::us2s(audience_)); + std::vector segments; + boost::split(segments, audience, boost::is_any_of(".")); + + const auto& auds = decoded_token.get_audience(); + for (const auto& aud_ : auds) + { + // strip the scheme (https://) if presented + auto aud = strip_trailing_dot(aud_); + web::http::uri aud_uri(utility::s2us(aud)); + if (!aud_uri.scheme().empty()) + { + aud = utility::us2s(aud_uri.host()); + } + + // is the audience an exact match to the token audience + if (audience == aud) + { + return true; + } + + // do reverse segment matching between audience and token audience + std::vector aud_segments; + boost::split(aud_segments, aud, boost::is_any_of(".")); + + if (segments.size() >= aud_segments.size() && aud_segments.size()) + { + // in order to match the token audience has to be in wildcard domain name format + // with a leftmost "*" character. + // see https://tools.ietf.org/html/rfc4592#section-2.1.1 + if (aud_segments[0] != "*") + { + return false; + } + + // token audience is in wildcard domain name format + // let's do a segment to segment comparison between audience and token audience + bool matched{ true }; + auto idx = aud_segments.size() - 1; + for (auto it = aud_segments.rbegin(); it != aud_segments.rend() && matched; ++it) + { + if (idx && *it != segments[idx--]) + { + matched = false; + } + } + if (matched) + { + return true; + } + } + } + return false; + }; + if (!verify_aud(audience)) + { + throw insufficient_scope_exception(utility::us2s(audience) + " not found in audience"); + } + + // scope optional + // If scope claim does not contain the expected scope, the Resource Server will reject the token. + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#scope + auto verify_scope = [&decoded_token](const nmos::experimental::scope& scope) + { + if (decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope))) + { + const auto& scope_claim = decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::scope)); + const auto scopes_set = scopes(utility::s2us(scope_claim.as_string())); + return (scopes_set.end() != std::find(scopes_set.begin(), scopes_set.end(), scope)); + } + return true; + }; + if (!verify_scope(scope)) + { + throw insufficient_scope_exception(utility::us2s(scope.name) + " not found in " + utility::us2s(nmos::experimental::fields::scope)); + } + + // verify client_id and azp (optional) + auto verify_client_id = [&decoded_token]() + { + const auto client_id_found = decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::client_id)); + const auto azp_found = decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::azp)); + + if ((client_id_found && !azp_found) || (!client_id_found && azp_found)) + { + return true; + } + + if (client_id_found && + azp_found && + decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::client_id)).as_string() == decoded_token.get_payload_claim(utility::us2s(nmos::experimental::fields::azp)).as_string()) + { + return true; + } + + return false; + }; + if (!verify_client_id()) + { + throw insufficient_scope_exception("missing client_id or azp, or client_id and azp are not matching"); + } + + // verify Private Claims + + // x-nmos-* (Contains information particular to the NMOS API the token is intended for) + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#x-nmos- + auto verify_x_nmos_scope_claim = [&decoded_token, &method](const std::string& x_nmos_scope_claim_, const std::string& path) + { + if (!decoded_token.has_payload_claim(x_nmos_scope_claim_)) { return false; } + const auto x_nmos_scope_claim = decoded_token.get_payload_claim(x_nmos_scope_claim_).to_json(); + + if (!x_nmos_scope_claim.is_null()) + { + auto accessible = [&x_nmos_scope_claim, &method, &path](const std::string& access_right) + { + if (x_nmos_scope_claim.contains(access_right)) + { + auto accessible_paths = jwt::basic_claim(x_nmos_scope_claim.at(access_right)).as_array(); + for (auto& accessible_path : accessible_paths) + { + // construct path regex for regex comparison + + auto acc_path = accessible_path.get(); + // replace any '*' => '.*' + boost::replace_all(acc_path, "*", ".*"); + const bst::regex path_regex(acc_path); + if (bst::regex_match(path, path_regex)) + { + return true; + } + } + } + return false; + }; + + // write accessible + if (is_write_method(method)) + { + return accessible("write"); + } + + // read accessible + if (is_read_method(method)) + { + return accessible("read"); + } + } + return false; + }; + + // verify the relevant x-nmos-* private claim + if (!scope.name.empty()) + { + const auto x_nmos_scope_claim = "x-nmos-" + utility::us2s(scope.name); + const auto relative_uri = utility::us2s(relative_uri_.to_string()); + // extract {path} from /x-nmos/{api name, the scope name}/{api version}/{path} + auto extract_path = [&relative_uri](const nmos::experimental::scope& scope) + { + const bst::regex search_regex("/x-nmos/" + utility::us2s(scope.name) + "/v[0-9]+\\.[0-9]"); + + if (bst::regex_search(relative_uri, search_regex)) + { + auto path = bst::regex_replace(relative_uri, search_regex, ""); + if (path.size() && ('/' == path[0])) + { + return path.erase(0, 1); + } + else + { + return std::string{}; + } + } + return std::string{};; + }; + const auto path = extract_path(scope); + + if (path.empty()) + { + // "The token MUST include either an x-nmos-* claim matching the API name, a scope matching the API name or both in order to obtain 'read' permission." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation + + // "Presence of an x-nmos-* claim matching an NMOS API grants implicit read only access to some API base paths as specified in Resource Servers. + // The value of the claim is a JSON object, indicating access permissions for the API.An omitted x-nmos-* object indicates that no access is permitted + // to the namespace-identified API beyond what may be granted by the presence of a matching scope." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#x-nmos- + const auto x_nmos_scope_claim_found = decoded_token.has_payload_claim(x_nmos_scope_claim); + const auto scope_found = decoded_token.has_payload_claim(utility::us2s(nmos::experimental::fields::scope)); + const auto is_read_request = is_read_method(method); + + if (is_read_request) + { + if (!x_nmos_scope_claim_found && !scope_found) + { + // missing both x-nmos private claim and scope claim + throw insufficient_scope_exception("missing claim x-nmos-" + utility::us2s(scope.name) + " and claim scope, " + relative_uri + " not accessible"); + } + } + else + { + // invalid request method + throw insufficient_scope_exception("this is not a read request, " + relative_uri + " not accessible"); + } + } + else + { + // "The token MUST include an x-nmos-* claim matching the API name and the path, in line with the method outlined in Tokens." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#path-validation + + // "The value of each x-nmos-* claim is the access permissions object for the given user for that specific API." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#the-access-permissions-object + if (!verify_x_nmos_scope_claim(x_nmos_scope_claim, path)) + { + throw insufficient_scope_exception("fail to verify claim " + x_nmos_scope_claim + ", " + relative_uri + " not accessible"); + } + } + } + } + + // Get token client Id + // may throw + static utility::string_t get_client_id(const utility::string_t& token) + { + using namespace jwt::traits; + + auto decoded_token = jwt::decode(utility::us2s(token)); + // token is not guaranteed to have a client_id + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#client_id + if (decoded_token.has_payload_claim("client_id")) + { + const auto client_id = decoded_token.get_payload_claim("client_id"); + return utility::s2us(client_id.as_string()); + } + // azp is an OPTIONAL claim for OpenID Connect + // Authorized party - the party to which the ID Token was issued.If present, it MUST contain the OAuth 2.0 Client ID of this party. + // This Claim is only needed when the ID Token has a single audience value and that audience is different than the authorized party. + // It MAY be included even when the authorized party is the same as the sole audience. + // The azp value is a case sensitive string containing a StringOrURI value. + // see https://openid.net/specs/openid-connect-core-1_0.html#IDToken + else if (decoded_token.has_payload_claim("azp")) + { + const auto client_id = decoded_token.get_payload_claim("azp"); + return utility::s2us(client_id.as_string()); + } + return{}; + } + + // Get token issuer + // may throw + static web::uri get_token_issuer(const utility::string_t& token) + { + using namespace jwt::traits; + + auto decoded_token = jwt::decode(utility::us2s(token)); + return utility::s2us(decoded_token.get_issuer()); + } + + private: + std::string format_errors(const std::vector& errs) const + { + std::string separator; + std::stringstream ss; + for (const auto& err : errs) + { + ss << separator << err; + separator = ", "; + } + return ss.str(); + } + + static bool is_write_method(const web::http::method& method) + { + return ((web::http::methods::POST == method) || + (web::http::methods::PUT == method) || + (web::http::methods::PATCH == method) || + (web::http::methods::DEL == method)); + }; + + static bool is_read_method (const web::http::method& method) + { + return ((web::http::methods::OPTIONS == method) || + (web::http::methods::GET == method) || + (web::http::methods::HEAD == method)); + }; + + private: + std::vector> validators; + token_json_validator token_validation; + }; + } + + jwt_validator::jwt_validator(const web::json::value& pubkeys, token_json_validator token_validation) + : impl(new details::jwt_validator_impl(pubkeys, token_validation)) + { + } + + // is JWT validator initialised + bool jwt_validator::is_initialized() const + { + return impl ? true : false; + } + + // Basic token validation + // may throw + void jwt_validator::basic_validation(const utility::string_t& token) const + { + if (!impl) { throw std::runtime_error("JWT validator has not initiliased"); } + + impl->basic_validation(token); + } + + // Registered claims validation + // may throw + void jwt_validator::registered_claims_validation(const utility::string_t& token, const web::http::method& method, const web::uri& relative_uri, const scope& scope, const utility::string_t& audience) + { + details::jwt_validator_impl::registered_claims_validation(token, method, relative_uri, scope, audience); + } + + // Get token client Id + // may throw + utility::string_t jwt_validator::get_client_id(const utility::string_t& token) + { + return details::jwt_validator_impl::get_client_id(token); + } + + // Get token issuer + // may throw + web::uri jwt_validator::get_token_issuer(const utility::string_t& token) + { + return details::jwt_validator_impl::get_token_issuer(token); + } + } +} diff --git a/Development/nmos/logging_api.cpp b/Development/nmos/logging_api.cpp index 30d870b1a..432d6fa95 100644 --- a/Development/nmos/logging_api.cpp +++ b/Development/nmos/logging_api.cpp @@ -36,16 +36,24 @@ namespace nmos return logging_api; } + static inline rql::extractor make_rql_extractor(const web::json::value& value) + { + return [&value](web::json::value& results, const web::json::value& key) + { + return web::json::extract(value.as_object(), results, key.as_string()); + }; + } + bool match_logging_rql(const web::json::value& value, const web::json::value& query) { - return query.is_null() || rql::evaluator + try { - [&value](web::json::value& results, const web::json::value& key) - { - return web::json::extract(value.as_object(), results, key.as_string()); - }, - rql::default_any_operators() - }(query) == rql::value_true; + return query.is_null() || rql::evaluator{ make_rql_extractor(value), rql::default_any_operators() }(query) == rql::value_true; + } + catch (const std::runtime_error&) // i.e. rql::details::rql_exception + { + return false; + } } // Predicate to match events against a query @@ -80,6 +88,8 @@ namespace nmos if (field.first == U("rql")) { rql_query = rql::parse_query(field.second.as_string()); + // validate against call-operators used in nmos::experimental::match_logging_rql + rql::validate_query(rql_query, rql::default_any_operators()); } // an error is reported for unimplemented parameters else @@ -261,7 +271,7 @@ namespace nmos // RFC 5988 allows relative URLs, but NMOS specification examples are all absolute URLs // See https://tools.ietf.org/html/rfc5988#section-5 - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.5.%20APIs%20-%20Query%20Parameters.md + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.5._APIs_-_Query_Parameters.html // get the request host and port (or use the primary host address, and port, from settings) auto req_host_port = web::http::get_host_port(req); diff --git a/Development/nmos/mdns.cpp b/Development/nmos/mdns.cpp index f5fe432e5..2549e5f43 100644 --- a/Development/nmos/mdns.cpp +++ b/Development/nmos/mdns.cpp @@ -13,14 +13,15 @@ #include "mdns/service_advertiser.h" #include "mdns/service_discovery.h" #include "nmos/is09_versions.h" +#include "nmos/is10_versions.h" #include "nmos/random.h" namespace nmos { // "APIs MUST produce an mDNS advertisement [...] accompanied by DNS TXT records" - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/RegistrationAPI.raml#L17 - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/QueryAPI.raml#L122 - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/NodeAPI.raml#L37 + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/RegistrationAPI.html#dns_sd_advertisement + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/QueryAPI.html#dns_sd_advertisement + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/NodeAPI.html#dns_sd_advertisement // For now, the TXT record keys and the functions to make/parse the values are kept as implementation details @@ -36,11 +37,24 @@ namespace nmos const std::string ver_dvc{ "ver_dvc" }; const std::string ver_snd{ "ver_snd" }; const std::string ver_rcv{ "ver_rcv" }; + + const std::string api_selector{ "api_selector" }; } - // returns "http" or "https" depending on settings - service_protocol get_service_protocol(const nmos::settings& settings) + // returns true if the specified service protocol is secure + bool is_service_protocol_secure(const service_protocol& api_proto) { + return service_protocols::https == api_proto || service_protocols::secure_mqtt == api_proto; + } + + // returns e.g. "http" or "https" depending on settings + service_protocol get_service_protocol(const nmos::service_type& service, const nmos::settings& settings) + { + if (nmos::service_types::mqtt == service) { + return nmos::experimental::fields::client_secure(settings) + ? service_protocols::secure_mqtt + : service_protocols::mqtt; + } return nmos::experimental::fields::client_secure(settings) ? service_protocols::https : service_protocols::http; @@ -62,6 +76,7 @@ namespace nmos // find and parse the 'api_proto' TXT record (or return the default) service_protocol parse_api_proto_record(const mdns::structured_txt_records& records) { + // hmm, default not appropriate for nmos::service_types::mqtt return mdns::parse_txt_record(records, txt_record_keys::api_proto, details::parse_api_proto_value, service_protocols::http); } @@ -76,7 +91,7 @@ namespace nmos { // "The value of this TXT record is a comma separated list of API versions supported by the server. For example: 'v1.0,v1.1,v2.0'. // There should be no whitespace between commas, and versions should be listed in ascending order." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/RegistrationAPI.raml#L33 + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/RegistrationAPI.html#dns_sd_advertisement std::vector api_vers; boost::algorithm::split(api_vers, api_ver, [](char c){ return ',' == c; }); // Since ascending order is recommended, not required, convert straight to an ordered set without checking that. @@ -87,13 +102,27 @@ namespace nmos // find and parse the 'api_ver' TXT record (or return the default) std::set parse_api_ver_record(const mdns::structured_txt_records& records) { + // hm, logically speaking default only appropriate for IS-04 return mdns::parse_txt_record(records, txt_record_keys::api_ver, details::parse_api_ver_value, is04_versions::unspecified); } - bool get_service_authorization(const nmos::settings& settings) + bool get_service_authorization(const nmos::service_type& service, const nmos::settings& settings) { - const auto client_authorization = false; - return client_authorization; + // IS-09 System API does not use authorization + // See https://github.com/AMWA-TV/is-09/issues/21 + // IS-10 Authorization API does not use authorization + if (nmos::service_types::system == service || nmos::service_types::authorization == service) return false; + + return nmos::experimental::fields::client_authorization(settings) | nmos::experimental::fields::server_authorization(settings); + } + + bool is_api_authorization_protected(const nmos::service_type& service, const nmos::settings& settings) + { + // IS-09 System API does not use authorization + // See https://github.com/AMWA-TV/is-09/issues/21 + if (nmos::service_types::system == service) return false; + + return nmos::experimental::fields::server_authorization(settings); } namespace details @@ -156,11 +185,32 @@ namespace nmos return mdns::parse_txt_record(records, txt_record_keys::pri, details::parse_pri_value, service_priorities::no_priority); } + namespace details + { + inline std::string make_api_selector_value(utility::string_t api_selector = {}) + { + return utility::us2s(api_selector); + } + + inline utility::string_t parse_api_selector_value(const std::string& api_selector) + { + return utility::s2us(api_selector); + } + } + + // find and parse the 'api_selector' TXT record (or return the default) + utility::string_t parse_api_selector_record(const mdns::structured_txt_records& records) + { + return mdns::parse_txt_record(records, txt_record_keys::api_selector, details::parse_api_selector_value, utility::string_t{}); + } + // make the required TXT records from the specified values (or sensible default values) - mdns::structured_txt_records make_txt_records(const nmos::service_type& service, service_priority pri, const std::set& api_ver, const service_protocol& api_proto, bool api_auth) + mdns::structured_txt_records make_txt_records(const nmos::service_type& service, service_priority pri, const std::set& api_ver, const service_protocol& api_proto, bool api_auth, const utility::string_t& selector) { if (service == nmos::service_types::node) { + // see https://specs.amwa.tv/is-04/releases/v1.3.1/docs/3.2._Discovery_-_Peer_to_Peer_Operation.html#dns-sd-txt-records + // IS-04 Node API is not distributed so does not use 'pri' TXT record return { { txt_record_keys::api_proto, details::make_api_proto_value(api_proto) }, @@ -168,8 +218,10 @@ namespace nmos { txt_record_keys::api_auth, details::make_api_auth_value(api_auth) } }; } - else + else if (service == nmos::service_types::query || service == nmos::service_types::registration) { + // see https://specs.amwa.tv/is-04/releases/v1.3.1/docs/3.1._Discovery_-_Registered_Operation.html#dns-sd-txt-records-1 + // and https://specs.amwa.tv/is-04/releases/v1.3.1/docs/3.1._Discovery_-_Registered_Operation.html#dns-sd-txt-records return { { txt_record_keys::api_proto, details::make_api_proto_value(api_proto) }, @@ -178,6 +230,39 @@ namespace nmos { txt_record_keys::pri, details::make_pri_value(pri) } }; } + else if (service == nmos::service_types::system) + { + // see https://specs.amwa.tv/is-09/releases/v1.0.0/docs/3.1._Discovery_-_Operation.html#dns-sd-txt-records + // IS-09 System API does not use authorization + return + { + { txt_record_keys::api_proto, details::make_api_proto_value(api_proto) }, + { txt_record_keys::api_ver, details::make_api_ver_value(api_ver) }, + { txt_record_keys::pri, details::make_pri_value(pri) } + }; + } + else if (service == nmos::service_types::authorization) + { + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/3.0._Discovery.html#dns-sd-txt-records + return + { + { txt_record_keys::api_proto, details::make_api_proto_value(api_proto) }, + { txt_record_keys::api_ver, details::make_api_ver_value(api_ver) }, + { txt_record_keys::pri, details::make_pri_value(pri) }, + { txt_record_keys::api_selector, details::make_api_selector_value(selector) } + }; + } + else if (service == nmos::service_types::mqtt) + { + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.1._Transport_-_MQTT.html#7-broker-discovery + // the 'pri' TXT record could make sense for the MQTT Broker but that wasn't adopted for IS-07 v1.0 + return + { + { txt_record_keys::api_proto, details::make_api_proto_value(api_proto) }, + { txt_record_keys::api_auth, details::make_api_auth_value(api_auth) } + }; + } + return {}; } // find and parse the Node 'ver_' TXT records @@ -225,6 +310,7 @@ namespace nmos if (nmos::service_types::registration == service) return nmos::fields::registration_port(settings); if (nmos::service_types::register_ == service) return nmos::fields::registration_port(settings); if (nmos::service_types::system == service) return nmos::fields::system_port(settings); + if (nmos::service_types::authorization == service) return nmos::experimental::fields::authorization_port(settings); return 0; } @@ -235,18 +321,21 @@ namespace nmos if (nmos::service_types::registration == service) return "registration"; if (nmos::service_types::register_ == service) return "registration"; if (nmos::service_types::system == service) return "system"; + if (nmos::service_types::authorization == service) return "auth"; return{}; } - inline std::string service_base_name(const nmos::service_type& service) + inline std::string service_base_name(const nmos::service_type& service, const nmos::settings& settings) { - return "nmos-cpp_" + service_api(service); + return utility::us2s(nmos::fields::service_name_prefix(settings)) + "_" + service_api(service); } inline std::set service_versions(const nmos::service_type& service, const nmos::settings& settings) { // the System API is defined by IS-09 (having been originally specified in JT-NM TR-1001-1:2018 Annex A) if (nmos::service_types::system == service) return nmos::is09_versions::from_settings(settings); + // the Authorization API is defined by IS-10 + if (nmos::service_types::authorization == service) return nmos::is10_versions::from_settings(settings); // all the other APIs are defined by IS-04, and should advertise consistent versions return nmos::is04_versions::from_settings(settings); } @@ -256,7 +345,7 @@ namespace nmos { // this just serves as an example of a possible service naming strategy // replacing '.' with '-', since although '.' is legal in service names, some DNS-SD implementations just don't like it - return boost::algorithm::replace_all_copy(details::service_base_name(service) + "_" + utility::us2s(nmos::get_host(settings)) + ":" + utility::us2s(utility::ostringstreamed(details::service_port(service, settings))), ".", "-"); + return boost::algorithm::replace_all_copy(details::service_base_name(service, settings) + "_" + utility::us2s(nmos::get_host(settings)) + ":" + utility::us2s(utility::ostringstreamed(details::service_port(service, settings))), ".", "-"); } // helper function for registering addresses when the host name is explicitly configured @@ -291,12 +380,12 @@ namespace nmos if (0 > instance_port_or_disabled) return; const auto instance_port = (uint16_t)instance_port_or_disabled; const auto api_ver = details::service_versions(service, settings); - const auto records = nmos::make_txt_records(service, nmos::fields::pri(settings), api_ver, nmos::get_service_protocol(settings), nmos::get_service_authorization(settings)); + const auto records = nmos::make_txt_records(service, nmos::fields::pri(settings), api_ver, nmos::get_service_protocol(service, settings), nmos::is_api_authorization_protected(service, settings)); const auto txt_records = mdns::make_txt_records(records); // advertise "_nmos-register._tcp" for v1.3 (and as an experimental extension, for lower versions) // don't advertise "_nmos-registration._tcp" if only v1.3 - // see https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.3/docs/3.1.%20Discovery%20-%20Registered%20Operation.md#dns-sd-advertisement + // see https://specs.amwa.tv/is-04/releases/v1.3.0/docs/3.1._Discovery_-_Registered_Operation.html#dns-sd-advertisement if (nmos::service_types::registration == service) { if (*api_ver.begin() < nmos::is04_versions::v1_3) @@ -306,7 +395,7 @@ namespace nmos advertiser.register_service(instance_name, nmos::service_types::register_, instance_port, domain, host_name, txt_records).wait(); } // don't advertise "_nmos-node._tcp" if only v1.3 - // see https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.3/docs/3.2.%20Discovery%20-%20Peer%20to%20Peer%20Operation.md#dns-sd-advertisement + // see https://specs.amwa.tv/is-04/releases/v1.3.0/docs/3.2._Discovery_-_Peer_to_Peer_Operation.html#dns-sd-advertisement else if (nmos::service_types::node == service) { if (*api_ver.begin() < nmos::is04_versions::v1_3) @@ -335,9 +424,9 @@ namespace nmos // helper function for registering addresses when the host name is explicitly configured void register_addresses(mdns::service_advertiser& advertiser, const nmos::settings& settings) { - const auto domain = utility::us2s(nmos::fields::domain(settings)); + const auto domain = utility::us2s(nmos::get_domain(settings)); // nmos::settings has the fully-qualified host name, but mdns::service_advertiser needs the host name and domain separately - const auto full_name = utility::us2s(nmos::fields::host_name(settings)); + const auto full_name = utility::us2s(nmos::get_host_name(settings)); const auto host_name = ierase_tail_copy(full_name, "." + domain); if (!is_local_domain(domain)) @@ -351,9 +440,9 @@ namespace nmos // helper function for registering the specified service (API) void register_service(mdns::service_advertiser& advertiser, const nmos::service_type& service, const nmos::settings& settings) { - const auto domain = utility::us2s(nmos::fields::domain(settings)); + const auto domain = utility::us2s(nmos::get_domain(settings)); // nmos::settings has the fully-qualified host name, but mdns::service_advertiser needs the host name and domain separately - const auto full_name = utility::us2s(nmos::fields::host_name(settings)); + const auto full_name = utility::us2s(nmos::get_host_name(settings)); const auto host_name = ierase_tail_copy(full_name, "." + domain); if (!is_local_domain(domain)) @@ -369,7 +458,7 @@ namespace nmos { const auto instance_name = service_name(service, settings); const auto api_ver = details::service_versions(service, settings); - auto records = nmos::make_txt_records(service, nmos::fields::pri(settings), api_ver, nmos::get_service_protocol(settings), nmos::get_service_authorization(settings)); + auto records = nmos::make_txt_records(service, nmos::fields::pri(settings), api_ver, nmos::get_service_protocol(service, settings), nmos::is_api_authorization_protected(service, settings)); records.insert(records.end(), std::make_move_iterator(add_records.begin()), std::make_move_iterator(add_records.end())); const auto txt_records = mdns::make_txt_records(records); @@ -397,7 +486,7 @@ namespace nmos // helper function for updating the specified service (API) TXT records void update_service(mdns::service_advertiser& advertiser, const nmos::service_type& service, const nmos::settings& settings, mdns::structured_txt_records add_records) { - const auto domain = utility::us2s(nmos::fields::domain(settings)); + const auto domain = utility::us2s(nmos::get_domain(settings)); if (!is_local_domain(domain)) { // also advertise via mDNS @@ -408,8 +497,6 @@ namespace nmos namespace details { - typedef std::pair api_ver_pri; - typedef std::pair resolved_service; typedef std::vector resolved_services; pplx::task resolve_service(std::shared_ptr results, mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set& api_ver, const std::pair& priorities, const std::set& api_proto, const std::set& api_auth, const std::chrono::steady_clock::time_point& timeout, const pplx::cancellation_token& token) @@ -419,14 +506,14 @@ namespace nmos const bool cancel = pplx::canceled == discovery.resolve([=](const mdns::resolve_result& resolved) { // "The Node [filters] out any APIs which do not support its required API version, protocol and authorization mode (TXT api_ver, api_proto and api_auth)." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.3/docs/3.1.%20Discovery%20-%20Registered%20Operation.md#client-interaction-procedure + // See https://specs.amwa.tv/is-04/releases/v1.3.0/docs/3.1._Discovery_-_Registered_Operation.html#client-interaction-procedure // note, since we specified the interface_id, we expect only one result... // parse into structured TXT records auto records = mdns::parse_txt_records(resolved.txt_records); - // 'pri' must not be omitted for Registration API and Query API (see nmos::make_txt_records) + // 'pri' must not be omitted for Registration API, Query API and Authorization API (see nmos::make_txt_records) auto resolved_pri = nmos::parse_pri_record(records); if (service != nmos::service_types::node) { @@ -449,26 +536,35 @@ namespace nmos auto resolved_ver = std::find_first_of(resolved_vers.rbegin(), resolved_vers.rend(), api_ver.rbegin(), api_ver.rend()); if (resolved_vers.rend() == resolved_ver) return true; - auto resolved_uri = web::uri_builder() - .set_scheme(utility::s2us(resolved_proto)) - .set_port(resolved.port) - .set_path(U("/x-nmos/") + utility::s2us(details::service_api(service))); + // hmm, maybe in the future check for the matching 'api_selector' value + auto resolved_selector = nmos::parse_api_selector_record(records); - if (nmos::service_protocols::https == resolved_proto) + auto resolved_uri = web::uri_builder(); + if (service == nmos::service_types::authorization) { - auto host_name = utility::s2us(resolved.host_name); - // remove a trailing '.' to turn an FQDN into a DNS name, for SSL certificate matching - // hmm, this might be more appropriately done by tweaking the Host header in the client request? - if (!host_name.empty() && U('.') == host_name.back()) host_name.pop_back(); - - results->push_back({ { *resolved_ver, resolved_pri }, resolved_uri - .set_host(host_name) - .to_uri() - }); + resolved_uri + .set_scheme(utility::s2us(resolved_proto)) + .set_port(resolved.port) + .set_path(U("/.well-known/oauth-authorization-server")).append_path(!resolved_selector.empty() ? U("/") + resolved_selector : U("")); } - else for (const auto& ip_address : resolved.ip_addresses) + else { + resolved_uri + .set_scheme(utility::s2us(resolved_proto)) + .set_port(resolved.port) + .set_path(U("/x-nmos/") + utility::s2us(details::service_api(service))); + } + + auto host_name = utility::s2us(resolved.host_name); + // remove a trailing '.' to turn an FQDN into a DNS name, for SSL certificate matching + if (!host_name.empty() && U('.') == host_name.back()) host_name.pop_back(); + + for (const auto& ip_address : resolved.ip_addresses) + { + // sneakily stash the host name for the Host header in user info + // cf. nmos::details::make_http_client results->push_back({ { *resolved_ver, resolved_pri }, resolved_uri + .set_user_info(host_name) .set_host(utility::s2us(ip_address)) .to_uri() }); @@ -486,11 +582,17 @@ namespace nmos // the higher version is preferred; for the same version, the 'higher' priority is preferred return lhs.first > rhs.first || (lhs.first == rhs.first && lhs.second < rhs.second); } + + std::pair service_priorities(const nmos::service_type& service, const nmos::settings& settings) + { + if (nmos::service_types::authorization == service) return { nmos::fields::authorization_highest_pri(settings), nmos::fields::authorization_lowest_pri(settings) }; + return { nmos::fields::highest_pri(settings), nmos::fields::lowest_pri(settings) }; + } } // helper function for resolving instances of the specified service (API) - // with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly - pplx::task> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set& api_ver, const std::pair& priorities, const std::set& api_proto, const std::set& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) + // with the highest version, highest priority instances at the front, and optionally services with the same priority ordered randomly + pplx::task> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set& api_ver, const std::pair& priorities, const std::set& api_proto, const std::set& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) { const auto absolute_timeout = std::chrono::steady_clock::now() + timeout; @@ -524,7 +626,7 @@ namespace nmos // when either task is completed, cancel and wait for the other to be completed // and then merge the two sets of results - resolve_task = pplx::when_any(both_tasks.begin(), both_tasks.end()).then([both_results, linked_source, both_tasks](std::pair first_result) + resolve_task = pplx::ranges::when_any(both_tasks).then([both_results, linked_source, both_tasks](std::pair first_result) { if (!both_results[first_result.second]->empty()) linked_source.cancel(); @@ -559,11 +661,11 @@ namespace nmos { // since each advertisement may be discovered via multiple interfaces and, in the case of the Registration API, via two service types // remove duplicate uris, after sorting to ensure the highest advertised priority is kept for each - std::stable_sort(results->begin(), results->end(), [](const details::resolved_service& lhs, const details::resolved_service& rhs) + std::stable_sort(results->begin(), results->end(), [](const resolved_service& lhs, const resolved_service& rhs) { return lhs.second < rhs.second || (lhs.second == rhs.second && details::less_api_ver_pri(lhs.first, rhs.first)); }); - results->erase(std::unique(results->begin(), results->end(), [](const details::resolved_service& lhs, const details::resolved_service& rhs) + results->erase(std::unique(results->begin(), results->end(), [](const resolved_service& lhs, const resolved_service& rhs) { return lhs.second == rhs.second; }), results->end()); @@ -578,18 +680,68 @@ namespace nmos } // "Given multiple returned Registration APIs, the Node orders these based on their advertised priority (TXT pri)" - std::stable_sort(results->begin(), results->end(), [](const details::resolved_service& lhs, const details::resolved_service& rhs) + std::stable_sort(results->begin(), results->end(), [](const resolved_service& lhs, const resolved_service& rhs) { // hmm, for the moment, the scheme is *not* considered; one might want to prefer 'https' over 'http'? return details::less_api_ver_pri(lhs.first, rhs.first); }); + // return the randomized services + std::list resolved_services; + for (const auto& result : *results) + { + resolved_services.push_back(result); + } + return resolved_services; + }); + } + + // helper function for resolving instances of the specified service (API) + // with the highest version, highest priority instances at the front, and optionally services with the same priority ordered randomly + pplx::task> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set& api_ver, const std::pair& priorities, const std::set& api_proto, const std::set& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token) + { + return resolve_service_(discovery, service, browse_domain, api_ver, priorities, api_proto, api_auth, randomize, timeout, token).then([](std::list resolved_services) + { // add the version to each uri - return boost::copy_range>(*results | boost::adaptors::transformed([](const details::resolved_service& s) + return boost::copy_range>(resolved_services | boost::adaptors::transformed([](const resolved_service& s) { return web::uri_builder(s.second).append_path(U("/") + make_api_version(s.first.first)).to_uri(); })); }); } + + // helper function for resolving instances of the specified service (API) based on the specified settings + // with the highest version, highest priority instances at the front, and services with the same priority ordered randomly + pplx::task> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const nmos::settings& settings, const pplx::cancellation_token& token) + { + const auto browse_domain = utility::us2s(nmos::get_domain(settings)); + const auto versions = details::service_versions(service, settings); + const auto priorities = details::service_priorities(service, settings); + const auto protocols = std::set{ nmos::get_service_protocol(service, settings) }; + const auto authorization = std::set{ nmos::get_service_authorization(service, settings) }; + + // use a short timeout that's long enough to ensure the daemon's cache is exhausted + // when no cancellation token is specified + const auto timeout = token.is_cancelable() ? nmos::fields::discovery_backoff_max(settings) : 1; + + return resolve_service(discovery, service, browse_domain, versions, priorities, protocols, authorization, true, std::chrono::duration_cast(std::chrono::seconds(timeout)), token); + } + + // helper function for resolving instances of the specified service (API) based on the specified settings + // with the highest version, highest priority instances at the front, and services with the same priority ordered randomly + pplx::task> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const nmos::settings& settings, const pplx::cancellation_token& token) + { + const auto browse_domain = utility::us2s(nmos::get_domain(settings)); + const auto versions = details::service_versions(service, settings); + const auto priorities = details::service_priorities(service, settings); + const auto protocols = std::set{ nmos::get_service_protocol(service, settings) }; + const auto authorization = std::set{ nmos::get_service_authorization(service, settings) }; + + // use a short timeout that's long enough to ensure the daemon's cache is exhausted + // when no cancellation token is specified + const auto timeout = token.is_cancelable() ? nmos::fields::discovery_backoff_max(settings) : 1; + + return resolve_service_(discovery, service, browse_domain, versions, priorities, protocols, authorization, true, std::chrono::duration_cast(std::chrono::seconds(timeout)), token); + } } } diff --git a/Development/nmos/mdns.h b/Development/nmos/mdns.h index e2171c65d..8267ab850 100644 --- a/Development/nmos/mdns.h +++ b/Development/nmos/mdns.h @@ -16,7 +16,7 @@ namespace mdns namespace nmos { // "APIs MUST produce an mDNS advertisement [...] accompanied by DNS TXT records" - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/RegistrationAPI.raml#L17 etc. + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/RegistrationAPI.html#dns_sd_advertisement etc. typedef std::string service_type; @@ -31,9 +31,9 @@ namespace nmos // IS-04 Registration API // "RFC6763 Section 7.2 specifies that the maximum service name length for an mDNS advertisement // is 16 characters when including the leading underscore, but "_nmos-registration" is 18 characters." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2.1/APIs/RegistrationAPI.raml#L19 - // This is to be addressed in v1.3, by specifying a shorter service type, "_nmos-register._tcp". - // See https://github.com/AMWA-TV/nmos-discovery-registration/pull/71 + // See https://specs.amwa.tv/is-04/releases/v1.2.1/APIs/RegistrationAPI.html#dns_sd_advertisement + // This was addressed in v1.3, by specifying a shorter service type, "_nmos-register._tcp". + // See https://specs.amwa.tv/is-04/releases/v1.3.0/docs/3.1._Discovery_-_Registered_Operation.html#dns-sd-advertisement const service_type registration{ "_nmos-registration._tcp" }; // IS-09 System API (originally specified in JT-NM TR-1001-1:2018 Annex A) @@ -41,6 +41,10 @@ namespace nmos // IS-10 Authorization API const service_type authorization{ "_nmos-auth._tcp" }; + + // MQTT Broker + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.1._Transport_-_MQTT.html#7-broker-discovery + const service_type mqtt{ "_nmos-mqtt._tcp" }; } // "The DNS-SD advertisement MUST be accompanied by a TXT record of name 'api_proto' with a value @@ -51,14 +55,26 @@ namespace nmos namespace service_protocols { + // Values for the 'api_proto' TXT record for e.g. IS-04 Registration API, IS-04 Query API, IS-09 System API and IS-10 Authorization API + // See https://specs.amwa.tv/is-04/releases/v1.3.1/docs/3.1._Discovery_-_Registered_Operation.html#dns-sd-txt-records + // and https://specs.amwa.tv/is-09/releases/v1.0.0/docs/3.1._Discovery_-_Operation.html#dns-sd-txt-records + // and https://specs.amwa.tv/is-10/releases/v1.0.0/docs/3.0._Discovery.html#dns-sd-txt-records const service_protocol http{ "http" }; const service_protocol https{ "https" }; + // Values for the 'api_proto' TXT record for MQTT broker advertisements + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.1._Transport_-_MQTT.html#7-broker-discovery + const service_protocol mqtt{ "mqtt" }; + const service_protocol secure_mqtt{ "secure-mqtt" }; + const std::set all{ nmos::service_protocols::http, nmos::service_protocols::https }; } - // returns "http" or "https" depending on settings - service_protocol get_service_protocol(const nmos::settings& settings); + // returns true if the specified service protocol is secure + bool is_service_protocol_secure(const service_protocol& api_proto); + + // returns e.g. "http" or "https" depending on settings + service_protocol get_service_protocol(const nmos::service_type& service, const nmos::settings& settings); // find and parse the 'api_proto' TXT record (or return the default) service_protocol parse_api_proto_record(const mdns::structured_txt_records& records); @@ -82,7 +98,7 @@ namespace nmos // (This record is added in v1.3, so when it is omitted, "false" should be assumed.) // returns true or false depending on settings - bool get_service_authorization(const nmos::settings& settings); + bool get_service_authorization(const nmos::service_type& service, const nmos::settings& settings); // find and parse the 'api_auth' TXT record (or return the default) bool parse_api_auth_record(const mdns::structured_txt_records& records); @@ -106,7 +122,7 @@ namespace nmos service_priority parse_pri_record(const mdns::structured_txt_records& records); // make the required TXT records from the specified values (or sensible default values) - mdns::structured_txt_records make_txt_records(const nmos::service_type& service, service_priority pri = service_priorities::highest_development_priority, const std::set& api_ver = is04_versions::all, const service_protocol& api_proto = service_protocols::http, bool api_auth = false); + mdns::structured_txt_records make_txt_records(const nmos::service_type& service, service_priority pri = service_priorities::highest_development_priority, const std::set& api_ver = is04_versions::all, const service_protocol& api_proto = service_protocols::http, bool api_auth = false, const utility::string_t& selector = {}); // "The value of each of the ['ver_' TXT records] should be an unsigned 8-bit integer initialised // to '0'. This integer MUST be incremented and mDNS TXT record updated whenever a change is made @@ -145,11 +161,36 @@ namespace nmos // with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly pplx::task> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set& api_ver, const std::pair& priorities, const std::set& api_proto, const std::set& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + // helper function for resolving instances of the specified service (API) based on the specified options or defaults + // with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly template inline pplx::task> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain = {}, const std::set& api_ver = nmos::is04_versions::all, const std::pair& priorities = { service_priorities::highest_active_priority, service_priorities::no_priority }, const std::set& api_proto = nmos::service_protocols::all, const std::set& api_auth = { false, true }, bool randomize = true, const std::chrono::duration& timeout = std::chrono::seconds(mdns::default_timeout_seconds), const pplx::cancellation_token& token = pplx::cancellation_token::none()) { return resolve_service(discovery, service, browse_domain, api_ver, api_proto, api_auth, randomize, std::chrono::duration_cast(timeout), token); } + + // helper function for resolving instances of the specified service (API) based on the specified settings + // with the highest version, highest priority instances at the front, and services with the same priority ordered randomly + pplx::task> resolve_service(mdns::service_discovery& discovery, const nmos::service_type& service, const nmos::settings& settings, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + + typedef std::pair api_ver_pri; + typedef std::pair resolved_service; + + // helper function for resolving instances of the specified service (API) + // with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly + pplx::task> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain, const std::set& api_ver, const std::pair& priorities, const std::set& api_proto, const std::set& api_auth, bool randomize, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + + // helper function for resolving instances of the specified service (API) based on the specified options or defaults + // with the highest version, highest priority instances at the front, and (by default) services with the same priority ordered randomly + template + inline pplx::task> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const std::string& browse_domain = {}, const std::set& api_ver = nmos::is04_versions::all, const std::pair& priorities = { service_priorities::highest_active_priority, service_priorities::no_priority }, const std::set& api_proto = nmos::service_protocols::all, const std::set& api_auth = { false, true }, bool randomize = true, const std::chrono::duration& timeout = std::chrono::seconds(mdns::default_timeout_seconds), const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + return resolve_service_(discovery, service, browse_domain, api_ver, api_proto, api_auth, randomize, std::chrono::duration_cast(timeout), token); + } + + // helper function for resolving instances of the specified service (API) based on the specified settings + // with the highest version, highest priority instances at the front, and services with the same priority ordered randomly + pplx::task> resolve_service_(mdns::service_discovery& discovery, const nmos::service_type& service, const nmos::settings& settings, const pplx::cancellation_token& token = pplx::cancellation_token::none()); } } diff --git a/Development/nmos/mdns_api.cpp b/Development/nmos/mdns_api.cpp index ac2e5c6a6..15bc7806b 100644 --- a/Development/nmos/mdns_api.cpp +++ b/Development/nmos/mdns_api.cpp @@ -206,13 +206,10 @@ namespace nmos return mdns_result(browsed1, resolved); })); } - return pplx::when_all(tasks.begin(), tasks.end()).then([res, version, tasks](pplx::task> finally) mutable + return pplx::ranges::when_all(tasks).then([res, version, tasks](pplx::task> finally) mutable { // to ensure an exception from one doesn't leave other tasks' exceptions unobserved - for (auto& task : tasks) - { - try { task.wait(); } catch (...) {} - } + for (auto& task : tasks) pplx::details::wait_nothrow(task); // merge results that have the same host_target, port and txt records // and only differ in the resolved addresses diff --git a/Development/nmos/media_type.h b/Development/nmos/media_type.h index bcaa5442d..0b8da8fcb 100644 --- a/Development/nmos/media_type.h +++ b/Development/nmos/media_type.h @@ -7,15 +7,17 @@ namespace nmos { // Media types (used in flows and receivers) - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_video.json - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_audio_raw.json - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_video.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_video.html + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_audio_raw.html + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_video.html // etc. DEFINE_STRING_ENUM(media_type) namespace media_types { // Video media types + // Uncompressed Video + // See https://tools.ietf.org/html/rfc4175#section-6 const media_type video_raw{ U("video/raw") }; // Audio media types @@ -37,6 +39,17 @@ namespace nmos // See SMPTE ST 2022-8:2019 const media_type video_SMPTE2022_6{ U("video/SMPTE2022-6") }; + + // Additional media types for NMOS responses + + const media_type application_sdp{ U("application/sdp") }; + + // experimental extension, to support HTML rendering of NMOS responses + const media_type text_html{ U("text/html") }; + + // experimental extension, to support JSON rendering in NMOS responses + const media_type application_schema_json{ U("application/schema+json") }; + const media_type application_sdp_json{ U("application/sdp+json") }; } } diff --git a/Development/nmos/model.h b/Development/nmos/model.h index d5c6b9f99..d9c25559c 100644 --- a/Development/nmos/model.h +++ b/Development/nmos/model.h @@ -101,6 +101,10 @@ namespace nmos // IS-08 inputs and outputs for this node // see nmos/channelmapping_resources.h nmos::resources channelmapping_resources; + + // IS-12 resources for this node + // see nmos/control_protocol_resources.h + nmos::resources control_protocol_resources; }; struct registry_model : model diff --git a/Development/nmos/node_api.cpp b/Development/nmos/node_api.cpp index 852cdca13..a40708bec 100644 --- a/Development/nmos/node_api.cpp +++ b/Development/nmos/node_api.cpp @@ -7,14 +7,14 @@ #include "nmos/is04_versions.h" #include "nmos/json_schema.h" #include "nmos/model.h" +#include "nmos/scope.h" #include "nmos/slog.h" -#include "cpprest/host_utils.h" namespace nmos { web::http::experimental::listener::api_router make_unmounted_node_api(const nmos::model& model, node_api_target_handler target_handler, slog::base_gate& gate); - web::http::experimental::listener::api_router make_node_api(const nmos::model& model, node_api_target_handler target_handler, slog::base_gate& gate) + web::http::experimental::listener::api_router make_node_api(const nmos::model& model, node_api_target_handler target_handler, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -32,6 +32,12 @@ namespace nmos return pplx::task_from_result(true); }); + if (validate_authorization) + { + node_api.support(U("/x-nmos/") + nmos::patterns::node_api.pattern + U("/?"), validate_authorization); + node_api.support(U("/x-nmos/") + nmos::patterns::node_api.pattern + U("/.*"), validate_authorization); + } + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is04_versions::from_settings(model.settings); }); node_api.support(U("/x-nmos/") + nmos::patterns::node_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { diff --git a/Development/nmos/node_api.h b/Development/nmos/node_api.h index 8ad7f90bf..2d457172c 100644 --- a/Development/nmos/node_api.h +++ b/Development/nmos/node_api.h @@ -5,12 +5,17 @@ #include "nmos/node_api_target_handler.h" // Node API implementation -// See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/NodeAPI.raml +// See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/NodeAPI.html namespace nmos { struct model; - web::http::experimental::listener::api_router make_node_api(const nmos::model& model, node_api_target_handler target_handler, slog::base_gate& gate); + web::http::experimental::listener::api_router make_node_api(const nmos::model& model, node_api_target_handler target_handler, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); + + inline web::http::experimental::listener::api_router make_node_api(const nmos::model& model, node_api_target_handler target_handler, slog::base_gate& gate) + { + return make_node_api(model, std::move(target_handler), {}, gate); + } } #endif diff --git a/Development/nmos/node_api_target_handler.cpp b/Development/nmos/node_api_target_handler.cpp index 2e0203137..a98d26967 100644 --- a/Development/nmos/node_api_target_handler.cpp +++ b/Development/nmos/node_api_target_handler.cpp @@ -6,16 +6,18 @@ #include "nmos/client_utils.h" #include "nmos/is05_versions.h" #include "nmos/json_fields.h" +#include "nmos/media_type.h" // for nmos::media_types::application_sdp #include "nmos/model.h" +#include "nmos/scope.h" #include "nmos/slog.h" namespace nmos { - // implement the Node API /receivers/{receiverId}/target endpoint using the Connection API implementation with the specified transport file parser and the specified validator + // implement the Node API /receivers/{receiverId}/target endpoint using the Connection API implementation with the specified transport file parser, the specified validator and the bearer token getter // (the /target endpoint is only required to support RTP transport, other transport types use the Connection API) - node_api_target_handler make_node_api_target_handler(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged) + node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token) { - return [&model, parse_transport_file, validate_merged](const nmos::id& receiver_id, const web::json::value& sender_data, slog::base_gate& gate) + return [&model, load_ca_certificates, parse_transport_file, validate_merged, get_authorization_bearer_token](const nmos::id& receiver_id, const web::json::value& sender_data, slog::base_gate& gate) { using web::json::value; using web::json::value_of; @@ -28,7 +30,7 @@ namespace nmos // if manifest_href is null, this will throw json_exception which will be reported appropriately as 400 Bad Request const auto manifest_href = nmos::fields::manifest_href(sender_data).as_string(); - web::http::client::http_client client(manifest_href, nmos::with_read_lock(model.mutex, [&model] { return nmos::make_http_client_config(model.settings); })); + web::http::client::http_client client(manifest_href, nmos::with_read_lock(model.mutex, [&, load_ca_certificates, get_authorization_bearer_token] { return nmos::make_http_client_config(model.settings, load_ca_certificates, get_authorization_bearer_token, gate); })); return api_request(client, web::http::methods::GET, gate).then([manifest_href, &gate](web::http::http_response res) { if (res.status_code() != web::http::status_codes::OK) @@ -47,16 +49,16 @@ namespace nmos { slog::log(gate, SLOG_FLF) << "Missing Content-Type: should be application/sdp"; } - else if (U("application/sdp") != content_type) + else if (nmos::media_types::application_sdp.name != content_type) { - throw web::http::http_exception(U("Incorrect Content-Type: ") + content_type + U(", should be application/sdp")); + throw web::http::http_exception(U("Incorrect Content-Type: ") + content_type + U(", should be ") + nmos::media_types::application_sdp.name); } return res.extract_string(true); }).then([&model, receiver_id, sender_id, parse_transport_file, validate_merged, &gate](const utility::string_t& sdp) { // "The Connection Management API supersedes the now deprecated method of updating the 'target' resource on Node API Receivers in order to establish connections." - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/docs/3.1.%20Interoperability%20-%20NMOS%20IS-04.md#support-for-legacy-is-04-connection-management + // See https://specs.amwa.tv/is-05/releases/v1.0.0/docs/3.1._Interoperability_-_NMOS_IS-04.html#support-for-legacy-is-04-connection-management const auto patch = value_of({ { nmos::fields::sender_id, sender_id }, @@ -66,7 +68,7 @@ namespace nmos })}, { nmos::fields::transport_file, value_of({ { nmos::fields::data, sdp }, - { nmos::fields::type, U("application/sdp") } + { nmos::fields::type, nmos::media_types::application_sdp.name } })} }); @@ -97,8 +99,6 @@ namespace nmos }; } - // implement the Node API /receivers/{receiverId}/target endpoint using the Connection API implementation with the default transport file parser - // (the /target endpoint is only required to support RTP transport, other transport types use the Connection API) node_api_target_handler make_node_api_target_handler(nmos::node_model& model) { return make_node_api_target_handler(model, &nmos::parse_rtp_transport_file, {}); diff --git a/Development/nmos/node_api_target_handler.h b/Development/nmos/node_api_target_handler.h index aa1dfc451..2b22990d7 100644 --- a/Development/nmos/node_api_target_handler.h +++ b/Development/nmos/node_api_target_handler.h @@ -1,6 +1,8 @@ #ifndef NMOS_NODE_API_TARGET_HANDLER_H #define NMOS_NODE_API_TARGET_HANDLER_H +#include "nmos/authorization_handlers.h" +#include "nmos/certificate_handlers.h" #include "nmos/connection_api.h" namespace web @@ -11,11 +13,6 @@ namespace web } } -namespace slog -{ - class base_gate; -} - namespace nmos { struct node_model; @@ -23,19 +20,25 @@ namespace nmos // handler for the Node API /receivers/{receiverId}/target endpoint typedef std::function(const nmos::id& receiver_id, const web::json::value& sender_data, slog::base_gate& gate)> node_api_target_handler; - // implement the Node API /receivers/{receiverId}/target endpoint using the Connection API implementation with the specified transport file parser and the specified validator + // implement the Node API /receivers/{receiverId}/target endpoint using the Connection API implementation with the specified handlers // (the /target endpoint is only required to support RTP transport, other transport types use the Connection API) - node_api_target_handler make_node_api_target_handler(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged); + node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token); + + inline node_api_target_handler make_node_api_target_handler(nmos::node_model& model, load_ca_certificates_handler load_ca_certificates, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged) + { + return make_node_api_target_handler(model, std::move(load_ca_certificates), std::move(parse_transport_file), std::move(validate_merged), {}); + } + + inline node_api_target_handler make_node_api_target_handler(nmos::node_model& model, transport_file_parser parse_transport_file, details::connection_resource_patch_validator validate_merged) + { + return make_node_api_target_handler(model, {}, std::move(parse_transport_file), std::move(validate_merged)); + } - // implement the Node API /receivers/{receiverId}/target endpoint using the Connection API implementation with the specified transport file parser - // (the /target endpoint is only required to support RTP transport, other transport types use the Connection API) inline node_api_target_handler make_node_api_target_handler(nmos::node_model& model, transport_file_parser parse_transport_file) { - return make_node_api_target_handler(model, parse_transport_file, {}); + return make_node_api_target_handler(model, std::move(parse_transport_file), {}); } - // implement the Node API /receivers/{receiverId}/target endpoint using the Connection API implementation with the default transport file parser - // (the /target endpoint is only required to support RTP transport, other transport types use the Connection API) node_api_target_handler make_node_api_target_handler(nmos::node_model& model); } diff --git a/Development/nmos/node_behaviour.cpp b/Development/nmos/node_behaviour.cpp index 1d51f4d5f..5dc00d891 100644 --- a/Development/nmos/node_behaviour.cpp +++ b/Development/nmos/node_behaviour.cpp @@ -7,6 +7,7 @@ #include "mdns/service_discovery.h" #include "nmos/api_downgrade.h" #include "nmos/api_utils.h" // for nmos::type_from_resourceType +#include "nmos/authorization_state.h" #include "nmos/client_utils.h" #include "nmos/mdns.h" #include "nmos/model.h" @@ -21,11 +22,11 @@ namespace nmos { namespace details { - void node_behaviour_thread(nmos::model& model, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); // registered operation - void initial_registration(nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, slog::base_gate& gate); - void registered_operation(const nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, registration_handler registration_changed, slog::base_gate& gate); + void initial_registration(nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate); + void registered_operation(const nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate); // peer to peer operation void peer_to_peer_operation(nmos::model& model, const nmos::id& grain_id, mdns::service_discovery& discovery, mdns::service_advertiser& advertiser, slog::base_gate& gate); @@ -42,7 +43,7 @@ namespace nmos // uses the default DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_behaviour_thread(nmos::model& model, registration_handler registration_changed, slog::base_gate& gate_) + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate_) { nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::node_behaviour)); @@ -51,35 +52,50 @@ namespace nmos mdns::service_discovery discovery(gate); - details::node_behaviour_thread(model, std::move(registration_changed), advertiser, discovery, gate); + details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), std::move(get_authorization_bearer_token), advertiser, discovery, gate); + } + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, slog::base_gate& gate_) + { + nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::node_behaviour)); + + mdns::service_advertiser advertiser(gate); + mdns::service_advertiser_guard advertiser_guard(advertiser); + + mdns::service_discovery discovery(gate); + + details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), {}, advertiser, discovery, gate); } // uses the specified DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_behaviour_thread(nmos::model& model, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate_) + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate_) { nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::node_behaviour)); - details::node_behaviour_thread(model, std::move(registration_changed), advertiser, discovery, gate); + details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), std::move(get_authorization_bearer_token), advertiser, discovery, gate); + } + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) + { + node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), {}, advertiser, discovery, gate); } // uses the default DNS-SD implementation - void node_behaviour_thread(nmos::model& model, slog::base_gate& gate) + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { - node_behaviour_thread(model, {}, gate); + node_behaviour_thread(model, load_ca_certificates, {}, {}, gate); } // uses the specified DNS-SD implementation - void node_behaviour_thread(nmos::model& model, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) { - node_behaviour_thread(model, {}, advertiser, discovery, gate); + node_behaviour_thread(model, load_ca_certificates, {}, {}, advertiser, discovery, gate); } - void details::node_behaviour_thread(nmos::model& model, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) + void details::node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate) { // The possible states of node behaviour represent the two primary modes (registered operation and peer-to-peer operation) // and a few hopefully ephemeral states as the node works through the "Standard Registration Sequences". - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html enum { initial_discovery, @@ -97,7 +113,7 @@ namespace nmos details::advertise_node_service(model, advertiser); // "If the chosen Registration API does not respond correctly at any time, another Registration API should be selected from the discovered list." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/3.1.%20Discovery%20-%20Registered%20Operation.md + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/3.1._Discovery_-_Registered_Operation.html // hmm, it seems inefficient to store the discovered list in settings, when it's currently only used by this thread, but TR-1001-1:2018 insists // "Media Nodes should, through product-specific means, provide a status parameter indicating which registration service is currently in use." @@ -147,7 +163,7 @@ namespace nmos // "Should a 5xx error be encountered when interacting with all discoverable Registration APIs it is recommended that clients // implement an exponential backoff algorithm in their next attempts until a non-5xx response code is received." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md#node-encounters-http-500-or-other-5xx-inability-to-connect-or-a-timeout-on-heartbeat + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#node-encounters-http-500-or-other-5xx-inability-to-connect-or-a-timeout-on-heartbeat auto lock = model.read_lock(); discovery_backoff = (std::min)((std::max)((double)nmos::fields::discovery_backoff_min(model.settings), discovery_backoff * nmos::fields::discovery_backoff_factor(model.settings)), (double)nmos::fields::discovery_backoff_max(model.settings)); } @@ -160,7 +176,7 @@ namespace nmos case initial_registration: // "5. The Node registers itself with the Registration API by taking the object it holds under the Node API's /self resource and POSTing this to the Registration API." - details::initial_registration(self_id, model, grain_id, gate); + details::initial_registration(self_id, model, grain_id, load_ca_certificates, get_authorization_bearer_token, gate); if (details::has_discovered_registration_services(model)) { @@ -177,7 +193,7 @@ namespace nmos case registered_operation: // "6. The Node persists itself in the registry by issuing heartbeats." // "7. The Node registers its other resources (from /devices, /sources etc) with the Registration API." - details::registered_operation(self_id, model, grain_id, registration_changed, gate); + details::registered_operation(self_id, model, grain_id, load_ca_certificates, registration_changed, get_authorization_bearer_token, gate); if (details::has_discovered_registration_services(model)) { @@ -225,39 +241,6 @@ namespace nmos advertise_node_service(advertiser, with_read_lock(model.mutex, [&] { return model.settings; })); } - // query DNS Service Discovery for any Registration API in the specified browse domain, having priority in the specified range - // otherwise, after timeout or cancellation, returning the fallback registration service - web::json::value discover_registration_services(mdns::service_discovery& discovery, const std::string& browse_domain, const std::set& versions, const std::pair& priorities, const std::set& protocols, const std::set& authorization, const web::uri& fallback_registration_service, slog::base_gate& gate, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token = pplx::cancellation_token::none()) - { - std::list registration_services; - - if (nmos::service_priorities::no_priority != priorities.first) - { - slog::log(gate, SLOG_FLF) << "Attempting discovery of a Registration API in domain: " << browse_domain; - - registration_services = nmos::experimental::resolve_service(discovery, nmos::service_types::registration, browse_domain, versions, priorities, protocols, authorization, true, timeout, token).get(); - - if (!registration_services.empty()) - { - slog::log(gate, SLOG_FLF) << "Discovered " << registration_services.size() << " Registration API(s)"; - } - else - { - slog::log(gate, SLOG_FLF) << "Did not discover a suitable Registration API via DNS-SD"; - } - } - - if (registration_services.empty()) - { - if (!fallback_registration_service.is_empty()) - { - registration_services.push_back(fallback_registration_service); - } - } - - return web::json::value_from_elements(registration_services | boost::adaptors::transformed([](const web::uri& u) { return u.to_string(); })); - } - // get the fallback registration service from settings (if present) web::uri get_registration_service(const nmos::settings& settings) { @@ -274,34 +257,55 @@ namespace nmos // query DNS Service Discovery for any Registration API based on settings bool discover_registration_services(nmos::base_model& model, mdns::service_discovery& discovery, slog::base_gate& gate, const pplx::cancellation_token& token) { - std::string browse_domain; - std::set versions; - std::pair priorities; - std::set protocols; - std::set authorization; // yes, this is almost equivalent to a tribool - web::uri fallback_registration_service; - int timeout; - with_read_lock(model.mutex, [&] + slog::log(gate, SLOG_FLF) << "Trying Registration API discovery"; + + // lock to read settings, then unlock to wait for the discovery task to complete + auto registration_services = with_read_lock(model.mutex, [&] { auto& settings = model.settings; - browse_domain = utility::us2s(nmos::get_domain(settings)); - versions = nmos::is04_versions::from_settings(settings); - priorities = { nmos::fields::highest_pri(settings), nmos::fields::lowest_pri(settings) }; - protocols = { nmos::get_service_protocol(settings) }; - authorization = { nmos::get_service_authorization(settings) }; - fallback_registration_service = get_registration_service(settings); - - // use a short timeout that's long enough to ensure the daemon's cache is exhausted - // when no cancellation token is specified - timeout = token.is_cancelable() ? nmos::fields::discovery_backoff_max(settings) : 1; - }); - slog::log(gate, SLOG_FLF) << "Trying Registration API discovery for about " << std::fixed << std::setprecision(3) << (double)timeout << " seconds"; - auto registration_services = discover_registration_services(discovery, browse_domain, versions, priorities, protocols, authorization, fallback_registration_service, gate, std::chrono::seconds(timeout), token); - with_write_lock(model.mutex, [&] { model.settings[nmos::fields::registration_services] = registration_services; }); - model.notify(); + if (nmos::service_priorities::no_priority != nmos::fields::highest_pri(settings)) + { + slog::log(gate, SLOG_FLF) << "Attempting discovery of a Registration API in domain: " << nmos::get_domain(settings); - return !web::json::empty(registration_services); + return nmos::experimental::resolve_service(discovery, nmos::service_types::registration, settings, token); + } + else + { + return pplx::task_from_result(std::list{}); + } + }).get(); + + with_write_lock(model.mutex, [&] + { + if (!registration_services.empty()) + { + slog::log(gate, SLOG_FLF) << "Discovered " << registration_services.size() << " Registration API(s)"; + } + else + { + slog::log(gate, SLOG_FLF) << "Did not discover a suitable Registration API via DNS-SD"; + + auto fallback_registration_service = get_registration_service(model.settings); + if (!fallback_registration_service.is_empty()) + { + registration_services.push_back(fallback_registration_service); + } + } + + if (!registration_services.empty()) slog::log(gate, SLOG_FLF) << "Using the Registration API(s):" << slog::log_manip([&](slog::log_statement& s) + { + for (auto& registration_service : registration_services) + { + s << '\n' << registration_service.to_string(); + } + }); + + model.settings[nmos::fields::registration_services] = web::json::value_from_elements(registration_services | boost::adaptors::transformed([](const web::uri& u) { return u.to_string(); })); + model.notify(); + }); + + return !registration_services.empty(); } bool empty_registration_services(const nmos::settings& settings) @@ -428,13 +432,13 @@ namespace nmos // "For HTTP codes 400 and upwards, a JSON format response MUST be returned [in which] // the 'code' should always match the HTTP status code. 'error' must always be present // and in string format. 'debug' may be null if no further debug information is available" - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.0.%20APIs.md#error-codes--responses + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.0._APIs.html#error-codes--responses // Especially in the case of client (4xx) errors, logging these would be a good idea, but // would necessitate blocking for the response body, and extracting them from the json // and dealing with potential errors along the way... // "A 500 [or other 5xx] error, inability to connect or a timeout indicates a server side or connectivity issue." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md#node-encounters-http-500-or-other-5xx-inability-to-connect-or-a-timeout-on-heartbeat + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#node-encounters-http-500-or-other-5xx-inability-to-connect-or-a-timeout-on-heartbeat if (handle_client_error_as_server_error ? web::http::is_error_status_code(response.status_code()) : web::http::is_server_error_status_code(response.status_code())) { // this could be regarded as a 'severe' error - presumably it is for the registry @@ -446,7 +450,7 @@ namespace nmos } // "A 400 [or other 4xx] error [in response to a POST] indicates a client error which is likely // to be the result of a validation failure identified by the Registration API." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md#node-encounters-http-400-or-other-4xx-on-registration + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#node-encounters-http-400-or-other-4xx-on-registration else if (web::http::is_client_error_status_code(response.status_code())) { // the severity here is trickier, since if it truly indicated a validation failure, this is a 'severe' error @@ -482,16 +486,16 @@ namespace nmos handle_registration_error_conditions(response, false, gate, operation); } - web::http::client::http_client_config make_registration_client_config(const nmos::settings& settings) + web::http::client::http_client_config make_registration_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) { - auto config = nmos::make_http_client_config(settings); + auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), bearer_token, gate); config.set_timeout(std::chrono::seconds(nmos::fields::registration_request_max(settings))); return config; } - web::http::client::http_client_config make_heartbeat_client_config(const nmos::settings& settings) + web::http::client::http_client_config make_heartbeat_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, const web::http::oauth2::experimental::oauth2_token& bearer_token, slog::base_gate& gate) { - auto config = nmos::make_http_client_config(settings); + auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), bearer_token, gate); config.set_timeout(std::chrono::seconds(nmos::fields::registration_heartbeat_max(settings))); return config; } @@ -507,7 +511,7 @@ namespace nmos // A 'removed' event calls for registration deletion, i.e. a DELETE request with a 204 'No Content' response // A 'modified' event calls for a registration update, i.e. a POST request with a 200 'OK' response (201 'Created'is unexpected) // A 'sync' event is also an (unnecessary) registration update, i.e. a POST request with a 200 'OK' response - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/RegistrationAPI.raml + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/RegistrationAPI.html const bool creation = resource_added_event == event_type; const bool update = resource_modified_event == event_type || resource_unchanged_event == event_type; @@ -526,7 +530,7 @@ namespace nmos // "On first registration with a Registration API this should result in a '201 Created' HTTP response code. // If a Node receives a 200 code in this case, a previous record of the Node can be assumed to still exist." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md#node-encounters-http-200-on-first-registration + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#node-encounters-http-200-on-first-registration if (web::http::status_codes::Created == response.status_code()) { @@ -550,7 +554,8 @@ namespace nmos // Location may be a relative (to the request URL) or absolute URL auto request_uri = web::uri_builder(client.base_uri()).append_path(U("/resource")).to_uri(); auto location_uri = request_uri.resolve_uri(response.headers()[web::http::header_names::location]); - deletion = api_request(web::http::client::http_client(location_uri, client.client_config()), web::http::methods::DEL, gate, token); + auto deletion_client = nmos::details::make_http_client(location_uri, client.client_config()); + deletion = api_request(*deletion_client, web::http::methods::DEL, gate, token); } else { @@ -654,7 +659,7 @@ namespace nmos slog::log(gate, SLOG_FLF) << "Registration heartbeat error: " << response.status_code() << " " << response.reason_phrase(); // "On encountering this code, a Node must re-register each of its resources with the Registration API in order." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md#node-encounters-http-404-on-heartbeat + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#node-encounters-http-404-on-heartbeat return false; } else @@ -669,7 +674,7 @@ namespace nmos } // there is significant similarity between initial_registration and registered_operation but I'm too tired to refactor again right now... - void initial_registration(nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, slog::base_gate& gate) + void initial_registration(nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate) { slog::log(gate, SLOG_FLF) << "Attempting initial registration"; @@ -729,7 +734,7 @@ namespace nmos // "Nodes which support multiple versions simultaneously MUST ensure that all of their resources meet the schemas for each corresponding version of the specification[...] // It may be necessary to expose only a limited subset of a Node's resources from lower versioned endpoints." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2.2/docs/6.0.%20Upgrade%20Path.md#version-translations + // See https://specs.amwa.tv/is-04/releases/v1.2.2/docs/6.0._Upgrade_Path.html#version-translations // base uri should be like http://api.example.com/x-nmos/registration/{version} const auto registry_version = parse_api_version(web::uri::split_path(base_uri.path()).back()); @@ -756,7 +761,8 @@ namespace nmos grain.updated = strictly_increasing_update(resources); }); - registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings))); + const auto bearer_token = get_authorization_bearer_token ? get_authorization_bearer_token() : web::http::oauth2::experimental::oauth2_token{}; + registration_client = nmos::details::make_http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, bearer_token, gate)); } events = web::json::value::array(); @@ -779,7 +785,7 @@ namespace nmos self_id = id_type.first; - slog::log(gate, SLOG_FLF) << "Registering nmos-cpp node with the Registration API at: " << registration_client->base_uri().host() << ":" << registration_client->base_uri().port(); + slog::log(gate, SLOG_FLF) << "Registering nmos-cpp node with the Registration API at: " << registration_client->base_uri().to_string(); auto token = cancellation_source.get_token(); request = details::request_registration(*registration_client, events.at(0), gate, token).then([&](pplx::task finally) @@ -827,7 +833,7 @@ namespace nmos request.wait(); } - void registered_operation(const nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, registration_handler registration_changed, slog::base_gate& gate) + void registered_operation(const nmos::id& self_id, nmos::model& model, const nmos::id& grain_id, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate) { slog::log(gate, SLOG_FLF) << "Adopting registered operation"; @@ -850,6 +856,9 @@ namespace nmos std::chrono::steady_clock::time_point heartbeat_time; + web::http::oauth2::experimental::oauth2_token registration_bearer_token; + web::http::oauth2::experimental::oauth2_token heartbeat_bearer_token; + // background tasks may read/write the above local state by reference pplx::cancellation_token_source cancellation_source; pplx::task request = pplx::task_from_result(); @@ -893,13 +902,14 @@ namespace nmos const auto registry_version = parse_api_version(web::uri::split_path(base_uri.path()).back()); if (registry_version != grain->version) break; - registration_client.reset(new web::http::client::http_client(base_uri, make_registration_client_config(model.settings))); - heartbeat_client.reset(new web::http::client::http_client(base_uri, make_heartbeat_client_config(model.settings))); + const auto bearer_token = get_authorization_bearer_token ? get_authorization_bearer_token() : web::http::oauth2::experimental::oauth2_token{}; + registration_client = nmos::details::make_http_client(base_uri, make_registration_client_config(model.settings, load_ca_certificates, bearer_token, gate)); + heartbeat_client = nmos::details::make_http_client(base_uri, make_heartbeat_client_config(model.settings, load_ca_certificates, bearer_token, gate)); // "The first interaction with a new Registration API [after a server side or connectivity issue] // should be a heartbeat to confirm whether whether the Node is still present in the registry" - slog::log(gate, SLOG_FLF) << "Attempting registration heartbeats with the Registration API at: " << registration_client->base_uri().host() << ":" << registration_client->base_uri().port(); + slog::log(gate, SLOG_FLF) << "Attempting registration heartbeats with the Registration API at: " << registration_client->base_uri().to_string(); node_registered = false; @@ -928,15 +938,29 @@ namespace nmos } model.notify(); - }).then([=, &heartbeat_time, &heartbeat_client, &gate] + }).then([=, &model, &heartbeat_time, &heartbeat_client, &heartbeat_bearer_token, &gate] { // "6. The Node persists itself in the registry by issuing heartbeats." - return pplx::do_while([=, &heartbeat_time, &heartbeat_client, &gate] + return pplx::do_while([=, &model, &heartbeat_time, &heartbeat_client, &heartbeat_bearer_token, &gate] { - return pplx::complete_at(heartbeat_time + heartbeat_interval, token).then([=, &heartbeat_time, &heartbeat_client, &gate]() mutable + return pplx::complete_at(heartbeat_time + heartbeat_interval, token).then([=, &model, &heartbeat_time, &heartbeat_client, &heartbeat_bearer_token, &gate]() mutable { heartbeat_time = std::chrono::steady_clock::now(); + + // renew heartbeat_client if bearer token has changed + if (get_authorization_bearer_token) + { + const auto& bearer_token = get_authorization_bearer_token(); + if (heartbeat_bearer_token.access_token() != bearer_token.access_token()) + { + slog::log(gate, SLOG_FLF) << "Update heartbeat client with new authorization token"; + + heartbeat_bearer_token = bearer_token; + heartbeat_client = nmos::details::make_http_client(base_uri, make_heartbeat_client_config(model.settings, load_ca_certificates, bearer_token, gate)); + } + } + return update_node_health(*heartbeat_client, self_id, gate, token); }); }, token); @@ -981,6 +1005,20 @@ namespace nmos const auto event_type = get_resource_event_type(events.at(0)); auto token = cancellation_source.get_token(); + + // renew registration_client if bearer token has changed + if (get_authorization_bearer_token) + { + const auto& bearer_token = get_authorization_bearer_token(); + if (registration_bearer_token.access_token() != bearer_token.access_token()) + { + slog::log(gate, SLOG_FLF) << "Update registration client with new authorization token"; + + registration_bearer_token = bearer_token; + registration_client = nmos::details::make_http_client(registration_client->base_uri(), make_registration_client_config(model.settings, load_ca_certificates, bearer_token, gate)); + } + } + request = details::request_registration(*registration_client, events.at(0), gate, token).then([&](pplx::task finally) { auto lock = model.write_lock(); // in order to update local state @@ -996,7 +1034,7 @@ namespace nmos } // "Following deletion of all other resources, the Node resource may be deleted and heartbeating stopped." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md#controlled-unregistration + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#controlled-unregistration if (self_id == id_type.first && resource_removed_event == event_type) { node_unregistered = true; @@ -1147,12 +1185,13 @@ namespace nmos events = web::json::value::array(); // throttle updates to the DNS-SD daemon - // (because otherwise it may report that it's having to do that itself) // "to protect the network against excessive packet flooding due to software // bugs or malicious attack, a Multicast DNS responder MUST NOT multicast a // record on a given interface until at least one second has elapsed since // the last time that record was multicast on that particular interface." // see https://tools.ietf.org/html/rfc6762#section-6 + // (unfortunately mDNSResponder may still report "excessive update rate" + // because it implements a target interval of 6 seconds...) const auto max_update_rate = std::chrono::seconds(1); const auto now = tai_clock::now(); if (earliest_allowed_update > now) @@ -1164,12 +1203,13 @@ namespace nmos continue; } + slog::log(gate, SLOG_FLF) << "Updating the 'ver_' TXT records"; update_node_service(advertiser, model.settings, ver); earliest_allowed_update = now + max_update_rate; } - // withdraw the 'ver_' TXT records + slog::log(gate, SLOG_FLF) << "Withdrawing the 'ver_' TXT records"; update_node_service(advertiser, model.settings); cancellation_source.cancel(); diff --git a/Development/nmos/node_behaviour.h b/Development/nmos/node_behaviour.h index 855c4ccde..c98abf854 100644 --- a/Development/nmos/node_behaviour.h +++ b/Development/nmos/node_behaviour.h @@ -2,6 +2,8 @@ #define NMOS_NODE_BEHAVIOUR_H #include +#include "nmos/authorization_handlers.h" +#include "nmos/certificate_handlers.h" namespace web { @@ -20,8 +22,8 @@ namespace mdns } // Node behaviour including both registered operation and peer to peer operation -// See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md -// and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/3.1.%20Discovery%20-%20Registered%20Operation.md +// See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html +// and https://specs.amwa.tv/is-04/releases/v1.2.0/docs/3.1._Discovery_-_Registered_Operation.html namespace nmos { struct model; @@ -34,17 +36,19 @@ namespace nmos // uses the default DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_behaviour_thread(nmos::model& model, registration_handler registration_changed, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, slog::base_gate& gate); // uses the specified DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_behaviour_thread(nmos::model& model, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, nmos::experimental::get_authorization_bearer_token_handler get_authorization_bearer_token, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, registration_handler registration_changed, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); // uses the default DNS-SD implementation - void node_behaviour_thread(nmos::model& model, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); // uses the specified DNS-SD implementation - void node_behaviour_thread(nmos::model& model, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); + void node_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, mdns::service_advertiser& advertiser, mdns::service_discovery& discovery, slog::base_gate& gate); } #endif diff --git a/Development/nmos/node_resource.cpp b/Development/nmos/node_resource.cpp index 84e47f0d9..8e5af4744 100644 --- a/Development/nmos/node_resource.cpp +++ b/Development/nmos/node_resource.cpp @@ -9,7 +9,7 @@ namespace nmos { - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/schemas/node.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/node.html nmos::resource make_node(const nmos::id& id, const web::json::value& clocks, const web::json::value& interfaces, const nmos::settings& settings) { using web::json::value; @@ -35,7 +35,8 @@ namespace nmos web::json::push_back(data[U("api")][U("endpoints")], value_of({ { U("host"), host }, { U("port"), uri.port() }, - { U("protocol"), uri.scheme() } + { U("protocol"), uri.scheme() }, + { U("authorization"), nmos::experimental::fields::server_authorization(settings) } })); } @@ -56,7 +57,7 @@ namespace nmos return make_node(id, {}, {}, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/clock_internal.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/clock_internal.html web::json::value make_internal_clock(const nmos::clock_name& clk) { using web::json::value_of; @@ -67,7 +68,7 @@ namespace nmos }); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/clock_ptp.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/clock_ptp.html web::json::value make_ptp_clock(const nmos::clock_name& clk, bool traceable, const utility::string_t& gmid, bool locked) { using web::json::value_of; diff --git a/Development/nmos/node_resource.h b/Development/nmos/node_resource.h index 17cc3c57c..6d4a358ba 100644 --- a/Development/nmos/node_resource.h +++ b/Development/nmos/node_resource.h @@ -20,14 +20,14 @@ namespace nmos struct clock_name; struct resource; - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/schemas/node.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/node.html nmos::resource make_node(const nmos::id& id, const web::json::value& clocks, const web::json::value& interfaces, const nmos::settings& settings); nmos::resource make_node(const nmos::id& id, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/clock_internal.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/clock_internal.html web::json::value make_internal_clock(const nmos::clock_name& clock_name); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/clock_ptp.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/clock_ptp.html web::json::value make_ptp_clock(const nmos::clock_name& clock_name, bool traceable, const utility::string_t& gmid, bool locked); } diff --git a/Development/nmos/node_resources.cpp b/Development/nmos/node_resources.cpp index 7765f6425..534b6b2b8 100644 --- a/Development/nmos/node_resources.cpp +++ b/Development/nmos/node_resources.cpp @@ -16,15 +16,17 @@ #include "nmos/is05_versions.h" #include "nmos/is07_versions.h" #include "nmos/is08_versions.h" +#include "nmos/is12_versions.h" #include "nmos/media_type.h" #include "nmos/resource.h" +#include "nmos/sdp_utils.h" // for nmos::make_components #include "nmos/transfer_characteristic.h" #include "nmos/transport.h" #include "nmos/version.h" namespace nmos { - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/device.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/device.html nmos::resource make_device(const nmos::id& id, const nmos::id& node_id, const std::vector& senders, const std::vector& receivers, const nmos::settings& settings) { using web::json::value; @@ -54,7 +56,8 @@ namespace nmos { web::json::push_back(data[U("controls")], value_of({ { U("href"), connection_uri.set_host(host).to_uri().to_string() }, - { U("type"), type } + { U("type"), type }, + { U("authorization"), nmos::experimental::fields::server_authorization(settings) } })); } } @@ -74,7 +77,8 @@ namespace nmos { web::json::push_back(data[U("controls")], value_of({ { U("href"), events_uri.set_host(host).to_uri().to_string() }, - { U("type"), type } + { U("type"), type }, + { U("authorization"), nmos::experimental::fields::server_authorization(settings) } })); } } @@ -85,7 +89,7 @@ namespace nmos // At the moment, it doesn't seem necessary to enable support multiple API instances via the API selector mechanism // so therefore just a single Channel Mapping API instance is mounted directly at /x-nmos/channelmapping/{version}/ // If it becomes necessary, each device could associated with a specific API selector - // See https://github.com/AMWA-TV/nmos-audio-channel-mapping/blob/v1.0.x/docs/2.0.%20APIs.md#api-paths + // See https://specs.amwa.tv/is-08/releases/v1.0.1/docs/2.0._APIs.html#api-paths for (const auto& version : nmos::is08_versions::from_settings(settings)) { @@ -99,7 +103,8 @@ namespace nmos { web::json::push_back(data[U("controls")], value_of({ { U("href"), channelmapping_uri.set_host(host).to_uri().to_string() }, - { U("type"), type } + { U("type"), type }, + { U("authorization"), nmos::experimental::fields::server_authorization(settings) } })); } } @@ -107,7 +112,7 @@ namespace nmos if (0 <= nmos::experimental::fields::manifest_port(settings)) { - // See https://github.com/AMWA-TV/nmos-parameter-registers/blob/master/device-control-types/manifest-base.md + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/device-control-types/manifest-base.html // and nmos::experimental::make_manifest_api_manifest auto manifest_uri = web::uri_builder() .set_scheme(nmos::http_scheme(settings)) @@ -119,15 +124,38 @@ namespace nmos { web::json::push_back(data[U("controls")], value_of({ { U("href"), manifest_uri.set_host(host).to_uri().to_string() }, - { U("type"), type } + { U("type"), type }, + { U("authorization"), nmos::experimental::fields::server_authorization(settings) } })); } } + if (0 <= nmos::fields::control_protocol_ws_port(settings)) + { + for (const auto& version : nmos::is12_versions::from_settings(settings)) + { + // See https://specs.amwa.tv/is-12/branches/v1.0.x/docs/IS-04_interactions.html + auto ncp_uri = web::uri_builder() + .set_scheme(nmos::ws_scheme(settings)) + .set_port(nmos::fields::control_protocol_ws_port(settings)) + .set_path(U("/x-nmos/ncp/") + make_api_version(version)); + auto type = U("urn:x-nmos:control:ncp/") + make_api_version(version); + + for (const auto& host : hosts) + { + web::json::push_back(data[U("controls")], value_of({ + { U("href"), ncp_uri.set_host(host).to_uri().to_string() }, + { U("type"), type }, + { U("authorization"), nmos::experimental::fields::server_authorization(settings) } + })); + } + } + } + return{ is04_versions::v1_3, types::device, std::move(data), false }; } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_core.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_core.html nmos::resource make_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const nmos::settings& settings) { using web::json::value; @@ -138,12 +166,12 @@ namespace nmos data[U("caps")] = value::object(); data[U("device_id")] = value::string(device_id); data[U("parents")] = value::array(); - data[U("clock_name")] = !clk.name.empty() ? value::string(clk.name) : value::null(); + data[U("clock_name")] = !clk.empty() ? value::string(clk.name) : value::null(); return{ is04_versions::v1_3, types::source, std::move(data), false }; } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_generic.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_generic.html nmos::resource make_generic_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const nmos::format& format, const nmos::settings& settings) { using web::json::value; @@ -161,7 +189,7 @@ namespace nmos return make_generic_source(id, device_id, {}, grain_rate, format, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_generic.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_generic.html nmos::resource make_video_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const nmos::settings& settings) { return make_generic_source(id, device_id, clk, grain_rate, nmos::formats::video, settings); @@ -172,7 +200,7 @@ namespace nmos return make_video_source(id, device_id, {}, grain_rate, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_generic.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_generic.html nmos::resource make_data_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const nmos::settings& settings) { return make_generic_source(id, device_id, clk, grain_rate, nmos::formats::data, settings); @@ -183,7 +211,7 @@ namespace nmos return make_data_source(id, device_id, {}, grain_rate, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.3/APIs/schemas/source_data.json + // See https://specs.amwa.tv/is-04/releases/v1.3.0/APIs/schemas/with-refs/source_data.html nmos::resource make_data_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const nmos::event_type& event_type, const nmos::settings& settings) { using web::json::value; @@ -201,7 +229,7 @@ namespace nmos return make_data_source(id, device_id, {}, grain_rate, event_type, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_audio.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_audio.html nmos::resource make_audio_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const std::vector& channels, const nmos::settings& settings) { using web::json::value; @@ -236,7 +264,7 @@ namespace nmos return make_mux_source(id, device_id, {}, grain_rate, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_core.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_core.html nmos::resource make_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, const nmos::settings& settings) { using web::json::value; @@ -252,7 +280,7 @@ namespace nmos return{ is04_versions::v1_3, types::flow, std::move(data), false }; } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_video.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_video.html nmos::resource make_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, unsigned int frame_width, unsigned int frame_height, const nmos::interlace_mode& interlace_mode, const nmos::colorspace& colorspace, const nmos::transfer_characteristic& transfer_characteristic, const nmos::settings& settings) { using web::json::value; @@ -270,7 +298,22 @@ namespace nmos return resource; } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_video_raw.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_video_raw.html + nmos::resource make_raw_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, unsigned int frame_width, unsigned int frame_height, const nmos::interlace_mode& interlace_mode, const nmos::colorspace& colorspace, const nmos::transfer_characteristic& transfer_characteristic, const sdp::sampling& color_sampling, unsigned int bit_depth, const nmos::settings& settings) + { + using web::json::value; + + auto resource = make_video_flow(id, source_id, device_id, grain_rate, frame_width, frame_height, interlace_mode, colorspace, transfer_characteristic, settings); + auto& data = resource.data; + + data[U("media_type")] = value::string(nmos::media_types::video_raw.name); + + data[U("components")] = make_components(color_sampling, frame_width, frame_height, bit_depth); + + return resource; + } + + // deprecated, see overload with sdp::sampling nmos::resource make_raw_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, unsigned int frame_width, unsigned int frame_height, const nmos::interlace_mode& interlace_mode, const nmos::colorspace& colorspace, const nmos::transfer_characteristic& transfer_characteristic, chroma_subsampling chroma_subsampling, unsigned int bit_depth, const nmos::settings& settings) { using web::json::value; @@ -285,14 +328,16 @@ namespace nmos return resource; } + // deprecated, constructs a 1920 x 1080, interlaced, BT709, SDR, YCbCr-4:2:2, 10 bit raw video Flow nmos::resource make_raw_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::settings& settings) { return make_raw_video_flow(id, source_id, device_id, {}, 1920, 1080, nmos::interlace_modes::interlaced_bff, nmos::colorspaces::BT709, nmos::transfer_characteristics::SDR, YCbCr422, 10, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_video_coded.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_video_coded.html + // and https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#components // (media_type must *not* be nmos::media_types::video_raw; cf. nmos::make_raw_video_flow) - nmos::resource make_coded_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, unsigned int frame_width, unsigned int frame_height, const nmos::interlace_mode& interlace_mode, const nmos::colorspace& colorspace, const nmos::transfer_characteristic& transfer_characteristic, const nmos::media_type& media_type, const nmos::settings& settings) + nmos::resource make_coded_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, unsigned int frame_width, unsigned int frame_height, const nmos::interlace_mode& interlace_mode, const nmos::colorspace& colorspace, const nmos::transfer_characteristic& transfer_characteristic, const sdp::sampling& color_sampling, unsigned int bit_depth, const nmos::media_type& media_type, const nmos::settings& settings) { using web::json::value; @@ -301,10 +346,20 @@ namespace nmos data[U("media_type")] = value::string(media_type.name); + if (!color_sampling.empty()) + { + data[U("components")] = make_components(color_sampling, frame_width, frame_height, bit_depth); + } + return resource; } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_audio.json + nmos::resource make_coded_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, unsigned int frame_width, unsigned int frame_height, const nmos::interlace_mode& interlace_mode, const nmos::colorspace& colorspace, const nmos::transfer_characteristic& transfer_characteristic, const nmos::media_type& media_type, const nmos::settings& settings) + { + return make_coded_video_flow(id, source_id, device_id, grain_rate, frame_width, frame_height, interlace_mode, colorspace, transfer_characteristic, {}, {}, media_type, settings); + } + + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_audio.html nmos::resource make_audio_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& sample_rate, const nmos::settings& settings) { using web::json::value; @@ -318,7 +373,7 @@ namespace nmos return resource; } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_audio_raw.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_audio_raw.html nmos::resource make_raw_audio_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& sample_rate, unsigned int bit_depth, const nmos::settings& settings) { using web::json::value; @@ -337,7 +392,7 @@ namespace nmos return make_raw_audio_flow(id, source_id, device_id, 48000, 24, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_audio_coded.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_audio_coded.html // (media_type must *not* be nmos::media_types::audio_L(bit_depth); cf. nmos::make_raw_audio_flow) nmos::resource make_coded_audio_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& sample_rate, const nmos::media_type& media_type, const nmos::settings& settings) { @@ -351,7 +406,7 @@ namespace nmos return resource; } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_sdianc_data.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_sdianc_data.html nmos::resource make_sdianc_data_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const std::vector& did_sdids, const nmos::settings& settings) { using web::json::value; @@ -378,7 +433,7 @@ namespace nmos return make_sdianc_data_flow(id, source_id, device_id, {}, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.3/APIs/schemas/flow_json_data.json + // See https://specs.amwa.tv/is-04/releases/v1.3.0/APIs/schemas/with-refs/flow_json_data.html nmos::resource make_json_data_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::event_type& event_type, const nmos::settings& settings) { using web::json::value; @@ -389,7 +444,7 @@ namespace nmos data[U("format")] = value::string(nmos::formats::data.name); data[U("media_type")] = value::string(nmos::media_types::application_json.name); - if (!event_type.name.empty()) + if (!event_type.empty()) { data[U("event_type")] = value::string(event_type.name); } @@ -402,7 +457,7 @@ namespace nmos return make_json_data_flow(id, source_id, device_id, {}, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_data.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_data.html // (media_type must *not* be nmos::media_types::video_smpte291 or nmos::media_types::application_json; cf. nmos::make_sdianc_data_flow and nmos::make_json_data_flow) nmos::resource make_data_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::media_type& media_type, const nmos::settings& settings) { @@ -417,7 +472,7 @@ namespace nmos return resource; } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_mux.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_mux.html nmos::resource make_mux_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::media_type& media_type, const nmos::settings& settings) { using web::json::value; @@ -436,7 +491,7 @@ namespace nmos return make_mux_flow(id, source_id, device_id, nmos::media_types::video_SMPTE2022_6, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/sender.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/sender.html nmos::resource make_sender(const nmos::id& id, const nmos::id& flow_id, const nmos::transport& transport, const nmos::id& device_id, const utility::string_t& manifest_href, const std::vector& interfaces, const nmos::settings& settings) { using web::json::value; @@ -449,7 +504,7 @@ namespace nmos data[U("transport")] = value::string(transport.name); data[U("device_id")] = value::string(device_id); // "Permit a Sender's 'manifest_href' to be null when the transport type does not require a transport file" from IS-04 v1.3 - // See https://github.com/AMWA-TV/nmos-discovery-registration/pull/97 + // See https://github.com/AMWA-TV/is-04/pull/97 data[U("manifest_href")] = !manifest_href.empty() ? value::string(manifest_href) : value::null(); auto& interface_bindings = data[U("interface_bindings")] = value::array(); @@ -466,7 +521,7 @@ namespace nmos nmos::resource result{ is04_versions::v1_3, types::sender, std::move(data), false }; // only RTP Senders are permitted prior to v1.3, so specify an appropriate minimum API version - // see https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.3/docs/2.1.%20APIs%20-%20Common%20Keys.md#transport + // see https://specs.amwa.tv/is-04/releases/v1.3.0/docs/2.1._APIs_-_Common_Keys.html#transport result.downgrade_version = nmos::transports::rtp == nmos::transport_base(transport) ? is04_versions::v1_0 : is04_versions::v1_3; @@ -492,7 +547,7 @@ namespace nmos return make_sender(id, flow_id, nmos::transports::rtp_mcast, device_id, experimental::make_manifest_api_manifest(id, settings).to_string(), interfaces, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_core.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_core.html nmos::resource make_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::settings& settings) { using web::json::value; @@ -516,7 +571,7 @@ namespace nmos nmos::resource result{ is04_versions::v1_3, types::receiver, std::move(data), false }; // only RTP Receivers are permitted prior to v1.3, so specify an appropriate minimum API version - // see https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.3/docs/2.1.%20APIs%20-%20Common%20Keys.md#transport + // see https://specs.amwa.tv/is-04/releases/v1.3.0/docs/2.1._APIs_-_Common_Keys.html#transport result.downgrade_version = nmos::transports::rtp == nmos::transport_base(transport) ? is04_versions::v1_0 : is04_versions::v1_3; @@ -524,35 +579,39 @@ namespace nmos return result; } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_video.json - nmos::resource make_video_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::settings& settings) + nmos::resource make_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::format& format, const std::vector& media_types, const nmos::settings& settings) { using web::json::value; auto resource = make_receiver(id, device_id, transport, interfaces, settings); auto& data = resource.data; - data[U("format")] = value::string(nmos::formats::video.name); - data[U("caps")][U("media_types")][0] = value::string(nmos::media_types::video_raw.name); + data[U("format")] = value::string(format.name); + auto& caps = data[U("caps")] = value::object(); + + for (const auto& media_type : media_types) + { + web::json::push_back(caps[U("media_types")], value::string(media_type.name)); + } return resource; } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_audio.json - nmos::resource make_audio_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const std::vector& bit_depths, const nmos::settings& settings) + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_video.html + nmos::resource make_video_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::settings& settings) { - using web::json::value; - - auto resource = make_receiver(id, device_id, transport, interfaces, settings); - auto& data = resource.data; + return make_receiver(id, device_id, transport, interfaces, nmos::formats::video, { nmos::media_types::video_raw }, settings); + } - data[U("format")] = value::string(nmos::formats::audio.name); + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_audio.html + nmos::resource make_audio_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const std::vector& bit_depths, const nmos::settings& settings) + { + std::vector media_types; for (const auto& bit_depth : bit_depths) { - web::json::push_back(data[U("caps")][U("media_types")], value::string(nmos::media_types::audio_L(bit_depth).name)); + media_types.push_back(nmos::media_types::audio_L(bit_depth)); } - - return resource; + return make_receiver(id, device_id, transport, interfaces, nmos::formats::audio, media_types, settings); } nmos::resource make_audio_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, unsigned int bit_depth, const nmos::settings& settings) @@ -560,31 +619,21 @@ namespace nmos return make_audio_receiver(id, device_id, transport, interfaces, std::vector{ bit_depth }, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_data.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_data.html nmos::resource make_sdianc_data_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::settings& settings) { - using web::json::value; - - auto resource = make_receiver(id, device_id, transport, interfaces, settings); - auto& data = resource.data; - - data[U("format")] = value::string(nmos::formats::data.name); - data[U("caps")][U("media_types")][0] = value::string(nmos::media_types::video_smpte291.name); - - return resource; + return make_receiver(id, device_id, transport, interfaces, nmos::formats::data, { nmos::media_types::video_smpte291 }, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_data.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_data.html // (media_type must *not* be nmos::media_types::video_smpte291; cf. nmos::make_sdianc_data_receiver) nmos::resource make_data_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::media_type& media_type, const std::vector& event_types, const nmos::settings& settings) { using web::json::value; - auto resource = make_receiver(id, device_id, transport, interfaces, settings); + auto resource = make_receiver(id, device_id, transport, interfaces, nmos::formats::data, { media_type }, settings); auto& data = resource.data; - data[U("format")] = value::string(nmos::formats::data.name); - data[U("caps")][U("media_types")][0] = value::string(media_type.name); for (const auto& event_type : event_types) { web::json::push_back(data[U("caps")][U("event_types")], value::string(event_type.name)); @@ -598,18 +647,10 @@ namespace nmos return make_data_receiver(id, device_id, transport, interfaces, media_type, {}, settings); } - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_mux.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_mux.html nmos::resource make_mux_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::media_type& media_type, const nmos::settings& settings) { - using web::json::value; - - auto resource = make_receiver(id, device_id, transport, interfaces, settings); - auto& data = resource.data; - - data[U("format")] = value::string(nmos::formats::mux.name); - data[U("caps")][U("media_types")][0] = value::string(media_type.name); - - return resource; + return make_receiver(id, device_id, transport, interfaces, nmos::formats::mux, { media_type }, settings); } nmos::resource make_mux_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::settings& settings) diff --git a/Development/nmos/node_resources.h b/Development/nmos/node_resources.h index aad2e9e20..a1b9f6555 100644 --- a/Development/nmos/node_resources.h +++ b/Development/nmos/node_resources.h @@ -10,6 +10,11 @@ namespace web class uri; } +namespace sdp +{ + struct sampling; +} + namespace nmos { // IS-04 Node API resources @@ -27,104 +32,111 @@ namespace nmos struct transfer_characteristic; struct transport; - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/device.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/device.html nmos::resource make_device(const nmos::id& id, const nmos::id& node_id, const std::vector& senders, const std::vector& receivers, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_core.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_core.html nmos::resource make_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_generic.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_generic.html nmos::resource make_generic_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const nmos::format& format, const nmos::settings& settings); nmos::resource make_generic_source(const nmos::id& id, const nmos::id& device_id, const nmos::rational& grain_rate, const nmos::format& format, const nmos::settings& settings); nmos::resource make_video_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const nmos::settings& settings); nmos::resource make_video_source(const nmos::id& id, const nmos::id& device_id, const nmos::rational& grain_rate, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_generic.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_generic.html nmos::resource make_data_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const nmos::settings& settings); nmos::resource make_data_source(const nmos::id& id, const nmos::id& device_id, const nmos::rational& grain_rate, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.3/APIs/schemas/source_data.json + // See https://specs.amwa.tv/is-04/releases/v1.3.0/APIs/schemas/with-refs/source_data.html nmos::resource make_data_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const nmos::event_type& event_type, const nmos::settings& settings); nmos::resource make_data_source(const nmos::id& id, const nmos::id& device_id, const nmos::rational& grain_rate, const nmos::event_type& event_type, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_audio.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_audio.html nmos::resource make_audio_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const std::vector& channels, const nmos::settings& settings); nmos::resource make_audio_source(const nmos::id& id, const nmos::id& device_id, const nmos::rational& grain_rate, const std::vector& channels, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/source_generic.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/source_generic.html nmos::resource make_mux_source(const nmos::id& id, const nmos::id& device_id, const nmos::clock_name& clk, const nmos::rational& grain_rate, const nmos::settings& settings); nmos::resource make_mux_source(const nmos::id& id, const nmos::id& device_id, const nmos::rational& grain_rate, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_core.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_core.html nmos::resource make_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_video.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_video.html nmos::resource make_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, unsigned int frame_width, unsigned int frame_height, const nmos::interlace_mode& interlace_mode, const nmos::colorspace& colorspace, const nmos::transfer_characteristic& transfer_characteristic, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_video_raw.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_video_raw.html + nmos::resource make_raw_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, unsigned int frame_width, unsigned int frame_height, const nmos::interlace_mode& interlace_mode, const nmos::colorspace& colorspace, const nmos::transfer_characteristic& transfer_characteristic, const sdp::sampling& color_sampling, unsigned int bit_depth, const nmos::settings& settings); + // deprecated, see overload with sdp::sampling nmos::resource make_raw_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, unsigned int frame_width, unsigned int frame_height, const nmos::interlace_mode& interlace_mode, const nmos::colorspace& colorspace, const nmos::transfer_characteristic& transfer_characteristic, chroma_subsampling chroma_subsampling, unsigned int bit_depth, const nmos::settings& settings); + // deprecated, constructs a 1920 x 1080, interlaced, BT709, SDR, YCbCr-4:2:2, 10 bit raw video Flow nmos::resource make_raw_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_video_coded.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_video_coded.html + // and https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#components // (media_type must *not* be nmos::media_types::video_raw; cf. nmos::make_raw_video_flow) + nmos::resource make_coded_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, unsigned int frame_width, unsigned int frame_height, const nmos::interlace_mode& interlace_mode, const nmos::colorspace& colorspace, const nmos::transfer_characteristic& transfer_characteristic, const sdp::sampling& color_sampling, unsigned int bit_depth, const nmos::media_type& media_type, const nmos::settings& settings); nmos::resource make_coded_video_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& grain_rate, unsigned int frame_width, unsigned int frame_height, const nmos::interlace_mode& interlace_mode, const nmos::colorspace& colorspace, const nmos::transfer_characteristic& transfer_characteristic, const nmos::media_type& media_type, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_audio.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_audio.html nmos::resource make_audio_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& sample_rate, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_audio_raw.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_audio_raw.html nmos::resource make_raw_audio_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& sample_rate, unsigned int bit_depth, const nmos::settings& settings); nmos::resource make_raw_audio_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_audio_coded.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_audio_coded.html // (media_type must *not* be nmos::media_types::audio_L(bit_depth); cf. nmos::make_raw_audio_flow) nmos::resource make_coded_audio_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::rational& sample_rate, const nmos::media_type& media_type, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_sdianc_data.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_sdianc_data.html nmos::resource make_sdianc_data_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const std::vector& did_sdids, const nmos::settings& settings); nmos::resource make_sdianc_data_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.3/APIs/schemas/flow_json_data.json + // See https://specs.amwa.tv/is-04/releases/v1.3.0/APIs/schemas/with-refs/flow_json_data.html nmos::resource make_json_data_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::event_type& event_type, const nmos::settings& settings); nmos::resource make_json_data_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_data.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_data.html // (media_type must *not* be nmos::media_types::video_smpte291 or nmos::media_types::application_json; cf. nmos::make_sdianc_data_flow and nmos::make_json_data_flow) nmos::resource make_data_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::media_type& media_type, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_mux.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_mux.html nmos::resource make_mux_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::media_type& media_type, const nmos::settings& settings); nmos::resource make_mux_flow(const nmos::id& id, const nmos::id& source_id, const nmos::id& device_id, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/sender.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/sender.html nmos::resource make_sender(const nmos::id& id, const nmos::id& flow_id, const nmos::transport& transport, const nmos::id& device_id, const utility::string_t& manifest_href, const std::vector& interfaces, const nmos::settings& settings); namespace experimental { web::uri make_manifest_api_manifest(const nmos::id& sender_id, const nmos::settings& settings); } - + nmos::resource make_sender(const nmos::id& id, const nmos::id& flow_id, const nmos::id& device_id, const std::vector& interfaces, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_core.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_core.html nmos::resource make_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_video.json + nmos::resource make_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::format& format, const std::vector& media_types, const nmos::settings& settings); + + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_video.html nmos::resource make_video_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_audio.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_audio.html nmos::resource make_audio_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const std::vector& bit_depths, const nmos::settings& settings); nmos::resource make_audio_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, unsigned int bit_depth, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_data.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_data.html nmos::resource make_sdianc_data_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.3/APIs/schemas/receiver_data.json + // See https://specs.amwa.tv/is-04/releases/v1.3.0/APIs/schemas/with-refs/receiver_data.html // (media_type must *not* be nmos::media_types::video_smpte291; cf. nmos::make_sdianc_data_receiver) nmos::resource make_data_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::media_type& media_type, const std::vector& event_types, const nmos::settings& settings); nmos::resource make_data_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::media_type& media_type, const nmos::settings& settings); - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_mux.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_mux.html nmos::resource make_mux_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::media_type& media_type, const nmos::settings& settings); nmos::resource make_mux_receiver(const nmos::id& id, const nmos::id& device_id, const nmos::transport& transport, const std::vector& interfaces, const nmos::settings& settings); } diff --git a/Development/nmos/node_server.cpp b/Development/nmos/node_server.cpp index d69e94308..00258389d 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -3,8 +3,10 @@ #include "cpprest/ws_utils.h" #include "nmos/api_utils.h" #include "nmos/channelmapping_activation.h" +#include "nmos/control_protocol_ws_api.h" #include "nmos/events_api.h" #include "nmos/events_ws_api.h" +#include "nmos/is04_versions.h" #include "nmos/logging_api.h" #include "nmos/manifest_api.h" #include "nmos/model.h" @@ -19,13 +21,19 @@ namespace nmos { namespace experimental { - // Construct a server instance for an NMOS Node, implementing the IS-04 Node API, IS-05 Connection API, IS-07 Events API + // Construct a server instance for an NMOS Node, implementing the IS-04 Node API, IS-05 Connection API, IS-07 Events API, the IS-10 Authorization API // and the experimental Logging API and Settings API, according to the specified data models and callbacks nmos::server make_node_server(nmos::node_model& node_model, nmos::experimental::node_implementation node_implementation, nmos::experimental::log_model& log_model, slog::base_gate& gate) { // Log the API addresses we'll be using - slog::log(gate, SLOG_FLF) << "Configuring nmos-cpp node with its primary Node API at: " << nmos::get_host(node_model.settings) << ":" << nmos::fields::node_port(node_model.settings); + slog::log(gate, SLOG_FLF) << "Configuring nmos-cpp node with its primary Node API at: " + << web::uri_builder() + .set_scheme(nmos::http_scheme(node_model.settings)) + .set_host(nmos::get_host(node_model.settings)) + .set_port(nmos::fields::node_port(node_model.settings)) + .set_path(U("/x-nmos/node/") + nmos::make_api_version(*nmos::is04_versions::from_settings(node_model.settings).rbegin())) + .to_string(); nmos::server node_server{ node_model }; @@ -33,6 +41,10 @@ namespace nmos const auto server_secure = nmos::experimental::fields::server_secure(node_model.settings); + const auto hsts = nmos::experimental::get_hsts(node_model.settings); + + const auto server_address = nmos::experimental::fields::server_address(node_model.settings); + // Configure the Settings API const host_port settings_address(nmos::experimental::fields::settings_address(node_model.settings), nmos::experimental::fields::settings_port(node_model.settings)); @@ -45,61 +57,93 @@ namespace nmos // Configure the Node API - nmos::node_api_target_handler target_handler = nmos::make_node_api_target_handler(node_model, node_implementation.parse_transport_file, node_implementation.validate_staged); - node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_node_api(node_model, target_handler, gate)); + nmos::node_api_target_handler target_handler = nmos::make_node_api_target_handler(node_model, node_implementation.load_ca_certificates, node_implementation.parse_transport_file, node_implementation.validate_staged, node_implementation.get_authorization_bearer_token); + auto validate_authorization = node_implementation.validate_authorization; + node_server.api_routers[{ {}, nmos::fields::node_port(node_model.settings) }].mount({}, nmos::make_node_api(node_model, target_handler, validate_authorization ? validate_authorization(nmos::experimental::scopes::node) : nullptr, gate)); node_server.api_routers[{ {}, nmos::experimental::fields::manifest_port(node_model.settings) }].mount({}, nmos::experimental::make_manifest_api(node_model, gate)); // Configure the Connection API - node_server.api_routers[{ {}, nmos::fields::connection_port(node_model.settings) }].mount({}, nmos::make_connection_api(node_model, node_implementation.parse_transport_file, node_implementation.validate_staged, gate)); + node_server.api_routers[{ {}, nmos::fields::connection_port(node_model.settings) }].mount({}, nmos::make_connection_api(node_model, node_implementation.parse_transport_file, node_implementation.validate_staged, validate_authorization ? validate_authorization(nmos::experimental::scopes::connection) : nullptr, gate)); // Configure the Events API - node_server.api_routers[{ {}, nmos::fields::events_port(node_model.settings) }].mount({}, nmos::make_events_api(node_model, gate)); + + node_server.api_routers[{ {}, nmos::fields::events_port(node_model.settings) }].mount({}, nmos::make_events_api(node_model, validate_authorization ? validate_authorization(nmos::experimental::scopes::events) : nullptr, gate)); // Configure the Channel Mapping API - node_server.api_routers[{ {}, nmos::fields::channelmapping_port(node_model.settings) }].mount({}, nmos::make_channelmapping_api(node_model, node_implementation.validate_map, gate)); + node_server.api_routers[{ {}, nmos::fields::channelmapping_port(node_model.settings) }].mount({}, nmos::make_channelmapping_api(node_model, node_implementation.validate_map, validate_authorization ? validate_authorization(nmos::experimental::scopes::channelmapping) : nullptr, gate)); + + const auto& events_ws_port = nmos::fields::events_ws_port(node_model.settings); auto& events_ws_api = node_server.ws_handlers[{ {}, nmos::fields::events_ws_port(node_model.settings) }]; - events_ws_api.first = nmos::make_events_ws_api(node_model, events_ws_api.second, gate); + events_ws_api.first = nmos::make_events_ws_api(node_model, events_ws_api.second, node_implementation.ws_validate_authorization, gate); + + // can't share a port between the events ws and the control protocol ws + const auto& control_protocol_enabled = (0 <= nmos::fields::control_protocol_ws_port(node_model.settings)); + const auto& control_protocol_ws_port = nmos::fields::control_protocol_ws_port(node_model.settings); + if (control_protocol_enabled) + { + if (control_protocol_ws_port == events_ws_port) throw std::runtime_error("Same port used for events and control protocol websockets are not supported"); + auto& control_protocol_ws_api = node_server.ws_handlers[{ {}, control_protocol_ws_port }]; + control_protocol_ws_api.first = nmos::make_control_protocol_ws_api(node_model, control_protocol_ws_api.second, node_implementation.ws_validate_authorization, node_implementation.get_control_protocol_class_descriptor, node_implementation.get_control_protocol_datatype_descriptor, node_implementation.get_control_protocol_method_descriptor, node_implementation.control_protocol_property_changed, gate); + } // Set up the listeners for each HTTP API port - auto http_config = nmos::make_http_listener_config(node_model.settings); + auto http_config = nmos::make_http_listener_config(node_model.settings, node_implementation.load_server_certificates, node_implementation.load_dh_param, node_implementation.get_ocsp_response, gate); for (auto& api_router : node_server.api_routers) { - // default empty string means the wildcard address - const auto& host = !api_router.first.first.empty() ? api_router.first.first : web::http::experimental::listener::host_wildcard; + // if IP address isn't specified for this router, use default server address or wildcard address + const auto& host = !api_router.first.first.empty() ? api_router.first.first : !server_address.empty() ? server_address : web::http::experimental::listener::host_wildcard; // map the configured client port to the server port on which to listen // hmm, this should probably also take account of the address - node_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, nmos::experimental::server_port(api_router.first.second, node_model.settings), api_router.second, http_config, gate)); + node_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, nmos::experimental::server_port(api_router.first.second, node_model.settings), api_router.second, http_config, hsts, gate)); } // Set up the handlers for each WebSocket API port - auto websocket_config = nmos::make_websocket_listener_config(node_model.settings); + auto websocket_config = nmos::make_websocket_listener_config(node_model.settings, node_implementation.load_server_certificates, node_implementation.load_dh_param, node_implementation.get_ocsp_response, gate); websocket_config.set_log_callback(nmos::make_slog_logging_callback(gate)); + size_t event_ws_pos{ 0 }; + bool found_event_ws{ false }; + size_t control_protocol_ws_pos{ 0 }; + bool found_control_protocol_ws{ false }; for (auto& ws_handler : node_server.ws_handlers) { - // default empty string means the wildcard address - const auto& host = !ws_handler.first.first.empty() ? ws_handler.first.first : web::websockets::experimental::listener::host_wildcard; + // if IP address isn't specified for this router, use default server address or wildcard address + const auto& host = !ws_handler.first.first.empty() ? ws_handler.first.first : !server_address.empty() ? server_address : web::websockets::experimental::listener::host_wildcard; // map the configured client port to the server port on which to listen // hmm, this should probably also take account of the address node_server.ws_listeners.push_back(nmos::make_ws_api_listener(server_secure, host, nmos::experimental::server_port(ws_handler.first.second, node_model.settings), ws_handler.second.first, websocket_config, gate)); + + if (!found_event_ws) + { + if (ws_handler.first.second == events_ws_port) { found_event_ws = true; } + else { ++event_ws_pos; } + } + + if (control_protocol_enabled && !found_control_protocol_ws) + { + if (ws_handler.first.second == control_protocol_ws_port) { found_control_protocol_ws = true; } + else { ++control_protocol_ws_pos; } + } } - auto& events_ws_listener = node_server.ws_listeners.back(); + auto& events_ws_listener = node_server.ws_listeners.at(event_ws_pos); // Set up node operation (including the DNS-SD advertisements) + auto load_ca_certificates = node_implementation.load_ca_certificates; auto registration_changed = node_implementation.registration_changed; auto resolve_auto = node_implementation.resolve_auto; auto set_transportfile = node_implementation.set_transportfile; auto connection_activated = node_implementation.connection_activated; auto channelmapping_activated = node_implementation.channelmapping_activated; + auto get_authorization_bearer_token = node_implementation.get_authorization_bearer_token; node_server.thread_functions.assign({ - [&, registration_changed] { nmos::node_behaviour_thread(node_model, registration_changed, gate); }, + [&, load_ca_certificates, registration_changed, get_authorization_bearer_token] { nmos::node_behaviour_thread(node_model, load_ca_certificates, registration_changed, get_authorization_bearer_token, gate); }, [&] { nmos::send_events_ws_messages_thread(events_ws_listener, node_model, events_ws_api.second, gate); }, [&] { nmos::erase_expired_events_resources_thread(node_model, gate); }, [&, resolve_auto, set_transportfile, connection_activated] { nmos::connection_activation_thread(node_model, resolve_auto, set_transportfile, connection_activated, gate); }, @@ -109,22 +153,30 @@ namespace nmos auto system_changed = node_implementation.system_changed; if (system_changed) { - node_server.thread_functions.push_back([&, system_changed] { nmos::node_system_behaviour_thread(node_model, system_changed, gate); }); + node_server.thread_functions.push_back([&, load_ca_certificates, system_changed] { nmos::node_system_behaviour_thread(node_model, load_ca_certificates, system_changed, gate); }); + } + + if (control_protocol_enabled) + { + auto& control_protocol_ws_listener = node_server.ws_listeners.at(control_protocol_ws_pos); + auto& control_protocol_ws_api = node_server.ws_handlers.at({ {}, control_protocol_ws_port }); + node_server.thread_functions.push_back([&] { nmos::send_control_protocol_ws_messages_thread(control_protocol_ws_listener, node_model, control_protocol_ws_api.second, gate); }); } return node_server; } + // deprecated nmos::server make_node_server(nmos::node_model& node_model, nmos::transport_file_parser parse_transport_file, nmos::details::connection_resource_patch_validator validate_merged, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::experimental::log_model& log_model, slog::base_gate& gate) { return make_node_server(node_model, node_implementation().on_parse_transport_file(std::move(parse_transport_file)).on_validate_merged(std::move(validate_merged)).on_resolve_auto(std::move(resolve_auto)).on_set_transportfile(std::move(set_transportfile)).on_connection_activated(std::move(connection_activated)), log_model, gate); } - + // deprecated nmos::server make_node_server(nmos::node_model& node_model, nmos::transport_file_parser parse_transport_file, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::experimental::log_model& log_model, slog::base_gate& gate) { return make_node_server(node_model, node_implementation().on_parse_transport_file(std::move(parse_transport_file)).on_resolve_auto(std::move(resolve_auto)).on_set_transportfile(std::move(set_transportfile)).on_connection_activated(std::move(connection_activated)), log_model, gate); } - + // deprecated nmos::server make_node_server(nmos::node_model& node_model, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::experimental::log_model& log_model, slog::base_gate& gate) { return make_node_server(node_model, node_implementation().on_resolve_auto(std::move(resolve_auto)).on_set_transportfile(std::move(set_transportfile)).on_connection_activated(std::move(connection_activated)), log_model, gate); diff --git a/Development/nmos/node_server.h b/Development/nmos/node_server.h index c0c3a991d..25a15d4b7 100644 --- a/Development/nmos/node_server.h +++ b/Development/nmos/node_server.h @@ -1,12 +1,17 @@ #ifndef NMOS_NODE_SERVER_H #define NMOS_NODE_SERVER_H +#include "nmos/authorization_handlers.h" +#include "nmos/certificate_handlers.h" #include "nmos/channelmapping_api.h" #include "nmos/channelmapping_activation.h" #include "nmos/connection_api.h" #include "nmos/connection_activation.h" +#include "nmos/control_protocol_handlers.h" #include "nmos/node_behaviour.h" #include "nmos/node_system_behaviour.h" +#include "nmos/ocsp_response_handler.h" +#include "nmos/ws_api_utils.h" namespace nmos { @@ -22,14 +27,29 @@ namespace nmos // underlying implementation into the server instance for the NMOS Node struct node_implementation { - node_implementation(nmos::system_global_handler system_changed, nmos::registration_handler registration_changed, nmos::transport_file_parser parse_transport_file, nmos::details::connection_resource_patch_validator validate_staged, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated) - : system_changed(std::move(system_changed)) + node_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::system_global_handler system_changed, nmos::registration_handler registration_changed, nmos::transport_file_parser parse_transport_file, nmos::details::connection_resource_patch_validator validate_staged, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::ocsp_response_handler get_ocsp_response, get_authorization_bearer_token_handler get_authorization_bearer_token, validate_authorization_handler validate_authorization, ws_validate_authorization_handler ws_validate_authorization, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, nmos::get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, nmos::get_control_protocol_method_descriptor_handler get_control_protocol_method_descriptor, nmos::control_protocol_property_changed_handler control_protocol_property_changed) + : load_server_certificates(std::move(load_server_certificates)) + , load_dh_param(std::move(load_dh_param)) + , load_ca_certificates(std::move(load_ca_certificates)) + , system_changed(std::move(system_changed)) , registration_changed(std::move(registration_changed)) , parse_transport_file(std::move(parse_transport_file)) , validate_staged(std::move(validate_staged)) , resolve_auto(std::move(resolve_auto)) , set_transportfile(std::move(set_transportfile)) , connection_activated(std::move(connection_activated)) + , get_ocsp_response(std::move(get_ocsp_response)) + , get_authorization_bearer_token(std::move(get_authorization_bearer_token)) + , validate_authorization(std::move(validate_authorization)) + , ws_validate_authorization(std::move(ws_validate_authorization)) + , load_rsa_private_keys(std::move(load_rsa_private_keys)) + , load_authorization_clients(std::move(load_authorization_clients)) + , save_authorization_client(std::move(save_authorization_client)) + , request_authorization_code(std::move(request_authorization_code)) + , get_control_protocol_class_descriptor(std::move(get_control_protocol_class_descriptor)) + , get_control_protocol_datatype_descriptor(std::move(get_control_protocol_datatype_descriptor)) + , get_control_protocol_method_descriptor(std::move(get_control_protocol_method_descriptor)) + , control_protocol_property_changed(std::move(control_protocol_property_changed)) {} // use the default constructor and chaining member functions for fluent initialization @@ -38,6 +58,9 @@ namespace nmos : parse_transport_file(&nmos::parse_rtp_transport_file) {} + node_implementation& on_load_server_certificates(nmos::load_server_certificates_handler load_server_certificates) { this->load_server_certificates = std::move(load_server_certificates); return *this; } + node_implementation& on_load_dh_param(nmos::load_dh_param_handler load_dh_param) { this->load_dh_param = std::move(load_dh_param); return *this; } + node_implementation& on_load_ca_certificates(nmos::load_ca_certificates_handler load_ca_certificates) { this->load_ca_certificates = std::move(load_ca_certificates); return *this; } node_implementation& on_system_changed(nmos::system_global_handler system_changed) { this->system_changed = std::move(system_changed); return *this; } node_implementation& on_registration_changed(nmos::registration_handler registration_changed) { this->registration_changed = std::move(registration_changed); return *this; } node_implementation& on_parse_transport_file(nmos::transport_file_parser parse_transport_file) { this->parse_transport_file = std::move(parse_transport_file); return *this; } @@ -47,6 +70,18 @@ namespace nmos node_implementation& on_connection_activated(nmos::connection_activation_handler connection_activated) { this->connection_activated = std::move(connection_activated); return *this; } node_implementation& on_validate_channelmapping_output_map(nmos::details::channelmapping_output_map_validator validate_map) { this->validate_map = std::move(validate_map); return *this; } node_implementation& on_channelmapping_activated(nmos::channelmapping_activation_handler channelmapping_activated) { this->channelmapping_activated = std::move(channelmapping_activated); return *this; } + node_implementation& on_get_ocsp_response(nmos::ocsp_response_handler get_ocsp_response) { this->get_ocsp_response = std::move(get_ocsp_response); return *this; } + node_implementation& on_get_authorization_bearer_token(get_authorization_bearer_token_handler get_authorization_bearer_token) { this->get_authorization_bearer_token = std::move(get_authorization_bearer_token); return *this; } + node_implementation& on_validate_authorization(validate_authorization_handler validate_authorization) { this->validate_authorization = std::move(validate_authorization); return *this; } + node_implementation& on_ws_validate_authorization(ws_validate_authorization_handler ws_validate_authorization) { this->ws_validate_authorization = std::move(ws_validate_authorization); return *this; } + node_implementation& on_load_rsa_private_keys(nmos::load_rsa_private_keys_handler load_rsa_private_keys) { this->load_rsa_private_keys = std::move(load_rsa_private_keys); return *this; } + node_implementation& on_load_authorization_clients(load_authorization_clients_handler load_authorization_clients) { this->load_authorization_clients = std::move(load_authorization_clients); return *this; } + node_implementation& on_save_authorization_client(save_authorization_client_handler save_authorization_client) { this->save_authorization_client = std::move(save_authorization_client); return *this; } + node_implementation& on_request_authorization_code(request_authorization_code_handler request_authorization_code) { this->request_authorization_code = std::move(request_authorization_code); return *this; } + node_implementation& on_get_control_class_descriptor(nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor) { this->get_control_protocol_class_descriptor = std::move(get_control_protocol_class_descriptor); return *this; } + node_implementation& on_get_control_datatype_descriptor(nmos::get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor) { this->get_control_protocol_datatype_descriptor = std::move(get_control_protocol_datatype_descriptor); return *this; } + node_implementation& on_get_control_protocol_method_descriptor(nmos::get_control_protocol_method_descriptor_handler get_control_protocol_method_descriptor) { this->get_control_protocol_method_descriptor = std::move(get_control_protocol_method_descriptor); return *this; } + node_implementation& on_control_protocol_property_changed(nmos::control_protocol_property_changed_handler control_protocol_property_changed) { this->control_protocol_property_changed = std::move(control_protocol_property_changed); return *this; } // deprecated, use on_validate_connection_resource_patch node_implementation& on_validate_merged(nmos::details::connection_resource_patch_validator validate_merged) { return on_validate_connection_resource_patch(std::move(validate_merged)); } @@ -57,6 +92,10 @@ namespace nmos return parse_transport_file && resolve_auto && set_transportfile && connection_activated; } + nmos::load_server_certificates_handler load_server_certificates; + nmos::load_dh_param_handler load_dh_param; + nmos::load_ca_certificates_handler load_ca_certificates; + nmos::system_global_handler system_changed; nmos::registration_handler registration_changed; @@ -70,12 +109,28 @@ namespace nmos nmos::details::channelmapping_output_map_validator validate_map; nmos::channelmapping_activation_handler channelmapping_activated; + + nmos::ocsp_response_handler get_ocsp_response; + + get_authorization_bearer_token_handler get_authorization_bearer_token; + validate_authorization_handler validate_authorization; + ws_validate_authorization_handler ws_validate_authorization; + nmos::load_rsa_private_keys_handler load_rsa_private_keys; + load_authorization_clients_handler load_authorization_clients; + save_authorization_client_handler save_authorization_client; + request_authorization_code_handler request_authorization_code; + + nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor; + nmos::get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor; + nmos::get_control_protocol_method_descriptor_handler get_control_protocol_method_descriptor; + nmos::control_protocol_property_changed_handler control_protocol_property_changed; }; // Construct a server instance for an NMOS Node, implementing the IS-04 Node API, IS-05 Connection API, IS-07 Events API // and the experimental Logging API and Settings API, according to the specified data models and callbacks nmos::server make_node_server(nmos::node_model& node_model, nmos::experimental::node_implementation node_implementation, nmos::experimental::log_model& log_model, slog::base_gate& gate); + // deprecated nmos::server make_node_server(nmos::node_model& node_model, nmos::transport_file_parser parse_transport_file, nmos::details::connection_resource_patch_validator validate_merged, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::experimental::log_model& log_model, slog::base_gate& gate); nmos::server make_node_server(nmos::node_model& node_model, nmos::transport_file_parser parse_transport_file, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::experimental::log_model& log_model, slog::base_gate& gate); nmos::server make_node_server(nmos::node_model& node_model, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::experimental::log_model& log_model, slog::base_gate& gate); diff --git a/Development/nmos/node_system_behaviour.cpp b/Development/nmos/node_system_behaviour.cpp index 0dbaecb7b..bc29d0da5 100644 --- a/Development/nmos/node_system_behaviour.cpp +++ b/Development/nmos/node_system_behaviour.cpp @@ -18,9 +18,9 @@ namespace nmos { namespace details { - void node_system_behaviour_thread(nmos::model& model, system_global_handler system_changed, mdns::service_discovery& discovery, slog::base_gate& gate); + void node_system_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, system_global_handler system_changed, mdns::service_discovery& discovery, slog::base_gate& gate); - void node_system_behaviour(nmos::model& model, system_global_handler system_changed, slog::base_gate& gate); + void node_system_behaviour(nmos::model& model, load_ca_certificates_handler load_ca_certificates, system_global_handler system_changed, slog::base_gate& gate); // background service discovery void system_services_background_discovery(nmos::model& model, mdns::service_discovery& discovery, slog::base_gate& gate); @@ -32,25 +32,25 @@ namespace nmos // uses the default DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_system_behaviour_thread(nmos::model& model, system_global_handler system_changed, slog::base_gate& gate_) + void node_system_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, system_global_handler system_changed, slog::base_gate& gate_) { nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::node_system_behaviour)); mdns::service_discovery discovery(gate); - details::node_system_behaviour_thread(model, std::move(system_changed), discovery, gate); + details::node_system_behaviour_thread(model, std::move(load_ca_certificates), std::move(system_changed), discovery, gate); } // uses the specified DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_system_behaviour_thread(nmos::model& model, system_global_handler system_changed, mdns::service_discovery& discovery, slog::base_gate& gate_) + void node_system_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, system_global_handler system_changed, mdns::service_discovery& discovery, slog::base_gate& gate_) { nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::node_system_behaviour)); - details::node_system_behaviour_thread(model, std::move(system_changed), discovery, gate); + details::node_system_behaviour_thread(model, std::move(load_ca_certificates), std::move(system_changed), discovery, gate); } - void details::node_system_behaviour_thread(nmos::model& model, system_global_handler system_changed, mdns::service_discovery& discovery, slog::base_gate& gate) + void details::node_system_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, system_global_handler system_changed, mdns::service_discovery& discovery, slog::base_gate& gate) { enum { @@ -103,7 +103,7 @@ namespace nmos break; case node_system_behaviour: - details::node_system_behaviour(model, system_changed, gate); + details::node_system_behaviour(model, load_ca_certificates, system_changed, gate); // Should no further System APIs be available or TTLs on advertised services expired, a re-query may be performed. mode = rediscovery; @@ -125,40 +125,6 @@ namespace nmos // service discovery namespace details { - // query DNS Service Discovery for any System API in the specified browse domain, having priority in the specified range - // otherwise, after timeout or cancellation, returning the fallback system service - // see https://github.com/AMWA-TV/nmos-system/blob/v1.0/docs/3.0.%20Discovery.md - web::json::value discover_system_services(mdns::service_discovery& discovery, const std::string& browse_domain, const std::set& versions, const std::pair& priorities, const std::set& protocols, const web::uri& fallback_service, slog::base_gate& gate, const std::chrono::steady_clock::duration& timeout, const pplx::cancellation_token& token = pplx::cancellation_token::none()) - { - std::list system_services; - - if (nmos::service_priorities::no_priority != priorities.first) - { - slog::log(gate, SLOG_FLF) << "Attempting discovery of a System API in domain: " << browse_domain; - - system_services = nmos::experimental::resolve_service(discovery, nmos::service_types::system, browse_domain, versions, priorities, protocols, { false }, true, timeout, token).get(); - - if (!system_services.empty()) - { - slog::log(gate, SLOG_FLF) << "Discovered " << system_services.size() << " System API(s)"; - } - else - { - slog::log(gate, SLOG_FLF) << "Did not discover a suitable System API via DNS-SD"; - } - } - - if (system_services.empty()) - { - if (!fallback_service.is_empty()) - { - system_services.push_back(fallback_service); - } - } - - return web::json::value_from_elements(system_services | boost::adaptors::transformed([](const web::uri& u) { return u.to_string(); })); - } - // get the fallback system service from settings (if present) web::uri get_system_service(const nmos::settings& settings) { @@ -175,32 +141,55 @@ namespace nmos // query DNS Service Discovery for any System API based on settings bool discover_system_services(nmos::base_model& model, mdns::service_discovery& discovery, slog::base_gate& gate, const pplx::cancellation_token& token) { - std::string browse_domain; - std::set versions; - std::pair priorities; - std::set protocols; - web::uri fallback_system_service; - int timeout; - with_read_lock(model.mutex, [&] + slog::log(gate, SLOG_FLF) << "Trying System API discovery"; + + // lock to read settings, then unlock to wait for the discovery task to complete + auto system_services = with_read_lock(model.mutex, [&] { auto& settings = model.settings; - browse_domain = utility::us2s(nmos::get_domain(settings)); - versions = nmos::is09_versions::from_settings(settings); - priorities = { nmos::fields::highest_pri(settings), nmos::fields::lowest_pri(settings) }; - protocols = { nmos::get_service_protocol(settings) }; - fallback_system_service = get_system_service(settings); - - // use a short timeout that's long enough to ensure the daemon's cache is exhausted - // when no cancellation token is specified - timeout = token.is_cancelable() ? nmos::fields::discovery_backoff_max(settings) : 1; - }); - slog::log(gate, SLOG_FLF) << "Trying System API discovery for about " << std::fixed << std::setprecision(3) << (double)timeout << " seconds"; - auto services = discover_system_services(discovery, browse_domain, versions, priorities, protocols, fallback_system_service, gate, std::chrono::seconds(timeout), token); - with_write_lock(model.mutex, [&] { model.settings[nmos::fields::system_services] = services; }); - model.notify(); + if (nmos::service_priorities::no_priority != nmos::fields::highest_pri(settings)) + { + slog::log(gate, SLOG_FLF) << "Attempting discovery of a System API in domain: " << nmos::get_domain(settings); + + return nmos::experimental::resolve_service(discovery, nmos::service_types::system, settings, token); + } + else + { + return pplx::task_from_result(std::list{}); + } + }).get(); + + with_write_lock(model.mutex, [&] + { + if (!system_services.empty()) + { + slog::log(gate, SLOG_FLF) << "Discovered " << system_services.size() << " System API(s)"; + } + else + { + slog::log(gate, SLOG_FLF) << "Did not discover a suitable System API via DNS-SD"; + + auto fallback_system_service = get_system_service(model.settings); + if (!fallback_system_service.is_empty()) + { + system_services.push_back(fallback_system_service); + } + } + + if (!system_services.empty()) slog::log(gate, SLOG_FLF) << "Using the System API(s):" << slog::log_manip([&](slog::log_statement& s) + { + for (auto& system_service : system_services) + { + s << '\n' << system_service.to_string(); + } + }); + + model.settings[nmos::fields::system_services] = web::json::value_from_elements(system_services | boost::adaptors::transformed([](const web::uri& u) { return u.to_string(); })); + model.notify(); + }); - return !web::json::empty(services); + return !system_services.empty(); } bool empty_system_services(const nmos::settings& settings) @@ -229,9 +218,9 @@ namespace nmos namespace details { - web::http::client::http_client_config make_system_client_config(const nmos::settings& settings) + web::http::client::http_client_config make_system_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { - auto config = nmos::make_http_client_config(settings); + auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), gate); config.set_timeout(std::chrono::seconds(nmos::fields::system_request_max(settings))); return config; } @@ -368,7 +357,7 @@ namespace nmos }); } - void node_system_behaviour(nmos::model& model, system_global_handler system_changed, slog::base_gate& gate) + void node_system_behaviour(nmos::model& model, load_ca_certificates_handler load_ca_certificates, system_global_handler system_changed, slog::base_gate& gate) { slog::log(gate, SLOG_FLF) << "Attempting System API node behaviour"; @@ -407,7 +396,7 @@ namespace nmos if (!state.client) { const auto base_uri = top_system_service(model.settings); - state.client.reset(new web::http::client::http_client(base_uri, make_system_client_config(model.settings))); + state.client = nmos::details::make_http_client(base_uri, make_system_client_config(model.settings, load_ca_certificates, gate)); } auto token = cancellation_source.get_token(); diff --git a/Development/nmos/node_system_behaviour.h b/Development/nmos/node_system_behaviour.h index 32f5d1b40..9cda7c66b 100644 --- a/Development/nmos/node_system_behaviour.h +++ b/Development/nmos/node_system_behaviour.h @@ -2,6 +2,7 @@ #define NMOS_NODE_SYSTEM_BEHAVIOUR_H #include +#include "nmos/certificate_handlers.h" namespace web { @@ -24,7 +25,7 @@ namespace mdns } // Node behaviour with the System API -// See https://github.com/AMWA-TV/nmos-system/blob/v1.0/README.md +// See https://specs.amwa.tv/is-09/v1.0.0/docs/4.0._Behaviour.html namespace nmos { struct model; @@ -38,11 +39,11 @@ namespace nmos // uses the default DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_system_behaviour_thread(nmos::model& model, system_global_handler system_changed, slog::base_gate& gate); + void node_system_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, system_global_handler system_changed, slog::base_gate& gate); // uses the specified DNS-SD implementation // callbacks from this function are called with the model locked, and may read or write directly to the model - void node_system_behaviour_thread(nmos::model& model, system_global_handler system_changed, mdns::service_discovery& discovery, slog::base_gate& gate); + void node_system_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, system_global_handler system_changed, mdns::service_discovery& discovery, slog::base_gate& gate); } #endif diff --git a/Development/nmos/ocsp_behaviour.cpp b/Development/nmos/ocsp_behaviour.cpp new file mode 100644 index 000000000..a835d5937 --- /dev/null +++ b/Development/nmos/ocsp_behaviour.cpp @@ -0,0 +1,367 @@ +#include "nmos/ocsp_behaviour.h" + +#include "pplx/pplx_utils.h" // for pplx::complete_at +#include "cpprest/uri_schemes.h" +#include "nmos/client_utils.h" +#include "nmos/model.h" +#include "nmos/ocsp_state.h" +#include "nmos/ocsp_utils.h" +#include "nmos/random.h" +#include "nmos/slog.h" +#include "ssl/ssl_utils.h" + +namespace nmos +{ + namespace details + { + struct ocsp_shared_state + { + load_ca_certificates_handler load_ca_certificates; + web::uri base_uri; + bool ocsp_service_error; + std::vector ocsp_request; + + // how many seconds before next certificate status request + double next_request; + + nmos::details::seed_generator seeder; + std::default_random_engine engine; + std::unique_ptr client; + + explicit ocsp_shared_state(load_ca_certificates_handler load_ca_certificates) + : load_ca_certificates(std::move(load_ca_certificates)) + , ocsp_service_error(false) + , next_request((std::numeric_limits::max)()) + , engine(seeder) + {} + }; + + void ocsp_behaviour_thread(nmos::base_model& model, nmos::experimental::ocsp_state& ocsp_state, load_ca_certificates_handler load_ca_certificates, load_server_certificates_handler load_server_certificates, slog::base_gate& gate); + + double certificate_expiry_from_now(const std::vector& certificate_chains, slog::base_gate& gate); + std::vector get_ocsp_uris(const std::vector& certificate_chains, slog::base_gate& gate); + std::vector make_ocsp_request(const std::vector& certificate_chains, slog::base_gate& gate); + void ocsp_behaviour(nmos::base_model& model, nmos::experimental::ocsp_state& ocsp_state, std::vector& ocsp_uris, ocsp_shared_state& state, slog::base_gate& gate); + } + + // callbacks from this function are called with the model locked, and may read the model + void ocsp_behaviour_thread(nmos::base_model& model, nmos::experimental::ocsp_state& ocsp_state, load_ca_certificates_handler load_ca_certificates, load_server_certificates_handler load_server_certificates, slog::base_gate& gate_) + { + nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::ocsp_behaviour)); + + details::ocsp_behaviour_thread(model, ocsp_state, load_ca_certificates, load_server_certificates, gate); + } + + void details::ocsp_behaviour_thread(nmos::base_model& model, nmos::experimental::ocsp_state& ocsp_state, load_ca_certificates_handler load_ca_certificates, load_server_certificates_handler load_server_certificates, slog::base_gate& gate) + { + enum + { + initial_ocsp_operation, + ocsp_behaviour + } mode = initial_ocsp_operation; + + std::vector ocsp_uris; + + nmos::details::seed_generator backoff_seeder; + std::default_random_engine backoff_engine(backoff_seeder); + double backoff = 0; + + ocsp_shared_state state{ load_ca_certificates }; + + // continue until the server is being shut down + for (;;) + { + if (with_read_lock(model.mutex, [&] { return model.shutdown; })) break; + + switch (mode) + { + case initial_ocsp_operation: + { + // note: use the same discovery backoff settings for OCSP server retry + if (0 != backoff) + { + auto lock = model.read_lock(); + const auto random_backoff = std::uniform_real_distribution<>(0, backoff)(backoff_engine); + slog::log(gate, SLOG_FLF) << "Waiting to retry OCSP server for about " << std::fixed << std::setprecision(3) << random_backoff << " seconds (current backoff limit: " << backoff << " seconds)"; + model.wait_for(lock, std::chrono::milliseconds(std::chrono::milliseconds::rep(1000 * random_backoff)), [&] { return model.shutdown; }); + if (model.shutdown) break; + } + + // get the list of server certificates chain + const auto server_certificates = load_server_certificates(); + const auto server_certificate_chains = boost::copy_range>(server_certificates | boost::adaptors::transformed([](const nmos::certificate& certificate) { return certificate.certificate_chain; })); + + try + { + // get OCSP URIs + ocsp_uris = details::get_ocsp_uris(server_certificate_chains, gate); + + mode = ocsp_behaviour; + + // extract the shortest half certificate expiry time from all server certificates + state.next_request = details::certificate_expiry_from_now(server_certificate_chains, gate) * 0.5; + + // construct an OCSP request with the server certificates + state.ocsp_request = details::make_ocsp_request(server_certificate_chains, gate); + + slog::log(gate, SLOG_FLF) << "Using the OCSP server(s):" << slog::log_manip([&](slog::log_statement& s) + { + for (auto& ocsp_uri : ocsp_uris) + { + s << '\n' << ocsp_uri.to_string(); + } + }); + } + catch (const nmos::experimental::ocsp_exception& e) + { + slog::log(gate, SLOG_FLF) << "OCSP error during initial OCSP operation: " << e.what(); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Unexpected exception during initial OCSP operation: " << e.what(); + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Unexpected unknown exception during initial OCSP operation"; + } + + // implement an exponential backoff algorithm to avoid overloading the OCSP server in the event of a system restart + auto lock = model.read_lock(); + backoff = (std::min)((std::max)((double)nmos::fields::discovery_backoff_min(model.settings), backoff * nmos::fields::discovery_backoff_factor(model.settings)), (double)nmos::fields::discovery_backoff_max(model.settings)); + } + break; + + case ocsp_behaviour: + details::ocsp_behaviour(model, ocsp_state, ocsp_uris, state, gate); + + mode = initial_ocsp_operation; + break; + } + } + } + + namespace details + { + web::http::client::http_client_config make_ocsp_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + auto config = nmos::make_http_client_config(secure, settings, std::move(load_ca_certificates), gate); + config.set_timeout(std::chrono::seconds(nmos::experimental::fields::ocsp_request_max(settings))); + return config; + } + + struct ocsp_service_exception {}; + + // make an asynchronous POST request on the OCSP server to get certificate status + // see https://specs.amwa.tv/bcp-003-01/releases/v1.0.1/docs/Secure_Communication.html#certificate-management-client + pplx::task> request_certificate_status(web::http::client::http_client client, std::vector& ocsp_request, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting certificate status"; + + using namespace web::http; + + http_request req(methods::POST); + req.headers().add(header_names::content_type, U("application/ocsp-request")); + req.set_body(ocsp_request); + + return nmos::api_request(client, req, gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (status_codes::OK == response.status_code()) + { + if (response.body()) + { + return response.extract_vector().then([&gate](std::vector body) + { + slog::log(gate, SLOG_FLF) << "Received OCSP response"; + return body; + + }, token); + } + else + { + slog::log(gate, SLOG_FLF) << "OCSP certificate status request error: missing OCSP response"; + throw ocsp_service_exception(); + } + } + else + { + slog::log(gate, SLOG_FLF) << "OCSP certificate status request error: " << response.status_code() << " " << response.reason_phrase(); + throw ocsp_service_exception(); + } + }, token); + } + + // task to continuously fetch the certificate status (OCSP response) on a time interval until failure or cancellation + pplx::task do_certificate_status_requests(nmos::base_model& model, nmos::experimental::ocsp_state& ocsp_state, ocsp_shared_state& state, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + const auto& ocsp_interval_min(nmos::experimental::fields::ocsp_interval_min(model.settings)); + const auto& ocsp_interval_max(nmos::experimental::fields::ocsp_interval_max(model.settings)); + + // start a background task to continously request certificate status on a given interval + return pplx::do_while([=, &model, &ocsp_state, &state, &gate] + { + auto request_interval = std::chrono::seconds(0); + if (state.base_uri == state.client->base_uri()) + { + auto interval = std::uniform_int_distribution<>( + (std::min)((int)state.next_request, ocsp_interval_min), + (std::min)((int)state.next_request, ocsp_interval_max))(state.engine); + request_interval = std::chrono::seconds(interval); + + slog::log(gate, SLOG_FLF) << "Waiting to request certificate status for about " << interval << " seconds"; + } + else + { + state.base_uri = state.client->base_uri(); + } + + auto time_now = std::chrono::steady_clock::now(); + return pplx::complete_at(time_now + request_interval, token).then([=, &state, &gate]() + { + return request_certificate_status(*state.client, state.ocsp_request, gate, token); + }).then([&model, &ocsp_state, &state, &gate](std::vector ocsp_response) + { + // cache the OCSP response + slog::log(gate, SLOG_FLF) << "Cache the OCSP response"; + nmos::with_write_lock(ocsp_state.mutex, [&] { ocsp_state.ocsp_response = ocsp_response; }); + + return true; + }); + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Certificate status request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + } + catch (const ocsp_service_exception&) + { + slog::log(gate, SLOG_FLF) << "Certificate status request error"; + } + + // reaching here, there must be something has gone wrong with the OCSP server + // let's select the next available OCSP server + state.ocsp_service_error = true; + + model.notify(); + }); + } + + double certificate_expiry_from_now(const std::vector& certificate_chains, slog::base_gate& gate) + { + double expiry_time = (std::numeric_limits::max)(); + try + { + // get the shortest expiry time of the server certificates in each certificate chain + for (const auto& certificate_chain : certificate_chains) + { + const auto expiry_time_ = ssl::experimental::certificate_expiry_from_now(utility::us2s(certificate_chain)); + if (expiry_time_ < expiry_time) + { + expiry_time = expiry_time_; + } + } + } + catch (const ssl::experimental::ssl_exception& e) + { + throw nmos::experimental::ocsp_exception("SSL error while getting certificate expiry time: " + std::string(e.what())); + } + return expiry_time; + } + + // construct a list of OCSP URIs from a list of server certificate chains + std::vector get_ocsp_uris(const std::vector& certificate_chains, slog::base_gate& gate) + { + std::vector ocsp_uris; + + for (const auto& certificate_chain : certificate_chains) + { + const auto uris = nmos::experimental::get_ocsp_uris(utility::us2s(certificate_chain)); + + // only add new OCSP URIs to the list + for (const auto& uri : uris) + { + if (ocsp_uris.end() == std::find(ocsp_uris.begin(), ocsp_uris.end(), uri)) + { + ocsp_uris.push_back(uri); + } + } + } + if (ocsp_uris.empty()) + { + throw nmos::experimental::ocsp_exception("missing OCSP URIs from server certificate"); + } + return ocsp_uris; + } + + // construct an OCSP request from the specified list of server certificate chains + std::vector make_ocsp_request(const std::vector& certificate_chains_, slog::base_gate& gate) + { + const auto certificate_chains = boost::copy_range>(certificate_chains_ | boost::adaptors::transformed([](const utility::string_t& certificate_chain) { return utility::us2s(certificate_chain); })); + return nmos::experimental::make_ocsp_request(certificate_chains); + } + + void ocsp_behaviour(nmos::base_model& model, nmos::experimental::ocsp_state& ocsp_state, std::vector& ocsp_uris, ocsp_shared_state& state, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting certificates status requests"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + pplx::cancellation_token_source cancellation_source; + auto requests = pplx::task_from_result(); + + state.base_uri = {}; + + for (;;) + { + // wait for the thread to be interrupted because an error has been encountered with the selected OCSP service + // or because the server is being shut down + // (or this is the first time through) + condition.wait(lock, [&] { return shutdown || state.ocsp_service_error || state.base_uri.is_empty(); }); + if (state.ocsp_service_error) + { + ocsp_uris.erase(ocsp_uris.begin()); + + state.ocsp_service_error = false; + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + requests.wait(); + + state.client.reset(); + cancellation_source = pplx::cancellation_token_source(); + } + if (shutdown || ocsp_uris.empty()) break; + + // selects the first OCSP server from the OCSP URI list + if (!state.client) + { + const auto ocsp_uri = ocsp_uris.front(); + const auto secure = web::is_secure_uri_scheme(ocsp_uri.scheme()); + state.client = nmos::details::make_http_client(ocsp_uri, make_ocsp_client_config(secure, model.settings, state.load_ca_certificates, gate)); + } + + auto token = cancellation_source.get_token(); + + // start a background task to intermittently request certificate status (OCSP response) + requests = do_certificate_status_requests(model, ocsp_state, state, gate, token); + + condition.wait(lock, [&] { return shutdown || state.ocsp_service_error; }); + } + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + requests.wait(); + } + } +} diff --git a/Development/nmos/ocsp_behaviour.h b/Development/nmos/ocsp_behaviour.h new file mode 100644 index 000000000..fb131d5bf --- /dev/null +++ b/Development/nmos/ocsp_behaviour.h @@ -0,0 +1,25 @@ +#ifndef NMOS_OCSP_BEHAVIOUR_H +#define NMOS_OCSP_BEHAVIOUR_H + +#include +#include "nmos/certificate_handlers.h" + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + struct base_model; + + namespace experimental + { + struct ocsp_state; + } + + // callbacks from this function are called with the model locked, and may read the model + void ocsp_behaviour_thread(nmos::base_model& model, nmos::experimental::ocsp_state& ocsp_state, load_ca_certificates_handler load_ca_certificates, load_server_certificates_handler load_server_certificates, slog::base_gate& gate); +} + +#endif diff --git a/Development/nmos/ocsp_response_handler.cpp b/Development/nmos/ocsp_response_handler.cpp new file mode 100644 index 000000000..2c6bf558b --- /dev/null +++ b/Development/nmos/ocsp_response_handler.cpp @@ -0,0 +1,19 @@ +#include "nmos/ocsp_response_handler.h" + +#include "cpprest/basic_utils.h" +#include "nmos/ocsp_state.h" +#include "nmos/slog.h" + +namespace nmos +{ + ocsp_response_handler make_ocsp_response_handler(nmos::experimental::ocsp_state& ocsp_state, slog::base_gate& gate) + { + return [&]() + { + slog::log(gate, SLOG_FLF) << "Retrieve OCSP response from cache"; + + auto lock = ocsp_state.read_lock(); + return ocsp_state.ocsp_response; + }; + } +} diff --git a/Development/nmos/ocsp_response_handler.h b/Development/nmos/ocsp_response_handler.h new file mode 100644 index 000000000..13dbc74e4 --- /dev/null +++ b/Development/nmos/ocsp_response_handler.h @@ -0,0 +1,29 @@ +#ifndef NMOS_OCSP_RESPONSE_HANDLER_H +#define NMOS_OCSP_RESPONSE_HANDLER_H + +#include +#include +#include + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + namespace experimental + { + struct ocsp_state; + } + + // callback to return OCSP response in DER format + // this callback is executed for each new TLS connection + // this callback should not throw exceptions + typedef std::function()> ocsp_response_handler; + + // construct callback to retrieve OCSP response + ocsp_response_handler make_ocsp_response_handler(nmos::experimental::ocsp_state& ocsp_state, slog::base_gate& gate); +} + +#endif diff --git a/Development/nmos/ocsp_state.h b/Development/nmos/ocsp_state.h new file mode 100644 index 000000000..95a0c132b --- /dev/null +++ b/Development/nmos/ocsp_state.h @@ -0,0 +1,24 @@ +#ifndef NMOS_OCSP_STATE_H +#define NMOS_OCSP_STATE_H + +#include +#include "nmos/mutex.h" + +namespace nmos +{ + namespace experimental + { + struct ocsp_state + { + // mutex to be used to protect the members from simultaneous access by multiple threads + mutable nmos::mutex mutex; + + std::vector ocsp_response; + + nmos::read_lock read_lock() const { return nmos::read_lock{ mutex }; } + nmos::write_lock write_lock() const { return nmos::write_lock{ mutex }; } + }; + } +} + +#endif diff --git a/Development/nmos/ocsp_utils.cpp b/Development/nmos/ocsp_utils.cpp new file mode 100644 index 000000000..d46f62eac --- /dev/null +++ b/Development/nmos/ocsp_utils.cpp @@ -0,0 +1,220 @@ +#include "nmos/ocsp_utils.h" + +#include +#include +#include +#include "cpprest/basic_utils.h" +#include "nmos/ocsp_state.h" +#include "ssl/ssl_utils.h" + +namespace nmos +{ + namespace experimental + { + typedef std::unique_ptr OCSP_REQUEST_ptr; + typedef std::unique_ptr OCSP_RESPONSE_ptr; + + namespace details + { + typedef std::map> issuer_certificate_vs_server_certificates_map; + + // construct OCSP request using an issuer certificate to server certificates lookup map + // see https://stackoverflow.com/questions/56253312/how-to-create-ocsp-request-using-openssl-in-c + std::vector make_ocsp_request(const issuer_certificate_vs_server_certificates_map& issuer_certificate_vs_server_certificates) + { + using ssl::experimental::BIO_ptr; + using ssl::experimental::X509_ptr; + + // set up OCSP request to load certificates + OCSP_REQUEST_ptr ocsp_request(OCSP_REQUEST_new(), &OCSP_REQUEST_free); + if (!ocsp_request) + { + throw ocsp_exception("failed to make_ocsp_request while setting up OCSP request: OCSP_REQUEST_new failure: " + ssl::experimental::last_openssl_error()); + } + + for (const auto& issuer_certificate_vs_server_certificates_item : issuer_certificate_vs_server_certificates) + { + // load issuer certificate + BIO_ptr issuer_bio(BIO_new(BIO_s_mem()), &BIO_free); + if (!issuer_bio) + { + throw ocsp_exception("failed to make_ocsp_request while creating BIO memory: BIO_new failure: " + ssl::experimental::last_openssl_error()); + } + const auto& issuer_certificate = issuer_certificate_vs_server_certificates_item.first; + if ((size_t)BIO_write(issuer_bio.get(), issuer_certificate.data(), (int)issuer_certificate.size()) != issuer_certificate.size()) + { + throw ocsp_exception("failed to make_ocsp_request while loading issuer certificate: BIO_write failure: " + ssl::experimental::last_openssl_error()); + } + X509_ptr issuer_x509(PEM_read_bio_X509_AUX(issuer_bio.get(), NULL, NULL, NULL), &X509_free); + if (!issuer_x509) + { + throw ocsp_exception("failed to make_ocsp_request while loading issuer certificate: PEM_read_bio_X509_AUX failure: " + ssl::experimental::last_openssl_error()); + } + + const auto& server_certificates = issuer_certificate_vs_server_certificates_item.second; + // load server certificate then add to OCSP request + for (const auto& server_certificate : server_certificates) + { + // load server certificate + BIO_ptr server_bio(BIO_new(BIO_s_mem()), &BIO_free); + if (!server_bio) + { + throw ocsp_exception("failed to make_ocsp_request while creating BIO memory: BIO_new failure: " + ssl::experimental::last_openssl_error()); + } + if ((size_t)BIO_write(server_bio.get(), server_certificate.data(), (int)server_certificate.size()) != server_certificate.size()) + { + throw ocsp_exception("failed to make_ocsp_request while loading server certificate: BIO_write failure: " + ssl::experimental::last_openssl_error()); + } + X509_ptr server_x509(PEM_read_bio_X509_AUX(server_bio.get(), NULL, NULL, NULL), &X509_free); + if (!server_x509) + { + throw ocsp_exception("failed to make_ocsp_request while loading server certificate: PEM_read_bio_X509_AUX failure: " + ssl::experimental::last_openssl_error()); + } + + auto const cert_id_md = EVP_sha1(); + + // creates and returns a new OCSP_CERTID structure using SHA1 message digest for the server certificate with the issuer + auto id = OCSP_cert_to_id(cert_id_md, server_x509.get(), issuer_x509.get()); + if (!id) + { + throw ocsp_exception("failed to make_ocsp_request while creating new OCSP_CERTID: OCSP_cert_to_id failure: " + ssl::experimental::last_openssl_error()); + } + if (!OCSP_request_add0_id(ocsp_request.get(), id)) + { + throw ocsp_exception("failed to make_ocsp_request while adding certificate to OCSP request: OCSP_request_add0_id failure: " + ssl::experimental::last_openssl_error()); + } + } + } + + // write the OCSP request out in DER format + BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if (!bio) + { + throw ocsp_exception("failed to make_ocsp_request while creating BIO to load OCSP request: BIO_new failure: " + ssl::experimental::last_openssl_error()); + } + if (ASN1_i2d_bio(reinterpret_cast(i2d_OCSP_REQUEST), bio.get(), reinterpret_cast(ocsp_request.get())) == 0) + { + throw ocsp_exception("failed to make_ocsp_request while converting OCSP REQUEST in DER format: ASN1_i2d_bio failed: " + ssl::experimental::last_openssl_error()); + } + + BUF_MEM* buf; + BIO_get_mem_ptr(bio.get(), &buf); + std::vector ocsp_request_der(size_t(buf->length), 0); + if ((size_t)BIO_read(bio.get(), (void*)ocsp_request_der.data(), (int)ocsp_request_der.size()) != ocsp_request_der.size()) + { + throw ocsp_exception("failed to make_ocsp_request while converting OCSP REQUEST to byte array: BIO_read failed: " + ssl::experimental::last_openssl_error()); + } + + return ocsp_request_der; + } + +#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) + // this callback is called when client includes a certificate status request extension in the TLS handshake + int server_certificate_status_request(SSL* ssl, void* arg) + { + auto ocsp_response = *(std::vector*)arg; + return nmos::experimental::set_ocsp_response(ssl, ocsp_response) ? SSL_TLSEXT_ERR_OK : SSL_TLSEXT_ERR_NOACK; + } +#endif + } + + // get a list of OCSP URIs from server certificate + std::vector get_ocsp_uris(const std::string& certificate) + { + using ssl::experimental::BIO_ptr; + using ssl::experimental::X509_ptr; + + BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if (!bio) + { + throw ocsp_exception("failed to get_ocsp_uris while creating BIO memory: BIO_new failure: " + ssl::experimental::last_openssl_error()); + } + if ((size_t)BIO_write(bio.get(), certificate.data(), (int)certificate.size()) != certificate.size()) + { + throw ocsp_exception("failed to get_ocsp_uris while loading server certificate to BIO: BIO_new failure: " + ssl::experimental::last_openssl_error()); + } + X509_ptr x509(PEM_read_bio_X509_AUX(bio.get(), NULL, NULL, NULL), &X509_free); + if (!x509) + { + throw ocsp_exception("failed to get_ocsp_uris while converting server certificate BIO to X509: PEM_read_bio_X509_AUX failure: " + ssl::experimental::last_openssl_error()); + } + auto ocsp_uris_ = X509_get1_ocsp(x509.get()); + + std::vector ocsp_uris; + for (int idx = 0; idx < sk_OPENSSL_STRING_num(ocsp_uris_); idx++) + { + web::uri ocsp_uri(utility::s2us(sk_OPENSSL_STRING_value(ocsp_uris_, idx))); + if (ocsp_uris.end() == std::find_if(ocsp_uris.begin(), ocsp_uris.end(), [&](const web::uri& uri) { return uri == ocsp_uri; })) + { + ocsp_uris.push_back(ocsp_uri); + } + } + return ocsp_uris; + } + + // construct an OCSP request from the specified list of server certificate chains + std::vector make_ocsp_request(const std::vector& certificate_chains) + { + if (certificate_chains.empty()) + { + throw ocsp_exception("failed to make_ocsp_request: no server certificate chains"); + } + + details::issuer_certificate_vs_server_certificates_map issuer_certificate_vs_server_certificates; + for (const auto& certificate_chain : certificate_chains) + { + // a minimal server certificate chain starts with the server's certificate, followed by the server's issuer certificate. + + // split the server certificate chain to a list of individual certificates + const auto certificates = ssl::experimental::split_certificate_chain(certificate_chain); + if (2 > certificates.size()) + { + throw ocsp_exception("failed to make_ocsp_request: not all the required certificates found in the server certificate chain"); + } + // get the issuer certificate from the certificate chain, this should be the 2nd certificate in the chain + const auto issuer_certificate = certificates[1]; + // get the server certificate from the certificate chain, this should be the 1st certificate in the chain + const auto server_certificate = certificates[0]; + + // construct the issuer certificate to server certificates lookup map + const auto found = issuer_certificate_vs_server_certificates.find(issuer_certificate); + if (issuer_certificate_vs_server_certificates.end() == found) + { + issuer_certificate_vs_server_certificates[issuer_certificate] = { server_certificate }; + } + else + { + issuer_certificate_vs_server_certificates[issuer_certificate].push_back(server_certificate); + } + } + + return details::make_ocsp_request(issuer_certificate_vs_server_certificates); + } + + // set up OCSP response for the OCSP stapling in the TLS handshake + bool set_ocsp_response(SSL* ssl, const std::vector& ocsp_response) + { + if (ocsp_response.empty()) return false; + +#if (OPENSSL_VERSION_NUMBER < 0x1010100fL) + auto buffer = OPENSSL_malloc(ocsp_response.size()); +#else + auto buffer = OPENSSL_memdup(ocsp_response.data(), ocsp_response.size()); +#endif + if (0 == buffer) return false; +#if (OPENSSL_VERSION_NUMBER < 0x1010100fL) + std::memcpy(buffer, ocsp_response.data(), ocsp_response.size()); +#endif + return 0 != SSL_set_tlsext_status_ocsp_resp(ssl, buffer, (int)ocsp_response.size()); + } + +#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) + // set up server certificate status callback when client includes a certificate status request extension in the TLS handshake + void set_server_certificate_status_handler(boost::asio::ssl::context& ctx, const std::vector& ocsp_response) + { + SSL_CTX_set_tlsext_status_cb(ctx.native_handle(), details::server_certificate_status_request); + SSL_CTX_set_tlsext_status_arg(ctx.native_handle(), (void*)(&ocsp_response)); + } +#endif + } +} diff --git a/Development/nmos/ocsp_utils.h b/Development/nmos/ocsp_utils.h new file mode 100644 index 000000000..ffcb3713a --- /dev/null +++ b/Development/nmos/ocsp_utils.h @@ -0,0 +1,37 @@ +#ifndef NMOS_OCSP_UTILS_H +#define NMOS_OCSP_UTILS_H + +#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) +#include +#endif +#include +#include +#include +#include "cpprest/uri.h" + +namespace nmos +{ + namespace experimental + { + struct ocsp_exception : std::runtime_error + { + ocsp_exception(const std::string& message) : std::runtime_error(message) {} + }; + + // get a list of OCSP URIs from server certificate + std::vector get_ocsp_uris(const std::string& certificate); + + // construct an OCSP request from the specified list of server certificate chains + std::vector make_ocsp_request(const std::vector& certificate_chains); + + // set up OCSP response for the OCSP stapling in the TLS handshake + bool set_ocsp_response(SSL* ssl, const std::vector& ocsp_resp); + +#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) + // set up server certificate status callback when client includes a certificate status request extension in the TLS handshake + void set_server_certificate_status_handler(boost::asio::ssl::context& ctx, const std::vector& ocsp_response); +#endif + } +} + +#endif diff --git a/Development/nmos/process_utils.cpp b/Development/nmos/process_utils.cpp index 0f13f0bde..bda7b2f9d 100644 --- a/Development/nmos/process_utils.cpp +++ b/Development/nmos/process_utils.cpp @@ -39,7 +39,7 @@ namespace nmos #endif SIGTERM); signals.async_wait([](const boost::system::error_code&, int){}); - service.run_one(); + service.run(); } } } diff --git a/Development/nmos/query_api.cpp b/Development/nmos/query_api.cpp index 2e745e718..750ef3e8c 100644 --- a/Development/nmos/query_api.cpp +++ b/Development/nmos/query_api.cpp @@ -17,7 +17,7 @@ namespace nmos { inline web::http::experimental::listener::api_router make_unmounted_query_api(nmos::registry_model& model, slog::base_gate& gate); - web::http::experimental::listener::api_router make_query_api(nmos::registry_model& model, slog::base_gate& gate) + web::http::experimental::listener::api_router make_query_api(nmos::registry_model& model, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -35,6 +35,12 @@ namespace nmos return pplx::task_from_result(true); }); + if (validate_authorization) + { + query_api.support(U("/x-nmos/") + nmos::patterns::query_api.pattern + U("/?"), validate_authorization); + query_api.support(U("/x-nmos/") + nmos::patterns::query_api.pattern + U("/.*"), validate_authorization); + } + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is04_versions::from_settings(model.settings); }); query_api.support(U("/x-nmos/") + nmos::patterns::query_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { @@ -124,7 +130,7 @@ namespace nmos // RFC 5988 allows relative URLs, but NMOS specification examples are all absolute URLs // See https://tools.ietf.org/html/rfc5988#section-5 - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.5.%20APIs%20-%20Query%20Parameters.md + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.5._APIs_-_Query_Parameters.html // get the request host and port (or use the primary host address, and port, from settings) auto req_host_port = web::http::get_host_port(req); @@ -274,7 +280,7 @@ namespace nmos href_type = fields.end() != field ? field->second : nmos::type{}; } - else if (href_type.name.empty()) + else if (href_type.empty()) { assign(std::move(value)); } @@ -515,7 +521,7 @@ namespace nmos // "Query API clients MAY choose to include [the 'secure'] attribute in requests for subscriptions, // however they will receive a 400 response code unless the Query API explicitly supports a mismatch // between encrypted HTTP and WebSocket connections." - // see https://github.com/AMWA-TV/nmos-discovery-registration/pull/84/commits/2d842770c1fc6c38b46fe6735c0b87bc42c48666 + // see https://github.com/AMWA-TV/is-04/pull/84/commits/2d842770c1fc6c38b46fe6735c0b87bc42c48666 const bool valid_secure_field = api_secure == nmos::fields::secure(data); valid = valid && valid_secure_field; } @@ -525,7 +531,7 @@ namespace nmos slog::log(gate, SLOG_FLF) << "Subscription requested on " << nmos::fields::resource_path(data) << ", to be " << (nmos::fields::persist(data) ? "persistent" : "non-persistent"); // if the query parameters are not supported, an exception from either of these constructors will result in an appropriate response, e.g. a 501 HTTP status code - // see https://github.com/AMWA-TV/nmos-discovery-registration/pull/99 + // see https://github.com/AMWA-TV/is-04/pull/99 const resource_query match(version, nmos::fields::resource_path(data), nmos::fields::params(data)); const resource_paging paging(nmos::fields::params(data)); @@ -574,6 +580,7 @@ namespace nmos // never expire persistent subscriptions, they are only deleted when explicitly requested nmos::resource subscription{ version, nmos::types::subscription, data, nmos::fields::persist(data) }; + slog::log(gate, SLOG_FLF) << "Creating subscription: " << id; resource = insert_resource(resources, std::move(subscription)).first; } else @@ -591,6 +598,8 @@ namespace nmos { resource->health = health_now(); } + + slog::log(gate, SLOG_FLF) << "Returning subscription: " << resource->id; } set_reply(res, creating ? status_codes::Created : status_codes::OK, data); diff --git a/Development/nmos/query_api.h b/Development/nmos/query_api.h index 93d29307e..ec1200368 100644 --- a/Development/nmos/query_api.h +++ b/Development/nmos/query_api.h @@ -10,12 +10,17 @@ namespace slog } // Query API implementation -// See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/QueryAPI.raml +// See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/QueryAPI.html namespace nmos { struct registry_model; - web::http::experimental::listener::api_router make_query_api(nmos::registry_model& model, slog::base_gate& gate); + web::http::experimental::listener::api_router make_query_api(nmos::registry_model& model, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); + + inline web::http::experimental::listener::api_router make_query_api(nmos::registry_model& model, slog::base_gate& gate) + { + return make_query_api(model, {}, gate); + } struct resource_paging; diff --git a/Development/nmos/query_utils.cpp b/Development/nmos/query_utils.cpp index 5491e345a..62977ff09 100644 --- a/Development/nmos/query_utils.cpp +++ b/Development/nmos/query_utils.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include "cpprest/basic_utils.h" #include "nmos/api_downgrade.h" #include "nmos/api_utils.h" // for nmos::resourceType_from_type @@ -29,6 +30,8 @@ namespace nmos } } + rql::operators make_rql_operators(const nmos::resources& resources); + resource_query::resource_query(const nmos::api_version& version, const utility::string_t& resource_path, const web::json::value& flat_query_params) : version(version) , resource_path(resource_path) @@ -54,10 +57,12 @@ namespace nmos else if (field.first == U("rql")) { rql_query = rql::parse_query(field.second.as_string()); + // validate against call-operators used in nmos::match_rql + rql::validate_query(rql_query, make_rql_operators({})); } // extract the experimental flag, used to override the default behaviour that resources // "must have all [higher-versioned] keys stripped by the Query API before they are returned" - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.5.%20APIs%20-%20Query%20Parameters.md#downgrade-queries + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.5._APIs_-_Query_Parameters.html#downgrade-queries else if (field.first == U("strip")) { strip = field.second.as_bool(); @@ -69,7 +74,7 @@ namespace nmos } // taking query.ancestry_id as an example, an error should be reported for unimplemented parameters // "A 501 HTTP status code should be returned where an ancestry query is attempted against a Query API which does not implement it." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.5.%20APIs%20-%20Query%20Parameters.md#ancestry-queries-optional + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.5._APIs_-_Query_Parameters.html#ancestry-queries-optional else { throw std::runtime_error("unimplemented parameter - query." + utility::us2s(field.first)); @@ -97,7 +102,7 @@ namespace nmos if (field.first == U("order")) { // paging.order is "create" or "update" - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/QueryAPI.raml#L40 + // See https://specs.amwa.tv/is-04/releases/v1.3.1/APIs/QueryAPI.html#nodes_get etc. order_by_created = U("create") == field.second.as_string(); } else if (field.first == U("until")) @@ -238,11 +243,23 @@ namespace nmos : input; } - static inline rql::extractor make_extractor(const web::json::value& value) + static inline rql::extractor make_rql_extractor(const web::json::value& value) { - return [&value](web::json::value& results, const web::json::value& key) + return [&value](web::json::value& results, const web::json::value& key_path_) { - return web::json::extract(value.as_object(), results, key.as_string()); + if (key_path_.is_array()) + { + auto key_path = boost::copy_range>(key_path_.as_array() | boost::adaptors::transformed([](const web::json::value& key) + { + return key.as_string(); + })); + + return web::json::extract(value.as_object(), results, key_path); + } + else + { + return web::json::extract(value.as_object(), results, key_path_.as_string()); + } }; } @@ -264,7 +281,7 @@ namespace nmos const auto rel = [&resolve, &relation_name, &operators, &query](const web::json::value& relation_value) { // evaluate the call-operator against the specified data - return rql::evaluator{ make_extractor(resolve(relation_name, relation_value)), operators }(query); + return rql::evaluator{ make_rql_extractor(resolve(relation_name, relation_value)), operators }(query); }; // cf. rql::details::logical_or @@ -331,14 +348,26 @@ namespace nmos } } - bool match_rql(const web::json::value& value, const web::json::value& query, const nmos::resources& resources) + rql::operators make_rql_operators(const nmos::resources& resources) { auto operators = rql::default_any_operators(equal_to, less); operators[U("rel")] = std::bind(experimental::rel, std::cref(resources), std::placeholders::_1, std::placeholders::_2); operators[U("sub")] = experimental::sub; - return query.is_null() || rql::evaluator{ make_extractor(value), operators }(query) == rql::value_true; + return operators; + } + + bool match_rql(const web::json::value& value, const web::json::value& query, const nmos::resources& resources) + { + try + { + return query.is_null() || rql::evaluator{ make_rql_extractor(value), make_rql_operators(resources) }(query) == rql::value_true; + } + catch (const std::runtime_error&) // i.e. rql::details::rql_exception + { + return false; + } } resource_query::result_type resource_query::operator()(const nmos::api_version& resource_version, const nmos::api_version& resource_downgrade_version, const nmos::type& resource_type, const web::json::value& resource_data, const nmos::resources& resources) const @@ -476,7 +505,7 @@ namespace nmos // also omitted unless resource_path is empty (since that's also an extension); // ironically, the latter is a schema violation, but the former wouldn't be because the schema // does not have "additionalProperties": false - // see nmos-discovery-registration/APIs/schemas/queryapi-subscriptions-websocket.json + // see https://specs.amwa.tv/is-04/releases/v1.1.0/APIs/schemas/with-refs/queryapi-subscriptions-websocket.html if (resource_path.empty()) { if (!match.strip || resource.version < match.version) @@ -548,4 +577,49 @@ namespace nmos } } } + + // insert 'value changed', 'sequence item added', 'sequence item changed' or 'sequence item removed' notification events into all grains whose subscriptions match the specified version, type and "pre" or "post" values + // this is used for the IS-12 propertry changed event + void insert_notification_events(nmos::resources& resources, const nmos::api_version& version, const nmos::api_version& downgrade_version, const nmos::type& type, const web::json::value& pre, const web::json::value& post, const web::json::value& event) + { + using web::json::value; + + if (pre == post) return; + + if (!details::is_queryable_resource(type)) return; + + auto& by_type = resources.get(); + const auto subscriptions = by_type.equal_range(details::has_data(nmos::types::subscription)); + + for (auto it = subscriptions.first; subscriptions.second != it; ++it) + { + // for each subscription + const auto& subscription = *it; + + // check whether the resource_path matches the resource type and the query parameters match either the "pre" or "post" resource + + const auto resource_path = nmos::fields::resource_path(subscription.data); + const resource_query match(subscription.version, resource_path, nmos::fields::params(subscription.data)); + + const bool pre_match = match(version, downgrade_version, type, pre, resources); + const bool post_match = match(version, downgrade_version, type, post, resources); + + if (!pre_match && !post_match) continue; + + // add the event to the grain for each websocket connection to this subscription + + for (const auto& id : subscription.sub_resources) + { + auto grain = find_resource(resources, { id, nmos::types::grain }); + if (resources.end() == grain) continue; // check websocket connection is still open + + resources.modify(grain, [&resources, &event](nmos::resource& grain) + { + auto& events = nmos::fields::message_grain_data(grain.data); + web::json::push_back(events, event); + grain.updated = strictly_increasing_update(resources); + }); + } + } + } } diff --git a/Development/nmos/query_utils.h b/Development/nmos/query_utils.h index c32e9d357..fcfbe9c0b 100644 --- a/Development/nmos/query_utils.h +++ b/Development/nmos/query_utils.h @@ -105,7 +105,7 @@ namespace nmos inline nmos::resources::index::type::const_iterator lower_bound(const nmos::resources::index::type& index, const nmos::tai& timestamp) { return index.lower_bound(timestamp); } // Helpers for constructing /subscriptions websocket grains - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.2.%20Behaviour%20-%20Querying.md + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.2._Behaviour_-_Querying.html // make the initial 'sync' resource events for a new grain, including all resources that match the specified version, resource path and flat query parameters // optionally, make 'added' resource events instead of 'sync' events @@ -114,6 +114,9 @@ namespace nmos // insert 'added', 'removed' or 'modified' resource events into all grains whose subscriptions match the specified version, type and "pre" or "post" values void insert_resource_events(nmos::resources& resources, const nmos::api_version& version, const nmos::api_version& downgrade_version, const nmos::type& type, const web::json::value& pre, const web::json::value& post); + // insert 'value changed', 'sequence item added', 'sequence item changed' or 'sequence item removed' notification events into all grains whose subscriptions match the specified version, type and "pre" or "post" values + void insert_notification_events(nmos::resources& resources, const nmos::api_version& version, const nmos::api_version& downgrade_version, const nmos::type& type, const web::json::value& pre, const web::json::value& post, const web::json::value& event); + namespace fields { const web::json::field_as_string_or query_rql{ U("query.rql"), {} }; diff --git a/Development/nmos/query_ws_api.cpp b/Development/nmos/query_ws_api.cpp index b24b6a6ef..32e3a62fe 100644 --- a/Development/nmos/query_ws_api.cpp +++ b/Development/nmos/query_ws_api.cpp @@ -5,17 +5,18 @@ #include "nmos/query_utils.h" #include "nmos/rational.h" #include "nmos/thread_utils.h" // for wait_until +#include "nmos/scope.h" #include "nmos/slog.h" #include "nmos/version.h" namespace nmos { - web::websockets::experimental::listener::validate_handler make_query_ws_validate_handler(nmos::registry_model& model, slog::base_gate& gate_) + web::websockets::experimental::listener::validate_handler make_query_ws_validate_handler(nmos::registry_model& model, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate_) { - return [&model, &gate_](web::http::http_request req) + return [&model, ws_validate_authorization, &gate_](web::http::http_request req) { nmos::ws_api_gate gate(gate_, req.request_uri()); - auto lock = model.read_lock(); + auto lock = model.write_lock(); auto& resources = model.registry_resources; // RFC 6750 defines two methods of sending bearer access tokens which are applicable to WebSocket @@ -23,6 +24,11 @@ namespace nmos // Clients MAY use a "URI Query Parameter". // See https://tools.ietf.org/html/rfc6750#section-2 + if (ws_validate_authorization) + { + if (!ws_validate_authorization(req, nmos::experimental::scopes::query)) { return false; } + } + // For now, to determine whether the "resource name" is valid, only look at the path, and ignore any query parameters const auto& ws_resource_path = req.request_uri().path(); slog::log(gate, SLOG_FLF) << "Validating websocket connection to: " << ws_resource_path; @@ -231,7 +237,7 @@ namespace nmos // determine the grain timestamps // the meanings of each of these are being clarified in IS-04 v1.3 - // see https://github.com/AMWA-TV/nmos-discovery-registration/pull/102 + // see https://github.com/AMWA-TV/is-04/pull/102 // origin_timestamp is like paging.until, the timestamp of the most recent update potentially included in this message // it has therefore been subject to the usual adjustments to make it unique and strictly increasing diff --git a/Development/nmos/query_ws_api.h b/Development/nmos/query_ws_api.h index 60632ca0c..bafc96512 100644 --- a/Development/nmos/query_ws_api.h +++ b/Development/nmos/query_ws_api.h @@ -1,7 +1,9 @@ #ifndef NMOS_QUERY_WS_API_H #define NMOS_QUERY_WS_API_H +#include "nmos/authorization_handlers.h" #include "nmos/websockets.h" +#include "nmos/ws_api_utils.h" namespace slog { @@ -9,22 +11,22 @@ namespace slog } // Query API websocket implementation -// See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.2.%20Behaviour%20-%20Querying.md +// See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.2._Behaviour_-_Querying.html namespace nmos { struct registry_model; - web::websockets::experimental::listener::validate_handler make_query_ws_validate_handler(nmos::registry_model& model, slog::base_gate& gate); + web::websockets::experimental::listener::validate_handler make_query_ws_validate_handler(nmos::registry_model& model, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate); // note, model mutex is assumed to also protect websockets web::websockets::experimental::listener::open_handler make_query_ws_open_handler(const nmos::id& source_id, nmos::registry_model& model, nmos::websockets& websockets, slog::base_gate& gate); // note, model mutex is assumed to also protect websockets web::websockets::experimental::listener::close_handler make_query_ws_close_handler(nmos::registry_model& model, nmos::websockets& websockets, slog::base_gate& gate); // note, model mutex is assumed to also protect websockets - inline web::websockets::experimental::listener::websocket_listener_handlers make_query_ws_api(const nmos::id& source_id, nmos::registry_model& model, nmos::websockets& websockets, slog::base_gate& gate) + inline web::websockets::experimental::listener::websocket_listener_handlers make_query_ws_api(const nmos::id& source_id, nmos::registry_model& model, nmos::websockets& websockets, nmos::experimental::ws_validate_authorization_handler ws_validate_authorization, slog::base_gate& gate) { return{ - nmos::make_query_ws_validate_handler(model, gate), + nmos::make_query_ws_validate_handler(model, ws_validate_authorization, gate), nmos::make_query_ws_open_handler(source_id, model, websockets, gate), nmos::make_query_ws_close_handler(model, websockets, gate), {} diff --git a/Development/nmos/rational.cpp b/Development/nmos/rational.cpp index 9423518ac..321070106 100644 --- a/Development/nmos/rational.cpp +++ b/Development/nmos/rational.cpp @@ -11,6 +11,12 @@ namespace nmos }); } + bool is_rational(const web::json::value& value) + { + // denominator has a default of 1 so may be omitted + return value.has_integer_field(nmos::fields::numerator); + } + rational parse_rational(const web::json::value& value) { return{ diff --git a/Development/nmos/rational.h b/Development/nmos/rational.h index fc3c2e683..9e1b4015d 100644 --- a/Development/nmos/rational.h +++ b/Development/nmos/rational.h @@ -31,6 +31,8 @@ namespace nmos return make_rational({ numerator, denominator }); } + bool is_rational(const web::json::value& value); + rational parse_rational(const web::json::value& value); } diff --git a/Development/nmos/registration_api.cpp b/Development/nmos/registration_api.cpp index cf3d4708e..40bd3c50c 100644 --- a/Development/nmos/registration_api.cpp +++ b/Development/nmos/registration_api.cpp @@ -2,8 +2,10 @@ #include #include "cpprest/json_validator.h" +#include "cpprest/resource_server_error.h" #include "nmos/api_downgrade.h" // for details::make_permitted_downgrade_error #include "nmos/api_utils.h" +#include "nmos/authorization.h" #include "nmos/is04_versions.h" #include "nmos/json_schema.h" #include "nmos/log_manip.h" @@ -68,7 +70,7 @@ namespace nmos inline web::http::experimental::listener::api_router make_unmounted_registration_api(nmos::registry_model& model, slog::base_gate& gate); - web::http::experimental::listener::api_router make_registration_api(nmos::registry_model& model, slog::base_gate& gate) + web::http::experimental::listener::api_router make_registration_api(nmos::registry_model& model, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) { using namespace web::http::experimental::listener::api_router_using_declarations; @@ -86,6 +88,12 @@ namespace nmos return pplx::task_from_result(true); }); + if (validate_authorization) + { + registration_api.support(U("/x-nmos/") + nmos::patterns::registration_api.pattern + U("/?"), validate_authorization); + registration_api.support(U("/x-nmos/") + nmos::patterns::registration_api.pattern + U("/.*"), validate_authorization); + } + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is04_versions::from_settings(model.settings); }); registration_api.support(U("/x-nmos/") + nmos::patterns::registration_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) { @@ -159,6 +167,11 @@ namespace nmos { return nmos::make_api_version(request_version) + U(" request conflicts with the existing ") + nmos::make_api_version(super_resource_version) + U(" registration of the parent"); } + + inline utility::string_t make_valid_client_id_error(const utility::string_t& request_client_id) + { + return U("request for resource modification with invalid client_id ") + request_client_id; + } } namespace details @@ -282,7 +295,15 @@ namespace nmos const bool valid_version = creating || unchanged || nmos::fields::version(data) > nmos::fields::version(resource->data); valid = valid && valid_version; - if (!valid_type) + // check received request isn't being processed out of order + const auto received_time = req.headers().find(details::received_time); + const auto received = req.headers().end() != received_time ? nmos::parse_version(received_time->second) : nmos::tai{}; + const bool valid_received = creating || received == nmos::tai{} || received > resource->received; + valid = valid && valid_received; + + if (!valid_received) + slog::log(gate, SLOG_FLF) << "Registration requested for " << id_type << " at " << nmos::make_version(resource->received) << " processed before request received at " << nmos::make_version(received); + else if (!valid_type) slog::log(gate, SLOG_FLF) << "Registration requested for " << id_type << " would modify type from " << resource->type.name; else if (!valid_api_version) slog::log(gate, SLOG_FLF) << "Registration requested for " << id_type << " would modify API version from " << nmos::make_api_version(resource->version); @@ -309,7 +330,7 @@ namespace nmos { // "The 'senders' and 'receivers' arrays in a Device have been deprecated, but will continue to be present until v2.0." // Therefore, issue warnings rather than errors here - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2.1/docs/4.2.%20Behaviour%20-%20Querying.md#referential-integrity + // See https://specs.amwa.tv/is-04/releases/v1.2.1/docs/4.2._Behaviour_-_Querying.html#referential-integrity for (auto& element : nmos::fields::senders(data)) { @@ -339,7 +360,7 @@ namespace nmos { // v1.1 introduced device_id for flow, and uses it for referential integrity rather than source_id // so if the source is not (yet) registered, issue a warning not an error, and don't treat this as invalid? - // see https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2.1/docs/4.1.%20Behaviour%20-%20Registration.md#referential-integrity + // see https://specs.amwa.tv/is-04/releases/v1.2.1/docs/4.1._Behaviour_-_Registration.html#referential-integrity if (nmos::is04_versions::v1_1 <= version) { const auto& source_id = nmos::fields::source_id(data); @@ -396,39 +417,73 @@ namespace nmos // always reject updates that would modify resource type or super-resource if (valid_type && valid_super_id_type && (valid || allow_invalid_resources)) { + // Registry MUST register the Client ID of the client performing the registration. Subsequent requests to modify or delete a registered + // resource MUST validate the Client ID to ensure that clients do not, maliciously or incorrectly, alter resources belonging to other nodes + // see https://specs.amwa.tv/bcp-003-02/releases/v1.0.0/docs/1.0._Authorization_Practice.html#registry-client-authorization + utility::string_t client_id; + if (nmos::experimental::fields::server_authorization(model.settings)) + { + // get client_id from header's access token + client_id = nmos::experimental::get_client_id(req.headers(), gate); + } + if (creating) { - nmos::resource created_resource{ version, type, data, false }; + nmos::resource created_resource{ version, type, data, false, client_id }; + created_resource.received = received; set_reply(res, status_codes::Created, data); res.headers().add(web::http::header_names::location, make_registration_api_resource_location(created_resource)); resource = insert_resource(resources, std::move(created_resource), allow_invalid_resources).first; } + // invalid Client ID, reject resource modification + // see https://specs.amwa.tv/bcp-003-02/releases/v1.0.0/docs/1.0._Authorization_Practice.html#registry-client-authorization + else if (client_id != resource->client_id) + { + auto req_host = web::http::get_host_port(req).first; + if (req_host.empty()) + { + req_host = nmos::get_host(model.settings); + } + const auto error_description = details::make_valid_client_id_error(client_id); + const utility::string_t auth_params{ U("Bearer realm=") + req_host + U(",error=") + web::http::oauth2::experimental::resource_server_errors::insufficient_scope.name + U(",error_description=") + error_description }; + res.headers().add(web::http::header_names::www_authenticate, auth_params); + set_error_reply(res, status_codes::Forbidden, error_description); + } else { set_reply(res, status_codes::OK, data); res.headers().add(web::http::header_names::location, make_registration_api_resource_location(*resource)); - modify_resource(resources, id, [&data](nmos::resource& resource) + modify_resource(resources, id, [&received, &data](nmos::resource& resource) { + resource.received = received; resource.data = data; }); } - // experimental extension, for debugging - res.headers().add(U("X-Paging-Timestamp"), make_version(resource->updated)); + // resource created/updated + if (client_id == resource->client_id) + { + // experimental extension, for debugging + res.headers().add(U("X-Paging-Timestamp"), make_version(resource->updated)); - slog::log(gate, SLOG_FLF) << "At " << nmos::make_version(nmos::tai_now()) << ", the registry contains " << nmos::put_resources_statistics(resources); + slog::log(gate, SLOG_FLF) << "At " << nmos::make_version(nmos::tai_now()) << ", the registry contains " << nmos::put_resources_statistics(resources); - slog::log(gate, SLOG_FLF) << "Notifying query websockets thread"; // and anyone else who cares... - model.notify(); + slog::log(gate, SLOG_FLF) << "Notifying query websockets thread"; // and anyone else who cares... + model.notify(); + } + } + else if (!valid_received) + { + set_reply(res, status_codes::InternalError); } else if (!valid_api_version) { // experimental extension, proposed for v1.3, using a more specific status code to distinguish conflicts from validation errors // when that conflict may be resolvable automatically by the Node - // see https://github.com/AMWA-TV/nmos-discovery-registration/pull/85 + // see https://github.com/AMWA-TV/is-04/pull/85 set_error_reply(res, status_codes::Conflict, U("Conflict; ") + details::make_valid_api_version_error(version, resource->version)); // the Location header would enable an HTTP DELETE to be performed to explicitly clear the registry of the conflicting registration @@ -585,10 +640,19 @@ namespace nmos const string_t resourceType = parameters.at(nmos::patterns::resourceType.name); const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); - auto resource = find_resource(resources, { resourceId, nmos::type_from_resourceType(resourceType) }); + const std::pair id_type{ resourceId, nmos::type_from_resourceType(resourceType) }; + auto resource = find_resource(resources, id_type); if (resources.end() != resource) { - if (resource->version == version) + // check received request isn't being processed out of order + const auto received_time = req.headers().find(details::received_time); + const auto received = req.headers().end() != received_time ? nmos::parse_version(received_time->second) : nmos::tai{}; + if (received != nmos::tai{} && received < resource->received) + { + slog::log(gate, SLOG_FLF) << "Registration deletion requested for " << id_type << " at " << nmos::make_version(resource->received) << " processed before request received at " << nmos::make_version(received); + set_reply(res, status_codes::InternalError); + } + else if (resource->version == version) { slog::log(gate, SLOG_FLF) << "Deleting resource: " << resourceId; @@ -606,7 +670,7 @@ namespace nmos // "If a Node unregisters a resource in the incorrect order, the Registration API MUST clean up related child resources // on the Node's behalf in order to prevent stale entries remaining in the registry." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md#controlled-unregistration + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#controlled-unregistration erase_resource(resources, resource->id, false); slog::log(gate, SLOG_FLF) << "Notifying query websockets thread"; // and anyone else who cares... diff --git a/Development/nmos/registration_api.h b/Development/nmos/registration_api.h index 387822f1c..f254e02cd 100644 --- a/Development/nmos/registration_api.h +++ b/Development/nmos/registration_api.h @@ -9,14 +9,19 @@ namespace slog } // Registration API implementation -// See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/RegistrationAPI.raml +// See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/RegistrationAPI.html namespace nmos { struct registry_model; void erase_expired_resources_thread(nmos::registry_model& model, slog::base_gate& gate); - web::http::experimental::listener::api_router make_registration_api(nmos::registry_model& model, slog::base_gate& gate); + web::http::experimental::listener::api_router make_registration_api(nmos::registry_model& model, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); + + inline web::http::experimental::listener::api_router make_registration_api(nmos::registry_model& model, slog::base_gate& gate) + { + return make_registration_api(model, {}, gate); + } } #endif diff --git a/Development/nmos/registry_server.cpp b/Development/nmos/registry_server.cpp index 43bcad39d..540581215 100644 --- a/Development/nmos/registry_server.cpp +++ b/Development/nmos/registry_server.cpp @@ -27,15 +27,33 @@ namespace nmos namespace experimental { - // Construct a server instance for an NMOS Registry instance, implementing the IS-04 Registration and Query APIs, the Node API, the IS-09 System API + // Construct a server instance for an NMOS Registry instance, implementing the IS-04 Registration and Query APIs, the Node API, the IS-09 System API, the IS-10 Authorization API // and the experimental DNS-SD Browsing API, Logging API and Settings API, according to the specified data models - nmos::server make_registry_server(nmos::registry_model& registry_model, nmos::experimental::log_model& log_model, slog::base_gate& gate) + nmos::server make_registry_server(nmos::registry_model& registry_model, nmos::experimental::registry_implementation registry_implementation, nmos::experimental::log_model& log_model, slog::base_gate& gate) { // Log the API addresses we'll be using - slog::log(gate, SLOG_FLF) << "Configuring nmos-cpp registry with its primary Node API at: " << nmos::get_host(registry_model.settings) << ":" << nmos::fields::node_port(registry_model.settings); - slog::log(gate, SLOG_FLF) << "Configuring nmos-cpp registry with its primary Registration API at: " << nmos::get_host(registry_model.settings) << ":" << nmos::fields::registration_port(registry_model.settings); - slog::log(gate, SLOG_FLF) << "Configuring nmos-cpp registry with its primary Query API at: " << nmos::get_host(registry_model.settings) << ":" << nmos::fields::query_port(registry_model.settings); + slog::log(gate, SLOG_FLF) << "Configuring nmos-cpp registry with its primary Node API at: " + << web::uri_builder() + .set_scheme(nmos::http_scheme(registry_model.settings)) + .set_host(nmos::get_host(registry_model.settings)) + .set_port(nmos::fields::node_port(registry_model.settings)) + .set_path(U("/x-nmos/node/") + nmos::make_api_version(*nmos::is04_versions::from_settings(registry_model.settings).rbegin())) + .to_string(); + slog::log(gate, SLOG_FLF) << "Configuring nmos-cpp registry with its primary Registration API at: " + << web::uri_builder() + .set_scheme(nmos::http_scheme(registry_model.settings)) + .set_host(nmos::get_host(registry_model.settings)) + .set_port(nmos::fields::registration_port(registry_model.settings)) + .set_path(U("/x-nmos/registration/") + nmos::make_api_version(*nmos::is04_versions::from_settings(registry_model.settings).rbegin())) + .to_string(); + slog::log(gate, SLOG_FLF) << "Configuring nmos-cpp registry with its primary Query API at: " + << web::uri_builder() + .set_scheme(nmos::http_scheme(registry_model.settings)) + .set_host(nmos::get_host(registry_model.settings)) + .set_port(nmos::fields::query_port(registry_model.settings)) + .set_path(U("/x-nmos/query/") + nmos::make_api_version(*nmos::is04_versions::from_settings(registry_model.settings).rbegin())) + .to_string(); nmos::server registry_server{ registry_model }; @@ -43,6 +61,10 @@ namespace nmos const auto server_secure = nmos::experimental::fields::server_secure(registry_model.settings); + const auto hsts = nmos::experimental::get_hsts(registry_model.settings); + + const auto server_address = nmos::experimental::fields::server_address(registry_model.settings); + // Configure the DNS-SD Browsing API const host_port mdns_address(nmos::experimental::fields::mdns_address(registry_model.settings), nmos::experimental::fields::mdns_port(registry_model.settings)); @@ -60,22 +82,23 @@ namespace nmos // Configure the Query API - registry_server.api_routers[{ {}, nmos::fields::query_port(registry_model.settings) }].mount({}, nmos::make_query_api(registry_model, gate)); + auto validate_authorization = registry_implementation.validate_authorization; + registry_server.api_routers[{ {}, nmos::fields::query_port(registry_model.settings) }].mount({}, nmos::make_query_api(registry_model, validate_authorization ? validate_authorization(nmos::experimental::scopes::query) : nullptr, gate)); // "Source ID of the Query API instance issuing the data Grain" - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/queryapi-subscriptions-websocket.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/queryapi-subscriptions-websocket.html const nmos::id query_id = nmos::make_repeatable_id(nmos::experimental::fields::seed_id(registry_model.settings), U("/x-nmos/query")); auto& query_ws_api = registry_server.ws_handlers[{ {}, nmos::fields::query_ws_port(registry_model.settings) }]; - query_ws_api.first = nmos::make_query_ws_api(query_id, registry_model, query_ws_api.second, gate); + query_ws_api.first = nmos::make_query_ws_api(query_id, registry_model, query_ws_api.second, registry_implementation.ws_validate_authorization, gate); // Configure the Registration API - registry_server.api_routers[{ {}, nmos::fields::registration_port(registry_model.settings) }].mount({}, nmos::make_registration_api(registry_model, gate)); + registry_server.api_routers[{ {}, nmos::fields::registration_port(registry_model.settings) }].mount({}, nmos::make_registration_api(registry_model, validate_authorization ? validate_authorization(nmos::experimental::scopes::registration) : nullptr, gate)); // Configure the Node API - registry_server.api_routers[{ {}, nmos::fields::node_port(registry_model.settings) }].mount({}, nmos::make_node_api(registry_model, {}, gate)); + registry_server.api_routers[{ {}, nmos::fields::node_port(registry_model.settings) }].mount({}, nmos::make_node_api(registry_model, {}, validate_authorization ? validate_authorization(nmos::experimental::scopes::node) : nullptr, gate)); // set up the node resources auto& self_resources = registry_model.node_resources; @@ -105,26 +128,26 @@ namespace nmos // Set up the listeners for each HTTP API port - auto http_config = nmos::make_http_listener_config(registry_model.settings); + auto http_config = nmos::make_http_listener_config(registry_model.settings, registry_implementation.load_server_certificates, registry_implementation.load_dh_param, registry_implementation.get_ocsp_response, gate); for (auto& api_router : registry_server.api_routers) { - // default empty string means the wildcard address - const auto& host = !api_router.first.first.empty() ? api_router.first.first : web::http::experimental::listener::host_wildcard; + // if IP address isn't specified for this router, use default server address or wildcard address + const auto& host = !api_router.first.first.empty() ? api_router.first.first : !server_address.empty() ? server_address : web::http::experimental::listener::host_wildcard; // map the configured client port to the server port on which to listen // hmm, this should probably also take account of the address - registry_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, nmos::experimental::server_port(api_router.first.second, registry_model.settings), api_router.second, http_config, gate)); + registry_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, nmos::experimental::server_port(api_router.first.second, registry_model.settings), api_router.second, http_config, hsts, gate)); } // Set up the handlers for each WebSocket API port - auto websocket_config = nmos::make_websocket_listener_config(registry_model.settings); + auto websocket_config = nmos::make_websocket_listener_config(registry_model.settings, registry_implementation.load_server_certificates, registry_implementation.load_dh_param, registry_implementation.get_ocsp_response, gate); websocket_config.set_log_callback(nmos::make_slog_logging_callback(gate)); for (auto& ws_handler : registry_server.ws_handlers) { - // default empty string means the wildcard address - const auto& host = !ws_handler.first.first.empty() ? ws_handler.first.first : web::websockets::experimental::listener::host_wildcard; + // if IP address isn't specified for this router, use default server address or wildcard address + const auto& host = !ws_handler.first.first.empty() ? ws_handler.first.first : !server_address.empty() ? server_address : web::websockets::experimental::listener::host_wildcard; // map the configured client port to the server port on which to listen // hmm, this should probably also take account of the address registry_server.ws_listeners.push_back(nmos::make_ws_api_listener(server_secure, host, nmos::experimental::server_port(ws_handler.first.second, registry_model.settings), ws_handler.second.first, websocket_config, gate)); @@ -142,6 +165,12 @@ namespace nmos return registry_server; } + + // deprecated + nmos::server make_registry_server(nmos::registry_model& registry_model, nmos::experimental::log_model& log_model, slog::base_gate& gate) + { + return make_registry_server(registry_model, registry_implementation(), log_model, gate); + } } void advertise_registry_thread(nmos::registry_model& model, slog::base_gate& gate) diff --git a/Development/nmos/registry_server.h b/Development/nmos/registry_server.h index 9a5290789..82dce35ce 100644 --- a/Development/nmos/registry_server.h +++ b/Development/nmos/registry_server.h @@ -1,6 +1,11 @@ #ifndef NMOS_REGISTRY_SERVER_H #define NMOS_REGISTRY_SERVER_H +#include "nmos/authorization_handlers.h" +#include "nmos/certificate_handlers.h" +#include "nmos/ocsp_response_handler.h" +#include "nmos/ws_api_utils.h" + namespace slog { class base_gate; @@ -16,8 +21,52 @@ namespace nmos { struct log_model; - // Construct a server instance for an NMOS Registry instance, implementing the IS-04 Registration and Query APIs, the Node API, the IS-09 System API + // a type to simplify passing around the application callbacks necessary to integrate the device-specific + // underlying implementation into the server instance for the NMOS Registry + struct registry_implementation + { + registry_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::ocsp_response_handler get_ocsp_response, validate_authorization_handler validate_authorization, ws_validate_authorization_handler ws_validate_authorization) + : load_server_certificates(std::move(load_server_certificates)) + , load_dh_param(std::move(load_dh_param)) + , load_ca_certificates(std::move(load_ca_certificates)) + , get_ocsp_response(std::move(get_ocsp_response)) + , validate_authorization(std::move(validate_authorization)) + , ws_validate_authorization(std::move(ws_validate_authorization)) + {} + + // use the default constructor and chaining member functions for fluent initialization + // (by itself, the default constructor does not construct a valid instance) + registry_implementation() + {} + + registry_implementation& on_load_server_certificates(nmos::load_server_certificates_handler load_server_certificates) { this->load_server_certificates = std::move(load_server_certificates); return *this; } + registry_implementation& on_load_dh_param(nmos::load_dh_param_handler load_dh_param) { this->load_dh_param = std::move(load_dh_param); return *this; } + registry_implementation& on_load_ca_certificates(nmos::load_ca_certificates_handler load_ca_certificates) { this->load_ca_certificates = std::move(load_ca_certificates); return *this; } + registry_implementation& on_get_ocsp_response(nmos::ocsp_response_handler get_ocsp_response) { this->get_ocsp_response = std::move(get_ocsp_response); return *this; } + registry_implementation& on_validate_authorization(validate_authorization_handler validate_authorization) { this->validate_authorization = std::move(validate_authorization); return* this; } + registry_implementation& on_ws_validate_authorization(ws_validate_authorization_handler ws_validate_authorization) { this->ws_validate_authorization = std::move(ws_validate_authorization); return *this; } + + // determine if the required callbacks have been specified + bool valid() const + { + return true; + } + + nmos::load_server_certificates_handler load_server_certificates; + nmos::load_dh_param_handler load_dh_param; + nmos::load_ca_certificates_handler load_ca_certificates; + + nmos::ocsp_response_handler get_ocsp_response; + + validate_authorization_handler validate_authorization; + ws_validate_authorization_handler ws_validate_authorization; + }; + + // Construct a server instance for an NMOS Registry instance, implementing the IS-04 Registration and Query APIs, the Node API, the IS-09 System API, the IS-10 Authorization API // and the experimental DNS-SD Browsing API, Logging API and Settings API, according to the specified data models + nmos::server make_registry_server(nmos::registry_model& registry_model, nmos::experimental::registry_implementation registry_implementation, nmos::experimental::log_model& log_model, slog::base_gate& gate); + + // deprecated nmos::server make_registry_server(nmos::registry_model& registry_model, nmos::experimental::log_model& log_model, slog::base_gate& gate); } } diff --git a/Development/nmos/resource.cpp b/Development/nmos/resource.cpp index 3e763e508..872a2a42b 100644 --- a/Development/nmos/resource.cpp +++ b/Development/nmos/resource.cpp @@ -7,8 +7,8 @@ namespace nmos { namespace details { - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/resource_core.json - web::json::value make_resource_core(const nmos::id& id, const utility::string_t& label, const utility::string_t& description) + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/resource_core.html + web::json::value make_resource_core(const nmos::id& id, const utility::string_t& label, const utility::string_t& description, const web::json::value& tags) { using web::json::value; using web::json::value_of; @@ -18,15 +18,16 @@ namespace nmos { nmos::fields::version, nmos::make_version() }, { nmos::fields::label, label }, { nmos::fields::description, description }, - { nmos::fields::tags, value::object() } + { nmos::fields::tags, tags } }); } web::json::value make_resource_core(const nmos::id& id, const nmos::settings& settings) { const auto& label = nmos::experimental::fields::label(settings); + const auto& description = nmos::experimental::fields::description(settings); - return make_resource_core(id, label, label); + return make_resource_core(id, label, description); } } } diff --git a/Development/nmos/resource.h b/Development/nmos/resource.h index 9378d4ca8..c2c2a20ee 100644 --- a/Development/nmos/resource.h +++ b/Development/nmos/resource.h @@ -24,7 +24,7 @@ namespace nmos // when any data is modified, the update timestamp must be set, and resource events should be generated // *or more accurately, after insertion into the registry - resource(api_version version, type type, web::json::value&& data, nmos::id id, bool never_expire) + resource(api_version version, type type, web::json::value&& data, nmos::id id, bool never_expire, utility::string_t client_id = {}) : version(version) , downgrade_version() , type(type) @@ -33,10 +33,11 @@ namespace nmos , created(tai_now()) , updated(created) , health(never_expire ? health_forever : created.seconds) + , client_id(std::move(client_id)) {} - resource(api_version version, type type, web::json::value data, bool never_expire) - : resource(version, type, std::move(data), fields::id(data), never_expire) + resource(api_version version, type type, web::json::value data, bool never_expire, utility::string_t client_id = {}) + : resource(version, type, std::move(data), fields::id(data), never_expire, std::move(client_id)) {} // the API version of the Node API, Registration API or Query API exposing this resource @@ -63,18 +64,25 @@ namespace nmos // sub-resources are tracked in order to optimise resource expiry and deletion std::set sub_resources; - // see https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.5.%20APIs%20-%20Query%20Parameters.md#pagination + // see https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.5._APIs_-_Query_Parameters.html#pagination tai created; tai updated; + // when the most recently applied request was received + tai received; - // see https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md#heartbeating + // see https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#heartbeating mutable details::copyable_atomic health; + + // Registry MUST register the Client ID of the client performing the registration. Subsequent requests to modify or delete a registered + // resource MUST validate the Client ID to ensure that clients do not, maliciously or incorrectly, alter resources belonging to other nodes + // see https://specs.amwa.tv/bcp-003-02/releases/v1.0.0/docs/1.0._Authorization_Practice.html#registry-client-authorization + utility::string_t client_id; }; namespace details { - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/resource_core.json - web::json::value make_resource_core(const nmos::id& id, const utility::string_t& label, const utility::string_t& description); + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/resource_core.html + web::json::value make_resource_core(const nmos::id& id, const utility::string_t& label, const utility::string_t& description, const web::json::value& tags = web::json::value::object()); web::json::value make_resource_core(const nmos::id& id, const nmos::settings& settings); } diff --git a/Development/nmos/resources.cpp b/Development/nmos/resources.cpp index 436aab475..a4c0b2144 100644 --- a/Development/nmos/resources.cpp +++ b/Development/nmos/resources.cpp @@ -285,7 +285,7 @@ namespace nmos static inline std::pair no_resource() { return{}; } // get the super-resource id and type, according to the guidelines on referential integrity - // see https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2.1/docs/4.1.%20Behaviour%20-%20Registration.md#referential-integrity + // see https://specs.amwa.tv/is-04/releases/v1.2.1/docs/4.1._Behaviour_-_Registration.html#referential-integrity std::pair get_super_resource(const api_version& version, const type& type, const web::json::value& data) { if (data.is_null()) diff --git a/Development/nmos/resources.h b/Development/nmos/resources.h index d9efa8609..338a166ed 100644 --- a/Development/nmos/resources.h +++ b/Development/nmos/resources.h @@ -96,7 +96,7 @@ namespace nmos // Other helper functions for resources // get the super-resource id and type, according to the guidelines on referential integrity - // see https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2.1/docs/4.1.%20Behaviour%20-%20Registration.md#referential-integrity + // see https://specs.amwa.tv/is-04/releases/v1.2.1/docs/4.1._Behaviour_-_Registration.html#referential-integrity std::pair get_super_resource(const api_version& version, const type& type, const web::json::value& data); inline std::pair get_super_resource(const resource& resource) diff --git a/Development/nmos/schemas_api.cpp b/Development/nmos/schemas_api.cpp index b5cb3a080..0a15c165f 100644 --- a/Development/nmos/schemas_api.cpp +++ b/Development/nmos/schemas_api.cpp @@ -6,6 +6,7 @@ #include "nmos/api_utils.h" #include "nmos/json_schema.h" #include "nmos/log_manip.h" +#include "nmos/media_type.h" // for nmos::media_types::application_schema_json namespace nmos { @@ -14,7 +15,7 @@ namespace nmos namespace patterns { const route_pattern schemas = make_route_pattern(U("api"), U("schemas")); - const route_pattern schemasRepository = make_route_pattern(U("repository"), U("nmos-[^/]+")); + const route_pattern schemasRepository = make_route_pattern(U("repository"), U("[^/]+")); const route_pattern schemasTag = make_route_pattern(U("tag"), U("v[0-9]+\\.[^/]+")); const route_pattern schemasRef = make_route_pattern(U("ref"), U("[^/]+")); } @@ -185,10 +186,10 @@ namespace nmos if (schemas.end() != found) { - res.headers().set_content_type(U("application/schema+json")); + res.headers().set_content_type(nmos::media_types::application_schema_json.name); // experimental extension, to support human-readable HTML rendering of NMOS responses - if (experimental::details::is_html_response_preferred(req, U("application/schema+json"))) + if (experimental::details::is_html_response_preferred(req, nmos::media_types::application_schema_json.name)) { const auto base_uri = web::uri_builder().set_path(U("/schemas/") + repository + U("/") + tag + U("/")).to_uri(); set_reply(res, status_codes::OK, details::make_json_schema_html_response_body(base_uri, found->second)); diff --git a/Development/nmos/scope.h b/Development/nmos/scope.h new file mode 100644 index 000000000..25d65004e --- /dev/null +++ b/Development/nmos/scope.h @@ -0,0 +1,51 @@ +#ifndef NMOS_SCOPE_H +#define NMOS_SCOPE_H + +#include "nmos/string_enum.h" + +namespace nmos +{ + // experimental extension, for BCP-003-02 Authorization + namespace experimental + { + // scope (used in JWT) + DEFINE_STRING_ENUM(scope) + namespace scopes + { + // IS-04 + const scope registration{ U("registration") }; + const scope query{ U("query") }; + const scope node{ U("node") }; + // IS-05 + const scope connection{ U("connection") }; + // IS-06 + const scope netctrl{ U("netctrl") }; + // IS-07 + const scope events{ U("events") }; + // IS-08 + const scope channelmapping{ U("channelmapping") }; + // IS-12 + const scope ncp{ U("ncp") }; + } + + inline utility::string_t make_scope(const scope& scope) + { + return scope.name; + } + + inline scope parse_scope(const utility::string_t& scope) + { + if (scopes::registration.name == scope) { return scopes::registration; } + if (scopes::query.name == scope) { return scopes::query; } + if (scopes::node.name == scope) { return scopes::node; } + if (scopes::connection.name == scope) { return scopes::connection; } + if (scopes::netctrl.name == scope) { return scopes::netctrl; } + if (scopes::events.name == scope) { return scopes::events; } + if (scopes::channelmapping.name == scope) { return scopes::channelmapping; } + if (scopes::ncp.name == scope) { return scopes::ncp; } + return{}; + } + } +} + +#endif diff --git a/Development/nmos/sdp_attributes.cpp b/Development/nmos/sdp_attributes.cpp new file mode 100644 index 000000000..c86fc9d5f --- /dev/null +++ b/Development/nmos/sdp_attributes.cpp @@ -0,0 +1,61 @@ +#include "nmos/sdp_attributes.h" + +#include "cpprest/json_utils.h" + +namespace nmos +{ + namespace details + { + // hm, forward declaration for function in nmos/sdp_utils.cpp + std::pair get_address_type_multicast(const utility::string_t& address); + } + + namespace sdp_attributes + { + web::json::value make_extmap(const extmap& extmap) + { + using web::json::value_of; + + const bool keep_order = true; + + return value_of({ + { sdp::fields::name, sdp::attributes::extmap }, + { sdp::fields::value, value_of({ + { sdp::fields::local_id, extmap.local_id }, + { extmap.direction != sdp::direction{} ? sdp::fields::direction.key : U(""), extmap.direction.name }, + { sdp::fields::uri, extmap.uri }, + { !extmap.ext_attributes.empty() ? sdp::fields::extensionattributes.key : U(""), extmap.ext_attributes }, + }, keep_order) } + }, keep_order); + } + + extmap parse_extmap(const web::json::value& extmap) + { + return{ sdp::fields::local_id(extmap), sdp::direction(sdp::fields::direction(extmap)), sdp::fields::uri(extmap), sdp::fields::extensionattributes(extmap) }; + } + + web::json::value make_hkep(const hkep& hkep) + { + using web::json::value_of; + + const bool keep_order = true; + + return value_of({ + { sdp::fields::name, sdp::attributes::hkep }, + { sdp::fields::value, value_of({ + { sdp::fields::port, hkep.port }, + { sdp::fields::network_type, sdp::network_types::internet.name }, + { sdp::fields::address_type, details::get_address_type_multicast(hkep.unicast_address).first.name }, + { sdp::fields::unicast_address, hkep.unicast_address }, + { sdp::fields::node_id, hkep.node_id }, + { sdp::fields::port_id, hkep.port_id }, + }, keep_order) } + }, keep_order); + } + + hkep parse_hkep(const web::json::value& hkep) + { + return{ sdp::fields::port(hkep), sdp::fields::unicast_address(hkep), sdp::fields::node_id(hkep), sdp::fields::port_id(hkep) }; + } + } +} diff --git a/Development/nmos/sdp_attributes.h b/Development/nmos/sdp_attributes.h new file mode 100644 index 000000000..d5b7d3371 --- /dev/null +++ b/Development/nmos/sdp_attributes.h @@ -0,0 +1,48 @@ +#ifndef NMOS_SDP_ATTRIBUTES_H +#define NMOS_SDP_ATTRIBUTES_H + +#include "sdp/json.h" + +namespace nmos +{ + namespace sdp_attributes + { + // RTP Header Extensions + // See https://tools.ietf.org/html/rfc5285#section-5 + struct extmap + { + uint64_t local_id; + sdp::direction direction; + utility::string_t uri; + utility::string_t ext_attributes; + + extmap() : local_id() {} + extmap(uint64_t local_id, const utility::string_t& uri) : local_id(local_id), uri(uri) {} + extmap(uint64_t local_id, const sdp::direction& direction, const utility::string_t& uri) : local_id(local_id), direction(direction), uri(uri) {} + extmap(uint64_t local_id, const utility::string_t& uri, const utility::string_t& ext_attributes) : local_id(local_id), uri(uri), ext_attributes(ext_attributes) {} + extmap(uint64_t local_id, const sdp::direction& direction, const utility::string_t& uri, const utility::string_t& ext_attributes) : local_id(local_id), direction(direction), uri(uri), ext_attributes(ext_attributes) {} + }; + + web::json::value make_extmap(const extmap& extmap); + extmap parse_extmap(const web::json::value& extmap); + + // HDCP Key Exchange Protocol (HKEP) Signalling + // See VSF TR-10-5:2022 Internet Protocol Media Experience (IPMX): HDCP Key Exchange Protocol, Section 10 + // at https://videoservicesforum.com/download/technical_recommendations/VSF_TR-10-5_2022-03-22.pdf + struct hkep + { + uint64_t port; + utility::string_t unicast_address; + utility::string_t node_id; + utility::string_t port_id; + + hkep() : port() {} + hkep(uint64_t port, const utility::string_t& unicast_address, const utility::string_t& node_id, const utility::string_t& port_id) : port(port), unicast_address(unicast_address), node_id(node_id), port_id(port_id) {} + }; + + web::json::value make_hkep(const hkep& hkep); + hkep parse_hkep(const web::json::value& hkep); + } +} + +#endif diff --git a/Development/nmos/sdp_utils.cpp b/Development/nmos/sdp_utils.cpp index 7625e1a68..eb9b378c6 100644 --- a/Development/nmos/sdp_utils.cpp +++ b/Development/nmos/sdp_utils.cpp @@ -1,14 +1,16 @@ #include "nmos/sdp_utils.h" -#include #include #include #include #include +#include +#include #include "cpprest/basic_utils.h" #include "nmos/capabilities.h" #include "nmos/clock_ref_type.h" #include "nmos/channels.h" +#include "nmos/components.h" #include "nmos/format.h" #include "nmos/interlace_mode.h" #include "nmos/json_fields.h" @@ -19,11 +21,6 @@ namespace nmos { namespace details { - std::logic_error sdp_creation_error(const std::string& message) - { - return std::logic_error{ "sdp creation error - " + message }; - } - std::pair get_address_type_multicast(const utility::string_t& address) { #if BOOST_VERSION >= 106600 @@ -34,6 +31,7 @@ namespace nmos return{ ip_address.is_v4() ? sdp::address_types::IP4 : sdp::address_types::IP6, ip_address.is_multicast() }; } + // Construct ts-refclk attributes for each leg based on the IS-04 resources std::vector make_ts_refclk(const web::json::value& node, const web::json::value& source, const web::json::value& sender, bst::optional ptp_domain) { const auto& clock_name = nmos::fields::clock_name(source); @@ -90,69 +88,153 @@ namespace nmos else throw sdp_creation_error("unexpected clock ref_type"); } + // See sdp::samplings + typedef std::pair width_height_t; + static const std::map>> samplers + { + // Red-Green-Blue-Alpha + { sdp::samplings::RGBA, { { component_names::R, { 1, 1 } }, { component_names::G, { 1, 1 } }, { component_names::B, { 1, 1 } }, { component_names::A, { 1, 1 } } } }, + // Red-Green-Blue + { sdp::samplings::RGB, { { component_names::R, { 1, 1 } }, { component_names::G, { 1, 1 } }, { component_names::B, { 1, 1 } } } }, + // Non-constant luminance YCbCr + { sdp::samplings::YCbCr_4_4_4, { { component_names::Y, { 1, 1 } }, { component_names::Cb, { 1, 1 } }, { component_names::Cr, { 1, 1 } } } }, + { sdp::samplings::YCbCr_4_2_2, { { component_names::Y, { 1, 1 } }, { component_names::Cb, { 2, 1 } }, { component_names::Cr, { 2, 1 } } } }, + { sdp::samplings::YCbCr_4_2_0, { { component_names::Y, { 1, 1 } }, { component_names::Cb, { 2, 2 } }, { component_names::Cr, { 2, 2 } } } }, + { sdp::samplings::YCbCr_4_1_1, { { component_names::Y, { 1, 1 } }, { component_names::Cb, { 4, 1 } }, { component_names::Cr, { 4, 1 } } } }, + // Constant luminance YCbCr + { sdp::samplings::CLYCbCr_4_4_4, { { component_names::Yc, { 1, 1 } }, { component_names::Cbc, { 1, 1 } }, { component_names::Crc, { 1, 1 } } } }, + { sdp::samplings::CLYCbCr_4_2_2, { { component_names::Yc, { 1, 1 } }, { component_names::Cbc, { 2, 1 } }, { component_names::Crc, { 2, 1 } } } }, + { sdp::samplings::CLYCbCr_4_2_0, { { component_names::Yc, { 1, 1 } }, { component_names::Cbc, { 2, 2 } }, { component_names::Crc, { 2, 2 } } } }, + // Constant intensity ICtCp + { sdp::samplings::ICtCp_4_4_4, { { component_names::I, { 1, 1 } }, { component_names::Ct, { 1, 1 } }, { component_names::Cp, { 1, 1 } } } }, + { sdp::samplings::ICtCp_4_2_2, { { component_names::I, { 1, 1 } }, { component_names::Ct, { 2, 1 } }, { component_names::Cp, { 2, 1 } } } }, + { sdp::samplings::ICtCp_4_2_0, { { component_names::I, { 1, 1 } }, { component_names::Ct, { 2, 2 } }, { component_names::Cp, { 2, 2 } } } }, + // XYZ + { sdp::samplings::XYZ, { { component_names::X, { 1, 1 } }, { component_names::Y, { 1, 1 } }, { component_names::Z, { 1, 1 } } } }, + // Key signal represented as a single component + { sdp::samplings::KEY, { { component_names::Key, { 1, 1 } } } }, + // Sampling signaled by the payload + { sdp::samplings::UNSPECIFIED, {} } + }; + } + + web::json::value make_components(const sdp::sampling& sampling, uint32_t width, uint32_t height, uint32_t depth) + { + using web::json::value; + using web::json::value_from_elements; + + const auto sampler = nmos::details::samplers.find(sampling); + if (nmos::details::samplers.end() == sampler) return value::null(); + + return value_from_elements(sampler->second | boost::adaptors::transformed([&](const std::pair& component) + { + return make_component(component.first, width / component.second.first, height / component.second.second, depth); + })); + } + + namespace details + { sdp::sampling make_sampling(const web::json::array& components) { // https://tools.ietf.org/html/rfc4175#section-6.1 - // convert json to component name vs dimension lookup for easy access, - // as components can be in any order inside the json - struct dimension { int width; int height; }; - const auto dimensions = boost::copy_range>(components | boost::adaptors::transformed([](const web::json::value& component) + // each entry of the array indicates the number of samples in horizontal and vertical direction for the specified component + // which depends on the frame width and height, whereas the sampling values only indicate the relative (sub-)sampling frequency + // so to account for this and handle any order of the components in the array, convert into a map from component name to + // relative sampling period, i.e. components array of Y@1920x1080, Cb@960x540, Cr@960x540 is converted into the relative + // component sampler Y@1x1, Cb@2x2, Cr@2x2, which is YCbCr-4:2:0 + + typedef std::map components_t; + typedef std::map samplers_t; + + static const auto samplers = boost::copy_range(nmos::details::samplers | boost::adaptors::transformed([](const std::pair>>& sampler) { - return std::map::value_type{ nmos::fields::name(component), dimension{ nmos::fields::width(component), nmos::fields::height(component) } }; + return samplers_t::value_type{ boost::copy_range(sampler.second), sampler.first }; })); - const auto de = dimensions.end(); - if (de != dimensions.find(U("R")) && de != dimensions.find(U("G")) && de != dimensions.find(U("B")) && de != dimensions.find(U("A"))) + const auto max_samples = boost::accumulate(components | boost::adaptors::transformed([](const web::json::value& component) { - return sdp::samplings::RGBA; - } - else if (de != dimensions.find(U("R")) && de != dimensions.find(U("G")) && de != dimensions.find(U("B"))) + return width_height_t{ nmos::fields::width(component), nmos::fields::height(component) }; + }), width_height_t{}, [](const width_height_t& lhs, const width_height_t& rhs) { - return sdp::samplings::RGB; - } - else if (de != dimensions.find(U("Y")) && de != dimensions.find(U("Cb")) && de != dimensions.find(U("Cr"))) + return width_height_t{ (std::max)(lhs.first, rhs.first), (std::max)(lhs.second, rhs.second) }; + }); + const auto components_sampler = boost::copy_range(components | boost::adaptors::transformed([&max_samples](const web::json::value& component) { - const auto& Y = dimensions.at(U("Y")); - const auto& Cb = dimensions.at(U("Cb")); - const auto& Cr = dimensions.at(U("Cr")); - if (Cb.width != Cr.width || Cb.height != Cr.height) throw sdp_creation_error("unsupported YCbCr dimensions"); - const auto& C = Cb; - if (Y.height == C.height) - { - if (Y.width == C.width) return sdp::samplings::YCbCr_4_4_4; - else if (Y.width / 2 == C.width) return sdp::samplings::YCbCr_4_2_2; - else if (Y.width / 4 == C.width) return sdp::samplings::YCbCr_4_1_1; - else throw sdp_creation_error("unsupported YCbCr dimensions"); - } - else if (Y.height / 2 == C.height) - { - if (Y.width / 2 == C.width) return sdp::samplings::YCbCr_4_2_0; - else throw sdp_creation_error("unsupported YCbCr dimensions"); - } - else throw sdp_creation_error("unsupported YCbCr dimensions"); - } - else throw sdp_creation_error("unsupported components"); + const width_height_t samples{ nmos::fields::width(component), nmos::fields::height(component) }; + if (0 != max_samples.first % samples.first || 0 != max_samples.second % samples.second) throw sdp_creation_error("unsupported components"); + return components_t::value_type{ component_name{ nmos::fields::name(component) }, { max_samples.first / samples.first, max_samples.second / samples.second } }; + })); + + const auto sampler = samplers.find(components_sampler); + if (samplers.end() == sampler) throw sdp_creation_error("unsupported components"); + + return sampler->second; + } + + // Exact Frame Rate + // "Integer frame rates shall be signaled as a single decimal number (e.g. "25") whilst non-integer frame rates shall be + // signaled as a ratio of two integer decimal numbers separated by a "forward-slash" character (e.g. "30000/1001"), + // utilizing the numerically smallest numerator value possible." + // See ST 2110-20:2017 Section 7.2 Required Media Type Parameters + utility::string_t make_exactframerate(const nmos::rational& exactframerate) + { + return exactframerate.denominator() != 1 + ? utility::ostringstreamed(exactframerate.numerator()) + U("/") + utility::ostringstreamed(exactframerate.denominator()) + : utility::ostringstreamed(exactframerate.numerator()); + } + + nmos::rational parse_exactframerate(const utility::string_t& exactframerate) + { + const auto slash = exactframerate.find(U('/')); + return utility::string_t::npos != slash + ? nmos::rational(utility::istringstreamed(exactframerate.substr(0, slash)), utility::istringstreamed(exactframerate.substr(slash + 1))) + : nmos::rational(utility::istringstreamed(exactframerate)); + } + + // Pixel Aspect Ratio + // "PAR shall be signaled as a ratio of two integer decimal numbers separated by a "colon" character (e.g. "12:11")." + // See ST 2110-20:2017 Section 7.3 Media Type Parameters with default values + utility::string_t make_pixel_aspect_ratio(const nmos::rational& par) + { + return utility::ostringstreamed(par.numerator()) + U(":") + utility::ostringstreamed(par.denominator()); + } + + nmos::rational parse_pixel_aspect_ratio(const utility::string_t& par) + { + const auto slash = par.find(U(':')); + return utility::string_t::npos != slash + ? nmos::rational(utility::istringstreamed(par.substr(0, slash)), utility::istringstreamed(par.substr(slash + 1))) + : nmos::rational(utility::istringstreamed(par)); + } + + // Construct simple media stream ids based on the sender's number of legs + std::vector make_media_stream_ids(const web::json::value& sender) + { + const auto legs = nmos::fields::interface_bindings(sender).size(); + return boost::copy_range>(boost::irange(0, (int)legs) | boost::adaptors::transformed([&](const int& index) + { + return utility::ostringstreamed(index); + })); } } - static sdp_parameters make_video_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, const std::vector& media_stream_ids, bst::optional ptp_domain) + // Construct additional "video/raw" parameters from the IS-04 resources, using default values for unspecified items + video_raw_parameters make_video_raw_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional tp) { - sdp_parameters::video_t params; - params.tp = sdp::type_parameters::type_N; + video_raw_parameters params; - // colorimetry map directly to flow_video json "colorspace" - params.colorimetry = sdp::colorimetry{ nmos::fields::colorspace(flow) }; + // fmtp - // use flow json "components" to work out sampling const auto& components = nmos::fields::components(flow); params.sampling = details::make_sampling(components); + params.depth = nmos::fields::bit_depth(components.at(0)); params.width = nmos::fields::frame_width(flow); params.height = nmos::fields::frame_height(flow); - params.depth = nmos::fields::bit_depth(components.at(0)); - // also maps directly - params.tcs = sdp::transfer_characteristic_system{ nmos::fields::transfer_characteristic(flow) }; + // grain_rate is optional in the flow, but if it's not there, for a video flow, it must be in the source + const auto& grain_rate = nmos::fields::grain_rate(flow.has_field(nmos::fields::grain_rate) ? flow : source); + params.exactframerate = nmos::parse_rational(grain_rate); const auto& interlace_mode = nmos::fields::interlace_mode(flow); params.interlace = !interlace_mode.empty() && nmos::interlace_modes::progressive.name != interlace_mode; @@ -160,25 +242,54 @@ namespace nmos // RFC 4175 top-field-first refers to a specific payload packing option for chrominance samples in 4:2:0 video; // it does not correspond to nmos::interlace_modes::interlaced_tff - // grain_rate is optional in the flow, but if it's not there, for a video flow, it must be in the source - const auto& grain_rate = nmos::fields::grain_rate(flow.has_field(nmos::fields::grain_rate) ? flow : source); - params.exactframerate = nmos::rational(nmos::fields::numerator(grain_rate), nmos::fields::denominator(grain_rate)); + // map directly + params.tcs = sdp::transfer_characteristic_system{ nmos::fields::transfer_characteristic(flow) }; + params.colorimetry = sdp::colorimetry{ nmos::fields::colorspace(flow) }; + + // hm, RANGE and PAR not currently indicated in IS-04 so omit these + + // hm, PM not indicated in IS-04 so default this + params.pm = sdp::packing_modes::general; + + // "Senders implementing this standard shall signal the value ST2110-20:2017 unless the colorimetry value ALPHA + // or the TCS value ST2115LOGS3 are used, in which case the value ST2110-20:2022 shall be signaled." + params.ssn = params.colorimetry == sdp::colorimetries::ALPHA || params.tcs == sdp::transfer_characteristic_systems::ST2115LOGS3 + ? sdp::smpte_standard_numbers::ST2110_20_2022 + : sdp::smpte_standard_numbers::ST2110_20_2017; + + // hm, TP is equivalent to the new sender attribute, but for now, support optional override and default value + params.tp = tp + ? *tp + : sender.has_field(nmos::fields::st2110_21_sender_type) + ? sdp::type_parameter{ nmos::fields::st2110_21_sender_type(sender) } + : sdp::type_parameters::type_N; + + // hm, ST 2110-21 TROFF and CMAX not indicated in IS-04 so omit these + // hm, ST 2110-10 MAXUDP, TSMODE and TSDELAY not indicated in IS-04 so omit these - return{ sender.at(nmos::fields::label).as_string(), params, 96, media_stream_ids, details::make_ts_refclk(node, source, sender, ptp_domain) }; + return params; } - static sdp_parameters make_audio_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, const std::vector& media_stream_ids, bst::optional ptp_domain) + // deprecated, use make_video_raw_parameters and then make_video_raw_sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters make_video_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional payload_type, const std::vector& media_stream_ids, bst::optional ptp_domain, bst::optional tp) { - sdp_parameters::audio_t params; + auto params = make_video_raw_parameters(node, source, flow, sender, tp); + return{ nmos::fields::label(sender), params, payload_type ? *payload_type : details::payload_type_video_default, !media_stream_ids.empty() ? media_stream_ids : details::make_media_stream_ids(sender), details::make_ts_refclk(node, source, sender, ptp_domain) }; + } + + // Construct additional "audio/L" parameters from the IS-04 resources, using default values for unspecified items + audio_L_parameters make_audio_L_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional packet_time) + { + audio_L_parameters params; // rtpmap params.channel_count = (uint32_t)nmos::fields::channels(source).size(); params.bit_depth = nmos::fields::bit_depth(flow); const auto& sample_rate(flow.at(nmos::fields::sample_rate)); - params.sample_rate = nmos::rational(nmos::fields::numerator(sample_rate), nmos::fields::denominator(sample_rate)); + params.sample_rate = uint64_t(double(nmos::fields::numerator(sample_rate)) / double(nmos::fields::denominator(sample_rate)) + 0.5); - // format_specific_parameters + // fmtp const auto channel_symbols = boost::copy_range>(nmos::fields::channels(source) | boost::adaptors::transformed([](const web::json::value& channel) { @@ -186,55 +297,110 @@ namespace nmos })); params.channel_order = nmos::make_fmtp_channel_order(channel_symbols); - // ptime - params.packet_time = 1; + // hm, ST 2110-10 TSMODE and TSDELAY not indicated in IS-04 so omit these + + // ptime, e.g. 1 ms or 0.125 ms + // hm, there's a parameter constraint defined in the Capabilities register + // but there isn't (yet?) an equivalent Sender Attributes register entry + params.packet_time = packet_time ? *packet_time : 1; + + return params; + } - return{ sender.at(nmos::fields::label).as_string(), params, 97, media_stream_ids, details::make_ts_refclk(node, source, sender, ptp_domain) }; + // deprecated, use make_audio_L_parameters and then make_audio_L_sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters make_audio_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional payload_type, const std::vector& media_stream_ids, bst::optional ptp_domain, bst::optional packet_time) + { + auto params = make_audio_L_parameters(node, source, flow, sender, packet_time); + return{ nmos::fields::label(sender), params, payload_type ? *payload_type : details::payload_type_audio_default, !media_stream_ids.empty() ? media_stream_ids : details::make_media_stream_ids(sender), details::make_ts_refclk(node, source, sender, ptp_domain) }; } - static sdp_parameters make_data_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, const std::vector& media_stream_ids, bst::optional ptp_domain) + // Construct additional "video/smpte291" parameters from the IS-04 resources, using default values for unspecified items + video_smpte291_parameters make_video_smpte291_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional vpid_code, bst::optional tm) { - sdp_parameters::data_t params; + video_smpte291_parameters params; - // format_specific_parameters + // fmtp - // did_sdid map directly to flow_sdianc_data "DID_SDID" + // maps directly params.did_sdids = boost::copy_range>(nmos::fields::DID_SDID(flow).as_array() | boost::adaptors::transformed([](const web::json::value& did_sdid) { return nmos::parse_did_sdid(did_sdid); })); - // hm, no vpid_code in the flow + // hm, VPID_Code not currently indicated in IS-04 + params.vpid_code = vpid_code ? *vpid_code : 0; + + // grain_rate is optional in the flow, but if it's not there, it should be in the source + const auto& grain_rate = flow.has_field(nmos::fields::grain_rate) + ? nmos::fields::grain_rate(flow) + : source.has_field(nmos::fields::grain_rate) + ? nmos::fields::grain_rate(source) + : nmos::make_rational(0); + params.exactframerate = nmos::parse_rational(grain_rate); + + // hm, TM not indicated in IS-04 + if (tm) params.tm = *tm; - return{ sender.at(nmos::fields::label).as_string(), params, 100, media_stream_ids, details::make_ts_refclk(node, source, sender, ptp_domain) }; + params.ssn = params.tm.empty() ? sdp::smpte_standard_numbers::ST2110_40_2018 : sdp::smpte_standard_numbers::ST2110_40_2023; + + // hm, ST 2110-21 TROFF not indicated in IS-04 so omit this + // hm, ST 2110-10 TSMODE and TSDELAY not indicated in IS-04 so omit these + + return params; } - static sdp_parameters make_mux_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, const std::vector& media_stream_ids, bst::optional ptp_domain) + // deprecated, use make_video_smpte291_parameters and then make_video_smpte291_sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters make_data_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional payload_type, const std::vector& media_stream_ids, bst::optional ptp_domain, bst::optional vpid_code) + { + auto params = make_video_smpte291_parameters(node, source, flow, sender, vpid_code); + return{ nmos::fields::label(sender), params, payload_type ? *payload_type : details::payload_type_data_default, !media_stream_ids.empty() ? media_stream_ids : details::make_media_stream_ids(sender), details::make_ts_refclk(node, source, sender, ptp_domain) }; + } + + // Construct additional "video/SMPTE2022-6" parameters from the IS-04 resources, using default values for unspecified items + video_SMPTE2022_6_parameters make_video_SMPTE2022_6_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional tp) { sdp_parameters::mux_t params; + + // fmtp + // "Senders shall comply with either the Narrow Linear Senders (Type NL) requirements, or the Wide Senders (Type W) requirements." // See SMPTE ST 2022-8:2019 Section 6 Network Compatibility and Transmission Traffic Shape Models - params.tp = sdp::type_parameters::type_NL; + // hm, TP is equivalent to the new sender attribute, but for now, support optional override and default value + params.tp = tp + ? *tp + : sender.has_field(nmos::fields::st2110_21_sender_type) + ? sdp::type_parameter{ nmos::fields::st2110_21_sender_type(sender) } + : sdp::type_parameters::type_NL; + + // hm, ST 2110-21 TROFF not indicated in IS-04 so omit this + + return params; + } - // Payload type 98 is "High bit rate media transport / 27-MHz Clock" - // Payload type 99 is "High bit rate media transport FEC / 27-MHz Clock" - // See SMPTE ST 2022-6:2012 Section 6.3 RTP/UDP/IP Header - return{ sender.at(nmos::fields::label).as_string(), params, 98, media_stream_ids, details::make_ts_refclk(node, source, sender, ptp_domain) }; + // deprecated, use make_video_SMPTE2022_6_parameters and then make_video_SMPTE2022_6_sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters make_mux_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional payload_type, const std::vector& media_stream_ids, bst::optional ptp_domain, bst::optional tp) + { + auto params = make_video_SMPTE2022_6_parameters(node, source, flow, sender, tp); + return{ nmos::fields::label(sender), params, payload_type ? *payload_type : details::payload_type_mux_default, !media_stream_ids.empty() ? media_stream_ids : details::make_media_stream_ids(sender), details::make_ts_refclk(node, source, sender, ptp_domain) }; } + // Construct SDP parameters from the IS-04 resources for "video/raw", "audio/L", "video/smpte291" and "video/SMPTE2022-6" + // using default values for unspecified items + + // deprecated, use format-specific make__parameters and then make__sdp_parameters or equivalent overload of make_sdp_parameters sdp_parameters make_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, const std::vector& media_stream_ids, bst::optional ptp_domain) { const auto& format = nmos::fields::format(flow); if (nmos::formats::video.name == format) - return make_video_sdp_parameters(node, source, flow, sender, media_stream_ids, ptp_domain); + return make_video_sdp_parameters(node, source, flow, sender, {}, media_stream_ids, ptp_domain, {}); else if (nmos::formats::audio.name == format) - return make_audio_sdp_parameters(node, source, flow, sender, media_stream_ids, ptp_domain); + return make_audio_sdp_parameters(node, source, flow, sender, {}, media_stream_ids, ptp_domain, {}); else if (nmos::formats::data.name == format) - return make_data_sdp_parameters(node, source, flow, sender, media_stream_ids, ptp_domain); + return make_data_sdp_parameters(node, source, flow, sender, {}, media_stream_ids, ptp_domain, {}); else if (nmos::formats::mux.name == format) - return make_mux_sdp_parameters(node, source, flow, sender, media_stream_ids, ptp_domain); + return make_mux_sdp_parameters(node, source, flow, sender, {}, media_stream_ids, ptp_domain, {}); else - throw details::sdp_creation_error("unsuported media format"); + throw details::sdp_creation_error("unsupported media format"); } // deprecated, provided for backwards compatibility, because it may be necessary to also specify the PTP domain to generate an RFC 7273 'ts-refclk' attribute that meets the additional constraints of ST 2110-10 @@ -243,11 +409,53 @@ namespace nmos return make_sdp_parameters(node, source, flow, sender, media_stream_ids, bst::nullopt); } - static web::json::value make_session_description(const sdp_parameters& sdp_params, const web::json::value& transport_params, const web::json::value& ptime, const web::json::value& rtpmap, const web::json::value& fmtp) + namespace details + { + // " is the address of the machine from which the session was created." + // See https://tools.ietf.org/html/rfc4566#section-5.2 + const utility::string_t& get_origin_address(const web::json::value& transport_params) + { + // this doesn't need to be the source address of the data stream + // e.g. a host_address which was a management interface would be OK + // however, by convention, use the source_ip of the first leg for + // an SDP file created at a Sender and the interface_ip of the + // first leg for an SDP created at a Receiver + const auto& transport_param = transport_params.at(0); + const auto& interface_ip = nmos::fields::interface_ip(transport_param); + // a Receiver always has an interface_ip + if (!interface_ip.is_null()) return interface_ip.as_string(); + const auto& source_ip = nmos::fields::source_ip(transport_param); + // a Sender always has a source_ip + return source_ip.as_string(); + } + + // "If the session is multicast, the connection address will be an IP multicast group address. + // If the session is not multicast, then the connection address contains the unicast IP address of the + // expected [...] data sink." + // See https://tools.ietf.org/html/rfc4566#section-5.7 + const utility::string_t& get_connection_address(const web::json::value& transport_param) + { + const auto& multicast_ip = nmos::fields::multicast_ip(transport_param); + // a Receiver has a multicast_ip for multicast but not unicast + if (!multicast_ip.is_null()) return multicast_ip.as_string(); + const auto& interface_ip = nmos::fields::interface_ip(transport_param); + // a Receiver always has a (unicast) interface_ip + if (!interface_ip.is_null()) return interface_ip.as_string(); + // a Sender always has a destination_ip which may be multicast or unicast + const auto& destination_ip = nmos::fields::destination_ip(transport_param); + return destination_ip.as_string(); + } + } + + // Make a json representation of an SDP file, e.g. for sdp::make_session_description, from the specified parameters; explicitly specify whether 'source-filter' attributes are included to override the default behaviour + static web::json::value make_session_description(const sdp_parameters& sdp_params, const web::json::value& transport_params, const web::json::value& rtpmap, const web::json::value& fmtp, bst::optional source_filters) { using web::json::value; using web::json::value_of; - using web::json::array; + + // note that a media description is always created for each leg in the transport_params + // and the rtp_enabled status does not affect the leg's media description + // see https://github.com/AMWA-TV/is-05/issues/109#issuecomment-598721418 // check to ensure enough media_stream_ids for multi-leg transport_params if (transport_params.size() > 1 && transport_params.size() > sdp_params.group.media_stream_ids.size()) @@ -261,8 +469,7 @@ namespace nmos const bool keep_order = true; - const auto& destination_ip = nmos::fields::destination_ip(transport_params.at(0)).as_string(); - const auto address_type_multicast = details::get_address_type_multicast(destination_ip); + const auto& origin_address = details::get_origin_address(transport_params); auto session_description = value_of({ // Protocol Version @@ -276,8 +483,8 @@ namespace nmos { sdp::fields::session_id, sdp_params.origin.session_id }, { sdp::fields::session_version, sdp_params.origin.session_version }, { sdp::fields::network_type, sdp::network_types::internet.name }, - { sdp::fields::address_type, address_type_multicast.first.name }, - { sdp::fields::unicast_address, nmos::fields::source_ip(transport_params.at(0)) } + { sdp::fields::address_type, details::get_address_type_multicast(origin_address).first.name }, + { sdp::fields::unicast_address, origin_address } }, keep_order) }, // Session Name @@ -293,27 +500,34 @@ namespace nmos { sdp::fields::stop_time, sdp_params.timing.stop_time } }) } }, keep_order) - }) } + }) }, + // Attributes + // See https://tools.ietf.org/html/rfc4566#section-5.13 + { sdp::fields::attributes, value::array() }, + + // Media Descriptions + // See https://tools.ietf.org/html/rfc4566#section-5 + { sdp::fields::media_descriptions, value::array() } }, keep_order); + auto& session_attributes = session_description.at(sdp::fields::attributes); + auto& media_descriptions = session_description.at(sdp::fields::media_descriptions); // group & mid attributes // see https://tools.ietf.org/html/rfc5888 auto mids = value::array(); - // build media_descriptions with given media_stream_ids - // Media Descriptions - // See https://tools.ietf.org/html/rfc4566#section-5 - auto media_descriptions = value::array(); size_t leg = 0; for (const auto& transport_param : transport_params.as_array()) { + const auto& connection_address = details::get_connection_address(transport_param); + const auto& address_type_multicast = details::get_address_type_multicast(connection_address); + const auto& ts_refclk = sdp_params.ts_refclk.size() > leg ? sdp_params.ts_refclk[leg] : ts_refclk_default; - // build media_description auto media_description = value_of({ // Media // See https://tools.ietf.org/html/rfc4566#section-5.14 @@ -331,20 +545,38 @@ namespace nmos { sdp::fields::network_type, sdp::network_types::internet.name }, { sdp::fields::address_type, address_type_multicast.first.name }, { sdp::fields::connection_address, sdp::address_types::IP4 == address_type_multicast.first && address_type_multicast.second - ? nmos::fields::destination_ip(transport_param).as_string() + U("/") + utility::ostringstreamed(sdp_params.connection_data.ttl) - : nmos::fields::destination_ip(transport_param).as_string() } + ? connection_address + U("/") + utility::ostringstreamed(sdp_params.connection_data.ttl) + : connection_address } + }, keep_order) + }) }, + + // Bandwidth + // See https://tools.ietf.org/html/rfc4566#section-5.8 + { !sdp_params.bandwidth.bandwidth_type.empty() ? sdp::fields::bandwidth_information.key : U(""), value_of({ + value_of({ + { sdp::fields::bandwidth_type, sdp_params.bandwidth.bandwidth_type.name }, + { sdp::fields::bandwidth, sdp_params.bandwidth.bandwidth } }, keep_order) }) }, // Attributes // See https://tools.ietf.org/html/rfc4566#section-5.13 - { sdp::fields::attributes, value_of({ - // a=ts-refclk:ptp=:[:] - // a=ts-refclk:ptp=:traceable - // See https://tools.ietf.org/html/rfc7273 - // a=ts-refclk:localmac= - // See SMPTE ST 2110-10:2017 Professional Media Over Managed IP Networks: System Timing and Definitions, Section 8.2 Reference Clock - value_of({ + { sdp::fields::attributes, value::array() } + + }, keep_order); + + auto& media_attributes = media_description.at(sdp::fields::attributes); + + // insert ts-refclk if specified + if (ts_refclk.clock_source != sdp::ts_refclk_source{}) + { + // a=ts-refclk:ptp=:[:] + // a=ts-refclk:ptp=:traceable + // See https://tools.ietf.org/html/rfc7273 + // a=ts-refclk:localmac= + // See SMPTE ST 2110-10:2017 Professional Media Over Managed IP Networks: System Timing and Definitions, Section 8.2 Reference Clock + web::json::push_back( + media_attributes, value_of({ { sdp::fields::name, sdp::attributes::ts_refclk }, { sdp::fields::value, sdp::ts_refclk_sources::ptp == ts_refclk.clock_source ? ts_refclk.ptp_server.empty() ? value_of({ { sdp::fields::clock_source, sdp::ts_refclk_sources::ptp.name }, @@ -358,308 +590,283 @@ namespace nmos { sdp::fields::clock_source, sdp::ts_refclk_sources::local_mac.name }, { sdp::fields::mac_address, ts_refclk.mac_address } }, keep_order) : value::null() } - }, keep_order), + }, keep_order) + ); + } - // a=mediaclk:[id= ][=] - // See https://tools.ietf.org/html/rfc7273#section-5 - value_of({ + // insert mediaclk if specified + if (sdp_params.mediaclk.clock_source != sdp::mediaclk_source{}) + { + // a=mediaclk:[id= ][=] + // See https://tools.ietf.org/html/rfc7273#section-5 + web::json::push_back( + media_attributes, value_of({ { sdp::fields::name, sdp::attributes::mediaclk }, - { sdp::fields::value, sdp_params.mediaclk.clock_source.name + U("=") + sdp_params.mediaclk.clock_parameters } + { sdp::fields::value, !sdp_params.mediaclk.clock_parameters.empty() + ? sdp_params.mediaclk.clock_source.name + U("=") + sdp_params.mediaclk.clock_parameters + : sdp_params.mediaclk.clock_source.name } }, keep_order) + ); + } - }) } //attribues - - }, keep_order); //media_description - - // insert source-filter if multicast - if (address_type_multicast.second) + // insert source-filter if source address is specified, depending on source_filters + // for now, when source_filters does not contain an explicit value, the default is to include the source-filter attribute + // another choice would be to do so only for source-specific multicast addresses (232.0.0.0-232.255.255.255) + const auto& source_ip = nmos::fields::source_ip(transport_param); + if (!source_ip.is_null() && (!source_filters || *source_filters)) { - auto& attributes = media_description.at(sdp::fields::attributes); // a=source-filter: // See https://tools.ietf.org/html/rfc4570 web::json::push_back( - attributes, value_of({ + media_attributes, value_of({ { sdp::fields::name, sdp::attributes::source_filter }, { sdp::fields::value, value_of({ { sdp::fields::filter_mode, sdp::filter_modes::incl.name }, { sdp::fields::network_type, sdp::network_types::internet.name }, { sdp::fields::address_types, address_type_multicast.first.name }, - { sdp::fields::destination_address, transport_param.at(nmos::fields::destination_ip) }, - { sdp::fields::source_addresses, value_of({ transport_param.at(nmos::fields::source_ip) }) } + { sdp::fields::destination_address, connection_address }, + { sdp::fields::source_addresses, value_of({ source_ip }) } }, keep_order) } }, keep_order) ); } - // insert ptime if set - // a=ptime: - // See https://tools.ietf.org/html/rfc4566#section-6 - if (!ptime.is_null()) + if (0 != sdp_params.packet_time) { - auto& attributes = media_description.at(sdp::fields::attributes); - web::json::push_back(attributes, ptime); + // a=ptime: + // See https://tools.ietf.org/html/rfc4566#section-6 + web::json::push_back( + media_attributes, value_of({ + { sdp::fields::name, sdp::attributes::ptime }, + { sdp::fields::value, sdp_params.packet_time } + }, keep_order) + ); } - // insert rtpmap if set - // a=rtpmap: /[/] - // See https://tools.ietf.org/html/rfc4566#section-6 + if (0 != sdp_params.max_packet_time) + { + // a=maxptime: + // See https://tools.ietf.org/html/rfc4566#section-6 + web::json::push_back( + media_attributes, value_of({ + { sdp::fields::name, sdp::attributes::maxptime }, + { sdp::fields::value, sdp_params.max_packet_time } + }, keep_order) + ); + } + + // insert rtpmap if specified if (!rtpmap.is_null()) { - auto& attributes = media_description.at(sdp::fields::attributes); - web::json::push_back(attributes, rtpmap); + // a=rtpmap: /[/] + // See https://tools.ietf.org/html/rfc4566#section-6 + web::json::push_back(media_attributes, rtpmap); + } + + // insert framerate if specified + if (0 != sdp_params.framerate) + { + // a=framerate: + // See https://tools.ietf.org/html/rfc4566#section-6 + web::json::push_back( + media_attributes, value_of({ + { sdp::fields::name, sdp::attributes::framerate }, + { sdp::fields::value, sdp_params.framerate } + }, keep_order) + ); } - // insert fmtp if set - // a=fmtp: - // See https://tools.ietf.org/html/rfc4566#section-6 + // insert fmtp if specified if (!fmtp.is_null()) { - auto& attributes = media_description.at(sdp::fields::attributes); - web::json::push_back(attributes, fmtp); + // a=fmtp: + // See https://tools.ietf.org/html/rfc4566#section-6 + web::json::push_back(media_attributes, fmtp); } - // insert "media stream identification" if there are more than 1 leg - // a=mid: - // See https://tools.ietf.org/html/rfc5888 + // insert "media stream identification" if there is more than 1 leg if (transport_params.size() > 1) { + // a=mid: + // See https://tools.ietf.org/html/rfc5888 const auto& mid = sdp_params.group.media_stream_ids[leg]; // build up mids based on group::media_stream_ids web::json::push_back(mids, mid); - auto& attributes = media_description.at(sdp::fields::attributes); web::json::push_back( - attributes, value_of({ + media_attributes, value_of({ { sdp::fields::name, sdp::attributes::mid }, { sdp::fields::value, mid } }, keep_order) ); } - web::json::push_back(media_descriptions, media_description); + web::json::push_back(media_descriptions, std::move(media_description)); ++leg; } - // add group attribute if there are more than 1 leg - // a=group:[ ]* - // See https://tools.ietf.org/html/rfc5888 + // add group attribute if there is more than 1 leg if (mids.size() > 1) { - session_description[sdp::fields::attributes] = value_of({ - web::json::value_of({ + // a=group:[ ]* + // See https://tools.ietf.org/html/rfc5888 + web::json::push_back( + session_attributes, value_of({ { sdp::fields::name, sdp::attributes::group }, { sdp::fields::value, web::json::value_of({ { sdp::fields::semantics, sdp_params.group.semantics.name }, - { sdp::fields::mids, mids } + { sdp::fields::mids, std::move(mids) } }, keep_order) }, }, keep_order) - }); + ); } - session_description[sdp::fields::media_descriptions] = media_descriptions; - return session_description; } - static web::json::value make_video_session_description(const sdp_parameters& sdp_params, const web::json::value& transport_params) + static web::json::value make_rtpmap(const sdp_parameters& sdp_params) { using web::json::value_of; const bool keep_order = true; - // a=rtpmap: /[/] - // See https://tools.ietf.org/html/rfc4566#section-6 - const auto rtpmap = value_of({ + return value_of({ { sdp::fields::name, sdp::attributes::rtpmap }, - { sdp::fields::value, web::json::value_of({ + { sdp::fields::value, value_of({ { sdp::fields::payload_type, sdp_params.rtpmap.payload_type }, { sdp::fields::encoding_name, sdp_params.rtpmap.encoding_name }, - { sdp::fields::clock_rate, sdp_params.rtpmap.clock_rate } - }, keep_order) } - }, keep_order); - - // a=fmtp: - // for simplicity, following the order of parameters given in VSF TR-05:2017 - // See https://tools.ietf.org/html/rfc4566#section-6 - // and http://www.videoservicesforum.org/download/technical_recommendations/VSF_TR-05_2018-06-23.pdf - // and comments regarding the fmtp attribute parameters in get_session_description_sdp_parameters - auto format_specific_parameters = value_of({ - sdp::named_value(sdp::fields::width, utility::ostringstreamed(sdp_params.video.width)), - sdp::named_value(sdp::fields::height, utility::ostringstreamed(sdp_params.video.height)), - sdp::named_value(sdp::fields::exactframerate, sdp_params.video.exactframerate.denominator() != 1 - ? utility::ostringstreamed(sdp_params.video.exactframerate.numerator()) + U("/") + utility::ostringstreamed(sdp_params.video.exactframerate.denominator()) - : utility::ostringstreamed(sdp_params.video.exactframerate.numerator())) - }); - if (sdp_params.video.interlace) web::json::push_back(format_specific_parameters, sdp::named_value(sdp::fields::interlace)); - if (sdp_params.video.segmented) web::json::push_back(format_specific_parameters, sdp::named_value(sdp::fields::segmented)); - web::json::push_back(format_specific_parameters, sdp::named_value(sdp::fields::sampling, sdp_params.video.sampling.name)); - web::json::push_back(format_specific_parameters, sdp::named_value(sdp::fields::depth, utility::ostringstreamed(sdp_params.video.depth))); - web::json::push_back(format_specific_parameters, sdp::named_value(sdp::fields::colorimetry, sdp_params.video.colorimetry.name)); - if (!sdp_params.video.tcs.name.empty()) web::json::push_back(format_specific_parameters, sdp::named_value(sdp::fields::transfer_characteristic_system, sdp_params.video.tcs.name)); - web::json::push_back(format_specific_parameters, sdp::named_value(sdp::fields::packing_mode, sdp::packing_modes::general.name)); // or block... - web::json::push_back(format_specific_parameters, sdp::named_value(sdp::fields::smpte_standard_number, sdp::smpte_standard_numbers::ST2110_20_2017.name)); - if (!sdp_params.video.tp.name.empty()) web::json::push_back(format_specific_parameters, sdp::named_value(sdp::fields::type_parameter, sdp_params.video.tp.name)); - - const auto fmtp = web::json::value_of({ - { sdp::fields::name, sdp::attributes::fmtp }, - { sdp::fields::value, web::json::value_of({ - { sdp::fields::format, utility::ostringstreamed(sdp_params.rtpmap.payload_type) }, - { sdp::fields::format_specific_parameters, format_specific_parameters } + { sdp::fields::clock_rate, sdp_params.rtpmap.clock_rate }, + { sdp_params.rtpmap.encoding_parameters > 0 ? sdp::fields::encoding_parameters.key : U(""), sdp_params.rtpmap.encoding_parameters } }, keep_order) } }, keep_order); - - return make_session_description(sdp_params, transport_params, {}, rtpmap, fmtp); } - static web::json::value make_audio_session_description(const sdp_parameters& sdp_params, const web::json::value& transport_params) + static web::json::value make_fmtp(const sdp_parameters& sdp_params) { using web::json::value; + using web::json::value_from_elements; using web::json::value_of; const bool keep_order = true; - // a=ptime: - // See https://tools.ietf.org/html/rfc4566#section-6 - const auto ptime = value_of({ - { sdp::fields::name, sdp::attributes::ptime }, - { sdp::fields::value, sdp_params.audio.packet_time } + return sdp_params.fmtp.empty() ? value::null() : value_of({ + { sdp::fields::name, sdp::attributes::fmtp }, + { sdp::fields::value, web::json::value_of({ + { sdp::fields::format, utility::ostringstreamed(sdp_params.rtpmap.payload_type) }, + { sdp::fields::format_specific_parameters, value_from_elements(sdp_params.fmtp | boost::adaptors::transformed([&](const sdp_parameters::fmtp_t::value_type& param) + { + return sdp::named_value(param.first, param.second, keep_order); + })) } + }, keep_order) } }, keep_order); + } + // Construct SDP parameters for "video/raw", with sensible defaults for unspecified fields + sdp_parameters make_video_raw_sdp_parameters(const utility::string_t& session_name, const video_raw_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids, const std::vector& ts_refclk) + { // a=rtpmap: /[/] // See https://tools.ietf.org/html/rfc4566#section-6 - const auto rtpmap = value_of({ - { sdp::fields::name, sdp::attributes::rtpmap }, - { sdp::fields::value, value_of({ - { sdp::fields::payload_type, sdp_params.rtpmap.payload_type }, - { sdp::fields::encoding_name, sdp_params.rtpmap.encoding_name }, - { sdp::fields::clock_rate, sdp_params.rtpmap.clock_rate }, - { sdp::fields::encoding_parameters, sdp_params.audio.channel_count } - }, keep_order) } - }, keep_order); + sdp_parameters::rtpmap_t rtpmap = { payload_type, U("raw"), 90000 }; // a=fmtp: + // for simplicity, following the order of parameters given in VSF TR-05:2017 // See https://tools.ietf.org/html/rfc4566#section-6 - const auto format_specific_parameters = sdp_params.audio.channel_order.empty() ? value::array() : value_of({ - sdp::named_value(sdp::fields::channel_order, sdp_params.audio.channel_order) - }); - const auto fmtp = value_of({ - { sdp::fields::name, sdp::attributes::fmtp }, - { sdp::fields::value, value_of({ - { sdp::fields::format, utility::ostringstreamed(sdp_params.rtpmap.payload_type) }, - { sdp::fields::format_specific_parameters, format_specific_parameters } - }, keep_order) } - }, keep_order); - - return make_session_description(sdp_params, transport_params, ptime, rtpmap, fmtp); + // and http://www.videoservicesforum.org/download/technical_recommendations/VSF_TR-05_2018-06-23.pdf + // and comments regarding the fmtp attribute parameters in get_session_description_sdp_parameters + sdp_parameters::fmtp_t fmtp = { + { sdp::fields::width, utility::ostringstreamed(params.width) }, + { sdp::fields::height, utility::ostringstreamed(params.height) }, + { sdp::fields::exactframerate, nmos::details::make_exactframerate(params.exactframerate) } + }; + if (params.interlace) fmtp.push_back({ sdp::fields::interlace, {} }); + if (params.segmented) fmtp.push_back({ sdp::fields::segmented, {} }); + fmtp.push_back({ sdp::fields::sampling, params.sampling.name }); + fmtp.push_back({ sdp::fields::depth, utility::ostringstreamed(params.depth) }); + fmtp.push_back({ sdp::fields::colorimetry, params.colorimetry.name }); + if (!params.tcs.empty()) fmtp.push_back({ sdp::fields::transfer_characteristic_system, params.tcs.name }); + if (!params.pm.empty()) fmtp.push_back({ sdp::fields::packing_mode, params.pm.name }); + if (!params.ssn.empty()) fmtp.push_back({ sdp::fields::smpte_standard_number, params.ssn.name }); + if (!params.tp.empty()) fmtp.push_back({ sdp::fields::type_parameter, params.tp.name }); + + // additional parameters introduced by SMPTE specs since then... + if (!params.range.empty()) fmtp.push_back({ sdp::fields::range, params.range.name }); + if (0 != params.par) fmtp.push_back({ sdp::fields::pixel_aspect_ratio, nmos::details::make_pixel_aspect_ratio(params.par) }); + if (params.troff) fmtp.push_back({ sdp::fields::TROFF, utility::ostringstreamed(*params.troff) }); + if (0 != params.cmax) fmtp.push_back({ sdp::fields::CMAX, utility::ostringstreamed(params.cmax) }); + if (0 != params.maxudp) fmtp.push_back({ sdp::fields::max_udp_packet_size, utility::ostringstreamed(params.maxudp) }); + if (!params.tsmode.empty()) fmtp.push_back({ sdp::fields::timestamp_mode, params.tsmode.name }); + if (params.tsdelay) fmtp.push_back({ sdp::fields::timestamp_delay, utility::ostringstreamed(*params.tsdelay) }); + + return{ session_name, sdp::media_types::video, rtpmap, fmtp, {}, {}, {}, {}, media_stream_ids, ts_refclk }; } - static web::json::value make_data_format_specific_parameters(const sdp_parameters::data_t& data_params) + // Construct SDP parameters for "audio/L", with sensible defaults for unspecified fields + sdp_parameters make_audio_L_sdp_parameters(const utility::string_t& session_name, const audio_L_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids, const std::vector& ts_refclk) { - auto result = web::json::value_from_elements(data_params.did_sdids | boost::adaptors::transformed([](const nmos::did_sdid& did_sdid) - { - return sdp::named_value(sdp::fields::DID_SDID, make_fmtp_did_sdid(did_sdid)); - })); + // a=rtpmap: /[/] + // See https://tools.ietf.org/html/rfc4566#section-6 + sdp_parameters::rtpmap_t rtpmap = { payload_type, U("L") + utility::ostringstreamed(params.bit_depth), params.sample_rate, params.channel_count }; - if (0 != data_params.vpid_code) - { - web::json::push_back(result, sdp::named_value(sdp::fields::VPID_Code, utility::ostringstreamed(data_params.vpid_code))); - } + // a=fmtp: + // See https://tools.ietf.org/html/rfc4566#section-6 + sdp_parameters::fmtp_t fmtp = {}; + if (!params.channel_order.empty()) fmtp.push_back({ sdp::fields::channel_order, params.channel_order }); + if (!params.tsmode.empty()) fmtp.push_back({ sdp::fields::timestamp_mode, params.tsmode.name }); + if (params.tsdelay) fmtp.push_back({ sdp::fields::timestamp_delay, utility::ostringstreamed(*params.tsdelay) }); - return result; + return{ session_name, sdp::media_types::audio, rtpmap, fmtp, {}, params.packet_time, {}, {}, media_stream_ids, ts_refclk }; } - static web::json::value make_data_session_description(const sdp_parameters& sdp_params, const web::json::value& transport_params) + // Construct SDP parameters for "video/smpte291", with sensible defaults for unspecified fields + sdp_parameters make_video_smpte291_sdp_parameters(const utility::string_t& session_name, const video_smpte291_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids, const std::vector& ts_refclk) { - using web::json::value; - using web::json::value_of; - - const bool keep_order = true; - // a=rtpmap: /[/] // See https://tools.ietf.org/html/rfc4566#section-6 - const auto rtpmap = value_of({ - { sdp::fields::name, sdp::attributes::rtpmap }, - { sdp::fields::value, value_of({ - { sdp::fields::payload_type, sdp_params.rtpmap.payload_type }, - { sdp::fields::encoding_name, sdp_params.rtpmap.encoding_name }, - { sdp::fields::clock_rate, sdp_params.rtpmap.clock_rate } - }, keep_order) } - }, keep_order); + sdp_parameters::rtpmap_t rtpmap = { payload_type, U("smpte291"), 90000 }; // a=fmtp: // See https://tools.ietf.org/html/rfc4566#section-6 - const auto fmtp = sdp_params.data.did_sdids.empty() && 0 == sdp_params.data.vpid_code ? value::null() : value_of({ - { sdp::fields::name, sdp::attributes::fmtp }, - { sdp::fields::value, value_of({ - { sdp::fields::format, utility::ostringstreamed(sdp_params.rtpmap.payload_type) }, - { sdp::fields::format_specific_parameters, make_data_format_specific_parameters(sdp_params.data) } - }, keep_order) } - }, keep_order); - - return make_session_description(sdp_params, transport_params, {}, rtpmap, fmtp); + sdp_parameters::fmtp_t fmtp = boost::copy_range(params.did_sdids | boost::adaptors::transformed([](const nmos::did_sdid& did_sdid) + { + return sdp_parameters::fmtp_t::value_type{ sdp::fields::DID_SDID, make_fmtp_did_sdid(did_sdid) }; + })); + if (0 != params.vpid_code) fmtp.push_back({ sdp::fields::VPID_Code, utility::ostringstreamed((uint32_t)params.vpid_code) }); + if (0 != params.exactframerate) fmtp.push_back({ sdp::fields::exactframerate, nmos::details::make_exactframerate(params.exactframerate) }); + if (!params.tm.empty()) fmtp.push_back({ sdp::fields::TM, params.tm.name }); + if (!params.ssn.empty()) fmtp.push_back({ sdp::fields::smpte_standard_number, params.ssn.name }); + if (params.troff) fmtp.push_back({ sdp::fields::TROFF, utility::ostringstreamed(*params.troff) }); + if (!params.tsmode.empty()) fmtp.push_back({ sdp::fields::timestamp_mode, params.tsmode.name }); + if (params.tsdelay) fmtp.push_back({ sdp::fields::timestamp_delay, utility::ostringstreamed(*params.tsdelay) }); + + return{ session_name, sdp::media_types::video, rtpmap, fmtp, {}, {}, {}, {}, media_stream_ids, ts_refclk }; } - static web::json::value make_mux_session_description(const sdp_parameters& sdp_params, const web::json::value& transport_params) + // Construct SDP parameters for "video/SMPTE2022-6", with sensible defaults for unspecified fields + sdp_parameters make_video_SMPTE2022_6_sdp_parameters(const utility::string_t& session_name, const video_SMPTE2022_6_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids, const std::vector& ts_refclk) { - using web::json::value; - using web::json::value_of; - - const bool keep_order = true; - // a=rtpmap: /[/] // See https://tools.ietf.org/html/rfc4566#section-6 - const auto rtpmap = value_of({ - { sdp::fields::name, sdp::attributes::rtpmap }, - { sdp::fields::value, value_of({ - { sdp::fields::payload_type, sdp_params.rtpmap.payload_type }, - { sdp::fields::encoding_name, sdp_params.rtpmap.encoding_name }, - { sdp::fields::clock_rate, sdp_params.rtpmap.clock_rate } - }, keep_order) } - }, keep_order); - - auto format_specific_parameters = value::array(); - if (!sdp_params.mux.tp.name.empty()) web::json::push_back(format_specific_parameters, sdp::named_value(sdp::fields::type_parameter, sdp_params.mux.tp.name)); + sdp_parameters::rtpmap_t rtpmap = { payload_type, U("SMPTE2022-6"), 27000000 }; // a=fmtp: // See https://tools.ietf.org/html/rfc4566#section-6 - const auto fmtp = value_of({ - { sdp::fields::name, sdp::attributes::fmtp }, - { sdp::fields::value, value_of({ - { sdp::fields::format, utility::ostringstreamed(sdp_params.rtpmap.payload_type) }, - { sdp::fields::format_specific_parameters, format_specific_parameters } - }, keep_order) } - }, keep_order); + sdp_parameters::fmtp_t fmtp = {}; + if (!params.tp.empty()) fmtp.push_back({ sdp::fields::type_parameter, params.tp.name }); + if (params.troff) fmtp.push_back({ sdp::fields::TROFF, utility::ostringstreamed(*params.troff) }); - return make_session_description(sdp_params, transport_params, {}, rtpmap, fmtp); + return{ session_name, sdp::media_types::video, rtpmap, fmtp, {}, {}, {}, {}, media_stream_ids, ts_refclk }; } - namespace details + media_type get_media_type(const sdp_parameters& sdp_params) { - nmos::format get_format(const sdp_parameters& sdp_params) - { - if (sdp::media_types::video == sdp_params.media_type && U("raw") == sdp_params.rtpmap.encoding_name) return nmos::formats::video; - if (sdp::media_types::audio == sdp_params.media_type) return nmos::formats::audio; - if (sdp::media_types::video == sdp_params.media_type && U("smpte291") == sdp_params.rtpmap.encoding_name) return nmos::formats::data; - if (sdp::media_types::video == sdp_params.media_type && U("SMPTE2022-6") == sdp_params.rtpmap.encoding_name) return nmos::formats::mux; - return{}; - } - - nmos::media_type get_media_type(const sdp_parameters& sdp_params) - { - return nmos::media_type{ sdp_params.media_type.name + U("/") + sdp_params.rtpmap.encoding_name }; - } + return media_type{ sdp_params.media_type.name + U("/") + sdp_params.rtpmap.encoding_name }; } - web::json::value make_session_description(const sdp_parameters& sdp_params, const web::json::value& transport_params) + web::json::value make_session_description(const sdp_parameters& sdp_params, const web::json::value& transport_params, bst::optional source_filters) { - const auto format = details::get_format(sdp_params); - if (nmos::formats::video == format) return make_video_session_description(sdp_params, transport_params); - if (nmos::formats::audio == format) return make_audio_session_description(sdp_params, transport_params); - if (nmos::formats::data == format) return make_data_session_description(sdp_params, transport_params); - if (nmos::formats::mux == format) return make_mux_session_description(sdp_params, transport_params); - throw details::sdp_creation_error("unsupported ST2110 media"); + return make_session_description(sdp_params, transport_params, make_rtpmap(sdp_params), make_fmtp(sdp_params), source_filters); } namespace details @@ -719,9 +926,6 @@ namespace nmos if (details::get_address_type_multicast(address).second) { - // any-source multicast, unless there's a source-filter - params[nmos::fields::source_ip] = value::null(); - params[nmos::fields::multicast_ip] = value::string(address); params[nmos::fields::interface_ip] = value::string(U("auto")); } @@ -733,17 +937,16 @@ namespace nmos } } - // Get transport parameters from the parsed SDP file + // Get IS-05 transport parameters from the json representation of an SDP file, e.g. from sdp::parse_session_description web::json::value get_session_description_transport_params(const web::json::value& session_description) { using web::json::value; - using web::json::value_of; web::json::value transport_params; // There isn't much of a specification for interpreting SDP files and updating the // equivalent transport parameters, just some examples... - // See https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/docs/4.1.%20Behaviour%20-%20RTP%20Transport%20Type.md#interpretation-of-sdp-files + // See https://specs.amwa.tv/is-05/releases/v1.0.0/docs/4.1._Behaviour_-_RTP_Transport_Type.html#interpretation-of-sdp-files // For now, this function should handle the following cases identified in the documentation: // * Unicast @@ -763,7 +966,10 @@ namespace nmos { web::json::value params; - params[nmos::fields::source_ip] = value::string(sdp::fields::unicast_address(sdp::fields::origin(session_description))); + // source_ip is null when there is no source-filter, indicating that "the source IP address + // has not been configured in unicast mode, or the Receiver is in any-source multicast mode" + // see https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/receiver_transport_params_rtp.html + params[nmos::fields::source_ip] = value::null(); // session connection data is the default for each media description auto& session_connection_data = sdp::fields::connection_data(session_description); @@ -811,6 +1017,9 @@ namespace nmos { auto& ma = media_attributes.as_array(); + // hmm, this code assumes that is 'incl' and ought to check that , and + // match the connection address, and fall back to any "session-level" source-filter values if they don't match + auto source_filter = sdp::find_name(ma, sdp::attributes::source_filter); if (ma.end() != source_filter) { @@ -848,18 +1057,10 @@ namespace nmos return transport_params; } - namespace details - { - std::runtime_error sdp_processing_error(const std::string& message) - { - return std::runtime_error{ "sdp processing error - " + message }; - } - } - + // Get the additional (non-transport) SDP parameters from the json representation of an SDP file, e.g. from sdp::parse_session_description sdp_parameters get_session_description_sdp_parameters(const web::json::value& sdp) { using web::json::value; - using web::json::value_of; sdp_parameters sdp_params; @@ -907,19 +1108,16 @@ namespace nmos // Group // a=group:[ ]* // See https://tools.ietf.org/html/rfc5888 - auto sdp_attributes = sdp::fields::attributes(sdp).as_array(); - if (sdp_attributes.size()) + auto& session_attributes = sdp::fields::attributes(sdp).as_array(); + auto group = sdp::find_name(session_attributes, sdp::attributes::group); + if (session_attributes.end() != group) { - auto group = sdp::find_name(sdp_attributes, sdp::attributes::group); - if (sdp_attributes.end() != group) - { - const auto& value = sdp::fields::value(*group); + const auto& value = sdp::fields::value(*group); - sdp_params.group.semantics = sdp::group_semantics_type{ sdp::fields::semantics(value) }; - for (const auto& mid : sdp::fields::mids(value)) - { - sdp_params.group.media_stream_ids.push_back(mid.as_string()); - } + sdp_params.group.semantics = sdp::group_semantics_type{ sdp::fields::semantics(value) }; + for (const auto& mid : sdp::fields::mids(value)) + { + sdp_params.group.media_stream_ids.push_back(mid.as_string()); } } @@ -929,16 +1127,21 @@ namespace nmos // ts-refclk attributes // See https://tools.ietf.org/html/rfc7273 - sdp_params.ts_refclk = boost::copy_range>(media_descriptions.as_array() | boost::adaptors::filtered([](const value& media_description) - { - auto& attributes = sdp::fields::attributes(media_description).as_array(); - auto ts_refclk = sdp::find_name(attributes, sdp::attributes::ts_refclk); - return attributes.end() != ts_refclk; - }) | boost::adaptors::transformed([](const value& media_description) + sdp_params.ts_refclk = boost::copy_range>(media_descriptions.as_array() | boost::adaptors::transformed([&sdp](const value& media_description) -> sdp_parameters::ts_refclk_t { - auto& attributes = sdp::fields::attributes(media_description).as_array(); - auto ts_refclk = sdp::find_name(attributes, sdp::attributes::ts_refclk); - + auto& media_attributes = sdp::fields::attributes(media_description).as_array(); + auto ts_refclk = sdp::find_name(media_attributes, sdp::attributes::ts_refclk); + // default to the "session-level" value if no "media-level" value + if (media_attributes.end() == ts_refclk) + { + auto& session_attributes = sdp::fields::attributes(sdp).as_array(); + ts_refclk = sdp::find_name(session_attributes, sdp::attributes::ts_refclk); + if (session_attributes.end() == ts_refclk) + { + // indicate not found by default-constructed value + return{}; + } + } const auto& value = sdp::fields::value(*ts_refclk); sdp::ts_refclk_source clock_source{ sdp::fields::clock_source(value) }; if (sdp::ts_refclk_sources::ptp == clock_source) @@ -972,6 +1175,23 @@ namespace nmos } } + // Bandwidth + // See https://tools.ietf.org/html/rfc4566#section-5.8 + { + const auto& media_bandwidth_information = sdp::fields::bandwidth_information(media_description); + auto bandwidths = boost::copy_range>(media_bandwidth_information.as_array() | boost::adaptors::transformed([](const web::json::value& bandwidth) + { + return sdp_parameters::bandwidth_t{ sdp::bandwidth_type{ sdp::fields::bandwidth_type(bandwidth) }, sdp::fields::bandwidth(bandwidth) }; + })); + if (!bandwidths.empty()) + { + // multiple "media-level" bandwidth lines seem to be rarely used + // e.g. RFC 3556 for RTP Control Protocol (RTCP) Bandwidth + // see https://tools.ietf.org/html/rfc3556 + sdp_params.bandwidth = bandwidths.front(); + } + } + // Media // See https://tools.ietf.org/html/rfc4566#section-5.14 const auto& media = sdp::fields::media(media_description); @@ -979,274 +1199,429 @@ namespace nmos sdp_params.protocol = sdp::protocol{ sdp::fields::protocol(media) }; // media description attributes - auto& attributes = sdp::fields::attributes(media_description).as_array(); + const auto& media_attributes = sdp::fields::attributes(media_description).as_array(); // mediaclk attribute // See https://tools.ietf.org/html/rfc7273 - auto mediaclk = sdp::find_name(attributes, sdp::attributes::mediaclk); - if (attributes.end() != mediaclk) + sdp_params.mediaclk = [&sdp](const value& media_description) -> sdp_parameters::mediaclk_t { + auto& media_attributes = sdp::fields::attributes(media_description).as_array(); + auto mediaclk = sdp::find_name(media_attributes, sdp::attributes::mediaclk); + // default to the "session-level" value if no "media-level" value + if (media_attributes.end() == mediaclk) + { + auto& session_attributes = sdp::fields::attributes(sdp).as_array(); + mediaclk = sdp::find_name(session_attributes, sdp::attributes::mediaclk); + if (session_attributes.end() == mediaclk) + { + // indicate not found by default-constructed value + return{}; + } + } const auto& value = sdp::fields::value(*mediaclk).as_string(); const auto eq = value.find(U('=')); - sdp_params.mediaclk = { sdp::media_clock_source{ value.substr(0, eq) }, utility::string_t::npos != eq ? value.substr(eq + 1) : utility::string_t{} }; - } + return{ sdp::media_clock_source{ value.substr(0, eq) }, utility::string_t::npos != eq ? value.substr(eq + 1) : utility::string_t{} }; + }(media_description); // rtpmap attribute // See https://tools.ietf.org/html/rfc4566#section-6 - auto rtpmap = sdp::find_name(attributes, sdp::attributes::rtpmap); - if (attributes.end() == rtpmap) throw details::sdp_processing_error("missing attribute: rtpmap"); + auto rtpmap = sdp::find_name(media_attributes, sdp::attributes::rtpmap); + if (media_attributes.end() == rtpmap) throw details::sdp_processing_error("missing attribute: rtpmap"); const auto& rtpmap_value = sdp::fields::value(*rtpmap); sdp_params.rtpmap.encoding_name = sdp::fields::encoding_name(rtpmap_value); sdp_params.rtpmap.payload_type = sdp::fields::payload_type(rtpmap_value); sdp_params.rtpmap.clock_rate = sdp::fields::clock_rate(rtpmap_value); + sdp_params.rtpmap.encoding_parameters = sdp::fields::encoding_parameters(rtpmap_value); - const auto format = details::get_format(sdp_params); - const auto is_video_sdp = nmos::formats::video == format; - const auto is_audio_sdp = nmos::formats::audio == format; - const auto is_data_sdp = nmos::formats::data == format; - const auto is_mux_sdp = nmos::formats::mux == format; - - if (is_audio_sdp) + // ptime attribute + // See https://tools.ietf.org/html/rfc4566#section-6 { - const auto& encoding_name = sdp::fields::encoding_name(rtpmap_value); - sdp_params.audio.bit_depth = !encoding_name.empty() && U('L') == encoding_name.front() ? utility::istringstreamed(encoding_name.substr(1)) : 0; + auto ptime = sdp::find_name(media_attributes, sdp::attributes::ptime); + if (media_attributes.end() != ptime) + { + sdp_params.packet_time = sdp::fields::value(*ptime).as_double(); + } + } - sdp_params.audio.sample_rate = nmos::rational{ (nmos::rational::int_type)sdp::fields::clock_rate(rtpmap_value) }; - sdp_params.audio.channel_count = (uint32_t)sdp::fields::encoding_parameters(rtpmap_value); + // maxptime attribute + // See https://tools.ietf.org/html/rfc4566#section-6 + { + auto maxptime = sdp::find_name(media_attributes, sdp::attributes::maxptime); + if (media_attributes.end() != maxptime) + { + sdp_params.max_packet_time = sdp::fields::value(*maxptime).as_double(); + } } - // ptime attribute + // framerate attribute // See https://tools.ietf.org/html/rfc4566#section-6 { - auto ptime = sdp::find_name(attributes, sdp::attributes::ptime); - if (is_audio_sdp) + auto framerate = sdp::find_name(media_attributes, sdp::attributes::framerate); + if (media_attributes.end() != framerate) { - if (attributes.end() == ptime) throw details::sdp_processing_error("missing attribute: ptime"); - sdp_params.audio.packet_time = sdp::fields::value(*ptime).as_double(); + sdp_params.framerate = sdp::fields::value(*framerate).as_double(); } } // fmtp attribute // See https://tools.ietf.org/html/rfc4566#section-6 - auto fmtp = sdp::find_name(attributes, sdp::attributes::fmtp); - if (is_video_sdp) { - if (attributes.end() == fmtp) throw details::sdp_processing_error("missing attribute: fmtp"); - const auto& fmtp_value = sdp::fields::value(*fmtp); - const auto& format_specific_parameters = sdp::fields::format_specific_parameters(fmtp_value); + auto fmtp = sdp::find_name(media_attributes, sdp::attributes::fmtp); + if (media_attributes.end() != fmtp) + { + const auto& format_specific_parameters = sdp::fields::format_specific_parameters(sdp::fields::value(*fmtp)); + sdp_params.fmtp = boost::copy_range(format_specific_parameters | boost::adaptors::transformed([&](const value& param) + { + const auto& name = sdp::fields::name(param); + const auto& value_or_null = sdp::fields::value(param); + return sdp_parameters::fmtp_t::value_type{ name, !value_or_null.is_null() ? value_or_null.as_string() : U("") }; + })); + } + } - // See SMPTE ST 2110-20:2017 Section 7.2 Required Media Type Parameters - // and Section 7.3 Media Type Parameters with default values + return sdp_params; + } - const auto width = sdp::find_name(format_specific_parameters, sdp::fields::width); - if (format_specific_parameters.end() == width) throw details::sdp_processing_error("missing format parameter: width"); - sdp_params.video.width = utility::istringstreamed(sdp::fields::value(*width).as_string()); + // Get additional "video/raw" parameters from the SDP parameters + template + video_raw_parameters get_video_raw_parameters(const sdp_parameters& sdp_params, MissingRequiredParameter missing = MissingRequiredParameter{}) + { + video_raw_parameters params; - const auto height = sdp::find_name(format_specific_parameters, sdp::fields::height); - if (format_specific_parameters.end() == height) throw details::sdp_processing_error("missing format parameter: height"); - sdp_params.video.height = utility::istringstreamed(sdp::fields::value(*height).as_string()); + // See SMPTE ST 2110-20:2017 Section 7.2 Required Media Type Parameters + // and Section 7.3 Media Type Parameters with default values - auto parse_rational = [](const utility::string_t& rational_string) - { - const auto slash = rational_string.find(U('/')); - return nmos::rational(utility::istringstreamed(rational_string.substr(0, slash)), utility::string_t::npos != slash ? utility::istringstreamed(rational_string.substr(slash + 1)) : 1); - }; - const auto exactframerate = sdp::find_name(format_specific_parameters, sdp::fields::exactframerate); - if (format_specific_parameters.end() == exactframerate) throw details::sdp_processing_error("missing format parameter: exactframerate"); - sdp_params.video.exactframerate = parse_rational(sdp::fields::value(*exactframerate).as_string()); - - // optional - const auto interlace = sdp::find_name(format_specific_parameters, sdp::fields::interlace); - sdp_params.video.interlace = format_specific_parameters.end() != interlace; - - // optional - const auto segmented = sdp::find_name(format_specific_parameters, sdp::fields::segmented); - sdp_params.video.segmented = format_specific_parameters.end() != segmented; - - const auto sampling = sdp::find_name(format_specific_parameters, sdp::fields::sampling); - if (format_specific_parameters.end() == sampling) throw details::sdp_processing_error("missing format parameter: sampling"); - sdp_params.video.sampling = sdp::sampling{ sdp::fields::value(*sampling).as_string() }; - - const auto depth = sdp::find_name(format_specific_parameters, sdp::fields::depth); - if (format_specific_parameters.end() == depth) throw details::sdp_processing_error("missing format parameter: depth"); - sdp_params.video.depth = utility::istringstreamed(sdp::fields::value(*depth).as_string()); - - // optional - const auto tcs = sdp::find_name(format_specific_parameters, sdp::fields::transfer_characteristic_system); - if (format_specific_parameters.end() != tcs) - { - sdp_params.video.tcs = sdp::transfer_characteristic_system{ sdp::fields::value(*tcs).as_string() }; - } - // else sdp_params.video.tcs = sdp::transfer_characteristic_systems::SDR; - // but better to let the caller distinguish that it's been defaulted? + const auto sampling = details::find_fmtp(sdp_params.fmtp, sdp::fields::sampling); + if (sdp_params.fmtp.end() != sampling) params.sampling = sdp::sampling{ sampling->second }; + else missing(sdp::fields::sampling); - const auto colorimetry = sdp::find_name(format_specific_parameters, sdp::fields::colorimetry); - if (format_specific_parameters.end() == colorimetry) throw details::sdp_processing_error("missing format parameter: colorimetry"); - sdp_params.video.colorimetry = sdp::colorimetry{ sdp::fields::value(*colorimetry).as_string() }; + const auto depth = details::find_fmtp(sdp_params.fmtp, sdp::fields::depth); + if (sdp_params.fmtp.end() != depth) params.depth = utility::istringstreamed(depth->second); + else missing(sdp::fields::depth); - // don't examine required parameters "PM" (packing mode), "SSN" (SMPTE standard number) - // don't examine optional parameters "segmented", "RANGE", "MAXUDP", "PAR" + const auto width = details::find_fmtp(sdp_params.fmtp, sdp::fields::width); + if (sdp_params.fmtp.end() != width) params.width = utility::istringstreamed(width->second); + else missing(sdp::fields::width); - // "Senders and Receivers compliant to [ST 2110-20] shall comply with the provisions of SMPTE ST 2110-21." - // See SMPTE ST 2110-20:2017 Section 6.1.1 + const auto height = details::find_fmtp(sdp_params.fmtp, sdp::fields::height); + if (sdp_params.fmtp.end() != height) params.height = utility::istringstreamed(height->second); + else missing(sdp::fields::height); - // See SMPTE ST 2110-21:2017 Section 8.1 Required Parameters - // and Section 8.2 Optional Parameters + const auto exactframerate = details::find_fmtp(sdp_params.fmtp, sdp::fields::exactframerate); + if (sdp_params.fmtp.end() != exactframerate) params.exactframerate = nmos::details::parse_exactframerate(exactframerate->second); + else missing(sdp::fields::exactframerate); - // hmm, "TP" (type parameter) is required, but currently omitted by several vendors, so allow that for now... - const auto tp = sdp::find_name(format_specific_parameters, sdp::fields::type_parameter); - if (format_specific_parameters.end() != tp) - { - sdp_params.video.tp = sdp::type_parameter{ sdp::fields::value(*tp).as_string() }; - } - // else sdp_params.video.tp = {}; + // optional + const auto interlace = details::find_fmtp(sdp_params.fmtp, sdp::fields::interlace); + params.interlace = sdp_params.fmtp.end() != interlace; - // don't examine optional parameters "TROFF", "CMAX" - } - else if (is_audio_sdp && attributes.end() != fmtp) - { - const auto& fmtp_value = sdp::fields::value(*fmtp); - const auto& format_specific_parameters = sdp::fields::format_specific_parameters(fmtp_value); + // optional + const auto segmented = details::find_fmtp(sdp_params.fmtp, sdp::fields::segmented); + params.segmented = sdp_params.fmtp.end() != segmented; - // optional - const auto channel_order = sdp::find_name(format_specific_parameters, sdp::fields::channel_order); - if (format_specific_parameters.end() != channel_order) - { - sdp_params.audio.channel_order = sdp::fields::value(*channel_order).as_string(); - } - } - else if (is_data_sdp && attributes.end() != fmtp) - { - const auto& fmtp_value = sdp::fields::value(*fmtp); - const auto& format_specific_parameters = sdp::fields::format_specific_parameters(fmtp_value); + // optional + const auto tcs = details::find_fmtp(sdp_params.fmtp, sdp::fields::transfer_characteristic_system); + if (sdp_params.fmtp.end() != tcs) params.tcs = sdp::transfer_characteristic_system{ tcs->second }; - // "The SDP object shall be constructed as described in IETF RFC 8331" - // See SMPTE ST 2110-40:2018 Section 6 - // and https://tools.ietf.org/html/rfc8331 + const auto colorimetry = details::find_fmtp(sdp_params.fmtp, sdp::fields::colorimetry); + if (sdp_params.fmtp.end() != colorimetry) params.colorimetry = sdp::colorimetry{ colorimetry->second }; + else missing(sdp::fields::colorimetry); - // optional - sdp_params.data.did_sdids = boost::copy_range>(format_specific_parameters | boost::adaptors::filtered([](const web::json::value& nv) - { - return sdp::fields::DID_SDID.key == sdp::fields::name(nv); - }) | boost::adaptors::transformed([](const web::json::value& did_sdid) - { - return parse_fmtp_did_sdid(sdp::fields::value(did_sdid).as_string()); - })); + // optional + const auto range = details::find_fmtp(sdp_params.fmtp, sdp::fields::range); + if (sdp_params.fmtp.end() != range) params.range = sdp::range{ range->second }; - // optional - const auto vpid_code = sdp::find_name(format_specific_parameters, sdp::fields::VPID_Code); - if (format_specific_parameters.end() != vpid_code) - { - sdp_params.data.vpid_code = (nmos::vpid_code)utility::istringstreamed(sdp::fields::value(*vpid_code).as_string()); - } - } - else if (is_mux_sdp && attributes.end() != fmtp) + // optional + const auto par = details::find_fmtp(sdp_params.fmtp, sdp::fields::pixel_aspect_ratio); + if (sdp_params.fmtp.end() != par) params.par = nmos::details::parse_pixel_aspect_ratio(par->second); + + const auto pm = details::find_fmtp(sdp_params.fmtp, sdp::fields::packing_mode); + if (sdp_params.fmtp.end() != pm) params.pm = sdp::packing_mode{ pm->second }; + else missing(sdp::fields::packing_mode); + + const auto ssn = details::find_fmtp(sdp_params.fmtp, sdp::fields::smpte_standard_number); + if (sdp_params.fmtp.end() != ssn) params.ssn = sdp::smpte_standard_number{ ssn->second }; + else missing(sdp::fields::smpte_standard_number); + + // "Senders and Receivers compliant to [ST 2110-20] shall comply with the provisions of SMPTE ST 2110-21." + // See SMPTE ST 2110-20:2017 Section 6.1.1 + + // See SMPTE ST 2110-21:2017 Section 8.1 Required Parameters + // and Section 8.2 Optional Parameters + + // hmm, "TP" (type parameter) is required, but currently omitted by several vendors, so allow that for now... + const auto tp = details::find_fmtp(sdp_params.fmtp, sdp::fields::type_parameter); + if (sdp_params.fmtp.end() != tp) params.tp = sdp::type_parameter{ tp->second }; + + // optional + const auto troff = details::find_fmtp(sdp_params.fmtp, sdp::fields::TROFF); + if (sdp_params.fmtp.end() != troff) params.troff = utility::istringstreamed(troff->second); + + // optional + const auto cmax = details::find_fmtp(sdp_params.fmtp, sdp::fields::CMAX); + if (sdp_params.fmtp.end() != cmax) params.cmax = utility::istringstreamed(cmax->second); + + // optional + const auto maxudp = details::find_fmtp(sdp_params.fmtp, sdp::fields::max_udp_packet_size); + if (sdp_params.fmtp.end() != maxudp) params.maxudp = utility::istringstreamed(maxudp->second); + + // optional + const auto tsmode = details::find_fmtp(sdp_params.fmtp, sdp::fields::timestamp_mode); + if (sdp_params.fmtp.end() != tsmode) params.tsmode = sdp::timestamp_mode{ tsmode->second }; + + // optional + const auto tsdelay = details::find_fmtp(sdp_params.fmtp, sdp::fields::timestamp_delay); + if (sdp_params.fmtp.end() != tsdelay) params.tsdelay = utility::istringstreamed(tsdelay->second); + + return params; + } + + // Get additional "video/raw" parameters from the SDP parameters + video_raw_parameters get_video_raw_parameters(const sdp_parameters& sdp_params) + { + return get_video_raw_parameters(sdp_params); + } + + // Get additional "video/raw" parameters from the SDP parameters + video_raw_parameters get_video_raw_parameters_or_defaults(const sdp_parameters& sdp_params) + { + return get_video_raw_parameters<>(sdp_params, [](const utility::string_t&) {}); + } + + // Get additional "audio/L" parameters from the SDP parameters + audio_L_parameters get_audio_L_parameters(const sdp_parameters& sdp_params) + { + audio_L_parameters params; + + params.channel_count = (uint32_t)sdp_params.rtpmap.encoding_parameters; + if (0 == params.channel_count) params.channel_count = 1; + + const auto& encoding_name = sdp_params.rtpmap.encoding_name; + params.bit_depth = !encoding_name.empty() && U('L') == encoding_name.front() ? utility::istringstreamed(encoding_name.substr(1)) : 0; + + params.sample_rate = sdp_params.rtpmap.clock_rate; + + // optional + const auto channel_order = details::find_fmtp(sdp_params.fmtp, sdp::fields::channel_order); + if (sdp_params.fmtp.end() != channel_order) params.channel_order = channel_order->second; + + // optional + const auto tsmode = details::find_fmtp(sdp_params.fmtp, sdp::fields::timestamp_mode); + if (sdp_params.fmtp.end() != tsmode) params.tsmode = sdp::timestamp_mode{ tsmode->second }; + + // optional + const auto tsdelay = details::find_fmtp(sdp_params.fmtp, sdp::fields::timestamp_delay); + if (sdp_params.fmtp.end() != tsdelay) params.tsdelay = utility::istringstreamed(tsdelay->second); + + // optional + params.packet_time = sdp_params.packet_time; + + return params; + } + + // Get additional "video/smpte291" parameters from the SDP parameters + video_smpte291_parameters get_video_smpte291_parameters(const sdp_parameters& sdp_params) + { + video_smpte291_parameters params; + + // "The SDP object shall be constructed as described in IETF RFC 8331" + // See SMPTE ST 2110-40:2018 Section 6 + // and https://tools.ietf.org/html/rfc8331 + + // optional + params.did_sdids = boost::copy_range>(sdp_params.fmtp | boost::adaptors::filtered([](const sdp_parameters::fmtp_t::value_type& param) + { + return sdp::fields::DID_SDID.key == param.first; + }) | boost::adaptors::transformed([](const sdp_parameters::fmtp_t::value_type& did_sdid) { - const auto& fmtp_value = sdp::fields::value(*fmtp); - const auto& format_specific_parameters = sdp::fields::format_specific_parameters(fmtp_value); + return parse_fmtp_did_sdid(did_sdid.second); + })); - // "Senders shall signal Media Type Parameters TP and TROFF as specified in ST 2110-21" - // See SMPTE ST 2022-8:2019 Section 6 + // optional + const auto vpid_code = details::find_fmtp(sdp_params.fmtp, sdp::fields::VPID_Code); + if (sdp_params.fmtp.end() != vpid_code) params.vpid_code = (nmos::vpid_code)utility::istringstreamed(vpid_code->second); - // See SMPTE ST 2110-21:2017 Section 8.1 Required Parameters - // and Section 8.2 Optional Parameters + // optional + const auto exactframerate = details::find_fmtp(sdp_params.fmtp, sdp::fields::exactframerate); + if (sdp_params.fmtp.end() != exactframerate) params.exactframerate = nmos::details::parse_exactframerate(exactframerate->second); - // "TP" (type parameter) is required, but allow it to be omitted for now... - const auto tp = sdp::find_name(format_specific_parameters, sdp::fields::type_parameter); - if (format_specific_parameters.end() != tp) - { - sdp_params.mux.tp = sdp::type_parameter{ sdp::fields::value(*tp).as_string() }; - } - // else sdp_params.mux.tp = {}; + // optional + const auto tm = details::find_fmtp(sdp_params.fmtp, sdp::fields::TM); + if (sdp_params.fmtp.end() != tm) params.tm = sdp::transmission_model{ tm->second }; - // don't examine optional parameter "TROFF" - } + // optional + const auto ssn = details::find_fmtp(sdp_params.fmtp, sdp::fields::smpte_standard_number); + if (sdp_params.fmtp.end() != ssn) params.ssn = sdp::smpte_standard_number{ ssn->second }; - return sdp_params; + // optional + const auto troff = details::find_fmtp(sdp_params.fmtp, sdp::fields::TROFF); + if (sdp_params.fmtp.end() != troff) params.troff = utility::istringstreamed(troff->second); + + // optional + const auto tsmode = details::find_fmtp(sdp_params.fmtp, sdp::fields::timestamp_mode); + if (sdp_params.fmtp.end() != tsmode) params.tsmode = sdp::timestamp_mode{ tsmode->second }; + + // optional + const auto tsdelay = details::find_fmtp(sdp_params.fmtp, sdp::fields::timestamp_delay); + if (sdp_params.fmtp.end() != tsdelay) params.tsdelay = utility::istringstreamed(tsdelay->second); + + return params; } - std::pair parse_session_description(const web::json::value& session_description) + // Get additional "video/SMPTE2022-6" parameters from the SDP parameters + video_SMPTE2022_6_parameters get_video_SMPTE2022_6_parameters(const sdp_parameters& sdp_params) { - return{ get_session_description_sdp_parameters(session_description), get_session_description_transport_params(session_description) }; + video_SMPTE2022_6_parameters params; + + // "Senders shall signal Media Type Parameters TP and TROFF as specified in ST 2110-21" + // See SMPTE ST 2022-8:2019 Section 6 + + // See SMPTE ST 2110-21:2017 Section 8.1 Required Parameters + // and Section 8.2 Optional Parameters + + // "TP" (type parameter) is required, but allow it to be omitted for now... + const auto tp = details::find_fmtp(sdp_params.fmtp, sdp::fields::type_parameter); + if (sdp_params.fmtp.end() != tp) params.tp = sdp::type_parameter{ tp->second }; + + // optional + const auto troff = details::find_fmtp(sdp_params.fmtp, sdp::fields::TROFF); + if (sdp_params.fmtp.end() != troff) params.troff = utility::istringstreamed(troff->second); + + return params; } - bool match_interlace_mode_constraint(bool interlace, bool segmented, const web::json::value& constraint_set) + // Get SDP parameters from the json representation of an SDP file, e.g. from sdp::parse_session_description + std::pair parse_session_description(const web::json::value& session_description) { - if (!interlace) return nmos::match_string_constraint(nmos::interlace_modes::progressive.name, constraint_set); - if (segmented) return nmos::match_string_constraint(nmos::interlace_modes::interlaced_psf.name, constraint_set); - // hmm, don't think we can be more precise than this, see comment regarding RFC 4175 top-field-first in make_video_sdp_parameters - return nmos::match_string_constraint(nmos::interlace_modes::interlaced_tff.name, constraint_set) - || nmos::match_string_constraint(nmos::interlace_modes::interlaced_bff.name, constraint_set); + return{ get_session_description_sdp_parameters(session_description), get_session_description_transport_params(session_description) }; } - bool match_sdp_parameters_constraint_set(const sdp_parameters& sdp_params, const web::json::value& constraint_set) + namespace details { - using web::json::value; + nmos::format get_format(const sdp_parameters& sdp_params) + { + if (sdp::media_types::video == sdp_params.media_type && U("raw") == sdp_params.rtpmap.encoding_name) return nmos::formats::video; + if (sdp::media_types::audio == sdp_params.media_type && U("L") == sdp_params.rtpmap.encoding_name.substr(0, 1)) return nmos::formats::audio; + if (sdp::media_types::video == sdp_params.media_type && U("smpte291") == sdp_params.rtpmap.encoding_name) return nmos::formats::data; + if (sdp::media_types::video == sdp_params.media_type && U("SMPTE2022-6") == sdp_params.rtpmap.encoding_name) return nmos::formats::mux; + throw sdp_processing_error("unsupported media type/encoding name"); + } - if (!nmos::caps::meta::enabled(constraint_set)) return false; + format_parameters get_format_parameters(const sdp_parameters& sdp_params) + { + if (sdp::media_types::video == sdp_params.media_type && U("raw") == sdp_params.rtpmap.encoding_name) return get_video_raw_parameters(sdp_params); + if (sdp::media_types::audio == sdp_params.media_type && U("L") == sdp_params.rtpmap.encoding_name.substr(0, 1)) return get_audio_L_parameters(sdp_params); + if (sdp::media_types::video == sdp_params.media_type && U("smpte291") == sdp_params.rtpmap.encoding_name) return get_video_smpte291_parameters(sdp_params); + if (sdp::media_types::video == sdp_params.media_type && U("SMPTE2022-6") == sdp_params.rtpmap.encoding_name) return get_video_SMPTE2022_6_parameters(sdp_params); + throw sdp_processing_error("unsupported media type/encoding name"); + } + + // Check the specified SDP interlace and segmented parameters against the specified interlace_mode constraint + bool match_interlace_mode_constraint(bool interlace, bool segmented, const web::json::value& constraint) + { + if (!interlace) return nmos::match_string_constraint(nmos::interlace_modes::progressive.name, constraint); + if (segmented) return nmos::match_string_constraint(nmos::interlace_modes::interlaced_psf.name, constraint); + // hmm, don't think we can be more precise than this, see comment regarding RFC 4175 top-field-first in make_video_sdp_parameters + return nmos::match_string_constraint(nmos::interlace_modes::interlaced_tff.name, constraint) + || nmos::match_string_constraint(nmos::interlace_modes::interlaced_bff.name, constraint); + } + + // for a little brevity, cf. sdp_parameters member type names + const video_raw_parameters* get_video(const format_parameters* format) { return get(format); } + const audio_L_parameters* get_audio(const format_parameters* format) { return get(format); } + const video_smpte291_parameters* get_data(const format_parameters* format) { return get(format); } + const video_SMPTE2022_6_parameters* get_mux(const format_parameters* format) { return get(format); } + + // both video/raw and video/smpte291 may have exactframerate + nmos::rational get_exactframerate(const format_parameters* format) + { + if (auto video = get_video(format)) return video->exactframerate; + else if (auto data = get_data(format)) return data->exactframerate; + else return{}; + } // NMOS Parameter Registers - Capabilities register - // See https://github.com/AMWA-TV/nmos-parameter-registers/blob/capabilities/capabilities/README.md - static const std::map> match_constraints + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/ +#define CAPS_ARGS const sdp_parameters& sdp, const format_parameters& format, const web::json::value& con + static const std::map> format_constraints { // General Constraints - { nmos::caps::format::media_type, [](const sdp_parameters& sdp, const value& con) { return nmos::match_string_constraint(sdp.media_type.name, con); } }, - { nmos::caps::format::grain_rate, [](const sdp_parameters& sdp, const value& con) { return nmos::rational{} == sdp.video.exactframerate || nmos::match_rational_constraint(sdp.video.exactframerate, con); } }, + { nmos::caps::format::media_type, [](CAPS_ARGS) { return nmos::match_string_constraint(get_media_type(sdp).name, con); } }, + // hm, how best to match (rational) nmos::caps::format::grain_rate against (double) framerate e.g. for video/SMPTE2022-6? + // is 23.976 a match for 24000/1001? how about 23.98, or 23.9? or even 23?! + { nmos::caps::format::grain_rate, [](CAPS_ARGS) { auto exactframerate = get_exactframerate(&format); return nmos::rational{} == exactframerate || nmos::match_rational_constraint(exactframerate, con); } }, // Video Constraints - { nmos::caps::format::frame_height, [](const sdp_parameters& sdp, const value& con) { return nmos::match_integer_constraint(sdp.video.height, con); } }, - { nmos::caps::format::frame_width, [](const sdp_parameters& sdp, const value& con) { return nmos::match_integer_constraint(sdp.video.width, con); } }, - { nmos::caps::format::color_sampling, [](const sdp_parameters& sdp, const value& con) { return nmos::match_string_constraint(sdp.video.sampling.name, con); } }, - { nmos::caps::format::interlace_mode, [](const sdp_parameters& sdp, const value& con) { return nmos::match_interlace_mode_constraint(sdp.video.interlace, sdp.video.segmented, con); } }, - { nmos::caps::format::colorspace, [](const sdp_parameters& sdp, const value& con) { return nmos::match_string_constraint(sdp.video.colorimetry.name, con); } }, - { nmos::caps::format::transfer_characteristic, [](const sdp_parameters& sdp, const value& con) { return nmos::match_string_constraint(sdp.video.tcs.name, con); } }, - { nmos::caps::format::component_depth, [](const sdp_parameters& sdp, const value& con) { return nmos::match_integer_constraint(sdp.video.depth, con); } }, + { nmos::caps::format::frame_height, [](CAPS_ARGS) { auto video = get_video(&format); return video && nmos::match_integer_constraint(video->height, con); } }, + { nmos::caps::format::frame_width, [](CAPS_ARGS) { auto video = get_video(&format); return video && nmos::match_integer_constraint(video->width, con); } }, + { nmos::caps::format::color_sampling, [](CAPS_ARGS) { auto video = get_video(&format); return video && nmos::match_string_constraint(video->sampling.name, con); } }, + { nmos::caps::format::interlace_mode, [](CAPS_ARGS) { auto video = get_video(&format); return video && nmos::details::match_interlace_mode_constraint(video->interlace, video->segmented, con); } }, + { nmos::caps::format::colorspace, [](CAPS_ARGS) { auto video = get_video(&format); return video && nmos::match_string_constraint(video->colorimetry.name, con); } }, + { nmos::caps::format::transfer_characteristic, [](CAPS_ARGS) { auto video = get_video(&format); return video && nmos::match_string_constraint(!video->tcs.empty() ? video->tcs.name : sdp::transfer_characteristic_systems::SDR.name, con); } }, + { nmos::caps::format::component_depth, [](CAPS_ARGS) { auto video = get_video(&format); return video && nmos::match_integer_constraint(video->depth, con); } }, // Audio Constraints - { nmos::caps::format::channel_count, [](const sdp_parameters& sdp, const value& con) { return nmos::match_integer_constraint(sdp.audio.channel_count, con); } }, - { nmos::caps::format::sample_rate, [](const sdp_parameters& sdp, const value& con) { return nmos::match_rational_constraint(sdp.audio.sample_rate, con); } }, - { nmos::caps::format::sample_depth, [](const sdp_parameters& sdp, const value& con) { return nmos::match_integer_constraint(sdp.audio.bit_depth, con); } }, + { nmos::caps::format::channel_count, [](CAPS_ARGS) { auto audio = get_audio(&format); return audio && nmos::match_integer_constraint(audio->channel_count, con); } }, + { nmos::caps::format::sample_rate, [](CAPS_ARGS) { auto audio = get_audio(&format); return audio && nmos::match_rational_constraint(audio->sample_rate, con); } }, + { nmos::caps::format::sample_depth, [](CAPS_ARGS) { auto audio = get_audio(&format); return audio && nmos::match_integer_constraint(audio->bit_depth, con); } }, // Transport Constraints - { nmos::caps::transport::packet_time, [](const sdp_parameters& sdp, const value& con) { return nmos::match_number_constraint(sdp.audio.packet_time, con); } }, - // hm, nmos::caps::transport::max_packet_time - { nmos::caps::transport::st2110_21_sender_type, [](const sdp_parameters& sdp, const value& con) { return nmos::match_string_constraint(sdp.video.tp.name, con) || nmos::match_string_constraint(sdp.mux.tp.name, con); } } + { nmos::caps::transport::packet_time, [](CAPS_ARGS) { return 0 == sdp.packet_time || nmos::match_number_constraint(sdp.packet_time, con); } }, + { nmos::caps::transport::max_packet_time, [](CAPS_ARGS) { return 0 == sdp.max_packet_time || nmos::match_number_constraint(sdp.max_packet_time, con); } }, + { nmos::caps::transport::st2110_21_sender_type, [](CAPS_ARGS) { if (auto video = get_video(&format)) return nmos::match_string_constraint(video->tp.name, con); else if (auto mux = get_mux(&format)) return nmos::match_string_constraint(mux->tp.name, con); else return false; } } }; +#undef CAPS_ARGS - const auto& constraints = constraint_set.as_object(); - return constraints.end() == std::find_if(constraints.begin(), constraints.end(), [&](const std::pair& constraint) + // Check the specified SDP parameters and format-specific parameters against the specified constraint set + // using the specified parameter constraint functions + bool match_sdp_parameters_constraint_set(const sdp_parameter_constraints& constraints, const sdp_parameters& sdp_params, const format_parameters& format_params, const web::json::value& constraint_set_) { - const auto& found = match_constraints.find(constraint.first); - return match_constraints.end() != found && !found->second(sdp_params, constraint.second); - }); - } - - void validate_sdp_parameters(const web::json::value& receiver, const sdp_parameters& sdp_params) - { - const auto format = details::get_format(sdp_params); - const auto media_type = details::get_media_type(sdp_params); + using web::json::value; - if (nmos::format{ nmos::fields::format(receiver) } != format) throw details::sdp_processing_error("unexpected media type/encoding name"); + if (!nmos::caps::meta::enabled(constraint_set_)) return false; - const auto& caps = nmos::fields::caps(receiver); - const auto& media_types_or_null = nmos::fields::media_types(caps); - if (!media_types_or_null.is_null()) - { - const auto& media_types = media_types_or_null.as_array(); - const auto found = std::find(media_types.begin(), media_types.end(), web::json::value::string(media_type.name)); - if (media_types.end() == found) throw details::sdp_processing_error("unsupported encoding name"); + const auto& constraint_set = constraint_set_.as_object(); + return constraint_set.end() == std::find_if(constraint_set.begin(), constraint_set.end(), [&](const std::pair& constraint) + { + const auto found = constraints.find(constraint.first); + return constraints.end() != found && !found->second(sdp_params, format_params, constraint.second); + }); } - const auto& constraint_sets_or_null = nmos::fields::constraint_sets(caps); - if (!constraint_sets_or_null.is_null()) + + // Validate the specified SDP parameters and format-specific parameters against the specified receiver + // using the specified parameter constraint functions + void validate_sdp_parameters(const sdp_parameter_constraints& constraints, const sdp_parameters& sdp_params, const format& format, const format_parameters& format_params, const web::json::value& receiver) { - const auto& constraint_sets = constraint_sets_or_null.as_array(); - const auto found = std::find_if(constraint_sets.begin(), constraint_sets.end(), std::bind(match_sdp_parameters_constraint_set, std::ref(sdp_params), std::placeholders::_1)); - if (constraint_sets.end() == found) throw details::sdp_processing_error("unsupported transport or format-specific parameters"); + const auto media_type = get_media_type(sdp_params); + + if (nmos::format{ nmos::fields::format(receiver) } != format) throw details::sdp_processing_error("unexpected media type/encoding name"); + + const auto& caps = nmos::fields::caps(receiver); + const auto& media_types_or_null = nmos::fields::media_types(caps); + if (!media_types_or_null.is_null()) + { + const auto& media_types = media_types_or_null.as_array(); + const auto found = std::find(media_types.begin(), media_types.end(), web::json::value::string(media_type.name)); + if (media_types.end() == found) throw details::sdp_processing_error("unsupported encoding name"); + } + const auto& constraint_sets_or_null = nmos::fields::constraint_sets(caps); + if (!constraint_sets_or_null.is_null()) + { + const auto& constraint_sets = constraint_sets_or_null.as_array(); + const auto found = std::find_if(constraint_sets.begin(), constraint_sets.end(), [&](const web::json::value& constraint_set) { return details::match_sdp_parameters_constraint_set(constraints, sdp_params, format_params, constraint_set); }); + if (constraint_sets.end() == found) throw details::sdp_processing_error("unsupported transport or format-specific parameters"); + } } } + + // Validate the SDP parameters against a receiver for "video/raw", "audio/L", "video/smpte291" or "video/SMPTE2022-6" + void validate_sdp_parameters(const web::json::value& receiver, const sdp_parameters& sdp_params) + { + details::validate_sdp_parameters(details::format_constraints, sdp_params, details::get_format(sdp_params), details::get_format_parameters(sdp_params), receiver); + } } diff --git a/Development/nmos/sdp_utils.h b/Development/nmos/sdp_utils.h index c96ee169b..9d0568af7 100644 --- a/Development/nmos/sdp_utils.h +++ b/Development/nmos/sdp_utils.h @@ -1,6 +1,11 @@ #ifndef NMOS_SDP_UTILS_H #define NMOS_SDP_UTILS_H +#include +#include +#include +#include +#include "bst/any.h" #include "bst/optional.h" #include "cpprest/basic_utils.h" #include "sdp/json.h" @@ -11,36 +16,69 @@ namespace nmos { - struct sdp_parameters; + struct format; + struct media_type; + + struct sdp_parameters; // defined below // Sender helper functions - namespace details - { - sdp::sampling make_sampling(const web::json::array& components); - } + web::json::value make_components(const sdp::sampling& sampling, uint32_t width, uint32_t height, uint32_t depth); - sdp_parameters make_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, const std::vector& media_stream_ids, bst::optional ptp_domain); + // Construct SDP parameters from the IS-04 resources for "video/raw", "audio/L", "video/smpte291" and "video/SMPTE2022-6" + // using default values for unspecified items + // deprecated, use format-specific make__parameters and then make__sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters make_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, const std::vector& media_stream_ids, bst::optional ptp_domain); // deprecated, provided for backwards compatibility, because it may be necessary to also specify the PTP domain to generate an RFC 7273 'ts-refclk' attribute that meets the additional constraints of ST 2110-10 sdp_parameters make_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, const std::vector& media_stream_ids); - web::json::value make_session_description(const sdp_parameters& sdp_params, const web::json::value& transport_params); + // Construct SDP parameters for the specified format from the IS-04 resources, using default values for unspecified items + + // deprecated, use make_video_raw_parameters and then make_video_raw_sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters make_video_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional payload_type, const std::vector& media_stream_ids, bst::optional ptp_domain, bst::optional tp); + // deprecated, use make_audio_L_parameters and then make_audio_L_sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters make_audio_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional payload_type, const std::vector& media_stream_ids, bst::optional ptp_domain, bst::optional packet_time); + // deprecated, use make_video_smpte291_parameters and then make_video_smpte291_sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters make_data_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional payload_type, const std::vector& media_stream_ids, bst::optional ptp_domain, bst::optional vpid_code); + // deprecated, use make_video_SMPTE2022_6_parameters and then make_video_SMPTE2022_6_sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters make_mux_sdp_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional payload_type, const std::vector& media_stream_ids, bst::optional ptp_domain, bst::optional tp); + + // Sender/Receiver helper functions + + // Make a json representation of an SDP file, e.g. for sdp::make_session_description, from the specified parameters; explicitly specify whether 'source-filter' attributes are included to override the default behaviour + web::json::value make_session_description(const sdp_parameters& sdp_params, const web::json::value& transport_params, bst::optional source_filters = bst::nullopt); // Receiver helper functions - // Get transport parameters from the parsed SDP file + // Get IS-05 transport parameters from the json representation of an SDP file, e.g. from sdp::parse_session_description web::json::value get_session_description_transport_params(const web::json::value& session_description); - // Get other SDP parameters from the parsed SDP file + // Get the additional (non-transport) SDP parameters from the json representation of an SDP file, e.g. from sdp::parse_session_description sdp_parameters get_session_description_sdp_parameters(const web::json::value& session_description); + // Get SDP parameters from the json representation of an SDP file, e.g. from sdp::parse_session_description std::pair parse_session_description(const web::json::value& session_description); + // Validate the SDP parameters against a receiver for "video/raw", "audio/L", "video/smpte291" or "video/SMPTE2022-6" void validate_sdp_parameters(const web::json::value& receiver, const sdp_parameters& sdp_params); + // Format-specific types + + struct video_raw_parameters; + struct audio_L_parameters; + struct video_smpte291_parameters; + struct video_SMPTE2022_6_parameters; + + // sdp_parameters represents the additional (non-transport) SDP parameters. + // It does not cover the case of multiple media types in a single SDP file because NMOS associates an SDP file + // with each RTP sender and receiver. + // When redundancy is being used, the media description and media-level attributes for each stream are assumed + // to be identical except for the values corresponding to the IS-05 transport parameters for each leg. struct sdp_parameters { + // Origin ("o=") + // See https://tools.ietf.org/html/rfc4566#section-5.2 struct origin_t { utility::string_t user_name; @@ -60,16 +98,23 @@ namespace nmos {} } origin; + // Session Name ("s=") + // See https://tools.ietf.org/html/rfc4566#section-5.3 utility::string_t session_name; + // Connection Data ("c=") + // See https://tools.ietf.org/html/rfc4566#section-5.7 struct connection_data_t { + // most fields in the "c=" line have corresponding values in the IS-05 transport parameters uint32_t ttl; connection_data_t() : ttl() {} connection_data_t(uint32_t ttl) : ttl(ttl) {} } connection_data; + // Timing ("t=") + // See https://tools.ietf.org/html/rfc4566#section-5.9 struct timing_t { uint64_t start_time; @@ -78,6 +123,8 @@ namespace nmos timing_t(uint64_t start_time, uint64_t stop_time) : start_time(start_time), stop_time(stop_time) {} } timing; + // Grouping Framework ("a=mid:" and "a=group:") + // See https://tools.ietf.org/html/rfc5888 struct group_t { sdp::group_semantics_type semantics; @@ -88,109 +135,65 @@ namespace nmos group_t(const sdp::group_semantics_type& semantics, const std::vector& media_stream_ids) : semantics(semantics), media_stream_ids(media_stream_ids) {} } group; + // Media ("m=") + // See https://tools.ietf.org/html/rfc4566#section-5.14 sdp::media_type media_type; sdp::protocol protocol; + // Bandwidth ("b=") (e.g. for "video/jxsv") + // See https://tools.ietf.org/html/rfc4566#section-5.8 + struct bandwidth_t + { + sdp::bandwidth_type bandwidth_type; + uint64_t bandwidth; + + bandwidth_t() : bandwidth() {} + bandwidth_t(const sdp::bandwidth_type& bandwidth_type, uint64_t bandwidth) : bandwidth_type(bandwidth_type), bandwidth(bandwidth) {} + } bandwidth; + + // Packet Time ("a=ptime:") (e.g. for "audio/L16") + // See https://tools.ietf.org/html/rfc4566#section-6 + double packet_time; + + // Maximum Packet Time ("a=maxptime:") (e.g. for "audio/L16") + // See https://tools.ietf.org/html/rfc4566#section-6 + double max_packet_time; + + // RTP Payload Mapping ("a=rtpmap:") + // See https://tools.ietf.org/html/rfc4566#section-6 struct rtpmap_t { uint64_t payload_type; - // encoding-name is "raw" for video, "L24" or "L16" for audio, "smpte291" for data, "SMPTE2022-6" for mux + // encoding-name is the media subtype + // e.g. "raw" for video, "L24" or "L16" for audio, "smpte291" for data, "SMPTE2022-6" for mux utility::string_t encoding_name; uint64_t clock_rate; + // encoding-parameters optionally indicates channel count for audio + uint64_t encoding_parameters; - rtpmap_t() : payload_type(), clock_rate() {} - rtpmap_t(uint64_t payload_type, const utility::string_t& encoding_name, uint64_t clock_rate) + rtpmap_t() : payload_type(), clock_rate(), encoding_parameters() {} + rtpmap_t(uint64_t payload_type, const utility::string_t& encoding_name, uint64_t clock_rate, uint64_t encoding_parameters = {}) : payload_type(payload_type) , encoding_name(encoding_name) , clock_rate(clock_rate) + , encoding_parameters(encoding_parameters) {} } rtpmap; - // additional "video/raw" parameters (video only) - struct video_t - { - // fmtp indicates format - uint32_t width; - uint32_t height; - nmos::rational exactframerate; - bool interlace; - bool segmented; - sdp::sampling sampling; - uint32_t depth; - sdp::transfer_characteristic_system tcs; // nmos::transfer_characteristic is a subset - sdp::colorimetry colorimetry; // nmos::colorspace is a subset - sdp::type_parameter tp; - - video_t() : width(), height(), interlace(), segmented(), depth() {} - video_t(uint32_t width, uint32_t height, const nmos::rational& exactframerate, bool interlace, bool segmented, const sdp::sampling& sampling, uint32_t depth, const sdp::transfer_characteristic_system& tcs, const sdp::colorimetry& colorimetry, const sdp::type_parameter& tp) - : width(width) - , height(height) - , exactframerate(exactframerate) - , interlace(interlace) - , segmented(segmented) - , sampling(sampling) - , depth(depth) - , tcs(tcs) - , colorimetry(colorimetry) - , tp(tp) - {} - } video; + // Frame Rate ("a=framerate:") (e.g. for "video/SMPTE2022-6") + // See https://tools.ietf.org/html/rfc4566#section-6 + double framerate; - // additional "audio/L" parameters (audio only) - struct audio_t - { - // rtpmap encoding-parameters indicates channel_count - uint32_t channel_count; - // rtpmap encoding-name (e.g. "L24") indicates bit_depth - uint32_t bit_depth; - // rtpmap clock-rate indicates sample_rate - nmos::rational sample_rate; - - // fmtp indicates channel-order (e.g. "SMPTE2110.(ST)") - utility::string_t channel_order; - - // ptime - double packet_time; - - audio_t() : channel_count(), bit_depth(), packet_time() {} - audio_t(uint32_t channel_count, uint32_t bit_depth, const nmos::rational& sample_rate, const utility::string_t& channel_order, double packet_time) - : channel_count(channel_count) - , bit_depth(bit_depth) - , sample_rate(sample_rate) - , channel_order(channel_order) - , packet_time(packet_time) - {} - } audio; + // Format-specific Parameters ("a=fmtp:") + // See https://tools.ietf.org/html/rfc4566#section-6 + typedef std::vector> fmtp_t; + fmtp_t fmtp; - // additional "video/smpte291" data parameters (data only) - // see SMPTE ST 2110-40:2018 - // and https://www.iana.org/assignments/media-types/video/smpte291 - // and https://tools.ietf.org/html/rfc8331 - struct data_t - { - // fmtp optionally indicates multiple DID_SDID parameters - std::vector did_sdids; - // fmtp optionally indicates VPID Code of the source interface - nmos::vpid_code vpid_code; - - data_t(const std::vector& did_sdids = {}, nmos::vpid_code vpid_code = {}) - : did_sdids(did_sdids) - , vpid_code(vpid_code) - {} - } data; - - // additional "video/SMPTE2022-6" parameters (mux only) - // see SMPTE ST 2022-8:2019 - struct mux_t - { - sdp::type_parameter tp; - - mux_t() {} - mux_t(const sdp::type_parameter& tp) - : tp(tp) - {} - } mux; + // For now, only the default payload format is covered. + //std::vector> alternative_rtpmap_fmtp; + // Timestamp Reference Clock Source Signalling ("a=ts-refclk:") + // See https://tools.ietf.org/html/rfc7273#section-4 struct ts_refclk_t { sdp::ts_refclk_source clock_source; @@ -227,95 +230,447 @@ namespace nmos , mac_address(mac_address) {} }; - std::vector ts_refclk; + std::vector ts_refclk; // hm, this is one for each leg + // Media Clock Source Signalling + // See https://tools.ietf.org/html/rfc7273#section-5 struct mediaclk_t { sdp::mediaclk_source clock_source; utility::string_t clock_parameters; mediaclk_t() {} - mediaclk_t(const sdp::mediaclk_source& clock_source, const utility::string_t& clock_parameters) + mediaclk_t(const sdp::mediaclk_source& clock_source, const utility::string_t& clock_parameters = {}) : clock_source(clock_source) , clock_parameters(clock_parameters) {} } mediaclk; // construct null SDP parameters - sdp_parameters() {} + sdp_parameters() : packet_time(), max_packet_time(), framerate() {} - // construct "video/raw" SDP parameters with sensible defaults for unspecified fields - sdp_parameters(const utility::string_t& session_name, const video_t& video, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}) + // construct SDP parameters with sensible defaults for unspecified fields + sdp_parameters(const utility::string_t& session_name, const sdp::media_type& media_type, const rtpmap_t& rtpmap, const fmtp_t& fmtp = {}, uint64_t bandwidth = {}, double packet_time = {}, double max_packet_time = {}, double framerate = {}, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}) : origin(U("-"), sdp::ntp_now() >> 32) , session_name(session_name) , connection_data(32) , timing() , group(!media_stream_ids.empty() ? group_t{ sdp::group_semantics::duplication, media_stream_ids } : group_t{}) - , media_type(sdp::media_types::video) + , media_type(media_type) , protocol(sdp::protocols::RTP_AVP) - , rtpmap(payload_type, U("raw"), 90000) - , video(video) - , audio() - , data() - , mux() + , bandwidth(0 != bandwidth ? bandwidth_t{ sdp::bandwidth_types::application_specific, bandwidth } : bandwidth_t{}) + , packet_time(packet_time) + , max_packet_time(max_packet_time) + , rtpmap(rtpmap) + , framerate(framerate) + , fmtp(fmtp) , ts_refclk(ts_refclk) , mediaclk(sdp::mediaclk_sources::direct, U("0")) {} - // construct "audio/L" SDP parameters with sensible defaults for unspecified fields - sdp_parameters(const utility::string_t& session_name, const audio_t& audio, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}) - : origin(U("-"), sdp::ntp_now() >> 32) - , session_name(session_name) - , connection_data(32) - , timing() - , group(!media_stream_ids.empty() ? group_t{ sdp::group_semantics::duplication, media_stream_ids } : group_t{}) - , media_type(sdp::media_types::audio) - , protocol(sdp::protocols::RTP_AVP) - , rtpmap(payload_type, U("L") + utility::ostringstreamed(audio.bit_depth), uint64_t(double(audio.sample_rate.numerator()) / double(audio.sample_rate.denominator()) + 0.5)) - , video() - , audio(audio) - , data() - , mux() - , ts_refclk(ts_refclk) - , mediaclk(sdp::mediaclk_sources::direct, U("0")) + // deprecated, provided to slightly simplify updating code to use fmtp + typedef video_raw_parameters video_t; + typedef audio_L_parameters audio_t; + typedef video_smpte291_parameters data_t; + typedef video_SMPTE2022_6_parameters mux_t; + + // deprecated, use make_video_raw_sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters(const utility::string_t& session_name, const video_t& video, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}); + + // deprecated, use make_audio_L_sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters(const utility::string_t& session_name, const audio_t& audio, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}); + + // deprecated, use make_video_smpte291_sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters(const utility::string_t& session_name, const data_t& data, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}); + + // deprecated, use make_video_SMPTE2022_6_sdp_parameters or equivalent overload of make_sdp_parameters + sdp_parameters(const utility::string_t& session_name, const mux_t& mux, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}); + }; + + // "/" + media_type get_media_type(const sdp_parameters& sdp_params); + + // Format-specific types and functions + + // Additional "video/raw" parameters + // See SMPTE ST 2110-20:2022 + // and SMPTE ST 2110-10:2022 + // and SMPTE ST 2110-21:2022 + // and https://www.iana.org/assignments/media-types/video/raw + // and https://tools.ietf.org/html/rfc4175 + struct video_raw_parameters + { + // fmtp indicates format + sdp::sampling sampling; + uint32_t depth; + uint32_t width; + uint32_t height; + nmos::rational exactframerate; + bool interlace; + bool segmented; + sdp::transfer_characteristic_system tcs; // if omitted (empty), assume sdp::transfer_characteristic_systems::SDR; nmos::transfer_characteristic is compatible + sdp::colorimetry colorimetry; // nmos::colorspace is compatible + sdp::range range; // if omitted (empty), assume sdp::ranges::NARROW + nmos::rational par; // if omitted (zero), assume 1:1 + sdp::packing_mode pm; + sdp::smpte_standard_number ssn; + + // additional fmtp parameters from ST 2110-21:2022 + sdp::type_parameter tp; + bst::optional troff; // if omitted, assume default + uint32_t cmax; // if omitted (zero), assume max defined for tp + + // additional fmtp parameters from ST 2110-10:2022 + uint32_t maxudp; // if omitted (zero), assume the Standard UP Size Limit + sdp::timestamp_mode tsmode; // if omitted (empty), assume sdp::timestamp_modes::NEW + bst::optional tsdelay; + + video_raw_parameters() : depth(), width(), height(), interlace(), segmented(), troff(), cmax(), maxudp(), tsdelay() {} + + video_raw_parameters( + sdp::sampling sampling, + uint32_t depth, + uint32_t width, + uint32_t height, + nmos::rational exactframerate, + bool interlace, + bool segmented, + sdp::transfer_characteristic_system tcs, + sdp::colorimetry colorimetry, + sdp::range range, + nmos::rational par, + sdp::packing_mode pm, + sdp::smpte_standard_number ssn, + sdp::type_parameter tp, + bst::optional troff, + uint32_t cmax, + uint32_t maxudp, + sdp::timestamp_mode tsmode, + bst::optional tsdelay + ) + : sampling(std::move(sampling)) + , depth(depth) + , width(width) + , height(height) + , exactframerate(exactframerate) + , interlace(interlace) + , segmented(segmented) + , tcs(std::move(tcs)) + , colorimetry(std::move(colorimetry)) + , range(std::move(range)) + , par(par) + , pm(std::move(pm)) + , ssn(std::move(ssn)) + , tp(std::move(tp)) + , troff(troff) + , cmax(cmax) + , maxudp(maxudp) + , tsmode(tsmode) + , tsdelay(tsdelay) {} - // construct "video/smpte291" SDP parameters with sensible defaults for unspecified fields - sdp_parameters(const utility::string_t& session_name, const data_t& data, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}) - : origin(U("-"), sdp::ntp_now() >> 32) - , session_name(session_name) - , connection_data(32) - , timing() - , group(!media_stream_ids.empty() ? group_t{ sdp::group_semantics::duplication, media_stream_ids } : group_t{}) - , media_type(sdp::media_types::video) - , protocol(sdp::protocols::RTP_AVP) - , rtpmap(payload_type, U("smpte291"), 90000) - , video() - , audio() - , data(data) - , mux() - , ts_refclk(ts_refclk) - , mediaclk(sdp::mediaclk_sources::direct, U("0")) + // deprecated + video_raw_parameters(uint32_t width, uint32_t height, const nmos::rational& exactframerate, bool interlace, bool segmented, const sdp::sampling& sampling, uint32_t depth, const sdp::transfer_characteristic_system& tcs, const sdp::colorimetry& colorimetry, const sdp::type_parameter& tp) + : video_raw_parameters(sampling, depth, width, height, exactframerate, interlace, segmented, tcs, colorimetry, {}, {}, sdp::packing_modes::general, sdp::smpte_standard_numbers::ST2110_20_2017, tp, {}, {}, {}, {}, {}) {} + }; - // construct "video/SMPTE2022-6" SDP parameters with sensible defaults for unspecified fields - sdp_parameters(const utility::string_t& session_name, const mux_t& mux, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}) - : origin(U("-"), sdp::ntp_now() >> 32) - , session_name(session_name) - , connection_data(32) - , timing() - , group(!media_stream_ids.empty() ? group_t{ sdp::group_semantics::duplication, media_stream_ids } : group_t{}) - , media_type(sdp::media_types::video) - , protocol(sdp::protocols::RTP_AVP) - , rtpmap(payload_type, U("SMPTE2022-6"), 27000000) - , video() - , audio() - , data() - , mux(mux) - , ts_refclk(ts_refclk) - , mediaclk(sdp::mediaclk_sources::direct, U("0")) + // Additional "audio/L" parameters + // See SMPTE ST 2110-30:2017 + // and https://www.iana.org/assignments/media-types/audio/L24 + // and https://www.iana.org/assignments/media-types/audio/L16 + // and https://www.iana.org/assignments/media-types/audio/L8 + struct audio_L_parameters + { + // rtpmap encoding-parameters indicates channel_count + uint32_t channel_count; + // rtpmap encoding-name (e.g. "L24") indicates bit_depth + uint32_t bit_depth; + // rtpmap clock-rate indicates sample_rate + uint64_t sample_rate; + + // fmtp indicates channel-order (e.g. "SMPTE2110.(ST)") + utility::string_t channel_order; + + // additional fmtp parameters from ST 2110-10:2022 + sdp::timestamp_mode tsmode; // if omitted (empty), assume sdp::timestamp_modes::NEW + bst::optional tsdelay; + + // ptime + double packet_time; + + audio_L_parameters() : channel_count(), bit_depth(), sample_rate(), tsdelay(), packet_time() {} + + audio_L_parameters( + uint32_t channel_count, + uint32_t bit_depth, + uint64_t sample_rate, + utility::string_t channel_order, + sdp::timestamp_mode tsmode, + bst::optional tsdelay, + double packet_time + ) + : channel_count(channel_count) + , bit_depth(bit_depth) + , sample_rate(sample_rate) + , channel_order(std::move(channel_order)) + , tsmode(std::move(tsmode)) + , tsdelay(tsdelay) + , packet_time(packet_time) + {} + + // deprecated + audio_L_parameters(uint32_t channel_count, uint32_t bit_depth, uint64_t sample_rate, const utility::string_t& channel_order, double packet_time) + : audio_L_parameters(channel_count, bit_depth, sample_rate, channel_order, {}, {}, packet_time) + {} + }; + + // Additional "video/smpte291" data payload parameters + // See SMPTE ST 2110-40:2023 + // and https://www.iana.org/assignments/media-types/video/smpte291 + // and https://tools.ietf.org/html/rfc8331 + struct video_smpte291_parameters + { + // fmtp optionally indicates multiple DID_SDID parameters + std::vector did_sdids; + // fmtp optionally indicates VPID Code of the source interface + nmos::vpid_code vpid_code; + // fmtp is required to indicate frame rate, since ST 2110-40:2023 + nmos::rational exactframerate; + // fmtp optionally indicates TM, since ST 2110-40:2023 + sdp::transmission_model tm; // if omitted (empty), assume sdp::transmission_models::CTM + // fmtp is required to indicate SSN, since ST 2110-40:2023 + sdp::smpte_standard_number ssn; + + // additional fmtp parameters from ST 2110-21:2022 + bst::optional troff; // if omitted, assume default + + // additional fmtp parameters from ST 2110-10:2022 + sdp::timestamp_mode tsmode; // if omitted (empty), assume sdp::timestamp_modes::NEW + bst::optional tsdelay; + + video_smpte291_parameters() : vpid_code(), troff(), tsdelay() {} + + video_smpte291_parameters( + std::vector did_sdids, + nmos::vpid_code vpid_code, + nmos::rational exactframerate, + sdp::transmission_model tm, + sdp::smpte_standard_number ssn, + bst::optional troff, + sdp::timestamp_mode tsmode, + bst::optional tsdelay + ) + : did_sdids(std::move(did_sdids)) + , vpid_code(vpid_code) + , exactframerate(exactframerate) + , tm(std::move(tm)) + , ssn(std::move(ssn)) + , troff(troff) + , tsmode(std::move(tsmode)) + , tsdelay(tsdelay) + {} + + // deprecated + video_smpte291_parameters(const std::vector& did_sdids, const nmos::vpid_code& vpid_code = {}) + : video_smpte291_parameters(did_sdids, vpid_code, {}, {}, {}, {}, {}, {}) + {} + }; + + // Additional "video/SMPTE2022-6" multiplexed payload parameters + // See SMPTE ST 2022-8:2019 + struct video_SMPTE2022_6_parameters + { + // additional fmtp parameters from ST 2110-21:2017 + sdp::type_parameter tp; + bst::optional troff; // if omitted, assume default + + video_SMPTE2022_6_parameters() : troff() {} + + video_SMPTE2022_6_parameters( + sdp::type_parameter tp, + bst::optional troff + ) + : tp(std::move(tp)) + , troff(troff) + {} + + // deprecated + video_SMPTE2022_6_parameters(const sdp::type_parameter& tp) + : video_SMPTE2022_6_parameters(tp, {}) {} }; + + // Construct additional "video/raw" parameters from the IS-04 resources, using default values for unspecified items + video_raw_parameters make_video_raw_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional tp); + // Construct SDP parameters for "video/raw", with sensible defaults for unspecified fields + sdp_parameters make_video_raw_sdp_parameters(const utility::string_t& session_name, const video_raw_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}); + // Get additional "video/raw" parameters from the SDP parameters + video_raw_parameters get_video_raw_parameters(const sdp_parameters& sdp_params); + video_raw_parameters get_video_raw_parameters_or_defaults(const sdp_parameters& sdp_params); + + // Construct additional "audio/L" parameters from the IS-04 resources, using default values for unspecified items + audio_L_parameters make_audio_L_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional packet_time); + // Construct SDP parameters for "audio/L", with sensible defaults for unspecified fields + sdp_parameters make_audio_L_sdp_parameters(const utility::string_t& session_name, const audio_L_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}); + // Get additional "audio/L" parameters from the SDP parameters + audio_L_parameters get_audio_L_parameters(const sdp_parameters& sdp_params); + + // Construct additional "video/smpte291" parameters from the IS-04 resources, using default values for unspecified items + video_smpte291_parameters make_video_smpte291_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional vpid_code, bst::optional tm = bst::nullopt); + // Construct SDP parameters for "video/smpte291", with sensible defaults for unspecified fields + sdp_parameters make_video_smpte291_sdp_parameters(const utility::string_t& session_name, const video_smpte291_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}); + // Get additional "video/smpte291" parameters from the SDP parameters + video_smpte291_parameters get_video_smpte291_parameters(const sdp_parameters& sdp_params); + + // Construct additional "video/SMPTE2022-6" parameters from the IS-04 resources, using default values for unspecified items + video_SMPTE2022_6_parameters make_video_SMPTE2022_6_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender, bst::optional tp); + // Construct SDP parameters for "video/SMPTE2022-6", with sensible defaults for unspecified fields + sdp_parameters make_video_SMPTE2022_6_sdp_parameters(const utility::string_t& session_name, const video_SMPTE2022_6_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}); + // Get additional "video/SMPTE2022-6" parameters from the SDP parameters + video_SMPTE2022_6_parameters get_video_SMPTE2022_6_parameters(const sdp_parameters& sdp_params); + + // Construct SDP parameters for "video/raw", with sensible defaults for unspecified fields + inline sdp_parameters make_sdp_parameters(const utility::string_t& session_name, const video_raw_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}) + { + return make_video_raw_sdp_parameters(session_name, params, payload_type, media_stream_ids, ts_refclk); + } + // Construct SDP parameters for "audio/L", with sensible defaults for unspecified fields + inline sdp_parameters make_sdp_parameters(const utility::string_t& session_name, const audio_L_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}) + { + return make_audio_L_sdp_parameters(session_name, params, payload_type, media_stream_ids, ts_refclk); + } + // Construct SDP parameters for "video/smpte291", with sensible defaults for unspecified fields + inline sdp_parameters make_sdp_parameters(const utility::string_t& session_name, const video_smpte291_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}) + { + return make_video_smpte291_sdp_parameters(session_name, params, payload_type, media_stream_ids, ts_refclk); + } + // Construct SDP parameters for "video/SMPTE2022-6", with sensible defaults for unspecified fields + inline sdp_parameters make_sdp_parameters(const utility::string_t& session_name, const video_SMPTE2022_6_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}) + { + return make_video_SMPTE2022_6_sdp_parameters(session_name, params, payload_type, media_stream_ids, ts_refclk); + } + + // deprecated, use make_video_raw_sdp_parameters or equivalent overload of make_sdp_parameters + inline sdp_parameters::sdp_parameters(const utility::string_t& session_name, const video_t& video, uint64_t payload_type, const std::vector& media_stream_ids, const std::vector& ts_refclk) + : sdp_parameters(make_sdp_parameters(session_name, video, payload_type, media_stream_ids, ts_refclk)) + {} + + // deprecated, use make_audio_L_sdp_parameters or equivalent overload of make_sdp_parameters + inline sdp_parameters::sdp_parameters(const utility::string_t& session_name, const audio_t& audio, uint64_t payload_type, const std::vector& media_stream_ids, const std::vector& ts_refclk) + : sdp_parameters(make_sdp_parameters(session_name, audio, payload_type, media_stream_ids, ts_refclk)) + {} + + // deprecated, use make_video_smpte291_sdp_parameters or equivalent overload of make_sdp_parameters + inline sdp_parameters::sdp_parameters(const utility::string_t& session_name, const data_t& data, uint64_t payload_type, const std::vector& media_stream_ids, const std::vector& ts_refclk) + : sdp_parameters(make_sdp_parameters(session_name, data, payload_type, media_stream_ids, ts_refclk)) + {} + + // deprecated, use make_video_SMPTE2022_6_sdp_parameters or equivalent overload of make_sdp_parameters + inline sdp_parameters::sdp_parameters(const utility::string_t& session_name, const mux_t& mux, uint64_t payload_type, const std::vector& media_stream_ids, const std::vector& ts_refclk) + : sdp_parameters(make_sdp_parameters(session_name, mux, payload_type, media_stream_ids, ts_refclk)) + {} + + // Helper functions for implementing format-specific functions + namespace details + { + inline std::logic_error sdp_creation_error(const std::string& message) + { + return std::logic_error{ "sdp creation error - " + message }; + } + + inline std::runtime_error sdp_processing_error(const std::string& message) + { + return std::runtime_error{ "sdp processing error - " + message }; + } + + struct throw_missing_fmtp + { + void operator()(const utility::string_t& name) const + { + throw details::sdp_processing_error("missing format parameter: " + utility::us2s(name)); + } + }; + + inline sdp_parameters::fmtp_t::const_iterator find_fmtp(const sdp_parameters::fmtp_t& fmtp, const utility::string_t& name) + { + return std::find_if(fmtp.begin(), fmtp.end(), [&](const sdp_parameters::fmtp_t::value_type& param) + { + return param.first == name; + }); + } + inline sdp_parameters::fmtp_t::iterator find_fmtp(sdp_parameters::fmtp_t& fmtp, const utility::string_t& name) + { + return std::find_if(fmtp.begin(), fmtp.end(), [&](const sdp_parameters::fmtp_t::value_type& param) + { + return param.first == name; + }); + } + + // type-erased format-specific parameters + // e.g. can hold a video_raw_parameters, an audio_L_parameters, etc. + typedef bst::any format_parameters; + + template + inline const FormatParameters* get(const format_parameters* any) + { + return bst::any_cast(any); + } + + // a function to check the specified SDP parameters and format-specific parameters + // against the specified parameter constraint value, see nmos/capabilities.h + typedef std::function sdp_parameter_constraint; + + // a map from parameter constraint URNs to parameter constraint functions + typedef std::map sdp_parameter_constraints; + + // Check the specified SDP interlace and segmented parameters against the specified interlace_mode constraint + bool match_interlace_mode_constraint(bool interlace, bool segmented, const web::json::value& constraint); + + // Check the specified SDP parameters and format-specific parameters against the specified constraint set + // using the specified parameter constraint functions + bool match_sdp_parameters_constraint_set(const sdp_parameter_constraints& constraints, const sdp_parameters& sdp_params, const format_parameters& format_params, const web::json::value& constraint_set); + + // Validate the specified SDP parameters and format-specific parameters against the specified receiver + // using the specified parameter constraint functions + void validate_sdp_parameters(const sdp_parameter_constraints& constraints, const sdp_parameters& sdp_params, const format& format, const format_parameters& format_params, const web::json::value& receiver); + + // Construct ts-refclk attributes for each leg based on the IS-04 resources + std::vector make_ts_refclk(const web::json::value& node, const web::json::value& source, const web::json::value& sender, bst::optional ptp_domain); + + // Construct simple media stream ids based on the sender's number of legs + std::vector make_media_stream_ids(const web::json::value& sender); + + // cf. nmos::make_components + sdp::sampling make_sampling(const web::json::array& components); + + // Exact Frame Rate + // "Integer frame rates shall be signaled as a single decimal number (e.g. "25") whilst non-integer frame rates shall be + // signaled as a ratio of two integer decimal numbers separated by a "forward-slash" character (e.g. "30000/1001"), + // utilizing the numerically smallest numerator value possible." + // See ST 2110-20:2017 Section 7.2 Required Media Type Parameters + utility::string_t make_exactframerate(const nmos::rational& exactframerate); + nmos::rational parse_exactframerate(const utility::string_t& exactframerate); + + // Pixel Aspect Ratio + // "PAR shall be signaled as a ratio of two integer decimal numbers separated by a "colon" character (e.g. "12:11")." + // See ST 2110-20:2017 Section 7.3 Media Type Parameters with default values + utility::string_t make_pixel_aspect_ratio(const nmos::rational& par); + nmos::rational parse_pixel_aspect_ratio(const utility::string_t& par); + + // Payload identifiers 96-127 are used for payloads defined dynamically during a session + // 96 and 97 are suitable for video and audio encodings not covered by the IANA registry + // See https://tools.ietf.org/html/rfc3551#section-3 + // and https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml#rtp-parameters-1 + const uint64_t payload_type_video_default = 96; + const uint64_t payload_type_audio_default = 97; + const uint64_t payload_type_data_default = 100; + // Payload type 98 is recommended for "High bit rate media transport / 27-MHz Clock" + // Payload type 99 is recommended for "High bit rate media transport FEC / 27-MHz Clock" + // "Alternatively, payload types may be set by other means in accordance with RFC 3550." + // See SMPTE ST 2022-6:2012 Section 6.3 RTP/UDP/IP Header + const uint64_t payload_type_mux_default = 98; + } } #endif diff --git a/Development/nmos/server.cpp b/Development/nmos/server.cpp index 52ff269cd..909112470 100644 --- a/Development/nmos/server.cpp +++ b/Development/nmos/server.cpp @@ -7,11 +7,6 @@ namespace nmos : model(model) {} - namespace details - { - void wait_nothrow(pplx::task t) { try { t.wait(); } catch (...) {} } - } - pplx::task server::open() { return pplx::create_task([&] @@ -70,11 +65,7 @@ namespace nmos if (0 <= ws_listener.uri().port()) tasks.push_back(ws_listener.open()); } - return pplx::when_all(tasks.begin(), tasks.end()).then([tasks](pplx::task finally) - { - for (auto& task : tasks) details::wait_nothrow(task); - finally.wait(); - }); + return pplx::ranges::when_all(tasks).then(pplx::observe_exceptions(tasks)); }); } @@ -95,11 +86,7 @@ namespace nmos if (0 <= ws_listener.uri().port()) tasks.push_back(ws_listener.close()); } - return pplx::when_all(tasks.begin(), tasks.end()).then([tasks](pplx::task finally) - { - for (auto& task : tasks) details::wait_nothrow(task); - finally.wait(); - }); + return pplx::ranges::when_all(tasks).then(pplx::observe_exceptions(tasks)); }); } diff --git a/Development/nmos/server_utils.cpp b/Development/nmos/server_utils.cpp index 487f1b731..464e1fc6f 100644 --- a/Development/nmos/server_utils.cpp +++ b/Development/nmos/server_utils.cpp @@ -1,7 +1,8 @@ #include "nmos/server_utils.h" #include -#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) +// cf. preprocessor conditions in nmos::details::make_listener_ssl_context_callback +#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) #include "boost/asio/ssl/set_cipher_list.hpp" #include "boost/asio/ssl/use_tmp_ecdh.hpp" #endif @@ -9,6 +10,9 @@ #include "cpprest/details/system_error.h" #include "cpprest/http_listener.h" #include "cpprest/ws_listener.h" +#include "nmos/ocsp_state.h" +#include "nmos/ocsp_utils.h" +#include "nmos/slog.h" #include "nmos/ssl_context_options.h" // Utility types, constants and functions for implementing NMOS REST API servers @@ -16,43 +20,74 @@ namespace nmos { namespace details { -#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) +// cf. preprocessor conditions in nmos::make_http_listener_config and nmos::make_websocket_listener_config +#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) template - inline std::function make_listener_ssl_context_callback(const nmos::settings& settings) + inline std::function make_listener_ssl_context_callback(const nmos::settings& settings, load_server_certificates_handler load_server_certificates, load_dh_param_handler load_dh_param, ocsp_response_handler get_ocsp_response, slog::base_gate& gate) { - const auto& private_key_files = nmos::experimental::fields::private_key_files(settings); - const auto& certificate_chain_files = nmos::experimental::fields::certificate_chain_files(settings); - const auto& dh_param_file = utility::us2s(nmos::experimental::fields::dh_param_file(settings)); - return [private_key_files, certificate_chain_files, dh_param_file](boost::asio::ssl::context& ctx) + if (!load_server_certificates) + { + load_server_certificates = make_load_server_certificates_handler(settings, gate); + } + + if (!load_dh_param) + { + load_dh_param = make_load_dh_param_handler(settings, gate); + } + + auto ocsp_response = std::make_shared>(); + + return [&gate, load_server_certificates, load_dh_param, get_ocsp_response, ocsp_response](boost::asio::ssl::context& ctx) { try { ctx.set_options(nmos::details::ssl_context_options); - if (private_key_files.size() == 0) + const auto server_certificates = load_server_certificates(); + + if (server_certificates.empty()) { - throw ExceptionType({}, "Missing private key file"); + throw ExceptionType({}, "Missing server certificates"); } - for (const auto& private_key_file : private_key_files.as_array()) + + for (const auto& server_certificate : server_certificates) { - ctx.use_private_key_file(utility::us2s(private_key_file.as_string()), boost::asio::ssl::context::pem); + const auto key = utility::us2s(server_certificate.private_key); + if (0 == key.size()) + { + throw ExceptionType({}, "Missing private key"); + } + const auto cert_chain = utility::us2s(server_certificate.certificate_chain); + if (0 == cert_chain.size()) + { + throw ExceptionType({}, "Missing certificate chain"); + } + ctx.use_private_key(boost::asio::buffer(key.data(), key.size()), boost::asio::ssl::context_base::pem); + ctx.use_certificate_chain(boost::asio::buffer(cert_chain.data(), cert_chain.size())); + + const auto key_algorithm = server_certificate.key_algorithm; + if (key_algorithm.empty() || key_algorithm == key_algorithms::ECDSA) + { + // certificates may not have ECDH parameters, so ignore errors... + boost::system::error_code ec; + use_tmp_ecdh(ctx, boost::asio::buffer(cert_chain.data(), cert_chain.size()), ec); + } } - if (certificate_chain_files.size() == 0) + set_cipher_list(ctx, nmos::details::ssl_cipher_list); + + const auto dh_param = utility::us2s(load_dh_param()); + if (dh_param.size()) { - throw ExceptionType({}, "Missing certificate chain file"); + ctx.use_tmp_dh(boost::asio::buffer(dh_param.data(), dh_param.size())); } - for (const auto& certificate_chain_file : certificate_chain_files.as_array()) + + // set up server certificate status callback when client includes a certificate status request extension in the TLS handshake + if (get_ocsp_response) { - ctx.use_certificate_chain_file(utility::us2s(certificate_chain_file.as_string())); - // any one of the certificates may have ECDH parameters, so ignore errors... - boost::system::error_code ec; - use_tmp_ecdh_file(ctx, utility::us2s(certificate_chain_file.as_string()), ec); + *ocsp_response = get_ocsp_response(); + nmos::experimental::set_server_certificate_status_handler(ctx, *ocsp_response.get()); } - - set_cipher_list(ctx, nmos::details::ssl_cipher_list); - - if (!dh_param_file.empty()) ctx.use_tmp_dh_file(dh_param_file); } catch (const boost::system::system_error& e) { @@ -64,7 +99,7 @@ namespace nmos } // construct listener config based on settings - web::http::experimental::listener::http_listener_config make_http_listener_config(const nmos::settings& settings) + web::http::experimental::listener::http_listener_config make_http_listener_config(const nmos::settings& settings, load_server_certificates_handler load_server_certificates, load_dh_param_handler load_dh_param, ocsp_response_handler get_ocsp_response, slog::base_gate& gate) { web::http::experimental::listener::http_listener_config config; config.set_backlog(nmos::fields::listen_backlog(settings)); @@ -72,19 +107,19 @@ namespace nmos // hmm, hostport_listener::on_accept(...) in http_server_asio.cpp // only expects boost::system::system_error to be thrown, so for now // don't use web::http::http_exception - config.set_ssl_context_callback(details::make_listener_ssl_context_callback(settings)); + config.set_ssl_context_callback(details::make_listener_ssl_context_callback(settings, load_server_certificates, load_dh_param, get_ocsp_response, gate)); #endif return config; } // construct listener config based on settings - web::websockets::experimental::listener::websocket_listener_config make_websocket_listener_config(const nmos::settings& settings) + web::websockets::experimental::listener::websocket_listener_config make_websocket_listener_config(const nmos::settings& settings, load_server_certificates_handler load_server_certificates, load_dh_param_handler load_dh_param, ocsp_response_handler get_ocsp_response, slog::base_gate& gate) { web::websockets::experimental::listener::websocket_listener_config config; config.set_backlog(nmos::fields::listen_backlog(settings)); #if !defined(_WIN32) || !defined(__cplusplus_winrt) - config.set_ssl_context_callback(details::make_listener_ssl_context_callback(settings)); + config.set_ssl_context_callback(details::make_listener_ssl_context_callback(settings, load_server_certificates, load_dh_param, get_ocsp_response, gate)); #endif return config; diff --git a/Development/nmos/server_utils.h b/Development/nmos/server_utils.h index bd6238264..f3ad85d05 100644 --- a/Development/nmos/server_utils.h +++ b/Development/nmos/server_utils.h @@ -3,16 +3,20 @@ #include "cpprest/http_listener.h" // forward declaration of web::http::experimental::listener::http_listener_config #include "cpprest/ws_listener.h" // forward declaration of web::websockets::experimental::listener::websocket_listener_config +#include "nmos/certificate_handlers.h" +#include "nmos/ocsp_response_handler.h" #include "nmos/settings.h" +namespace slog { class base_gate; } + // Utility types, constants and functions for implementing NMOS REST API servers namespace nmos { // construct listener config based on settings - web::http::experimental::listener::http_listener_config make_http_listener_config(const nmos::settings& settings); + web::http::experimental::listener::http_listener_config make_http_listener_config(const nmos::settings& settings, load_server_certificates_handler load_server_certificates, load_dh_param_handler load_dh_param, ocsp_response_handler get_ocsp_response, slog::base_gate& gate); // construct listener config based on settings - web::websockets::experimental::listener::websocket_listener_config make_websocket_listener_config(const nmos::settings& settings); + web::websockets::experimental::listener::websocket_listener_config make_websocket_listener_config(const nmos::settings& settings, load_server_certificates_handler load_server_certificates, load_dh_param_handler load_dh_param, ocsp_response_handler get_ocsp_response, slog::base_gate& gate); namespace experimental { diff --git a/Development/nmos/settings.cpp b/Development/nmos/settings.cpp index 896ee8f2c..5608fbcac 100644 --- a/Development/nmos/settings.cpp +++ b/Development/nmos/settings.cpp @@ -3,9 +3,10 @@ #include #include #include +#include #include "cpprest/host_utils.h" +#include "cpprest/http_utils.h" #include "cpprest/version.h" -#include "openssl/opensslv.h" #include "nmos/id.h" #include "websocketpp/version.hpp" @@ -63,23 +64,28 @@ namespace nmos if (settings.has_field(nmos::fields::http_port)) { const auto http_port = nmos::fields::http_port(settings); + // can't share a port between an http_listener and a websocket_listener, so use next higher port + const auto ws_port = http_port + 1; + // can't share a port between the events ws and the control protocol ws + const auto ncp_ws_port = ws_port + 1; if (registry) web::json::insert(settings, std::make_pair(nmos::fields::query_port, http_port)); - // can't share a port between an http_listener and a websocket_listener, so don't apply this one... - //if (registry) web::json::insert(settings, std::make_pair(nmos::fields::query_ws_port, http_port)); + if (registry) web::json::insert(settings, std::make_pair(nmos::fields::query_ws_port, ws_port)); if (registry) web::json::insert(settings, std::make_pair(nmos::fields::registration_port, http_port)); web::json::insert(settings, std::make_pair(nmos::fields::node_port, http_port)); if (registry) web::json::insert(settings, std::make_pair(nmos::fields::system_port, http_port)); if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::connection_port, http_port)); if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::events_port, http_port)); if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::channelmapping_port, http_port)); - // can't share a port between an http_listener and a websocket_listener, so don't apply this one... - //if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::events_ws_port, http_port)); + if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::events_ws_port, ws_port)); if (!registry) web::json::insert(settings, std::make_pair(nmos::experimental::fields::manifest_port, http_port)); web::json::insert(settings, std::make_pair(nmos::experimental::fields::settings_port, http_port)); web::json::insert(settings, std::make_pair(nmos::experimental::fields::logging_port, http_port)); if (registry) web::json::insert(settings, std::make_pair(nmos::experimental::fields::admin_port, http_port)); if (registry) web::json::insert(settings, std::make_pair(nmos::experimental::fields::mdns_port, http_port)); if (registry) web::json::insert(settings, std::make_pair(nmos::experimental::fields::schemas_port, http_port)); + web::json::insert(settings, std::make_pair(nmos::experimental::fields::authorization_redirect_port, http_port)); + web::json::insert(settings, std::make_pair(nmos::experimental::fields::jwks_uri_port, http_port)); + if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::control_protocol_ws_port, ncp_ws_port)); } } } @@ -177,6 +183,19 @@ namespace nmos })); } + namespace experimental + { + // Get HTTP Strict-Transport-Security settings + bst::optional get_hsts(const settings& settings) + { + // when using a reverse proxy for TLS termination, the proxy should be responsible for HSTS + // so check server_secure rather than client_secure + if (nmos::experimental::fields::server_secure(settings) && nmos::experimental::fields::hsts_max_age(settings) > 0) + return web::http::experimental::hsts{ (uint32_t)nmos::experimental::fields::hsts_max_age(settings), nmos::experimental::fields::hsts_include_sub_domains(settings) }; + return bst::nullopt; + } + } + // Get a summary of the build configuration, including versions of dependencies utility::string_t get_build_settings_info() { diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 75d741d80..c0981f8bf 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -1,6 +1,7 @@ #ifndef NMOS_SETTINGS_H #define NMOS_SETTINGS_H +#include "bst/optional.h" #include "cpprest/json_utils.h" namespace web @@ -12,6 +13,13 @@ namespace web struct host_interface; } } + namespace http + { + namespace experimental + { + struct hsts; + } + } } // Configuration settings and defaults @@ -40,6 +48,12 @@ namespace nmos // Get interfaces corresponding to the host addresses in the settings std::vector get_host_interfaces(const settings& settings); + namespace experimental + { + // Get HTTP Strict-Transport-Security settings + bst::optional get_hsts(const settings& settings); + } + // Get a summary of the build configuration, including versions of dependencies utility::string_t get_build_settings_info(); @@ -87,6 +101,12 @@ namespace nmos // is09_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration const web::json::field_as_array is09_versions{ U("is09_versions") }; // when omitted, nmos::is09_versions::all is used + // is10_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration + const web::json::field_as_array is10_versions{ U("is10_versions") }; // when omitted, nmos::is10_versions::all is used + + // is12_versions [node]: used to specify the enabled API versions for a version-locked configuration + const web::json::field_as_array is12_versions{ U("is12_versions") }; // when omitted, nmos::is12_versions::all is used + // pri [registry, node]: used for the 'pri' TXT record; specifying nmos::service_priorities::no_priority (maximum value) disables advertisement completely const web::json::field_as_integer_or pri{ U("pri"), 100 }; // default to highest_development_priority @@ -94,20 +114,28 @@ namespace nmos const web::json::field_as_integer_or highest_pri{ U("highest_pri"), 0 }; // default to highest_active_priority; specifying no_priority disables discovery completely const web::json::field_as_integer_or lowest_pri{ U("lowest_pri"), (std::numeric_limits::max)() }; // default to no_priority - // discovery_backoff_min/discovery_backoff_max/discovery_backoff_factor [node]: used to back-off after errors interacting with all discoverable Registration APIs or System APIs + // authorization_highest_pri, authorization_lowest_pri [registry, node]: used to specify the (inclusive) range of suitable 'pri' values of discovered Authorization APIs, to avoid development and live systems colliding + const web::json::field_as_integer_or authorization_highest_pri{ U("authorization_highest_pri"), 0 }; // default to highest_active_priority; specifying no_priority disables discovery completely + const web::json::field_as_integer_or authorization_lowest_pri{ U("authorization_lowest_pri"), (std::numeric_limits::max)() }; // default to no_priority + + // discovery_backoff_min/discovery_backoff_max/discovery_backoff_factor [registry, node]: used to back-off after errors interacting with all discoverable service instances + // e.g. Registration APIs, System APIs, or OCSP servers const web::json::field_as_integer_or discovery_backoff_min{ U("discovery_backoff_min"), 1 }; const web::json::field_as_integer_or discovery_backoff_max{ U("discovery_backoff_max"), 30 }; const web::json::field_with_default discovery_backoff_factor{ U("discovery_backoff_factor"), 1.5 }; + // service_name_prefix [registry, node]: used as a prefix in the advertised service names ("__:", e.g. "nmos-cpp_node_127-0-0-1:3212") + const web::json::field_as_string_or service_name_prefix{ U("service_name_prefix"), U("nmos-cpp") }; + // registry_address [node]: IP address or host name used to construct request URLs for registry APIs (if not discovered via DNS-SD) const web::json::field_as_string registry_address{ U("registry_address") }; // registry_version [node]: used to construct request URLs for registry APIs (if not discovered via DNS-SD) - const web::json::field_as_string_or registry_version{ U("registry_version"), U("v1.2") }; + const web::json::field_as_string_or registry_version{ U("registry_version"), U("v1.3") }; // port numbers [registry, node]: ports to which clients should connect for each API - // http_port [registry, node]: if specified, used in preference to the individual defaults for each HTTP API + // http_port [registry, node]: if specified, this becomes the default port for each HTTP API and the next higher port becomes the default for each WebSocket API const web::json::field_as_integer_or http_port{ U("http_port"), 0 }; const web::json::field_as_integer_or query_port{ U("query_port"), 3211 }; @@ -121,6 +149,8 @@ namespace nmos const web::json::field_as_integer_or channelmapping_port{ U("channelmapping_port"), 3215 }; // system_port [node]: used to construct request URLs for the System API (if not discovered via DNS-SD) const web::json::field_as_integer_or system_port{ U("system_port"), 10641 }; + // control_protocol_ws_port [node]: used to construct request URLs for the Control Protocol websocket, or negative to disable the control protocol features + const web::json::field_as_integer_or control_protocol_ws_port{ U("control_protocol_ws_port"), 3218 }; // listen_backlog [registry, node]: the maximum length of the queue of pending connections, or zero for the implementation default (the implementation may not honour this value) const web::json::field_as_integer_or listen_backlog{ U("listen_backlog"), 0 }; @@ -129,14 +159,19 @@ namespace nmos // this list is created and maintained by nmos::node_behaviour_thread; each entry is a uri like http://api.example.com/x-nmos/registration/{version} const web::json::field_as_value registration_services{ U("registration_services") }; - // registration_heartbeat_interval [node]: + // registration_heartbeat_interval [registry, node]: + // [registry]: used in System API resource is04 object's heartbeat_interval field + // "Constants related to the AMWA IS-04 Discovery and Registration Specification are contained in the is04 object. + // heartbeat_interval defines how often Nodes should perform a heartbeat to maintain their resources in the Registration API." + // See https://specs.amwa.tv/is-09/releases/v1.0.0/docs/4.2._Behaviour_-_Global_Configuration_Parameters.html#amwa-is-04-nmos-discovery-and-registration-parameters + // [node]: // "Nodes are expected to peform a heartbeat every 5 seconds by default." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md#heartbeating + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#heartbeating const web::json::field_as_integer_or registration_heartbeat_interval{ U("registration_heartbeat_interval"), 5 }; // registration_expiry_interval [registry]: // "Registration APIs should use a garbage collection interval of 12 seconds by default (triggered just after two failed heartbeats at the default 5 second interval)." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/4.1.%20Behaviour%20-%20Registration.md#heartbeating + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#heartbeating const web::json::field_as_integer_or registration_expiry_interval{ U("registration_expiry_interval"), 12 }; // registration_request_max [node]: timeout for interactions with the Registration API /resource endpoint @@ -162,13 +197,13 @@ namespace nmos // events_heartbeat_interval [node, client]: // "Upon connection, the client is required to report its health every 5 seconds in order to maintain its session and subscription." - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md#41-heartbeats + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#41-heartbeats const web::json::field_as_integer_or events_heartbeat_interval{ U("events_heartbeat_interval"), 5 }; // events_expiry_interval [node]: // "The server is expected to check health commands and after a 12 seconds timeout (2 consecutive missed health commands plus 2 seconds to allow for latencies) // it should clear the subscriptions for that particular client and close the websocket connection." - // See https://github.com/AMWA-TV/nmos-event-tally/blob/v1.0/docs/5.2.%20Transport%20-%20Websocket.md#41-heartbeats + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#41-heartbeats const web::json::field_as_integer_or events_expiry_interval{ U("events_expiry_interval"), 12 }; // system_services [node]: the discovered list of System APIs, in the order they should be used @@ -194,9 +229,12 @@ namespace nmos // seed id [registry, node]: optional, used to generate repeatable id values when running with the same configuration const web::json::field_as_string seed_id{ U("seed_id") }; - // label [registry, node]: used in resource description/label fields + // label [registry, node]: used in resource label field const web::json::field_as_string_or label{ U("label"), U("") }; + // description [registry, node]: used in resource description field + const web::json::field_as_string_or description{ U("description"), U("") }; + // registration_available [registry]: used to flag the Registration API as temporarily unavailable const web::json::field_as_bool_or registration_available{ U("registration_available"), true }; @@ -217,17 +255,26 @@ namespace nmos const web::json::field_as_integer_or mdns_port{ U("mdns_port"), 3208 }; const web::json::field_as_integer_or schemas_port{ U("schemas_port"), 3208 }; - // addresses [registry, node]: addresses on which to listen for each API, or empty string for the wildcard address + // addresses [registry, node]: IP addresses on which to listen for each API, or empty string for the wildcard address + + // server_address [registry, node]: if specified, this becomes the default address on which to listen for each API instead of the wildcard address + const web::json::field_as_string_or server_address{ U("server_address"), U("") }; + + // addresses [registry, node]: IP addresses on which to listen for specific APIs const web::json::field_as_string_or settings_address{ U("settings_address"), U("") }; const web::json::field_as_string_or logging_address{ U("logging_address"), U("") }; - // addresses [registry]: addresses on which to listen for each API, or empty string for the wildcard address + // addresses [registry]: IP addresses on which to listen for specific APIs const web::json::field_as_string_or admin_address{ U("admin_address"), U("") }; const web::json::field_as_string_or mdns_address{ U("mdns_address"), U("") }; const web::json::field_as_string_or schemas_address{ U("schemas_address"), U("") }; + // client_address [registry, node]: IP address of the network interface to bind client connections + // for now, only supporting HTTP/HTTPS client connections on Linux + const web::json::field_as_string_or client_address{ U("client_address"), U("") }; + // query_ws_paging_default/query_ws_paging_limit [registry]: default/maximum number of events per message when using the Query WebSocket API (a client may request a lower limit) const web::json::field_as_integer_or query_ws_paging_default{ U("query_ws_paging_default"), 10 }; const web::json::field_as_integer_or query_ws_paging_limit{ U("query_ws_paging_limit"), 100 }; @@ -256,37 +303,167 @@ namespace nmos const web::json::field_as_integer_or href_mode{ U("href_mode"), 0 }; // when omitted, a default heuristic is used // client_secure [registry, node]: whether clients should use a secure connection for communication (https and wss) - // when true, CA root certificates must also be configured + // when true, CA root certificates must also be configured, see nmos/certificate_settings.h const web::json::field_as_bool_or client_secure{ U("client_secure"), false }; - // ca_certificate_file [registry, node]: full path of certification authorities file in PEM format - // on Windows, if C++ REST SDK is built with CPPREST_HTTP_CLIENT_IMPL=winhttp (reported as "client=winhttp" by nmos::get_build_settings_info) - // the trusted root CA certificates must also be imported into the certificate store - const web::json::field_as_string_or ca_certificate_file{ U("ca_certificate_file"), U("") }; - // server_secure [registry, node]: whether server should listen for secure connection for communication (https and wss) // e.g. typically false when using a reverse proxy, or the same as client_secure otherwise - // when true, server certificates etc. must also be configured + // when true, server certificates etc. must also be configured, see nmos/certificate_settings.h const web::json::field_as_bool_or server_secure{ U("server_secure"), false }; - // private_key_files [registry, node]: full paths of private key files in PEM format - const web::json::field_as_value_or private_key_files{ U("private_key_files"), web::json::value::array() }; - - // certificate_chain_files [registry, node]: full paths of server certificate chain files which must be in PEM format and must be sorted - // starting with the server's certificate, followed by any intermediate CA certificates, and ending with the highest level (root) CA - // on Windows, if C++ REST SDK is built with CPPREST_HTTP_LISTENER_IMPL=httpsys (reported as "listener=httpsys" by nmos::get_build_settings_info) - // one of the certificates must also be bound to each port e.g. using 'netsh add sslcert' - const web::json::field_as_value_or certificate_chain_files{ U("certificate_chain_files"), web::json::value::array() }; - // validate_certificates [registry, node]: boolean value, false (ignore all server certificate validation errors), or true (do not ignore, the default behaviour) const web::json::field_as_bool_or validate_certificates{ U("validate_certificates"), true }; - // dh_param_file [registry, node]: Diffie-Hellman parameters file in PEM format for ephemeral key exchange support, or empty string for no support - const web::json::field_as_string_or dh_param_file{ U("dh_param_file"), U("") }; - // system_interval_min/system_interval_max [node]: used to poll for System API changes; default is about one hour const web::json::field_as_integer_or system_interval_min{ U("system_interval_min"), 3600 }; const web::json::field_as_integer_or system_interval_max{ U("system_interval_max"), 3660 }; + + // system_label [registry]: used in System API resource label field + const web::json::field_as_string_or system_label{ U("system_label"), U("") }; + + // system_description [registry]: used in System API resource description field + const web::json::field_as_string_or system_description{ U("system_description"), U("") }; + + // system_tags [registry]: used in System API resource tags field + // "Each tag has a single key, but MAY have multiple values. Each tags SHOULD be interpreted using the comparison of a single key value pair, + // with the comparison being case-insensitive following the Unicode Simple Case Folding specification." + // { + // "tag_1": [ "tag_1_value_1", "tag_1_value_2" ], + // "tag_2": [ "tag_2_value_1" ] + // } + // See https://specs.amwa.tv/is-09/releases/v1.0.0/docs/2.1._APIs_-_Common_Keys.html#tags + const web::json::field_as_value_or system_tags{ U("system_tags"), web::json::value::object() }; + + // "syslog contains hostname and port for the system's syslog "version 1" server using the UDP transport (IETF RFC 5246)" + // See https://specs.amwa.tv/is-09/releases/v1.0.0/docs/4.2._Behaviour_-_Global_Configuration_Parameters.html#syslog-parameters + + // system_syslog_host_name [registry]: the fully-qualified host name or the IP address of the system's syslog "version 1" server + const web::json::field_as_string_or system_syslog_host_name{ U("system_syslog_host_name"), U("") }; + + // system_syslog_port [registry]: the port number for the system's syslog "version 1" server + const web::json::field_as_integer_or system_syslog_port{ U("system_syslog_port"), 514 }; + + // "syslogv2 contains hostname and port for the system's syslog "version 2" server using the TLS transport (IETF RFC 5245)" + // See https://specs.amwa.tv/is-09/releases/v1.0.0/docs/4.2._Behaviour_-_Global_Configuration_Parameters.html#syslog-parameters + + // system_syslogv2_host_name [registry]: the fully-qualified host name or the IP address of the system's syslog "version 2" server + const web::json::field_as_string_or system_syslogv2_host_name{ U("system_syslogv2_host_name"), U("") }; + + // system_syslogv2_port [registry]: the port number for the system's syslog "version 2" server + const web::json::field_as_integer_or system_syslogv2_port{ U("system_syslogv2_port"), 6514 }; + + // hsts_max_age [registry, node]: the HTTP Strict-Transport-Security response header's max-age value; default is approximately 365 days + // (the header is omitted if server_secure is false, or hsts_max_age is negative) + // See https://tools.ietf.org/html/rfc6797#section-6.1.1 + const web::json::field_as_integer_or hsts_max_age{ U("hsts_max_age"), 31536000 }; + + // hsts_include_sub_domains [registry, node]: the HTTP Strict-Transport-Security HTTP response header's includeSubDomains value + // See https://tools.ietf.org/html/rfc6797#section-6.1.2 + const web::json::field_as_bool_or hsts_include_sub_domains{ U("hsts_include_sub_domains"), false }; + + // ocsp_interval_min/ocsp_interval_max [registry, node]: used to poll for certificate status (OCSP) changes; default is about one hour + // Note that if half of the server certificate expiry time is shorter, then the ocsp_interval_min/max will be overridden by it + const web::json::field_as_integer_or ocsp_interval_min{ U("ocsp_interval_min"), 3600 }; + const web::json::field_as_integer_or ocsp_interval_max{ U("ocsp_interval_max"), 3660 }; + + // ocsp_request_max [registry, node]: timeout for interactions with the OCSP server + const web::json::field_as_integer_or ocsp_request_max{ U("ocsp_request_max"), 30 }; + + // authorization_selector [registry, node]: used to construct request URLs for the authorization API (if not discovered via DNS-SD) + const web::json::field_as_string_or authorization_selector{ U("authorization_selector"), U("") }; + + // authorization_address [registry, node]: IP address or host name used to construct request URLs for Authorization APIs (if not discovered via DNS-SD) + const web::json::field_as_string authorization_address{ U("authorization_address") }; + + // authorization_port [registry, node]: used to construct request URLs for the authorization server's Authorization API (if not discovered via DNS-SD) + const web::json::field_as_integer_or authorization_port{ U("authorization_port"), 443 }; + + // authorization_version [registry, node]: used to construct request URLs for authorization APIs (if not discovered via DNS-SD) + const web::json::field_as_string_or authorization_version{ U("authorization_version"), U("v1.0") }; + + // authorization_services [registry, node]: the discovered list of Authorization APIs, in the order they should be used + // this list is created and maintained by nmos::authorization_operation_thread; each entry is a uri like http://example.api.com/x-nmos/auth/{version} + const web::json::field_as_value authorization_services{ U("authorization_services") }; + + // authorization_request_max [registry, node]: timeout for interactions with the Authorization API /certs & /token endpoints + const web::json::field_as_integer_or authorization_request_max{ U("authorization_request_max"), 30 }; + + // fetch_authorization_public_keys_interval_min/fetch_authorization_public_keys_interval_max [registry, node]: used to poll for Authorization API public keys changes; default is about one hour + // "Resource Servers (Nodes) SHOULD seek to fetch public keys from the Authorization Server at least once every hour. Resource Servers MUST vary their retrieval + // interval at random by up to at least one minute to avoid overloading the Authorization Server due to Resource Servers synchronising their retrieval time." + // See https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.1._Behaviour_-_Authorization_Servers.html#authorization-server-public-keys + const web::json::field_as_integer_or fetch_authorization_public_keys_interval_min{ U("fetch_authorization_public_keys_interval_min"), 3600 }; + const web::json::field_as_integer_or fetch_authorization_public_keys_interval_max{ U("fetch_authorization_public_keys_interval_max"), 3660 }; + + // access_token_refresh_interval [node]: time interval (in seconds) to refresh access token from Authorization Server + // It specified the access token refresh period otherwise Bearer token's expires_in is used instead. + // See https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#access-token-lifetime + const web::json::field_as_integer_or access_token_refresh_interval{ U("access_token_refresh_interval"), -1 }; + + // client_authorization [node]: whether clients should use authorization to access protected APIs + const web::json::field_as_bool_or client_authorization{ U("client_authorization"), false }; + + // server_authorization [registry, node]: whether server should use authorization to protect its APIs + const web::json::field_as_bool_or server_authorization{ U("server_authorization"), false }; + + // authorization_code_flow_max [node]: timeout for the authorization code workflow (in seconds) + // No timeout if value is set to -1, default to 30 seconds + const web::json::field_as_integer_or authorization_code_flow_max{ U("authorization_code_flow_max"), 30 }; + + // authorization_flow [node]: used to specify the authorization flow for the registered scopes + // supported flow are authorization_code and client_credentials + // client_credentials SHOULD only be used when the node/registry has NO user interface, otherwise authorization_code MUST be used + const web::json::field_as_string_or authorization_flow{ U("authorization_flow"), U("authorization_code") }; + + // authorization_redirect_port [node]: redirect URL port for listening authorization code, used for client registration + // see http_port + const web::json::field_as_integer_or authorization_redirect_port{ U("authorization_redirect_port"), 3218 }; + + // initial_access_token [node]: initial access token giving access to the client registration endpoint for non-opened registration + const web::json::field_as_string_or initial_access_token{ U("initial_access_token"), U("") }; + + // authorization_scopes [node]: used to specify the supported scopes for client registration + // supported scopes are registration, query, node, connection, events and channelmapping + const web::json::field_as_array authorization_scopes{ U("authorization_scopes") }; + + // token_endpoint_auth_method [node]: String indicator of the requested authentication method for the token endpoint + // supported methods are client_secret_basic and private_key_jwt, default to client_secret_basic + // when using private_key_jwt, the JWT is created and signed by the node's private key + const web::json::field_as_string_or token_endpoint_auth_method{ U("token_endpoint_auth_method"), U("client_secret_basic") }; + + // jwks_uri_port [node]: JWKs URL port for providing JSON Web Key Set (public keys) to Authorization Server for verifing client_assertion, used for client registration + // see http_port + const web::json::field_as_integer_or jwks_uri_port{ U("jwks_uri_port"), 3218 }; + + // validate_openid_client [node]: boolean value, false (bypass openid connect client validation), or true (do not bypass, the default behaviour) + const web::json::field_as_bool_or validate_openid_client{ U("validate_openid_client"), true }; + + // no_trailing_dot_for_authorization_callback_uri [node]: used to specify whether no trailing dot FQDN should be used to construct the URL for the authorization server callbacks + // as it is because not all Authorization server can cope with URL with trailing dot, default to true + const web::json::field_as_bool_or no_trailing_dot_for_authorization_callback_uri{ U("no_trailing_dot_for_authorization_callback_uri"), true }; + + // retry_after [registry, node]: used to specify the HTTP Retry-After header to indicate the number of seconds when the client may retry its request again, default to 5 seconds + // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss + // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it + // MAY temporarily respond with an HTTP 503 code in order to avoid blocking the incoming authorized request. When a HTTP 503 code is used, the Resource + // Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request. + // If the Resource Server fails to verify a token using all public keys available it MUST reject the token." + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + const web::json::field_as_integer_or service_unavailable_retry_after{ U("service_unavailable_retry_after"), 5 }; + + // manufacturer_name [node]: the manufacturer name of the NcDeviceManager used for NMOS Control Protocol + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicemanager + const web::json::field_as_string_or manufacturer_name{ U("manufacturer_name"), U("") }; + + // product_name/product_key/product_revision_level [node]: the product description of the NcDeviceManager used for NMOS Control Protocol + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncproduct + const web::json::field_as_string_or product_name{ U("product_name"), U("") }; + const web::json::field_as_string_or product_key{ U("product_key"), U("") }; + const web::json::field_as_string_or product_revision_level{ U("product_revision_level"), U("") }; + + // serial_number [node]: the serial number of the NcDeviceManager used for NMOS Control Protocol + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicemanager + const web::json::field_as_string_or serial_number{ U("serial_number"), U("") }; } } } diff --git a/Development/nmos/slog.h b/Development/nmos/slog.h index 248ab7b3e..3d027286c 100644 --- a/Development/nmos/slog.h +++ b/Development/nmos/slog.h @@ -16,6 +16,10 @@ namespace slog { return s << utility::conversions::to_utf8string(u16s); } + inline log_statement& operator<<(log_statement&& s, const utf16string& u16s) + { + return s << u16s; + } } namespace nmos @@ -25,6 +29,10 @@ namespace nmos { return s << id_type.second.name << ": " << id_type.first; } + inline slog::log_statement& operator<<(slog::log_statement&& s, const std::pair& id_type) + { + return s << id_type; + } // Log message categories typedef std::string category; @@ -43,6 +51,9 @@ namespace nmos const category events_expiry{ "events_expiry" }; const category send_events_ws_commands{ "send_events_ws_commands" }; const category node_system_behaviour{ "node_system_behaviour" }; + const category ocsp_behaviour{ "ocsp_behaviour" }; + const category authorization_behaviour{ "authorization_behaviour" }; + const category send_control_protocol_ws_messages{ "send_control_protocol_ws_messages" }; // other categories may be defined ad-hoc } diff --git a/Development/nmos/ssl_context_options.h b/Development/nmos/ssl_context_options.h index fcd80f71a..2039fbde7 100644 --- a/Development/nmos/ssl_context_options.h +++ b/Development/nmos/ssl_context_options.h @@ -1,7 +1,8 @@ #ifndef NMOS_SSL_CONTEXT_OPTIONS_H #define NMOS_SSL_CONTEXT_OPTIONS_H -#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) +// cf. preprocessor conditions in nmos/client_utils.cpp and nmos/server_utils.cpp +#if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) #include "boost/asio/ssl.hpp" namespace nmos @@ -12,7 +13,7 @@ namespace nmos // "Implementations SHALL NOT use TLS 1.0 or 1.1. These are deprecated." // "Implementations SHALL NOT use SSL. Although the SSL protocol has previously, // been used to secure HTTP traffic no version of SSL is now considered secure." - // See https://github.com/AMWA-TV/nmos-api-security/blob/master/best-practice-secure-comms.md#tls + // See https://specs.amwa.tv/bcp-003-01/releases/v1.0.0/docs/1.0._Secure_Communication.html#tls const auto ssl_context_options = ( boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::no_sslv3 diff --git a/Development/nmos/st2110_21_sender_type.h b/Development/nmos/st2110_21_sender_type.h new file mode 100644 index 000000000..047302c52 --- /dev/null +++ b/Development/nmos/st2110_21_sender_type.h @@ -0,0 +1,22 @@ +#ifndef NMOS_ST2110_21_SENDER_TYPE_H +#define NMOS_ST2110_21_SENDER_TYPE_H + +#include "nmos/string_enum.h" + +namespace nmos +{ + // ST 2110-21 Sender Type + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/sender-attributes/#st-2110-21-sender-type + DEFINE_STRING_ENUM(st2110_21_sender_type) + namespace st2110_21_sender_types + { + // Narrow Senders (Type N) + const st2110_21_sender_type type_N{ U("2110TPN") }; + // Narrow Linear Senders (Type NL) + const st2110_21_sender_type type_NL{ U("2110TPNL") }; + // Wide Senders (Type W) + const st2110_21_sender_type type_W{ U("2110TPW") }; + } +} + +#endif diff --git a/Development/nmos/string_enum.h b/Development/nmos/string_enum.h index 12d8dd0fb..c4be7c227 100644 --- a/Development/nmos/string_enum.h +++ b/Development/nmos/string_enum.h @@ -2,12 +2,13 @@ #define NMOS_STRING_ENUM_H #include "cpprest/details/basic_types.h" +#include "nmos/string_enum_fwd.h" namespace nmos { // Many of the JSON fields in the NMOS specifications are strings with an enumerated set of values. // Sometimes these enumerations are extensible (i.e. not a closed set), such as those for media types. - // string_enum is a base class using CRTP to implement type safe enums with simple conversion to string. + // string_enum is a base class using CRTP to implement type-safe enums with simple conversion to string. // See nmos/type.h for a usage example. template struct string_enum @@ -15,6 +16,8 @@ namespace nmos utility::string_t name; // could add explicit string conversion operator? + bool empty() const { return name.empty(); } + // totally_ordered rather than just equality_comparable only to allow use of type as a key // in associative containers; an alternative would be adding a std::hash override so that // unordered associative containers could be used instead? @@ -27,6 +30,7 @@ namespace nmos }; } +// Defines a type-safe extensible string enumeration type #define DEFINE_STRING_ENUM(Type) \ struct Type : public nmos::string_enum \ { \ diff --git a/Development/nmos/string_enum_fwd.h b/Development/nmos/string_enum_fwd.h new file mode 100644 index 000000000..c8f9d4dea --- /dev/null +++ b/Development/nmos/string_enum_fwd.h @@ -0,0 +1,8 @@ +#ifndef NMOS_STRING_ENUM_FWD_H +#define NMOS_STRING_ENUM_FWD_H + +// Declares a type-safe extensible string enumeration type +#define DECLARE_STRING_ENUM(Type) \ + struct Type; + +#endif diff --git a/Development/nmos/system_api.h b/Development/nmos/system_api.h index 2cf30f117..8e044fb3a 100644 --- a/Development/nmos/system_api.h +++ b/Development/nmos/system_api.h @@ -9,7 +9,7 @@ namespace slog } // System API implementation -// See https://github.com/AMWA-TV/nmos-system/blob/v1.0/APIs/SystemAPI.raml +// See https://specs.amwa.tv/is-09/releases/v1.0.0/APIs/SystemAPI.html namespace nmos { struct registry_model; diff --git a/Development/nmos/system_resources.cpp b/Development/nmos/system_resources.cpp index 4554d7e24..947f22e0f 100644 --- a/Development/nmos/system_resources.cpp +++ b/Development/nmos/system_resources.cpp @@ -1,21 +1,45 @@ #include "nmos/system_resources.h" +#include "cpprest/json_validator.h" #include "nmos/is09_versions.h" #include "nmos/json_fields.h" +#include "nmos/json_schema.h" #include "nmos/resource.h" namespace nmos { + namespace details + { + static const web::json::experimental::json_validator& system_validator() + { + // hmm, could be based on supported API versions from settings, like other APIs' validators? + static const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(is09_versions::all | boost::adaptors::transformed(experimental::make_systemapi_global_schema_uri)) + }; + return validator; + } + } + nmos::resource make_system_global(const nmos::id& id, const nmos::settings& settings) { - return{ is09_versions::v1_0, types::global, make_system_global_data(id, settings), true }; + const auto version = is09_versions::v1_0; + const auto data = make_system_global_data(id, settings); + + details::system_validator().validate(data, experimental::make_systemapi_global_schema_uri(version)); + + return{ version, types::global, std::move(data), true }; } web::json::value make_system_global_data(const nmos::id& id, const nmos::settings& settings) { using web::json::value_of; - auto data = details::make_resource_core(id, settings); + const auto& label = nmos::experimental::fields::system_label(settings); + const auto& description = nmos::experimental::fields::system_description(settings); + const auto& tags = nmos::experimental::fields::system_tags(settings); + auto data = details::make_resource_core(id, label, description, tags); data[nmos::fields::is04] = value_of({ { nmos::fields::heartbeat_interval, nmos::fields::registration_heartbeat_interval(settings) } @@ -25,24 +49,59 @@ namespace nmos { nmos::fields::domain_number, nmos::fields::ptp_domain_number(settings) } }); + const auto& system_syslog_host_name = nmos::experimental::fields::system_syslog_host_name(settings); + if (!system_syslog_host_name.empty()) + { + data[nmos::fields::syslog] = value_of({ + { nmos::fields::hostname, system_syslog_host_name }, + { nmos::fields::port, nmos::experimental::fields::system_syslog_port(settings) } + }); + } + + const auto& system_syslogv2_host_name = nmos::experimental::fields::system_syslogv2_host_name(settings); + if (!system_syslogv2_host_name.empty()) + { + data[nmos::fields::syslogv2] = value_of({ + { nmos::fields::hostname, system_syslogv2_host_name }, + { nmos::fields::port, nmos::experimental::fields::system_syslogv2_port(settings) } + }); + } + return data; } std::pair parse_system_global_data(const web::json::value& data) { + using web::json::value; using web::json::value_of; const auto& is04 = nmos::fields::is04(data); const auto& ptp = nmos::fields::ptp(data); - return{ - nmos::fields::id(data), - value_of({ - { nmos::fields::registration_heartbeat_interval, nmos::fields::heartbeat_interval(is04) }, - { nmos::fields::ptp_announce_receipt_timeout, nmos::fields::announce_receipt_timeout(ptp) }, - { nmos::fields::ptp_domain_number, nmos::fields::domain_number(ptp) } - }) - }; + auto settings = value_of({ + { nmos::experimental::fields::system_label, nmos::fields::label(data) }, + { nmos::experimental::fields::system_description, nmos::fields::description(data) }, + { nmos::fields::registration_heartbeat_interval, nmos::fields::heartbeat_interval(is04) }, + { nmos::fields::ptp_announce_receipt_timeout, nmos::fields::announce_receipt_timeout(ptp) }, + { nmos::fields::ptp_domain_number, nmos::fields::domain_number(ptp) }, + { nmos::experimental::fields::system_tags, data.at(nmos::fields::tags) } + }); + + const auto& syslog = nmos::fields::syslog(data); + if (!syslog.is_null()) + { + settings[nmos::experimental::fields::system_syslog_host_name] = value::string(nmos::fields::hostname(syslog)); + settings[nmos::experimental::fields::system_syslog_port] = nmos::fields::port(syslog); + } + + const auto& syslogv2 = nmos::fields::syslogv2(data); + if (!syslogv2.is_null()) + { + settings[nmos::experimental::fields::system_syslogv2_host_name] = value::string(nmos::fields::hostname(syslogv2)); + settings[nmos::experimental::fields::system_syslogv2_port] = nmos::fields::port(syslogv2); + } + + return{ nmos::fields::id(data), std::move(settings) }; } namespace experimental diff --git a/Development/nmos/system_resources.h b/Development/nmos/system_resources.h index 76bb77020..a01c3fa76 100644 --- a/Development/nmos/system_resources.h +++ b/Development/nmos/system_resources.h @@ -9,7 +9,7 @@ namespace nmos struct resource; // System API global configuration resource - // See https://github.com/AMWA-TV/nmos-system/blob/v1.0/APIs/schemas/global.json + // See https://specs.amwa.tv/is-09/releases/v1.0.0/APIs/schemas/with-refs/global.html nmos::resource make_system_global(const nmos::id& id, const nmos::settings& settings); web::json::value make_system_global_data(const nmos::id& id, const nmos::settings& settings); diff --git a/Development/nmos/tai.h b/Development/nmos/tai.h index c06752b7d..40176379e 100644 --- a/Development/nmos/tai.h +++ b/Development/nmos/tai.h @@ -40,7 +40,7 @@ namespace nmos typedef std::chrono::time_point time_point; // "It is important that there are no duplicate creation or update timestamps stored against resources." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.5.%20APIs%20-%20Query%20Parameters.md#pagination + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.5._APIs_-_Query_Parameters.html#pagination // Unfortunately, this clock is based on the system_clock, so may not produce monotonically increasing // time points; nmos::strictly_increasing_update is used to prevent duplicate values in nmos::resources static const bool is_steady = std::chrono::system_clock::is_steady; diff --git a/Development/nmos/test/capabilities_test.cpp b/Development/nmos/test/capabilities_test.cpp new file mode 100644 index 000000000..b477ca87b --- /dev/null +++ b/Development/nmos/test/capabilities_test.cpp @@ -0,0 +1,58 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/capabilities.h" + +#include "bst/test/test.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testMatchConstraint) +{ + using web::json::value; + + BST_REQUIRE(nmos::match_string_constraint(U("purr"), nmos::make_caps_string_constraint())); + BST_REQUIRE(nmos::match_integer_constraint(42, nmos::make_caps_integer_constraint({}))); + BST_REQUIRE(nmos::match_number_constraint(4.2, nmos::make_caps_number_constraint({}))); + BST_REQUIRE(nmos::match_boolean_constraint(true, nmos::make_caps_boolean_constraint({}))); + BST_REQUIRE(nmos::match_rational_constraint(nmos::rates::rate29_97, nmos::make_caps_rational_constraint())); + + BST_REQUIRE(nmos::match_string_constraint(U("purr"), nmos::make_caps_string_constraint({ U("meow"), U("purr"), U("hiss") }, U("^(meow|purr|hiss)$")))); + BST_REQUIRE(!nmos::match_string_constraint(U("purr"), nmos::make_caps_string_constraint({ U("meow"), U("hiss") }))); + BST_REQUIRE(nmos::match_string_constraint(U("purr"), nmos::make_caps_string_constraint({}, U("^(meow|purr|hiss)$")))); + BST_REQUIRE(!nmos::match_string_constraint(U("bark"), nmos::make_caps_string_constraint({}, U("^(meow|purr|hiss)$")))); + + BST_REQUIRE(nmos::match_integer_constraint(42, nmos::make_caps_integer_constraint({ 37, 42, 57 }, 37, 57))); + BST_REQUIRE(!nmos::match_integer_constraint(42, nmos::make_caps_integer_constraint({ 37, 57 }))); + for (auto i : { 37, 42, 57 }) + BST_REQUIRE(nmos::match_integer_constraint(i, nmos::make_caps_integer_constraint({}, 37, 57))); + for (auto i : { -100, 0, 100 }) + BST_REQUIRE(!nmos::match_integer_constraint(i, nmos::make_caps_integer_constraint({}, 37, 57))); + BST_REQUIRE(nmos::match_integer_constraint(INT64_C(0xBADC0FFEE), nmos::make_caps_integer_constraint({}, INT64_C(0xC0FFEE), INT64_C(0xC01DC0FFEE)))); + + BST_REQUIRE(nmos::match_number_constraint(4.2, nmos::make_caps_number_constraint({ 3.7, 4.2, 5.7 }, 3.7, 5.7))); + BST_REQUIRE(!nmos::match_number_constraint(4.2, nmos::make_caps_number_constraint({ 3.7, 5.7 }))); + for (auto d : { 3.7, 4.2, 5.7 }) + BST_REQUIRE(nmos::match_number_constraint(d, nmos::make_caps_number_constraint({}, 3.7, 5.7))); + for (auto d : { -10.0, 0.0, 10.0 }) + BST_REQUIRE(!nmos::match_number_constraint(d, nmos::make_caps_number_constraint({}, 3.7, 5.7))); + + BST_REQUIRE(nmos::match_boolean_constraint(true, nmos::make_caps_boolean_constraint({ false, true }))); + BST_REQUIRE(!nmos::match_boolean_constraint(true, nmos::make_caps_boolean_constraint({ false }))); + + BST_REQUIRE(nmos::match_rational_constraint(nmos::rates::rate29_97, nmos::make_caps_rational_constraint({ nmos::rates::rate25, nmos::rates::rate29_97, nmos::rates::rate30 }))); + BST_REQUIRE(!nmos::match_rational_constraint(nmos::rates::rate29_97, nmos::make_caps_rational_constraint({ nmos::rates::rate25, nmos::rates::rate30 }))); + for (auto r : { nmos::rates::rate25, nmos::rates::rate29_97, nmos::rates::rate30 }) + BST_REQUIRE(nmos::match_rational_constraint(r, nmos::make_caps_rational_constraint({}, nmos::rates::rate25, nmos::rates::rate30))); + for (auto r : { nmos::rational{}, nmos::rates::rate23_98, nmos::rates::rate59_94 }) + BST_REQUIRE(!nmos::match_rational_constraint(r, nmos::make_caps_rational_constraint({}, nmos::rates::rate25, nmos::rates::rate30))); + + BST_REQUIRE(nmos::match_constraint(value(U("purr")), nmos::make_caps_string_constraint({ U("meow"), U("purr"), U("hiss") }, U("^(meow|purr|hiss)$")))); + BST_REQUIRE(!nmos::match_constraint(value(U("purr")), nmos::make_caps_string_constraint({ U("meow"), U("hiss") }))); + BST_REQUIRE(nmos::match_constraint(value(42), nmos::make_caps_integer_constraint({ 37, 42, 57 }, 37, 57))); + BST_REQUIRE(!nmos::match_constraint(value(42), nmos::make_caps_integer_constraint({ 37, 57 }))); + BST_REQUIRE(nmos::match_constraint(value(INT64_C(0xBADC0FFEE)), nmos::make_caps_integer_constraint({}, INT64_C(0xC0FFEE), INT64_C(0xC01DC0FFEE)))); + BST_REQUIRE(nmos::match_constraint(value(4.2), nmos::make_caps_number_constraint({ 3.7, 4.2, 5.7 }, 3.7, 5.7))); + BST_REQUIRE(!nmos::match_constraint(value(4.2), nmos::make_caps_number_constraint({ 3.7, 5.7 }))); + BST_REQUIRE(nmos::match_constraint(value(true), nmos::make_caps_boolean_constraint({ false, true }))); + BST_REQUIRE(!nmos::match_constraint(value(true), nmos::make_caps_boolean_constraint({ false }))); + BST_REQUIRE(nmos::match_constraint(nmos::make_rational(nmos::rates::rate29_97), nmos::make_caps_rational_constraint({ nmos::rates::rate25, nmos::rates::rate29_97, nmos::rates::rate30 }))); + BST_REQUIRE(!nmos::match_constraint(nmos::make_rational(nmos::rates::rate29_97), nmos::make_caps_rational_constraint({ nmos::rates::rate25, nmos::rates::rate30 }))); +} diff --git a/Development/nmos/test/channels_test.cpp b/Development/nmos/test/channels_test.cpp index 1868ee853..ce316e592 100644 --- a/Development/nmos/test/channels_test.cpp +++ b/Development/nmos/test/channels_test.cpp @@ -28,3 +28,40 @@ BST_TEST_CASE(testMakeFmtpChannelOrder) const std::vector example_3{ M1, M1, M1, M1, L, R, C, LFE }; BST_REQUIRE_EQUAL(U("SMPTE2110.(M,M,M,M,ST,U02)"), nmos::make_fmtp_channel_order(example_3)); } + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testParseFmtpChannelOrder) +{ + using namespace nmos::channel_symbols; + + // two simple examples + + const std::vector stereo{ L, R }; + BST_REQUIRE_EQUAL(stereo, nmos::parse_fmtp_channel_order(U("SMPTE2110.(ST)"))); + + const std::vector dual_mono{ M1, M2 }; + BST_REQUIRE_EQUAL(dual_mono, nmos::parse_fmtp_channel_order(U("SMPTE2110.(DM)"))); + + // two examples from ST 2110-30:2017 Section 6.2.2 Channel Order Convention + + const std::vector example_1{ L, R, C, LFE, Ls, Rs, L, R }; + BST_REQUIRE_EQUAL(example_1, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,ST)"))); + + //const std::vector example_2{ M1, M1, M1, M1, L, R, Undefined(1), Undefined(2) }; + //BST_REQUIRE_EQUAL(example_2, nmos::parse_fmtp_channel_order(U("SMPTE2110.(M,M,M,M,ST,U02)"))); + + // bad examples + + const std::vector empty; + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,ST)BAD"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,ST,)"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,,ST)"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,BAD)"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,ST"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51,"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.(51"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110.("))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("SMPTE2110."))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U("BAD"))); + BST_REQUIRE_EQUAL(empty, nmos::parse_fmtp_channel_order(U(""))); +} diff --git a/Development/nmos/test/control_protocol_methods_test.cpp b/Development/nmos/test/control_protocol_methods_test.cpp new file mode 100644 index 000000000..d49370eed --- /dev/null +++ b/Development/nmos/test/control_protocol_methods_test.cpp @@ -0,0 +1,146 @@ +// The first "test" is of course whether the header compiles standalone +#include "boost/iostreams/stream.hpp" +#include "boost/iostreams/device/null.hpp" +#include "nmos/control_protocol_resource.h" +#include "nmos/control_protocol_resources.h" +#include "nmos/control_protocol_methods.h" +#include "nmos/control_protocol_state.h" +#include "nmos/control_protocol_typedefs.h" +#include "nmos/control_protocol_utils.h" +#include "nmos/is12_versions.h" +#include "nmos/json_fields.h" +#include "nmos/log_gate.h" +#include "nmos/slog.h" +#include "bst/test/test.h" + + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testRemoveSequenceItem) +{ + using web::json::value_of; + using web::json::value; + + bool property_changed_called = false; + + boost::iostreams::stream< boost::iostreams::null_sink > null_ostream((boost::iostreams::null_sink())); + + nmos::experimental::log_model log_model; + nmos::experimental::log_gate gate(null_ostream, null_ostream, log_model); + + nmos::resources resources; + nmos::control_protocol_property_changed_handler property_changed = [&property_changed_called](const nmos::resource& resource, const utility::string_t& property_name, int index) + { + // check that the property changed handler gets called + property_changed_called = true; + }; + nmos::experimental::control_protocol_state control_protocol_state(property_changed); + nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor = nmos::make_get_control_protocol_class_descriptor_handler(control_protocol_state); + + + // Create simple non-standard class with writable sequence property + const auto writable_sequence_class_id = nmos::make_nc_class_id(nmos::nc_worker_class_id, -1234, { 1000 }); + const web::json::field_as_array writable_value{ U("writableValue") }; + { + // Writable sequence_class property descriptors + std::vector writable_sequence_property_descriptors = { nmos::experimental::make_control_class_property_descriptor(U("Writable sequence"), { 3, 1 }, writable_value, U("NcInt16"), false, false, true, false, web::json::value::null()) }; + + // create writable_sequence class descriptor + auto writable_sequence_class_descriptor = nmos::experimental::make_control_class_descriptor(U("Writable sequence class descriptor"), writable_sequence_class_id, U("WritableSequence"), writable_sequence_property_descriptors); + + // insert writable_sequence class descriptor to global state, which will be used by the control_protocol_ws_message_handler to process incoming ws message + control_protocol_state.insert(writable_sequence_class_descriptor); + } + // helper function to create writable_sequence object + auto make_writable_sequence = [&writable_value, &writable_sequence_class_id](nmos::nc_oid oid, nmos::nc_oid owner, const utility::string_t& role, const utility::string_t& user_label, const utility::string_t& description) + { + auto data = nmos::details::make_nc_worker(writable_sequence_class_id, oid, true, owner, role, value::string(user_label), description, web::json::value::null(), web::json::value::null(), true); + auto values = value::array(); + web::json::push_back(values, value::number(10)); + web::json::push_back(values, value::number(9)); + web::json::push_back(values, value::number(8)); + data[writable_value] = values; + + return nmos::control_protocol_resource{ nmos::is12_versions::v1_0, nmos::types::nc_worker, std::move(data), true }; + }; + + // Create Device Model + // root + auto root_block = nmos::make_root_block(); + auto oid = nmos::root_block_oid; + // root, ClassManager + auto class_manager = nmos::make_class_manager(++oid, control_protocol_state); + auto receiver_block_oid = ++oid; + // root, receivers + auto receivers = nmos::make_block(receiver_block_oid, nmos::root_block_oid, U("receivers"), U("Receivers"), U("Receivers block")); + auto receivers_id = receivers.id; + + // root, receivers, mon1 + auto monitor1 = nmos::make_receiver_monitor(++oid, true, receiver_block_oid, U("mon1"), U("monitor 1"), U("monitor 1"), value::null()); + // root, receivers, mon2 + auto monitor2 = nmos::make_receiver_monitor(++oid, true, receiver_block_oid, U("mon2"), U("monitor 2"), U("monitor 2"), value::null()); + + auto writable_sequence = make_writable_sequence(++oid, nmos::root_block_oid, U("writableSequence"), U("writable sequence"), U("writable sequence")); + auto writable_sequence_id = writable_sequence.id; + + nmos::push_back(receivers, monitor1); + // add example-control to root-block + nmos::push_back(receivers, monitor2); + // add stereo-gain to root-block + nmos::push_back(root_block, receivers); + // add class-manager to root-block + nmos::push_back(root_block, class_manager); + // add writable sequence to root block + nmos::push_back(root_block, writable_sequence); + insert_resource(resources, std::move(root_block)); + insert_resource(resources, std::move(class_manager)); + insert_resource(resources, std::move(receivers)); + insert_resource(resources, std::move(monitor1)); + insert_resource(resources, std::move(monitor2)); + insert_resource(resources, std::move(writable_sequence)); + + // Attempt to remove a member from a block - read only error expected + { + property_changed_called = false; + + auto block_members_property_id = value_of({ + { U("level"), nmos::nc_block_members_property_id.level }, + { U("index"), nmos::nc_block_members_property_id.index}, + }); + + auto arguments = value_of({ + { nmos::fields::nc::id, block_members_property_id }, + { nmos::fields::nc::index, 0} + }); + + auto resource = nmos::find_resource(resources, receivers_id); + BST_CHECK_NE(resources.end(), resource); + auto result = nmos::remove_sequence_item(resources, *resource, arguments, false, get_control_protocol_class_descriptor, property_changed, gate); + + // Expect read only error, and for property changed not to be called + BST_CHECK_EQUAL(false, property_changed_called); + BST_CHECK_EQUAL(nmos::nc_method_status::read_only, nmos::fields::nc::status(result)); + } + + // Remove writable sequence item - success and property_changed event expected + { + property_changed_called = false; + + auto writable_sequence_property_id = value_of({ + { U("level"), 3 }, + { U("index"), 1}, + }); + + auto arguments = value_of({ + { nmos::fields::nc::id, writable_sequence_property_id }, + { nmos::fields::nc::index, 1} + }); + + auto resource = nmos::find_resource(resources, writable_sequence_id); + BST_CHECK_NE(resources.end(), resource); + auto result = nmos::remove_sequence_item(resources, *resource, arguments, false, get_control_protocol_class_descriptor, property_changed, gate); + + // Expect success, and property changed event + BST_CHECK_EQUAL(true, property_changed_called); + BST_CHECK_EQUAL(nmos::nc_method_status::ok, nmos::fields::nc::status(result)); + } +} diff --git a/Development/nmos/test/control_protocol_test.cpp b/Development/nmos/test/control_protocol_test.cpp new file mode 100644 index 000000000..408e4fd50 --- /dev/null +++ b/Development/nmos/test/control_protocol_test.cpp @@ -0,0 +1,1861 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/control_protocol_resource.h" +#include "nmos/control_protocol_state.h" +#include "nmos/control_protocol_typedefs.h" +#include "nmos/control_protocol_utils.h" + +#include "bst/test/test.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testNcClassDescriptor) +{ + using web::json::value_of; + using web::json::value; + + // NcObject + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/classes/1.html + + const auto property_class_id = value_of({ + { U("description"), U("Static value. All instances of the same class will have the same identity value") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 1 } + }) }, + { U("name"), U("classId") }, + { U("typeName"), U("NcClassId") }, + { U("isReadOnly"), true }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("isDeprecated"), false }, + { U("constraints"), value::null() } + }); + const auto property_class_id_ = nmos::details::make_nc_property_descriptor(U("Static value. All instances of the same class will have the same identity value"), nmos::nc_object_class_id_property_id, nmos::fields::nc::class_id, U("NcClassId"), true, false, false, false, value::null()); + BST_REQUIRE_EQUAL(property_class_id, property_class_id_); + + const auto property_oid = value_of({ + { U("description"), U("Object identifier") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 2 } + }) }, + { U("name"), U("oid") }, + { U("typeName"), U("NcOid") }, + { U("isReadOnly"), true }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("isDeprecated"), false }, + { U("constraints"), value::null() } + }); + const auto property_oid_ = nmos::details::make_nc_property_descriptor(U("Object identifier"), nmos::nc_object_oid_property_id, nmos::fields::nc::oid, U("NcOid"), true, false, false, false, value::null()); + BST_REQUIRE_EQUAL(property_oid, property_oid_); + + const auto property_constant_oid = value_of({ + { U("description"), U("TRUE iff OID is hardwired into device") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 3 } + }) }, + { U("name"), U("constantOid") }, + { U("typeName"), U("NcBoolean") }, + { U("isReadOnly"), true }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("isDeprecated"), false }, + { U("constraints"), value::null() } + }); + const auto property_constant_oid_ = nmos::details::make_nc_property_descriptor(U("TRUE iff OID is hardwired into device"), nmos::nc_object_constant_oid_property_id, nmos::fields::nc::constant_oid, U("NcBoolean"), true, false, false, false, value::null()); + BST_REQUIRE_EQUAL(property_constant_oid, property_constant_oid_); + + const auto property_owner = value_of({ + { U("description"), U("OID of containing block. Can only ever be null for the root block") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 4 } + }) }, + { U("name"), U("owner") }, + { U("typeName"), U("NcOid") }, + { U("isReadOnly"), true }, + { U("isNullable"), true }, + { U("isSequence"), false }, + { U("isDeprecated"), false }, + { U("constraints"), value::null() } + }); + const auto property_owner_ = nmos::details::make_nc_property_descriptor(U("OID of containing block. Can only ever be null for the root block"), nmos::nc_object_owner_property_id, nmos::fields::nc::owner, U("NcOid"), true, true, false, false, value::null()); + BST_REQUIRE_EQUAL(property_owner, property_owner_); + + const auto property_role = value_of({ + { U("description"), U("Role of object in the containing block") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 5 } + }) }, + { U("name"), U("role") }, + { U("typeName"), U("NcString") }, + { U("isReadOnly"), true }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("isDeprecated"), false }, + { U("constraints"), value::null() } + }); + const auto property_role_ = nmos::details::make_nc_property_descriptor(U("Role of object in the containing block"), nmos::nc_object_role_property_id, nmos::fields::nc::role, U("NcString"), true, false, false, false, value::null()); + BST_REQUIRE_EQUAL(property_role, property_role_); + + const auto property_user_label = value_of({ + { U("description"), U("Scribble strip") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 6 } + }) }, + { U("name"), U("userLabel") }, + { U("typeName"), U("NcString") }, + { U("isReadOnly"), false }, + { U("isNullable"), true }, + { U("isSequence"), false }, + { U("isDeprecated"), false }, + { U("constraints"), value::null() } + }); + const auto property_user_label_ = nmos::details::make_nc_property_descriptor(U("Scribble strip"), nmos::nc_object_user_label_property_id, nmos::fields::nc::user_label, U("NcString"), false, true, false, false, value::null()); + BST_REQUIRE_EQUAL(property_user_label, property_user_label_); + + const auto property_touchpoints = value_of({ + { U("description"), U("Touchpoints to other contexts") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 7 } + }) }, + { U("name"), U("touchpoints") }, + { U("typeName"), U("NcTouchpoint") }, + { U("isReadOnly"), true }, + { U("isNullable"), true }, + { U("isSequence"), true }, + { U("isDeprecated"), false }, + { U("constraints"), value::null() } + }); + const auto property_touchpoints_ = nmos::details::make_nc_property_descriptor(U("Touchpoints to other contexts"), nmos::nc_object_touchpoints_property_id, nmos::fields::nc::touchpoints, U("NcTouchpoint"), true, true, true, false, value::null()); + BST_REQUIRE_EQUAL(property_touchpoints, property_touchpoints_); + + const auto property_runtime_property_constraints = value_of({ + { U("description"), U("Runtime property constraints") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 8 } + }) }, + { U("name"), U("runtimePropertyConstraints") }, + { U("typeName"), U("NcPropertyConstraints") }, + { U("isReadOnly"), true }, + { U("isNullable"), true }, + { U("isSequence"), true }, + { U("isDeprecated"), false }, + { U("constraints"), value::null() } + }); + const auto property_runtime_property_constraints_ = nmos::details::make_nc_property_descriptor(U("Runtime property constraints"), nmos::nc_object_runtime_property_constraints_property_id, nmos::fields::nc::runtime_property_constraints, U("NcPropertyConstraints"), true, true, true, false, value::null()); + BST_REQUIRE_EQUAL(property_runtime_property_constraints, property_runtime_property_constraints_); + + const auto method_get = value_of({ + { U("description"), U("Get property value") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 1 } + }) }, + { U("name"), U("Get") }, + { U("resultDatatype"), U("NcMethodResultPropertyValue") }, + { U("parameters"), value_of({ + value_of({ + { U("description"), U("Property id") }, + { U("name"), U("id") }, + { U("typeName"), U("NcPropertyId") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }) + }) }, + { U("isDeprecated"), false } + }); + + { + auto parameters = value::array(); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + const auto method_get_ = nmos::details::make_nc_method_descriptor(U("Get property value"), nmos::nc_object_get_method_id, U("Get"), U("NcMethodResultPropertyValue"), parameters, false); + + BST_REQUIRE_EQUAL(method_get, method_get_); + } + + const auto method_set = value_of({ + { U("description"), U("Set property value") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 2 } + }) }, + { U("name"), U("Set") }, + { U("resultDatatype"), U("NcMethodResult") }, + { U("parameters"), value_of({ + value_of({ + { U("description"), U("Property id") }, + { U("name"), U("id") }, + { U("typeName"), U("NcPropertyId") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }), + value_of({ + { U("description"), U("Property value") }, + { U("name"), U("value") }, + { U("typeName"), value::null() }, + { U("isNullable"), true }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }) + }) }, + { U("isDeprecated"), false } + }); + + { + auto parameters = value::array(); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Property value"), nmos::fields::nc::value, true, false, value::null())); + const auto method_set_ = nmos::details::make_nc_method_descriptor(U("Set property value"), nmos::nc_object_set_method_id, U("Set"), U("NcMethodResult"), parameters, false); + + BST_REQUIRE_EQUAL(method_set, method_set_); + } + + const auto method_get_sequence_item = value_of({ + { U("description"), U("Get sequence item") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 3 } + }) }, + { U("name"), U("GetSequenceItem") }, + { U("resultDatatype"), U("NcMethodResultPropertyValue") }, + { U("parameters"), value_of({ + value_of({ + { U("description"), U("Property id") }, + { U("name"), U("id") }, + { U("typeName"), U("NcPropertyId") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }), + value_of({ + { U("description"), U("Index of item in the sequence") }, + { U("name"), U("index") }, + { U("typeName"), U("NcId")}, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }) + }) }, + { U("isDeprecated"), false } + }); + + { + auto parameters = value::array(); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Index of item in the sequence"), nmos::fields::nc::index, U("NcId"), false, false, value::null())); + const auto method_get_sequence_item_ = nmos::details::make_nc_method_descriptor(U("Get sequence item"), nmos::nc_object_get_sequence_item_method_id, U("GetSequenceItem"), U("NcMethodResultPropertyValue"), parameters, false); + + BST_REQUIRE_EQUAL(method_get_sequence_item, method_get_sequence_item_); + } + + const auto method_set_sequence_item = value_of({ + { U("description"), U("Set sequence item value") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 4 } + }) }, + { U("name"), U("SetSequenceItem") }, + { U("resultDatatype"), U("NcMethodResult") }, + { U("parameters"), value_of({ + value_of({ + { U("description"), U("Property id") }, + { U("name"), U("id") }, + { U("typeName"), U("NcPropertyId") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }), + value_of({ + { U("description"), U("Index of item in the sequence") }, + { U("name"), U("index") }, + { U("typeName"), U("NcId") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }), + value_of({ + { U("description"), U("Value") }, + { U("name"), U("value") }, + { U("typeName"), value::null() }, + { U("isNullable"), true }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }) + }) }, + { U("isDeprecated"), false } + }); + + { + auto parameters = value::array(); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Index of item in the sequence"), nmos::fields::nc::index, U("NcId"), false, false, value::null())); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Value"), nmos::fields::nc::value, true, false, value::null())); + const auto method_set_sequence_item_ = nmos::details::make_nc_method_descriptor(U("Set sequence item value"), nmos::nc_object_set_sequence_item_method_id, U("SetSequenceItem"), U("NcMethodResult"), parameters, false); + + BST_REQUIRE_EQUAL(method_set_sequence_item, method_set_sequence_item_); + } + + const auto method_add_sequence_item = value_of({ + { U("description"), U("Add item to sequence") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 5 } + }) }, + { U("name"), U("AddSequenceItem") }, + { U("resultDatatype"), U("NcMethodResultId") }, + { U("parameters"), value_of({ + value_of({ + { U("description"), U("Property id") }, + { U("name"), U("id") }, + { U("typeName"), U("NcPropertyId") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }), + value_of({ + { U("description"), U("Value") }, + { U("name"), U("value") }, + { U("typeName"), value::null() }, + { U("isNullable"), true }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }) + }) }, + { U("isDeprecated"), false } + }); + + { + auto parameters = value::array(); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Value"), nmos::fields::nc::value, true, false, value::null())); + const auto method_add_sequence_item_ = nmos::details::make_nc_method_descriptor(U("Add item to sequence"), nmos::nc_object_add_sequence_item_method_id, U("AddSequenceItem"), U("NcMethodResultId"), parameters, false); + + BST_REQUIRE_EQUAL(method_add_sequence_item, method_add_sequence_item_); + } + + const auto method_remove_sequence_item = value_of({ + { U("description"), U("Delete sequence item") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 6 } + }) }, + { U("name"), U("RemoveSequenceItem") }, + { U("resultDatatype"), U("NcMethodResult") }, + { U("parameters"), value_of({ + value_of({ + { U("description"), U("Property id") }, + { U("name"), U("id") }, + { U("typeName"), U("NcPropertyId") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }), + value_of({ + { U("description"), U("Index of item in the sequence") }, + { U("name"), U("index") }, + { U("typeName"), U("NcId") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }) + }) }, + { U("isDeprecated"), false } + }); + + { + auto parameters = value::array(); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Index of item in the sequence"), nmos::fields::nc::index, U("NcId"), false, false, value::null())); + const auto method_remove_sequence_item_ = nmos::details::make_nc_method_descriptor(U("Delete sequence item"), nmos::nc_object_remove_sequence_item_method_id, U("RemoveSequenceItem"), U("NcMethodResult"), parameters, false); + + BST_REQUIRE_EQUAL(method_remove_sequence_item, method_remove_sequence_item_); + } + + const auto method_get_sequence_length = value_of({ + { U("description"), U("Get sequence length") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 7 } + }) }, + { U("name"), U("GetSequenceLength") }, + { U("resultDatatype"), U("NcMethodResultLength") }, + { U("parameters"), value_of({ + value_of({ + { U("description"), U("Property id") }, + { U("name"), U("id") }, + { U("typeName"), U("NcPropertyId") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }) + }) }, + { U("isDeprecated"), false } + }); + + { + auto parameters = value::array(); + web::json::push_back(parameters, nmos::details::make_nc_parameter_descriptor(U("Property id"), nmos::fields::nc::id, U("NcPropertyId"), false, false, value::null())); + const auto method_get_sequence_length_ = nmos::details::make_nc_method_descriptor(U("Get sequence length"), nmos::nc_object_get_sequence_length_method_id, U("GetSequenceLength"), U("NcMethodResultLength"), parameters, false); + + BST_REQUIRE_EQUAL(method_get_sequence_length, method_get_sequence_length_); + } + + const auto event_property_changed = value_of({ + { U("description"), U("Property changed event") }, + { U("id"), value_of({ + { U("level"), 1 }, + { U("index"), 1 } + }) }, + { U("name"), U("PropertyChanged") }, + { U("eventDatatype"), U("NcPropertyChangedEventData") }, + { U("isDeprecated"), false } + }); + + const auto event_property_changed_ = nmos::details::make_nc_event_descriptor(U("Property changed event"), nmos::nc_object_property_changed_event_id, U("PropertyChanged"), U("NcPropertyChangedEventData"), false); + BST_REQUIRE_EQUAL(event_property_changed, event_property_changed_); + + const auto nc_object_class = value_of({ + { U("description"), U("NcObject class descriptor") }, + { U("classId"), value_of({ + { 1 } + }) }, + { U("name"), U("NcObject") }, + { U("fixedRole"), value::null() }, + { U("properties"), value_of({ + property_class_id, + property_oid, + property_constant_oid, + property_owner, + property_role, + property_user_label, + property_touchpoints, + property_runtime_property_constraints + }) }, + { U("methods"), value_of({ + method_get, + method_set, + method_get_sequence_item, + method_set_sequence_item, + method_add_sequence_item, + method_remove_sequence_item, + method_get_sequence_length + }) }, + { U("events"), value_of({ + event_property_changed + }) } + }); + const auto nc_object_class_ = nmos::details::make_nc_class_descriptor(U("NcObject class descriptor"), nmos::nc_object_class_id, U("NcObject"), nmos::make_nc_object_properties(), nmos::make_nc_object_methods(), nmos::make_nc_object_events()); + BST_REQUIRE_EQUAL(nc_object_class, nc_object_class_); +} + +BST_TEST_CASE(testNcDatatypeDescriptorStruct) +{ + using web::json::value_of; + using web::json::value; + + // NcBlockMemberDescriptor + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcBlockMemberDescriptor.html + const auto nc_datatype_descriptor = value_of({ + { U("description"), U("Descriptor which is specific to a block member") }, + { U("name"), U("NcBlockMemberDescriptor") }, + { U("type"), 2 }, + { U("fields"), value_of({ + value_of({ + { U("description"), U("Role of member in its containing block") }, + { U("name"), U("role") }, + { U("typeName"), U("NcString") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }), + value_of({ + { U("description"), U("OID of member") }, + { U("name"), U("oid") }, + { U("typeName"), U("NcOid") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }), + value_of({ + { U("description"), U("TRUE iff member's OID is hardwired into device") }, + { U("name"), U("constantOid") }, + { U("typeName"), U("NcBoolean") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }), + value_of({ + { U("description"), U("Class ID") }, + { U("name"), U("classId") }, + { U("typeName"), U("NcClassId") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }), + value_of({ + { U("description"), U("User label") }, + { U("name"), U("userLabel") }, + { U("typeName"), U("NcString") }, + { U("isNullable"), true }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }), + value_of({ + { U("description"), U("Containing block's OID") }, + { U("name"), U("owner") }, + { U("typeName"), U("NcOid") }, + { U("isNullable"), false }, + { U("isSequence"), false }, + { U("constraints"), value::null() } + }) + }) }, + { U("parentType"), U("NcDescriptor") }, + { U("constraints"), value::null() } + }); + + auto fields = value::array(); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Role of member in its containing block"), nmos::fields::nc::role, U("NcString"), false, false, value::null())); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("OID of member"), nmos::fields::nc::oid, U("NcOid"), false, false, value::null())); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("TRUE iff member's OID is hardwired into device"), nmos::fields::nc::constant_oid, U("NcBoolean"), false, false, value::null())); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Class ID"), nmos::fields::nc::class_id, U("NcClassId"), false, false, value::null())); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("User label"), nmos::fields::nc::user_label, U("NcString"), true, false, value::null())); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Containing block's OID"), nmos::fields::nc::owner, U("NcOid"), false, false, value::null())); + const auto nc_datatype_descriptor_ = nmos::details::make_nc_datatype_descriptor_struct(U("Descriptor which is specific to a block member"), U("NcBlockMemberDescriptor"), fields, U("NcDescriptor"), value::null()); + + BST_REQUIRE_EQUAL(nc_datatype_descriptor, nc_datatype_descriptor_); +} + +BST_TEST_CASE(testNcDatatypeTypedef) +{ + using web::json::value_of; + using web::json::value; + + // NcClassId + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcClassId.html + const auto nc_class_id = value_of({ + { U("description"), U("Sequence of class ID fields") }, + { U("name"), U("NcClassId") }, + { U("type"), 1 }, + { U("parentType"), U("NcInt32") }, + { U("isSequence"), true }, + { U("constraints"), value::null() } + }); + const auto nc_class_id_ = nmos::details::make_nc_datatype_typedef(U("Sequence of class ID fields"), U("NcClassId"), true, U("NcInt32"), value::null()); + + BST_REQUIRE_EQUAL(nc_class_id, nc_class_id_); +} + +BST_TEST_CASE(testNcDatatypeDescriptorEnum) +{ + using web::json::value_of; + using web::json::value; + + // NcDeviceGenericState + // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/models/datatypes/NcDeviceGenericState.html + const auto nc_device_generic_state = value_of({ + { U("description"), U("Device generic operational state") }, + { U("name"), U("NcDeviceGenericState") }, + { U("type"), 3 }, + { U("items"), value_of({ + value_of({ + { U("description"), U("Unknown") }, + { U("name"), U("Unknown") }, + { U("value"), 0 } + }), + value_of({ + { U("description"), U("Normal operation") }, + { U("name"), U("NormalOperation") }, + { U("value"), 1 } + }), + value_of({ + { U("description"), U("Device is initializing") }, + { U("name"), U("Initializing") }, + { U("value"), 2 } + }), + value_of({ + { U("description"), U("Device is performing a software or firmware update") }, + { U("name"), U("Updating") }, + { U("value"), 3 } + }), + value_of({ + { U("description"), U("Device is experiencing a licensing error") }, + { U("name"), U("LicensingError") }, + { U("value"), 4 } + }), + value_of({ + { U("description"), U("Device is experiencing an internal error") }, + { U("name"), U("InternalError") }, + { U("value"), 5 } + }) + }) }, + { U("constraints"), value::null() } + }); + + auto items = value::array(); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("Unknown"), U("Unknown"), 0)); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("Normal operation"), U("NormalOperation"), 1)); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("Device is initializing"), U("Initializing"), 2)); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("Device is performing a software or firmware update"), U("Updating"), 3)); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("Device is experiencing a licensing error"), U("LicensingError"), 4)); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("Device is experiencing an internal error"), U("InternalError"), 5)); + const auto nc_device_generic_state_ = nmos::details::make_nc_datatype_descriptor_enum(U("Device generic operational state"), U("NcDeviceGenericState"), items, value::null()); + + BST_REQUIRE_EQUAL(nc_device_generic_state, nc_device_generic_state_); +} + +BST_TEST_CASE(testNcDatatypeDescriptorPrimitive) +{ + using web::json::value_of; + using web::json::value; + + const auto test_primitive = value_of({ + { U("description"), U("Primitive datatype descriptor") }, + { U("name"), U("test_primitive") }, + { U("type"), 0 }, + { U("constraints"), value::null() } + }); + + const auto test_primitive_ = nmos::details::make_nc_datatype_descriptor_primitive(U("Primitive datatype descriptor"), U("test_primitive"), value::null()); + + BST_REQUIRE_EQUAL(test_primitive, test_primitive_); +} + +BST_TEST_CASE(testNcClassId) +{ + BST_REQUIRE_EQUAL(false, nmos::is_nc_block({ })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_block({ 1 })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_block({ 1, 2 })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_block({ 1, 2, 0 })); + BST_REQUIRE(nmos::is_nc_block(nmos::nc_block_class_id)); + BST_REQUIRE(nmos::is_nc_block(nmos::make_nc_class_id(nmos::nc_block_class_id, { 1 }))); + + BST_REQUIRE_EQUAL(false, nmos::is_nc_worker({ })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_worker({ 1 })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_worker({ 1, 1 })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_worker({ 1, 1, 1 })); + BST_REQUIRE(nmos::is_nc_worker(nmos::nc_worker_class_id)); + BST_REQUIRE(nmos::is_nc_worker(nmos::make_nc_class_id(nmos::nc_worker_class_id, { 1 }))); + + BST_REQUIRE_EQUAL(false, nmos::is_nc_manager({ })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_manager({ 1 })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_manager({ 1, 1 })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_manager({ 1, 1, 1 })); + BST_REQUIRE(nmos::is_nc_manager(nmos::nc_manager_class_id)); + BST_REQUIRE(nmos::is_nc_manager(nmos::make_nc_class_id(nmos::nc_manager_class_id, { 1 }))); + + BST_REQUIRE_EQUAL(false, nmos::is_nc_device_manager({ })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_device_manager({ 1 })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_device_manager({ 1, 1 })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_device_manager({ 1, 1, 1 })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_device_manager({ 1, 3, 2 })); + BST_REQUIRE(nmos::is_nc_device_manager(nmos::nc_device_manager_class_id)); + BST_REQUIRE(nmos::is_nc_device_manager(nmos::make_nc_class_id(nmos::nc_device_manager_class_id, { 1 }))); + + BST_REQUIRE_EQUAL(false, nmos::is_nc_class_manager({ })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_class_manager({ 1 })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_class_manager({ 1, 1 })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_class_manager({ 1, 1, 1 })); + BST_REQUIRE_EQUAL(false, nmos::is_nc_class_manager({ 1, 3, 1 })); + BST_REQUIRE(nmos::is_nc_class_manager(nmos::nc_class_manager_class_id)); + BST_REQUIRE(nmos::is_nc_class_manager(nmos::make_nc_class_id(nmos::nc_class_manager_class_id, { 1 }))); +} + +BST_TEST_CASE(testFindProperty) +{ + auto& nc_block_members_property_id = nmos::nc_block_members_property_id; + auto& nc_block_class_id = nmos::nc_block_class_id; + auto& nc_worker_class_id = nmos::nc_worker_class_id; + const auto invalid_property_id = nmos::nc_property_id(1000, 1000); + const auto invalid_class_id = nmos::nc_class_id({ 1000, 1000 }); + + nmos::experimental::control_protocol_state control_protocol_state(nullptr); + auto get_control_protocol_class_descriptor = nmos::make_get_control_protocol_class_descriptor_handler(control_protocol_state); + + { + // valid - find members property in NcBlock + auto property = nmos::find_property_descriptor(nc_block_members_property_id, nc_block_class_id, get_control_protocol_class_descriptor); + BST_REQUIRE(!property.is_null()); + } + { + // invalid - find members property in NcWorker + auto property = nmos::find_property_descriptor(nc_block_members_property_id, nc_worker_class_id, get_control_protocol_class_descriptor); + BST_REQUIRE(property.is_null()); + } + { + // invalid - find unknown propertry in NcBlock + auto property = nmos::find_property_descriptor(invalid_property_id, nc_block_class_id, get_control_protocol_class_descriptor); + BST_REQUIRE(property.is_null()); + } + { + // invalid - find unknown property in unknown class + auto property = nmos::find_property_descriptor(invalid_property_id, invalid_class_id, get_control_protocol_class_descriptor); + BST_REQUIRE(property.is_null()); + } +} + +BST_TEST_CASE(testConstraints) +{ + using web::json::value_of; + using web::json::value; + + const nmos::nc_property_id property_string_id{ 100, 1 }; + const nmos::nc_property_id property_int32_id{ 100, 2 }; + const nmos::nc_property_id unknown_property_id{ 100, 3 }; + + // constraints + + // runtime constraints + const auto runtime_property_string_constraints = nmos::details::make_nc_property_constraints_string(property_string_id, 10, U("^[0-9]+$")); + const auto runtime_property_int32_constraints = nmos::details::make_nc_property_constraints_number(property_int32_id, 10, 1000, 1); + + const auto runtime_property_constraints = value_of({ + { runtime_property_string_constraints }, + { runtime_property_int32_constraints } + }); + + // property constraints + const auto property_string_constraints = nmos::details::make_nc_parameter_constraints_string(5, U("^[a-z]+$")); + const auto property_int32_constraints = nmos::details::make_nc_parameter_constraints_number(50, 500, 5); + + // datatype constraints + const auto datatype_string_constraints = nmos::details::make_nc_parameter_constraints_string(2, U("^[0-9a-z]+$")); + const auto datatype_int32_constraints = nmos::details::make_nc_parameter_constraints_number(100, 250, 10); + + // datatypes + const auto no_constraints_bool_datatype = nmos::details::make_nc_datatype_typedef(U("No constraints boolean datatype"), U("NoConstraintsBoolean"), false, U("NcBoolean"), value::null()); + const auto no_constraints_int16_datatype = nmos::details::make_nc_datatype_typedef(U("No constraints int16 datatype"), U("NoConstraintsInt16"), false, U("NcInt16"), value::null()); + const auto no_constraints_int32_datatype = nmos::details::make_nc_datatype_typedef(U("No constraints int32 datatype"), U("NoConstraintsInt32"), false, U("NcInt32"), value::null()); + const auto no_constraints_int64_datatype = nmos::details::make_nc_datatype_typedef(U("No constraints int64 datatype"), U("NoConstraintsInt64"), false, U("NcInt64"), value::null()); + const auto no_constraints_uint16_datatype = nmos::details::make_nc_datatype_typedef(U("No constraints uint16 datatype"), U("NoConstraintsUint16"), false, U("NcUint16"), value::null()); + const auto no_constraints_uint32_datatype = nmos::details::make_nc_datatype_typedef(U("No constraints uint32 datatype"), U("NoConstraintsUint32"), false, U("NcUint32"), value::null()); + const auto no_constraints_uint64_datatype = nmos::details::make_nc_datatype_typedef(U("No constraints uint64 datatype"), U("NoConstraintsUint64"), false, U("NcUint64"), value::null()); + const auto no_constraints_float32_datatype = nmos::details::make_nc_datatype_typedef(U("No constraints float32 datatype"), U("NoConstraintsFloat32"), false, U("NcFloat32"), value::null()); + const auto no_constraints_float64_datatype = nmos::details::make_nc_datatype_typedef(U("No constraints float64 datatype"), U("NoConstraintsFloat64"), false, U("NcFloat64"), value::null()); + const auto no_constraints_string_datatype = nmos::details::make_nc_datatype_typedef(U("No constraints string datatype"), U("NoConstraintsString"), false, U("NcString"), value::null()); + const auto with_constraints_string_datatype = nmos::details::make_nc_datatype_typedef(U("With constraints string datatype"), U("WithConstraintsString"), false, U("NcString"), datatype_string_constraints); + const auto with_constraints_int32_datatype = nmos::details::make_nc_datatype_typedef(U("With constraints int32 datatype"), U("WithConstraintsInt32"), false, U("NcInt32"), datatype_int32_constraints); + const auto no_constraints_int32_seq_datatype = nmos::details::make_nc_datatype_typedef(U("No constraints int64 datatype"), U("NoConstraintsInt64"), true, U("NcInt32"), value::null()); + const auto no_constraints_string_seq_datatype = nmos::details::make_nc_datatype_typedef(U("No constraints string datatype"), U("NoConstraintsString"), true, U("NcString"), value::null()); + + enum enum_value { foo, bar, baz }; + auto items = value::array(); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("foo"), U("foo"), enum_value::foo)); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("bar"), U("bar"), enum_value::bar)); + web::json::push_back(items, nmos::details::make_nc_enum_item_descriptor(U("baz"), U("baz"), enum_value::baz)); + const auto enum_datatype = nmos::details::make_nc_datatype_descriptor_enum(U("enum datatype"), U("enumDatatype"), items, value::null()); // no datatype constraints for enum datatype + + auto simple_struct_fields = value::array(); + web::json::push_back(simple_struct_fields, nmos::details::make_nc_field_descriptor(U("simple enum property example"), U("simpleEnumProperty"), U("enumDatatype"), false, false, value::null())); // no field constraints for enum field, as it is already described by its type + web::json::push_back(simple_struct_fields, nmos::details::make_nc_field_descriptor(U("simple string property example"), U("simpleStringProperty"), U("NcString"), false, false, datatype_string_constraints)); + web::json::push_back(simple_struct_fields, nmos::details::make_nc_field_descriptor(U("simple number property example"), U("simpleNumberProperty"), U("NcInt32"), false, false, datatype_int32_constraints)); + web::json::push_back(simple_struct_fields, nmos::details::make_nc_field_descriptor(U("simle boolean property example"), U("simpleBooleanProperty"), U("NcBoolean"), false, false, value::null())); // no field constraints for boolean field, as it is already described by its type + const auto simple_struct_datatype = nmos::details::make_nc_datatype_descriptor_struct(U("simple struct datatype"), U("simpleStructDatatype"), simple_struct_fields, value::null()); // no datatype constraints for struct datatype + + auto fields = value::array(); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Enum property example"), U("enumProperty"), U("enumDatatype"), false, false, value::null())); // no field constraints for enum field, as it is already described by its type + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("String property example"), U("stringProperty"), U("NcString"), false, false, datatype_string_constraints)); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Number property example"), U("numberProperty"), U("NcInt32"), false, false, datatype_int32_constraints)); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Boolean property example"), U("booleanProperty"), U("NcBoolean"), false, false, value::null())); // no field constraints for boolean field, as it is already described by its type + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Struct property example"), U("structProperty"), U("simpleStructDatatype"), false, false, value::null())); // no datatype constraints for struct datatype + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Sequence enum property example"), U("sequenceEnumProperty"), U("enumDatatype"), false, true, value::null())); // no field constraints for enum field, as it is already described by its type + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Sequence string property example"), U("sequenceStringProperty"), U("NcString"), false, true, datatype_string_constraints)); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Sequence number property example"), U("sequenceNumberProperty"), U("NcInt32"), false, true, datatype_int32_constraints)); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Sequence boolean property example"), U("sequenceBooleanProperty"), U("NcBoolean"), false, true, value::null())); // no field constraints for boolean field, as it is already described by its type + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Sequence struct property example"), U("sequenceStructProperty"), U("simpleStructDatatype"), false, true, value::null())); // no field constraints for struct field + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Nullable Enum property example"), U("enumPropertyNullable"), U("enumDatatype"), true, false, value::null())); // no field constraints for enum field, as it is already described by its type + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Nullable String property example"), U("stringPropertyNullable"), U("NcString"), true, false, datatype_string_constraints)); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Nullable Number property example"), U("numberPropertyNullable"), U("NcInt32"), true, false, datatype_int32_constraints)); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Nullable Boolean property example"), U("booleanPropertyNullable"), U("NcBoolean"), true, false, value::null())); // no field constraints for boolean field, as it is already described by its type + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Nullable Struct property example"), U("structPropertyNullable"), U("simpleStructDatatype"), true, false, value::null())); // no datatype constraints for struct datatype + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Nullable Sequence enum property example"), U("sequenceEnumPropertyNullable"), U("enumDatatype"), true, true, value::null())); // no field constraints for enum field, as it is already described by its type + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Nullable Sequence string property example"), U("sequenceStringPropertyNullable"), U("NcString"), true, true, datatype_string_constraints)); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Nullable Sequence number property example"), U("sequenceNumberPropertyNullable"), U("NcInt32"), true, true, datatype_int32_constraints)); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Nullable Sequence boolean property example"), U("sequenceBooleanPropertyNullable"), U("NcBoolean"), true, true, value::null())); // no field constraints for boolean field, as it is already described by its type + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Nullable Sequence struct property example"), U("sequenceStructPropertyNullable"), U("simpleStructDatatype"), true, true, value::null())); // no field constraints for struct field + const auto struct_datatype = nmos::details::make_nc_datatype_descriptor_struct(U("struct datatype"), U("structDatatype"), fields, value::null()); // no datatype constraints for struct datatype + + // setup datatypes in control_protocol_state + nmos::experimental::control_protocol_state control_protocol_state(nullptr); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ no_constraints_int16_datatype }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ no_constraints_int32_datatype }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ no_constraints_int64_datatype }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ no_constraints_uint16_datatype }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ no_constraints_uint32_datatype }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ no_constraints_uint64_datatype }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ no_constraints_string_datatype }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ with_constraints_int32_datatype }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ with_constraints_string_datatype }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ enum_datatype }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ simple_struct_datatype }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ no_constraints_int32_seq_datatype }); + control_protocol_state.insert(nmos::experimental::datatype_descriptor{ no_constraints_string_seq_datatype }); + + // test get_runtime_property_constraints + BST_REQUIRE_EQUAL(nmos::details::get_runtime_property_constraints(property_string_id, runtime_property_constraints), runtime_property_string_constraints); + BST_REQUIRE_EQUAL(nmos::details::get_runtime_property_constraints(property_int32_id, runtime_property_constraints), runtime_property_int32_constraints); + BST_REQUIRE_EQUAL(nmos::details::get_runtime_property_constraints(unknown_property_id, runtime_property_constraints), value::null()); + + // string property constraints validation + + // runtime property constraints validation + const nmos::details::datatype_constraints_validation_parameters with_constraints_string_constraints_validation_params{ with_constraints_string_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(value::string(U("1234567890")), runtime_property_string_constraints, property_string_constraints, with_constraints_string_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value::string(U("12345678901")), runtime_property_string_constraints, property_string_constraints, with_constraints_string_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value::string(U("123456789A")), runtime_property_string_constraints, property_string_constraints, with_constraints_string_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(value_of({ value::string(U("1234567890")), value::string(U("1234567890")) }), runtime_property_string_constraints, property_string_constraints, with_constraints_string_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ value::string(U("1234567890")), value::string(U("12345678901")) }), runtime_property_string_constraints, property_string_constraints, with_constraints_string_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ value::string(U("1234567890")), 1 }), runtime_property_string_constraints, property_string_constraints, with_constraints_string_constraints_validation_params), nmos::control_protocol_exception); + // property constraints validation + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(value::string(U("abcde")), value::null(), property_string_constraints, with_constraints_string_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value::string(U("abcdef")), value::null(), property_string_constraints, with_constraints_string_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value::string(U("abcd1")), value::null(), property_string_constraints, with_constraints_string_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(value_of({ value::string(U("abcde")), value::string(U("abcde")) }), value::null(), property_string_constraints, with_constraints_string_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ value::string(U("abcde")), value::string(U("abcdef")) }), value::null(), property_string_constraints, with_constraints_string_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ value::string(U("abcde")), 1 }), value::null(), property_string_constraints, with_constraints_string_constraints_validation_params), nmos::control_protocol_exception); + // datatype constraints validation + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(value::string(U("1a")), value::null(), value::null(), with_constraints_string_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value::string(U("1a2")), value::null(), value::null(), with_constraints_string_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value::string(U("1*")), value::null(), value::null(), with_constraints_string_constraints_validation_params), nmos::control_protocol_exception); + const nmos::details::datatype_constraints_validation_parameters no_constraints_string_constraints_validation_params{ no_constraints_string_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(value::string(U("1234567890-abcde-!\"$%^&*()_+=")), value::null(), value::null(), no_constraints_string_constraints_validation_params)); + + // number property constraints validation + + // runtime property constraints validation + const nmos::details::datatype_constraints_validation_parameters with_constraints_int32_constraints_validation_params{ with_constraints_int32_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(10, runtime_property_int32_constraints, property_int32_constraints, with_constraints_int32_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(1000, runtime_property_int32_constraints, property_int32_constraints, with_constraints_int32_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(9, runtime_property_int32_constraints, property_int32_constraints, with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(1001, runtime_property_int32_constraints, property_int32_constraints, with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(value_of({ 10, 1000 }), runtime_property_int32_constraints, property_int32_constraints, with_constraints_int32_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ 10, 1001 }), runtime_property_int32_constraints, property_int32_constraints, with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ 10, value::string(U("a")) }), runtime_property_int32_constraints, property_int32_constraints, with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + // property constraints validation + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(50, value::null(), property_int32_constraints, with_constraints_int32_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(500, value::null(), property_int32_constraints, with_constraints_int32_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(45, value::null(), property_int32_constraints, with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(505, value::null(), property_int32_constraints, with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(499, value::null(), property_int32_constraints, with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(value_of({ 50, 500 }), value::null(), property_int32_constraints, with_constraints_int32_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ 49, 500 }), value::null(), property_int32_constraints, with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ 50, 501 }), value::null(), property_int32_constraints, with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ 45, 500 }), value::null(), property_int32_constraints, with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ 50, value::string(U("a")) }), value::null(), property_int32_constraints, with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + // datatype constraints validation + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(100, value::null(), value::null(), with_constraints_int32_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(250, value::null(), value::null(), with_constraints_int32_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(90, value::null(), value::null(), with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(260, value::null(), value::null(), with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(99, value::null(), value::null(), with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + // int16 datatype constraints validation + const nmos::details::datatype_constraints_validation_parameters no_constraints_int16_constraints_validation_params{ no_constraints_int16_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_THROW(nmos::details::constraints_validation(int64_t(std::numeric_limits::min()) - 1, value::null(), value::null(), no_constraints_int16_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(int64_t(std::numeric_limits::max()) + 1, value::null(), value::null(), no_constraints_int16_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::min(), value::null(), value::null(), no_constraints_int16_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::max(), value::null(), value::null(), no_constraints_int16_constraints_validation_params)); + // int32 datatype constraints validation + const nmos::details::datatype_constraints_validation_parameters no_constraints_int32_constraints_validation_params{ no_constraints_int32_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_THROW(nmos::details::constraints_validation(int64_t(std::numeric_limits::min()) - 1, value::null(), value::null(), no_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(int64_t(std::numeric_limits::max()) + 1, value::null(), value::null(), no_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::min(), value::null(), value::null(), no_constraints_int32_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::max(), value::null(), value::null(), no_constraints_int32_constraints_validation_params)); + // int64 datatype constraints validation + const nmos::details::datatype_constraints_validation_parameters no_constraints_int64_constraints_validation_params{ no_constraints_int64_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_THROW(nmos::details::constraints_validation(std::numeric_limits::min(), value::null(), value::null(), no_constraints_int64_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(std::numeric_limits::max(), value::null(), value::null(), no_constraints_int64_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::min(), value::null(), value::null(), no_constraints_int64_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::max(), value::null(), value::null(), no_constraints_int64_constraints_validation_params)); + // uint16 datatype constraints validation + const nmos::details::datatype_constraints_validation_parameters no_constraints_uint16_constraints_validation_params{ no_constraints_uint16_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_THROW(nmos::details::constraints_validation(-1, value::null(), value::null(), no_constraints_uint16_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(uint64_t(std::numeric_limits::max()) + 1, value::null(), value::null(), no_constraints_uint16_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::min(), value::null(), value::null(), no_constraints_uint16_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::max(), value::null(), value::null(), no_constraints_uint16_constraints_validation_params)); + // uint32 datatype constraints validation + const nmos::details::datatype_constraints_validation_parameters no_constraints_uint32_constraints_validation_params{ no_constraints_uint32_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_THROW(nmos::details::constraints_validation(-1, value::null(), value::null(), no_constraints_uint32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(uint64_t(std::numeric_limits::max()) + 1, value::null(), value::null(), no_constraints_uint32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::min(), value::null(), value::null(), no_constraints_uint32_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::max(), value::null(), value::null(), no_constraints_uint32_constraints_validation_params)); + // uint64 datatype constraints validation + const nmos::details::datatype_constraints_validation_parameters no_constraints_uint64_constraints_validation_params{ no_constraints_uint64_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_THROW(nmos::details::constraints_validation(-1, value::null(), value::null(), no_constraints_uint64_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(std::numeric_limits::max(), value::null(), value::null(), no_constraints_int64_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::min(), value::null(), value::null(), no_constraints_uint64_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::max(), value::null(), value::null(), no_constraints_uint64_constraints_validation_params)); + // float32 datatype constraints validation + const nmos::details::datatype_constraints_validation_parameters no_constraints_float32_constraints_validation_params{ no_constraints_float32_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_THROW(nmos::details::constraints_validation(std::numeric_limits::lowest(), value::null(), value::null(), no_constraints_float32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(std::numeric_limits::max(), value::null(), value::null(), no_constraints_float32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::lowest(), value::null(), value::null(), no_constraints_float32_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::max(), value::null(), value::null(), no_constraints_float32_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(0.0, value::null(), value::null(), no_constraints_float32_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(-1000.0, value::null(), value::null(), no_constraints_float32_constraints_validation_params)); + // float64 datatype constraints validation + const nmos::details::datatype_constraints_validation_parameters no_constraints_float64_constraints_validation_params{ no_constraints_float64_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_THROW(nmos::details::constraints_validation(1000, value::null(), value::null(), no_constraints_float64_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(1000.0, value::null(), value::null(), no_constraints_float64_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::min(), value::null(), value::null(), no_constraints_float64_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(std::numeric_limits::max(), value::null(), value::null(), no_constraints_float64_constraints_validation_params)); + // enum property datatype constraints validation + const nmos::details::datatype_constraints_validation_parameters enum_constraints_validation_params{ enum_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(enum_value::foo, value::null(), value::null(), enum_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(4, value::null(), value::null(), enum_constraints_validation_params), nmos::control_protocol_exception); + // invalid data vs primitive datatype constraints + const nmos::details::datatype_constraints_validation_parameters no_constraints_string_seq_constraints_validation_params{ no_constraints_string_seq_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(value_of({ value::string(U("1234567890-abcde-!\"$%^&*()_+=")) }), value::null(), value::null(), no_constraints_string_seq_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(value_of({ value::string(U("1234567890-abcde-!\"$%^&*()_+=")), value::string(U("1234567890-abcde-!\"$%^&*()_+=")) }), value::null(), value::null(), no_constraints_string_seq_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ 1 }), value::null(), value::null(), no_constraints_string_seq_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(1, value::null(), value::null(), no_constraints_string_seq_constraints_validation_params), nmos::control_protocol_exception); + const nmos::details::datatype_constraints_validation_parameters no_constraints_int32_seq_constraints_validation_params{ no_constraints_int32_seq_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ value::string(U("1234567890-abcde-!\"$%^&*()_+=")) }), value::null(), value::null(), no_constraints_int32_seq_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(value_of({ 1 }), value::null(), value::null(), no_constraints_int32_seq_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(value_of({ 1, 2 }), value::null(), value::null(), no_constraints_int32_seq_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ value::string(U("1234567890-abcde-!\"$%^&*()_+=")) }), value::null(), value::null(), no_constraints_int32_seq_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value::string(U("1234567890-abcde-!\"$%^&*()_+=")), value::null(), value::null(), no_constraints_int32_seq_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ 1, 2 }), value::null(), value::null(), with_constraints_int32_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(value_of({ 1, 2 }), value::null(), value::null(), with_constraints_string_constraints_validation_params), nmos::control_protocol_exception); + + // struct property datatype constraints validation + const auto good_struct1 = value_of({ + { U("enumProperty"), enum_value::baz }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), enum_value::baz }, + { U("stringPropertyNullable"), U("xy") }, + { U("numberPropertyNullable"), 100 }, + { U("booleanPropertyNullable"), true }, + { U("structPropertyNullable"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumPropertyNullable"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringPropertyNullable"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberPropertyNullable"), value_of({ 100, 110 }) }, + { U("sequenceBooleanPropertyNullable"), value_of({ true, false }) }, + { U("sequenceStructPropertyNullable"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) } + }); + const auto good_struct2 = value_of({ + { U("enumProperty"), enum_value::baz }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + // missing field + const auto bad_struct1 = value_of({ + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + // invalid fields + const auto bad_struct2 = value_of({ + { U("enumProperty"), 3 }, // bad value + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_1 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xyz") }, // bad value + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_2 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("x$") }, // bad value + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_3 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 99 }, // bad value + { U("booleanProperty"), true }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_4 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), 0 }, // bad value + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_5 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), 3 }, // bad value + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) } + }); + const auto bad_struct2_5_1 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xyz") }, // bad value + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_5_2 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 99 }, // bad value + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_5_3 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), 3 } // bad value + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_6 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar, 4 }) }, // bad value + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_6_1 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bbb") }) }, // bad value + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_6_2 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 99, 110 }) }, // bad value + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_6_3 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, 0 }) }, // bad value + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_7 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), 3 }, // bad value + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_7_1 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("abc") }, // bad value + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_7_2 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 251 }, // bad value + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct2_7_3 = value_of({ + { U("enumProperty"), enum_value::foo }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), 0 } // bad value + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + const auto bad_struct3 = value_of({ + { U("enumProperty"), value::null() }, //bad value + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + + const auto bad_struct3_1 = value_of({ + { U("enumProperty"), enum_value::baz }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), value::null() }, // bad value + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + + const auto bad_struct3_2 = value_of({ + { U("enumProperty"), enum_value::baz }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, + { U("booleanProperty"), true }, + { U("structProperty"), value::null() }, // bad value + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value_of({ + { U("simpleEnumProperty"), enum_value::foo }, + { U("simpleStringProperty"), U("ab") }, + { U("simpleNumberProperty"), 200 }, + { U("simpleBooleanProperty"), false } + }) }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + + const auto bad_struct3_3 = value_of({ + { U("enumProperty"), enum_value::baz }, + { U("stringProperty"), U("xy") }, + { U("numberProperty"), 100 }, // bad value + { U("booleanProperty"), true }, + { U("structProperty"), value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }) }, + { U("sequenceEnumProperty"), value_of({ enum_value::foo, enum_value::bar }) }, + { U("sequenceStringProperty"), value_of({ U("aa"), U("bb") }) }, + { U("sequenceNumberProperty"), value_of({ 100, 110 }) }, + { U("sequenceBooleanProperty"), value_of({ true, false }) }, + { U("sequenceStructProperty"), value_of({ + value_of({ + { U("simpleEnumProperty"), enum_value::bar }, + { U("simpleStringProperty"), U("xy") }, + { U("simpleNumberProperty"), 100 }, + { U("simpleBooleanProperty"), true } + }), value::null() // bad value + }) }, + { U("enumPropertyNullable"), value::null() }, + { U("stringPropertyNullable"), value::null() }, + { U("numberPropertyNullable"), value::null() }, + { U("booleanPropertyNullable"), value::null() }, + { U("structPropertyNullable"), value::null() }, + { U("sequenceEnumPropertyNullable"), value::null() }, + { U("sequenceStringPropertyNullable"), value::null() }, + { U("sequenceNumberPropertyNullable"), value::null() }, + { U("sequenceBooleanPropertyNullable"), value::null() }, + { U("sequenceStructPropertyNullable"), value::null() } + }); + + const nmos::details::datatype_constraints_validation_parameters struct_constraints_validation_params{ struct_datatype, nmos::make_get_control_protocol_datatype_descriptor_handler(control_protocol_state) }; + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(good_struct1, value::null(), value::null(), struct_constraints_validation_params)); + BST_REQUIRE_NO_THROW(nmos::details::constraints_validation(good_struct2, value::null(), value::null(), struct_constraints_validation_params)); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct1, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_1, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_2, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_3, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_4, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_5, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_5_1, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_5_2, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_5_3, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_6, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_6_1, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_6_2, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_6_3, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_7, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_7_1, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_7_2, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct2_7_3, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct3, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct3_1, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct3_2, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); + BST_REQUIRE_THROW(nmos::details::constraints_validation(bad_struct3_3, value::null(), value::null(), struct_constraints_validation_params), nmos::control_protocol_exception); +} diff --git a/Development/nmos/test/json_validator_test.cpp b/Development/nmos/test/json_validator_test.cpp new file mode 100644 index 000000000..ff7c48995 --- /dev/null +++ b/Development/nmos/test/json_validator_test.cpp @@ -0,0 +1,88 @@ +// The first "test" is of course whether the header compiles standalone +#include "cpprest/json_validator.h" + +#include "bst/test/test.h" +#include "cpprest/basic_utils.h" // for utility::us2s, utility::s2us +#include "cpprest/json_utils.h" + +namespace +{ + const auto id = web::uri{ U("/test") }; + + web::json::experimental::json_validator make_validator(const web::json::value& schema, const web::uri& id) + { + return web::json::experimental::json_validator + { + [&](const web::uri&) { return schema; }, + { id } + }; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testInvalidTypeSchema) +{ + using web::json::value_of; + + const bool keep_order = true; + + const auto schema = value_of({ + { U("$schema"), U("http://json-schema.org/draft-04/schema#")}, + { U("type"), U("object")}, + { U("properties"), value_of({ + { U("foo"), value_of({ + { U("anyOf"), value_of({ + value_of({ + { U("type"), U("string") }, + { U("pattern"), U("^auto$") } + }, keep_order), + U("bad") + }) + }}) + }}) + }}); + + // invalid JSON-type for schema + BST_REQUIRE_THROW(make_validator(schema, id), std::invalid_argument); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testEnumSchema) +{ + using web::json::value_of; + + const bool keep_order = true; + + const auto schema = value_of({ + { U("$schema"), U("http://json-schema.org/draft-04/schema#")}, + { U("type"), U("object")}, + { U("properties"), value_of({ + { U("foo"), value_of({ + { U("anyOf"), value_of({ + value_of({ + { U("type"), U("string") }, + { U("pattern"), U("^auto$") } + }, keep_order), + value_of({ + { U("enum"), value_of({ + { U("good") } + }) + }}) + }) + }}) + }}) + }}); + + auto validator = make_validator(schema, id); + + // not in anyOf + BST_REQUIRE_THROW(validator.validate(value_of({ { U("foo"), U("bad") } }), id), web::json::json_exception); + + // in anyOf, pattern + validator.validate(value_of({ { U("foo"), U("auto") } }), id); + BST_REQUIRE(true); + + // in anyOf, enum + validator.validate(value_of({ { U("foo"), U("good") } }), id); + BST_REQUIRE(true); +} diff --git a/Development/nmos/test/jwt_generator_test.cpp b/Development/nmos/test/jwt_generator_test.cpp new file mode 100644 index 000000000..e2b1050d8 --- /dev/null +++ b/Development/nmos/test/jwt_generator_test.cpp @@ -0,0 +1,69 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/jwt_generator.h" + +#include "bst/test/test.h" +#include "cpprest/basic_utils.h" +#include "nmos/jwk_utils.h" // for nmos::experimental::jwk_exception + +namespace +{ + // this is the private key (rsa.api.testsuite.nmos.tv.key.pem) from the nmos-testing + // https://https://github.com/AMWA-TV/nmos-testing/blob/master/test_data/BCP00301/ca/intermediate/private/rsa.api.testsuite.nmos.tv.key.pem + const auto test_private_key = utility::s2us(R"(-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAyXHgphlqcINx+ZKkBefDo5X5rHUuTpom9OcRKpWQHt7oYUr1 +UhoKJ+8SxbsSvtlrvvGa6kiSk/m6i7haU9dGKSDJzndYJSi+Qbc2jfSPfoHtHvsy +PIworhKniDA7YNE+olr23KGYSqdWidp3nzQLdaHuvOqjjjb3Jm2hvdt4Rfyk8r90 +5FY1kdZ/rINtvUDNHZnno9xPw9Hk+xc/cfOJyLUxBndy5wSp7Dhl8Wg1tLuK0rIG +JuFBFrZWykGySGP8s3KzeSeugojYa4JWoXFix6+hlTOfyyu5VXtDkTIZotXcAOBl +EEFLNtSko0yzWuSDo1HF0IwwCvgmwFnewdgFGQIDAQABAoIBAGnZ2ebNsh1/JHO0 +91VXDHk4BGL3jCanX9MOW/nZb0qZbNg68B99KVsEiAO4okgArVo/UFzNV6BD+B8U +9vnZQ7e2z/QayAl2mEqlwBflq0UZdoTyD9q692FI0hmA5qKgMN5VGCSlEQYhWhrD +3lmcmmzscyt3zAudnE7oCrZdzZxQD1r2dgjuLFiSaueUHPS/7FQavMx5sGGowcmN +ex+nEHEBeRn5Ws7NWKtX6UoFa8btJIapsqkLHgtXgEsrbFDfLXbEgdFgXg36e8Ak +lCuzUsehaM56eesVNyT/4FVN9ilJu0I1OlJs7sNXOIR4v//PqWoEnEGV/Fwb0YPT +YWPr81ECgYEA7jIIQD9TKYdBdR2A5b8AmRAuVpYsC36Cw6n/3Mq/Le1Bf0nj1cVf +JBkmLOYO/41h6Z+kITJfUZMniy6a/D7LXMSd/jwJM7WKLBfM/AIQHsSpIo/bkW/Q +zVa2inDLYnICWAuR2KNWR46CWy6wnjlWdag76YYJxlk91vpy6RqUCtUCgYEA2ICb +fBp5DNayESM7zDjh8PvgMn49tj6BjuO+oJ7vycQzDlgp/NV2kwtRpNQUcT4wAvTJ +W/GHOlUTxIDxhZqJu3Ix5ue/R0YprhlZofSzwXVoqh+NvZGi4UYiJkTr7zXhzoU1 +PV5vWb1YMqpiYJxA3BRj3K0YdVmPkdcoLsXgKzUCgYAuBh7QAyxXbtn3/h5kxfYg +nR7G/jc+dVBg7B0TFV3BSwGHzcgnCv7qI63bqQwm1rOfh4gYHfqK8YsHepbZvGxg +3WDFueXxRteO0355BxEEUO15TyCWxmsq8eFNeKPjvrGzP3EL0eue4etQIQJhYCTT +kREaexqyZ5XqTvQbFFacjQKBgEhC1KKda12/ovtZWTIWokL+rpvryskzH6cDmLKf +mcUsOSZGgu0iiksV8hAjwRby/K9f6H1JpirwDoL9zp8bL3Fi8gjxvMQbRPoY9/O4 +au7dMyvlEDf/je/Gqss/IchboZx+lYCALoYzTmbKu78nJ/bMz2/uTkWMuQCiYYUL +AoEpAoGAYIG2aCsuLV+1bPHC0vYvDC0V+NMA8e9HrplHdrQ4IBxyZnmHqvrGZ+04 +huUpDxAX+hxYap0k0RPJ3HaFmIHz7DpStX/aIjcfucEnoev7OLj4/3j6s2tHfeWL +JP+1+v/YSIKc7WXvz95YsmoJZ02Ikv8zBan9HIzczmkqDe0C1RQ= +-----END RSA PRIVATE KEY----- +)"); + +} + +BST_TEST_CASE(testClientAssertion) +{ + using namespace nmos::experimental; + + const utility::string_t issuer{ U("api.testsuite.nmos.tv") }; + const utility::string_t subject{ U("api.testsuite.nmos.tv") }; + const web::uri audience{ U("https://mocks.testsuite.nmos.tv:5010/testtoken") }; + const std::chrono::seconds token_lifetime{ 100 }; + const utility::string_t private_key{ test_private_key }; + const utility::string_t keyid{ U("key_1") }; + + // bad cases + const utility::string_t bad_issuer{}; + BST_REQUIRE_THROW(jwt_generator::create_client_assertion(bad_issuer, subject, audience, token_lifetime, private_key, keyid), nmos::experimental::jwk_exception); + + const utility::string_t bad_subject{}; + BST_REQUIRE_THROW(jwt_generator::create_client_assertion(issuer, bad_subject, audience, token_lifetime, private_key, keyid), nmos::experimental::jwk_exception); + + const utility::string_t bad_audience{}; + BST_REQUIRE_THROW(jwt_generator::create_client_assertion(issuer, subject, bad_audience, token_lifetime, private_key, keyid), nmos::experimental::jwk_exception); + + const utility::string_t bad_private_key{}; + BST_REQUIRE_THROW(jwt_generator::create_client_assertion(issuer, subject, audience, token_lifetime, bad_private_key, keyid), nmos::experimental::jwk_exception); + + // good case + BST_REQUIRE_NO_THROW(jwt_generator::create_client_assertion(issuer, subject, audience, token_lifetime, private_key, keyid)); +} diff --git a/Development/nmos/test/jwt_validation_test.cpp b/Development/nmos/test/jwt_validation_test.cpp new file mode 100644 index 000000000..2ce7ca71f --- /dev/null +++ b/Development/nmos/test/jwt_validation_test.cpp @@ -0,0 +1,343 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/jwt_validator.h" + +#include +#include +#include +#include "bst/test/test.h" +#include "cpprest/basic_utils.h" // for utility::us2s, utility::s2us +#include "cpprest/json_utils.h" +#include "cpprest/json_validator.h" +#include "nmos/is10_schemas/is10_schemas.h" +#include "nmos/jwk_utils.h" +#include "nmos/scope.h" + +namespace +{ + using web::json::value_of; + + // this is the private key (rsa.mocks.testsuite.nmos.tv.key.pem) from the nmos-testing + // https://github.com/AMWA-TV/nmos-testing/blob/master/test_data/BCP00301/ca/intermediate/private/rsa.mocks.testsuite.nmos.tv.key.pem + const auto test_private_key = utility::s2us(R"(-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA6F36+0gzW9XURoRGzRfFmIZQnCQJS7+sQrUmhPjm+X/gsNAm +zcGlgpG37jr4YbabBotlpOyRmpYfsts/9Ts1UFdqGx/aaONAmldc16arEYwSLNFW +KQ18rac0qGKAiUn2swpDIje8tTKHBE7XVLd5XVtcWrhGqX9liUFSKCb+TBuraMne +na5gA3ruXZZcn+wdHM777QQq6D1Hf9g62stePPzUeNPX+Ttulr+ju8OvQy6MK2Ij +6IHNzPT7McHUE0Z7sk7czWG60kCthRqv16OwuzR3Bhn5blPwRGXjbJJfkN6a7ck+ +4bt3LetDw/1idPArF2ONFgMO21ZJp1qm7ZnbcQIDAQABAoIBAQCKdW2XC7emsixx +9GHn1ZFlSCuCTqrHWyf++8g/Fb0z0DIHyZBFrGy996xcpQDZ4KBRbwCbHGfKcEfl +IGXk72nePKg6D2nqc/dLwGDPEz3+D7PIxtgLUEEJjIeBCmjC5bK9jpDgM8wbQEdZ +ls0Sat1Ddqv6VrGsUAAloCmfSVTf7b402o2JpJMlcqjGpZKMVZbSKlTavi6wDm9B +9BMTEER6EypK+B+jtp2CK4Jw8Tnwx7RkVVS731N0vmMqInQCzMhvO1wyeDWZkN3u +7Pzan7xiL9cL0K6icnsgHWsyC9Vq8vxMkQWuqnJ2t8u8FZik4OjD0K4uvtRuSQxU +/Exf0qPRAoGBAPPrKCtb/EMNvUdV2e3d82NsZmaQnOgoqkrpjPol8I/2YJeb7Imi +bx1Ky2qA9opydnm8br3WKGNBJGlPLhenlekaVcIOC+897ijqvy0x7ZhvUVam7N97 +Q9iYbI5iArPlDixBUnNDfsQh8ZRJy5NQz2Z6pNILToQMx83A0tKO4WbNAoGBAPPg +WlqUJNfEIuu67DHo7iFulV04CiL43LVGA0QsZiw75qJQvlsusj6pJbUN29rtihp/ +F6F09rbHqvEUv8MSp2SywqNpYgcDZxlcxnwj5RQONhkoChXYSGM6FnTVIHxa0Wai +C5JOHwIwy8mHn+roQLIe9g8vkSDKQnLrUYwj7981AoGBAOyKuOrLiqhwM4VxUSUn +H7fkUK3YQgG2Jeb99LRFhLPnpyZ/lHSo7H6IoRnItM3wUMqfnPlGLOaMLsZdfgJ8 +h5mF63KD8rjw4vwVIo6uo443LbcNrBrRzCrJLkUp8RsJ36O1OUMESnPjwwYeRmi3 +blogR6RWSK8wQbdb7lc5LodlAoGAMuQidrxrY8s+Lkr3dwLQjpFxAd7r3phoFjvh ++pv5RknJux12W7jG4WSSxdF6i5j+NMFIwRyTT1kjRuO5kI+X9t+G1mrrVeNT5GsD +0Gv9Jc5BY8aDNEPJ90rr3L2M5eZdxDkUiRdcSSy9mfR/XpnQxlrHpiua8WjDrQ+G +GOR27fECgYAQkxp8abfj4q57nWHt4Nmr5WDXrCIPNNBQvd596DGOFiSp7IsyPuzt +rKZp5TDgbxdcDIN0Jag78tzY5Ms6SHXpNe648tJBmnSHCFrx8dL95sdGKf4/DtOv +LWWWvv8Ld9XO7GPVLVFgg9wCgkIF9lUgjfhzoalCA1i1L90jcy8WDQ== +-----END RSA PRIVATE KEY----- +)"); + + // using openssl to extract the public key of the rsa.mocks.testsuite.nmos.tv.key.pem via private key + // $ openssl pkey -in rsa.mocks.testsuite.nmos.tv.key.pem -pubout + const auto test_public_key = utility::s2us(R"(-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6F36+0gzW9XURoRGzRfF +mIZQnCQJS7+sQrUmhPjm+X/gsNAmzcGlgpG37jr4YbabBotlpOyRmpYfsts/9Ts1 +UFdqGx/aaONAmldc16arEYwSLNFWKQ18rac0qGKAiUn2swpDIje8tTKHBE7XVLd5 +XVtcWrhGqX9liUFSKCb+TBuraMnena5gA3ruXZZcn+wdHM777QQq6D1Hf9g62ste +PPzUeNPX+Ttulr+ju8OvQy6MK2Ij6IHNzPT7McHUE0Z7sk7czWG60kCthRqv16Ow +uzR3Bhn5blPwRGXjbJJfkN6a7ck+4bt3LetDw/1idPArF2ONFgMO21ZJp1qm7Znb +cQIDAQAB +-----END PUBLIC KEY----- +)"); + + const auto id = web::uri{ U("/test") }; + const auto audience = U("https://api-nmos.testsuite.nmos.tv"); + const utility::string_t key_id{ U("test_key") }; + + const auto jwk1 = nmos::experimental::rsa_private_key_to_jwk(test_private_key, key_id, jwk::public_key_uses::signing, jwk::algorithms::RS512); + const auto pems = value_of({ + value_of({ + { U("jwk"), jwk1 }, + { U("pem"), test_public_key } + }) + }); + + web::json::value make_schema(const char* schema) + { + return web::json::value::parse(utility::s2us(schema)); + } + + web::json::experimental::json_validator make_json_validator(const web::json::value& schema, const web::uri& id) + { + return web::json::experimental::json_validator + { + [&](const web::uri&) { return schema; }, + { id } + }; + } + + const nmos::experimental::jwt_validator jwt_validator(pems, [](const web::json::value& payload) + { + auto token_json_validator = make_json_validator(make_schema(nmos::is10_schemas::v1_0_x::token_schema), id); + token_json_validator.validate(payload, id); + }); + +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testJWK) +{ + const auto public_key = nmos::experimental::rsa_public_key(test_private_key); + BST_REQUIRE_EQUAL(test_public_key, public_key); + + const auto jwk2 = nmos::experimental::rsa_public_key_to_jwk(public_key, key_id, jwk::public_key_uses::signing, jwk::algorithms::RS512); + BST_REQUIRE_EQUAL(jwk1, jwk2); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testAccessTokenJSON) +{ + // using rsa.mocks.testsuite.nmos.tv.key.pem private key to create an access token via the https://jwt.io/ + // HEADER: + //{ + // "typ": "JWT", + // "alg" : "RS512" + //} + // example PAYLOAD: + //{ + // "iss": "https://nmos-mocks.local:5011", + // "sub" : "test@testsuite.nmos.tv", + // "aud" : [ + // "https://*.testsuite.nmos.tv", + // "https://*.local" + // ], + // "exp" : 4828204800, + // "iat" : 1696868272, + // "scope" : "registration", + // "client_id" : "458f6d06-46b1-49fd-b778-7c30428889c6", + // "x-nmos-registration" : { + // "read": [ + // "*" + // ], + // "write": [ + // "*" + // ] + // } + //} + // missing iss(issuer) + const utility::string_t missing_iss_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJ0ZXN0QHRlc3RzdWl0ZS5ubW9zLnR2IiwiYXVkIjpbImh0dHBzOi8vKi50ZXN0c3VpdGUubm1vcy50diIsImh0dHBzOi8vKi5sb2NhbCJdLCJleHAiOjQ4MjgyMDQ4MDAsImlhdCI6MTY5Njg2ODI3Miwic2NvcGUiOiJyZWdpc3RyYXRpb24iLCJjbGllbnRfaWQiOiI0NThmNmQwNi00NmIxLTQ5ZmQtYjc3OC03YzMwNDI4ODg5YzYiLCJ4LW5tb3MtcmVnaXN0cmF0aW9uIjp7InJlYWQiOlsiKiJdLCJ3cml0ZSI6WyIqIl19fQ.XS9JkK4mmDtjcyrTT0jEWpcssqeU-aZ7xQImL3f-V7KMlOFOQJ5YiT5kXV9Gxb7xYNSJxqn7ym1oL5kNxID_15VZmzsT2h2oVY5x3yOtRcuhLIpD1d4GzXFak5nvR9D6i_fCm5Ov19oF92l7dhMY_DT6HDm89maGJ9DKVxuP1jqVcwFDcXZnGak0MJYETN8nM4xIuRTmmS7W2NpzVKyfw1sCjie2QyptlPoX_KyLaiv2VMkZh-d4Pi9nA9XLjOz-Gyj0-s-NiPx9Qbocpa-eJSqzxz6gtfx8rbSNaqeGV3ehVkGC-0DJq0iIhwxpxp98qYlodz1df8gSDo106OGI_w"); + // missing sub(subject) + const utility::string_t missing_sub_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsImF1ZCI6WyJodHRwczovLyoudGVzdHN1aXRlLm5tb3MudHYiLCJodHRwczovLyoubG9jYWwiXSwiZXhwIjo0ODI4MjA0ODAwLCJpYXQiOjE2OTY4NjgyNzIsInNjb3BlIjoicmVnaXN0cmF0aW9uIiwiY2xpZW50X2lkIjoiNDU4ZjZkMDYtNDZiMS00OWZkLWI3NzgtN2MzMDQyODg4OWM2IiwieC1ubW9zLXJlZ2lzdHJhdGlvbiI6eyJyZWFkIjpbIioiXSwid3JpdGUiOlsiKiJdfX0.opBKBVFuHXbc6VepFhELRJ7INYWd_W9SBaqfXoe2dMFvCIf0HJNiDnbBrZ9qC3xyyPGR_-Bv7taNTAk67Eirh_P6dv6kPGH-cyTn4G1xCowEiGxFT-nFHyDdV4Ym50avrU6hLRHKGRy5ke0fXXHmcmQETDZpMrQq6wyg0h-kj6KneQAfNCJyqd6-jQu5VuNPsuH54iHiKOLQITOp_WDQ_3-XDQycSdbJJMhdBBnFv-l0qWqDUZAZkkNdJvKxdyhRMB_P7PhhIZck20ylJFbrcjKyMAnUj1O82L9Mriuj23p4jWd0oUiZ9VQBiTtudrrNAON6ZlIjOrBuPWIH7FXQ8w"); + // missing aud(audience) + const utility::string_t missing_aud_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJleHAiOjQ4MjgyMDQ4MDAsImlhdCI6MTY5Njg2ODI3Miwic2NvcGUiOiJyZWdpc3RyYXRpb24iLCJjbGllbnRfaWQiOiI0NThmNmQwNi00NmIxLTQ5ZmQtYjc3OC03YzMwNDI4ODg5YzYiLCJ4LW5tb3MtcmVnaXN0cmF0aW9uIjp7InJlYWQiOlsiKiJdLCJ3cml0ZSI6WyIqIl19fQ.Gm4dmsDQOw-e6B5jtBLCKl6LJex41xlMPXaeeKUoZAFj9JUMsv-CpiMIGs9RYvfpPTMJcvrGSJfAHeIkPuUmuYzBkOsD0NFrXqnWg_TmokNZo-FvJ_W3gg2pVVWG4MMTrjs_npdSU6gWBu2GslZraDTphfCo-ooiFJZgR4xPQ5EJiJYHP9m3ZQPLfgIsxX2mvIycFTjuoNuGR-T9lR70vgmfuLacDoZWreKnzSY87Ug_OWanp33kHfuCqhu6X7gTb8DwJDrpEo3Y0b8pNDms9AEDsCyxOnQGdcb4QBvcLciausFov-GLnCS_hJ1F4hpkOIj88RXQCciWpjIyaVwFMQ"); + // missing exp(expiration) + const utility::string_t missing_exp_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImlhdCI6MTY5Njg2ODI3Miwic2NvcGUiOiJyZWdpc3RyYXRpb24iLCJjbGllbnRfaWQiOiI0NThmNmQwNi00NmIxLTQ5ZmQtYjc3OC03YzMwNDI4ODg5YzYiLCJ4LW5tb3MtcmVnaXN0cmF0aW9uIjp7InJlYWQiOlsiKiJdLCJ3cml0ZSI6WyIqIl19fQ.RQqfKCwOaBXOVH6CvPo13gT5SP8aQAUVorUoe860sSdETor6aXPZyE733OsRjMrspvgV6r6-abW4s1pUDLPcFQBPEU9QhCqGnTmACWkyBDDI2ZFfnC1tqySW7Qd1ZM8oNHNlIJUO7yXtg7YgJyWbr_Nwj-4W_cbhukIeSGBDTjG_Vhcg7O6sRZBVGFni8aqfegHMxnBFGPxfKb70C6sJbXmyb3-ufQYVs-uWbsRJmZyucjdd317lW7OTgi0nn2ZCUzI07EIArfhlJGeK4E0zzROCJbpFJs751IOpte-4lCUeHCJXg9yhS0N_jjIsdKC1G0SEMqAZ-Uo0RJ1FDU5TNg"); + + // missing iss(issuer), on GET request + BST_REQUIRE_THROW(jwt_validator.basic_validation(missing_iss_token), web::json::json_exception); + // missing sub(subject), on GET request + BST_REQUIRE_THROW(jwt_validator.basic_validation(missing_sub_token), web::json::json_exception); + // missing aud(audience), on GET request + BST_REQUIRE_THROW(jwt_validator.basic_validation(missing_aud_token), web::json::json_exception); + // missing exp(expiration), on GET request + BST_REQUIRE_THROW(jwt_validator.basic_validation(missing_exp_token), web::json::json_exception); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testAccessTokenStandardClaim) +{ + // using rsa.mocks.testsuite.nmos.tv.key.pem private key to create an access token via the https://jwt.io/ + // HEADER: + //{ + // "typ": "JWT", + // "alg" : "RS512" + //} + // example PAYLOAD: + //{ + // "iss": "https://nmos-mocks.local:5011", + // "sub" : "test@testsuite.nmos.tv", + // "aud" : [ + // "https://*.testsuite.nmos.tv", + // "https://*.local" + // ], + // "exp" : 4828204800, + // "iat" : 1696868272, + // "scope" : "registration", + // "client_id" : "458f6d06-46b1-49fd-b778-7c30428889c6", + // "x-nmos-registration" : { + // "read": [ + // "*" + // ], + // "write": [ + // "*" + // ] + // } + //} + // invalid iat(00:00:00 1/1/2123) + const utility::string_t invalid_iat_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0Ijo0ODI4MjA0ODAwLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.pZIddJ8wtSXR4KerpmxChWiqPCIrvPj0ZsrpkrBdOvfgP_rDC-Sy7LLnP4hPEMwGqdnZKK9hJGa1uGRz2O971jwbM-n2UPzfbVpyn66A5OLnppizuWcUIij_zS0ZiXG7Lq4jmZ4vd7GnvCtwpxBKZHSXMCBwps_E7xtg6thZKoTXRIAVPu2InlNyRO5g7BmI5eLZ2vyy5WanHkL29b_lMKEzG8nOw45BdNkRq1uLB6c_aOjR1Ln1Jpcd-DIdfSGSGHLAOGg-aM0R3804W7jtNUugmZ1xyybr6g09CQst4u9A8cNdtHyob5oyCPzlGwU5fnpeYnkaKqH7mADdgC5oyA"); + // missing client_id and azp + const utility::string_t missing_clientid_azp_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.PZlG03pVQMQMTCyOOSfRHQcQxLL5beDSa6J7yMPk80_KHFhUPzBttGu-cc3j6LH4tcjc_tSCbvAZTW4Po9lF4CgZ-K6DqYuCnKT3S-Q2JUSBILRVy8JcogVT12QtwNzECIIHaQsy2M4t4Geyux5lvMRPQwmfx8QOb4ZuM9_ArEDt9vWdmrJ1l81Luj6XoduwoumyivUUE7ZydFXCE1BCIPA79xOMtidPwbiym0AlSQ00lg0TsRpjcmxcy8E_BXnxKiVyRjy6R9e7eEI3ABqvnDL2KbMd4iOYPmO3Gd3r-KMTTXFx3xcQkDmfw0rAqKofp6H4S5Qhzfk-Qq90Hl6yAQ"); + // missing client_id only azp + const utility::string_t azp_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImF6cCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.njx3yJsJVLO6r8P_U7Eutpr7Iygv5D0T0B9h5kPsryZ8JFc1k3OQ5-ZROeKMTl3an2VvaXXVRJrkzn_k-6W-PwbSW7XMKMNpmeXbOGGu81YFL0bLXoaZrF1Tq6_3ZTjmOj0mHV7kxIXrc239lMRPQu5fAOtLUQFVHR-IdmraWI_1kQCh1UijJSuE2wKSr31PyF2BhfQ3w17JIYWy5SHR9psygUlg9e5EgHrMOpr67gOtrsYtJ1G5enbNYQGSXN6Wcy7U35Py_foqTGk8nmExr5MnEYyUTmfisXYIfKqp6nbYyBPE_ybGUNFx8XsyTW8t_Vqa79hOzKwupx2GuqnutQ"); + // mismacthed client_id and azp + const utility::string_t mismatch_clientid_azp_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsImF6cCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNyIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.hxs8NR2mMgjky0mkrhV487_stKVdbjxIkYq8kTszSdYrTqOPHZ3e9GC6husO0uirLAroD_yXngTfKekS3SDTMVbDyjNdzDQpC3eXVLQSMg5_Fi3dEHXPWRguOuQ4U6LNX6xoNQVNWWotjbHpndXzKnaySrfS7B2tzcj95pb1f64JPzvkGIWKiZ-STw1sej-T4AQpwO3whMe2_9k_ngB6r5Yvwj33nZfF5SWwiIUkQL4YW3HnSJhW2iz85kWoBrwzeSF8DboE_t2blVN16CMZPI9ZitFEFfTnAAfbx_zsV9sktLjsP2Rg659FqOpZNSo60HX4qr0GfTLPOXDhJDH9yg"); + // bad scope + const utility::string_t bad_scope_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6ImJhZCIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.udlp4ZfONuajmcIYdopQH4CUg-N1d_Ok5cibhhmG5uS3JS2dK3LyYfdra3tKsrRCGuTYvn7UCcd7XfR1fIh_4413CdQ19o5suU04e_zZRy3guDoLsyVuvY1X8_PSCkz00BgoQ1M4kjMc9bDiCzhl-2iTDxAMma34MGTz5_hCgvdPjv5SZ3k2XCQmkC-_ZI3j3WqTkvEV9XvNAUSAgF5Q-zgRJagyqdGvRBz-XMAG0aJVFEcA_X7j8eP3C5RomCPuoBDectcIysOUZGqgqzbKnJf-UjIMFiVGc2t5WntsPrLQj6OJKQgn4FRY65j0QZQFt-Pam8KIaLpmRAtKO5bRUA"); + // missing optional scope + const utility::string_t missing_scope_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJjbGllbnRfaWQiOiI0NThmNmQwNi00NmIxLTQ5ZmQtYjc3OC03YzMwNDI4ODg5YzYiLCJ4LW5tb3MtcmVnaXN0cmF0aW9uIjp7InJlYWQiOlsiKiJdLCJ3cml0ZSI6WyIqIl19fQ.S-Jhf-q7_eNr733TZao5vHAFeYd2e9ZuLm1isF9fXyqvtodPFAQpiZJcUGZyWyeDsfbe56VWaoI0JubEBU8PFDq8IrqY3g6ySVr8jk-kB-4f9szy9-hmCjWCrZLmxJXgRR9xcYhwzgA7U_Enb5rrSO8afrOYxKxZeqySAKQryIqQYU6aOSzAGKGpkhdtZwzyraQb0LJE0nJrWonEST13Ebzg6LyXD72cISNdUN5miWn77kZ5E5fv_zb-AyvcqBAhM2FxYi6gM8L9Bv6nN-dbFxXZgiaoBkPXn--PfYb5jwiis3w3x79ZcSoUIMr3JLiWPR6U4QI2ApU4V2rEgEZzlg"); + // invalid nbf(not before 00:00:00 1/1/2123) + const utility::string_t invalid_nbf_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJuYmYiOjQ4MjgyMDQ4MDAsInNjb3BlIjoicmVnaXN0cmF0aW9uIiwiY2xpZW50X2lkIjoiNDU4ZjZkMDYtNDZiMS00OWZkLWI3NzgtN2MzMDQyODg4OWM2IiwieC1ubW9zLXJlZ2lzdHJhdGlvbiI6eyJyZWFkIjpbIioiXSwid3JpdGUiOlsiKiJdfX0.WL8XBv2IQB-2TZegIgkJ6oqjH0hkYxeAL3vL_eGE2Xy31U7RKWpq9PkmSfvf4wOe9UNkgEjfc8XIXwdQm4YB5aT8WSXkB9DnXRi6Dr8BJ2v_oRNzT8n75UAnbheqdq9CVNFSy7QVNr95oBGpSeeUL4vRCbGOghjOKUOjNjzuksoLB-52-VNoIRA0T5kwSqaRAL-r0Am8v0ucCzJ1OVtdV-WVMqw9-JrLde9oq_dJPAIZ6no4kfvE2ulxKNu8jRni4L6h3ejlrxdExiQFIt-PGHjeJ8ES8WxIYGdUNxulMiP99ta4pWkGwlRMwNvqa5saflvT1uHXKgOgENpZoZSEew"); + // expired token + const utility::string_t expired_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6MTY5Njg2OTI3MiwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.VAzXD5vkrhumbOAtqgeCJL2quZktwySI8AIE9QUOIm_wIUs7ICvd5vzWhxmgie0SGYdDvX3bQxQyFzCHU2Vccp-v3DSzO4vWx7b8zUJpIL8815dtUVpFr81V1Y8G40Ok-QRJhiJWHNHk3y-dh0AEyWtGjiqnfgMThIw3SnbVk0krUb6d-hTHHmyzk5qFkLGPVRWG2d29tTTKH0j4VY4XD_ONp-M6rTO3zGlCMV2wvlJA8jtuScRzfc5gimfNAZVPiIIqKEQHIGXX1ZaI-iJYIHxKFxXkca5K4r1p0FWaXlGgDTFQEmcgCMw9YRSv3Hl83b8ysQqkkdkkBIVugVm3ew"); + // missing private-claim (x-nmos-*) + const utility::string_t missing_private_claim_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiJ9.fyRLkLPIS1WIK5z0FCEuTss7SHAKFtsM6EUB_I7yz1YyIRR4UE-DV2V8YPCNF4dFy-4CzsCpQWUsiGvTtnfwzdcygheiyhB6QyloINJXeSCm-0wB95z285KW6AH5vbVadlBRFkMphsxkDeWP1X7lunkqv3oKei1jFGSNnc3QE4gORoGj4YqAVvUA82X3nV8eI5vNE2XHmBG_HkgTjX_JEqVr-9UcQ1EnqVDPuzrCFaQiFirZCpwg0cRHhVrmJCOrfG-bPIcX3KRfWKCaH5O2n736AwOMFqX7f4VdSbSSx7HcO1CxsmVGwQ-i8fab1IBi4KOvRsSHp3Ti3FxQTEnsEw"); + // valid token (expired at 00:00:00 1/1/2123) + const utility::string_t valid_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl0sIndyaXRlIjpbIioiXX19.ybx4VU2E6tuFbWFbCUwKyKm_MPmAXZv70x_2eyuS_Z4qF8rgB0M_yXIJMt_5padA-NPRTd8XIvnq7TLJTYMUV9-F45oQLBBWgiBQh2shsmjYg-1fHCHLxXXdlVLzxennbE38Sm60Jo-u3ZC9yFiYBMaOL5ai6f8bhzNdYaz0xbI8XZaki1pICKgVfpq1XKbXBhUD0quRwfl4PjzKfu0rtAxYc_5IxDWkxJx7BYSHR_lkMaOINda8mkSnim9V7wqkGylOc6b38OoXORtfGJCdmhc_oR9n2jwj_42r4HPo6rEul9_yYUwcYOBG65RLEB3-cbwbj8DNPguHu_TnbzBJsA"); + + { + // invalid nbf(not before 00:00:00 1/1/2123), on GET request + BST_REQUIRE_THROW(jwt_validator.basic_validation(invalid_nbf_token), jwt::error::token_verification_exception); + // expired token, on GET request + BST_REQUIRE_THROW(jwt_validator.basic_validation(expired_token), jwt::error::token_verification_exception); + // invalid audience, on GET request + BST_REQUIRE_THROW(nmos::experimental::jwt_validator::registered_claims_validation(valid_token, web::http::methods::GET, U("/x-nmos/registration/v1.3"), nmos::experimental::scopes::registration, U("https://api-nmos.bad_audience.com")), nmos::experimental::insufficient_scope_exception); + + // missing optional scope, on GET request + BST_REQUIRE_NO_THROW(jwt_validator.basic_validation(missing_scope_token)); + BST_REQUIRE_NO_THROW(nmos::experimental::jwt_validator::registered_claims_validation(missing_scope_token, web::http::methods::GET, U("/x-nmos/registration/v1.3"), nmos::experimental::scopes::registration, audience)); + // missing x-nmos-*, on GET request + BST_REQUIRE_NO_THROW(jwt_validator.basic_validation(missing_private_claim_token)); + BST_REQUIRE_NO_THROW(nmos::experimental::jwt_validator::registered_claims_validation(missing_private_claim_token, web::http::methods::GET, U("/x-nmos/registration/v1.3"), nmos::experimental::scopes::registration, audience)); + // valid token (expired at 00:00:00 1/1/2123), on GET request + BST_REQUIRE_NO_THROW(jwt_validator.basic_validation(valid_token)); + BST_REQUIRE_NO_THROW(nmos::experimental::jwt_validator::registered_claims_validation(valid_token, web::http::methods::GET, U("/x-nmos/registration/v1.3"), nmos::experimental::scopes::registration, audience)); + } + + { + // missing optional scope, on POST request + BST_REQUIRE_THROW(nmos::experimental::jwt_validator::registered_claims_validation(missing_scope_token, web::http::methods::POST, U("/x-nmos/registration/v1.3"), nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + // missing x-nmos-*, on POST request + BST_REQUIRE_THROW(nmos::experimental::jwt_validator::registered_claims_validation(missing_private_claim_token, web::http::methods::POST, U("/x-nmos/registration/v1.3"), nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testAccessTokenPrivateClaim1) +{ + // using rsa.mocks.testsuite.nmos.tv.key.pem private key to create an access token via the https://jwt.io/ + // HEADER: + //{ + // "typ": "JWT", + // "alg" : "RS512" + //} + // example PAYLOAD: + //{ + // "iss": "https://nmos-mocks.local:5011", + // "sub" : "test@testsuite.nmos.tv", + // "aud" : [ + // "https://*.testsuite.nmos.tv", + // "https://*.local" + // ], + // "exp" : 4828204800, + // "iat" : 1696868272, + // "scope" : "registration", + // "client_id" : "458f6d06-46b1-49fd-b778-7c30428889c6", + // "x-nmos-registration" : { + // "read": [ + // "*" + // ] + // } + //} + + // readonly token + const utility::string_t readonly_token = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsicmVhZCI6WyIqIl19fQ.0offeC5TooP73p2VedN27DeyHdjXIY-RFZzf2NCsyrB03dX89v2i3eHDF3nl-ZNFviNAlTiEMZqA9Sb6kvUI4jsmwpHRQ19nA9QQBKmYCog_uLvxUcGroxTJ7f9Nj8WIaWM1NZ25ZlylyOtz7QHhmkqNSVr8-eXYx8zVUtOurFUXNTN7UnCZ3ZpKoj9sR5O4bRb-11oxEKoOjQadHq22CN9_8AReKl1e3dx5aILYG1Xf_gvYxWpTfzYcgIVYjxKarE7msCUe6PnXBzJMlpu1Abu2llNQz7eCTAbNNA-PPN5cYFYuEdXSIcd8erkXSAK_8VbyizJRU1hE0uFFx0r3Iw"); + + // test x-nmos-* + // valid token with x-nmos-registration read only set + BST_REQUIRE_THROW(nmos::experimental::jwt_validator::registered_claims_validation(readonly_token, web::http::methods::POST, U("/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc"), nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testAccessTokenPrivateClaim2) +{ + // using rsa.mocks.testsuite.nmos.tv.key.pem private key to create an access token via the https://jwt.io/ + // HEADER: + //{ + // "typ": "JWT", + // "alg" : "RS512" + //} + // example PAYLOAD: + //{ + // "iss": "https://nmos-mocks.local:5011", + // "sub" : "test@testsuite.nmos.tv", + // "aud" : [ + // "https://*.testsuite.nmos.tv", + // "https://*.local" + // ], + // "exp" : 4828204800, + // "iat" : 1696868272, + // "scope" : "registration", + // "client_id" : "458f6d06-46b1-49fd-b778-7c30428889c6", + // "x-nmos-registration" : { + // "write": [ + // "*" + // ] + // } + //} + + // valid token + // "x-nmos-registration" : { + // "write": [ + // "*" + // ] + // } + const utility::string_t valid_token1 = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsid3JpdGUiOlsiKiJdfX0.exiZrwWY1nvxnS_LA0R0YMbzkpQNbzUneKO5ruwkSlqW7XdI3TQgRoiGXW1vDbC8XH4RQCD8SPoS2vuX4rLMfLGZGLtpHUFp3khAvs142Oc6K15ldYGfGpjeyDxSw9syRtl37XiG1MPOygYaqjEOXpI9Ljwj8jzGyJXpLGWzLHPnC9SkNCfe7C1ATjz86938qEW-ksxKP7CCQbNVWy13Trti7ow5jiSSd71rqB448tliNi9CDcd_xlx9SvRXZmvomUQOWhJlAQnwKbT7krk1gWqw2JFtOVblP8sKsQHdLX6wxc6F_pHlwJJmWg-cLs0oOV7PKzokIqw7wHN0fnQLtQ"); + // + // valid token + // "x-nmos-registration" : { + // "write": [ + // "health/*" + // ] + // } + const utility::string_t valid_token2 = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsid3JpdGUiOlsiaGVhbHRoLyoiXX19.l-dZnLsODuEyGjpT9tWDq5GpzNFtjAIhLDWZI20yfskzKpW0dagNLFp3sKfZOAZspMp3DLb-lCRIp1fXS9rlkBB6mQ-z3XMf4pJXPFaCkxf3EGEsTtpsoYw6jic9Ue8EYPAx7Ma1ersd6TH41HZDi06K9Ko0vwl7qQ4HzctEXMA53afCkc4vIlChWZ8bFAU6gF2avfzU5nAsLPAGrGATFPG4meCmPFtdjnZBLPwyINOP9rCN3Qw6Hwt5f9Y7obAcbuwK9adTYFDqti9j3hzg8p-AGE4Ixo_ItOw0Kg1D1TowlPm7U2pMz-7S4OmwEq8alktufhLPuX_M3m_W5-37Ew"); + // + // valid token + // "x-nmos-registration" : { + // "write": [ + // "health/nodes/*" + // ] + // } + const utility::string_t valid_token3 = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsid3JpdGUiOlsiaGVhbHRoL25vZGVzLyoiXX19.UGcQLz47PYhVFulnKgVoFb6V53bvIwjACPHyvm8P8NCkYcnMvjcyDKPCBnIfEoVT8a9LbCK2rFisgo4Rw3NXhDnfbGRoq-4Dad8TbvFpJfEs-Wcb1GDKeaCuS78NvEW8KhbTXoOD04Yj6vRkLSg_Vk-nalNmpjG1vnUPuLO2DZux36l7Ggaq3kDBcIfDCIicrA7V2cu9qL9EqzgEB2DXtrjZ0y219nkGp7UK6wxdI8_-p1LqvpU7vNJmqserri_waEJ-vWhP3JU8b5aeFuQS946Sjr3PHAAraO0RkDAje20dGPpCE5doMmjNZRIEa529MO-g3LQoZABhUCIr57Z0kA"); + // + // bad token + // "x-nmos-registration" : { + // "write": [ + // "bad/*" + // ] + // } + const utility::string_t invalid_token1 = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsid3JpdGUiOlsiYmFkLyoiXX19.nZZ3gmmuvEJhF51EJM3OMwsT9_xDwix0U2y8tGQ6G-3nrIMDjM5zAYk8_IeyOXgI063wcQLwY0s83hYZXjKH4ifEb9xDAGBSaF-lijVQAaAbzTX5aIFEngz6pBloUGpWnS7LUJbDDhX8bBO00dH8Umh88GNaxxfBmKTDBb7CAlRpMjRHVid4MPdDAcO0SkeI8K5_71LitDjoXGXkqd1r_AKFh5jRQvdZuNy-6pkg1xSHS8HRsskNIguIYFEpciw22KMDbVZKSBiWUq1tTjGzwv2fDrEEnQZDvyNHqep6DxOOzrJPQtwZoADcq1simZ6IZFKf0ewo6SMMfOmC7JNcuQ"); + // + // bad token + // "x-nmos-registration" : { + // "write": [ + // "health/bad/*" + // ] + // } + const utility::string_t invalid_token2 = U("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJpc3MiOiJodHRwczovL25tb3MtbW9ja3MubG9jYWw6NTAxMSIsInN1YiI6InRlc3RAdGVzdHN1aXRlLm5tb3MudHYiLCJhdWQiOlsiaHR0cHM6Ly8qLnRlc3RzdWl0ZS5ubW9zLnR2IiwiaHR0cHM6Ly8qLmxvY2FsIl0sImV4cCI6NDgyODIwNDgwMCwiaWF0IjoxNjk2ODY4MjcyLCJzY29wZSI6InJlZ2lzdHJhdGlvbiIsImNsaWVudF9pZCI6IjQ1OGY2ZDA2LTQ2YjEtNDlmZC1iNzc4LTdjMzA0Mjg4ODljNiIsIngtbm1vcy1yZWdpc3RyYXRpb24iOnsid3JpdGUiOlsiaGVhbHRoL2JhZC8qIl19fQ.o_5XAUKjv4Dyf7cxvuL6bP8GsFhV5IcscndUYenzmGo50sRw0sHvi7eANMTdoh1HAMvTcAYzdpPRPEsIrk2tvKsEVKQzKjCVXw_uKc_Xew00qEF6nUbCPAPd0TotJXTQKtqP_NIcUsRDFWL4X9wpAJQkPdv9xzE_j3RKmbOv3uQq3iRA-TBSOcgJlsCZ37IGNM-_gyOzyRZSKaaY2xAHuPpEt7Gm88sjRmgerIyRLC9zSFt-5jIYAOXlUSMv1tsQK0BQCvqxF_nppHKyfpQacxDTN-UOiD7DvJWhMTpny0mM0mwFnoS-UyQq_cHPA03BDF9-noYeBqo4VMRMx_gnlA"); + + BST_REQUIRE_NO_THROW(nmos::experimental::jwt_validator::registered_claims_validation(valid_token1, web::http::methods::POST, U("/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc"), nmos::experimental::scopes::registration, audience)); + BST_REQUIRE_NO_THROW(nmos::experimental::jwt_validator::registered_claims_validation(valid_token2, web::http::methods::POST, U("/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc"), nmos::experimental::scopes::registration, audience)); + BST_REQUIRE_NO_THROW(nmos::experimental::jwt_validator::registered_claims_validation(valid_token3, web::http::methods::POST, U("/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc"), nmos::experimental::scopes::registration, audience)); + BST_REQUIRE_THROW(nmos::experimental::jwt_validator::registered_claims_validation(invalid_token1, web::http::methods::POST, U("/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc"), nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); + BST_REQUIRE_THROW(nmos::experimental::jwt_validator::registered_claims_validation(invalid_token2, web::http::methods::POST, U("/x-nmos/registration/v1.3/health/nodes/88888888-4444-4444-4444-cccccccccccc"), nmos::experimental::scopes::registration, audience), nmos::experimental::insufficient_scope_exception); +} diff --git a/Development/nmos/test/paging_utils_test.cpp b/Development/nmos/test/paging_utils_test.cpp index 33667193d..724d0b02a 100644 --- a/Development/nmos/test/paging_utils_test.cpp +++ b/Development/nmos/test/paging_utils_test.cpp @@ -112,7 +112,7 @@ namespace BST_TEST_CASE(testCursorBasedPagingDocumentationExamples) { // Initial test cases based on the examples in NMOS documentation - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.5.%20APIs%20-%20Query%20Parameters.md#examples + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.5._APIs_-_Query_Parameters.html#examples const resources resources{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 }; diff --git a/Development/nmos/test/query_api_test.cpp b/Development/nmos/test/query_api_test.cpp new file mode 100644 index 000000000..de7664bc3 --- /dev/null +++ b/Development/nmos/test/query_api_test.cpp @@ -0,0 +1,42 @@ +// The first "test" is of course whether the header compiles standalone +#include "cpprest/json_validator.h" + +#include +#include +#include +#include "bst/test/test.h" +#include "nmos/is04_versions.h" +#include "nmos/json_schema.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testQueryAPISubscriptionsExtensionSchema) +{ + using web::json::value_of; + using web::json::value; + + const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(nmos::is04_versions::all | boost::adaptors::transformed(nmos::experimental::make_queryapi_subscriptions_post_request_schema_uri)) + }; + + // valid subscriptions post request data + // see https://specs.amwa.tv/is-04/releases/v1.3.0/examples/queryapi-subscriptions-post-request.html + auto data = value_of({ + { U("max_update_rate_ms"), 100 }, + { U("resource_path"), U("/nodes") }, + { U("params"), value_of({ + { U("label"), U("host1") } + }) }, + { U("persist"), false }, + { U("secure"), false } + }); + + // validate successfully, i.e. no exception + // see https://specs.amwa.tv/is-04/releases/v1.3.0/APIs/schemas/with-refs/queryapi-subscriptions-post-request.html + validator.validate(data, nmos::experimental::make_queryapi_subscriptions_post_request_schema_uri(nmos::is04_versions::v1_3)); + + // empty path, for experimental extension + data[U("resource_path")] = value::string(U("")); + validator.validate(data, nmos::experimental::make_queryapi_subscriptions_post_request_schema_uri(nmos::is04_versions::v1_3)); +} diff --git a/Development/nmos/test/sdp_test_utils.cpp b/Development/nmos/test/sdp_test_utils.cpp new file mode 100644 index 000000000..f4c141e56 --- /dev/null +++ b/Development/nmos/test/sdp_test_utils.cpp @@ -0,0 +1,31 @@ +#include "nmos/test/sdp_test_utils.h" + +#include "bst/test/test.h" +#include "nmos/sdp_utils.h" + +namespace nmos +{ + typedef std::multimap comparable_fmtp_t; + + inline comparable_fmtp_t comparable_fmtp(const nmos::sdp_parameters::fmtp_t& fmtp) + { + return comparable_fmtp_t{ fmtp.begin(), fmtp.end() }; + } + + void check_sdp_parameters(const nmos::sdp_parameters& lhs, const nmos::sdp_parameters& rhs) + { + BST_REQUIRE_EQUAL(lhs.session_name, rhs.session_name); + BST_REQUIRE_EQUAL(lhs.rtpmap.payload_type, rhs.rtpmap.payload_type); + BST_REQUIRE_EQUAL(lhs.rtpmap.encoding_name, rhs.rtpmap.encoding_name); + BST_REQUIRE_EQUAL(lhs.rtpmap.clock_rate, rhs.rtpmap.clock_rate); + if (0 != lhs.rtpmap.encoding_parameters) + BST_REQUIRE_EQUAL(lhs.rtpmap.encoding_parameters, rhs.rtpmap.encoding_parameters); + else + BST_REQUIRE((0 == rhs.rtpmap.encoding_parameters || 1 == rhs.rtpmap.encoding_parameters)); + BST_REQUIRE_EQUAL(comparable_fmtp(lhs.fmtp), comparable_fmtp(rhs.fmtp)); + BST_REQUIRE_EQUAL(lhs.packet_time, rhs.packet_time); + BST_REQUIRE_EQUAL(lhs.max_packet_time, rhs.max_packet_time); + BST_REQUIRE_EQUAL(lhs.bandwidth.bandwidth_type, rhs.bandwidth.bandwidth_type); + BST_REQUIRE_EQUAL(lhs.bandwidth.bandwidth, rhs.bandwidth.bandwidth); + } +} diff --git a/Development/nmos/test/sdp_test_utils.h b/Development/nmos/test/sdp_test_utils.h new file mode 100644 index 000000000..5de233cf8 --- /dev/null +++ b/Development/nmos/test/sdp_test_utils.h @@ -0,0 +1,11 @@ +#ifndef NMOS_SDP_TEST_UTILS_H +#define NMOS_SDP_TEST_UTILS_H + +namespace nmos +{ + struct sdp_parameters; + + void check_sdp_parameters(const nmos::sdp_parameters& lhs, const nmos::sdp_parameters& rhs); +} + +#endif diff --git a/Development/nmos/test/sdp_utils_test.cpp b/Development/nmos/test/sdp_utils_test.cpp new file mode 100644 index 000000000..edf850a23 --- /dev/null +++ b/Development/nmos/test/sdp_utils_test.cpp @@ -0,0 +1,940 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/sdp_utils.h" + +#include "bst/test/test.h" +#include "nmos/capabilities.h" +#include "nmos/components.h" +#include "nmos/format.h" +#include "nmos/interlace_mode.h" +#include "nmos/json_fields.h" +#include "nmos/media_type.h" +#include "nmos/random.h" +#include "nmos/test/sdp_test_utils.h" +#include "sdp/sdp.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testMakeComponentsMakeSampling) +{ + using web::json::value_of; + + // use the older function to test the newer function + BST_REQUIRE(nmos::make_components(nmos::YCbCr422, 1920, 1080, 8) == nmos::make_components(sdp::samplings::YCbCr_4_2_2, 1920, 1080, 8)); + BST_REQUIRE(nmos::make_components(nmos::RGB444, 3840, 2160, 12) == nmos::make_components(sdp::samplings::RGB, 3840, 2160, 12)); + + const std::vector samplings{ + // Red-Green-Blue-Alpha + sdp::samplings::RGBA, + // Red-Green-Blue + sdp::samplings::RGB, + // Non-constant luminance YCbCr + sdp::samplings::YCbCr_4_4_4, + sdp::samplings::YCbCr_4_2_2, + sdp::samplings::YCbCr_4_2_0, + sdp::samplings::YCbCr_4_1_1, + // Constant luminance YCbCr + sdp::samplings::CLYCbCr_4_4_4, + sdp::samplings::CLYCbCr_4_2_2, + sdp::samplings::CLYCbCr_4_2_0, + // Constant intensity ICtCp + sdp::samplings::ICtCp_4_4_4, + sdp::samplings::ICtCp_4_2_2, + sdp::samplings::ICtCp_4_2_0, + // XYZ + sdp::samplings::XYZ, + // Key signal represented as a single component + sdp::samplings::KEY, + // Sampling signaled by the payload + sdp::samplings::UNSPECIFIED + }; + + const std::vector> dims{ + { 3840, 2160 }, + { 1920, 1080 }, + { 1280, 720 } + }; + + nmos::details::seed_generator seeder; + std::default_random_engine gen(seeder); + + for (const auto& sampling : samplings) + { + for (const auto& dim : dims) + { + auto components = nmos::make_components(sampling, dim.first, dim.second, 10); + BST_REQUIRE(sampling == nmos::details::make_sampling(components.as_array())); + std::shuffle(components.as_array().begin(), components.as_array().end(), gen); + BST_REQUIRE(sampling == nmos::details::make_sampling(components.as_array())); + } + } + + const auto test_no_YCbCr_3_1_1 = value_of({ + nmos::make_component(nmos::component_names::Y, 1440, 1080, 8), + nmos::make_component(nmos::component_names::Cb, 480, 1080, 8), + nmos::make_component(nmos::component_names::Cr, 480, 1080, 8) + }); + BST_CHECK_THROW(nmos::details::make_sampling(test_no_YCbCr_3_1_1.as_array()), std::logic_error); + + const auto test_no_integer_divisor = value_of({ + nmos::make_component(nmos::component_names::Y, 100, 100, 8), + nmos::make_component(nmos::component_names::Cb, 40, 40, 8), + nmos::make_component(nmos::component_names::Cr, 40, 40, 8) + }); + BST_REQUIRE_THROW(nmos::details::make_sampling(test_no_integer_divisor.as_array()), std::logic_error); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testValidateSdpParameters) +{ + using web::json::value; + using web::json::value_of; + + { + // omitting TCS should be treated as "SDR" + const sdp::transfer_characteristic_system omit_tcs; + + // an unimplemented parameter constraint should be ignored + const utility::string_t unimplemented_parameter_constraint{ U("urn:x-nmos:cap:unimplemented") }; + + nmos::video_raw_parameters params{ + 1920, 1080, nmos::rates::rate29_97, true, false, sdp::samplings::YCbCr_4_2_2, 10, + omit_tcs, sdp::colorimetries::BT2020, sdp::type_parameters::type_N + }; + auto sdp_params = nmos::make_video_raw_sdp_parameters(U("-"), params, nmos::details::payload_type_video_default); + + // only format and caps are used to validate SDP parameters + auto receiver = value_of({ + { nmos::fields::format, nmos::formats::video.name }, + { nmos::fields::caps, value_of({ + { nmos::fields::media_types, value_of({ nmos::media_types::video_raw.name }) }, + { nmos::fields::constraint_sets, value_of({ + value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25, nmos::rates::rate29_97 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_bff.name, nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ sdp::colorimetries::BT2020.name, sdp::colorimetries::BT709.name }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ sdp::transfer_characteristic_systems::SDR.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({}, 8, 12) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) }, + { unimplemented_parameter_constraint, nmos::make_caps_string_constraint({ U("ignored") }) } + }) + }) } + }) } + }); + + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(receiver, sdp_params)); + + receiver[nmos::fields::caps][nmos::fields::media_types] = value_of({ U("foo/meow"), U("foo/purr") }); + BST_REQUIRE_THROW(nmos::validate_sdp_parameters(receiver, sdp_params), std::runtime_error); + + receiver[nmos::fields::caps].erase(nmos::fields::media_types); + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(receiver, sdp_params)); + + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0][nmos::caps::format::media_type] = nmos::make_caps_string_constraint({ U("foo/meow") }); + BST_REQUIRE_THROW(nmos::validate_sdp_parameters(receiver, sdp_params), std::runtime_error); + + // empty parameter constraint is always satisfied + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0][nmos::caps::format::media_type] = value::object(); + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(receiver, sdp_params)); + + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0][nmos::caps::format::grain_rate] = nmos::make_caps_rational_constraint({ nmos::rates::rate50 }); + BST_REQUIRE_THROW(nmos::validate_sdp_parameters(receiver, sdp_params), std::runtime_error); + + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0].erase(nmos::caps::format::grain_rate); + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(receiver, sdp_params)); + + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0][nmos::caps::format::component_depth] = nmos::make_caps_integer_constraint({}, 10); + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(receiver, sdp_params)); + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0][nmos::caps::format::component_depth] = nmos::make_caps_integer_constraint({}, nmos::no_minimum(), 10); + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(receiver, sdp_params)); + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0][nmos::caps::format::component_depth] = nmos::make_caps_integer_constraint({}, 10, 10); + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(receiver, sdp_params)); + + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0][nmos::caps::format::component_depth] = nmos::make_caps_integer_constraint({}, 11); + BST_REQUIRE_THROW(nmos::validate_sdp_parameters(receiver, sdp_params), std::runtime_error); + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0][nmos::caps::format::component_depth] = nmos::make_caps_integer_constraint({}, nmos::no_minimum(), 9); + BST_REQUIRE_THROW(nmos::validate_sdp_parameters(receiver, sdp_params), std::runtime_error); + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0][nmos::caps::format::component_depth] = nmos::make_caps_integer_constraint({ 9 }, 8, 12); + BST_REQUIRE_THROW(nmos::validate_sdp_parameters(receiver, sdp_params), std::runtime_error); + + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0].erase(nmos::caps::format::component_depth); + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(receiver, sdp_params)); + + // empty enabled constraint set is always satisfied + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0] = value::object(); + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(receiver, sdp_params)); + + // empty disabled constraint set is not considered and when no constraint set is satisfied, the constraint sets altogether are not satisfied + receiver[nmos::fields::caps][nmos::fields::constraint_sets][0][nmos::caps::meta::enabled] = value::boolean(false); + BST_REQUIRE_THROW(nmos::validate_sdp_parameters(receiver, sdp_params), std::runtime_error); + + // when there are no (enabled) constraint sets, the constraint sets altogether are not satisfied + receiver[nmos::fields::caps][nmos::fields::constraint_sets] = value::array(); + BST_REQUIRE_THROW(nmos::validate_sdp_parameters(receiver, sdp_params), std::runtime_error); + + // when constraint sets aren't in use, that's valid! + receiver[nmos::fields::caps].erase(nmos::fields::constraint_sets); + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(receiver, sdp_params)); + } + + { + nmos::audio_L_parameters params{ 4, 16, 48000, {}, 1 }; + auto sdp_params = nmos::make_audio_L_sdp_parameters(U("-"), params, nmos::details::payload_type_audio_default); + + // only format and caps are used to validate SDP parameters + auto receiver = value_of({ + { nmos::fields::format, nmos::formats::audio.name }, + { nmos::fields::caps, value_of({ + { nmos::fields::media_types, value_of({ nmos::media_types::audio_L(16).name, nmos::media_types::audio_L(24).name }) }, + { nmos::fields::constraint_sets, value_of({ + value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::audio_L(16).name }) }, + { nmos::caps::format::channel_count, nmos::make_caps_integer_constraint({}, 1, 8) }, + { nmos::caps::format::sample_rate, nmos::make_caps_rational_constraint({ 48000 }) }, + { nmos::caps::format::sample_depth, nmos::make_caps_integer_constraint({ 16 }) }, + { nmos::caps::transport::packet_time, nmos::make_caps_number_constraint({ 0.125, 1 }) }, + { nmos::caps::transport::max_packet_time, nmos::make_caps_number_constraint({ 0.125, 1 }) } + }), + value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::audio_L(24).name }) } + }) + }) } + }) } + }); + + // because the SDP parameters don't include 'maxptime', the 'max_packet_time' parameter constraint will be ignored + + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(receiver, sdp_params)); + + params.channel_count = 16; + sdp_params = nmos::make_audio_L_sdp_parameters(U("-"), params, nmos::details::payload_type_audio_default); + BST_REQUIRE_THROW(nmos::validate_sdp_parameters(receiver, sdp_params), std::runtime_error); + + params.bit_depth = 24; + sdp_params = nmos::make_audio_L_sdp_parameters(U("-"), params, nmos::details::payload_type_audio_default); + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(receiver, sdp_params)); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpParametersRoundtrip) +{ + using web::json::value; + + const std::string test_sdp = R"(v=0 +o=- 1643910985 1643910985 IN IP4 192.0.2.0 +s=SDP Example +t=0 0 +m=video 5000 RTP/AVP 96 +c=IN IP4 233.252.0.0/32 +a=source-filter: incl IN IP4 233.252.0.0 192.0.2.0 +a=rtpmap:96 raw/90000 +)"; + + auto test_description = sdp::parse_session_description(test_sdp); + auto params = nmos::parse_session_description(test_description); + params.second[0][nmos::fields::interface_ip] = value::string(U("192.0.2.0")); + auto session_description = nmos::make_session_description(params.first, params.second); + + auto test_sdp2 = sdp::make_session_description(session_description); + std::istringstream expected(test_sdp), actual(test_sdp2); + do + { + std::string expected_line, actual_line; + std::getline(expected, expected_line); + std::getline(actual, actual_line); + // CR cannot appear in a raw string literal, so remove it from the actual line + if (!actual_line.empty() && '\r' == actual_line.back()) actual_line.pop_back(); + BST_CHECK_EQUAL(expected_line, actual_line); + } while (!expected.fail() && !actual.fail()); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testInterpretationOfSdpFilesUnicast) +{ + using web::json::value; + using web::json::value_of; + + // See https://specs.amwa.tv/is-05/releases/v1.1.1/docs/4.1._Behaviour_-_RTP_Transport_Type.html#unicast + + const std::string test_sdp = R"(v=0 +o=- 2890844526 2890842807 IN IP4 10.47.16.5 +s=SDP Example +c=IN IP4 10.46.16.34/127 +t=2873397496 2873404696 +a=recvonly +m=video 51372 RTP/AVP 99 +a=rtpmap:99 h263-1998/90000 +)"; + + const auto test_params = value_of({ + value_of({ + { nmos::fields::source_ip, value::null() }, + { nmos::fields::multicast_ip, value::null() }, + { nmos::fields::interface_ip, U("10.46.16.34") }, + { nmos::fields::destination_port, 51372 }, + { nmos::fields::rtp_enabled, true }, + }) + }); + + auto session_description = sdp::parse_session_description(test_sdp); + auto transport_params = nmos::get_session_description_transport_params(session_description); + + BST_REQUIRE(test_params == transport_params); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testInterpretationOfSdpFilesSourceSpecificMulticast) +{ + using web::json::value; + using web::json::value_of; + + // See https://specs.amwa.tv/is-05/releases/v1.1.1/docs/4.1._Behaviour_-_RTP_Transport_Type.html#source-specific-multicast + + const std::string test_sdp = R"(v=0 +o=- 1497010742 1497010742 IN IP4 172.29.26.24 +s=SDP Example +t=2873397496 2873404696 +m=video 5000 RTP/AVP 103 +c=IN IP4 232.21.21.133/32 +a=source-filter:incl IN IP4 232.21.21.133 172.29.226.24 +a=rtpmap:103 raw/90000 +)"; + + const auto test_params = value_of({ + value_of({ + { nmos::fields::source_ip, U("172.29.226.24") }, + { nmos::fields::multicast_ip, U("232.21.21.133") }, + { nmos::fields::interface_ip, U("auto") }, + { nmos::fields::destination_port, 5000 }, + { nmos::fields::rtp_enabled, true }, + }) + }); + + auto session_description = sdp::parse_session_description(test_sdp); + auto transport_params = nmos::get_session_description_transport_params(session_description); + + BST_REQUIRE(test_params == transport_params); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testInterpretationOfSdpFilesSeparateSourceAddresses) +{ + using web::json::value; + using web::json::value_of; + + // See https://specs.amwa.tv/is-05/releases/v1.1.1/docs/4.1._Behaviour_-_RTP_Transport_Type.html#separate-source-addresses + + const std::string test_sdp = R"(v=0 +o=ali 1122334455 1122334466 IN IP4 dup.example.com +s=DUP Grouping Semantics +t=0 0 +m=video 30000 RTP/AVP 100 +c=IN IP4 233.252.0.1/127 +a=source-filter:incl IN IP4 233.252.0.1 198.51.100.1 198.51.100.2 +a=rtpmap:100 MP2T/90000 +a=ssrc:1000 cname:ch1@example.com +a=ssrc:1010 cname:ch1@example.com +a=ssrc-group:DUP 1000 1010 +a=mid:Ch1 +)"; + + const auto test_params = value_of({ + value_of({ + { nmos::fields::source_ip, U("198.51.100.1") }, + { nmos::fields::multicast_ip, U("233.252.0.1") }, + { nmos::fields::interface_ip, U("auto") }, + { nmos::fields::destination_port, 30000 }, + { nmos::fields::rtp_enabled, true }, + }), + value_of({ + { nmos::fields::source_ip, U("198.51.100.2") }, + { nmos::fields::multicast_ip, U("233.252.0.1") }, + { nmos::fields::interface_ip, U("auto") }, + { nmos::fields::destination_port, 30000 }, + { nmos::fields::rtp_enabled, true }, + }) + }); + + auto session_description = sdp::parse_session_description(test_sdp); + auto transport_params = nmos::get_session_description_transport_params(session_description); + + BST_REQUIRE(test_params == transport_params); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testInterpretationOfSdpFilesSeparateDestinationAddresses) +{ + using web::json::value; + using web::json::value_of; + + // See https://specs.amwa.tv/is-05/releases/v1.1.1/docs/4.1._Behaviour_-_RTP_Transport_Type.html#separate-destination-addresses + + const std::string test_sdp = R"(v=0 +o=ali 1122334455 1122334466 IN IP4 dup.example.com +s=DUP Grouping Semantics +t=0 0 +a=group:DUP S1a S1b +m=video 30000 RTP/AVP 100 +c=IN IP4 233.252.0.1/127 +a=source-filter:incl IN IP4 233.252.0.1 198.51.100.1 +a=rtpmap:100 MP2T/90000 +a=mid:S1a +m=video 30000 RTP/AVP 101 +c=IN IP4 233.252.0.2/127 +a=source-filter:incl IN IP4 233.252.0.2 198.51.100.1 +a=rtpmap:101 MP2T/90000 +a=mid:S1b +)"; + + const auto test_params = value_of({ + value_of({ + { nmos::fields::source_ip, U("198.51.100.1") }, + { nmos::fields::multicast_ip, U("233.252.0.1") }, + { nmos::fields::interface_ip, U("auto") }, + { nmos::fields::destination_port, 30000 }, + { nmos::fields::rtp_enabled, true }, + }), + value_of({ + { nmos::fields::source_ip, U("198.51.100.1") }, + { nmos::fields::multicast_ip, U("233.252.0.2") }, + { nmos::fields::interface_ip, U("auto") }, + { nmos::fields::destination_port, 30000 }, + { nmos::fields::rtp_enabled, true }, + }) + }); + + auto session_description = sdp::parse_session_description(test_sdp); + auto transport_params = nmos::get_session_description_transport_params(session_description); + + BST_REQUIRE(test_params == transport_params); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpTransportParamsUnicast) +{ + using web::json::value; + using web::json::value_of; + + const auto test_params = nmos::sdp_parameters(U("SDP Example"), nmos::sdp_parameters::video_t{}, 99); + + const auto test_sender_params = value_of({ + value_of({ + { nmos::fields::source_ip, U("10.46.116.34") }, + { nmos::fields::destination_ip, U("10.46.16.34") }, + { nmos::fields::source_port, 5004 }, + { nmos::fields::destination_port, 51372 }, + { nmos::fields::rtp_enabled, true } + }) + }); + + for (int i = 0; i < 2; ++i) + { + const bool source_filters = i == 0; + + const auto test_receiver_params = value_of({ + value_of({ + { nmos::fields::source_ip, source_filters ? value::string(U("10.46.116.34")) : value::null() }, + { nmos::fields::multicast_ip, value::null() }, + { nmos::fields::interface_ip, U("10.46.16.34") }, + { nmos::fields::destination_port, 51372 }, + { nmos::fields::rtp_enabled, true }, + }) + }); + + const auto sender_sdp = nmos::make_session_description(test_params, test_sender_params, source_filters); + auto receiver_params = nmos::get_session_description_transport_params(sender_sdp); + BST_REQUIRE(test_receiver_params == receiver_params); + + const auto receiver_sdp = nmos::make_session_description(test_params, receiver_params); + receiver_params = nmos::get_session_description_transport_params(receiver_sdp); + BST_REQUIRE(test_receiver_params == receiver_params); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpTransportParamsMulticast) +{ + using web::json::value; + using web::json::value_of; + + const auto test_params = nmos::sdp_parameters(U("SDP Example"), nmos::sdp_parameters::video_t{}, 103); + + const auto test_sender_params = value_of({ + value_of({ + { nmos::fields::source_ip, U("172.29.226.24") }, + { nmos::fields::destination_ip, U("232.21.21.133") }, + { nmos::fields::source_port, 5004 }, + { nmos::fields::destination_port, 5000 }, + { nmos::fields::rtp_enabled, true } + }) + }); + + for (int i = 0; i < 2; ++i) + { + // i.e. source-specific multicast then any-source multicast + const bool source_filters = i == 0; + + const auto test_receiver_params = value_of({ + value_of({ + { nmos::fields::source_ip, source_filters ? value::string(U("172.29.226.24")) : value::null() }, + { nmos::fields::multicast_ip, U("232.21.21.133") }, + { nmos::fields::interface_ip, U("auto") }, + { nmos::fields::destination_port, 5000 }, + { nmos::fields::rtp_enabled, true }, + }) + }); + + const auto sender_sdp = nmos::make_session_description(test_params, test_sender_params, source_filters); + auto receiver_params = nmos::get_session_description_transport_params(sender_sdp); + BST_REQUIRE(test_receiver_params == receiver_params); + + // replace "auto" + receiver_params[0][nmos::fields::interface_ip] = value::string(U("172.29.126.24")); + + const auto receiver_sdp = nmos::make_session_description(test_params, receiver_params); + receiver_params = nmos::get_session_description_transport_params(receiver_sdp); + BST_REQUIRE(test_receiver_params == receiver_params); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpParametersVideoRaw) +{ + // a=fmtp:96 colorimetry=BT709; exactframerate=30000/1001; depth=10; TCS=SDR; sampling=YCbCr-4:2:2; width=1920; interlace; TP=2110TPN; PM=2110GPM; height=1080; SSN=ST2110-20:2017 + // cf. testSdpRoundtrip in sdp/test/sdp_test.cpp + + std::pair example{ + { + U("example"), + sdp::media_types::video, + { + 96, + U("raw"), + 90000 + }, + { + { U("colorimetry"), U("BT709") }, + { U("exactframerate"), U("30000/1001") }, + { U("depth"), U("10") }, + { U("TCS"), U("SDR") }, + { U("sampling"), U("YCbCr-4:2:2") }, + { U("width"), U("1920") }, + { U("interlace"), {} }, + { U("TP"), U("2110TPN") }, + { U("PM"), U("2110GPM") }, + { U("height"), U("1080") }, + { U("SSN"), U("ST2110-20:2017") } + } + }, + { + sdp::samplings::YCbCr_4_2_2, + 10, + 1920, + 1080, + nmos::rates::rate29_97, + true, + false, + sdp::transfer_characteristic_systems::SDR, + sdp::colorimetries::BT709, + {}, + {}, + sdp::packing_modes::general, + sdp::smpte_standard_numbers::ST2110_20_2017, + sdp::type_parameters::type_N, + {}, + {}, + {}, + {}, + {} + } + }; + + std::pair wacky{ + { + U("wacky"), + sdp::media_types::video, + { + 123, + U("raw"), + 90000 + }, + { + { U("sampling"), U("UNSPECIFIED") }, + { U("depth"), U("16") }, + { U("width"), U("9999") }, + { U("height"), U("6666") }, + { U("exactframerate"), U("123") }, + { U("interlace"), {} }, + { U("segmented"), {} }, + { U("TCS"), U("ST2115LOGS3") }, + { U("colorimetry"), U("BT2100") }, + { U("RANGE"), U("FULLPROTECT") }, + { U("PAR"), U("12:11") }, + { U("PM"), U("2110BPM") }, + { U("SSN"), U("ST2110-20:2022") }, + { U("TP"), U("2110TPW") }, + { U("TROFF"), U("37") }, + { U("CMAX"), U("42") }, + { U("MAXUDP"), U("57") }, + { U("TSMODE"), U("SAMP") }, + { U("TSDELAY"), U("82") } + } + }, + { + sdp::samplings::UNSPECIFIED, + 16, + 9999, + 6666, + { 123, 1 }, + true, + true, + sdp::transfer_characteristic_systems::ST2115LOGS3, + sdp::colorimetries::BT2100, + sdp::ranges::FULLPROTECT, + { 12, 11 }, + sdp::packing_modes::block, + sdp::smpte_standard_numbers::ST2110_20_2022, + sdp::type_parameters::type_W, + 37, + 42, + 57, + sdp::timestamp_modes::SAMP, + 82 + } + }; + + std::pair zero_troff_tsdelay{ + { + U("zero_troff_tsdelay"), + sdp::media_types::video, + { + 123, + U("raw"), + 90000 + }, + { + { U("sampling"), U("UNSPECIFIED") }, + { U("depth"), U("16") }, + { U("width"), U("9999") }, + { U("height"), U("6666") }, + { U("exactframerate"), U("123") }, + { U("interlace"), {} }, + { U("segmented"), {} }, + { U("TCS"), U("ST2115LOGS3") }, + { U("colorimetry"), U("BT2100") }, + { U("RANGE"), U("FULLPROTECT") }, + { U("PAR"), U("12:11") }, + { U("PM"), U("2110BPM") }, + { U("SSN"), U("ST2110-20:2022") }, + { U("TP"), U("2110TPW") }, + { U("TROFF"), U("0") }, + { U("CMAX"), U("42") }, + { U("MAXUDP"), U("57") }, + { U("TSMODE"), U("SAMP") }, + { U("TSDELAY"), U("0") } + } + }, + { + sdp::samplings::UNSPECIFIED, + 16, + 9999, + 6666, + { 123, 1 }, + true, + true, + sdp::transfer_characteristic_systems::ST2115LOGS3, + sdp::colorimetries::BT2100, + sdp::ranges::FULLPROTECT, + { 12, 11 }, + sdp::packing_modes::block, + sdp::smpte_standard_numbers::ST2110_20_2022, + sdp::type_parameters::type_W, + 0U, + 42, + 57, + sdp::timestamp_modes::SAMP, + 0U + } + }; + + for (auto& test : { example, wacky, zero_troff_tsdelay }) + { + auto made = nmos::make_video_raw_sdp_parameters(test.first.session_name, test.second, test.first.rtpmap.payload_type); + nmos::check_sdp_parameters(test.first, made); + auto roundtripped = nmos::make_video_raw_sdp_parameters(made.session_name, nmos::get_video_raw_parameters(made), made.rtpmap.payload_type); + nmos::check_sdp_parameters(test.first, roundtripped); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpParametersAudioL) +{ + std::pair example{ + { + U("example"), + sdp::media_types::audio, + { + 97, + U("L24"), + 48000, + 8 + }, + { + { U("channel-order"), U("SMPTE2110.(51,ST)") } + }, + {}, + 0.125 + }, + { + 8, + 24, + 48000, + U("SMPTE2110.(51,ST)"), // not testing nmos::make_fmtp_channel_order here + {}, + {}, + 0.125 + } + }; + + std::pair wacky{ + { + U("wacky"), + sdp::media_types::audio, + { + 123, + U("L16"), + 96000 + }, + { + { U("channel-order"), U("SMPTE2110.(M,M,M,M,ST,U02)") }, + { U("TSMODE"), U("SAMP") }, + { U("TSDELAY"), U("82") } + }, + {}, + 0.333 + }, + { + 1, + 16, + 96000, + U("SMPTE2110.(M,M,M,M,ST,U02)"), // not testing nmos::make_fmtp_channel_order here + sdp::timestamp_modes::SAMP, + 82, + 0.333 + } + }; + + std::pair zero_tsdelay{ + { + U("zero_tsdelay"), + sdp::media_types::audio, + { + 123, + U("L16"), + 96000 + }, + { + { U("channel-order"), U("SMPTE2110.(M,M,M,M,ST,U02)") }, + { U("TSMODE"), U("SAMP") }, + { U("TSDELAY"), U("0") } + }, + {}, + 0.333 + }, + { + 1, + 16, + 96000, + U("SMPTE2110.(M,M,M,M,ST,U02)"), // not testing nmos::make_fmtp_channel_order here + sdp::timestamp_modes::SAMP, + 0U, + 0.333 + } + }; + + for (auto& test : { example, wacky, zero_tsdelay }) + { + auto made = nmos::make_audio_L_sdp_parameters(test.first.session_name, test.second, test.first.rtpmap.payload_type); + nmos::check_sdp_parameters(test.first, made); + auto roundtripped = nmos::make_audio_L_sdp_parameters(made.session_name, nmos::get_audio_L_parameters(made), made.rtpmap.payload_type); + nmos::check_sdp_parameters(test.first, roundtripped); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpParametersVideoSmpte291) +{ + std::pair example{ + { + U("example"), + sdp::media_types::video, + { + 100, + U("smpte291"), + 90000 + }, + { + { U("DID_SDID"), U("{0x41,0x01}") }, + { U("VPID_Code"), U("133") } + } + }, + { + { { 0x41, 0x01 } }, + nmos::vpid_codes::vpid_1_5Gbps_1080_line, + {}, + {}, + {}, + {}, + {}, + {} + } + }; + + std::pair wacky{ + { + U("wacky"), + sdp::media_types::video, + { + 123, + U("smpte291"), + 90000 + }, + { + { U("DID_SDID"), U("{0xAB,0xCD}") }, + { U("DID_SDID"), U("{0xEF,0x01}") }, + { U("VPID_Code"), U("132") }, + { U("exactframerate"), U("60000/1001") }, + { U("TM"), U("CTM") }, + { U("SSN"), U("ST2110-40:2021") }, + { U("TROFF"), U("37") }, + { U("TSMODE"), U("SAMP") }, + { U("TSDELAY"), U("82") } + } + }, + { + { { 0xAB, 0xCD }, { 0xEF, 0x01 } }, + nmos::vpid_codes::vpid_1_5Gbps_720_line, + nmos::rates::rate59_94, + sdp::transmission_models::compatible, + sdp::smpte_standard_numbers::ST2110_40_2023, + 37, + sdp::timestamp_modes::SAMP, + 82 + } + }; + + std::pair zero_troff_tsdelay{ + { + U("zero_troff_tsdelay"), + sdp::media_types::video, + { + 123, + U("smpte291"), + 90000 + }, + { + { U("DID_SDID"), U("{0xAB,0xCD}") }, + { U("DID_SDID"), U("{0xEF,0x01}") }, + { U("VPID_Code"), U("132") }, + { U("exactframerate"), U("60000/1001") }, + { U("TM"), U("CTM") }, + { U("SSN"), U("ST2110-40:2021") }, + { U("TROFF"), U("0") }, + { U("TSMODE"), U("SAMP") }, + { U("TSDELAY"), U("0") } + } + }, + { + { { 0xAB, 0xCD }, { 0xEF, 0x01 } }, + nmos::vpid_codes::vpid_1_5Gbps_720_line, + nmos::rates::rate59_94, + sdp::transmission_models::compatible, + sdp::smpte_standard_numbers::ST2110_40_2023, + 0U, + sdp::timestamp_modes::SAMP, + 0U + } + }; + + for (auto& test : { example, wacky, zero_troff_tsdelay }) + { + auto made = nmos::make_video_smpte291_sdp_parameters(test.first.session_name, test.second, test.first.rtpmap.payload_type); + nmos::check_sdp_parameters(test.first, made); + auto roundtripped = nmos::make_video_smpte291_sdp_parameters(made.session_name, nmos::get_video_smpte291_parameters(made), made.rtpmap.payload_type); + nmos::check_sdp_parameters(test.first, roundtripped); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpParametersVideoSmpte2022_6) +{ + std::pair example{ + { + U("example"), + sdp::media_types::video, + { + 98, + U("SMPTE2022-6"), + 27000000 + }, + { + { U("TP"), U("2110TPN") } + } + }, + { + sdp::type_parameters::type_N, + {} + } + }; + + std::pair wacky{ + { + U("wacky"), + sdp::media_types::video, + { + 123, + U("SMPTE2022-6"), + 27000000 + }, + { + { U("TP"), U("2110TPW") }, + { U("TROFF"), U("37") } + } + }, + { + sdp::type_parameters::type_W, + 37 + } + }; + + std::pair zero_troff{ + { + U("zero_troff"), + sdp::media_types::video, + { + 123, + U("SMPTE2022-6"), + 27000000 + }, + { + { U("TP"), U("2110TPW") }, + { U("TROFF"), U("0") } + } + }, + { + sdp::type_parameters::type_W, + 0U + } + }; + + for (auto& test : { example, wacky, zero_troff }) + { + auto made = nmos::make_video_SMPTE2022_6_sdp_parameters(test.first.session_name, test.second, test.first.rtpmap.payload_type); + nmos::check_sdp_parameters(test.first, made); + auto roundtripped = nmos::make_video_SMPTE2022_6_sdp_parameters(made.session_name, nmos::get_video_SMPTE2022_6_parameters(made), made.rtpmap.payload_type); + nmos::check_sdp_parameters(test.first, roundtripped); + } +} diff --git a/Development/nmos/test/slog_test.cpp b/Development/nmos/test/slog_test.cpp new file mode 100644 index 000000000..2bdf60d8a --- /dev/null +++ b/Development/nmos/test/slog_test.cpp @@ -0,0 +1,26 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/slog.h" + +#include "bst/test/test.h" + +namespace +{ + struct dull_gate + { + bool pertinent(slog::severity level) const { return true; } + void log(const slog::log_message& message) {} + }; +} + +BST_TEST_CASE(testSlogLog) +{ + dull_gate gate; + const auto str = utility::string_t{ U("foo") }; + const auto it = std::make_pair(nmos::id{ U("bar") }, nmos::types::node); + // log statement + slog::log(gate, SLOG_FLF) << str << 42 << str; + slog::log(gate, SLOG_FLF) << it << 42 << it; + // no log statement + slog::log(gate, SLOG_FLF) << str << 42 << str; + slog::log(gate, SLOG_FLF) << it << 42 << it; +} diff --git a/Development/nmos/test/system_resources_test.cpp b/Development/nmos/test/system_resources_test.cpp new file mode 100644 index 000000000..7583729e6 --- /dev/null +++ b/Development/nmos/test/system_resources_test.cpp @@ -0,0 +1,37 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/system_resources.h" + +#include "bst/test/test.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSystemGlobal) +{ + using web::json::value_of; + + nmos::settings settings = value_of({ + { U("system_label"), U("ZBQ System") }, + { U("system_description"), U("System Global Information for ZBQ") }, + { U("system_tags"), value_of({ + { U("location"), value_of({ + { U("Salford") }, + { U("Media City") } + }) }, + { U("studio"), value_of({ + U("HQ1") + }) } + }) }, + { U("system_syslog_host_name"), U("syslog.example.com") }, + { U("system_syslog_port"), 514 }, + { U("system_syslogv2_host_name"), U("syslogv2.example.com") }, + { U("system_syslogv2_port"), 6514 }, + { U("ptp_announce_receipt_timeout"), 3 }, + { U("ptp_domain_number"), 127 }, + { U("registration_heartbeat_interval"), 5 } + }); + + const auto id = nmos::make_id(); + + auto result = nmos::parse_system_global_data(nmos::make_system_global_data(id, settings)); + + BST_REQUIRE_EQUAL(settings, result.second); +} diff --git a/Development/nmos/test/video_jxsv_test.cpp b/Development/nmos/test/video_jxsv_test.cpp new file mode 100644 index 000000000..cfbab508c --- /dev/null +++ b/Development/nmos/test/video_jxsv_test.cpp @@ -0,0 +1,245 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/video_jxsv.h" + +#include "bst/test/test.h" +#include "nmos/json_fields.h" +#include "nmos/test/sdp_test_utils.h" +#include "sdp/sdp.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpParametersRoundtripVideoJpegXs) +{ + using web::json::value; + + // typical SDP data for JPEG XS, based on BCP-006-01 example file + // see https://specs.amwa.tv/bcp-006-01/branches/v1.0-dev/examples/jpeg-xs.html + const std::string test_sdp = R"(v=0 +o=- 1443716955 1443716955 IN IP4 192.168.1.2 +s=SMPTE ST2110-22 JPEG XS +t=0 0 +m=video 30000 RTP/AVP 112 +c=IN IP4 224.1.1.1/64 +b=AS:116000 +a=ts-refclk:localmac=40-a3-6b-a0-2b-d2 +a=mediaclk:direct=0 +a=source-filter: incl IN IP4 224.1.1.1 192.168.1.2 +a=rtpmap:112 jxsv/90000 +a=fmtp:112 packetmode=0; profile=High444.12; level=1k-1; sublevel=Sublev3bpp; depth=10; width=1280; height=720; exactframerate=60000/1001; sampling=YCbCr-4:2:2; colorimetry=BT709; TCS=SDR; RANGE=FULL; SSN=ST2110-22:2019; TP=2110TPN +)"; + + auto test_description = sdp::parse_session_description(test_sdp); + const auto test_params = nmos::parse_session_description(test_description); + + auto& s = test_params.first; + auto& t = test_params.second; + + const auto params = nmos::get_video_jxsv_parameters(s); + + auto s2 = nmos::make_sdp_parameters(s.session_name, params, s.rtpmap.payload_type, s.group.media_stream_ids, s.ts_refclk); + // nmos::make_sdp_parameters always generates a new origin (o=) line and the connection data (c=) ttl value is hard-coded + s2.origin = s.origin; + s2.connection_data.ttl = s.connection_data.ttl; + + auto t2 = t; + t2[0][nmos::fields::interface_ip] = value::string(U("192.168.1.2")); + + auto session_description = nmos::make_session_description(s2, t2); + + auto test_sdp2 = sdp::make_session_description(session_description); + std::istringstream expected(test_sdp), actual(test_sdp2); + do + { + std::string expected_line, actual_line; + std::getline(expected, expected_line); + std::getline(actual, actual_line); + // CR cannot appear in a raw string literal, so remove it from the actual line + if (!actual_line.empty() && '\r' == actual_line.back()) actual_line.pop_back(); + BST_CHECK_EQUAL(expected_line, actual_line); + } while (!expected.fail() && !actual.fail()); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpParametersVideoJpegXs) +{ + std::pair example{ + { + U("example"), + sdp::media_types::video, + { + 112, + U("jxsv"), + 90000 + }, + { + { U("packetmode"), U("0") }, + { U("profile"), U("High444.12") }, + { U("level"), U("1k-1") }, + { U("sublevel"), U("Sublev3bpp") }, + { U("sampling"), U("YCbCr-4:2:2") }, + { U("width"), U("1280") }, + { U("height"), U("720") }, + { U("exactframerate"), U("60000/1001") }, + { U("depth"), U("10") }, + { U("colorimetry"), U("BT709") }, + { U("TCS"), U("SDR") }, + { U("RANGE"), U("FULL") }, + { U("SSN"), U("ST2110-22:2019") }, + { U("TP"), U("2110TPN") } + }, + 116000 + }, + { + sdp::video_jxsv::packetization_mode::codestream, + sdp::video_jxsv::transmission_mode::sequential, + sdp::video_jxsv::profiles::High444_12, + sdp::video_jxsv::levels::Level1k_1, + sdp::video_jxsv::sublevels::Sublev3bpp, + sdp::samplings::YCbCr_4_2_2, + 10, + 1280, + 720, + nmos::rates::rate59_94, + false, + false, + sdp::transfer_characteristic_systems::SDR, + sdp::colorimetries::BT709, + sdp::ranges::FULL, + sdp::smpte_standard_numbers::ST2110_22_2019, + sdp::type_parameters::type_N, + {}, + {}, + {}, + {}, + {}, + 116000 + } + }; + + std::pair wacky{ + { + U("wacky"), + sdp::media_types::video, + { + 123, + U("jxsv"), + 90000 + }, + { + { U("packetmode"), U("1") }, + { U("transmode"), U("0") }, + { U("profile"), U("Light444.12") }, + { U("level"), U("Bayer16k-1") }, + { U("sublevel"), U("Full") }, + { U("sampling"), U("UNSPECIFIED") }, + { U("width"), U("9999") }, + { U("height"), U("6666") }, + { U("exactframerate"), U("123") }, + { U("depth"), U("16") }, + { U("colorimetry"), U("BT2100") }, + { U("TCS"), U("UNSPECIFIED") }, + { U("RANGE"), U("NARROW") }, + { U("SSN"), U("ST2110-20:2022") }, + { U("TP"), U("2110TPW") }, + { U("TROFF"), U("37") }, + { U("CMAX"), U("42") }, + { U("MAXUDP"), U("57") }, + { U("TSMODE"), U("SAMP") }, + { U("TSDELAY"), U("82") } + }, + 200000 + }, + { + sdp::video_jxsv::packetization_mode::slice, + sdp::video_jxsv::transmission_mode::out_of_order, + sdp::video_jxsv::profiles::Light444_12, + sdp::video_jxsv::levels::Bayer16k_1, + sdp::video_jxsv::sublevels::Full, + sdp::samplings::UNSPECIFIED, + 16, + 9999, + 6666, + { 123, 1 }, + false, + false, + sdp::transfer_characteristic_systems::UNSPECIFIED, + sdp::colorimetries::BT2100, + sdp::ranges::NARROW, + sdp::smpte_standard_numbers::ST2110_20_2022, + sdp::type_parameters::type_W, + 37, + 42, + 57, + sdp::timestamp_modes::SAMP, + 82, + 200000 + } + }; + + std::pair zero_troff_tsdelay{ + { + U("zero_troff_tsdelay"), + sdp::media_types::video, + { + 123, + U("jxsv"), + 90000 + }, + { + { U("packetmode"), U("1") }, + { U("transmode"), U("0") }, + { U("profile"), U("Light444.12") }, + { U("level"), U("Bayer16k-1") }, + { U("sublevel"), U("Full") }, + { U("sampling"), U("UNSPECIFIED") }, + { U("width"), U("9999") }, + { U("height"), U("6666") }, + { U("exactframerate"), U("123") }, + { U("depth"), U("16") }, + { U("colorimetry"), U("BT2100") }, + { U("TCS"), U("UNSPECIFIED") }, + { U("RANGE"), U("NARROW") }, + { U("SSN"), U("ST2110-20:2022") }, + { U("TP"), U("2110TPW") }, + { U("TROFF"), U("0") }, + { U("CMAX"), U("42") }, + { U("MAXUDP"), U("57") }, + { U("TSMODE"), U("SAMP") }, + { U("TSDELAY"), U("0") } + }, + 200000 + }, + { + sdp::video_jxsv::packetization_mode::slice, + sdp::video_jxsv::transmission_mode::out_of_order, + sdp::video_jxsv::profiles::Light444_12, + sdp::video_jxsv::levels::Bayer16k_1, + sdp::video_jxsv::sublevels::Full, + sdp::samplings::UNSPECIFIED, + 16, + 9999, + 6666, + { 123, 1 }, + false, + false, + sdp::transfer_characteristic_systems::UNSPECIFIED, + sdp::colorimetries::BT2100, + sdp::ranges::NARROW, + sdp::smpte_standard_numbers::ST2110_20_2022, + sdp::type_parameters::type_W, + 0U, + 42, + 57, + sdp::timestamp_modes::SAMP, + 0U, + 200000 + } + }; + + for (auto& test : { example, wacky, zero_troff_tsdelay }) + { + auto made = nmos::make_video_jxsv_sdp_parameters(test.first.session_name, test.second, test.first.rtpmap.payload_type); + nmos::check_sdp_parameters(test.first, made); + auto roundtripped = nmos::make_video_jxsv_sdp_parameters(made.session_name, nmos::get_video_jxsv_parameters(made), made.rtpmap.payload_type); + nmos::check_sdp_parameters(test.first, roundtripped); + } +} diff --git a/Development/nmos/transfer_characteristic.h b/Development/nmos/transfer_characteristic.h index 07d9b1bb9..74f45e856 100644 --- a/Development/nmos/transfer_characteristic.h +++ b/Development/nmos/transfer_characteristic.h @@ -6,15 +6,34 @@ namespace nmos { // Transfer characteristic (used in video flows) - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/flow_video.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/flow_video.html + // and https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#transfer-characteristic DEFINE_STRING_ENUM(transfer_characteristic) namespace transfer_characteristics { const transfer_characteristic none{}; + // Standard Dynamic Range const transfer_characteristic SDR{ U("SDR") }; - const transfer_characteristic HLG{ U("HLG") }; + // Perceptual Quantization const transfer_characteristic PQ{ U("PQ") }; + // Hybrid Log Gamma + const transfer_characteristic HLG{ U("HLG") }; + + // Since IS-04 v1.3, transfer_characteristic values may be defined in the Flow Attributes register of the NMOS Parameter Registers + + // Video streams of linear encoded floating-point samples (depth=16f), such that all values fall within the range [0..1.0] + const transfer_characteristic LINEAR{ U("LINEAR") }; + // Video Stream of linear encoded floating-point samples (depth=16f) normalized from PQ as specified in ITU-R BT.2100-0 + const transfer_characteristic BT2100LINPQ{ U("BT2100LINPQ") }; + // Video Stream of linear encoded floating-point samples (depth=16f) normalized from HLG as specified in ITU-R BT.2100-0 + const transfer_characteristic BT2100LINHLG{ U("BT2100LINHLG") }; + // Video stream of linear encoded floating-point samples (depth=16f) as specified in SMPTE ST 2065-1 + const transfer_characteristic ST2065_1{ U("ST2065-1") }; + // Video stream utilizing the transfer characteristic specified in SMPTE ST 428-1 Section 4.3 + const transfer_characteristic ST428_1{ U("ST428-1") }; + // Video streams of density encoded samples, such as those defined in SMPTE ST 2065-3 + const transfer_characteristic DENSITY{ U("DENSITY") }; } } diff --git a/Development/nmos/transport.h b/Development/nmos/transport.h index 9778299bb..8b45279b0 100644 --- a/Development/nmos/transport.h +++ b/Development/nmos/transport.h @@ -6,9 +6,9 @@ namespace nmos { // Transports (used in senders and receivers) - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.1.%20APIs%20-%20Common%20Keys.md#transport - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/sender.json - // and https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/APIs/schemas/receiver_core.json + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.1._APIs_-_Common_Keys.html#transport + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/sender.html + // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_core.html // and experimentally, for IS-04 v1.3, IS-05 v1.1, IS-07 v1.0 // also https://github.com/AMWA-TV/nmos-parameter-registers/pull/6 DEFINE_STRING_ENUM(transport) @@ -25,7 +25,7 @@ namespace nmos // "Subclassifications are defined as the portion of the URN which follows the first occurrence of a '.', but prior to any '/' character." // "Versions are defined as the portion of the URN which follows the first occurrence of a '/'." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2.2/docs/2.1.%20APIs%20-%20Common%20Keys.md#use-of-urns + // See https://specs.amwa.tv/is-04/releases/v1.2.2/docs/2.1._APIs_-_Common_Keys.html#use-of-urns inline nmos::transport transport_base(const nmos::transport& transport) { return nmos::transport{ transport.name.substr(0, transport.name.find_first_of(U("./"))) }; diff --git a/Development/nmos/type.h b/Development/nmos/type.h index 6f8c98a41..8da37f685 100644 --- a/Development/nmos/type.h +++ b/Development/nmos/type.h @@ -30,7 +30,7 @@ namespace nmos // all types ordered so that sub-resource types appear after super-resource types // according to the guidelines on referential integrity - // see https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2.1/docs/4.1.%20Behaviour%20-%20Registration.md#referential-integrity + // see https://specs.amwa.tv/is-04/releases/v1.2.1/docs/4.1._Behaviour_-_Registration.html#referential-integrity const std::vector all{ nmos::types::node, nmos::types::device, nmos::types::source, nmos::types::flow, nmos::types::sender, nmos::types::receiver, nmos::types::subscription, nmos::types::grain }; // the Channel Mapping API resource types, see nmos/channelmapping_resources.h @@ -39,6 +39,16 @@ namespace nmos // the System API global configuration resource type, see nmos/system_resources.h const type global{ U("global") }; + + // the Control Protocol API resource type, see nmos/control_protcol_resources.h + const type nc_block{ U("nc_block") }; + const type nc_worker{ U("nc_worker") }; + const type nc_manager{ U("nc_manager") }; + const type nc_device_manager{ U("nc_device_manager") }; + const type nc_class_manager{ U("nc_class_manager") }; + const type nc_receiver_monitor{ U("nc_receiver_monitor") }; + const type nc_receiver_monitor_protected{ U("nc_receiver_monitor_protected") }; + const type nc_ident_beacon{ U("nc_ident_beacon") }; } } diff --git a/Development/nmos/version.h b/Development/nmos/version.h index a16f05660..be914ba8b 100644 --- a/Development/nmos/version.h +++ b/Development/nmos/version.h @@ -8,7 +8,7 @@ namespace nmos // "Core resources such as Sources, Flows, Nodes etc. include a 'version' attribute. // As properties of a given Flow or similar will change over its lifetime, the version // identifies the instant at which this change took place." - // See https://github.com/AMWA-TV/nmos-discovery-registration/blob/v1.2/docs/2.1.%20APIs%20-%20Common%20Keys.md#version + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/2.1._APIs_-_Common_Keys.html#version inline utility::string_t make_version(tai tai = tai_now()) { diff --git a/Development/nmos/video_jxsv.cpp b/Development/nmos/video_jxsv.cpp new file mode 100644 index 000000000..6da8b3a0d --- /dev/null +++ b/Development/nmos/video_jxsv.cpp @@ -0,0 +1,392 @@ +#include "nmos/video_jxsv.h" + +#include +#include "nmos/capabilities.h" +#include "nmos/format.h" +#include "nmos/interlace_mode.h" +#include "nmos/json_fields.h" +#include "nmos/resource.h" + +namespace sdp +{ + namespace video_jxsv + { + struct level_limits + { + uint32_t max_width; + uint32_t max_height; + uint64_t max_pixels; + uint64_t max_pixel_rate; + + friend bool operator<=(const level_limits& lhs, const level_limits& rhs) + { + if (lhs.max_width > rhs.max_width) return false; + if (lhs.max_height > rhs.max_height) return false; + if (lhs.max_pixels > rhs.max_pixels) return false; + if (lhs.max_pixel_rate > rhs.max_pixel_rate) return false; + return true; + } + }; + + // Calculate the lowest possible JPEG XS level from the specified frame rate and dimensions + level get_level(const nmos::rational& frame_rate, uint32_t frame_width, uint32_t frame_height) + { + // See https://en.wikipedia.org/wiki/JPEG_XS#Profiles,_levels_and_sublevels + static const std::pair levels[] = { + { levels::Level1k_1, { 1280, 5120, 2621440, 83558400 } }, + { levels::Level2k_1, { 2048, 8192, 4194304, 133693440 } }, + { levels::Level4k_1, { 4096, 16384, 8912896, 267386880 } }, + { levels::Level4k_2, { 4096, 16384, 16777216, 534773760 } }, + { levels::Level4k_3, { 4096, 16384, 16777216, 1069547520 } }, + { levels::Level8k_1, { 8192, 32768, 35651584, 1069547520 } }, + { levels::Level8k_2, { 8192, 32768, 67108864, 2139095040 } }, + { levels::Level8k_3, { 8192, 32768, 67108864, 4278190080 } }, + { levels::Level10k_1, { 10240, 40960, 104857600, 3342336000 } } + }; + + const auto sampling_points = (int64_t)frame_width * (int64_t)frame_height; + const auto pixels_per_second = sampling_points * frame_rate; + const level_limits value{ frame_width, frame_height, (uint64_t)sampling_points, uint64_t(boost::rational_cast(pixels_per_second) + 0.5) }; + + for (const auto& level : levels) + { + if (value <= level.second) return level.first; + } + + return{}; + } + } +} + +namespace nmos +{ + std::pair make_packet_transmission_mode(const nmos::packet_transmission_mode& mode) + { + if (nmos::packet_transmission_modes::codestream == mode) + { + return{ sdp::video_jxsv::codestream, sdp::video_jxsv::sequential }; + } + else if (nmos::packet_transmission_modes::slice_sequential == mode) + { + return{ sdp::video_jxsv::slice, sdp::video_jxsv::sequential }; + } + else if (nmos::packet_transmission_modes::slice_out_of_order == mode) + { + return{ sdp::video_jxsv::slice, sdp::video_jxsv::out_of_order }; + } + throw std::invalid_argument("invalid packet_transmission_mode"); + } + + nmos::packet_transmission_mode parse_packet_transmission_mode(sdp::video_jxsv::packetization_mode packetmode, sdp::video_jxsv::transmission_mode transmode) + { + if (sdp::video_jxsv::codestream == packetmode && sdp::video_jxsv::sequential == transmode) + { + return nmos::packet_transmission_modes::codestream; + } + else if (sdp::video_jxsv::slice == packetmode && sdp::video_jxsv::sequential == transmode) + { + return nmos::packet_transmission_modes::slice_sequential; + } + else if (sdp::video_jxsv::slice == packetmode && sdp::video_jxsv::out_of_order == transmode) + { + return nmos::packet_transmission_modes::slice_out_of_order; + } + throw std::invalid_argument("invalid packetmode/transmode"); + } + + // Construct additional "video/jxsv" parameters from the IS-04 resources + video_jxsv_parameters make_video_jxsv_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender) + { + video_jxsv_parameters params; + + std::tie(params.packetmode, params.transmode) = make_packet_transmission_mode(nmos::packet_transmission_mode{ nmos::fields::packet_transmission_mode(sender) }); + + params.profile = sdp::video_jxsv::profile{ nmos::fields::profile(flow) }; + params.level = sdp::video_jxsv::level{ nmos::fields::level(flow) }; + params.sublevel = sdp::video_jxsv::sublevel{ nmos::fields::sublevel(flow) }; + + // cf. nmos::make_video_raw_parameters + + const auto& components = nmos::fields::components(flow); + params.sampling = details::make_sampling(components); + params.depth = nmos::fields::bit_depth(components.at(0)); + params.width = nmos::fields::frame_width(flow); + params.height = nmos::fields::frame_height(flow); + + // grain_rate is optional in the flow, but if it's not there, for a video flow, it must be in the source + const auto& grain_rate = nmos::fields::grain_rate(flow.has_field(nmos::fields::grain_rate) ? flow : source); + params.exactframerate = nmos::parse_rational(grain_rate); + + const auto& interlace_mode = nmos::fields::interlace_mode(flow); + params.interlace = !interlace_mode.empty() && nmos::interlace_modes::progressive.name != interlace_mode; + params.segmented = !interlace_mode.empty() && nmos::interlace_modes::interlaced_psf.name == interlace_mode; + + // map directly + params.tcs = sdp::transfer_characteristic_system{ nmos::fields::transfer_characteristic(flow) }; + params.colorimetry = sdp::colorimetry{ nmos::fields::colorspace(flow) }; + + // hm, RANGE and PAR not currently indicated in IS-04 so omit these + + // hm, not sure how to decide between ST 2110-22:2019 and ST 2110-22:2022 + params.ssn = sdp::smpte_standard_numbers::ST2110_22_2022; + + // hm, TP is equivalent to the new sender attribute, but for now, support default value + params.tp = sender.has_field(nmos::fields::st2110_21_sender_type) + ? sdp::type_parameter{ nmos::fields::st2110_21_sender_type(sender) } + : sdp::type_parameters::type_N; + + // hm, ST 2110-21 TROFF and CMAX not indicated in IS-04 so omit these + // hm, ST 2110-10 MAXUDP, TSMODE and TSDELAY not indicated in IS-04 so omit these + + // bandwidth + + params.bit_rate = nmos::fields::bit_rate(sender); + + return params; + } + + // Construct SDP parameters for "video/jxsv", with sensible defaults for unspecified fields + sdp_parameters make_video_jxsv_sdp_parameters(const utility::string_t& session_name, const video_jxsv_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids, const std::vector& ts_refclk) + { + // a=rtpmap: /[/] + sdp_parameters::rtpmap_t rtpmap = { payload_type, U("jxsv"), 90000 }; + + // a=fmtp: + // following the order of parameters given in RFC 9134 + // See https://tools.ietf.org/html/rfc4566#section-6 + // and https://tools.ietf.org/html/rfc9134#section-7 + sdp_parameters::fmtp_t fmtp = { + { sdp::video_jxsv::fields::packetmode, utility::ostringstreamed((uint32_t)params.packetmode) } + }; + if (sdp::video_jxsv::sequential != params.transmode) fmtp.push_back({ sdp::video_jxsv::fields::transmode, utility::ostringstreamed((uint32_t)params.transmode) }); + if (!params.profile.empty()) fmtp.push_back({ sdp::video_jxsv::fields::profile, params.profile.name }); + if (!params.level.empty()) fmtp.push_back({ sdp::video_jxsv::fields::level, params.level.name }); + if (!params.sublevel.empty()) fmtp.push_back({ sdp::video_jxsv::fields::sublevel, params.sublevel.name }); + if (0 != params.depth) fmtp.push_back({ sdp::fields::depth, utility::ostringstreamed(params.depth) }); + if (0 != params.width) fmtp.push_back({ sdp::fields::width, utility::ostringstreamed(params.width) }); + if (0 != params.height) fmtp.push_back({ sdp::fields::height, utility::ostringstreamed(params.height) }); + if (0 != params.exactframerate) fmtp.push_back({ sdp::fields::exactframerate, nmos::details::make_exactframerate(params.exactframerate) }); + if (params.interlace) fmtp.push_back({ sdp::fields::interlace, {} }); + if (params.segmented) fmtp.push_back({ sdp::fields::segmented, {} }); + if (!params.sampling.empty()) fmtp.push_back({ sdp::fields::sampling, params.sampling.name }); + if (!params.colorimetry.empty()) fmtp.push_back({ sdp::fields::colorimetry, params.colorimetry.name }); + if (!params.tcs.empty()) fmtp.push_back({ sdp::fields::transfer_characteristic_system, params.tcs.name }); + if (!params.range.empty()) fmtp.push_back({ sdp::fields::range, params.range.name }); + + // additional parameters introduced by SMPTE specs since then... + if (!params.ssn.empty()) fmtp.push_back({ sdp::fields::smpte_standard_number, params.ssn.name }); + if (!params.tp.empty()) fmtp.push_back({ sdp::fields::type_parameter, params.tp.name }); + if (params.troff) fmtp.push_back({ sdp::fields::TROFF, utility::ostringstreamed(*params.troff) }); + if (0 != params.cmax) fmtp.push_back({ sdp::fields::CMAX, utility::ostringstreamed(params.cmax) }); + if (0 != params.maxudp) fmtp.push_back({ sdp::fields::max_udp_packet_size, utility::ostringstreamed(params.maxudp) }); + if (!params.tsmode.empty()) fmtp.push_back({ sdp::fields::timestamp_mode, params.tsmode.name }); + if (params.tsdelay) fmtp.push_back({ sdp::fields::timestamp_delay, utility::ostringstreamed(*params.tsdelay) }); + + return{ session_name, sdp::media_types::video, rtpmap, fmtp, params.bit_rate, {}, {}, {}, media_stream_ids, ts_refclk }; + } + + // Get additional "video/jxsv" parameters from the SDP parameters + template + video_jxsv_parameters get_video_jxsv_parameters(const sdp_parameters& sdp_params, MissingRequiredParameter missing = MissingRequiredParameter{}) + { + video_jxsv_parameters params; + + const auto packetmode = details::find_fmtp(sdp_params.fmtp, sdp::video_jxsv::fields::packetmode); + if (sdp_params.fmtp.end() != packetmode) params.packetmode = (sdp::video_jxsv::packetization_mode)utility::istringstreamed(packetmode->second); + else missing(sdp::video_jxsv::fields::packetmode); + + // optional + const auto transmode = details::find_fmtp(sdp_params.fmtp, sdp::video_jxsv::fields::transmode); + params.transmode = sdp_params.fmtp.end() != transmode + ? (sdp::video_jxsv::transmission_mode)utility::istringstreamed(transmode->second) + : sdp::video_jxsv::sequential; + + // optional + const auto profile = details::find_fmtp(sdp_params.fmtp, sdp::video_jxsv::fields::profile); + if (sdp_params.fmtp.end() != profile) params.profile = sdp::video_jxsv::profile{ profile->second }; + + // optional + const auto level = details::find_fmtp(sdp_params.fmtp, sdp::video_jxsv::fields::level); + if (sdp_params.fmtp.end() != level) params.level = sdp::video_jxsv::level{ level->second }; + + // optional + const auto sublevel = details::find_fmtp(sdp_params.fmtp, sdp::video_jxsv::fields::sublevel); + if (sdp_params.fmtp.end() != sublevel) params.sublevel = sdp::video_jxsv::sublevel{ sublevel->second }; + + // optional + const auto sampling = details::find_fmtp(sdp_params.fmtp, sdp::fields::sampling); + if (sdp_params.fmtp.end() != sampling) params.sampling = sdp::sampling{ sampling->second }; + + // optional + const auto depth = details::find_fmtp(sdp_params.fmtp, sdp::fields::depth); + if (sdp_params.fmtp.end() != depth) params.depth = utility::istringstreamed(depth->second); + + // optional + const auto width = details::find_fmtp(sdp_params.fmtp, sdp::fields::width); + if (sdp_params.fmtp.end() != width) params.width = utility::istringstreamed(width->second); + + // optional + const auto height = details::find_fmtp(sdp_params.fmtp, sdp::fields::height); + if (sdp_params.fmtp.end() != height) params.height = utility::istringstreamed(height->second); + + // optional + const auto exactframerate = details::find_fmtp(sdp_params.fmtp, sdp::fields::exactframerate); + if (sdp_params.fmtp.end() != exactframerate) params.exactframerate = nmos::details::parse_exactframerate(exactframerate->second); + + // optional + const auto interlace = details::find_fmtp(sdp_params.fmtp, sdp::fields::interlace); + params.interlace = sdp_params.fmtp.end() != interlace; + + // optional + const auto segmented = details::find_fmtp(sdp_params.fmtp, sdp::fields::segmented); + params.segmented = sdp_params.fmtp.end() != segmented; + + // optional + const auto tcs = details::find_fmtp(sdp_params.fmtp, sdp::fields::transfer_characteristic_system); + if (sdp_params.fmtp.end() != tcs) params.tcs = sdp::transfer_characteristic_system{ tcs->second }; + + // optional + const auto colorimetry = details::find_fmtp(sdp_params.fmtp, sdp::fields::colorimetry); + if (sdp_params.fmtp.end() != colorimetry) params.colorimetry = sdp::colorimetry{ colorimetry->second }; + + // optional + const auto range = details::find_fmtp(sdp_params.fmtp, sdp::fields::range); + if (sdp_params.fmtp.end() != range) params.range = sdp::range{ range->second }; + + // optional + const auto ssn = details::find_fmtp(sdp_params.fmtp, sdp::fields::smpte_standard_number); + if (sdp_params.fmtp.end() != ssn) params.ssn = sdp::smpte_standard_number{ ssn->second }; + + // optional + const auto tp = details::find_fmtp(sdp_params.fmtp, sdp::fields::type_parameter); + if (sdp_params.fmtp.end() != tp) params.tp = sdp::type_parameter{ tp->second }; + + // optional + const auto troff = details::find_fmtp(sdp_params.fmtp, sdp::fields::TROFF); + if (sdp_params.fmtp.end() != troff) params.troff = utility::istringstreamed(troff->second); + + // optional + const auto cmax = details::find_fmtp(sdp_params.fmtp, sdp::fields::CMAX); + if (sdp_params.fmtp.end() != cmax) params.cmax = utility::istringstreamed(cmax->second); + + // optional + const auto maxudp = details::find_fmtp(sdp_params.fmtp, sdp::fields::max_udp_packet_size); + if (sdp_params.fmtp.end() != maxudp) params.maxudp = utility::istringstreamed(maxudp->second); + + // optional + const auto tsmode = details::find_fmtp(sdp_params.fmtp, sdp::fields::timestamp_mode); + if (sdp_params.fmtp.end() != tsmode) params.tsmode = sdp::timestamp_mode{ tsmode->second }; + + // optional + const auto tsdelay = details::find_fmtp(sdp_params.fmtp, sdp::fields::timestamp_delay); + if (sdp_params.fmtp.end() != tsdelay) params.tsdelay = utility::istringstreamed(tsdelay->second); + + // optional + if (sdp::bandwidth_types::application_specific == sdp_params.bandwidth.bandwidth_type) params.bit_rate = sdp_params.bandwidth.bandwidth; + + return params; + } + + // Get additional "video/jxsv" parameters from the SDP parameters + video_jxsv_parameters get_video_jxsv_parameters(const sdp_parameters& sdp_params) + { + return get_video_jxsv_parameters(sdp_params); + } + + // Get additional "video/jxsv" parameters from the SDP parameters + video_jxsv_parameters get_video_jxsv_parameters_or_defaults(const sdp_parameters& sdp_params) + { + return get_video_jxsv_parameters<>(sdp_params, [](const utility::string_t&) {}); + } + + // Calculate the format bit rate (kilobits/second) from the specified frame rate, dimensions and bits per pixel + uint64_t get_video_jxsv_bit_rate(const nmos::rational& grain_rate, uint32_t frame_width, uint32_t frame_height, double bits_per_pixel) + { + const auto sampling_points = (int64_t)frame_width * (int64_t)frame_height; + const auto pixels_per_second = sampling_points * grain_rate; + const auto bit_rate = boost::rational_cast(pixels_per_second) * bits_per_pixel; + return uint64_t(bit_rate / 1e3 + 0.5); + } + + namespace details + { + const video_jxsv_parameters* get_jxsv(const format_parameters* format) { return get(format); } + + // NMOS Parameter Registers - Capabilities register + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/ +#define CAPS_ARGS const sdp_parameters& sdp, const format_parameters& format, const web::json::value& con + static const std::map> jxsv_constraints + { + { nmos::caps::format::media_type, [](CAPS_ARGS) { return nmos::match_string_constraint(get_media_type(sdp).name, con); } }, + { nmos::caps::format::grain_rate, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (nmos::rational{} == jxsv->exactframerate || nmos::match_rational_constraint(jxsv->exactframerate, con)); } }, + { nmos::caps::format::profile, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (jxsv->profile.empty() || nmos::match_string_constraint(jxsv->profile.name, con)); } }, + { nmos::caps::format::level, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (jxsv->level.empty() || nmos::match_string_constraint(jxsv->level.name, con)); } }, + { nmos::caps::format::sublevel, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (jxsv->sublevel.empty() || nmos::match_string_constraint(jxsv->sublevel.name, con)); } }, + { nmos::caps::format::frame_height, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (0 == jxsv->height || nmos::match_integer_constraint(jxsv->height, con)); } }, + { nmos::caps::format::frame_width, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (0 == jxsv->width || nmos::match_integer_constraint(jxsv->width, con)); } }, + { nmos::caps::format::color_sampling, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (jxsv->sampling.empty() || nmos::match_string_constraint(jxsv->sampling.name, con)); } }, + { nmos::caps::format::interlace_mode, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && nmos::details::match_interlace_mode_constraint(jxsv->interlace, jxsv->segmented, con); } }, + { nmos::caps::format::colorspace, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (jxsv->colorimetry.empty() || nmos::match_string_constraint(jxsv->colorimetry.name, con)); } }, + { nmos::caps::format::transfer_characteristic, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (jxsv->tcs.empty() || nmos::match_string_constraint(jxsv->tcs.name, con)); } }, + { nmos::caps::format::component_depth, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (0 == jxsv->depth || nmos::match_integer_constraint(jxsv->depth, con)); } }, + { nmos::caps::transport::packet_transmission_mode, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && nmos::match_string_constraint(nmos::parse_packet_transmission_mode(jxsv->packetmode, jxsv->transmode).name, con); } }, + { nmos::caps::transport::st2110_21_sender_type, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return nmos::match_string_constraint(jxsv->tp.name, con); } }, + { nmos::caps::transport::bit_rate, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (0 == jxsv->bit_rate || nmos::match_integer_constraint(jxsv->bit_rate, con)); } } + }; +#undef CAPS_ARGS + } + + // Validate SDP parameters for "video/jxsv" against IS-04 receiver capabilities + // cf. nmos::validate_sdp_parameters + void validate_video_jxsv_sdp_parameters(const web::json::value& receiver, const nmos::sdp_parameters& sdp_params) + { + // this function can only be used to validate SDP data for "video/jxsv"; logic error otherwise + const auto media_type = get_media_type(sdp_params); + if (nmos::media_types::video_jxsv != media_type) throw std::invalid_argument("unexpected media type/encoding name"); + + nmos::details::validate_sdp_parameters(details::jxsv_constraints, sdp_params, nmos::formats::video, get_video_jxsv_parameters(sdp_params), receiver); + } + + // See https://specs.amwa.tv/bcp-006-01/branches/v1.0-dev/docs/NMOS_With_JPEG_XS.html#flows + // cf. nmos::make_coded_video_flow + nmos::resource make_video_jxsv_flow( + const nmos::id& id, + const nmos::id& source_id, + const nmos::id& device_id, + const nmos::rational& grain_rate, + unsigned int frame_width, + unsigned int frame_height, + const nmos::interlace_mode& interlace_mode, + const nmos::colorspace& colorspace, + const nmos::transfer_characteristic& transfer_characteristic, + const sdp::sampling& color_sampling, + unsigned int bit_depth, + const nmos::profile& profile, + const nmos::level& level, + const nmos::sublevel& sublevel, + double bits_per_pixel, + const nmos::settings& settings) + { + using web::json::value; + + auto resource = nmos::make_coded_video_flow( + id, source_id, device_id, + grain_rate, + frame_width, frame_height, interlace_mode, + colorspace, transfer_characteristic, color_sampling, bit_depth, + nmos::media_types::video_jxsv, + settings + ); + auto& data = resource.data; + + // additional attributes required by BCP-006-01 + // see https://specs.amwa.tv/bcp-006-01/branches/v1.0-dev/docs/NMOS_With_JPEG_XS.html#flows + if (!profile.empty()) data[nmos::fields::profile] = value(profile.name); + if (!level.empty()) data[nmos::fields::level] = value(level.name); + if (!sublevel.empty()) data[nmos::fields::sublevel] = value(sublevel.name); + const auto bit_rate = nmos::get_video_jxsv_bit_rate(grain_rate, frame_width, frame_height, bits_per_pixel); + if (0 != bit_rate) data[nmos::fields::bit_rate] = value(bit_rate); + + return resource; + } +} diff --git a/Development/nmos/video_jxsv.h b/Development/nmos/video_jxsv.h new file mode 100644 index 000000000..947b8a49d --- /dev/null +++ b/Development/nmos/video_jxsv.h @@ -0,0 +1,391 @@ +#ifndef NMOS_VIDEO_JXSV_H +#define NMOS_VIDEO_JXSV_H + +#include "nmos/media_type.h" +#include "nmos/node_resources.h" +#include "nmos/sdp_utils.h" + +namespace sdp +{ + namespace video_jxsv + { + namespace fields + { + // See https://www.iana.org/assignments/media-types/video/jxsv + // and https://tools.ietf.org/html/rfc9134#section-7 + + // pacKetization mode (K) bit + const web::json::field packetmode{ U("packetmode") }; // cf. sdp::video_jxsv::packetization_mode + // Transmission mode (T) bit + const web::json::field_with_default transmode{ U("transmode"), 1 }; // sequential, cf. sdp::video_jxsv::transmission_mode + + const web::json::field_as_string profile{ U("profile") }; // cf. sdp::video_jxsv::profile + const web::json::field_as_string level{ U("level") }; // cf. sdp::video_jxsv::level + const web::json::field_as_string sublevel{ U("sublevel") }; // cf. sdp::video_jxsv::sublevel + } + + // pacKetization mode (K) bit + // See https://tools.ietf.org/html/rfc9134 + enum packetization_mode + { + codestream = 0, + slice = 1 + }; + + // Transmission mode (T) bit + // See https://tools.ietf.org/html/rfc9134 + enum transmission_mode + { + out_of_order = 0, + sequential = 1 + }; + + // JPEG XS Profile + // "The JPEG XS profile [ISO21122-2] in use. Any white space Unicode character in the profile name SHALL be omitted." + // See https://tools.ietf.org/html/rfc9134 + DEFINE_STRING_ENUM(profile) + namespace profiles + { + const profile HighBayer{ U("HighBayer") }; + const profile MainBayer{ U("MainBayer") }; + const profile LightBayer{ U("LightBayer") }; + const profile High4444_12{ U("High4444.12") }; + const profile Main4444_12{ U("Main4444.12") }; + const profile High444_12{ U("High444.12") }; + const profile Main444_12{ U("Main444.12") }; + const profile Light444_12{ U("Light444.12") }; + const profile Main422_10{ U("Main422.10") }; + const profile Light422_10{ U("Light422.10") }; + const profile Light_Subline422_10{ U("Light-Subline422.10") }; + const profile MLS_12{ U("MLS.12") }; + const profile High420_12{ U("High420.12") }; + const profile Main420_12{ U("Main420.12") }; + } + + // JPEG XS Level + // "The JPEG XS level [ISO21122-2] in use. Any white space Unicode character in the level name SHALL be omitted." + // See https://tools.ietf.org/html/rfc9134 + DEFINE_STRING_ENUM(level) + namespace levels + { + const level Level1k_1{ U("1k-1") }; + const level Bayer2k_1{ U("Bayer2k-1") }; + const level Level2k_1{ U("2k-1") }; + const level Bayer4k_1{ U("Bayer4k-1") }; + const level Level4k_1{ U("4k-1") }; + const level Bayer8k_1{ U("Bayer8k-1") }; + const level Level4k_2{ U("4k-2") }; + const level Bayer8k_2{ U("Bayer8k-2") }; + const level Level4k_3{ U("4k-3") }; + const level Bayer8k_3{ U("Bayer8k-3") }; + const level Level8k_1{ U("8k-1") }; + const level Bayer16k_1{ U("Bayer16k-1") }; + const level Level8k_2{ U("8k-2") }; + const level Bayer16k_2{ U("Bayer16k-2") }; + const level Level8k_3{ U("8k-3") }; + const level Bayer16k_3{ U("Bayer16k-3") }; + const level Level10k_1{ U("10k-1") }; + const level Bayer20k_1{ U("Bayer20k-1") }; + } + + // Calculate the lowest possible JPEG XS level from the specified frame rate and dimensions + level get_level(const nmos::rational& frame_rate, uint32_t frame_width, uint32_t frame_height); + + // JPEG XS Sublevel + // "The JPEG XS sublevel [ISO21122-2] in use. Any white space Unicode character in the sublevel name SHALL be omitted." + // See https://tools.ietf.org/html/rfc9134 + DEFINE_STRING_ENUM(sublevel) + namespace sublevels + { + const sublevel Full{ U("Full") }; + const sublevel Sublev12bpp{ U("Sublev12bpp") }; + const sublevel Sublev9bpp{ U("Sublev9bpp") }; + const sublevel Sublev6bpp{ U("Sublev6bpp") }; + const sublevel Sublev4bpp{ U("Sublev4bpp") }; + const sublevel Sublev3bpp{ U("Sublev3bpp") }; + const sublevel Sublev2bpp{ U("Sublev2bpp") }; + } + } +} + +namespace nmos +{ + namespace media_types + { + // JPEG XS + // See https://www.iana.org/assignments/media-types/video/jxsv + // and https://tools.ietf.org/html/rfc9134 + const media_type video_jxsv{ U("video/jxsv") }; + } + + namespace fields + { + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#bit-rate + // and https://specs.amwa.tv/nmos-parameter-registers/branches/main/sender-attributes/#bit-rate + const web::json::field_as_integer bit_rate{ U("bit_rate") }; + + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#profile + const web::json::field_as_string profile{ U("profile") }; + + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#level + const web::json::field_as_string level{ U("level") }; + + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#sublevel + const web::json::field_as_string sublevel{ U("sublevel") }; + + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/sender-attributes/#packet-transmission-mode + const web::json::field_as_string_or packet_transmission_mode{ U("packet_transmission_mode"), U("codestream") }; + } + + namespace caps + { + namespace format + { + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/#format-bit-rate + const web::json::field_as_value_or bit_rate{ U("urn:x-nmos:cap:format:bit_rate"), {} }; // number + + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/#profile + const web::json::field_as_value_or profile{ U("urn:x-nmos:cap:format:profile"), {} }; // string + + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/#level + const web::json::field_as_value_or level{ U("urn:x-nmos:cap:format:level"), {} }; // string + + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/#sublevel + const web::json::field_as_value_or sublevel{ U("urn:x-nmos:cap:format:sublevel"), {} }; // string + } + + namespace transport + { + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/#transport-bit-rate + const web::json::field_as_value_or bit_rate{ U("urn:x-nmos:cap:transport:bit_rate"), {} }; // number + + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/capabilities/#packet-transmission-mode + const web::json::field_as_value_or packet_transmission_mode{ U("urn:x-nmos:cap:transport:packet_transmission_mode"), {} }; // string + } + } + + // Profile + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#profile + DEFINE_STRING_ENUM(profile) + namespace profiles + { + // JPEG XS Profile + // "The JPEG XS profile [ISO21122-2] in use. Any white space Unicode character in the profile name SHALL be omitted." + // See https://tools.ietf.org/html/rfc9134 + // and https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/flow_video_jxsv_register.html + const profile HighBayer{ U("HighBayer") }; + const profile MainBayer{ U("MainBayer") }; + const profile LightBayer{ U("LightBayer") }; + const profile High4444_12{ U("High4444.12") }; + const profile Main4444_12{ U("Main4444.12") }; + const profile High444_12{ U("High444.12") }; + const profile Main444_12{ U("Main444.12") }; + const profile Light444_12{ U("Light444.12") }; + const profile Main422_10{ U("Main422.10") }; + const profile Light422_10{ U("Light422.10") }; + const profile Light_Subline422_10{ U("Light-Subline422.10") }; + const profile MLS_12{ U("MLS.12") }; + const profile High420_12{ U("High420.12") }; + const profile Main420_12{ U("Main420.12") }; + } + + // Level + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#level + DEFINE_STRING_ENUM(level) + namespace levels + { + // JPEG XS Level + // "The JPEG XS level [ISO21122-2] in use. Any white space Unicode character in the level name SHALL be omitted." + // See https://tools.ietf.org/html/rfc9134 + // and https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/flow_video_jxsv_register.html + const level Level1k_1{ U("1k-1") }; + const level Bayer2k_1{ U("Bayer2k-1") }; + const level Level2k_1{ U("2k-1") }; + const level Bayer4k_1{ U("Bayer4k-1") }; + const level Level4k_1{ U("4k-1") }; + const level Bayer8k_1{ U("Bayer8k-1") }; + const level Level4k_2{ U("4k-2") }; + const level Bayer8k_2{ U("Bayer8k-2") }; + const level Level4k_3{ U("4k-3") }; + const level Bayer8k_3{ U("Bayer8k-3") }; + const level Level8k_1{ U("8k-1") }; + const level Bayer16k_1{ U("Bayer16k-1") }; + const level Level8k_2{ U("8k-2") }; + const level Bayer16k_2{ U("Bayer16k-2") }; + const level Level8k_3{ U("8k-3") }; + const level Bayer16k_3{ U("Bayer16k-3") }; + const level Level10k_1{ U("10k-1") }; + const level Bayer20k_1{ U("Bayer20k-1") }; + } + + // Calculate the lowest possible JPEG XS level from the specified frame rate and dimensions + inline nmos::level get_video_jxsv_level(const nmos::rational& grain_rate, uint32_t frame_width, uint32_t frame_height) + { + return nmos::level{ sdp::video_jxsv::get_level(grain_rate, frame_width, frame_height).name }; + } + + // Sublevel + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/#sublevel + DEFINE_STRING_ENUM(sublevel) + namespace sublevels + { + // JPEG XS Sublevel + // "The JPEG XS sublevel [ISO21122-2] in use. Any white space Unicode character in the sublevel name SHALL be omitted." + // See https://tools.ietf.org/html/rfc9134 + // and https://specs.amwa.tv/nmos-parameter-registers/branches/main/flow-attributes/flow_video_jxsv_register.html + const sublevel Full{ U("Full") }; + const sublevel Sublev12bpp{ U("Sublev12bpp") }; + const sublevel Sublev9bpp{ U("Sublev9bpp") }; + const sublevel Sublev6bpp{ U("Sublev6bpp") }; + const sublevel Sublev4bpp{ U("Sublev4bpp") }; + const sublevel Sublev3bpp{ U("Sublev3bpp") }; + const sublevel Sublev2bpp{ U("Sublev2bpp") }; + } + + // Packet Transmission Mode + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/sender-attributes/#packet-transmission-mode + DEFINE_STRING_ENUM(packet_transmission_mode) + namespace packet_transmission_modes + { + // JPEG XS packetization and transmission mode + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/sender-attributes/#packet-transmission-mode + // and https://specs.amwa.tv/nmos-parameter-registers/branches/main/sender-attributes/sender_register.html + const packet_transmission_mode codestream{ U("codestream") }; + const packet_transmission_mode slice_sequential{ U("slice_sequential") }; + const packet_transmission_mode slice_out_of_order{ U("slice_out_of_order") }; + } + + std::pair make_packet_transmission_mode(const nmos::packet_transmission_mode& mode); + nmos::packet_transmission_mode parse_packet_transmission_mode(sdp::video_jxsv::packetization_mode packetmode, sdp::video_jxsv::transmission_mode transmode); + + // Additional "video/jxsv" parameters + // See https://www.iana.org/assignments/media-types/video/jxsv + // and https://tools.ietf.org/html/rfc9134 + struct video_jxsv_parameters + { + // fmtp indicates format + sdp::video_jxsv::packetization_mode packetmode; + sdp::video_jxsv::transmission_mode transmode; + sdp::video_jxsv::profile profile; // nmos::profile has compatible values + sdp::video_jxsv::level level; // nmos::level has compatible values + sdp::video_jxsv::sublevel sublevel; // nmos::sublevel has compatible values + sdp::sampling sampling; + uint32_t depth; + uint32_t width; + uint32_t height; + nmos::rational exactframerate; + bool interlace; + bool segmented; + sdp::transfer_characteristic_system tcs; // nmos::transfer_characteristic is compatible + sdp::colorimetry colorimetry; // nmos::colorspace is compatible + sdp::range range; // if omitted (empty), assume sdp::ranges::NARROW + sdp::smpte_standard_number ssn; + + // additional fmtp parameters from ST 2110-21:2022 + sdp::type_parameter tp; + bst::optional troff; // if omitted, assume default + uint32_t cmax; // if omitted (zero), assume max defined for tp + + // additional fmtp parameters from ST 2110-10:2022 + uint32_t maxudp; // if omitted (zero), assume the Standard UP Size Limit + sdp::timestamp_mode tsmode; // if omitted (empty), assume sdp::timestamp_modes::NEW + bst::optional tsdelay; + + // bandwidth + uint64_t bit_rate; // transport bit rate + + video_jxsv_parameters() : depth(), width(), height(), interlace(), segmented(), troff(), cmax(), maxudp(), tsdelay() {} + + video_jxsv_parameters( + sdp::video_jxsv::packetization_mode packetmode, + sdp::video_jxsv::transmission_mode transmode, + sdp::video_jxsv::profile profile, + sdp::video_jxsv::level level, + sdp::video_jxsv::sublevel sublevel, + sdp::sampling sampling, + uint32_t depth, + uint32_t width, + uint32_t height, + nmos::rational exactframerate, + bool interlace, + bool segmented, + sdp::transfer_characteristic_system tcs, + sdp::colorimetry colorimetry, + sdp::range range, + sdp::smpte_standard_number ssn, + sdp::type_parameter tp, + bst::optional troff, + uint32_t cmax, + uint32_t maxudp, + sdp::timestamp_mode tsmode, + bst::optional tsdelay, + uint64_t bit_rate + ) + : packetmode(packetmode) + , transmode(transmode) + , profile(std::move(profile)) + , level(std::move(level)) + , sublevel(std::move(sublevel)) + , sampling(std::move(sampling)) + , depth(depth) + , width(width) + , height(height) + , exactframerate(exactframerate) + , interlace(interlace) + , segmented(segmented) + , tcs(std::move(tcs)) + , colorimetry(std::move(colorimetry)) + , range(std::move(range)) + , ssn(std::move(ssn)) + , tp(std::move(tp)) + , troff(troff) + , cmax(cmax) + , maxudp(maxudp) + , tsmode(std::move(tsmode)) + , tsdelay(tsdelay) + , bit_rate(bit_rate) + {} + }; + + // Construct additional "video/jxsv" parameters from the IS-04 resources + video_jxsv_parameters make_video_jxsv_parameters(const web::json::value& node, const web::json::value& source, const web::json::value& flow, const web::json::value& sender); + // Construct SDP parameters for "video/jxsv" + sdp_parameters make_video_jxsv_sdp_parameters(const utility::string_t& session_name, const video_jxsv_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}); + // Get additional "video/jxsv" parameters from the SDP parameters + video_jxsv_parameters get_video_jxsv_parameters(const sdp_parameters& sdp_params); + video_jxsv_parameters get_video_jxsv_parameters_or_defaults(const sdp_parameters& sdp_params); + + // Construct SDP parameters for "video/jxsv" + inline sdp_parameters make_sdp_parameters(const utility::string_t& session_name, const video_jxsv_parameters& params, uint64_t payload_type, const std::vector& media_stream_ids = {}, const std::vector& ts_refclk = {}) + { + return make_video_jxsv_sdp_parameters(session_name, params, payload_type, media_stream_ids, ts_refclk); + } + + // Validate SDP parameters for "video/jxsv" against IS-04 receiver capabilities + void validate_video_jxsv_sdp_parameters(const web::json::value& receiver, const nmos::sdp_parameters& sdp_params); + + // Calculate the format bit rate (kilobits/second) from the specified frame rate, dimensions and bits per pixel + uint64_t get_video_jxsv_bit_rate(const nmos::rational& grain_rate, uint32_t frame_width, uint32_t frame_height, double bits_per_pixel); + + // See https://specs.amwa.tv/bcp-006-01/branches/v1.0-dev/docs/NMOS_With_JPEG_XS.html#flows + // cf. nmos::make_coded_video_flow + nmos::resource make_video_jxsv_flow( + const nmos::id& id, + const nmos::id& source_id, + const nmos::id& device_id, + const nmos::rational& grain_rate, + unsigned int frame_width, + unsigned int frame_height, + const nmos::interlace_mode& interlace_mode, + const nmos::colorspace& colorspace, + const nmos::transfer_characteristic& transfer_characteristic, + const sdp::sampling& color_sampling, + unsigned int bit_depth, + const nmos::profile& profile, + const nmos::level& level, + const nmos::sublevel& sublevel, + double bits_per_pixel, + const nmos::settings& settings); +} + +#endif diff --git a/Development/nmos/vpid_code.h b/Development/nmos/vpid_code.h index eb3519088..96dec8ad7 100644 --- a/Development/nmos/vpid_code.h +++ b/Development/nmos/vpid_code.h @@ -13,14 +13,16 @@ namespace nmos // See https://smpte-ra.org/video-payload-id-codes-serial-digital-interfaces namespace vpid_codes { - // 483/576-line interlaced payloads on 270 Mb/s and 360 Mb/s serial digital interfaces + // 483/576-line interlaced video payloads on 270 Mb/s and 360 Mb/s serial digital interfaces const vpid_code vpid_270Mbps = 129; - // 483/576-line extended payloads on 360 Mb/s single-link and 270 Mb/s dual-link serial digital interfaces + // 483/576-line extended video payloads on 360 Mb/s single-link and 270 Mb/s dual-link serial digital interfaces const vpid_code vpid_360Mbps = 130; - // 483/576-line payloads on a 540 Mb/s serial digital interface + // 483/576-line video payloads on a 540 Mb/s serial digital interface const vpid_code vpid_540Mbps = 131; - // 483/576-line payloads on a 1.485 Gb/s (nominal) serial digital interface - const vpid_code vpid_1_5Gbps = 132; + // 720-line video payloads on a 1.5 Gb/s (nominal) serial digital interface + const vpid_code vpid_1_5Gbps_720_line = 132; + // 1080-line video payloads on a 1.5 Gb/s (nominal) serial digital interface + const vpid_code vpid_1_5Gbps_1080_line = 133; // extensible enum } diff --git a/Development/nmos/ws_api_utils.cpp b/Development/nmos/ws_api_utils.cpp new file mode 100644 index 000000000..363e8e377 --- /dev/null +++ b/Development/nmos/ws_api_utils.cpp @@ -0,0 +1,60 @@ +#include "nmos/ws_api_utils.h" + +#include "cpprest/http_utils.h" +#include "nmos/api_utils.h" +#include "nmos/authorization.h" +#include "nmos/authorization_state.h" +#include "nmos/model.h" +#include "nmos/slog.h" + +namespace nmos +{ + namespace experimental + { + // callbacks from this function are called with the model locked, and may read or write directly to the model + ws_validate_authorization_handler make_ws_validate_authorization_handler(nmos::base_model& model, nmos::experimental::authorization_state& authorization_state, validate_authorization_token_handler access_token_validation, slog::base_gate& gate) + { + return [&model, &authorization_state, access_token_validation, &gate](web::http::http_request& request, const nmos::experimental::scope& scope) + { + if (web::http::methods::OPTIONS != request.method() && nmos::experimental::fields::server_authorization(model.settings)) + { + const auto& settings = model.settings; + + web::uri token_issuer; + // note: the ws_validate_authorization returns the token_issuer via function parameter + const auto result = ws_validate_authorization(request, scope, nmos::get_host_name(settings), token_issuer, access_token_validation, gate); + if (!result) + { + // set error repsonse + auto realm = web::http::get_host_port(request).first; + if (realm.empty()) { realm = nmos::get_host(settings); } + web::http::http_response res; + const auto retry_after = nmos::experimental::fields::service_unavailable_retry_after(settings); + nmos::experimental::details::set_error_reply(res, realm, retry_after, result); + request.reply(res); + + // if no matching public keys caused the error, trigger a re-fetch to obtain public keys from the token issuer (authorization_state.token_issuer) + if (result.value == authorization_error::no_matching_keys) + { + slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << request.request_uri().path() << ": " << result.message; + + with_write_lock(authorization_state.mutex, [&authorization_state, token_issuer] + { + authorization_state.fetch_token_issuer_pubkeys = true; + authorization_state.token_issuer = token_issuer; + }); + + model.notify(); + } + else + { + slog::log(gate, SLOG_FLF) << "Invalid websocket connection to: " << request.request_uri().path() << ": " << result.message; + } + return false; + } + } + return true; + }; + } + } +} diff --git a/Development/nmos/ws_api_utils.h b/Development/nmos/ws_api_utils.h new file mode 100644 index 000000000..fdf341a02 --- /dev/null +++ b/Development/nmos/ws_api_utils.h @@ -0,0 +1,26 @@ +#ifndef NMOS_WS_API_UTILS_H +#define NMOS_WS_API_UTILS_H + +#include +#include "cpprest/http_msg.h" +#include "nmos/authorization_handlers.h" + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + struct base_model; + + namespace experimental + { + struct authorization_state; + + typedef std::function ws_validate_authorization_handler; + ws_validate_authorization_handler make_ws_validate_authorization_handler(nmos::base_model& model, authorization_state& authorization_state, validate_authorization_token_handler access_token_validation, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/pplx/pplx_utils.h b/Development/pplx/pplx_utils.h index d7987e67a..fd20aa2f5 100644 --- a/Development/pplx/pplx_utils.h +++ b/Development/pplx/pplx_utils.h @@ -2,6 +2,7 @@ #define PPLX_PPLX_UTILS_H #include +#include #include "pplx/pplxtasks.h" #if (defined(_MSC_VER) && (_MSC_VER >= 1800)) && !CPPREST_FORCE_PPLX @@ -10,32 +11,16 @@ namespace Concurrency // since namespace pplx = Concurrency namespace pplx #endif { - /// - /// Creates a task that completes after a specified amount of time. - /// - /// - /// The number of milliseconds after which the task should complete. - /// - /// - /// Cancellation token for cancellation of this operation. - /// - /// - /// Because the scheduler is cooperative in nature, the delay before the task completes could be longer than the specified amount of time. - /// + // Creates a task that completes after a specified amount of time. + // milliseconds: The number of milliseconds after which the task should complete. + // token: Cancellation token for cancellation of this operation. + // Because the scheduler is cooperative in nature, the delay before the task completes could be longer than the specified amount of time. pplx::task complete_after(unsigned int milliseconds, const pplx::cancellation_token& token = pplx::cancellation_token::none()); - /// - /// Creates a task that completes after a specified amount of time. - /// - /// - /// The amount of time (milliseconds and up) after which the task should complete. - /// - /// - /// Cancellation token for cancellation of this operation. - /// - /// - /// Because the scheduler is cooperative in nature, the delay before the task completes could be longer than the specified amount of time. - /// + // Creates a task that completes after a specified amount of time. + // duration: The amount of time (milliseconds and up) after which the task should complete. + // token: Cancellation token for cancellation of this operation. + // Because the scheduler is cooperative in nature, the delay before the task completes could be longer than the specified amount of time. template inline pplx::task complete_after(const std::chrono::duration& duration, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { @@ -44,71 +29,176 @@ namespace pplx : pplx::task_from_result(); } - /// - /// Creates a task that completes at a specified time. - /// - /// - /// The time point at which the task should complete. - /// - /// - /// Cancellation token for cancellation of this operation. - /// - /// - /// Because the scheduler is cooperative in nature, the time at which the task completes could be after the specified time. - /// + // Creates a task that completes at a specified time. + // time: The time point at which the task should complete. + // token: Cancellation token for cancellation of this operation. + // Because the scheduler is cooperative in nature, the time at which the task completes could be after the specified time. template inline pplx::task complete_at(const std::chrono::time_point& time, const pplx::cancellation_token& token = pplx::cancellation_token::none()) { return complete_after(time - Clock::now(), token); } - /// - /// Creates a task for an asynchronous do-while loop. Executes a task repeatedly, until the returned condition value becomes false. - /// - /// - /// This function should create a task that performs the loop iteration and returns the Boolean value of the loop condition. - /// - /// - /// Cancellation token for cancellation of the do-while loop. - /// + // Creates a task for an asynchronous do-while loop. Executes a task repeatedly, until the returned condition value becomes false. + // create_iteration_task: This function should create a task that performs the loop iteration and returns the Boolean value of the loop condition. + // token: Cancellation token for cancellation of the do-while loop. pplx::task do_while(const std::function()>& create_iteration_task, const pplx::cancellation_token& token = pplx::cancellation_token::none()); - /// - /// Returns true if the task is default constructed. - /// - /// - /// A default constructed task cannot be used until you assign a valid task to it. Methods such as get, wait or then - /// will throw an invalid_argument exception when called on a default constructed task. - /// + // Returns true if the task is default constructed. + // A default constructed task cannot be used until you assign a valid task to it. Methods such as get, wait or then + // will throw an invalid_argument exception when called on a default constructed task. template bool empty(const pplx::task& task) { return pplx::task() == task; } - /// - /// Silently 'observe' any exception thrown from a task. - /// - /// - /// Exceptions that are unobserved when a task is destructed will terminate the process. - /// Add this as a continuation to silently swallow all exceptions. - /// - template - struct observe_exception + namespace details { - void operator()(pplx::task finally) const + template + void wait_nothrow(const pplx::task& task) { try { - finally.wait(); + task.wait(); } catch (...) {} } + } + + struct exception_observer + { + template + void operator()(pplx::task finally) const + { + details::wait_nothrow(finally); + } + }; + + // Silently 'observe' any exception thrown from a task. + // Exceptions that are unobserved when a task is destructed will terminate the process. + // Add this as a continuation to silently swallow all exceptions. + inline exception_observer observe_exception() + { + return exception_observer(); + } + + namespace details + { + // see http://ericniebler.com/2013/08/07/universal-references-and-the-copy-constructo/ + template + using disable_if_same_or_derived = + typename std::enable_if< + !std::is_base_of::type + >::value + >::type; + } + + template + struct exceptions_observer + { + template > + explicit exceptions_observer(InputRange&& tasks) : tasks(tasks.begin(), tasks.end()) {} + + template + exceptions_observer(InputIterator&& first, InputIterator&& last) : tasks(std::forward(first), std::forward(last)) {} + + template + void operator()(pplx::task finally) const + { + for (auto& task : tasks) details::wait_nothrow(task); + details::wait_nothrow(finally); + } + + private: + std::vector> tasks; }; - /// - /// RAII helper for classes that have asynchronous open/close member functions. - /// + // Silently 'observe' all exceptions thrown from a range of tasks. + // Exceptions that are unobserved when a task is destructed will terminate the process. + // Add this as a continuation to silently swallow all exceptions. + template ().begin())>::value_type::result_type> + inline exceptions_observer observe_exceptions(InputRange&& tasks) + { + return exceptions_observer(std::forward(tasks)); + } + + // Silently 'observe' all exceptions thrown from a range of tasks. + // Exceptions that are unobserved when a task is destructed will terminate the process. + // Add this as a continuation to silently swallow all exceptions. + template ::value_type::result_type> + inline exceptions_observer observe_exceptions(InputIterator&& first, InputIterator&& last) + { + return exceptions_observer(std::forward(first), std::forward(last)); + } + + namespace details + { + template + struct workaround_default_task + { + pplx::task operator()(pplx::task task) const + { + if (!pplx::empty(task)) return task; + // convert default constructed tasks into ones that pplx::when_{all,any} can handle + // see https://github.com/microsoft/cpprestsdk/issues/1701 + try + { + task.wait(); + // unreachable code + return task; + } + catch (const pplx::invalid_operation& e) + { + auto workaround = pplx::task_from_exception(e); + details::wait_nothrow(workaround); + return workaround; + } + } + }; + } + + namespace ranges + { + namespace details + { + template + auto when_all(InputRange&& tasks, const pplx::task_options& options = pplx::task_options()) + -> decltype(pplx::when_all(tasks.begin(), tasks.end(), options)) + { + return pplx::when_all(tasks.begin(), tasks.end(), options); + } + } + + template + auto when_all(InputRange&& tasks, const pplx::task_options& options = pplx::task_options()) + -> decltype(pplx::when_all(tasks.begin(), tasks.end(), options)) + { + using ReturnType = typename std::iterator_traits::value_type::result_type; + return pplx::ranges::details::when_all(tasks | boost::adaptors::transformed(pplx::details::workaround_default_task())); + } + + namespace details + { + template + auto when_any(InputRange&& tasks, const pplx::task_options& options = pplx::task_options()) + -> decltype(pplx::when_any(tasks.begin(), tasks.end(), options)) + { + return pplx::when_any(tasks.begin(), tasks.end(), options); + } + } + + template + auto when_any(InputRange&& tasks, const pplx::task_options& options = pplx::task_options()) + -> decltype(pplx::when_any(tasks.begin(), tasks.end(), options)) + { + using ReturnType = typename std::iterator_traits::value_type::result_type; + return pplx::ranges::details::when_any(tasks | boost::adaptors::transformed(pplx::details::workaround_default_task())); + } + } + + // RAII helper for classes that have asynchronous open/close member functions. template struct open_close_guard { diff --git a/Development/pplx/test/pplx_utils_test.cpp b/Development/pplx/test/pplx_utils_test.cpp new file mode 100644 index 000000000..a20a02162 --- /dev/null +++ b/Development/pplx/test/pplx_utils_test.cpp @@ -0,0 +1,109 @@ +// The first "test" is of course whether the header compiles standalone +#include "pplx/pplx_utils.h" + +#include "bst/test/test.h" + +namespace +{ + template + inline pplx::task task_from_default() { return pplx::task_from_result(ReturnType()); } + + template <> + inline pplx::task task_from_default() { return pplx::task_from_result(); } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEMPLATE_TEST_CASE_2(testPplxWhenAll, ReturnType, void, int) +{ + pplx::task default_task; + pplx::task successful_task1 = task_from_default(); + pplx::task successful_task2 = task_from_default(); + pplx::task failed_task = pplx::task_from_exception(std::runtime_error("failed")); + pplx::task_completion_event tce; + pplx::task incomplete_task(tce); + using final_task = decltype(pplx::ranges::when_all(std::declval>>())); + + BST_REQUIRE_THROW(failed_task.wait(), std::runtime_error); + BST_REQUIRE_THROW(default_task.wait(), pplx::invalid_operation); + + { + auto tasks = { successful_task1, successful_task2 }; + bool continuation = false; + pplx::ranges::when_all(tasks).then([&](final_task finally) + { + finally.wait(); + continuation = true; + }).wait(); + BST_REQUIRE(continuation); + } + + { + auto tasks = { successful_task1, failed_task, incomplete_task }; + bool continuation = false; + pplx::ranges::when_all(tasks).then([&](final_task finally) + { + BST_REQUIRE_THROW(finally.wait(), std::runtime_error); + continuation = true; + }).wait(); + BST_REQUIRE(continuation); + } + + { + auto tasks = { default_task, incomplete_task }; + bool continuation = false; + pplx::ranges::when_all(tasks).then([&](final_task finally) + { + BST_REQUIRE_THROW(finally.wait(), pplx::invalid_operation); + continuation = true; + }).wait(); + BST_REQUIRE(continuation); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEMPLATE_TEST_CASE_2(testPplxWhenAny, ReturnType, void, int) +{ + pplx::task default_task; + pplx::task successful_task1 = task_from_default(); + pplx::task successful_task2 = task_from_default(); + pplx::task failed_task = pplx::task_from_exception(std::runtime_error("failed")); + pplx::task_completion_event tce; + pplx::task incomplete_task(tce); + using final_task = decltype(pplx::ranges::when_any(std::declval>>())); + + BST_REQUIRE_THROW(failed_task.wait(), std::runtime_error); + BST_REQUIRE_THROW(default_task.wait(), pplx::invalid_operation); + + { + auto tasks = { successful_task1, successful_task2 }; + bool continuation = false; + pplx::ranges::when_any(tasks).then([&](final_task finally) + { + finally.wait(); + continuation = true; + }).wait(); + BST_REQUIRE(continuation); + } + + { + auto tasks = { successful_task1, failed_task, incomplete_task }; + bool continuation = false; + pplx::ranges::when_any(tasks).then([&](final_task finally) + { + finally.wait(); + continuation = true; + }).wait(); + BST_REQUIRE(continuation); + } + + { + auto tasks = { default_task }; + bool continuation = false; + pplx::ranges::when_any(tasks).then([&](final_task finally) + { + BST_REQUIRE_THROW(finally.wait(), pplx::invalid_operation); + continuation = true; + }).wait(); + BST_REQUIRE(continuation); + } +} diff --git a/Development/rql/rql.cpp b/Development/rql/rql.cpp index 421838c91..57bd56290 100644 --- a/Development/rql/rql.cpp +++ b/Development/rql/rql.cpp @@ -59,8 +59,8 @@ namespace rql value result; - auto decoded_type = web::uri::decode(encoded_type); - auto decoded_value = web::uri::decode(encoded_value); + const auto decoded_type = web::uri::decode(encoded_type); + const auto decoded_value = web::uri::decode(encoded_value); if (decoded_type.empty()) { @@ -308,6 +308,37 @@ namespace rql // Helpers for evaluating RQL + void validate_query(const web::json::value& query) + { + validate_query(query, default_operators()); + } + + void validate_query(const web::json::value& arg, const operators& operators) + { + if (is_call_operator(arg)) + { + const auto& name = arg.at(U("name")).as_string(); + const auto& args = arg.at(U("args")); + + const auto found = operators.find(name); + if (found == operators.end()) + { + throw details::unimplemented_operator(name); + } + validate_query(args, operators); + } + else if (arg.is_array()) + { + const auto& array_args = arg.as_array(); + + // depth-first recursion to report first unimplemented operator + for (const auto& array_arg : array_args) + { + validate_query(array_arg, operators); + } + } + } + evaluator::evaluator(extractor extract) : extract(extract) , operators(default_operators()) @@ -632,7 +663,7 @@ namespace rql return details::matches(target, pattern, icase); } - // any_matches(, [, ]) - Filters for objects as above or where the specified property's value is an array and any element of the array is a string which contains a match for the specified regex pattern/options + // matches(, [, ]) - Filters for objects as above or where the specified property's value is an array and any element of the array is a string which contains a match for the specified regex pattern/options web::json::value any_matches(const evaluator& eval, const web::json::value& args) { auto target = eval(args.at(0), true); diff --git a/Development/rql/rql.h b/Development/rql/rql.h index bfbf06ebc..983c345d7 100644 --- a/Development/rql/rql.h +++ b/Development/rql/rql.h @@ -32,6 +32,9 @@ namespace rql typedef std::function extractor; typedef std::unordered_map> operators; + void validate_query(const web::json::value& query); // with default call-operators + void validate_query(const web::json::value& query, const operators& operators); + struct evaluator { explicit evaluator(extractor extract); // with default call-operators diff --git a/Development/rql/test/rql_test.cpp b/Development/rql/test/rql_test.cpp new file mode 100644 index 000000000..1382c0728 --- /dev/null +++ b/Development/rql/test/rql_test.cpp @@ -0,0 +1,74 @@ +// The first "test" is of course whether the header compiles standalone +#include "rql/rql.h" + +#include "bst/test/test.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testRqlParseQuery) +{ + { + const utility::string_t query_rql = U("eq(foo.bar.baz.qux,meow)"); + const auto rql_query = rql::parse_query(query_rql); + const auto& key_path = rql_query.at(U("args")).at(0).as_string(); + BST_REQUIRE_STRING_EQUAL(U("foo.bar.baz.qux"), key_path); + } + + { + const utility::string_t query_rql = U("eq((foo,bar,baz.qux),purr)"); + const auto rql_query = rql::parse_query(query_rql); + const auto& key_path = rql_query.at(U("args")).at(0).as_array(); + BST_REQUIRE_STRING_EQUAL(U("baz.qux"), key_path.rbegin()->as_string()); + } + + { + const utility::string_t query_rql = U("eq((foo,bar,baz%2Equx),purr)"); + const auto rql_query = rql::parse_query(query_rql); + const auto& key_path = rql_query.at(U("args")).at(0).as_array(); + BST_REQUIRE_STRING_EQUAL(U("baz.qux"), key_path.rbegin()->as_string()); + } + + { + const utility::string_t query_rql = U("eq((foo,bar,baz%252Equx),hiss)"); + const auto rql_query = rql::parse_query(query_rql); + const auto& key_path = rql_query.at(U("args")).at(0).as_array(); + BST_REQUIRE_STRING_EQUAL(U("baz%2Equx"), key_path.rbegin()->as_string()); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testRqlValidateQuery) +{ + const rql::operators operators{ + { U("foo"), {} }, + { U("bar"), {} }, + { U("baz"), {} } + }; + + // no call-operator + { + const utility::string_t query_rql = U("meow"); + const auto rql_query = rql::parse_query(query_rql); + BST_REQUIRE_NO_THROW(rql::validate_query(rql_query, operators)); + } + + // only valid call-operators + { + const utility::string_t query_rql = U("foo(meow,bar(purr,baz(),hiss,(qux,yowl)))"); + const auto rql_query = rql::parse_query(query_rql); + BST_REQUIRE_NO_THROW(rql::validate_query(rql_query, operators)); + } + + // invalid call-operator + { + const utility::string_t query_rql = U("meow()"); + const auto rql_query = rql::parse_query(query_rql); + BST_REQUIRE_THROW(rql::validate_query(rql_query, operators), std::runtime_error); + } + + // invalid call-operator within an array arg nested in valid call-operators + { + const utility::string_t query_rql = U("foo(meow,bar(purr,baz(),hiss,(qux(),yowl)))"); + const auto rql_query = rql::parse_query(query_rql); + BST_REQUIRE_THROW(rql::validate_query(rql_query, operators), std::runtime_error); + } +} diff --git a/Development/sdp/json.h b/Development/sdp/json.h index be20ff9cc..64d7f7b29 100644 --- a/Development/sdp/json.h +++ b/Development/sdp/json.h @@ -143,6 +143,13 @@ namespace sdp // See https://tools.ietf.org/html/rfc7273 const utility::string_t ts_refclk{ U("ts-refclk") }; const utility::string_t mediaclk{ U("mediaclk") }; + + // See VSF TR-10-5:2022 Internet Protocol Media Experience (IPMX): HDCP Key Exchange Protocol, Section 10 + // at https://videoservicesforum.com/download/technical_recommendations/VSF_TR-10-5_2022-03-22.pdf + const utility::string_t hkep{ U("hkep") }; + + // See https://tools.ietf.org/html/rfc5285#section-5 + const utility::string_t extmap{ U("extmap") }; } namespace fields @@ -154,7 +161,7 @@ namespace sdp const web::json::field payload_type{ U("payload_type") }; const web::json::field_as_string encoding_name{ U("encoding_name") }; const web::json::field clock_rate{ U("clock_rate") }; - const web::json::field_with_default encoding_parameters{ U("encoding_parameters"), 1 }; + const web::json::field_with_default encoding_parameters{ U("encoding_parameters"), 0 }; // a=fmtp: // See https://tools.ietf.org/html/rfc4566#section-6 @@ -195,6 +202,18 @@ namespace sdp // a=mediaclk:[id= ][=] // See https://tools.ietf.org/html/rfc7273#section-5 + + // a=hkep: + // See VSF TR-10-5:2022 Internet Protocol Media Experience (IPMX): HDCP Key Exchange Protocol, Section 10 + // at https://videoservicesforum.com/download/technical_recommendations/VSF_TR-10-5_2022-03-22.pdf + const web::json::field_as_string node_id{ U("node_id") }; + const web::json::field_as_string port_id{ U("port_id") }; + + // a=extmap:["/"] + // See https://tools.ietf.org/html/rfc5285#section-5 + const web::json::field local_id{ U("local_id") }; + const web::json::field_as_string_or direction{ U("direction"), {} }; // see sdp::direction + const web::json::field_as_string_or extensionattributes{ U("extensionattributes"), {} }; } // make a named value (useful for attributes) @@ -206,9 +225,9 @@ namespace sdp } // make a named value (useful for format specific parameters) - inline web::json::value named_value(const utility::string_t& name, const utility::string_t& value) + inline web::json::value named_value(const utility::string_t& name, const utility::string_t& value, bool keep_order = true) { - return named_value(name, web::json::value::string(value)); + return named_value(name, !value.empty() ? web::json::value::string(value) : web::json::value::null(), keep_order); } // find an array element with the specified name (useful with attributes and format specific parameters) @@ -219,6 +238,13 @@ namespace sdp return sdp::fields::name(nv) == name; }); } + inline web::json::array::iterator find_name(web::json::array& name_value_array, const utility::string_t& name) + { + return std::find_if(name_value_array.begin(), name_value_array.end(), [&](const web::json::value& nv) + { + return sdp::fields::name(nv) == name; + }); + } // Time Units // See https://tools.ietf.org/html/rfc4566#section-5.10 @@ -292,6 +318,16 @@ namespace sdp // IPv6 const address_type IP6{ U("IP6") }; } + + // Direction + DEFINE_STRING_ENUM(direction) + namespace directions + { + const direction recvonly{ U("recvonly") }; + const direction sendrecv{ U("sendrecv") }; + const direction sendonly{ U("sendonly") }; + const direction inactive{ U("inactive") }; + } } // Session Description Protocol (SDP) Source Filters @@ -397,33 +433,75 @@ namespace sdp { namespace fields { + // See SMPTE ST 2110-10:2022 Section 8 Session Description Protocol (SDP) + + // ST 2110-10:2022 Section 8.6 UDP Datagram Size + // "Senders operating with UDP Sizes which exceed the Standard UDP Size Limit shall include + // a Format Specific Parameter MAXUDP with a decimal value indicating the largest UDP Datagram + // Size (in octets) that might be present in the stream. If the MAXUDP parameter is not + // present, Receivers shall assume the Standard UDP Size Limit [of 1460 octets]." + const web::json::field max_udp_packet_size{ U("MAXUDP") }; // used for ST 2110-20 and ST 2110-22 + // ST 2110-10:2022 Section 8.7 RTP Timestamp Mode and Delay + // "Format Specific Parameter TSMODE is defined to indicate the relationship of the RTP + // timestamps to the content sampling instnace or production timeline. If the TSMODE parameter + // is not present, the receiver shall presume a value of TSMODE=NEW." + const web::json::field_as_string timestamp_mode{ U("TSMODE") }; // see sdp::timestamp_mode + // "Format Specific Parameter TSDELAY is defined to signal the Transmission Delay. The time + // value is represented as a decimal positve integer number of microseconds. If the TSDELAY + // parameter is not present, the receiver shall take a receiver-dependent action." + const web::json::field timestamp_delay{ U("TSDELAY") }; + // See https://tools.ietf.org/html/rfc4175 - // and SMPTE ST 2110-20:2017 Section 7 Session Description Protocol (SDP) Considerations - // and VSF TR-05:2018 + // and SMPTE ST 2110-20:2022 Section 7 Session Description Protocol (SDP) Considerations + // and VSF TR-05:2018 Essential Formats and Descriptions for Interoperability of SMPTE ST 2110-20 Video Signals + // at https://videoservicesforum.net/download/technical_recommendations/VSF_TR-05_2018-06-23.pdf + + // ST 2110-20:2022 Section 7.2 Required Media Type Parameters + const web::json::field_as_string sampling{ U("sampling") }; // see sdp::sampling const web::json::field width{ U("width") }; const web::json::field height{ U("height") }; - const web::json::field_as_string exactframerate{ U("exactframerate") }; + const web::json::field depth{ U("depth") }; + const web::json::field_as_string exactframerate{ U("exactframerate") }; // also used for ST 2110-22 and ST 2110-40 + const web::json::field_as_string colorimetry{ U("colorimetry") }; // see sdp::colorimetry + const web::json::field_as_string packing_mode{ U("PM") }; // see sdp::packing_mode + const web::json::field_as_string smpte_standard_number{ U("SSN") }; // see sdp::smpte_standard_number + + // ST 2110-20:2022 Section 7.3 Media Type Parameters with default values const web::json::field_as_bool_or interlace{ U("interlace"), false }; const web::json::field_as_bool_or segmented{ U("segmented"), false }; + // RFC 4175 defines top-field-first, but it's not included in ST 2110-20 const web::json::field_as_bool_or top_field_first{ U("top-field-first"), false }; - const web::json::field_as_string sampling{ U("sampling") }; - const web::json::field depth{ U("depth") }; - const web::json::field_as_string transfer_characteristic_system{ U("TCS") }; // "if unspecified, receivers shall assume the value SDR" - const web::json::field_as_string colorimetry{ U("colorimetry") }; - const web::json::field_as_string packing_mode{ U("PM") }; - const web::json::field_as_string smpte_standard_number{ U("SSN") }; - - // See SMPTE ST 2110-21:2017 Section 8 Session Description Considerations - const web::json::field_as_string type_parameter{ U("TP") }; + // hm, for the following optional parameters, it seems important to distinguish + // an omitted parameter from an explicitly specified default value... + // "If the TCS value is not specified, receivers shall assume the value SDR" per ST 2110-20:2017 Section 7.6 + const web::json::field_as_string transfer_characteristic_system{ U("TCS") }; // see sdp::transfer_characteristic_system + // "In the absence of [the RANGE] parameter, NARROW shall be the assumed value" per ST 2110-20:2017 Section 7.3 + // Hmm, the JPEG XS payload mapping says that "when paired with the UNSPECIFIED colorimetry, FULL SHALL be the default assumed value" + // See https://tools.ietf.org/html/rfc9134#section-7 + const web::json::field_as_string range{ U("RANGE") }; // see sdp::range + // "When it is signaled, PAR shall be signaled as a ratio of two integer decimal numbers separated by a colon character (e.g. 12:11). + // The first integer in the PAR is the width of a luminance sample, and the second integer is the height. The smallest integer values + // possible for width and height shall be used. If PAR is not signaled, the receiver shall assume that PAR = 1:1." + const web::json::field_as_string pixel_aspect_ratio{ U("PAR") }; + + // See SMPTE ST 2110-21:2022 Section 8 Session Description Considerations + + // ST 2110-21:2022 Section 8.1 Required Parameters + const web::json::field_as_string type_parameter{ U("TP") }; // used for ST 2110-20 and ST 2110-22, see sdp::type_parameter + + // ST 2110-21:2022 Section 8.2 Optional Parameters + const web::json::field TROFF{ U("TROFF") }; // used for ST 2110-20, ST 2110-22 and ST 2110-40 + const web::json::field CMAX{ U("CMAX") }; // used for ST 2110-20 and ST 2110-22 // See SMPTE ST 2110-30:2017 // and https://tools.ietf.org/html/rfc3190 - const web::json::field_as_string channel_order{ U("channel-order") }; // ".", e.g. "SMPTE2110.(ST)" + const web::json::field_as_string channel_order{ U("channel-order") }; // ".", e.g. "SMPTE2110.(ST)", see nmos/channels.h - // See SMPTE ST 2110-40:2018 + // See SMPTE ST 2110-40:2023 // and https://tools.ietf.org/html/rfc8331 - const web::json::field_as_string DID_SDID{ U("DID_SDID") }; // e.g. "{0x41,0x01}" - const web::json::field VPID_Code{ U("VPID_Code") }; // 1..255 + const web::json::field_as_string DID_SDID{ U("DID_SDID") }; // e.g. "{0x41,0x01}", see nmos::did_sdid + const web::json::field VPID_Code{ U("VPID_Code") }; // 1..255, see nmos::vpid_code + const web::json::field_as_string TM{ U("TM") }; // see sdp::tranmission_model } } @@ -431,29 +509,44 @@ namespace sdp { // Colour (sub-)sampling mode of the video stream, e.g. "YCbCr-4:2:2" // See https://tools.ietf.org/html/rfc4175 - // and SMPTE ST 2110-20:2107 Section 7.4.1 Samping + // and SMPTE ST 2110-20:2017 Section 7.4.1 Sampling // and VSF TR-05:2018, etc. DEFINE_STRING_ENUM(sampling) namespace samplings { + // Red-Green-Blue-Alpha const sampling RGBA{ U("RGBA") }; + // Red-Green-Blue const sampling RGB{ U("RGB") }; // Non-constant luminance YCbCr const sampling YCbCr_4_4_4{ U("YCbCr-4:4:4") }; const sampling YCbCr_4_2_2{ U("YCbCr-4:2:2") }; const sampling YCbCr_4_2_0{ U("YCbCr-4:2:0") }; const sampling YCbCr_4_1_1{ U("YCbCr-4:1:1") }; + // Constant luminance YCbCr + // e.g. as specified in Recommendation ITU-R BT.2020-2 + const sampling CLYCbCr_4_4_4{ U("CLYCbCr-4:4:4") }; + const sampling CLYCbCr_4_2_2{ U("CLYCbCr-4:2:2") }; + const sampling CLYCbCr_4_2_0{ U("CLYCbCr-4:2:0") }; // Constant intensity ICtCp + // e.g. as specified in Recommendation ITU-R BT.2100 const sampling ICtCp_4_4_4{ U("ICtCp-4:4:4") }; const sampling ICtCp_4_2_2{ U("ICtCp-4:2:2") }; const sampling ICtCp_4_2_0{ U("ICtCp-4:2:0") }; // XYZ + // e.g. as specified in SMPTE ST 428-1 const sampling XYZ{ U("XYZ") }; + // Key signal represented as a single component + // e.g. as specified in SMPTE RP 157 + const sampling KEY{ U("KEY") }; + // Hmm, the JPEG XS payload mapping adds this value, for "sampling signaled by the payload" + // See https://tools.ietf.org/html/rfc9134#section-7 + const sampling UNSPECIFIED{ U("UNSPECIFIED") }; } // Colorimetry // See https://tools.ietf.org/html/rfc4175 - // and SMPTE ST 2110-20:2017 Section 7.5 Permitted values of Colorimetry + // and SMPTE ST 2110-20:2022 Section 7.5 Permitted values of Colorimetry // and AMWA IS-04 v1.2 "colorspace" DEFINE_STRING_ENUM(colorimetry) namespace colorimetries @@ -468,6 +561,29 @@ namespace sdp const colorimetry BT2020{ U("BT2020") }; // ITU-R BT.2100 Table 2 const colorimetry BT2100{ U("BT2100") }; + // SMPTE ST 2065-1 Academy Color Encoding Specification (ACES) + const colorimetry ST2065_1{ U("ST2065-1") }; + // SMPTE ST 2065-3 Academy Density Exchange Encoding (ADX) + const colorimetry ST2065_3{ U("ST2065-3") }; + // Colorimetry that is not specified and must be manually coordinated between sender and receiver + const colorimetry UNSPECIFIED{ U("UNSPECIFIED") }; + // ISO 11664-1 CIE 1931 standard colorimetric system + const colorimetry XYZ{ U("XYZ") }; + // Colorimetry value signaled for key signals as specified in SMPTE RP 157 + const colorimetry ALPHA{ U("ALPHA") }; + } + + // Timestamp Mode + // See SMPTE ST 2110-10:2022 Section 8.7 RTP Timestamp Mode and Delay + DEFINE_STRING_ENUM(timestamp_mode) + namespace timestamp_modes + { + // "Effective sampling instant" + const timestamp_mode SAMP{ U("SAMP") }; + // "Created anew at the egress of this sender" + const timestamp_mode NEW{ U("NEW") }; + // "Preserved from an input signal [that] did not indicate a value of TSMODE=SAMP" + const timestamp_mode PRES{ U("PRES") }; } // Packing Mode @@ -482,11 +598,30 @@ namespace sdp } // SMPTE Standard Number - // See SMPTE ST 2110-20:2017 Section 7.2 Required Media Type Parameters + // See SMPTE ST 2110-20:2022 Section 7.2 Required Media Type Parameters + // and SMPTE ST 2110-22:2022 Section 7.2 Format-specific Parameters + // and SMPTE ST 2110-40:2023 Section 7 Session Description Protocol (SDP) DEFINE_STRING_ENUM(smpte_standard_number) namespace smpte_standard_numbers { const smpte_standard_number ST2110_20_2017{ U("ST2110-20:2017") }; + // "Senders implementing this standard shall signal the value ST2110-20:2017 unless the colorimetry value ALPHA + // or the TCS value ST2115LOGS3 are used, in which case the value ST2110-20:2022 shall be signaled." + const smpte_standard_number ST2110_20_2022{ U("ST2110-20:2022") }; + + const smpte_standard_number ST2110_22_2019{ U("ST2110-22:2019") }; + // "If present, the value shall be either ST2110-22:2019 or ST2110-22:2022, depending on the version of the + // standard implemented." + // Note that SSN was not actually specified in ST 2110-22:2019... + const smpte_standard_number ST2110_22_2022{ U("ST2110-22:2022") }; + + const smpte_standard_number ST2110_40_2018{ U("ST2110-40:2018") }; + // "Senders implementing this standard shall signal the value ST2110-40:2018 unless they are signaling TM, in + // which case they shall signal the value ST2110-40:2021." + // Note that SSN was not actually specified in ST 2110-40:2018... + // ...and yes, the publication of the ST 2110-40 revision was delayed two calendar years but the SSN value + // has not thus far been corrected! + const smpte_standard_number ST2110_40_2023{ U("ST2110-40:2021") }; } // TP (Media Type Parameter) @@ -503,7 +638,7 @@ namespace sdp } // TCS (Transfer Characteristic System) - // See SMPTE ST 2110-21:2017 Section 7.6 Permitted values of TCS + // See SMPTE ST 2110-20:2022 Section 7.6 Permitted values of TCS // and AMWA IS-04 v1.2 "transfer_characteristic" DEFINE_STRING_ENUM(transfer_characteristic_system) namespace transfer_characteristic_systems @@ -514,6 +649,43 @@ namespace sdp const transfer_characteristic_system PQ{ U("PQ") }; // Hybrid Log Gamma const transfer_characteristic_system HLG{ U("HLG") }; + // Video streams of linear encoded floating-point samples (depth=16f), such that all values fall within the range [0..1.0]. + const transfer_characteristic_system LINEAR{ U("LINEAR") }; + // Video Stream of linear encoded floating-point samples (depth=16f) normalized from PQ as specified in ITU-R BT.2100-0 + const transfer_characteristic_system BT2100LINPQ{ U("BT2100LINPQ") }; + // Video Stream of linear encoded floating-point samples (depth=16f) normalized from HLG as specified in ITU-R BT.2100-0 + const transfer_characteristic_system BT2100LINHLG{ U("BT2100LINHLG") }; + // Video stream of linear encoded floating-point samples (depth=16f) as specified in SMPTE ST 2065-1 + const transfer_characteristic_system ST2065_1{ U("ST2065-1") }; + // Video stream utilizing the transfer characteristic specified in SMPTE ST 428-1 Section 4.3. + const transfer_characteristic_system ST428_1{ U("ST428-1") }; + // Video streams of density encoded samples, such as those defined in SMPTE ST 2065-3. + const transfer_characteristic_system DENSITY{ U("DENSITY") }; + // Video streams whose transfer characteristics are not specified. The transfer characteristics must be manually coordinated between sender and receiver. + const transfer_characteristic_system UNSPECIFIED{ U("UNSPECIFIED") }; + // Video streams of high dynamic range video that utilize the "Camera Log S3" (i.e. S-Log3) transfer characteristic specified in SMPTE ST 2115. + const transfer_characteristic_system ST2115LOGS3{ U("ST2115LOGS3") }; + } + + // RANGE + // See SMPTE ST 2110-20:2017 7.3 Media Type Parameters with default values + DEFINE_STRING_ENUM(range) + namespace ranges + { + const range NARROW{ U("NARROW") }; + const range FULLPROTECT{ U("FULLPROTECT") }; + const range FULL{ U("FULL") }; + } + + // TM (Transmission Model) + // See SMPTE ST 2110-40:2023 Section 7 Session Description Protocol (SDP) + DEFINE_STRING_ENUM(transmission_model) + namespace transmission_models + { + // Low-Latency Transmission Model + const transmission_model low_latency{ U("LLTM") }; + // Compatible Transmission Model (default) + const transmission_model compatible{ U("CTM") }; } } diff --git a/Development/sdp/sdp_grammar.cpp b/Development/sdp/sdp_grammar.cpp index ed8ad6385..06086d10e 100644 --- a/Development/sdp/sdp_grammar.cpp +++ b/Development/sdp/sdp_grammar.cpp @@ -4,6 +4,7 @@ #include #include "bst/regex.h" #include "cpprest/basic_utils.h" +#include "cpprest/json_visit.h" #include "sdp/json.h" namespace sdp @@ -35,8 +36,37 @@ namespace sdp inline std::string js2s(const web::json::value& v) { auto s = utility::us2s(v.as_string()); if (!s.empty()) return s; else throw sdp_format_error("expected a non-empty string"); } inline web::json::value s2js(const std::string& s) { if (!s.empty()) return web::json::value::string(utility::s2us(s)); else throw sdp_parse_error("expected a non-empty string"); } - inline std::string jn2s(const web::json::value& v) { return v.as_number(), utility::us2s(v.serialize()); } - inline web::json::value s2jn(const std::string& s) { auto v = web::json::value::parse(utility::s2us(s)); return v.as_number(), v; } + inline std::string jn2s(const web::json::value& v) + { + // use web::json::basic_ostream_visitor rather than web::json::value::serialize + // to avoid, for example, 59.94 being output as 59.939999999999998 + std::ostringstream os; + return v.as_number(), web::json::visit(web::json::basic_ostream_visitor(os), v), os.str(); + } + inline web::json::value s2jn(const std::string& s) + { + try + { + // using web::json::value::parse handles ints, doubles, etc. + auto v = web::json::value::parse(utility::s2us(s)); + return v.as_number(), v; + } + catch (const web::json::json_exception&) + { + throw sdp_parse_error("expected a number"); + } + } + + // since several fields have the grammar 1*DIGIT which allows leading zeros, use a different parser for these + // for now, leading zeros are not roundtrippable + inline web::json::value digits2jn(const std::string& s) + { + uint64_t v; + std::istringstream is(s); + is >> v; + if (is.fail() || !is.eof()) throw sdp_parse_error("expected a sequence of digits"); + return web::json::value(v); + } // find the first delimiter in str, beginning at pos, and return the substring from pos to the delimiter (or end) // set pos to the end of the delimiter @@ -64,6 +94,8 @@ namespace sdp const converter number_converter{ jn2s, s2jn }; + const converter digits_converter{ jn2s, digits2jn }; + // [] converter key_value_converter(char separator, const std::pair& key_converter, const std::pair& value_converter) { @@ -150,7 +182,11 @@ namespace sdp const converter strings_converter = array_converter(string_converter, " "); - const converter named_values_converter = array_converter(key_value_converter('=', { sdp::fields::name, string_converter }, { sdp::fields::value, string_converter }), "; ", ";[ \\t]+"); + // ST 2110-20:2022 says "the section shall consist of a sequence of + // media type parameter entries, separated by the semicolon (";") character followed by whitespace" + // but RFC 4566 does not itself specify the syntax of format-specific parameters and many examples + // in other RFCs and SMPTE standards are inconsistent, so allow additional whitespace + const converter named_values_converter = array_converter(key_value_converter('=', { sdp::fields::name, string_converter }, { sdp::fields::value, string_converter }), "; ", "[ \\t]*(;[ \\t]*|$)"); converter object_converter(const std::vector>& field_converters, const std::string& delimiter = " ") { @@ -188,12 +224,12 @@ namespace sdp const converter typed_time_converter { [](const web::json::value& v) { - return jn2s(v.at(sdp::fields::time_value)) + utility::us2s(sdp::fields::time_unit(v)); + return digits_converter.format(v.at(sdp::fields::time_value)) + utility::us2s(sdp::fields::time_unit(v)); }, [](const std::string& s) { return !s.empty() && std::string::npos != time_units.find(s.back()) - ? web::json::value_of({ { sdp::fields::time_value, s2jn(s.substr(0, s.size() - 1)) }, { sdp::fields::time_unit, s2js({ s.back() }) } }, keep_order) - : web::json::value_of({ { sdp::fields::time_value, s2jn(s) } }, keep_order); + ? web::json::value_of({ { sdp::fields::time_value, digits_converter.parse(s.substr(0, s.size() - 1)) }, { sdp::fields::time_unit, s2js({ s.back() }) } }, keep_order) + : web::json::value_of({ { sdp::fields::time_value, digits_converter.parse(s) } }, keep_order); } }; @@ -245,7 +281,7 @@ namespace sdp const line protocol_version = required_line( sdp::fields::protocol_version, 'v', - number_converter + digits_converter ); // See https://tools.ietf.org/html/rfc4566#section-5.2 @@ -254,8 +290,8 @@ namespace sdp 'o', object_converter({ { sdp::fields::user_name, string_converter }, - { sdp::fields::session_id, number_converter }, - { sdp::fields::session_version, number_converter }, + { sdp::fields::session_id, digits_converter }, + { sdp::fields::session_version, digits_converter }, { sdp::fields::network_type, string_converter }, { sdp::fields::address_type, string_converter }, { sdp::fields::unicast_address, string_converter } @@ -317,7 +353,7 @@ namespace sdp 'b', object_converter({ { sdp::fields::bandwidth_type, string_converter }, - { sdp::fields::bandwidth, number_converter } + { sdp::fields::bandwidth, digits_converter } }, ":") ); @@ -415,9 +451,11 @@ namespace sdp auto v = web::json::value::object(keep_order); const auto colon = s.find(':'); - // empty for (before the colon) is prohibited const auto name = utility::s2us(s.substr(0, colon)); + // empty for (before the colon) is prohibited + if (name.empty()) throw sdp_parse_error("expected an attribute name"); + v[sdp::fields::name] = web::json::value::string(name); // empty for (after the colon) is prohibited @@ -456,7 +494,7 @@ namespace sdp 'm', object_converter({ { sdp::fields::media_type, string_converter }, - { {}, key_value_converter('/', { sdp::fields::port, number_converter }, { sdp::fields::port_count, number_converter }) }, + { {}, key_value_converter('/', { sdp::fields::port, digits_converter }, { sdp::fields::port_count, number_converter }) }, { sdp::fields::protocol, string_converter }, { sdp::fields::formats, strings_converter } }) @@ -554,7 +592,7 @@ namespace sdp s += " "; // are required but may be empty const auto& params = v.at(sdp::fields::format_specific_parameters); - if (0 != params.size()) s += named_values_converter.format(params) + "; "; + s += named_values_converter.format(params); return s; }, [](const std::string& s) { @@ -564,9 +602,6 @@ namespace sdp v[sdp::fields::format] = string_converter.parse(substr_find(s, pos, whitespace)); // handle no space after if there are no auto params = std::string::npos != pos ? substr_find(s, pos) : ""; - // named_values_converter ignores a (correct, probably?) trailing "; " and equally copes if it's not present - // but needs a helping hand with a trailing ";" but no space - if (!params.empty() && ';' == params.back()) params.push_back(' '); v[sdp::fields::format_specific_parameters] = named_values_converter.parse(params); return v; }, @@ -704,6 +739,39 @@ namespace sdp { sdp::attributes::mediaclk, string_converter // sorry, cannot summon the energy + }, + { + sdp::attributes::extmap, + { + [](const web::json::value& v) { + std::string s; + s += digits_converter.format(v.at(sdp::fields::local_id)); + if (v.has_field(sdp::fields::direction)) s += "/" + string_converter.format(v.at(sdp::fields::direction)); + s += " " + string_converter.format(v.at(sdp::fields::uri)); + if (v.has_field(sdp::fields::extensionattributes)) s += " " + string_converter.format(v.at(sdp::fields::extensionattributes)); + return s; + }, + [](const std::string& s) { + auto v = web::json::value::object(keep_order); + size_t pos = 0; + v[sdp::fields::local_id] = digits_converter.parse(substr_find(s, pos, bst::regex{ R"(\D)" })); + if (s.at(pos - 1) == '/') v[sdp::fields::direction] = string_converter.parse(substr_find(s, pos, " ")); + v[sdp::fields::uri] = string_converter.parse(substr_find(s, pos, " ")); + if (std::string::npos != pos) v[sdp::fields::extensionattributes] = string_converter.parse(substr_find(s, pos)); + return v; + } + } + }, + { + sdp::attributes::hkep, + object_converter({ + { sdp::fields::port, digits_converter }, + { sdp::fields::network_type, string_converter }, + { sdp::fields::address_type, string_converter }, + { sdp::fields::unicast_address, string_converter }, + { sdp::fields::node_id, string_converter }, + { sdp::fields::port_id, string_converter }, + }) } }; } @@ -792,14 +860,21 @@ namespace sdp web::json::value read_equals_value(std::istream& is, const grammar::converter& value_converter) { + // RFC 4566 section 5 requires that every line is "of the form: =" + // and this is confirmed by the ABNF in section 9 if ('=' != is.get()) throw sdp_parse_error("expected '='"); - std::string line; - std::getline(is, line, '\n'); - if ('\r' == line.back()) line.pop_back(); - // else throw sdp_parse_error("expected CRLF"); + // RFC 4566 section 5 specifies that "CRLF is used to end a record, although parsers SHOULD be tolerant + // and also accept records terminated with a single newline charcter." + std::string value; + std::getline(is, value, '\n'); + if (!value.empty() && '\r' == value.back()) value.pop_back(); + + // RFC 4566 section 5 doesn't specify that the value must not be empty although the ABNF in section 9 + // ultimately prohibits empty values even for "s=" and "i=" lines + //if (value.empty()) throw sdp_parse_error("expected a value after '='"); - return value_converter.parse(line); + return value_converter.parse(value); } void read_line(std::istream& is, int& line_number, web::json::value& line, const grammar::line& grammar) diff --git a/Development/sdp/test/sdp_test.cpp b/Development/sdp/test/sdp_test.cpp index a18dd566c..6d216b2a3 100644 --- a/Development/sdp/test/sdp_test.cpp +++ b/Development/sdp/test/sdp_test.cpp @@ -14,12 +14,18 @@ o=- 3745911798 3745911798 IN IP4 192.168.9.142 s=Example Sender 1 (Video) t=0 0 a=group:DUP PRIMARY SECONDARY +a=extmap:1 http://example.com/082005/ext.htm#ttime +a=extmap:2/sendrecv http://example.com/082005/ext.htm#xmeta short +a=extmap:3/sendonly http://example.com/082005/ext.htm#xmeta +a=extmap:4 http://example.com/082005/ext.htm#ttime SHORT +a=hkep:9000 IN IP4 192.168.9.142 db31de40-19ad-450a-afb9-f4105be7b564 01-02-03-04-05-06 +a=hkep:9001 IN IP4 192.168.9.142 db31de40-19ad-450a-afb9-f4105be7b564 01-02-03-04-05-06 m=video 50020 RTP/AVP 96 c=IN IP4 239.22.142.1/32 a=ts-refclk:ptp=IEEE1588-2008:traceable a=source-filter: incl IN IP4 239.22.142.1 192.168.9.142 a=rtpmap:96 raw/90000 -a=fmtp:96 colorimetry=BT709; exactframerate=30000/1001; depth=10; TCS=SDR; sampling=YCbCr-4:2:2; width=1920; interlace; TP=2110TPN; PM=2110GPM; height=1080; SSN=ST2110-20:2017; +a=fmtp:96 colorimetry=BT709; exactframerate=30000/1001; depth=10; TCS=SDR; sampling=YCbCr-4:2:2; width=1920; interlace; TP=2110TPN; PM=2110GPM; height=1080; SSN=ST2110-20:2017 a=mediaclk:direct=0 a=mid:PRIMARY m=video 50120 RTP/AVP 96 @@ -27,7 +33,7 @@ c=IN IP4 239.122.142.1/32 a=ts-refclk:ptp=IEEE1588-2008:traceable a=source-filter: incl IN IP4 239.122.142.1 192.168.109.142 a=rtpmap:96 raw/90000 -a=fmtp:96 colorimetry=BT709; exactframerate=30000/1001; depth=10; TCS=SDR; sampling=YCbCr-4:2:2; width=1920; interlace; TP=2110TPN; PM=2110GPM; height=1080; SSN=ST2110-20:2017; +a=fmtp:96 colorimetry=BT709; exactframerate=30000/1001; depth=10; TCS=SDR; sampling=YCbCr-4:2:2; width=1920; interlace; TP=2110TPN; PM=2110GPM; height=1080; SSN=ST2110-20:2017 a=mediaclk:direct=0 a=mid:SECONDARY )"; @@ -63,9 +69,9 @@ a=mid:SECONDARY { std::string expected_line, actual_line; std::getline(expected, expected_line); - // CR cannot appear in a raw string literal, so add it - if (!expected_line.empty()) expected_line.push_back('\r'); std::getline(actual, actual_line); + // CR cannot appear in a raw string literal, so remove it from the actual line + if (!actual_line.empty() && '\r' == actual_line.back()) actual_line.pop_back(); BST_CHECK_EQUAL(expected_line, actual_line); } while (!expected.fail() && !actual.fail()); @@ -119,6 +125,60 @@ a=mid:SECONDARY U("SECONDARY") }) } }, keep_order) }, + }, keep_order), + web::json::value_of({ + { sdp::fields::name, sdp::attributes::extmap }, + { sdp::fields::value, web::json::value_of({ + { sdp::fields::local_id, 1 }, + { sdp::fields::uri, U("http://example.com/082005/ext.htm#ttime") } + }, keep_order) }, + }, keep_order), + web::json::value_of({ + { sdp::fields::name, sdp::attributes::extmap }, + { sdp::fields::value, web::json::value_of({ + { sdp::fields::local_id, 2 }, + { sdp::fields::direction, sdp::directions::sendrecv.name }, + { sdp::fields::uri, U("http://example.com/082005/ext.htm#xmeta") }, + { sdp::fields::extensionattributes, U("short") } + }, keep_order) }, + }, keep_order), + web::json::value_of({ + { sdp::fields::name, sdp::attributes::extmap }, + { sdp::fields::value, web::json::value_of({ + { sdp::fields::local_id, 3 }, + { sdp::fields::direction, sdp::directions::sendonly.name }, + { sdp::fields::uri, U("http://example.com/082005/ext.htm#xmeta") } + }, keep_order) }, + }, keep_order), + web::json::value_of({ + { sdp::fields::name, sdp::attributes::extmap }, + { sdp::fields::value, web::json::value_of({ + { sdp::fields::local_id, 4 }, + { sdp::fields::uri, U("http://example.com/082005/ext.htm#ttime") }, + { sdp::fields::extensionattributes, U("SHORT") } + }, keep_order) }, + }, keep_order), + web::json::value_of({ + { sdp::fields::name, sdp::attributes::hkep }, + { sdp::fields::value, web::json::value_of({ + { sdp::fields::port, 9000 }, + { sdp::fields::network_type, sdp::network_types::internet.name }, + { sdp::fields::address_type, sdp::address_types::IP4.name }, + { sdp::fields::unicast_address, U("192.168.9.142") }, + { sdp::fields::node_id, U("db31de40-19ad-450a-afb9-f4105be7b564") }, + { sdp::fields::port_id, U("01-02-03-04-05-06") } + }, keep_order) }, + }, keep_order), + web::json::value_of({ + { sdp::fields::name, sdp::attributes::hkep }, + { sdp::fields::value, web::json::value_of({ + { sdp::fields::port, 9001 }, + { sdp::fields::network_type, sdp::network_types::internet.name }, + { sdp::fields::address_type, sdp::address_types::IP4.name }, + { sdp::fields::unicast_address, U("192.168.9.142") }, + { sdp::fields::node_id, U("db31de40-19ad-450a-afb9-f4105be7b564") }, + { sdp::fields::port_id, U("01-02-03-04-05-06") } + }, keep_order) }, }, keep_order) }) }, { sdp::fields::media_descriptions, web::json::value_of({ @@ -253,6 +313,111 @@ a=mid:SECONDARY BST_CHECK_EQUAL(session_description3.serialize(), session_description2.serialize()); } +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpNumbers) +{ + const bool keep_order = true; + + const std::string test_sdp = R"(v=0 +o=- 0 0 IN IP4 192.0.2.0 +s=Awkward frame rate values +t=0 0 +a=framerate:23.98 +a=framerate:25 +a=framerate:29.97 +a=framerate:59.94 +)"; + + auto session_description = sdp::parse_session_description(test_sdp); + + auto test_sdp2 = sdp::make_session_description(session_description); + std::istringstream expected(test_sdp), actual(test_sdp2); + do + { + std::string expected_line, actual_line; + std::getline(expected, expected_line); + std::getline(actual, actual_line); + // CR cannot appear in a raw string literal, so remove it from the actual line + if (!actual_line.empty() && '\r' == actual_line.back()) actual_line.pop_back(); + BST_CHECK_EQUAL(expected_line, actual_line); + } while (!expected.fail() && !actual.fail()); + + auto session_description2 = web::json::value_of({ + { sdp::fields::protocol_version, 0 }, + { sdp::fields::origin, web::json::value_of({ + { sdp::fields::user_name, U("-") }, + { sdp::fields::session_id, 0 }, + { sdp::fields::session_version, 0 }, + { sdp::fields::network_type, sdp::network_types::internet.name }, + { sdp::fields::address_type, sdp::address_types::IP4.name }, + { sdp::fields::unicast_address, U("192.0.2.0") } + }, keep_order) }, + { sdp::fields::session_name, U("Awkward frame rate values") }, + { sdp::fields::time_descriptions, web::json::value_of({ + web::json::value_of({ + { sdp::fields::timing, web::json::value_of({ + { sdp::fields::start_time, 0 }, + { sdp::fields::stop_time, 0 } + }, keep_order) } + }) + }) }, + { sdp::fields::attributes, web::json::value_of({ + web::json::value_of({ + { sdp::fields::name, sdp::attributes::framerate }, + { sdp::fields::value, 23.98 } + }, keep_order), + web::json::value_of({ + { sdp::fields::name, sdp::attributes::framerate }, + { sdp::fields::value, 25 } + }, keep_order), + web::json::value_of({ + { sdp::fields::name, sdp::attributes::framerate }, + { sdp::fields::value, 29.97 } + }, keep_order), + web::json::value_of({ + { sdp::fields::name, sdp::attributes::framerate }, + { sdp::fields::value, 59.94 } + }, keep_order) + }) } + }, keep_order); + + BST_REQUIRE_EQUAL(session_description, session_description2); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpDigits) +{ + const std::string test_sdp = R"(v=000 +o=- 007 0987654321098765432 IN IP4 192.0.2.0 +s=Leading zeros +b=AS:09876543210 +t=0 0 +m=video 000000000000000000050020 RTP/AVP 0096 +)"; + + const std::string expected_sdp = R"(v=0 +o=- 7 987654321098765432 IN IP4 192.0.2.0 +s=Leading zeros +b=AS:9876543210 +t=0 0 +m=video 50020 RTP/AVP 0096 +)"; + + auto session_description = sdp::parse_session_description(test_sdp); + + auto test_sdp2 = sdp::make_session_description(session_description); + std::istringstream expected(expected_sdp), actual(test_sdp2); + do + { + std::string expected_line, actual_line; + std::getline(expected, expected_line); + std::getline(actual, actual_line); + // CR cannot appear in a raw string literal, so remove it from the actual line + if (!actual_line.empty() && '\r' == actual_line.back()) actual_line.pop_back(); + BST_CHECK_EQUAL(expected_line, actual_line); + } while (!expected.fail() && !actual.fail()); +} + //////////////////////////////////////////////////////////////////////////////////////////// BST_TEST_CASE(testSdpParseErrors) { @@ -272,8 +437,14 @@ BST_TEST_CASE(testSdpParseErrors) // appending just a single 'a' results in an "sdp parse error - expected '=' at line 5" BST_REQUIRE_THROW(sdp::parse_session_description(enough + "a"), std::runtime_error); - // appending a complete 'a' line (even without a final CRLF) parses successfully + // appending the '=' as well results in an "sdp parse error - expected an attribute name at line 5" + BST_REQUIRE_THROW(sdp::parse_session_description(enough + "a="), std::runtime_error); + // appending a valid "a=" form line (even without a final CRLF) parses successfully BST_REQUIRE_EQUAL(5, sdp::parse_session_description(enough + "a=foo").size()); + // appending an invalid "a=:" form line results in an "sdp parse error - expected an attribute value after ':' at line 5" + BST_REQUIRE_THROW(sdp::parse_session_description(enough + "a=foo:"), std::runtime_error); + // appending a valid "a=:" line also parses successfully + BST_REQUIRE_EQUAL(5, sdp::parse_session_description(enough + "a=foo:bar").size()); // appending an invalid type character results in "sdp parse error - unexpected characters before end-of-file at line 5" BST_REQUIRE_THROW(sdp::parse_session_description(enough + "x"), std::runtime_error); @@ -285,3 +456,118 @@ BST_TEST_CASE(testSdpParseErrors) // ... even if there's a complete valid line after it BST_REQUIRE_THROW(sdp::parse_session_description(enough + "\r\na=foo"), std::runtime_error); } + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpNumberErrors) +{ + const std::string before = "v=0\r\no=- 42 42 IN IP4 10.0.0.1\r\ns= \r\n"; + BST_REQUIRE_NO_THROW(sdp::parse_session_description(before + "t=0 0")); + BST_REQUIRE_NO_THROW(sdp::parse_session_description(before + "t=0 0\r\na=ptime:0.125")); + BST_REQUIRE_NO_THROW(sdp::parse_session_description(before + "t=0 0\r\na=ptime:1")); + // an invalid time results in "sdp parse error - expected a number at line 4" + BST_REQUIRE_THROW(sdp::parse_session_description(before + "t=foo 0"), std::runtime_error); + // an invalid packet time results in "sdp parse error - expected a number at line 5" + BST_REQUIRE_THROW(sdp::parse_session_description(before + "t=0 0\r\na=ptime:foo"), std::runtime_error); + //BST_REQUIRE_THROW(sdp::parse_session_description(before + "t=0 0\r\na=ptime:+1.25e-1"), std::runtime_error); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpDigitsErrors) +{ + const std::string after = "\r\no=- 42 42 IN IP4 10.0.0.1\r\ns= \r\nt=0 0\r\n"; + BST_REQUIRE_NO_THROW(sdp::parse_session_description("v=0" + after)); + BST_REQUIRE_NO_THROW(sdp::parse_session_description("v=0000000000" + after)); + // an invalid protocol version results in "sdp parse error - expected a sequence of digits at line 1" + BST_REQUIRE_THROW(sdp::parse_session_description("v=" + after), std::runtime_error); + BST_REQUIRE_THROW(sdp::parse_session_description("v=foo" + after), std::runtime_error); + BST_REQUIRE_THROW(sdp::parse_session_description("v=0foo" + after), std::runtime_error); + BST_REQUIRE_THROW(sdp::parse_session_description("v=0.0" + after), std::runtime_error); + BST_REQUIRE_THROW(sdp::parse_session_description("v=0x0" + after), std::runtime_error); + //BST_REQUIRE_THROW(sdp::parse_session_description("v=+0" + after), std::runtime_error); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpFmtp) +{ + const std::string test_sdp = R"(v=0 +o=- 0 0 IN IP4 192.0.2.1 +s=Example Sender 1 (Video) +t=0 0 +m=video 5004 RTP/AVP 96 +c=IN IP4 233.252.0.1/32 +a=fmtp:96)"; + + const std::vector> fmtp_params = { + { 0, "" }, + { 0, " " }, + { 1, " foo=meow" }, + { 1, " foo=meow;" }, + { 1, " foo=meow; " }, + { 2, " foo=meow;bar=purr" }, + { 2, " bar=purr; foo=meow;" }, + { 2, " bar=purr ; foo=meow " }, + { 2, " bar=purr ; foo=meow ; " } + }; + + for (const auto& fp : fmtp_params) + { + auto session_description = sdp::parse_session_description(test_sdp + fp.second); + auto& media_descriptions = sdp::fields::media_descriptions(session_description); + auto& media_description = media_descriptions.at(0); + auto& attributes = sdp::fields::attributes(media_description).as_array(); + + auto fmtp = sdp::find_name(attributes, sdp::attributes::fmtp); + BST_REQUIRE(attributes.end() != fmtp); + + auto& params = sdp::fields::format_specific_parameters(sdp::fields::value(*fmtp)); + BST_REQUIRE_EQUAL(fp.first, params.size()); + + if (0 != fp.first) + { + auto foo_param = sdp::find_name(params, U("foo")); + BST_REQUIRE(params.end() != foo_param); + BST_REQUIRE_EQUAL(U("meow"), sdp::fields::value(*foo_param).as_string()); + } + } + + const std::vector> fail_params = { + // invalid case... whitespace on its own isn't a separator + // but nor is it allowed in the or + { 2, " bar=purr ; foo=meow baz=hiss" } + }; + + for (const auto& fp : fail_params) + { + auto session_description = sdp::parse_session_description(test_sdp + fp.second); + auto& media_descriptions = sdp::fields::media_descriptions(session_description); + auto& media_description = media_descriptions.at(0); + auto& attributes = sdp::fields::attributes(media_description).as_array(); + + auto fmtp = sdp::find_name(attributes, sdp::attributes::fmtp); + BST_REQUIRE(attributes.end() != fmtp); + + auto& params = sdp::fields::format_specific_parameters(sdp::fields::value(*fmtp)); + BST_REQUIRE_EQUAL(fp.first, params.size()); + + if (0 != fp.first) + { + auto foo_param = sdp::find_name(params, U("foo")); + BST_REQUIRE(params.end() != foo_param); + BST_REQUIRE_NE(U("meow"), sdp::fields::value(*foo_param).as_string()); + } + } + + const std::vector bad_params = { + " ;", + " ; foo=meow", + " foo=", + " foo=meow;;", + " bar=; foo=meow", + " bar=purr; ; foo=meow" + }; + + for (const auto& bp : bad_params) + { + BST_REQUIRE_THROW(sdp::parse_session_description(test_sdp + bp), std::runtime_error); + } +} diff --git a/Development/ssl/ssl_utils.cpp b/Development/ssl/ssl_utils.cpp new file mode 100644 index 000000000..9dd4cdb90 --- /dev/null +++ b/Development/ssl/ssl_utils.cpp @@ -0,0 +1,216 @@ +#include "ssl/ssl_utils.h" + +#include // for boost::split +#include +#include + +namespace ssl +{ + namespace experimental + { + namespace details + { + // get the text value from the X509 Name with the given Number identifier (NID) + std::string get_text_by_NID(X509_NAME* x509_name, int nid) + { + const auto len = X509_NAME_get_text_by_NID(x509_name, nid, NULL, 0); + if (0 < len) + { + const auto buffer_size = len + 1; + std::vector buffer(buffer_size); + if (len == X509_NAME_get_text_by_NID(x509_name, nid, buffer.data(), buffer_size)) + { + return std::string(buffer.data(), len); + } + } + return ""; + } + + // get the Subject Alternative Name(s) from certificate + std::vector get_subject_alt_names(X509* x509) + { + std::vector subject_alternative_names; + GENERAL_NAMES_ptr subject_alt_names((GENERAL_NAMES*)X509_get_ext_d2i(x509, NID_subject_alt_name, NULL, NULL), &GENERAL_NAMES_free); + for (auto idx = 0; idx < sk_GENERAL_NAME_num(subject_alt_names.get()); idx++) + { + auto gen = sk_GENERAL_NAME_value(subject_alt_names.get(), idx); + if (gen->type == GEN_DNS) + { + auto asn1_str = gen->d.dNSName; +#if (OPENSSL_VERSION_NUMBER >= 0x1010100fL) + auto san = std::string(reinterpret_cast(ASN1_STRING_get0_data(asn1_str)), ASN1_STRING_length(asn1_str)); +#else + auto san = std::string(reinterpret_cast(ASN1_STRING_data(asn1_str)), ASN1_STRING_length(asn1_str)); +#endif + subject_alternative_names.push_back(san); + } + else + { + // hmm, not supporting other type of subject alt name + } + } + return subject_alternative_names; + } + + // convert ASN.1 time to POSIX (UTC) + time_t ASN1_TIME_to_time_t(const ASN1_TIME* time) + { + if (!time) + { + throw ssl_exception("failed to convert ASN1_TIME to UTC: invalid ASN1_TIME"); + } + + tm tm; + +#if (OPENSSL_VERSION_NUMBER >= 0x1010100fL) + if (!ASN1_TIME_to_tm(time, &tm)) + { + throw ssl_exception("failed to convert ASN1_TIME to tm: ASN1_TIME_to_tm failure: " + last_openssl_error()); + } +#else + auto s = time->data; + if (!s) + { + throw ssl_exception("failed to convert ASN1_TIME to UTC: invalid ASN1_TIME, no ANS1 data"); + } + + auto two_digits_to_uint = [&]() + { + uint32_t n = 10 * (*s++ - '0'); + return n + (*s++ - '0'); + }; + + switch (time->type) + { + // see https://tools.ietf.org/html/rfc5280#section-4.1.2.5.1 + case V_ASN1_UTCTIME: // YYMMDDHHMMSSZ + tm.tm_year = two_digits_to_uint(); + tm.tm_year += tm.tm_year < 50 ? 2000 : 1900; + tm.tm_year -= 1900; + break; + // https://tools.ietf.org/html/rfc5280#section-4.1.2.5.2 + case V_ASN1_GENERALIZEDTIME: // YYYYMMDDHHMMSSZ + tm.tm_year = 100 * two_digits_to_uint(); + tm.tm_year += two_digits_to_uint(); + tm.tm_year -= 1900; + break; + default: + throw ssl_exception("failed to convert ASN1_TIME to UTC: invalid ASN.1 time type"); + } + tm.tm_mon = two_digits_to_uint() - 1; + tm.tm_mday = two_digits_to_uint(); + tm.tm_hour = two_digits_to_uint(); + tm.tm_min = two_digits_to_uint(); + tm.tm_sec = two_digits_to_uint(); + if (*s != 'Z') { throw ssl_exception("failed to convert ASN1_TIME to UTC: invalid ASN.1 time format"); } + tm.tm_isdst = 0; +#endif + + return mktime(&tm); + } + } + + // get last openssl error + std::string last_openssl_error() + { + char buffer[1024] = { 0 }; + ERR_error_string_n(ERR_get_error(), buffer, sizeof(buffer)); + return buffer; + } + + // get certificate information, such as subject, issuer and validity + certificate_info get_certificate_info(const std::string& certificate) + { + BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if (!bio) + { + throw ssl_exception("failed to load certificate while creating BIO memory: BIO_new failure: " + last_openssl_error()); + } + + if ((size_t)BIO_write(bio.get(), certificate.data(), (int)certificate.size()) != certificate.size()) + { + throw ssl_exception("failed to load certificate to bio: BIO_write failure: " + last_openssl_error()); + } + + X509_ptr x509(PEM_read_bio_X509_AUX(bio.get(), NULL, NULL, NULL), &X509_free); + if (!x509) + { + throw ssl_exception("failed to load certificate bio to X509: PEM_read_bio_X509_AUX failure: " + last_openssl_error()); + } + + auto subject_alternative_names = details::get_subject_alt_names(x509.get()); + + auto subject_common_name = details::get_text_by_NID(X509_get_subject_name(x509.get()), NID_commonName); + if (subject_common_name.empty()) + { + throw ssl_exception("missing Subject Common Name"); + } + + auto issuer_common_name = details::get_text_by_NID(X509_get_issuer_name(x509.get()), NID_commonName); + if (issuer_common_name.empty()) + { + throw ssl_exception("missing Issuer Common Name"); + } + + // X509_get_notAfter returns the time that the cert expires, in Abstract Syntax Notation + // According to the openssl documentation, the returned value is an internal pointer which MUST NOT be freed +#if (OPENSSL_VERSION_NUMBER >= 0x1010000fL) + auto not_before = X509_get0_notBefore(x509.get()); +#else + auto not_before = X509_get_notBefore(x509.get()); +#endif + if (!not_before) + { + throw ssl_exception("failed to get notBefore: X509_get0_notBefore failure: " + last_openssl_error()); + } +#if (OPENSSL_VERSION_NUMBER >= 0x1010000fL) + auto not_after = X509_get0_notAfter(x509.get()); +#else + auto not_after = X509_get_notAfter(x509.get()); +#endif + if (!not_after) + { + throw ssl_exception("failed to get notAfter: X509_get0_notAfter failure: " + last_openssl_error()); + } + + auto not_before_time = details::ASN1_TIME_to_time_t(not_before); + auto not_after_time = details::ASN1_TIME_to_time_t(not_after); + + return{ subject_common_name, issuer_common_name, not_before_time, not_after_time, subject_alternative_names }; + } + + // split certificate chain to a list of certificates + std::vector split_certificate_chain(const std::string& certificate_chain) + { + std::vector certificates; + const std::string begin_delimiter{ "-----BEGIN CERTIFICATE-----" }; + const std::string end_delimiter{ "-----END CERTIFICATE-----" }; + size_t start = 0; + size_t end = 0; + do + { + start = certificate_chain.find(begin_delimiter, start); + end = certificate_chain.find(end_delimiter, start); + + if (std::string::npos != start && std::string::npos != end) + { + certificates.push_back(certificate_chain.substr(start, end - start + end_delimiter.length())); + start = end + end_delimiter.length(); + } + + } while (std::string::npos != start && std::string::npos != end); + + return certificates; + } + + // calculate the number of seconds until expiry of the specified certificate + // 0 is returned if certificate has already expired + double certificate_expiry_from_now(const std::string& certificate) + { + const auto certificate_info = get_certificate_info(certificate); + const auto now = time(NULL); + const auto from_now = difftime(certificate_info.not_after, now); + return (std::max)(0.0, from_now); + } + } +} diff --git a/Development/ssl/ssl_utils.h b/Development/ssl/ssl_utils.h new file mode 100644 index 000000000..2cf2853c6 --- /dev/null +++ b/Development/ssl/ssl_utils.h @@ -0,0 +1,52 @@ +#ifndef SSL_SSL_UTILS_H +#define SSL_SSL_UTILS_H + +#include +#include +#include +#include +#include + +namespace ssl +{ + namespace experimental + { + struct ssl_exception : std::runtime_error + { + ssl_exception(const std::string& message) : std::runtime_error(message) {} + }; + + typedef std::unique_ptr BIO_ptr; + typedef std::unique_ptr X509_ptr; + typedef std::unique_ptr X509_NAME_ptr; + typedef std::unique_ptr GENERAL_NAMES_ptr; + typedef std::unique_ptr ASN1_TIME_ptr; + typedef std::unique_ptr ASN1_OBJECT_ptr; + + // get last openssl error + std::string last_openssl_error(); + + struct certificate_info + { + std::string subject_common_name; + std::string issuer_common_name; + // not_before and not_after are the start and end of the certificate's validity + // represented as the number of seconds in UTC since 1970-01-01T0:0:0Z + time_t not_before; + time_t not_after; + std::vector subject_alternative_names; + }; + + // get certificate information, such as subject, issuer and validity + certificate_info get_certificate_info(const std::string& certificate); + + // split certificate chain to a list of certificates + std::vector split_certificate_chain(const std::string& certificate_chain); + + // calculate the number of seconds until expiry of the specified certificate + // 0 is returned if certificate has already expired + double certificate_expiry_from_now(const std::string& certificate); + } +} + +#endif diff --git a/Development/third_party/README.md b/Development/third_party/README.md index c1f3ef9a6..553116255 100644 --- a/Development/third_party/README.md +++ b/Development/third_party/README.md @@ -6,17 +6,21 @@ Third-party source files used by the nmos-cpp libraries The [Catch](https://github.com/philsquared/Catch) (automated test framework) single header version - [cmake](cmake) CMake modules derived from third-party sources +- [jwt-cpp](jwt-cpp) + The [Thalhammer/jwt-cpp](https://github.com/Thalhammer/jwt-cpp) header only library for creating and validating JSON Web Tokens in C++11 - [mDNSResponder](mDNSResponder) Patches and patched source files for the Bonjour DNS-SD implementation - [nlohmann](nlohmann) The [JSON for Modern C++](https://github.com/nlohmann/json) and [Modern C++ JSON schema validator](https://github.com/pboettch/json-schema-validator) libraries -- [nmos-audio-channel-mapping](nmos-audio-channel-mapping) - The JSON Schema files used for validation of Channel Mapping API requests and responses -- [nmos-device-connection-management](nmos-device-connection-management) - The JSON Schema files used for validation of Connection API requests and responses -- [nmos-discovery-registration](nmos-discovery-registration) +- [is-04](is-04) The JSON Schema files used for validation of e.g. Registration API requests and responses -- [nmos-system](nmos-system) +- [is-05](is-05) + The JSON Schema files used for validation of Connection API requests and responses +- [is-08](is-08) + The JSON Schema files used for validation of Channel Mapping API requests and responses +- [is-09](is-09) The JSON Schema files used for validation of System API requests and responses +- [is-10](is-10) + The JSON Schema files used for validation of Authorization API requests and responses - [WpdPack](WpdPack) Libraries and header files from the [WinPcap](https://www.winpcap.org/) Developer's Pack diff --git a/Development/third_party/catch/catch.hpp b/Development/third_party/catch/catch.hpp index d5bd44bc0..e63e37a20 100644 --- a/Development/third_party/catch/catch.hpp +++ b/Development/third_party/catch/catch.hpp @@ -6468,7 +6468,7 @@ namespace Catch { static bool isSet; static struct sigaction oldSigActions [sizeof(signalDefs)/sizeof(SignalDefs)]; static stack_t oldSigStack; - static char altStackMem[SIGSTKSZ]; + static char altStackMem[32768]; static void handleSignal( int sig ) { std::string name = ""; @@ -6488,7 +6488,7 @@ namespace Catch { isSet = true; stack_t sigStack; sigStack.ss_sp = altStackMem; - sigStack.ss_size = SIGSTKSZ; + sigStack.ss_size = 32768; sigStack.ss_flags = 0; sigaltstack(&sigStack, &oldSigStack); struct sigaction sa = { 0 }; @@ -6519,7 +6519,7 @@ namespace Catch { bool FatalConditionHandler::isSet = false; struct sigaction FatalConditionHandler::oldSigActions[sizeof(signalDefs)/sizeof(SignalDefs)] = {}; stack_t FatalConditionHandler::oldSigStack = {}; - char FatalConditionHandler::altStackMem[SIGSTKSZ] = {}; + char FatalConditionHandler::altStackMem[32768] = {}; } // namespace Catch diff --git a/Development/third_party/cmake/FindAvahi.cmake b/Development/third_party/cmake/FindAvahi.cmake new file mode 100644 index 000000000..399bb35e6 --- /dev/null +++ b/Development/third_party/cmake/FindAvahi.cmake @@ -0,0 +1,136 @@ +#============================================================================= +# Copyright (c) 2021, NVIDIA CORPORATION. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#============================================================================= +cmake_policy(PUSH) +cmake_policy(VERSION 3.17) + +#[=======================================================================[.rst: +FindAvahi +--------- + +Finds the Avahi library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``Avahi::common`` + The avahi-common library +``Avahi::client`` + The avahi-client library +``Avahi::compat-libdns_sd`` + The avahi-compat-libdns_sd library + +Result Variables +^^^^^^^^^^^^^^^^ + +This will define the following variables: + +``Avahi_FOUND`` + True if the system has the Avahi library. +``Avahi_INCLUDE_DIRS`` + Include directories needed to use Avahi. +``Avahi_LIBRARIES`` + Libraries needed to link to Avahi. + +Cache Variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set: + +``Avahi_INCLUDE_DIR`` + The directory containing ``avahi-client/client.h`` etc. +``Avahi_common_LIBRARY`` + The path to the avahi-common library. +``Avahi_client_LIBRARY`` + The path to the avahi-client library. +``Avahi_compat-libdns_sd_LIBRARY`` + The path to the avahi-compat-libdns_sd library. + +#]=======================================================================] + +find_path(Avahi_INCLUDE_DIR + NAMES avahi-client/client.h + HINTS $ENV{Avahi_ROOT} ${Avahi_ROOT} +) + +find_library(Avahi_common_LIBRARY + NAMES avahi-common +) +find_package(Threads) + +find_library(Avahi_client_LIBRARY + NAMES avahi-client +) +find_package(DBus) + +find_library(Avahi_compat-libdns_sd_LIBRARY + NAMES dns_sd +) + +# should we also check that the daemon (avahi-daemon) is installed? +# it's not required to build... + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Avahi + FOUND_VAR Avahi_FOUND + REQUIRED_VARS + Avahi_common_LIBRARY + Avahi_client_LIBRARY + Avahi_compat-libdns_sd_LIBRARY + Avahi_INCLUDE_DIR +) + +if(Avahi_FOUND) + set(Avahi_LIBRARIES ${Avahi_common_LIBRARY} ${Avahi_client_LIBRARY} ${Avahi_compat-libdns_sd_LIBRARY}) + set(Avahi_INCLUDE_DIRS ${Avahi_INCLUDE_DIR}) +endif() + +if(Avahi_FOUND AND NOT TARGET Avahi::common) + add_library(Avahi::common UNKNOWN IMPORTED) + set_target_properties(Avahi::common PROPERTIES + IMPORTED_LOCATION "${Avahi_common_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Avahi_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "Threads::Threads" + ) +endif() + +if(Avahi_FOUND AND NOT TARGET Avahi::client) + add_library(Avahi::client UNKNOWN IMPORTED) + set_target_properties(Avahi::client PROPERTIES + IMPORTED_LOCATION "${Avahi_client_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Avahi_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "Avahi::common;DBus::DBus" + ) +endif() + +if(Avahi_FOUND AND NOT TARGET Avahi::compat-libdns_sd) + add_library(Avahi::compat-libdns_sd UNKNOWN IMPORTED) + set_target_properties(Avahi::compat-libdns_sd PROPERTIES + IMPORTED_LOCATION "${Avahi_compat-libdns_sd_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Avahi_INCLUDE_DIR}" + INTERFACE_LINK_LIBRARIES "Avahi::client" + ) +endif() + +mark_as_advanced( + Avahi_INCLUDE_DIR + Avahi_common_LIBRARY + Avahi_client_LIBRARY + Avahi_compat-libdns_sd_LIBRARY +) + +cmake_policy(POP) diff --git a/Development/third_party/cmake/FindDBus.cmake b/Development/third_party/cmake/FindDBus.cmake new file mode 100644 index 000000000..318c52154 --- /dev/null +++ b/Development/third_party/cmake/FindDBus.cmake @@ -0,0 +1,93 @@ +#============================================================================= +# Copyright (c) 2021, NVIDIA CORPORATION. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#============================================================================= +cmake_policy(PUSH) +cmake_policy(VERSION 3.17) + +#[=======================================================================[.rst: +FindDBus +--------- + +Finds the DBus library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``DBus::DBus`` + The DBus library + +Result Variables +^^^^^^^^^^^^^^^^ + +This will define the following variables: + +``DBus_FOUND`` + True if the system has the DBus library. +``DBus_INCLUDE_DIRS`` + Include directories needed to use DBus. +``DBus_LIBRARIES`` + Libraries needed to link to DBus. + +Cache Variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set: + +``DBus_INCLUDE_DIR`` + The directory containing ``dbus/dbus.h`` etc. +``DBus_LIBRARY`` + The path to the DBus library. + +#]=======================================================================] + +find_path(DBus_INCLUDE_DIR + NAMES dbus/dbus.h + PATH_SUFFIXES dbus-1.0 + HINTS $ENV{DBus_ROOT} ${DBus_ROOT} +) + +find_library(DBus_LIBRARY + NAMES dbus-1 +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(DBus + FOUND_VAR DBus_FOUND + REQUIRED_VARS + DBus_LIBRARY + DBus_INCLUDE_DIR +) + +if(DBus_FOUND) + set(DBus_LIBRARIES ${DBus_LIBRARY}) + set(DBus_INCLUDE_DIRS ${DBus_INCLUDE_DIR}) +endif() + +if(DBus_FOUND AND NOT TARGET DBus::DBus) + add_library(DBus::DBus UNKNOWN IMPORTED) + set_target_properties(DBus::DBus PROPERTIES + IMPORTED_LOCATION "${DBus_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${DBus_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced( + DBus_INCLUDE_DIR + DBus_LIBRARY +) + +cmake_policy(POP) diff --git a/Development/third_party/cmake/FindDNSSD.cmake b/Development/third_party/cmake/FindDNSSD.cmake new file mode 100644 index 000000000..d83eda8b0 --- /dev/null +++ b/Development/third_party/cmake/FindDNSSD.cmake @@ -0,0 +1,139 @@ +#============================================================================= +# Copyright (c) 2021, NVIDIA CORPORATION. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +#============================================================================= +cmake_policy(PUSH) +cmake_policy(VERSION 3.17) + +#[=======================================================================[.rst: +FindDNSSD +--------- + +Finds the DNSSD library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module provides the following imported targets, if found: + +``DNSSD::DNSSD`` + The DNSSD library + +Result Variables +^^^^^^^^^^^^^^^^ + +This will define the following variables: + +``DNSSD_FOUND`` + True if the system has the DNSSD library. +``DNSSD_VERSION`` + The version of the DNSSD library which was found. +``DNSSD_INCLUDE_DIRS`` + Include directories needed to use DNSSD. +``DNSSD_LIBRARIES`` + Libraries needed to link to DNSSD. + +Cache Variables +^^^^^^^^^^^^^^^ + +The following cache variables may also be set: + +``DNSSD_INCLUDE_DIR`` + The directory containing ``dns_sd.h``. +``DNSSD_LIBRARY`` + The path to the DNSSD library. + +#]=======================================================================] + +if(WIN32) + set(_DNSSD_PATHS "$ENV{ProgramFiles}/Bonjour SDK") + set(_DNSSD_INCLUDE_SUFFIX Include) + if (CMAKE_VS_PLATFORM_NAME STREQUAL "Win32") + set(_DNSSD_LIB_SUFFIX Lib/Win32) + else() + set(_DNSSD_LIB_SUFFIX Lib/x64) + endif() + set(_DNSSD_LIB dnssd) +else() + list(APPEND _DNSSD_PATHS /usr /usr/local) + set(_DNSSD_INCLUDE_SUFFIX include) + set(_DNSSD_LIB_SUFFIX lib) + set(_DNSSD_LIB dns_sd) +endif() + +find_path(DNSSD_INCLUDE_DIR + NAMES dns_sd.h + PATHS ${_DNSSD_PATHS} + PATH_SUFFIXES ${_DNSSD_INCLUDE_SUFFIX} + HINTS $ENV{DNSSD_ROOT} ${DNSSD_ROOT} +) + +find_library(DNSSD_LIBRARY + NAMES ${_DNSSD_LIB} + PATHS ${_DNSSD_PATHS} + PATH_SUFFIXES ${_DNSSD_LIB_SUFFIX} +) + +# should we also check on Linux that the daemon (mdnsd) is installed +# or on Windows that the Bonjour service and client DLL (dnssd.dll) are installed? +# they're not required to build... + +if(DNSSD_INCLUDE_DIR) + file(STRINGS "${DNSSD_INCLUDE_DIR}/dns_sd.h" _define_DNS_SD_H REGEX "^#define[ \t]+_DNS_SD_H [0-9]+$") + if (_define_DNS_SD_H) + # unfortunately, Apple broke their own 'rule' and have released several versions now with minor version numbers > 100 + # e.g. 878.200.35 and 1310.140.1, which cannot be reconstructed from the _DNS_SD_H value :-( + string(REGEX REPLACE "^#define[ \t]+_DNS_SD_H ([0-9]+)$" "\\1" _DNS_SD_H "${_define_DNS_SD_H}") + math(EXPR DNSSD_VERSION_MAJOR "${_DNS_SD_H} / 10000") + math(EXPR DNSSD_VERSION_MINOR "${_DNS_SD_H} % 10000 / 100") + math(EXPR DNSSD_VERSION_PATCH "${_DNS_SD_H} % 100") + set(DNSSD_VERSION "${DNSSD_VERSION_MAJOR}.${DNSSD_VERSION_MINOR}.${DNSSD_VERSION_PATCH}") + unset(_DNS_SD_H) + endif() + unset(_define_DNS_SD_H) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(DNSSD + FOUND_VAR DNSSD_FOUND + REQUIRED_VARS + DNSSD_LIBRARY + DNSSD_INCLUDE_DIR + VERSION_VAR DNSSD_VERSION +) + +if(DNSSD_FOUND) + set(DNSSD_LIBRARIES ${DNSSD_LIBRARY}) + set(DNSSD_INCLUDE_DIRS ${DNSSD_INCLUDE_DIR}) +endif() + +if(DNSSD_FOUND AND NOT TARGET DNSSD::DNSSD) + add_library(DNSSD::DNSSD UNKNOWN IMPORTED) + set_target_properties(DNSSD::DNSSD PROPERTIES + IMPORTED_LOCATION "${DNSSD_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${DNSSD_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced( + DNSSD_INCLUDE_DIR + DNSSD_LIBRARY +) + +unset(_DNSSD_PATHS) +unset(_DNSSD_INCLUDE_SUFFIX) +unset(_DNSSD_LIB_SUFFIX) +unset(_DNSSD_LIB) + +cmake_policy(POP) diff --git a/Development/third_party/cmake/README.md b/Development/third_party/cmake/README.md index 14f390203..11b068e73 100644 --- a/Development/third_party/cmake/README.md +++ b/Development/third_party/cmake/README.md @@ -13,10 +13,28 @@ Original source code: ## Catch ``catch_discover_tests`` -Copied from [Dynamic Catch test discovery in CMake](https://gist.github.com/garethsb/a01ed0dbd4977d439c16200640549935), which was inspired by [Dynamic Google Test Discovery in CMake 3.10](https://blog.kitware.com/dynamic-google-test-discovery-in-cmake-3-10/). +Copied from [Dynamic Catch test discovery in CMake](https://github.com/garethsb/CMake/commit/0ce435fa3cb99a5b9e08d4b34dc134514f5c715d), which was inspired by [Dynamic Google Test Discovery in CMake 3.10](https://blog.kitware.com/dynamic-google-test-discovery-in-cmake-3-10/). Original source code: - Adapted by [Gareth Sylvester-Bradley](https://github.com/garethsb) from [GoogleTest ``gtest_discover_tests``](https://gitlab.kitware.com/cmake/cmake/merge_requests/1056). - BSD 3-clause "New" or "Revised" License. - Copyright 2000-2017 Kitware, Inc. and Contributors. All rights reserved. + +## Find Modules + +Copied from [garethsb/CMake](https://github.com/garethsb/CMake). + +Original source code: + +- Licensed under the Apache License, Version 2.0. +- Copyright (c) 2021, NVIDIA CORPORATION. + +## CMake Provider for Conan + +Copied from [conan-io/cmake-conan](https://github.com/conan-io/cmake-conan). + +Original source code: + +- Licensed under the MIT License +- Copyright (c) 2019 JFrog diff --git a/Development/third_party/cmake/conan_provider.cmake b/Development/third_party/cmake/conan_provider.cmake new file mode 100644 index 000000000..c21ab38ab --- /dev/null +++ b/Development/third_party/cmake/conan_provider.cmake @@ -0,0 +1,627 @@ +set(CONAN_MINIMUM_VERSION 2.0.5) + + +function(detect_os OS OS_API_LEVEL OS_SDK OS_SUBSYSTEM OS_VERSION) + # it could be cross compilation + message(STATUS "CMake-Conan: cmake_system_name=${CMAKE_SYSTEM_NAME}") + if(CMAKE_SYSTEM_NAME AND NOT CMAKE_SYSTEM_NAME STREQUAL "Generic") + if(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + set(${OS} Macos PARENT_SCOPE) + elseif(CMAKE_SYSTEM_NAME STREQUAL "QNX") + set(${OS} Neutrino PARENT_SCOPE) + elseif(CMAKE_SYSTEM_NAME STREQUAL "CYGWIN") + set(${OS} Windows PARENT_SCOPE) + set(${OS_SUBSYSTEM} cygwin PARENT_SCOPE) + elseif(CMAKE_SYSTEM_NAME MATCHES "^MSYS") + set(${OS} Windows PARENT_SCOPE) + set(${OS_SUBSYSTEM} msys2 PARENT_SCOPE) + else() + set(${OS} ${CMAKE_SYSTEM_NAME} PARENT_SCOPE) + endif() + if(CMAKE_SYSTEM_NAME STREQUAL "Android") + if(DEFINED ANDROID_PLATFORM) + string(REGEX MATCH "[0-9]+" _OS_API_LEVEL ${ANDROID_PLATFORM}) + elseif(DEFINED CMAKE_SYSTEM_VERSION) + set(_OS_API_LEVEL ${CMAKE_SYSTEM_VERSION}) + endif() + message(STATUS "CMake-Conan: android api level=${_OS_API_LEVEL}") + set(${OS_API_LEVEL} ${_OS_API_LEVEL} PARENT_SCOPE) + endif() + if(CMAKE_SYSTEM_NAME MATCHES "Darwin|iOS|tvOS|watchOS") + # CMAKE_OSX_SYSROOT contains the full path to the SDK for MakeFile/Ninja + # generators, but just has the original input string for Xcode. + if(NOT IS_DIRECTORY ${CMAKE_OSX_SYSROOT}) + set(_OS_SDK ${CMAKE_OSX_SYSROOT}) + else() + if(CMAKE_OSX_SYSROOT MATCHES Simulator) + set(apple_platform_suffix simulator) + else() + set(apple_platform_suffix os) + endif() + if(CMAKE_OSX_SYSROOT MATCHES AppleTV) + set(_OS_SDK "appletv${apple_platform_suffix}") + elseif(CMAKE_OSX_SYSROOT MATCHES iPhone) + set(_OS_SDK "iphone${apple_platform_suffix}") + elseif(CMAKE_OSX_SYSROOT MATCHES Watch) + set(_OS_SDK "watch${apple_platform_suffix}") + endif() + endif() + if(DEFINED _OS_SDK) + message(STATUS "CMake-Conan: cmake_osx_sysroot=${CMAKE_OSX_SYSROOT}") + set(${OS_SDK} ${_OS_SDK} PARENT_SCOPE) + endif() + if(DEFINED CMAKE_OSX_DEPLOYMENT_TARGET) + message(STATUS "CMake-Conan: cmake_osx_deployment_target=${CMAKE_OSX_DEPLOYMENT_TARGET}") + set(${OS_VERSION} ${CMAKE_OSX_DEPLOYMENT_TARGET} PARENT_SCOPE) + endif() + endif() + endif() +endfunction() + + +function(detect_arch ARCH) + # CMAKE_OSX_ARCHITECTURES can contain multiple architectures, but Conan only supports one. + # Therefore this code only finds one. If the recipes support multiple architectures, the + # build will work. Otherwise, there will be a linker error for the missing architecture(s). + if(DEFINED CMAKE_OSX_ARCHITECTURES) + string(REPLACE " " ";" apple_arch_list "${CMAKE_OSX_ARCHITECTURES}") + list(LENGTH apple_arch_list apple_arch_count) + if(apple_arch_count GREATER 1) + message(WARNING "CMake-Conan: Multiple architectures detected, this will only work if Conan recipe(s) produce fat binaries.") + endif() + endif() + if(CMAKE_SYSTEM_NAME MATCHES "Darwin|iOS|tvOS|watchOS" AND NOT CMAKE_OSX_ARCHITECTURES STREQUAL "") + set(host_arch ${CMAKE_OSX_ARCHITECTURES}) + elseif(MSVC) + set(host_arch ${CMAKE_CXX_COMPILER_ARCHITECTURE_ID}) + else() + set(host_arch ${CMAKE_SYSTEM_PROCESSOR}) + endif() + if(host_arch MATCHES "aarch64|arm64|ARM64") + set(_ARCH armv8) + elseif(host_arch MATCHES "armv7|armv7-a|armv7l|ARMV7") + set(_ARCH armv7) + elseif(host_arch MATCHES armv7s) + set(_ARCH armv7s) + elseif(host_arch MATCHES "i686|i386|X86") + set(_ARCH x86) + elseif(host_arch MATCHES "AMD64|amd64|x86_64|x64") + set(_ARCH x86_64) + endif() + message(STATUS "CMake-Conan: cmake_system_processor=${_ARCH}") + set(${ARCH} ${_ARCH} PARENT_SCOPE) +endfunction() + + +function(detect_cxx_standard CXX_STANDARD) + set(${CXX_STANDARD} ${CMAKE_CXX_STANDARD} PARENT_SCOPE) + if(CMAKE_CXX_EXTENSIONS) + set(${CXX_STANDARD} "gnu${CMAKE_CXX_STANDARD}" PARENT_SCOPE) + endif() +endfunction() + + +macro(detect_gnu_libstdcxx) + # _CONAN_IS_GNU_LIBSTDCXX true if GNU libstdc++ + check_cxx_source_compiles(" + #include + #if !defined(__GLIBCXX__) && !defined(__GLIBCPP__) + static_assert(false); + #endif + int main(){}" _CONAN_IS_GNU_LIBSTDCXX) + + # _CONAN_GNU_LIBSTDCXX_IS_CXX11_ABI true if C++11 ABI + check_cxx_source_compiles(" + #include + static_assert(sizeof(std::string) != sizeof(void*), \"using libstdc++\"); + int main () {}" _CONAN_GNU_LIBSTDCXX_IS_CXX11_ABI) + + set(_CONAN_GNU_LIBSTDCXX_SUFFIX "") + if(_CONAN_GNU_LIBSTDCXX_IS_CXX11_ABI) + set(_CONAN_GNU_LIBSTDCXX_SUFFIX "11") + endif() + unset (_CONAN_GNU_LIBSTDCXX_IS_CXX11_ABI) +endmacro() + + +macro(detect_libcxx) + # _CONAN_IS_LIBCXX true if LLVM libc++ + check_cxx_source_compiles(" + #include + #if !defined(_LIBCPP_VERSION) + static_assert(false); + #endif + int main(){}" _CONAN_IS_LIBCXX) +endmacro() + + +function(detect_lib_cxx LIB_CXX) + if(CMAKE_SYSTEM_NAME STREQUAL "Android") + message(STATUS "CMake-Conan: android_stl=${CMAKE_ANDROID_STL_TYPE}") + set(${LIB_CXX} ${CMAKE_ANDROID_STL_TYPE} PARENT_SCOPE) + return() + endif() + + include(CheckCXXSourceCompiles) + + if(CMAKE_CXX_COMPILER_ID MATCHES "GNU") + detect_gnu_libstdcxx() + set(${LIB_CXX} "libstdc++${_CONAN_GNU_LIBSTDCXX_SUFFIX}" PARENT_SCOPE) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "AppleClang") + set(${LIB_CXX} "libc++" PARENT_SCOPE) + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang" AND NOT CMAKE_SYSTEM_NAME MATCHES "Windows") + # Check for libc++ + detect_libcxx() + if(_CONAN_IS_LIBCXX) + set(${LIB_CXX} "libc++" PARENT_SCOPE) + return() + endif() + + # Check for libstdc++ + detect_gnu_libstdcxx() + if(_CONAN_IS_GNU_LIBSTDCXX) + set(${LIB_CXX} "libstdc++${_CONAN_GNU_LIBSTDCXX_SUFFIX}" PARENT_SCOPE) + return() + endif() + + # TODO: it would be an error if we reach this point + elseif(CMAKE_CXX_COMPILER_ID MATCHES "MSVC") + # Do nothing - compiler.runtime and compiler.runtime_type + # should be handled separately: https://github.com/conan-io/cmake-conan/pull/516 + return() + else() + # TODO: unable to determine, ask user to provide a full profile file instead + endif() +endfunction() + + +function(detect_compiler COMPILER COMPILER_VERSION COMPILER_RUNTIME COMPILER_RUNTIME_TYPE) + if(DEFINED CMAKE_CXX_COMPILER_ID) + set(_COMPILER ${CMAKE_CXX_COMPILER_ID}) + set(_COMPILER_VERSION ${CMAKE_CXX_COMPILER_VERSION}) + else() + if(NOT DEFINED CMAKE_C_COMPILER_ID) + message(FATAL_ERROR "C or C++ compiler not defined") + endif() + set(_COMPILER ${CMAKE_C_COMPILER_ID}) + set(_COMPILER_VERSION ${CMAKE_C_COMPILER_VERSION}) + endif() + + message(STATUS "CMake-Conan: CMake compiler=${_COMPILER}") + message(STATUS "CMake-Conan: CMake compiler version=${_COMPILER_VERSION}") + + if(_COMPILER MATCHES MSVC) + set(_COMPILER "msvc") + string(SUBSTRING ${MSVC_VERSION} 0 3 _COMPILER_VERSION) + # Configure compiler.runtime and compiler.runtime_type settings for MSVC + if(CMAKE_MSVC_RUNTIME_LIBRARY) + set(_msvc_runtime_library ${CMAKE_MSVC_RUNTIME_LIBRARY}) + else() + set(_msvc_runtime_library MultiThreaded$<$:Debug>DLL) # default value documented by CMake + endif() + + set(_KNOWN_MSVC_RUNTIME_VALUES "") + list(APPEND _KNOWN_MSVC_RUNTIME_VALUES MultiThreaded MultiThreadedDLL) + list(APPEND _KNOWN_MSVC_RUNTIME_VALUES MultiThreadedDebug MultiThreadedDebugDLL) + list(APPEND _KNOWN_MSVC_RUNTIME_VALUES MultiThreaded$<$:Debug> MultiThreaded$<$:Debug>DLL) + + # only accept the 6 possible values, otherwise we don't don't know to map this + if(NOT _msvc_runtime_library IN_LIST _KNOWN_MSVC_RUNTIME_VALUES) + message(FATAL_ERROR "CMake-Conan: unable to map MSVC runtime: ${_msvc_runtime_library} to Conan settings") + endif() + + # Runtime is "dynamic" in all cases if it ends in DLL + if(_msvc_runtime_library MATCHES ".*DLL$") + set(_COMPILER_RUNTIME "dynamic") + else() + set(_COMPILER_RUNTIME "static") + endif() + message(STATUS "CMake-Conan: CMake compiler.runtime=${_COMPILER_RUNTIME}") + + # Only define compiler.runtime_type when explicitly requested + # If a generator expression is used, let Conan handle it conditional on build_type + if(NOT _msvc_runtime_library MATCHES ":Debug>") + if(_msvc_runtime_library MATCHES "Debug") + set(_COMPILER_RUNTIME_TYPE "Debug") + else() + set(_COMPILER_RUNTIME_TYPE "Release") + endif() + message(STATUS "CMake-Conan: CMake compiler.runtime_type=${_COMPILER_RUNTIME_TYPE}") + endif() + + unset(_KNOWN_MSVC_RUNTIME_VALUES) + + elseif(_COMPILER MATCHES AppleClang) + set(_COMPILER "apple-clang") + string(REPLACE "." ";" VERSION_LIST ${CMAKE_CXX_COMPILER_VERSION}) + list(GET VERSION_LIST 0 _COMPILER_VERSION) + elseif(_COMPILER MATCHES Clang) + set(_COMPILER "clang") + string(REPLACE "." ";" VERSION_LIST ${CMAKE_CXX_COMPILER_VERSION}) + list(GET VERSION_LIST 0 _COMPILER_VERSION) + elseif(_COMPILER MATCHES GNU) + set(_COMPILER "gcc") + string(REPLACE "." ";" VERSION_LIST ${CMAKE_CXX_COMPILER_VERSION}) + list(GET VERSION_LIST 0 _COMPILER_VERSION) + endif() + + message(STATUS "CMake-Conan: [settings] compiler=${_COMPILER}") + message(STATUS "CMake-Conan: [settings] compiler.version=${_COMPILER_VERSION}") + if (_COMPILER_RUNTIME) + message(STATUS "CMake-Conan: [settings] compiler.runtime=${_COMPILER_RUNTIME}") + endif() + if (_COMPILER_RUNTIME_TYPE) + message(STATUS "CMake-Conan: [settings] compiler.runtime_type=${_COMPILER_RUNTIME_TYPE}") + endif() + + set(${COMPILER} ${_COMPILER} PARENT_SCOPE) + set(${COMPILER_VERSION} ${_COMPILER_VERSION} PARENT_SCOPE) + set(${COMPILER_RUNTIME} ${_COMPILER_RUNTIME} PARENT_SCOPE) + set(${COMPILER_RUNTIME_TYPE} ${_COMPILER_RUNTIME_TYPE} PARENT_SCOPE) +endfunction() + + +function(detect_build_type BUILD_TYPE) + get_property(_MULTICONFIG_GENERATOR GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + if(NOT _MULTICONFIG_GENERATOR) + # Only set when we know we are in a single-configuration generator + # Note: we may want to fail early if `CMAKE_BUILD_TYPE` is not defined + set(${BUILD_TYPE} ${CMAKE_BUILD_TYPE} PARENT_SCOPE) + endif() +endfunction() + +macro(set_conan_compiler_if_appleclang lang command output_variable) + if(CMAKE_${lang}_COMPILER_ID STREQUAL "AppleClang") + execute_process(COMMAND xcrun --find ${command} + OUTPUT_VARIABLE _xcrun_out OUTPUT_STRIP_TRAILING_WHITESPACE) + cmake_path(GET _xcrun_out PARENT_PATH _xcrun_toolchain_path) + cmake_path(GET CMAKE_${lang}_COMPILER PARENT_PATH _compiler_parent_path) + if ("${_xcrun_toolchain_path}" STREQUAL "${_compiler_parent_path}") + set(${output_variable} "") + endif() + unset(_xcrun_out) + unset(_xcrun_toolchain_path) + unset(_compiler_parent_path) + endif() +endmacro() + + +macro(append_compiler_executables_configuration) + set(_conan_c_compiler "") + set(_conan_cpp_compiler "") + if(CMAKE_C_COMPILER) + set(_conan_c_compiler "\"c\":\"${CMAKE_C_COMPILER}\",") + set_conan_compiler_if_appleclang(C cc _conan_c_compiler) + else() + message(WARNING "CMake-Conan: The C compiler is not defined. " + "Please define CMAKE_C_COMPILER or enable the C language.") + endif() + if(CMAKE_CXX_COMPILER) + set(_conan_cpp_compiler "\"cpp\":\"${CMAKE_CXX_COMPILER}\"") + set_conan_compiler_if_appleclang(CXX c++ _conan_cpp_compiler) + else() + message(WARNING "CMake-Conan: The C++ compiler is not defined. " + "Please define CMAKE_CXX_COMPILER or enable the C++ language.") + endif() + + if(NOT "x${_conan_c_compiler}${_conan_cpp_compiler}" STREQUAL "x") + string(APPEND PROFILE "tools.build:compiler_executables={${_conan_c_compiler}${_conan_cpp_compiler}}\n") + endif() + unset(_conan_c_compiler) + unset(_conan_cpp_compiler) +endmacro() + + +function(detect_host_profile output_file) + detect_os(MYOS MYOS_API_LEVEL MYOS_SDK MYOS_SUBSYSTEM MYOS_VERSION) + detect_arch(MYARCH) + detect_compiler(MYCOMPILER MYCOMPILER_VERSION MYCOMPILER_RUNTIME MYCOMPILER_RUNTIME_TYPE) + detect_cxx_standard(MYCXX_STANDARD) + detect_lib_cxx(MYLIB_CXX) + detect_build_type(MYBUILD_TYPE) + + set(PROFILE "") + string(APPEND PROFILE "[settings]\n") + if(MYARCH) + string(APPEND PROFILE arch=${MYARCH} "\n") + endif() + if(MYOS) + string(APPEND PROFILE os=${MYOS} "\n") + endif() + if(MYOS_API_LEVEL) + string(APPEND PROFILE os.api_level=${MYOS_API_LEVEL} "\n") + endif() + if(MYOS_VERSION) + string(APPEND PROFILE os.version=${MYOS_VERSION} "\n") + endif() + if(MYOS_SDK) + string(APPEND PROFILE os.sdk=${MYOS_SDK} "\n") + endif() + if(MYOS_SUBSYSTEM) + string(APPEND PROFILE os.subsystem=${MYOS_SUBSYSTEM} "\n") + endif() + if(MYCOMPILER) + string(APPEND PROFILE compiler=${MYCOMPILER} "\n") + endif() + if(MYCOMPILER_VERSION) + string(APPEND PROFILE compiler.version=${MYCOMPILER_VERSION} "\n") + endif() + if(MYCOMPILER_RUNTIME) + string(APPEND PROFILE compiler.runtime=${MYCOMPILER_RUNTIME} "\n") + endif() + if(MYCOMPILER_RUNTIME_TYPE) + string(APPEND PROFILE compiler.runtime_type=${MYCOMPILER_RUNTIME_TYPE} "\n") + endif() + if(MYCXX_STANDARD) + string(APPEND PROFILE compiler.cppstd=${MYCXX_STANDARD} "\n") + endif() + if(MYLIB_CXX) + string(APPEND PROFILE compiler.libcxx=${MYLIB_CXX} "\n") + endif() + if(MYBUILD_TYPE) + string(APPEND PROFILE "build_type=${MYBUILD_TYPE}\n") + endif() + + if(NOT DEFINED output_file) + set(_FN "${CMAKE_BINARY_DIR}/profile") + else() + set(_FN ${output_file}) + endif() + + string(APPEND PROFILE "[conf]\n") + string(APPEND PROFILE "tools.cmake.cmaketoolchain:generator=${CMAKE_GENERATOR}\n") + + # propagate compilers via profile + append_compiler_executables_configuration() + + if(MYOS STREQUAL "Android") + string(APPEND PROFILE "tools.android:ndk_path=${CMAKE_ANDROID_NDK}\n") + endif() + + message(STATUS "CMake-Conan: Creating profile ${_FN}") + file(WRITE ${_FN} ${PROFILE}) + message(STATUS "CMake-Conan: Profile: \n${PROFILE}") +endfunction() + + +function(conan_profile_detect_default) + message(STATUS "CMake-Conan: Checking if a default profile exists") + execute_process(COMMAND ${CONAN_COMMAND} profile path default + RESULT_VARIABLE return_code + OUTPUT_VARIABLE conan_stdout + ERROR_VARIABLE conan_stderr + ECHO_ERROR_VARIABLE # show the text output regardless + ECHO_OUTPUT_VARIABLE + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + if(NOT ${return_code} EQUAL "0") + message(STATUS "CMake-Conan: The default profile doesn't exist, detecting it.") + execute_process(COMMAND ${CONAN_COMMAND} profile detect + RESULT_VARIABLE return_code + OUTPUT_VARIABLE conan_stdout + ERROR_VARIABLE conan_stderr + ECHO_ERROR_VARIABLE # show the text output regardless + ECHO_OUTPUT_VARIABLE + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + endif() +endfunction() + + +function(conan_install) + cmake_parse_arguments(ARGS CONAN_ARGS ${ARGN}) + set(CONAN_OUTPUT_FOLDER ${CMAKE_BINARY_DIR}/conan) + # Invoke "conan install" with the provided arguments + set(CONAN_ARGS ${CONAN_ARGS} -of=${CONAN_OUTPUT_FOLDER}) + message(STATUS "CMake-Conan: conan install ${CMAKE_SOURCE_DIR} ${CONAN_ARGS} ${ARGN}") + + + # In case there was not a valid cmake executable in the PATH, we inject the + # same we used to invoke the provider to the PATH + if(DEFINED PATH_TO_CMAKE_BIN) + set(_OLD_PATH $ENV{PATH}) + set(ENV{PATH} "$ENV{PATH}:${PATH_TO_CMAKE_BIN}") + endif() + + execute_process(COMMAND ${CONAN_COMMAND} install ${CMAKE_SOURCE_DIR} ${CONAN_ARGS} ${ARGN} --format=json + RESULT_VARIABLE return_code + OUTPUT_VARIABLE conan_stdout + ERROR_VARIABLE conan_stderr + ECHO_ERROR_VARIABLE # show the text output regardless + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + + if(DEFINED PATH_TO_CMAKE_BIN) + set(ENV{PATH} "${_OLD_PATH}") + endif() + + if(NOT "${return_code}" STREQUAL "0") + message(FATAL_ERROR "Conan install failed='${return_code}'") + else() + # the files are generated in a folder that depends on the layout used, if + # one is specified, but we don't know a priori where this is. + # TODO: this can be made more robust if Conan can provide this in the json output + string(JSON CONAN_GENERATORS_FOLDER GET ${conan_stdout} graph nodes 0 generators_folder) + cmake_path(CONVERT ${CONAN_GENERATORS_FOLDER} TO_CMAKE_PATH_LIST CONAN_GENERATORS_FOLDER) + # message("conan stdout: ${conan_stdout}") + message(STATUS "CMake-Conan: CONAN_GENERATORS_FOLDER=${CONAN_GENERATORS_FOLDER}") + set_property(GLOBAL PROPERTY CONAN_GENERATORS_FOLDER "${CONAN_GENERATORS_FOLDER}") + # reconfigure on conanfile changes + string(JSON CONANFILE GET ${conan_stdout} graph nodes 0 label) + message(STATUS "CMake-Conan: CONANFILE=${CMAKE_SOURCE_DIR}/${CONANFILE}") + set_property(DIRECTORY ${CMAKE_SOURCE_DIR} APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/${CONANFILE}") + # success + set_property(GLOBAL PROPERTY CONAN_INSTALL_SUCCESS TRUE) + endif() +endfunction() + + +function(conan_get_version conan_command conan_current_version) + execute_process( + COMMAND ${conan_command} --version + OUTPUT_VARIABLE conan_output + RESULT_VARIABLE conan_result + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(conan_result) + message(FATAL_ERROR "CMake-Conan: Error when trying to run Conan") + endif() + + string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" conan_version ${conan_output}) + set(${conan_current_version} ${conan_version} PARENT_SCOPE) +endfunction() + + +function(conan_version_check) + set(options ) + set(oneValueArgs MINIMUM CURRENT) + set(multiValueArgs ) + cmake_parse_arguments(CONAN_VERSION_CHECK + "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + if(NOT CONAN_VERSION_CHECK_MINIMUM) + message(FATAL_ERROR "CMake-Conan: Required parameter MINIMUM not set!") + endif() + if(NOT CONAN_VERSION_CHECK_CURRENT) + message(FATAL_ERROR "CMake-Conan: Required parameter CURRENT not set!") + endif() + + if(CONAN_VERSION_CHECK_CURRENT VERSION_LESS CONAN_VERSION_CHECK_MINIMUM) + message(FATAL_ERROR "CMake-Conan: Conan version must be ${CONAN_VERSION_CHECK_MINIMUM} or later") + endif() +endfunction() + + +macro(construct_profile_argument argument_variable profile_list) + set(${argument_variable} "") + if("${profile_list}" STREQUAL "CONAN_HOST_PROFILE") + set(_arg_flag "--profile:host=") + elseif("${profile_list}" STREQUAL "CONAN_BUILD_PROFILE") + set(_arg_flag "--profile:build=") + endif() + + set(_profile_list "${${profile_list}}") + list(TRANSFORM _profile_list REPLACE "auto-cmake" "${CMAKE_BINARY_DIR}/conan_host_profile") + list(TRANSFORM _profile_list PREPEND ${_arg_flag}) + set(${argument_variable} ${_profile_list}) + + unset(_arg_flag) + unset(_profile_list) +endmacro() + + +macro(conan_provide_dependency method package_name) + set_property(GLOBAL PROPERTY CONAN_PROVIDE_DEPENDENCY_INVOKED TRUE) + get_property(_conan_install_success GLOBAL PROPERTY CONAN_INSTALL_SUCCESS) + if(NOT _conan_install_success) + find_program(CONAN_COMMAND "conan" REQUIRED) + conan_get_version(${CONAN_COMMAND} CONAN_CURRENT_VERSION) + conan_version_check(MINIMUM ${CONAN_MINIMUM_VERSION} CURRENT ${CONAN_CURRENT_VERSION}) + message(STATUS "CMake-Conan: first find_package() found. Installing dependencies with Conan") + if("default" IN_LIST CONAN_HOST_PROFILE OR "default" IN_LIST CONAN_BUILD_PROFILE) + conan_profile_detect_default() + endif() + if("auto-cmake" IN_LIST CONAN_HOST_PROFILE) + detect_host_profile(${CMAKE_BINARY_DIR}/conan_host_profile) + endif() + construct_profile_argument(_host_profile_flags CONAN_HOST_PROFILE) + construct_profile_argument(_build_profile_flags CONAN_BUILD_PROFILE) + if(EXISTS "${CMAKE_SOURCE_DIR}/conanfile.py") + file(READ "${CMAKE_SOURCE_DIR}/conanfile.py" outfile) + if(NOT "${outfile}" MATCHES ".*CMakeDeps.*") + message(WARNING "Cmake-conan: CMakeDeps generator was not defined in the conanfile") + endif() + set(generator "") + elseif (EXISTS "${CMAKE_SOURCE_DIR}/conanfile.txt") + file(READ "${CMAKE_SOURCE_DIR}/conanfile.txt" outfile) + if(NOT "${outfile}" MATCHES ".*CMakeDeps.*") + message(WARNING "Cmake-conan: CMakeDeps generator was not defined in the conanfile. " + "Please define the generator as it will be mandatory in the future") + endif() + set(generator "-g;CMakeDeps") + endif() + get_property(_multiconfig_generator GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) + if(NOT _multiconfig_generator) + message(STATUS "CMake-Conan: Installing single configuration ${CMAKE_BUILD_TYPE}") + conan_install(${_host_profile_flags} ${_build_profile_flags} ${CONAN_INSTALL_ARGS} ${generator}) + else() + message(STATUS "CMake-Conan: Installing both Debug and Release") + conan_install(${_host_profile_flags} ${_build_profile_flags} -s build_type=Release ${CONAN_INSTALL_ARGS} ${generator}) + conan_install(${_host_profile_flags} ${_build_profile_flags} -s build_type=Debug ${CONAN_INSTALL_ARGS} ${generator}) + endif() + unset(_host_profile_flags) + unset(_build_profile_flags) + unset(_multiconfig_generator) + unset(_conan_install_success) + else() + message(STATUS "CMake-Conan: find_package(${ARGV1}) found, 'conan install' already ran") + unset(_conan_install_success) + endif() + + get_property(_conan_generators_folder GLOBAL PROPERTY CONAN_GENERATORS_FOLDER) + + # Ensure that we consider Conan-provided packages ahead of any other, + # irrespective of other settings that modify the search order or search paths + # This follows the guidelines from the find_package documentation + # (https://cmake.org/cmake/help/latest/command/find_package.html): + # find_package ( PATHS paths... NO_DEFAULT_PATH) + # find_package () + + # Filter out `REQUIRED` from the argument list, as the first call may fail + set(_find_args_${package_name} "${ARGN}") + list(REMOVE_ITEM _find_args_${package_name} "REQUIRED") + if(NOT "MODULE" IN_LIST _find_args_${package_name}) + find_package(${package_name} ${_find_args_${package_name}} BYPASS_PROVIDER PATHS "${_conan_generators_folder}" NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) + unset(_find_args_${package_name}) + endif() + + # Invoke find_package a second time - if the first call succeeded, + # this will simply reuse the result. If not, fall back to CMake default search + # behaviour, also allowing modules to be searched. + if(NOT ${package_name}_FOUND) + list(FIND CMAKE_MODULE_PATH "${_conan_generators_folder}" _index) + if(_index EQUAL -1) + list(PREPEND CMAKE_MODULE_PATH "${_conan_generators_folder}") + endif() + unset(_index) + find_package(${package_name} ${ARGN} BYPASS_PROVIDER) + list(REMOVE_ITEM CMAKE_MODULE_PATH "${_conan_generators_folder}") + endif() +endmacro() + + +cmake_language( + SET_DEPENDENCY_PROVIDER conan_provide_dependency + SUPPORTED_METHODS FIND_PACKAGE +) + + +macro(conan_provide_dependency_check) + set(_CONAN_PROVIDE_DEPENDENCY_INVOKED FALSE) + get_property(_CONAN_PROVIDE_DEPENDENCY_INVOKED GLOBAL PROPERTY CONAN_PROVIDE_DEPENDENCY_INVOKED) + if(NOT _CONAN_PROVIDE_DEPENDENCY_INVOKED) + message(WARNING "Conan is correctly configured as dependency provider, " + "but Conan has not been invoked. Please add at least one " + "call to `find_package()`.") + if(DEFINED CONAN_COMMAND) + # supress warning in case `CONAN_COMMAND` was specified but unused. + set(_CONAN_COMMAND ${CONAN_COMMAND}) + unset(_CONAN_COMMAND) + endif() + endif() + unset(_CONAN_PROVIDE_DEPENDENCY_INVOKED) +endmacro() + + +# Add a deferred call at the end of processing the top-level directory +# to check if the dependency provider was invoked at all. +cmake_language(DEFER DIRECTORY "${CMAKE_SOURCE_DIR}" CALL conan_provide_dependency_check) + +# Configurable variables for Conan profiles +set(CONAN_HOST_PROFILE "default;auto-cmake" CACHE STRING "Conan host profile") +set(CONAN_BUILD_PROFILE "default" CACHE STRING "Conan build profile") +set(CONAN_INSTALL_ARGS "--build=missing" CACHE STRING "Command line arguments for conan install") + +find_program(_cmake_program NAMES cmake NO_PACKAGE_ROOT_PATH NO_CMAKE_PATH NO_CMAKE_ENVIRONMENT_PATH NO_CMAKE_SYSTEM_PATH NO_CMAKE_FIND_ROOT_PATH) +if(NOT _cmake_program) + get_filename_component(PATH_TO_CMAKE_BIN "${CMAKE_COMMAND}" DIRECTORY) + set(PATH_TO_CMAKE_BIN "${PATH_TO_CMAKE_BIN}" CACHE INTERNAL "Path where the CMake executable is") +endif() + diff --git a/Development/third_party/cmake/safeguards.cmake b/Development/third_party/cmake/safeguards.cmake index af753038d..a3e9b01f6 100644 --- a/Development/third_party/cmake/safeguards.cmake +++ b/Development/third_party/cmake/safeguards.cmake @@ -3,7 +3,7 @@ if(NOT CMAKE_BUILD_TYPE) endif() if(${PROJECT_SOURCE_DIR} STREQUAL ${PROJECT_BINARY_DIR}) - message(FATAL_ERROR "In-source builds not allowed. Please make a new directory (called a build directory) and run CMake from there.") + message(WARNING "In-source builds not recommended. Please make a new directory (called a build directory) and run CMake from there.") endif() string(TOLOWER "${CMAKE_BUILD_TYPE}" cmake_build_type_tolower) diff --git a/Development/third_party/nmos-discovery-registration/README.md b/Development/third_party/is-04/README.md similarity index 59% rename from Development/third_party/nmos-discovery-registration/README.md rename to Development/third_party/is-04/README.md index fcd0d595c..40ab1f0f7 100644 --- a/Development/third_party/nmos-discovery-registration/README.md +++ b/Development/third_party/is-04/README.md @@ -1,6 +1,6 @@ # AMWA IS-04 NMOS Discovery and Registration Specification -This directory contains files from the [AMWA IS-04 NMOS Discovery and Registration Specification](https://github.com/AMWA-TV/nmos-discovery-registration), in particular tagged versions of the JSON schemas used by the API specifications. +This directory contains files from the [AMWA IS-04 NMOS Discovery and Registration Specification](https://github.com/AMWA-TV/is-04), in particular tagged versions of the JSON schemas used by the API specifications. Original source code: diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/device.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/device.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/device.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/device.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/devices.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/devices.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/devices.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/devices.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/error.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/error.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/error.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/error.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/flow.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/flow.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/flow.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/flow.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/flows.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/flows.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/flows.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/flows.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/node.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/node.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/node.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/node.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/nodeapi-base.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/nodeapi-base.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/nodeapi-base.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/nodeapi-base.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/nodeapi-receiver-target.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/nodeapi-receiver-target.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/nodeapi-receiver-target.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/nodeapi-receiver-target.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/nodes.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/nodes.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/nodes.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/nodes.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/queryapi-base.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/queryapi-base.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/queryapi-base.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/queryapi-base.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/queryapi-subscription-response.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/queryapi-subscription-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/queryapi-subscription-response.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/queryapi-subscription-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/queryapi-subscriptions-response.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/queryapi-subscriptions-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/queryapi-subscriptions-response.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/queryapi-subscriptions-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/queryapi-v1.0-subscriptions-post-request.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/queryapi-v1.0-subscriptions-post-request.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/queryapi-v1.0-subscriptions-post-request.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/queryapi-v1.0-subscriptions-post-request.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/queryapi-v1.0-subscriptions-websocket.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/queryapi-v1.0-subscriptions-websocket.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/queryapi-v1.0-subscriptions-websocket.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/queryapi-v1.0-subscriptions-websocket.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/receiver.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/receiver.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/receiver.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/receiver.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/receivers.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/receivers.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/receivers.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/receivers.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/registrationapi-base.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/registrationapi-base.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/registrationapi-base.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/registrationapi-base.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/registrationapi-health-response.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/registrationapi-health-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/registrationapi-health-response.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/registrationapi-health-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/registrationapi-resource-response.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/registrationapi-resource-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/registrationapi-resource-response.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/registrationapi-resource-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/registrationapi-v1.0-resource-post-request.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/registrationapi-v1.0-resource-post-request.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/registrationapi-v1.0-resource-post-request.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/registrationapi-v1.0-resource-post-request.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/sender-target.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/sender-target.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/sender-target.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/sender-target.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/sender.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/sender.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/sender.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/sender.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/senders.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/senders.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/senders.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/senders.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/source.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/source.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/source.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/source.json diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/sources.json b/Development/third_party/is-04/v1.0.x/APIs/schemas/sources.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/sources.json rename to Development/third_party/is-04/v1.0.x/APIs/schemas/sources.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/clock_internal.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/clock_internal.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/clock_internal.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/clock_internal.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/clock_ptp.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/clock_ptp.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/clock_ptp.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/clock_ptp.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/device.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/device.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/device.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/device.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/devices.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/devices.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/devices.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/devices.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/error.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/error.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/error.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/error.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/flow.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/flow.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_audio.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/flow_audio.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_audio.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/flow_audio.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_audio_coded.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/flow_audio_coded.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_audio_coded.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/flow_audio_coded.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_audio_raw.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/flow_audio_raw.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_audio_raw.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/flow_audio_raw.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_core.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/flow_core.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_core.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/flow_core.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_data.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/flow_data.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_data.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/flow_data.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_mux.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/flow_mux.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_mux.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/flow_mux.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_sdianc_data.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/flow_sdianc_data.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_sdianc_data.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/flow_sdianc_data.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_video.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/flow_video.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_video.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/flow_video.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_video_coded.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/flow_video_coded.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_video_coded.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/flow_video_coded.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_video_raw.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/flow_video_raw.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flow_video_raw.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/flow_video_raw.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flows.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/flows.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/flows.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/flows.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/node.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/node.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/node.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/node.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/nodeapi-base.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/nodeapi-base.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/nodeapi-base.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/nodeapi-base.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/nodeapi-receiver-target.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/nodeapi-receiver-target.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/nodeapi-receiver-target.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/nodeapi-receiver-target.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/nodes.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/nodes.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/nodes.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/nodes.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/queryapi-base.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/queryapi-base.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/queryapi-base.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/queryapi-base.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/queryapi-subscription-response.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/queryapi-subscription-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/queryapi-subscription-response.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/queryapi-subscription-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/queryapi-subscriptions-post-request.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/queryapi-subscriptions-post-request.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/queryapi-subscriptions-post-request.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/queryapi-subscriptions-post-request.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/queryapi-subscriptions-response.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/queryapi-subscriptions-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/queryapi-subscriptions-response.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/queryapi-subscriptions-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/queryapi-subscriptions-websocket.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/queryapi-subscriptions-websocket.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/queryapi-subscriptions-websocket.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/queryapi-subscriptions-websocket.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receiver.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/receiver.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receiver.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/receiver.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receiver_audio.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/receiver_audio.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receiver_audio.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/receiver_audio.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receiver_core.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/receiver_core.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receiver_core.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/receiver_core.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receiver_data.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/receiver_data.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receiver_data.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/receiver_data.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receiver_mux.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/receiver_mux.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receiver_mux.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/receiver_mux.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receiver_video.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/receiver_video.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receiver_video.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/receiver_video.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receivers.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/receivers.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/receivers.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/receivers.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/registrationapi-base.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/registrationapi-base.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/registrationapi-base.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/registrationapi-base.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/registrationapi-health-response.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/registrationapi-health-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/registrationapi-health-response.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/registrationapi-health-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/registrationapi-resource-post-request.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/registrationapi-resource-post-request.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/registrationapi-resource-post-request.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/registrationapi-resource-post-request.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/registrationapi-resource-response.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/registrationapi-resource-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/registrationapi-resource-response.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/registrationapi-resource-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/resource_core.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/resource_core.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/resource_core.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/resource_core.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/sender.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/sender.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/sender.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/sender.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/senders.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/senders.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/senders.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/senders.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/source.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/source.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/source.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/source.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/source_audio.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/source_audio.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/source_audio.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/source_audio.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/source_core.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/source_core.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/source_core.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/source_core.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/source_generic.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/source_generic.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/source_generic.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/source_generic.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/sources.json b/Development/third_party/is-04/v1.1.x/APIs/schemas/sources.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/sources.json rename to Development/third_party/is-04/v1.1.x/APIs/schemas/sources.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/clock_internal.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/clock_internal.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/clock_internal.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/clock_internal.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/clock_ptp.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/clock_ptp.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/clock_ptp.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/clock_ptp.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/device.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/device.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/device.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/device.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/devices.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/devices.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/devices.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/devices.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/error.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/error.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/error.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/error.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/flow.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/flow.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_audio.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/flow_audio.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_audio.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/flow_audio.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_audio_coded.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/flow_audio_coded.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_audio_coded.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/flow_audio_coded.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_audio_raw.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/flow_audio_raw.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_audio_raw.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/flow_audio_raw.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_core.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/flow_core.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_core.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/flow_core.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_data.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/flow_data.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_data.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/flow_data.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_mux.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/flow_mux.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_mux.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/flow_mux.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_sdianc_data.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/flow_sdianc_data.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_sdianc_data.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/flow_sdianc_data.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_video.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/flow_video.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_video.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/flow_video.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_video_coded.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/flow_video_coded.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_video_coded.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/flow_video_coded.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_video_raw.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/flow_video_raw.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flow_video_raw.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/flow_video_raw.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flows.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/flows.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/flows.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/flows.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/node.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/node.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/node.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/node.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/nodeapi-base.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/nodeapi-base.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/nodeapi-base.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/nodeapi-base.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/nodeapi-receiver-target.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/nodeapi-receiver-target.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/nodeapi-receiver-target.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/nodeapi-receiver-target.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/nodes.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/nodes.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/nodes.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/nodes.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/queryapi-base.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/queryapi-base.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/queryapi-base.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/queryapi-base.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/queryapi-subscription-response.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/queryapi-subscription-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/queryapi-subscription-response.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/queryapi-subscription-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/queryapi-subscriptions-post-request.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/queryapi-subscriptions-post-request.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/queryapi-subscriptions-post-request.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/queryapi-subscriptions-post-request.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/queryapi-subscriptions-response.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/queryapi-subscriptions-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/queryapi-subscriptions-response.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/queryapi-subscriptions-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/queryapi-subscriptions-websocket.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/queryapi-subscriptions-websocket.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/queryapi-subscriptions-websocket.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/queryapi-subscriptions-websocket.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receiver.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/receiver.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receiver.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/receiver.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receiver_audio.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/receiver_audio.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receiver_audio.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/receiver_audio.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receiver_core.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/receiver_core.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receiver_core.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/receiver_core.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receiver_data.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/receiver_data.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receiver_data.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/receiver_data.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receiver_mux.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/receiver_mux.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receiver_mux.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/receiver_mux.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receiver_video.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/receiver_video.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receiver_video.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/receiver_video.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receivers.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/receivers.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/receivers.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/receivers.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/registrationapi-base.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/registrationapi-base.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/registrationapi-base.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/registrationapi-base.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/registrationapi-health-response.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/registrationapi-health-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/registrationapi-health-response.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/registrationapi-health-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/registrationapi-resource-post-request.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/registrationapi-resource-post-request.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/registrationapi-resource-post-request.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/registrationapi-resource-post-request.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/registrationapi-resource-response.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/registrationapi-resource-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/registrationapi-resource-response.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/registrationapi-resource-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/resource_core.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/resource_core.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/resource_core.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/resource_core.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/sender.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/sender.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/sender.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/sender.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/senders.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/senders.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/senders.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/senders.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/source.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/source.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/source.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/source.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/source_audio.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/source_audio.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/source_audio.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/source_audio.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/source_core.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/source_core.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/source_core.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/source_core.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/source_generic.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/source_generic.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/source_generic.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/source_generic.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/sources.json b/Development/third_party/is-04/v1.2.x/APIs/schemas/sources.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/sources.json rename to Development/third_party/is-04/v1.2.x/APIs/schemas/sources.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/clock_internal.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/clock_internal.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/clock_internal.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/clock_internal.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/clock_ptp.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/clock_ptp.json similarity index 90% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/clock_ptp.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/clock_ptp.json index 811b792f7..0f59cc1d3 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/clock_ptp.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/clock_ptp.json @@ -41,7 +41,7 @@ "pattern": "^[0-9a-f]{2}-[0-9a-f]{2}-[0-9a-f]{2}-[0-9a-f]{2}-[0-9a-f]{2}-[0-9a-f]{2}-[0-9a-f]{2}-[0-9a-f]{2}$" }, "locked": { - "description": "Lock state of this clock to the external reference. If true, this device is slaved, otherwise it has no defined relationship to the external reference", + "description": "Lock state of this clock to the external reference. If true, this device follows the external reference, otherwise it has no defined relationship to the external reference", "type": "boolean" } } diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/device.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/device.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/device.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/device.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/devices.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/devices.json similarity index 93% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/devices.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/devices.json index 5bcc641df..974373dfd 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/devices.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/devices.json @@ -6,6 +6,5 @@ "items": { "$ref": "device.json" }, - "minItems": 0, "uniqueItems": true } diff --git a/Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/error.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/error.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.0.x/APIs/schemas/error.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/error.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flow.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flow.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_audio.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flow_audio.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_audio.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flow_audio.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_audio_coded.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flow_audio_coded.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_audio_coded.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flow_audio_coded.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_audio_raw.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flow_audio_raw.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_audio_raw.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flow_audio_raw.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_core.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flow_core.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_core.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flow_core.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_data.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flow_data.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_data.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flow_data.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_json_data.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flow_json_data.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_json_data.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flow_json_data.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_mux.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flow_mux.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_mux.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flow_mux.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_sdianc_data.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flow_sdianc_data.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_sdianc_data.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flow_sdianc_data.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_video.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flow_video.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_video.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flow_video.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_video_coded.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flow_video_coded.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_video_coded.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flow_video_coded.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_video_raw.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flow_video_raw.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flow_video_raw.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flow_video_raw.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flows.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/flows.json similarity index 92% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flows.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/flows.json index 5360f09f5..d8efceab8 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/flows.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/flows.json @@ -6,6 +6,5 @@ "items": { "$ref": "flow.json" }, - "minItems": 0, "uniqueItems": true } diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/node.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/node.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/node.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/node.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/nodeapi-base.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/nodeapi-base.json similarity index 81% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/nodeapi-base.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/nodeapi-base.json index 61dcdec80..9a20f8549 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/nodeapi-base.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/nodeapi-base.json @@ -12,9 +12,9 @@ "devices/", "senders/", "receivers/" - ], - "minItems": 6, - "maxItems": 6, - "uniqueItems": true - } + ] + }, + "minItems": 6, + "maxItems": 6, + "uniqueItems": true } diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/nodeapi-receiver-target.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/nodeapi-receiver-target.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/nodeapi-receiver-target.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/nodeapi-receiver-target.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/nodes.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/nodes.json similarity index 92% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/nodes.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/nodes.json index 59e72d639..e289727cb 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/nodes.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/nodes.json @@ -6,6 +6,5 @@ "items": { "$ref": "node.json" }, - "minItems": 0, "uniqueItems": true } diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-base.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-base.json similarity index 82% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-base.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-base.json index 0cf5f72ae..3fb3b85f8 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-base.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-base.json @@ -13,9 +13,9 @@ "senders/", "receivers/", "subscriptions/" - ], - "minItems": 7, - "maxItems": 7, - "uniqueItems": true - } + ] + }, + "minItems": 7, + "maxItems": 7, + "uniqueItems": true } diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-subscription-response.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-subscription-response.json similarity index 96% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-subscription-response.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-subscription-response.json index 996fe9142..4eacbb1bc 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-subscription-response.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-subscription-response.json @@ -19,7 +19,7 @@ "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }, "ws_href": { - "description": "Address to connect to for the websocket subscription", + "description": "Address to connect to for the WebSocket subscription", "type": "string", "format": "uri" }, diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-subscriptions-post-request.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-subscriptions-post-request.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-subscriptions-post-request.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-subscriptions-post-request.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-subscriptions-response.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-subscriptions-response.json similarity index 93% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-subscriptions-response.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-subscriptions-response.json index fe2b561b2..eedc3616a 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-subscriptions-response.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-subscriptions-response.json @@ -6,6 +6,5 @@ "items": { "$ref": "queryapi-subscription-response.json" }, - "minItems": 0, "uniqueItems": true } diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-subscriptions-websocket.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-subscriptions-websocket.json similarity index 98% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-subscriptions-websocket.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-subscriptions-websocket.json index d944781a4..e12f0f1c1 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/queryapi-subscriptions-websocket.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/queryapi-subscriptions-websocket.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", - "description": "Describes a data Grain sent via the a Query API websocket subscription", + "description": "Describes a data Grain sent via the a Query API WebSocket subscription", "title": "Query API data Grain", "required": [ "grain_type", @@ -105,7 +105,7 @@ ] }, "topic": { - "description": "Query API topic which has been subscribed to using this websocket", + "description": "Query API topic which has been subscribed to using this WebSocket", "type": "string", "enum": [ "/", diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/receiver.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/receiver.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver_audio.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/receiver_audio.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver_audio.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/receiver_audio.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver_core.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/receiver_core.json similarity index 85% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver_core.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/receiver_core.json index 1b38e3671..07c9b5a9e 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver_core.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/receiver_core.json @@ -42,19 +42,19 @@ } }, "subscription": { - "description": "Object containing the 'sender_id' currently subscribed to.", + "description": "Object indicating how this Receiver is currently configured to receive data.", "type": "object", "required": ["sender_id", "active"], "properties": { "sender_id": { "type": ["string", "null"], - "description": "UUID of the Sender that this Receiver is currently subscribed to", + "description": "UUID of the Sender from which this Receiver is currently configured to receive data. Only set if it is active and receiving from an NMOS Sender; otherwise null.", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", "default": null }, "active": { "type": "boolean", - "description": "Receiver is enabled and configured with a Sender's connection parameters", + "description": "Receiver is enabled and configured to receive data", "default": false } } diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver_data.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/receiver_data.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver_data.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/receiver_data.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver_mux.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/receiver_mux.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver_mux.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/receiver_mux.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver_video.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/receiver_video.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receiver_video.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/receiver_video.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receivers.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/receivers.json similarity index 93% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receivers.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/receivers.json index d6503c993..f6dadfe4c 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/receivers.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/receivers.json @@ -6,6 +6,5 @@ "items": { "$ref": "receiver.json" }, - "minItems": 0, "uniqueItems": true } diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/registrationapi-base.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/registrationapi-base.json similarity index 78% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/registrationapi-base.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/registrationapi-base.json index 07bf137d1..6559bf681 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/registrationapi-base.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/registrationapi-base.json @@ -8,9 +8,9 @@ "enum": [ "resource/", "health/" - ], - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } + ] + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true } diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/registrationapi-health-response.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/registrationapi-health-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/registrationapi-health-response.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/registrationapi-health-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/registrationapi-resource-post-request.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/registrationapi-resource-post-request.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/registrationapi-resource-post-request.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/registrationapi-resource-post-request.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/registrationapi-resource-response.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/registrationapi-resource-response.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/registrationapi-resource-response.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/registrationapi-resource-response.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/resource_core.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/resource_core.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/resource_core.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/resource_core.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/sender.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/sender.json similarity index 88% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/sender.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/sender.json index f88d50e38..967da22ce 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/sender.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/sender.json @@ -61,13 +61,13 @@ } }, "subscription": { - "description": "Object containing the 'receiver_id' currently subscribed to (unicast only).", + "description": "Object indicating how this Sender is currently configured to send data.", "type": "object", "required": ["receiver_id", "active"], "properties": { "receiver_id": { "type": ["string", "null"], - "description": "UUID of the Receiver that this Sender is currently subscribed to", + "description": "UUID of the Receiver to which this Sender is currently configured to send data. Only set if it is active, uses a unicast push-based transport and is sending to an NMOS Receiver; otherwise null.", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", "default": null }, diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/senders.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/senders.json similarity index 93% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/senders.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/senders.json index e55bcf852..1ebd80a07 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/senders.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/senders.json @@ -6,6 +6,5 @@ "items": { "$ref": "sender.json" }, - "minItems": 0, "uniqueItems": true } diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/source.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/source.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/source.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/source.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/source_audio.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/source_audio.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/source_audio.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/source_audio.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/source_core.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/source_core.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/source_core.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/source_core.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/source_data.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/source_data.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/source_data.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/source_data.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/source_generic.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/source_generic.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/source_generic.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/source_generic.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/sources.json b/Development/third_party/is-04/v1.3.x/APIs/schemas/sources.json similarity index 93% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/sources.json rename to Development/third_party/is-04/v1.3.x/APIs/schemas/sources.json index df6a5e032..8c0302bba 100644 --- a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/sources.json +++ b/Development/third_party/is-04/v1.3.x/APIs/schemas/sources.json @@ -6,6 +6,5 @@ "items": { "$ref": "source.json" }, - "minItems": 0, "uniqueItems": true } diff --git a/Development/third_party/nmos-device-connection-management/README.md b/Development/third_party/is-05/README.md similarity index 58% rename from Development/third_party/nmos-device-connection-management/README.md rename to Development/third_party/is-05/README.md index 6088b97c7..594bebf46 100644 --- a/Development/third_party/nmos-device-connection-management/README.md +++ b/Development/third_party/is-05/README.md @@ -1,6 +1,6 @@ # AMWA IS-05 NMOS Device Connection Management Specification -This directory contains files from the [AMWA IS-05 NMOS Device Connection Management Specification](https://github.com/AMWA-TV/nmos-device-connection-management), in particular tagged versions of the JSON schemas used by the API specifications. +This directory contains files from the [AMWA IS-05 NMOS Device Connection Management Specification](https://github.com/AMWA-TV/is-05), in particular tagged versions of the JSON schemas used by the API specifications. Original source code: diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/connectionapi-base.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/connectionapi-base.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/connectionapi-base.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/connectionapi-base.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/connectionapi-bulk.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/connectionapi-bulk.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/connectionapi-bulk.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/connectionapi-bulk.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/connectionapi-receiver.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/connectionapi-receiver.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/connectionapi-receiver.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/connectionapi-receiver.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/connectionapi-sender.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/connectionapi-sender.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/connectionapi-sender.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/connectionapi-sender.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/connectionapi-single.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/connectionapi-single.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/connectionapi-single.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/connectionapi-single.json diff --git a/Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/error.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/error.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.1.x/APIs/schemas/error.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/error.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/sender-receiver-base.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/sender-receiver-base.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/sender-receiver-base.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/sender-receiver-base.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-activation-response-schema.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-activation-response-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-activation-response-schema.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-activation-response-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-activation-schema.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-activation-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-activation-schema.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-activation-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-bulk-receiver-post-schema.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-bulk-receiver-post-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-bulk-receiver-post-schema.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-bulk-receiver-post-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-bulk-response-schema.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-bulk-response-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-bulk-response-schema.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-bulk-response-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-bulk-sender-post-schema.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-bulk-sender-post-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-bulk-sender-post-schema.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-bulk-sender-post-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-constraints-schema.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-constraints-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-constraints-schema.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-constraints-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-receiver-response-schema.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-receiver-response-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-receiver-response-schema.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-receiver-response-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-receiver-stage-schema.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-receiver-stage-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-receiver-stage-schema.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-receiver-stage-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-sender-response-schema.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-sender-response-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-sender-response-schema.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-sender-response-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-sender-stage-schema.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-sender-stage-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0-sender-stage-schema.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0-sender-stage-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0_receiver_transport_params_dash.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0_receiver_transport_params_dash.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0_receiver_transport_params_dash.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0_receiver_transport_params_dash.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0_receiver_transport_params_rtp.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0_receiver_transport_params_rtp.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0_receiver_transport_params_rtp.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0_receiver_transport_params_rtp.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0_sender_transport_params_dash.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0_sender_transport_params_dash.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0_sender_transport_params_dash.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0_sender_transport_params_dash.json diff --git a/Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0_sender_transport_params_rtp.json b/Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0_sender_transport_params_rtp.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.0.x/APIs/schemas/v1.0_sender_transport_params_rtp.json rename to Development/third_party/is-05/v1.0.x/APIs/schemas/v1.0_sender_transport_params_rtp.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/activation-response-schema.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/activation-response-schema.json similarity index 77% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/activation-response-schema.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/activation-response-schema.json index 08390a921..5bb0f4f74 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/activation-response-schema.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/activation-response-schema.json @@ -32,7 +32,7 @@ }] }, "activation_time": { - "description": "String formatted TAI timestamp (:) indicating the absolute time the receiver will or did actually activate for scheduled activations, or the time activation occurred for immediate activations. On the staged endpoint this field returns to null once the activation is completed or when the resource is unlocked by setting the activation mode to null. For immediate activations on the staged endpoint this property will be the time the activation actually occurred in the response to the PATCH request, but null in response to any GET requests thereafter.", + "description": "String formatted TAI timestamp (:) indicating the absolute time the sender or receiver will or did actually activate for scheduled activations, or the time activation occurred for immediate activations. On the staged endpoint this field returns to null once the activation is completed or when the resource is unlocked by setting the activation mode to null. For immediate activations on the staged endpoint this property will be the time the activation actually occurred in the response to the PATCH request, but null in response to any GET requests thereafter.", "anyOf": [{ "type": "string", "pattern": "^[0-9]+:[0-9]+$" diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/activation-schema.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/activation-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/activation-schema.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/activation-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/bulk-receiver-post-schema.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/bulk-receiver-post-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/bulk-receiver-post-schema.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/bulk-receiver-post-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/bulk-response-schema.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/bulk-response-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/bulk-response-schema.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/bulk-response-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/bulk-sender-post-schema.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/bulk-sender-post-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/bulk-sender-post-schema.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/bulk-sender-post-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-base.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-base.json similarity index 78% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-base.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-base.json index 447af56fd..400106144 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-base.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-base.json @@ -8,9 +8,9 @@ "enum": [ "bulk/", "single/" - ], - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } + ] + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true } diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-bulk.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-bulk.json similarity index 79% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-bulk.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-bulk.json index 48e102639..03fc8f778 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-bulk.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-bulk.json @@ -8,9 +8,9 @@ "enum": [ "senders/", "receivers/" - ], - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } + ] + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true } diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-receiver.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-receiver.json similarity index 83% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-receiver.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-receiver.json index 409855a80..99cd2d347 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-receiver.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-receiver.json @@ -10,9 +10,9 @@ "staged/", "active/", "transporttype/" - ], - "minItems": 4, - "maxItems": 4, - "uniqueItems": true - } + ] + }, + "minItems": 4, + "maxItems": 4, + "uniqueItems": true } diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-sender.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-sender.json similarity index 84% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-sender.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-sender.json index d4ccc3914..8d072dbb7 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-sender.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-sender.json @@ -11,9 +11,9 @@ "active/", "transportfile/", "transporttype/" - ], - "minItems": 5, - "maxItems": 5, - "uniqueItems": true - } + ] + }, + "minItems": 5, + "maxItems": 5, + "uniqueItems": true } diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-single.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-single.json similarity index 79% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-single.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-single.json index f0502d15c..0d36e835f 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/connectionapi-single.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/connectionapi-single.json @@ -8,9 +8,9 @@ "enum": [ "senders/", "receivers/" - ], - "minItems": 2, - "maxItems": 2, - "uniqueItems": true - } + ] + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true } diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraint-schema.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/constraint-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraint-schema.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/constraint-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraints-schema-mqtt.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/constraints-schema-mqtt.json similarity index 88% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraints-schema-mqtt.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/constraints-schema-mqtt.json index 20abd4f36..1219dc19e 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraints-schema-mqtt.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/constraints-schema-mqtt.json @@ -2,6 +2,13 @@ "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "description": "Used to express the dynamic constraints on MQTT transport parameters. These constraints may be set and changed at run time. Every transport parameter must have an entry, even if it is only an empty object.", + "required": [ + "broker_topic", + "broker_protocol", + "broker_authorization", + "connection_status_broker_topic" + ], + "additionalProperties": false, "patternProperties": { "^ext_[a-zA-Z0-9_]+$":{ "$ref": "constraint-schema.json#/definitions/constraint" diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraints-schema-rtp.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/constraints-schema-rtp.json similarity index 95% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraints-schema-rtp.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/constraints-schema-rtp.json index 8269f79ba..a20613be6 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraints-schema-rtp.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/constraints-schema-rtp.json @@ -2,6 +2,12 @@ "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "description": "Used to express the dynamic constraints on RTP transport parameters. These constraints may be set and changed at run time. Every transport parameter must have an entry, even if it is only an empty object.", + "required": [ + "source_ip", + "destination_port", + "rtp_enabled" + ], + "additionalProperties": false, "patternProperties": { "^ext_[a-zA-Z0-9_]+$":{ "$ref": "constraint-schema.json#/definitions/constraint" diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraints-schema-websocket.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/constraints-schema-websocket.json similarity index 85% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraints-schema-websocket.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/constraints-schema-websocket.json index 70328eaba..cf3215eb8 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraints-schema-websocket.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/constraints-schema-websocket.json @@ -2,6 +2,11 @@ "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "description": "Used to express the dynamic constraints on WebSocket transport parameters. These constraints may be set and changed at run time. Every transport parameter must have an entry, even if it is only an empty object.", + "required": [ + "connection_uri", + "connection_authorization" + ], + "additionalProperties": false, "patternProperties": { "^ext_[a-zA-Z0-9_]+$":{ "$ref": "constraint-schema.json#/definitions/constraint" diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraints-schema.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/constraints-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/constraints-schema.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/constraints-schema.json diff --git a/Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/error.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/error.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.2.x/APIs/schemas/error.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/error.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver-response-schema.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/receiver-response-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver-response-schema.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/receiver-response-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver-stage-schema.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/receiver-stage-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver-stage-schema.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/receiver-stage-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver-transport-file.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/receiver-transport-file.json similarity index 80% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver-transport-file.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/receiver-transport-file.json index 5e74ec6ca..3c7be7c2e 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver-transport-file.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/receiver-transport-file.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", - "description": "Transport file parameters. 'data' and 'type' must both be strings or both be null", + "description": "Transport file parameters. 'data' and 'type' must both be strings or both be null. If 'type' is non-null 'data' is expected to contain a valid instance of the specified media type.", "title": "Transport file", "additionalProperties": false, "required": [ diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params_dash.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params_dash.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params_dash.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params_dash.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params_ext.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params_ext.json similarity index 60% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params_ext.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params_ext.json index 245a81822..9b0dbb9b5 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params_ext.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params_ext.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Describes external Receiver transport parameters defined in other AMWA IS specifications. The constraints in this schema are minimum constraints, but may be further constrained at the constraints endpoint.", + "description": "Describes external Receiver transport parameters defined in other AMWA specifications. The constraints in this schema are minimum constraints, but may be further constrained at the constraints endpoint.", "title": "External Receiver Transport Parameters", "type":[ "string", diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params_mqtt.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params_mqtt.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params_mqtt.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params_mqtt.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params_rtp.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params_rtp.json similarity index 97% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params_rtp.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params_rtp.json index 5428b4244..ee119205c 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params_rtp.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params_rtp.json @@ -10,7 +10,7 @@ "string", "null" ], - "description": "Source IP address of RTP packets in unicast mode, source filter for source specific multicast. A null value indicates that the receiver has not yet been configured, or in any-source multicast mode.", + "description": "Source IP address of RTP packets in unicast mode, source filter for source specific multicast. A null value indicates that the source IP address has not been configured in unicast mode, or the Receiver is in any-source multicast mode.", "anyOf": [{ "format": "ipv4" }, diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params_websocket.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params_websocket.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/receiver_transport_params_websocket.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/receiver_transport_params_websocket.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender-receiver-base.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/sender-receiver-base.json similarity index 84% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender-receiver-base.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/sender-receiver-base.json index 2a9b2d3a7..942bf0d86 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender-receiver-base.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/sender-receiver-base.json @@ -5,7 +5,7 @@ "title": "Connection API sender/receiver base resource", "items": { "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/$", - "uniqueItems": true - } + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/$" + }, + "uniqueItems": true } diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender-response-schema.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/sender-response-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender-response-schema.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/sender-response-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender-stage-schema.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/sender-stage-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender-stage-schema.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/sender-stage-schema.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params_dash.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params_dash.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params_dash.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params_dash.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params_ext.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params_ext.json similarity index 60% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params_ext.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params_ext.json index d2e146a8e..8209fd754 100644 --- a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params_ext.json +++ b/Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params_ext.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Describes external Sender transport parameters defined in other AMWA IS specifications. The constraints in this schema are minimum constraints, but may be further constrained at the constraints endpoint.", + "description": "Describes external Sender transport parameters defined in other AMWA specifications. The constraints in this schema are minimum constraints, but may be further constrained at the constraints endpoint.", "title": "External Sender Transport Parameters", "type":[ "string", diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params_mqtt.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params_mqtt.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params_mqtt.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params_mqtt.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params_rtp.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params_rtp.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params_rtp.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params_rtp.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params_websocket.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params_websocket.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/sender_transport_params_websocket.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/sender_transport_params_websocket.json diff --git a/Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/transporttype-response-schema.json b/Development/third_party/is-05/v1.1.x/APIs/schemas/transporttype-response-schema.json similarity index 100% rename from Development/third_party/nmos-device-connection-management/v1.1.x/APIs/schemas/transporttype-response-schema.json rename to Development/third_party/is-05/v1.1.x/APIs/schemas/transporttype-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/README.md b/Development/third_party/is-08/README.md similarity index 60% rename from Development/third_party/nmos-audio-channel-mapping/README.md rename to Development/third_party/is-08/README.md index 9eb300649..2da118047 100644 --- a/Development/third_party/nmos-audio-channel-mapping/README.md +++ b/Development/third_party/is-08/README.md @@ -1,6 +1,6 @@ # AMWA IS-08 NMOS Audio Channel Mapping Specification -This directory contains files from the [AMWA IS-08 NMOS Audio Channel Mapping Specification](https://github.com/AMWA-TV/nmos-audio-channel-mapping), in particular tagged versions of the JSON schemas used by the API specifications. +This directory contains files from the [AMWA IS-08 NMOS Audio Channel Mapping Specification](https://github.com/AMWA-TV/is-08), in particular tagged versions of the JSON schemas used by the API specifications. Original source code: diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/activation-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/activation-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/activation-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/activation-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/activation-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/activation-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/activation-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/activation-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/base-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/base-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/base-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/base-schema.json diff --git a/Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/error.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/error.json similarity index 100% rename from Development/third_party/nmos-discovery-registration/v1.3.x/APIs/schemas/error.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/error.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/input-base-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/input-base-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/input-base-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/input-base-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/input-caps-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/input-caps-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/input-caps-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/input-caps-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/input-channels-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/input-channels-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/input-channels-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/input-channels-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/input-parent-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/input-parent-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/input-parent-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/input-parent-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/input-properties-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/input-properties-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/input-properties-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/input-properties-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/inputs-outputs-base-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/inputs-outputs-base-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/inputs-outputs-base-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/inputs-outputs-base-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/io-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/io-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/io-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/io-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-activations-activation-get-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/map-activations-activation-get-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-activations-activation-get-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/map-activations-activation-get-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-activations-get-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/map-activations-get-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-activations-get-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/map-activations-get-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-activations-post-request-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/map-activations-post-request-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-activations-post-request-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/map-activations-post-request-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-activations-post-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/map-activations-post-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-activations-post-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/map-activations-post-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-active-output-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/map-active-output-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-active-output-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/map-active-output-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-active-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/map-active-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-active-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/map-active-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-base-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/map-base-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-base-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/map-base-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-entries-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/map-entries-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/map-entries-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/map-entries-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/output-base-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/output-base-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/output-base-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/output-base-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/output-caps-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/output-caps-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/output-caps-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/output-caps-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/output-channels-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/output-channels-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/output-channels-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/output-channels-response-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/output-properties-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/output-properties-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/output-properties-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/output-properties-schema.json diff --git a/Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/output-sourceid-response-schema.json b/Development/third_party/is-08/v1.0.x/APIs/schemas/output-sourceid-response-schema.json similarity index 100% rename from Development/third_party/nmos-audio-channel-mapping/v1.0.x/APIs/schemas/output-sourceid-response-schema.json rename to Development/third_party/is-08/v1.0.x/APIs/schemas/output-sourceid-response-schema.json diff --git a/Development/third_party/nmos-system/README.md b/Development/third_party/is-09/README.md similarity index 62% rename from Development/third_party/nmos-system/README.md rename to Development/third_party/is-09/README.md index c1368f211..eac2eec3b 100644 --- a/Development/third_party/nmos-system/README.md +++ b/Development/third_party/is-09/README.md @@ -1,6 +1,6 @@ # AMWA IS-09 NMOS System Parameters Specification -This directory contains files from the [AMWA IS-09 NMOS System Parameters Specification](https://github.com/AMWA-TV/nmos-system), in particular tagged versions of the JSON schemas used by the API specifications. +This directory contains files from the [AMWA IS-09 NMOS System Parameters Specification](https://github.com/AMWA-TV/is-09), in particular tagged versions of the JSON schemas used by the API specifications. Original source code: diff --git a/Development/third_party/nmos-system/v1.0.x/APIs/schemas/base.json b/Development/third_party/is-09/v1.0.x/APIs/schemas/base.json similarity index 100% rename from Development/third_party/nmos-system/v1.0.x/APIs/schemas/base.json rename to Development/third_party/is-09/v1.0.x/APIs/schemas/base.json diff --git a/Development/third_party/nmos-system/v1.0.x/APIs/schemas/error.json b/Development/third_party/is-09/v1.0.x/APIs/schemas/error.json similarity index 100% rename from Development/third_party/nmos-system/v1.0.x/APIs/schemas/error.json rename to Development/third_party/is-09/v1.0.x/APIs/schemas/error.json diff --git a/Development/third_party/nmos-system/v1.0.x/APIs/schemas/global.json b/Development/third_party/is-09/v1.0.x/APIs/schemas/global.json similarity index 100% rename from Development/third_party/nmos-system/v1.0.x/APIs/schemas/global.json rename to Development/third_party/is-09/v1.0.x/APIs/schemas/global.json diff --git a/Development/third_party/nmos-system/v1.0.x/APIs/schemas/resource_core.json b/Development/third_party/is-09/v1.0.x/APIs/schemas/resource_core.json similarity index 100% rename from Development/third_party/nmos-system/v1.0.x/APIs/schemas/resource_core.json rename to Development/third_party/is-09/v1.0.x/APIs/schemas/resource_core.json diff --git a/Development/third_party/is-10/README.md b/Development/third_party/is-10/README.md new file mode 100644 index 000000000..c5196cc7d --- /dev/null +++ b/Development/third_party/is-10/README.md @@ -0,0 +1,8 @@ +# AMWA IS-10 NMOS Authorization Specification + +This directory contains files from the [AMWA IS-10 NMOS Authorization Specification](https://github.com/AMWA-TV/is-10), in particular tagged versions of the JSON schemas used by the API specifications. + +Original source code: + +- (c) AMWA 2021 +- Licensed under the Apache License, Version 2.0; http://www.apache.org/licenses/LICENSE-2.0 diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/auth_metadata.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/auth_metadata.json new file mode 100644 index 000000000..fbfddd81e --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/auth_metadata.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "title": "Authorization API metadata resource", + "description": "Displays the Authorization server metadata", + "properties": { + "issuer": { + "description": "The authorization server's issuer identifier, which is a URL that uses the 'https' scheme and has no query or fragment components. Authorization server metadata is published at a location that is '.well-known' according to RFC 5785 [RFC5785] derived from this issuer identifier, as described in Section 3. The issuer identifier is used to prevent authorization server mix-up attacks.", + "format": "uri", + "type": "string" + }, + "authorization_endpoint": { + "description": "URL of the authorization server's authorization endpoint [RFC6749]. This is REQUIRED unless no grant types are supported that use the authorization endpoint.", + "format": "uri", + "type": "string" + }, + "token_endpoint": { + "description": "URL of the authorization server's token endpoint [RFC6749]. This is REQUIRED unless only the implicit grant type is supported.", + "format": "uri", + "type": "string" + }, + "jwks_uri": { + "description": "URL of the authorization server's JWK Set [JWK] document. The referenced document contains the signing key(s) the client uses to validate signatures from the authorization server. This URL MUST use the 'https' scheme. The JWK Set MAY also contain the server's encryption key or keys, which are used by clients to encrypt requests to the server. When both signing and encryption keys are made available, a 'use' (public key use) parameter value is REQUIRED for all keys in the referenced JWK Set to indicate each key's intended usage.", + "format": "uri", + "type": "string" + }, + "registration_endpoint": { + "description": "URL of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint [RFC7591].", + "format": "uri", + "type": "string" + }, + "scopes_supported": { + "description": "JSON array containing a list of the OAuth 2.0 [RFC6749] 'scope' values that this authorization server supports. Servers MAY choose not to advertise some supported scope values even when this parameter is used.", + "type": "array", + "items": { + "type": "string", + "uniqueItems": true + } + }, + "response_types_supported": { + "description": "JSON array containing a list of the OAuth 2.0 'response_type' values that this authorization server supports. The array values used are the same as those used with the 'response_types' parameter defined by 'OAuth 2.0 Dynamic Client Registration Protocol' in RFC7591", + "type": "array", + "items": { + "type": "string", + "uniqueItems": true + } + }, + "grant_types_supported": { + "description": "JSON array containing a list of the OAuth 2.0 grant type values that this authorization server supports. The array values used are the same as those used with the 'grant_types' parameter defined by 'OAuth 2.0 Dynamic Client Registration Protocol' in [RFC7591]. If omitted, the default value is ['authorization_code', 'implicit']", + "type": "array", + "items": { + "type": "string", + "uniqueItems": true + } + }, + "revocation_endpoint": { + "description": "URL of the authorization server's OAuth 2.0 revocation endpoint in RFC7009.", + "format": "uri", + "type": "string" + }, + "code_challenge_methods_supported": { + "description": "JSON array containing a list of Proof Key for Code Exchange (PKCE) [RFC7636] code challenge methods supported by this authorization server. Code challenge method values are used in the 'code_challenge_method' parameter defined in Section 4.3 of [RFC7636]. The valid code challenge method values are those registered in the IANA 'PKCE Code Challenge Methods' registry [IANA.OAuth.Parameters]. If omitted, the authorization server does not support PKCE.", + "type": "array", + "items": { + "type": "string", + "uniqueItems": true + } + } + }, + "required": [ + "issuer", + "authorization_endpoint", + "token_endpoint", + "jwks_uri", + "registration_endpoint", + "response_types_supported", + "code_challenge_methods_supported" + ] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_response.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_response.json new file mode 100644 index 000000000..95eee6f90 --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_response.json @@ -0,0 +1,9 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "JWKs Response", + "description": "JSON Web Key Set to validate Access Token", + "type": "object", + "allOf": [ + {"$ref": "jwks_schema.json"} + ] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_schema.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_schema.json new file mode 100644 index 000000000..44804bee3 --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/jwks_schema.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "JSON Web Key Set", + "description": "JSON Web Key Set to validate JSON Web Token", + "type": "object", + "properties": { + "keys": { + "description": "The value of the 'keys' parameter is an array of JWK values. By default, the order of the JWK values within the array does not imply an order of preference among them, although applications of JWK Sets can choose to assign a meaning to the order for their purposes, if desired.", + "type": "array", + "items": { + "type": "object", + "properties": { + "kty": { + "type": "string" + }, + "use": { + "type": "string" + }, + "key_ops": { + "type": "string" + }, + "alg": { + "type": "string" + }, + "kid": { + "type": "string" + }, + "x5u": { + "type": "string", + "format": "uri" + }, + "x5c": { + "type": "array", + "items": { + "type": "string" + } + }, + "x5t": { + "type": "string" + }, + "x5t#S256": { + "type": "string" + } + }, + "required": ["kty"] + } + } + }, + "required": ["keys"] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_error_response.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_error_response.json new file mode 100644 index 000000000..dcc313c8f --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_error_response.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Client Registration Error Response", + "description": "Describes the client registration endpoint's OAuth error response", + "type": "object", + "minItems": 1, + "properties": { + "error": { + "description": "Error Type", + "type": "string", + "enum": ["invalid_redirect_uri", "invalid_client_metadata", "invalid_software_statement", "unapproved_software_statement"] + }, + "error_description": { + "description": "Human-readable ASCII text providing additional information", + "type": "string" + }, + "error_uri": { + "description": "A URI identifying a human-readable web page with information about the error", + "type": "string", + "format": "uri" + } + }, + "required": ["error"] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_request.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_request.json new file mode 100644 index 000000000..7fcfcc7d2 --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_request.json @@ -0,0 +1,84 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Register Client Request", + "description": "Object defining client registration request", + "type": "object", + "properties": { + "redirect_uris": { + "description": "Array of redirection URI strings for use in redirect-based flows such as the authorization code and implicit flows", + "type": "array", + "items": { + "type": "string" + } + }, + "token_endpoint_auth_method": { + "description": "String indicator of the requested authentication method for the token endpoint", + "type": "string" + }, + "grant_types": { + "description": "Array of OAuth 2.0 grant type strings that the client can use at the token endpoint", + "type": "array", + "items": { + "type": "string" + } + }, + "response_types": { + "description": "Array of the OAuth 2.0 response type strings that the client can use at the authorization endpoint", + "type": "array", + "items": { + "type": "string" + } + }, + "client_name": { + "description": "Human-readable string name of the client to be presented to the end-user during authorization", + "type": "string" + }, + "client_uri": { + "description": "URL string of a web page providing information about the client", + "type": "string" + }, + "logo_uri": { + "description": "URL string that references a logo for the client", + "type": "string" + }, + "scope": { + "description": "String containing a space-separated list of scope values", + "type": "string" + }, + "contacts": { + "description": "Array of strings representing ways to contact people responsible for this client, typically email addresses", + "type": "array", + "items": { + "type": "string" + } + }, + "tos_uri": { + "description": "URL string that points to a human-readable terms of service document for the client", + "type": "string" + }, + "policy_uri": { + "description": "URL string that points to a human-readable privacy policy document", + "type": "string" + }, + "jwks_uri": { + "description": "URL string referencing the client's JSON Web Key (JWK) Set document, which contains the client's public keys", + "type": "string" + }, + "jwks": { + "description": "Client's JSON Web Key Set document value, which contains the client's public keys", + "type": "object", + "allOf": [ + {"$ref": "jwks_schema.json"} + ] + }, + "software_id": { + "description": "A unique identifier string (e.g. a UUID) assigned by the client developer or software publisher", + "type": "string" + }, + "software_version": { + "description": "A version identifier string for the client software identified by 'software_id'", + "type": "string" + } + }, + "required": [ "client_name" ] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_response.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_response.json new file mode 100644 index 000000000..ee54d7d35 --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/register_client_response.json @@ -0,0 +1,103 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Register Client Response", + "description": "Object defining successful client registration", + "type": "object", + "properties": { + "client_id": { + "description": "OAuth 2.0 client identifier string", + "type": "string" + }, + "client_secret": { + "description": "OAuth 2.0 client secret string", + "type": "string" + }, + "client_id_issued_at": { + "description": "UTC time at which the client identifier was issued", + "type": "number" + }, + "client_secret_expires_at": { + "description": "Time at which the client secret will expire or 0 if it will not expire", + "type": "number" + }, + "redirect_uris": { + "description": "Array of redirection URI strings for use in redirect-based flows such as the authorization code and implicit flows", + "type": "array", + "items": { + "type": "string" + } + }, + "token_endpoint_auth_method": { + "description": "String indicator of the requested authentication method for the token endpoint", + "type": "string", + "default": "client_secret_basic" + }, + "grant_types": { + "description": "Array of OAuth 2.0 grant type strings that the client can use at the token endpoint", + "type": "array", + "items": { + "type": "string" + }, + "default": [ "authorization_code" ] + }, + "response_types": { + "description": "Array of the OAuth 2.0 response type strings that the client can use at the authorization endpoint", + "type": "array", + "items": { + "type": "string" + }, + "default": [ "code" ] + }, + "client_name": { + "description": "Human-readable string name of the client to be presented to the end-user during authorization", + "type": "string" + }, + "client_uri": { + "description": "URL string of a web page providing information about the client", + "type": "string" + }, + "logo_uri": { + "description": "URL string that references a logo for the client", + "type": "string" + }, + "scope": { + "description": "String containing a space-separated list of scope values", + "type": "string" + }, + "contacts": { + "description": "Array of strings representing ways to contact people responsible for this client, typically email addresses", + "type": "array", + "items": { + "type": "string" + } + }, + "tos_uri": { + "description": "URL string that points to a human-readable terms of service document for the client", + "type": "string" + }, + "policy_uri": { + "description": "URL string that points to a human-readable privacy policy document", + "type": "string" + }, + "jwks_uri": { + "description": "URL string referencing the client's JSON Web Key (JWK) Set document, which contains the client's public keys", + "type": "string" + }, + "jwks": { + "description": "Client's JSON Web Key Set document value, which contains the client's public keys", + "type": "object", + "allOf": [ + {"$ref": "jwks_schema.json"} + ] + }, + "software_id": { + "description": "A unique identifier string (e.g. a UUID) assigned by the client developer or software publisher", + "type": "string" + }, + "software_version": { + "description": "A version identifier string for the client software identified by 'software_id'", + "type": "string" + } + }, + "required": ["client_id"] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/token_error_response.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/token_error_response.json new file mode 100644 index 000000000..a07584eec --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/token_error_response.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Token Error Response", + "description": "Describes the token endpoint's OAuth error response", + "type": "object", + "minItems": 1, + "properties": { + "error": { + "description": "Error Type", + "type": "string", + "enum": ["invalid_request", "invalid_client", "invalid_grant", "unauthorized_client", "unsupported_grant_type", "invalid_scope", "unsupported_token_type"] + }, + "error_description": { + "description": "Human-readable ASCII text providing additional information", + "type": "string" + }, + "error_uri": { + "description": "A URI identifying a human-readable web page with information about the error", + "type": "string", + "format": "uri" + } + }, + "required": ["error"] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/token_response.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/token_response.json new file mode 100644 index 000000000..636cfd116 --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/token_response.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Token Response", + "description": "OAuth2 Response for the request of a Bearer Token", + "type": "object", + "properties": { + "access_token": { + "description": "Access Token to be used in accessing protected endpoints", + "type": "string" + }, + "expires_in": { + "description": "The lifetime in seconds of the Access Token", + "type": "integer" + }, + "refresh_token": { + "description": "Refresh Token to be used to obtain further Access Tokens", + "type": "string" + }, + "scope": { + "description": "The scope of the Access Token", + "type": "string" + }, + "token_type": { + "description": "The type of the Token issued", + "type": "string" + } + }, + "required": ["access_token", "expires_in", "token_type"] +} diff --git a/Development/third_party/is-10/v1.0.x/APIs/schemas/token_schema.json b/Development/third_party/is-10/v1.0.x/APIs/schemas/token_schema.json new file mode 100644 index 000000000..2a6a66218 --- /dev/null +++ b/Development/third_party/is-10/v1.0.x/APIs/schemas/token_schema.json @@ -0,0 +1,76 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "JSON Web Token Contents", + "description": "Claims contained within JSON Web Token", + "type": "object", + "properties": { + "iss": { + "description": "A case-sensitive string containing a StringOrURI value that identifies the Authorization Server that issued the JWT", + "type": "string" + }, + "sub": { + "description": "The unique identifier assigned to the end-user by the user authorization system", + "type": "string" + }, + "aud": { + "description": "A JSON array of case-sensitive strings, each containing a StringOrURI value that identifies the recipients that the JWT is intended for", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string" + } + }, + { + "type": "string" + } + ] + }, + "exp": { + "description": "The UTC time at which the token expires", + "type": "number" + }, + "iat": { + "description": "The UTC time at which the token was issued", + "type": "number" + }, + "client_id": { + "description": "The client identifier of the OAuth 2.0 client that requested the token", + "type": "string" + }, + "azp": { + "description": "The client identifier of the OAuth 2.0 client that requested the token", + "type": "string" + }, + "scope": { + "description": "A string containing a space-separated list of scopes associated with the token", + "type": "string" + } + }, + "patternProperties": { + "^x-nmos-[a-z]+$": { + "description": "An object containing the access permissions of the user for the NMOS API identified by this attribute's name", + "type": "object", + "minProperties": 1, + "properties": { + "read": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + }, + "write": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "minItems": 1 + } + } + } + }, + "required": ["iss", "sub", "aud", "exp"] +} diff --git a/Development/third_party/is-12/README.md b/Development/third_party/is-12/README.md new file mode 100644 index 000000000..44ab19110 --- /dev/null +++ b/Development/third_party/is-12/README.md @@ -0,0 +1,9 @@ +# AMWA IS-12 NMOS Control & Monitoring Protocol Specification + +This directory contains files from the [AMWA NMOS Control & Monitoring Protocol](https://github.com/AMWA-TV/is-12), in particular tagged versions of the JSON schemas used by the API specifications. + +Original source code: + +- (c) AMWA 2023 +- Licensed under the Apache License, Version 2.0; http://www.apache.org/licenses/LICENSE-2.0 + diff --git a/Development/third_party/is-12/v1.0.x/APIs/schemas/base-message.json b/Development/third_party/is-12/v1.0.x/APIs/schemas/base-message.json new file mode 100644 index 000000000..1ecd16c6b --- /dev/null +++ b/Development/third_party/is-12/v1.0.x/APIs/schemas/base-message.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Base protocol message structure", + "title": "Base protocol message", + "required": [ + "messageType" + ], + "properties": { + "messageType": { + "description": "Protocol message type", + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3, + 4, + 5 + ] + } + } +} \ No newline at end of file diff --git a/Development/third_party/is-12/v1.0.x/APIs/schemas/command-message.json b/Development/third_party/is-12/v1.0.x/APIs/schemas/command-message.json new file mode 100644 index 000000000..093b69eda --- /dev/null +++ b/Development/third_party/is-12/v1.0.x/APIs/schemas/command-message.json @@ -0,0 +1,76 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Command protocol message structure", + "title": "Command protocol message", + "allOf": [ + { + "$ref": "base-message.json" + }, + { + "type": "object", + "required": [ + "commands", + "messageType" + ], + "properties": { + "commands": { + "description": "Commands being transmited in this transaction", + "type": "array", + "items": { + "type": "object", + "required": [ + "handle", + "oid", + "methodId" + ], + "properties": { + "handle": { + "type": "integer", + "description": "Integer value used for pairing with the response", + "minimum": 1, + "maximum": 65535 + }, + "oid": { + "type": "integer", + "description": "Object id containing the method", + "minimum": 1 + }, + "methodId": { + "type": "object", + "description": "ID structure for the target method", + "required": [ + "level", + "index" + ], + "properties": { + "level": { + "type": "integer", + "description": "Level component of the method ID", + "minimum": 1 + }, + "index": { + "type": "integer", + "description": "Index component of the method ID", + "minimum": 1 + } + } + }, + "arguments": { + "type": "object", + "description": "Method arguments" + } + } + } + }, + "messageType": { + "description": "Protocol message type", + "type": "integer", + "enum": [ + 0 + ] + } + } + } + ] +} \ No newline at end of file diff --git a/Development/third_party/is-12/v1.0.x/APIs/schemas/command-response-message.json b/Development/third_party/is-12/v1.0.x/APIs/schemas/command-response-message.json new file mode 100644 index 000000000..93711f583 --- /dev/null +++ b/Development/third_party/is-12/v1.0.x/APIs/schemas/command-response-message.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Command response protocol message structure", + "title": "Command response protocol message", + "allOf": [ + { + "$ref": "base-message.json" + }, + { + "type": "object", + "required": [ + "responses", + "messageType" + ], + "properties": { + "responses": { + "description": "Responses being transmited in this transaction", + "type": "array", + "items": { + "type": "object", + "required": [ + "handle", + "result" + ], + "properties": { + "handle": { + "type": "integer", + "description": "Integer value used for pairing with the command", + "minimum": 1, + "maximum": 65535 + }, + "result": { + "type": "object", + "description": "Response result", + "required": [ + "status" + ], + "properties": { + "status": { + "type": "integer", + "description": "Status of the command response. Must include the numeric values for NcMethodStatus or other types which inherit from it. 200 must be returned if the command was successful", + "minimum": 0, + "maximum": 65535 + }, + "value": { + "type": ["string", "number", "object", "array", "boolean", "null" ], + "description": "Method return value as described in the MS-05-02 Type definition or in a private Type definition" + }, + "errorMessage": { + "description": "Error message associated with the failure of the command (optional)", + "type": "string" + } + } + } + } + } + }, + "messageType": { + "description": "Protocol message type", + "type": "integer", + "enum": [ + 1 + ] + } + } + } + ] +} \ No newline at end of file diff --git a/Development/third_party/is-12/v1.0.x/APIs/schemas/error-message.json b/Development/third_party/is-12/v1.0.x/APIs/schemas/error-message.json new file mode 100644 index 000000000..139c77ffc --- /dev/null +++ b/Development/third_party/is-12/v1.0.x/APIs/schemas/error-message.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Error protocol message structure - used by devices to return general error messages for example when incoming messages do not have messageType, handles or contain invalid JSON", + "title": "Error protocol message", + "allOf": [ + { + "$ref": "base-message.json" + }, + { + "type": "object", + "required": [ + "status", + "errorMessage", + "messageType" + ], + "properties": { + "status": { + "type": "integer", + "description": "Status of the message response. Must include the numeric values for NcMethodStatus or other types which inherit from it.", + "minimum": 0, + "maximum": 65535 + }, + "errorMessage": { + "description": "Error details associated with the failure", + "type": "string" + }, + "messageType": { + "description": "Protocol message type", + "type": "integer", + "enum": [ + 5 + ] + } + } + } + ] +} diff --git a/Development/third_party/is-12/v1.0.x/APIs/schemas/event-data.json b/Development/third_party/is-12/v1.0.x/APIs/schemas/event-data.json new file mode 100644 index 000000000..9b644871c --- /dev/null +++ b/Development/third_party/is-12/v1.0.x/APIs/schemas/event-data.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Event data structure", + "title": "Event data", + "oneOf": [ + { + "$ref": "property-changed-event-data.json" + } + ] +} diff --git a/Development/third_party/is-12/v1.0.x/APIs/schemas/notification-message.json b/Development/third_party/is-12/v1.0.x/APIs/schemas/notification-message.json new file mode 100644 index 000000000..860770eb4 --- /dev/null +++ b/Development/third_party/is-12/v1.0.x/APIs/schemas/notification-message.json @@ -0,0 +1,69 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Notification protocol message structure", + "title": "Notification protocol message", + "allOf": [ + { + "$ref": "base-message.json" + }, + { + "type": "object", + "required": [ + "notifications", + "messageType" + ], + "properties": { + "notifications": { + "description": "Notifications being transmited in this transaction", + "type": "array", + "items": { + "type": "object", + "required": [ + "oid", + "eventId", + "eventData" + ], + "properties": { + "oid": { + "type": "integer", + "description": "Emitter object id", + "minimum": 1 + }, + "eventId": { + "type": "object", + "description": "Event ID structure", + "required": [ + "level", + "index" + ], + "properties": { + "level": { + "type": "integer", + "description": "Level component of the event ID", + "minimum": 1 + }, + "index": { + "type": "integer", + "description": "Index component of the event ID", + "minimum": 1 + } + } + }, + "eventData": { + "$ref": "event-data.json" + } + } + } + }, + "messageType": { + "description": "Protocol message type", + "type": "integer", + "enum": [ + 2 + ] + } + } + } + ] +} \ No newline at end of file diff --git a/Development/third_party/is-12/v1.0.x/APIs/schemas/property-changed-event-data.json b/Development/third_party/is-12/v1.0.x/APIs/schemas/property-changed-event-data.json new file mode 100644 index 000000000..7d6be6f1a --- /dev/null +++ b/Development/third_party/is-12/v1.0.x/APIs/schemas/property-changed-event-data.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Property changed event data structure", + "title": "Property changed event data", + "properties": { + "propertyId": { + "type": "object", + "description": "Property ID structure", + "required": [ + "level", + "index" + ], + "properties": { + "level": { + "type": "integer", + "description": "Level component of the property ID", + "minimum": 1 + }, + "index": { + "type": "integer", + "description": "Index component of the property ID", + "minimum": 1 + } + } + }, + "changeType": { + "type": "integer", + "description": "Event change type numeric value. Must include the numeric values for NcPropertyChangeType", + "minimum": 0, + "maximum": 65535 + }, + "value": { + "type": [ + "string", + "number", + "object", + "array", + "boolean", + "null" + ], + "description": "Property value as described in the MS-05-02 Class definition or in a private Class definition" + }, + "sequenceItemIndex": { + "type": [ + "number", + "null" + ], + "description": "Index of sequence item if the property is a sequence" + } + }, + "required": [ + "propertyId", + "changeType", + "value", + "sequenceItemIndex" + ] +} diff --git a/Development/third_party/is-12/v1.0.x/APIs/schemas/subscription-message.json b/Development/third_party/is-12/v1.0.x/APIs/schemas/subscription-message.json new file mode 100644 index 000000000..290ccd903 --- /dev/null +++ b/Development/third_party/is-12/v1.0.x/APIs/schemas/subscription-message.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Subscription protocol message structure", + "title": "Subscription protocol message", + "allOf": [ + { + "$ref": "base-message.json" + }, + { + "type": "object", + "required": [ + "subscriptions", + "messageType" + ], + "properties": { + "subscriptions": { + "description": "Array of OIDs desired for subscription", + "type": "array", + "items": { + "type": "integer" + } + }, + "messageType": { + "description": "Protocol message type", + "type": "integer", + "enum": [ + 3 + ] + } + } + } + ] +} diff --git a/Development/third_party/is-12/v1.0.x/APIs/schemas/subscription-response-message.json b/Development/third_party/is-12/v1.0.x/APIs/schemas/subscription-response-message.json new file mode 100644 index 000000000..587cdd62a --- /dev/null +++ b/Development/third_party/is-12/v1.0.x/APIs/schemas/subscription-response-message.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Subscription response protocol message structure", + "title": "Subscription response protocol message", + "allOf": [ + { + "$ref": "base-message.json" + }, + { + "type": "object", + "required": [ + "subscriptions", + "messageType" + ], + "properties": { + "subscriptions": { + "description": "Array of OIDs which have successfully been added to the subscription list.", + "type": "array", + "items": { + "type": "integer" + } + }, + "messageType": { + "description": "Protocol message type", + "type": "integer", + "enum": [ + 4 + ] + } + } + } + ] +} diff --git a/Development/third_party/jwt-cpp/README.md b/Development/third_party/jwt-cpp/README.md new file mode 100644 index 000000000..10dbe2265 --- /dev/null +++ b/Development/third_party/jwt-cpp/README.md @@ -0,0 +1,8 @@ +# jwt-cpp + +This directory contains files from the [Thalhammer/jwt-cpp](https://github.com/Thalhammer/jwt-cpp) header only library for creating and validating JSON Web Tokens in C++11. + +Original source code: + +- Licensed under the MIT License . +- Copyright (c) 2018 Dominik Thalhammer diff --git a/Development/third_party/jwt-cpp/base.h b/Development/third_party/jwt-cpp/base.h new file mode 100644 index 000000000..9d7c43c04 --- /dev/null +++ b/Development/third_party/jwt-cpp/base.h @@ -0,0 +1,270 @@ +#ifndef JWT_CPP_BASE_H +#define JWT_CPP_BASE_H + +#include +#include +#include +#include +#include +#include + +#ifdef __has_cpp_attribute +#if __has_cpp_attribute(fallthrough) +#define JWT_FALLTHROUGH [[fallthrough]] +#endif +#endif + +#ifndef JWT_FALLTHROUGH +#define JWT_FALLTHROUGH +#endif + +namespace jwt { + /** + * \brief character maps when encoding and decoding + */ + namespace alphabet { + /** + * \brief valid list of character when working with [Base64](https://datatracker.ietf.org/doc/html/rfc4648#section-4) + * + * As directed in [X.509 Parameter](https://datatracker.ietf.org/doc/html/rfc7517#section-4.7) certificate chains are + * base64-encoded as per [Section 4 of RFC4648](https://datatracker.ietf.org/doc/html/rfc4648#section-4) + */ + struct base64 { + static const std::array& data() { + static constexpr std::array data{ + {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'}}; + return data; + } + static const std::string& fill() { + static const std::string fill{"="}; + return fill; + } + }; + /** + * \brief valid list of character when working with [Base64URL](https://tools.ietf.org/html/rfc4648#section-5) + * + * As directed by [RFC 7519 Terminology](https://datatracker.ietf.org/doc/html/rfc7519#section-2) set the definition of Base64URL + * encoding as that in [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-2) that states: + * + * > Base64 encoding using the URL- and filename-safe character set defined in + * > [Section 5 of RFC 4648 RFC4648](https://tools.ietf.org/html/rfc4648#section-5), with all trailing '=' characters omitted + */ + struct base64url { + static const std::array& data() { + static constexpr std::array data{ + {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'}}; + return data; + } + static const std::string& fill() { + static const std::string fill{"%3d"}; + return fill; + } + }; + namespace helper { + /** + * @brief A General purpose base64url alphabet respecting the + * [URI Case Normalization](https://datatracker.ietf.org/doc/html/rfc3986#section-6.2.2.1) + * + * This is useful in situations outside of JWT encoding/decoding and is provided as a helper + */ + struct base64url_percent_encoding { + static const std::array& data() { + static constexpr std::array data{ + {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_'}}; + return data; + } + static const std::vector& fill() { + static const std::vector fill{"%3D", "%3d"}; + return fill; + } + }; + } // namespace helper + + inline uint32_t index(const std::array& alphabet, char symbol) { + auto itr = std::find_if(alphabet.cbegin(), alphabet.cend(), [symbol](char c) { return c == symbol; }); + if (itr == alphabet.cend()) { throw std::runtime_error("Invalid input: not within alphabet"); } + + return static_cast(std::distance(alphabet.cbegin(), itr)); + } + } // namespace alphabet + + /** + * \brief A collection of fellable functions for working with base64 and base64url + */ + namespace base { + + namespace details { + struct padding { + size_t count = 0; + size_t length = 0; + + padding() = default; + padding(size_t count, size_t length) : count(count), length(length) {} + + padding operator+(const padding& p) { return padding(count + p.count, length + p.length); } + + friend bool operator==(const padding& lhs, const padding& rhs) { + return lhs.count == rhs.count && lhs.length == rhs.length; + } + }; + + inline padding count_padding(const std::string& base, const std::vector& fills) { + for (const auto& fill : fills) { + if (base.size() < fill.size()) continue; + // Does the end of the input exactly match the fill pattern? + if (base.substr(base.size() - fill.size()) == fill) { + return padding{1, fill.length()} + + count_padding(base.substr(0, base.size() - fill.size()), fills); + } + } + + return {}; + } + + inline std::string encode(const std::string& bin, const std::array& alphabet, + const std::string& fill) { + size_t size = bin.size(); + std::string res; + + // clear incomplete bytes + size_t fast_size = size - size % 3; + for (size_t i = 0; i < fast_size;) { + uint32_t octet_a = static_cast(bin[i++]); + uint32_t octet_b = static_cast(bin[i++]); + uint32_t octet_c = static_cast(bin[i++]); + + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += alphabet[(triple >> 1 * 6) & 0x3F]; + res += alphabet[(triple >> 0 * 6) & 0x3F]; + } + + if (fast_size == size) return res; + + size_t mod = size % 3; + + uint32_t octet_a = fast_size < size ? static_cast(bin[fast_size++]) : 0; + uint32_t octet_b = fast_size < size ? static_cast(bin[fast_size++]) : 0; + uint32_t octet_c = fast_size < size ? static_cast(bin[fast_size++]) : 0; + + uint32_t triple = (octet_a << 0x10) + (octet_b << 0x08) + octet_c; + + switch (mod) { + case 1: + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += fill; + res += fill; + break; + case 2: + res += alphabet[(triple >> 3 * 6) & 0x3F]; + res += alphabet[(triple >> 2 * 6) & 0x3F]; + res += alphabet[(triple >> 1 * 6) & 0x3F]; + res += fill; + break; + default: break; + } + + return res; + } + + inline std::string decode(const std::string& base, const std::array& alphabet, + const std::vector& fill) { + const auto pad = count_padding(base, fill); + if (pad.count > 2) throw std::runtime_error("Invalid input: too much fill"); + + const size_t size = base.size() - pad.length; + if ((size + pad.count) % 4 != 0) throw std::runtime_error("Invalid input: incorrect total size"); + + size_t out_size = size / 4 * 3; + std::string res; + res.reserve(out_size); + + auto get_sextet = [&](size_t offset) { return alphabet::index(alphabet, base[offset]); }; + + size_t fast_size = size - size % 4; + for (size_t i = 0; i < fast_size;) { + uint32_t sextet_a = get_sextet(i++); + uint32_t sextet_b = get_sextet(i++); + uint32_t sextet_c = get_sextet(i++); + uint32_t sextet_d = get_sextet(i++); + + uint32_t triple = + (sextet_a << 3 * 6) + (sextet_b << 2 * 6) + (sextet_c << 1 * 6) + (sextet_d << 0 * 6); + + res += static_cast((triple >> 2 * 8) & 0xFFU); + res += static_cast((triple >> 1 * 8) & 0xFFU); + res += static_cast((triple >> 0 * 8) & 0xFFU); + } + + if (pad.count == 0) return res; + + uint32_t triple = (get_sextet(fast_size) << 3 * 6) + (get_sextet(fast_size + 1) << 2 * 6); + + switch (pad.count) { + case 1: + triple |= (get_sextet(fast_size + 2) << 1 * 6); + res += static_cast((triple >> 2 * 8) & 0xFFU); + res += static_cast((triple >> 1 * 8) & 0xFFU); + break; + case 2: res += static_cast((triple >> 2 * 8) & 0xFFU); break; + default: break; + } + + return res; + } + + inline std::string decode(const std::string& base, const std::array& alphabet, + const std::string& fill) { + return decode(base, alphabet, std::vector{fill}); + } + + inline std::string pad(const std::string& base, const std::string& fill) { + std::string padding; + switch (base.size() % 4) { + case 1: padding += fill; JWT_FALLTHROUGH; + case 2: padding += fill; JWT_FALLTHROUGH; + case 3: padding += fill; JWT_FALLTHROUGH; + default: break; + } + + return base + padding; + } + + inline std::string trim(const std::string& base, const std::string& fill) { + auto pos = base.find(fill); + return base.substr(0, pos); + } + } // namespace details + + template + std::string encode(const std::string& bin) { + return details::encode(bin, T::data(), T::fill()); + } + template + std::string decode(const std::string& base) { + return details::decode(base, T::data(), T::fill()); + } + template + std::string pad(const std::string& base) { + return details::pad(base, T::fill()); + } + template + std::string trim(const std::string& base) { + return details::trim(base, T::fill()); + } + } // namespace base +} // namespace jwt + +#endif diff --git a/Development/third_party/jwt-cpp/jwt.h b/Development/third_party/jwt-cpp/jwt.h new file mode 100644 index 000000000..b2b998a2e --- /dev/null +++ b/Development/third_party/jwt-cpp/jwt.h @@ -0,0 +1,3655 @@ +#ifndef JWT_CPP_JWT_H +#define JWT_CPP_JWT_H + +#ifndef JWT_DISABLE_PICOJSON +#ifndef PICOJSON_USE_INT64 +#define PICOJSON_USE_INT64 +#endif +#include "picojson/picojson.h" +#endif + +#ifndef JWT_DISABLE_BASE64 +#include "base.h" +#endif + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if __cplusplus > 201103L +#include +#endif + +#if __cplusplus >= 201402L +#ifdef __has_include +#if __has_include() +#include +#endif +#endif +#endif + +#if OPENSSL_VERSION_NUMBER >= 0x30000000L // 3.0.0 +#define JWT_OPENSSL_3_0 +#elif OPENSSL_VERSION_NUMBER >= 0x10101000L // 1.1.1 +#define JWT_OPENSSL_1_1_1 +#elif OPENSSL_VERSION_NUMBER >= 0x10100000L // 1.1.0 +#define JWT_OPENSSL_1_1_0 +#elif OPENSSL_VERSION_NUMBER >= 0x10000000L // 1.0.0 +#define JWT_OPENSSL_1_0_0 +#endif + +#if defined(LIBRESSL_VERSION_NUMBER) +#if LIBRESSL_VERSION_NUMBER >= 0x3050300fL +#define JWT_OPENSSL_1_1_0 +#else +#define JWT_OPENSSL_1_0_0 +#endif +#endif + +#if defined(LIBWOLFSSL_VERSION_HEX) +#define JWT_OPENSSL_1_1_1 +#endif + +#ifndef JWT_CLAIM_EXPLICIT +#define JWT_CLAIM_EXPLICIT explicit +#endif + +/** + * \brief JSON Web Token + * + * A namespace to contain everything related to handling JSON Web Tokens, JWT for short, + * as a part of [RFC7519](https://tools.ietf.org/html/rfc7519), or alternatively for + * JWS (JSON Web Signature) from [RFC7515](https://tools.ietf.org/html/rfc7515) + */ +namespace jwt { + /** + * Default system time point in UTC + */ + using date = std::chrono::system_clock::time_point; + + /** + * \brief Everything related to error codes issued by the library + */ + namespace error { + struct signature_verification_exception : public std::system_error { + using system_error::system_error; + }; + struct signature_generation_exception : public std::system_error { + using system_error::system_error; + }; + struct rsa_exception : public std::system_error { + using system_error::system_error; + }; + struct ecdsa_exception : public std::system_error { + using system_error::system_error; + }; + struct token_verification_exception : public std::system_error { + using system_error::system_error; + }; + /** + * \brief Errors related to processing of RSA signatures + */ + enum class rsa_error { + ok = 0, + cert_load_failed = 10, + get_key_failed, + write_key_failed, + write_cert_failed, + convert_to_pem_failed, + load_key_bio_write, + load_key_bio_read, + create_mem_bio_failed, + no_key_provided + }; + /** + * \brief Error category for RSA errors + */ + inline std::error_category& rsa_error_category() { + class rsa_error_cat : public std::error_category { + public: + const char* name() const noexcept override { return "rsa_error"; }; + std::string message(int ev) const override { + switch (static_cast(ev)) { + case rsa_error::ok: return "no error"; + case rsa_error::cert_load_failed: return "error loading cert into memory"; + case rsa_error::get_key_failed: return "error getting key from certificate"; + case rsa_error::write_key_failed: return "error writing key data in PEM format"; + case rsa_error::write_cert_failed: return "error writing cert data in PEM format"; + case rsa_error::convert_to_pem_failed: return "failed to convert key to pem"; + case rsa_error::load_key_bio_write: return "failed to load key: bio write failed"; + case rsa_error::load_key_bio_read: return "failed to load key: bio read failed"; + case rsa_error::create_mem_bio_failed: return "failed to create memory bio"; + case rsa_error::no_key_provided: return "at least one of public or private key need to be present"; + default: return "unknown RSA error"; + } + } + }; + static rsa_error_cat cat; + return cat; + } + + inline std::error_code make_error_code(rsa_error e) { return {static_cast(e), rsa_error_category()}; } + /** + * \brief Errors related to processing of RSA signatures + */ + enum class ecdsa_error { + ok = 0, + load_key_bio_write = 10, + load_key_bio_read, + create_mem_bio_failed, + no_key_provided, + invalid_key_size, + invalid_key, + create_context_failed + }; + /** + * \brief Error category for ECDSA errors + */ + inline std::error_category& ecdsa_error_category() { + class ecdsa_error_cat : public std::error_category { + public: + const char* name() const noexcept override { return "ecdsa_error"; }; + std::string message(int ev) const override { + switch (static_cast(ev)) { + case ecdsa_error::ok: return "no error"; + case ecdsa_error::load_key_bio_write: return "failed to load key: bio write failed"; + case ecdsa_error::load_key_bio_read: return "failed to load key: bio read failed"; + case ecdsa_error::create_mem_bio_failed: return "failed to create memory bio"; + case ecdsa_error::no_key_provided: + return "at least one of public or private key need to be present"; + case ecdsa_error::invalid_key_size: return "invalid key size"; + case ecdsa_error::invalid_key: return "invalid key"; + case ecdsa_error::create_context_failed: return "failed to create context"; + default: return "unknown ECDSA error"; + } + } + }; + static ecdsa_error_cat cat; + return cat; + } + + inline std::error_code make_error_code(ecdsa_error e) { return {static_cast(e), ecdsa_error_category()}; } + + /** + * \brief Errors related to verification of signatures + */ + enum class signature_verification_error { + ok = 0, + invalid_signature = 10, + create_context_failed, + verifyinit_failed, + verifyupdate_failed, + verifyfinal_failed, + get_key_failed, + set_rsa_pss_saltlen_failed, + signature_encoding_failed + }; + /** + * \brief Error category for verification errors + */ + inline std::error_category& signature_verification_error_category() { + class verification_error_cat : public std::error_category { + public: + const char* name() const noexcept override { return "signature_verification_error"; }; + std::string message(int ev) const override { + switch (static_cast(ev)) { + case signature_verification_error::ok: return "no error"; + case signature_verification_error::invalid_signature: return "invalid signature"; + case signature_verification_error::create_context_failed: + return "failed to verify signature: could not create context"; + case signature_verification_error::verifyinit_failed: + return "failed to verify signature: VerifyInit failed"; + case signature_verification_error::verifyupdate_failed: + return "failed to verify signature: VerifyUpdate failed"; + case signature_verification_error::verifyfinal_failed: + return "failed to verify signature: VerifyFinal failed"; + case signature_verification_error::get_key_failed: + return "failed to verify signature: Could not get key"; + case signature_verification_error::set_rsa_pss_saltlen_failed: + return "failed to verify signature: EVP_PKEY_CTX_set_rsa_pss_saltlen failed"; + case signature_verification_error::signature_encoding_failed: + return "failed to verify signature: i2d_ECDSA_SIG failed"; + default: return "unknown signature verification error"; + } + } + }; + static verification_error_cat cat; + return cat; + } + + inline std::error_code make_error_code(signature_verification_error e) { + return {static_cast(e), signature_verification_error_category()}; + } + + /** + * \brief Errors related to signature generation errors + */ + enum class signature_generation_error { + ok = 0, + hmac_failed = 10, + create_context_failed, + signinit_failed, + signupdate_failed, + signfinal_failed, + ecdsa_do_sign_failed, + digestinit_failed, + digestupdate_failed, + digestfinal_failed, + rsa_padding_failed, + rsa_private_encrypt_failed, + get_key_failed, + set_rsa_pss_saltlen_failed, + signature_decoding_failed + }; + /** + * \brief Error category for signature generation errors + */ + inline std::error_category& signature_generation_error_category() { + class signature_generation_error_cat : public std::error_category { + public: + const char* name() const noexcept override { return "signature_generation_error"; }; + std::string message(int ev) const override { + switch (static_cast(ev)) { + case signature_generation_error::ok: return "no error"; + case signature_generation_error::hmac_failed: return "hmac failed"; + case signature_generation_error::create_context_failed: + return "failed to create signature: could not create context"; + case signature_generation_error::signinit_failed: + return "failed to create signature: SignInit failed"; + case signature_generation_error::signupdate_failed: + return "failed to create signature: SignUpdate failed"; + case signature_generation_error::signfinal_failed: + return "failed to create signature: SignFinal failed"; + case signature_generation_error::ecdsa_do_sign_failed: return "failed to generate ecdsa signature"; + case signature_generation_error::digestinit_failed: + return "failed to create signature: DigestInit failed"; + case signature_generation_error::digestupdate_failed: + return "failed to create signature: DigestUpdate failed"; + case signature_generation_error::digestfinal_failed: + return "failed to create signature: DigestFinal failed"; + case signature_generation_error::rsa_padding_failed: + return "failed to create signature: EVP_PKEY_CTX_set_rsa_padding failed"; + case signature_generation_error::rsa_private_encrypt_failed: + return "failed to create signature: RSA_private_encrypt failed"; + case signature_generation_error::get_key_failed: + return "failed to generate signature: Could not get key"; + case signature_generation_error::set_rsa_pss_saltlen_failed: + return "failed to create signature: EVP_PKEY_CTX_set_rsa_pss_saltlen failed"; + case signature_generation_error::signature_decoding_failed: + return "failed to create signature: d2i_ECDSA_SIG failed"; + default: return "unknown signature generation error"; + } + } + }; + static signature_generation_error_cat cat = {}; + return cat; + } + + inline std::error_code make_error_code(signature_generation_error e) { + return {static_cast(e), signature_generation_error_category()}; + } + + /** + * \brief Errors related to token verification errors + */ + enum class token_verification_error { + ok = 0, + wrong_algorithm = 10, + missing_claim, + claim_type_missmatch, + claim_value_missmatch, + token_expired, + audience_missmatch + }; + /** + * \brief Error category for token verification errors + */ + inline std::error_category& token_verification_error_category() { + class token_verification_error_cat : public std::error_category { + public: + const char* name() const noexcept override { return "token_verification_error"; }; + std::string message(int ev) const override { + switch (static_cast(ev)) { + case token_verification_error::ok: return "no error"; + case token_verification_error::wrong_algorithm: return "wrong algorithm"; + case token_verification_error::missing_claim: return "decoded JWT is missing required claim(s)"; + case token_verification_error::claim_type_missmatch: + return "claim type does not match expected type"; + case token_verification_error::claim_value_missmatch: + return "claim value does not match expected value"; + case token_verification_error::token_expired: return "token expired"; + case token_verification_error::audience_missmatch: + return "token doesn't contain the required audience"; + default: return "unknown token verification error"; + } + } + }; + static token_verification_error_cat cat = {}; + return cat; + } + + inline std::error_code make_error_code(token_verification_error e) { + return {static_cast(e), token_verification_error_category()}; + } + + inline void throw_if_error(std::error_code ec) { + if (ec) { + if (ec.category() == rsa_error_category()) throw rsa_exception(ec); + if (ec.category() == ecdsa_error_category()) throw ecdsa_exception(ec); + if (ec.category() == signature_verification_error_category()) + throw signature_verification_exception(ec); + if (ec.category() == signature_generation_error_category()) throw signature_generation_exception(ec); + if (ec.category() == token_verification_error_category()) throw token_verification_exception(ec); + } + } + } // namespace error +} // namespace jwt + +namespace std { + template<> + struct is_error_code_enum : true_type {}; + template<> + struct is_error_code_enum : true_type {}; + template<> + struct is_error_code_enum : true_type {}; + template<> + struct is_error_code_enum : true_type {}; + template<> + struct is_error_code_enum : true_type {}; +} // namespace std + +namespace jwt { + /** + * \brief A collection for working with certificates + * + * These _helpers_ are usefully when working with certificates OpenSSL APIs. + * For example, when dealing with JWKS (JSON Web Key Set)[https://tools.ietf.org/html/rfc7517] + * you maybe need to extract the modulus and exponent of an RSA Public Key. + */ + namespace helper { + /** + * \brief Handle class for EVP_PKEY structures + * + * Starting from OpenSSL 1.1.0, EVP_PKEY has internal reference counting. This handle class allows + * jwt-cpp to leverage that and thus safe an allocation for the control block in std::shared_ptr. + * The handle uses shared_ptr as a fallback on older versions. The behaviour should be identical between both. + */ + class evp_pkey_handle { + public: + constexpr evp_pkey_handle() noexcept = default; +#ifdef JWT_OPENSSL_1_0_0 + /** + * \brief Construct a new handle. The handle takes ownership of the key. + * \param key The key to store + */ + explicit evp_pkey_handle(EVP_PKEY* key) { m_key = std::shared_ptr(key, EVP_PKEY_free); } + + EVP_PKEY* get() const noexcept { return m_key.get(); } + bool operator!() const noexcept { return m_key == nullptr; } + explicit operator bool() const noexcept { return m_key != nullptr; } + + private: + std::shared_ptr m_key{nullptr}; +#else + /** + * \brief Construct a new handle. The handle takes ownership of the key. + * \param key The key to store + */ + explicit constexpr evp_pkey_handle(EVP_PKEY* key) noexcept : m_key{key} {} + evp_pkey_handle(const evp_pkey_handle& other) : m_key{other.m_key} { + if (m_key != nullptr && EVP_PKEY_up_ref(m_key) != 1) throw std::runtime_error("EVP_PKEY_up_ref failed"); + } +// C++11 requires the body of a constexpr constructor to be empty +#if __cplusplus >= 201402L + constexpr +#endif + evp_pkey_handle(evp_pkey_handle&& other) noexcept + : m_key{other.m_key} { + other.m_key = nullptr; + } + evp_pkey_handle& operator=(const evp_pkey_handle& other) { + if (&other == this) return *this; + decrement_ref_count(m_key); + m_key = other.m_key; + increment_ref_count(m_key); + return *this; + } + evp_pkey_handle& operator=(evp_pkey_handle&& other) noexcept { + if (&other == this) return *this; + decrement_ref_count(m_key); + m_key = other.m_key; + other.m_key = nullptr; + return *this; + } + evp_pkey_handle& operator=(EVP_PKEY* key) { + decrement_ref_count(m_key); + m_key = key; + increment_ref_count(m_key); + return *this; + } + ~evp_pkey_handle() noexcept { decrement_ref_count(m_key); } + + EVP_PKEY* get() const noexcept { return m_key; } + bool operator!() const noexcept { return m_key == nullptr; } + explicit operator bool() const noexcept { return m_key != nullptr; } + + private: + EVP_PKEY* m_key{nullptr}; + + static void increment_ref_count(EVP_PKEY* key) { + if (key != nullptr && EVP_PKEY_up_ref(key) != 1) throw std::runtime_error("EVP_PKEY_up_ref failed"); + } + static void decrement_ref_count(EVP_PKEY* key) noexcept { + if (key != nullptr) EVP_PKEY_free(key); + } +#endif + }; + + inline std::unique_ptr make_mem_buf_bio() { + return std::unique_ptr(BIO_new(BIO_s_mem()), BIO_free_all); + } + + inline std::unique_ptr make_mem_buf_bio(const std::string& data) { + return std::unique_ptr( +#if OPENSSL_VERSION_NUMBER <= 0x10100003L + BIO_new_mem_buf(const_cast(data.data()), static_cast(data.size())), BIO_free_all +#else + BIO_new_mem_buf(data.data(), static_cast(data.size())), BIO_free_all +#endif + ); + } + + inline std::unique_ptr make_evp_md_ctx() { + return +#ifdef JWT_OPENSSL_1_0_0 + std::unique_ptr(EVP_MD_CTX_create(), &EVP_MD_CTX_destroy); +#else + std::unique_ptr(EVP_MD_CTX_new(), &EVP_MD_CTX_free); +#endif + } + + /** + * \brief Extract the public key of a pem certificate + * + * \param certstr String containing the certificate encoded as pem + * \param pw Password used to decrypt certificate (leave empty if not encrypted) + * \param ec error_code for error_detection (gets cleared if no error occurred) + */ + inline std::string extract_pubkey_from_cert(const std::string& certstr, const std::string& pw, + std::error_code& ec) { + ec.clear(); + auto certbio = make_mem_buf_bio(certstr); + auto keybio = make_mem_buf_bio(); + if (!certbio || !keybio) { + ec = error::rsa_error::create_mem_bio_failed; + return {}; + } + + std::unique_ptr cert( + PEM_read_bio_X509(certbio.get(), nullptr, nullptr, const_cast(pw.c_str())), X509_free); + if (!cert) { + ec = error::rsa_error::cert_load_failed; + return {}; + } + std::unique_ptr key(X509_get_pubkey(cert.get()), EVP_PKEY_free); + if (!key) { + ec = error::rsa_error::get_key_failed; + return {}; + } + if (PEM_write_bio_PUBKEY(keybio.get(), key.get()) == 0) { + ec = error::rsa_error::write_key_failed; + return {}; + } + char* ptr = nullptr; + auto len = BIO_get_mem_data(keybio.get(), &ptr); + if (len <= 0 || ptr == nullptr) { + ec = error::rsa_error::convert_to_pem_failed; + return {}; + } + return {ptr, static_cast(len)}; + } + + /** + * \brief Extract the public key of a pem certificate + * + * \param certstr String containing the certificate encoded as pem + * \param pw Password used to decrypt certificate (leave empty if not encrypted) + * \throw rsa_exception if an error occurred + */ + inline std::string extract_pubkey_from_cert(const std::string& certstr, const std::string& pw = "") { + std::error_code ec; + auto res = extract_pubkey_from_cert(certstr, pw, ec); + error::throw_if_error(ec); + return res; + } + + /** + * \brief Convert the certificate provided as DER to PEM. + * + * \param cert_der_str String containing the certificate encoded as base64 DER + * \param ec error_code for error_detection (gets cleared if no error occurs) + */ + inline std::string convert_der_to_pem(const std::string& cert_der_str, std::error_code& ec) { + ec.clear(); + + auto c_str = reinterpret_cast(cert_der_str.c_str()); + + std::unique_ptr cert( + d2i_X509(NULL, &c_str, static_cast(cert_der_str.size())), X509_free); + auto certbio = make_mem_buf_bio(); + if (!cert || !certbio) { + ec = error::rsa_error::create_mem_bio_failed; + return {}; + } + + if (!PEM_write_bio_X509(certbio.get(), cert.get())) { + ec = error::rsa_error::write_cert_failed; + return {}; + } + + char* ptr = nullptr; + const auto len = BIO_get_mem_data(certbio.get(), &ptr); + if (len <= 0 || ptr == nullptr) { + ec = error::rsa_error::convert_to_pem_failed; + return {}; + } + + return {ptr, static_cast(len)}; + } + + /** + * \brief Convert the certificate provided as base64 DER to PEM. + * + * This is useful when using with JWKs as x5c claim is encoded as base64 DER. More info + * (here)[https://tools.ietf.org/html/rfc7517#section-4.7] + * + * \tparam Decode is callabled, taking a string_type and returns a string_type. + * It should ensure the padding of the input and then base64 decode and return + * the results. + * + * \param cert_base64_der_str String containing the certificate encoded as base64 DER + * \param decode The function to decode the cert + * \param ec error_code for error_detection (gets cleared if no error occurs) + */ + template + std::string convert_base64_der_to_pem(const std::string& cert_base64_der_str, Decode decode, + std::error_code& ec) { + ec.clear(); + const auto decoded_str = decode(cert_base64_der_str); + return convert_der_to_pem(decoded_str, ec); + } + + /** + * \brief Convert the certificate provided as base64 DER to PEM. + * + * This is useful when using with JWKs as x5c claim is encoded as base64 DER. More info + * (here)[https://tools.ietf.org/html/rfc7517#section-4.7] + * + * \tparam Decode is callabled, taking a string_type and returns a string_type. + * It should ensure the padding of the input and then base64 decode and return + * the results. + * + * \param cert_base64_der_str String containing the certificate encoded as base64 DER + * \param decode The function to decode the cert + * \throw rsa_exception if an error occurred + */ + template + std::string convert_base64_der_to_pem(const std::string& cert_base64_der_str, Decode decode) { + std::error_code ec; + auto res = convert_base64_der_to_pem(cert_base64_der_str, std::move(decode), ec); + error::throw_if_error(ec); + return res; + } + + /** + * \brief Convert the certificate provided as DER to PEM. + * + * \param cert_der_str String containing the DER certificate + * \param decode The function to decode the cert + * \throw rsa_exception if an error occurred + */ + inline std::string convert_der_to_pem(const std::string& cert_der_str) { + std::error_code ec; + auto res = convert_der_to_pem(cert_der_str, ec); + error::throw_if_error(ec); + return res; + } + +#ifndef JWT_DISABLE_BASE64 + /** + * \brief Convert the certificate provided as base64 DER to PEM. + * + * This is useful when using with JWKs as x5c claim is encoded as base64 DER. More info + * (here)[https://tools.ietf.org/html/rfc7517#section-4.7] + * + * \param cert_base64_der_str String containing the certificate encoded as base64 DER + * \param ec error_code for error_detection (gets cleared if no error occurs) + */ + inline std::string convert_base64_der_to_pem(const std::string& cert_base64_der_str, std::error_code& ec) { + auto decode = [](const std::string& token) { + return base::decode(base::pad(token)); + }; + return convert_base64_der_to_pem(cert_base64_der_str, std::move(decode), ec); + } + + /** + * \brief Convert the certificate provided as base64 DER to PEM. + * + * This is useful when using with JWKs as x5c claim is encoded as base64 DER. More info + * (here)[https://tools.ietf.org/html/rfc7517#section-4.7] + * + * \param cert_base64_der_str String containing the certificate encoded as base64 DER + * \throw rsa_exception if an error occurred + */ + inline std::string convert_base64_der_to_pem(const std::string& cert_base64_der_str) { + std::error_code ec; + auto res = convert_base64_der_to_pem(cert_base64_der_str, ec); + error::throw_if_error(ec); + return res; + } +#endif + /** + * \brief Load a public key from a string. + * + * The string should contain a pem encoded certificate or public key + * + * \param key String containing the certificate encoded as pem + * \param password Password used to decrypt certificate (leave empty if not encrypted) + * \param ec error_code for error_detection (gets cleared if no error occurs) + */ + inline evp_pkey_handle load_public_key_from_string(const std::string& key, const std::string& password, + std::error_code& ec) { + ec.clear(); + auto pubkey_bio = make_mem_buf_bio(); + if (!pubkey_bio) { + ec = error::rsa_error::create_mem_bio_failed; + return {}; + } + if (key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { + auto epkey = helper::extract_pubkey_from_cert(key, password, ec); + if (ec) return {}; + const int len = static_cast(epkey.size()); + if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) { + ec = error::rsa_error::load_key_bio_write; + return {}; + } + } else { + const int len = static_cast(key.size()); + if (BIO_write(pubkey_bio.get(), key.data(), len) != len) { + ec = error::rsa_error::load_key_bio_write; + return {}; + } + } + + evp_pkey_handle pkey(PEM_read_bio_PUBKEY( + pubkey_bio.get(), nullptr, nullptr, + (void*)password.data())); // NOLINT(google-readability-casting) requires `const_cast` + if (!pkey) ec = error::rsa_error::load_key_bio_read; + return pkey; + } + + /** + * \brief Load a public key from a string. + * + * The string should contain a pem encoded certificate or public key + * + * \param key String containing the certificate or key encoded as pem + * \param password Password used to decrypt certificate or key (leave empty if not encrypted) + * \throw rsa_exception if an error occurred + */ + inline evp_pkey_handle load_public_key_from_string(const std::string& key, const std::string& password = "") { + std::error_code ec; + auto res = load_public_key_from_string(key, password, ec); + error::throw_if_error(ec); + return res; + } + + /** + * \brief Load a private key from a string. + * + * \param key String containing a private key as pem + * \param password Password used to decrypt key (leave empty if not encrypted) + * \param ec error_code for error_detection (gets cleared if no error occurs) + */ + inline evp_pkey_handle load_private_key_from_string(const std::string& key, const std::string& password, + std::error_code& ec) { + auto privkey_bio = make_mem_buf_bio(); + if (!privkey_bio) { + ec = error::rsa_error::create_mem_bio_failed; + return {}; + } + const int len = static_cast(key.size()); + if (BIO_write(privkey_bio.get(), key.data(), len) != len) { + ec = error::rsa_error::load_key_bio_write; + return {}; + } + evp_pkey_handle pkey( + PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str()))); + if (!pkey) ec = error::rsa_error::load_key_bio_read; + return pkey; + } + + /** + * \brief Load a private key from a string. + * + * \param key String containing a private key as pem + * \param password Password used to decrypt key (leave empty if not encrypted) + * \throw rsa_exception if an error occurred + */ + inline evp_pkey_handle load_private_key_from_string(const std::string& key, const std::string& password = "") { + std::error_code ec; + auto res = load_private_key_from_string(key, password, ec); + error::throw_if_error(ec); + return res; + } + + /** + * \brief Load a public key from a string. + * + * The string should contain a pem encoded certificate or public key + * + * \param key String containing the certificate encoded as pem + * \param password Password used to decrypt certificate (leave empty if not encrypted) + * \param ec error_code for error_detection (gets cleared if no error occurs) + */ + inline evp_pkey_handle load_public_ec_key_from_string(const std::string& key, const std::string& password, + std::error_code& ec) { + ec.clear(); + auto pubkey_bio = make_mem_buf_bio(); + if (!pubkey_bio) { + ec = error::ecdsa_error::create_mem_bio_failed; + return {}; + } + if (key.substr(0, 27) == "-----BEGIN CERTIFICATE-----") { + auto epkey = helper::extract_pubkey_from_cert(key, password, ec); + if (ec) return {}; + const int len = static_cast(epkey.size()); + if (BIO_write(pubkey_bio.get(), epkey.data(), len) != len) { + ec = error::ecdsa_error::load_key_bio_write; + return {}; + } + } else { + const int len = static_cast(key.size()); + if (BIO_write(pubkey_bio.get(), key.data(), len) != len) { + ec = error::ecdsa_error::load_key_bio_write; + return {}; + } + } + + evp_pkey_handle pkey(PEM_read_bio_PUBKEY( + pubkey_bio.get(), nullptr, nullptr, + (void*)password.data())); // NOLINT(google-readability-casting) requires `const_cast` + if (!pkey) ec = error::ecdsa_error::load_key_bio_read; + return pkey; + } + + /** + * \brief Load a public key from a string. + * + * The string should contain a pem encoded certificate or public key + * + * \param key String containing the certificate or key encoded as pem + * \param password Password used to decrypt certificate or key (leave empty if not encrypted) + * \throw ecdsa_exception if an error occurred + */ + inline evp_pkey_handle load_public_ec_key_from_string(const std::string& key, + const std::string& password = "") { + std::error_code ec; + auto res = load_public_ec_key_from_string(key, password, ec); + error::throw_if_error(ec); + return res; + } + + /** + * \brief Load a private key from a string. + * + * \param key String containing a private key as pem + * \param password Password used to decrypt key (leave empty if not encrypted) + * \param ec error_code for error_detection (gets cleared if no error occurs) + */ + inline evp_pkey_handle load_private_ec_key_from_string(const std::string& key, const std::string& password, + std::error_code& ec) { + auto privkey_bio = make_mem_buf_bio(); + if (!privkey_bio) { + ec = error::ecdsa_error::create_mem_bio_failed; + return {}; + } + const int len = static_cast(key.size()); + if (BIO_write(privkey_bio.get(), key.data(), len) != len) { + ec = error::ecdsa_error::load_key_bio_write; + return {}; + } + evp_pkey_handle pkey( + PEM_read_bio_PrivateKey(privkey_bio.get(), nullptr, nullptr, const_cast(password.c_str()))); + if (!pkey) ec = error::ecdsa_error::load_key_bio_read; + return pkey; + } + + /** + * \brief Load a private key from a string. + * + * \param key String containing a private key as pem + * \param password Password used to decrypt key (leave empty if not encrypted) + * \throw ecdsa_exception if an error occurred + */ + inline evp_pkey_handle load_private_ec_key_from_string(const std::string& key, + const std::string& password = "") { + std::error_code ec; + auto res = load_private_ec_key_from_string(key, password, ec); + error::throw_if_error(ec); + return res; + } + + /** + * Convert a OpenSSL BIGNUM to a std::string + * \param bn BIGNUM to convert + * \return bignum as string + */ + inline +#ifdef JWT_OPENSSL_1_0_0 + std::string + bn2raw(BIGNUM* bn) +#else + std::string + bn2raw(const BIGNUM* bn) +#endif + { + std::string res(BN_num_bytes(bn), '\0'); + BN_bn2bin(bn, (unsigned char*)res.data()); // NOLINT(google-readability-casting) requires `const_cast` + return res; + } + /** + * Convert an std::string to a OpenSSL BIGNUM + * \param raw String to convert + * \return BIGNUM representation + */ + inline std::unique_ptr raw2bn(const std::string& raw) { + return std::unique_ptr( + BN_bin2bn(reinterpret_cast(raw.data()), static_cast(raw.size()), nullptr), + BN_free); + } + } // namespace helper + + /** + * \brief Various cryptographic algorithms when working with JWT + * + * JWT (JSON Web Tokens) signatures are typically used as the payload for a JWS (JSON Web Signature) or + * JWE (JSON Web Encryption). Both of these use various cryptographic as specified by + * [RFC7518](https://tools.ietf.org/html/rfc7518) and are exposed through the a [JOSE + * Header](https://tools.ietf.org/html/rfc7515#section-4) which points to one of the JWA (JSON Web + * Algorithms)(https://tools.ietf.org/html/rfc7518#section-3.1) + */ + namespace algorithm { + /** + * \brief "none" algorithm. + * + * Returns and empty signature and checks if the given signature is empty. + */ + struct none { + /** + * \brief Return an empty string + */ + std::string sign(const std::string& /*unused*/, std::error_code& ec) const { + ec.clear(); + return {}; + } + /** + * \brief Check if the given signature is empty. + * + * JWT's with "none" algorithm should not contain a signature. + * \param signature Signature data to verify + * \param ec error_code filled with details about the error + */ + void verify(const std::string& /*unused*/, const std::string& signature, std::error_code& ec) const { + ec.clear(); + if (!signature.empty()) { ec = error::signature_verification_error::invalid_signature; } + } + /// Get algorithm name + std::string name() const { return "none"; } + }; + /** + * \brief Base class for HMAC family of algorithms + */ + struct hmacsha { + /** + * Construct new hmac algorithm + * \param key Key to use for HMAC + * \param md Pointer to hash function + * \param name Name of the algorithm + */ + hmacsha(std::string key, const EVP_MD* (*md)(), std::string name) + : secret(std::move(key)), md(md), alg_name(std::move(name)) {} + /** + * Sign jwt data + * \param data The data to sign + * \param ec error_code filled with details on error + * \return HMAC signature for the given data + */ + std::string sign(const std::string& data, std::error_code& ec) const { + ec.clear(); + std::string res(static_cast(EVP_MAX_MD_SIZE), '\0'); + auto len = static_cast(res.size()); + if (HMAC(md(), secret.data(), static_cast(secret.size()), + reinterpret_cast(data.data()), static_cast(data.size()), + (unsigned char*)res.data(), // NOLINT(google-readability-casting) requires `const_cast` + &len) == nullptr) { + ec = error::signature_generation_error::hmac_failed; + return {}; + } + res.resize(len); + return res; + } + /** + * Check if signature is valid + * \param data The data to check signature against + * \param signature Signature provided by the jwt + * \param ec Filled with details about failure. + */ + void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { + ec.clear(); + auto res = sign(data, ec); + if (ec) return; + + bool matched = true; + for (size_t i = 0; i < std::min(res.size(), signature.size()); i++) + if (res[i] != signature[i]) matched = false; + if (res.size() != signature.size()) matched = false; + if (!matched) { + ec = error::signature_verification_error::invalid_signature; + return; + } + } + /** + * Returns the algorithm name provided to the constructor + * \return algorithm's name + */ + std::string name() const { return alg_name; } + + private: + /// HMAC secrect + const std::string secret; + /// HMAC hash generator + const EVP_MD* (*md)(); + /// algorithm's name + const std::string alg_name; + }; + /** + * \brief Base class for RSA family of algorithms + */ + struct rsa { + /** + * Construct new rsa algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + * \param md Pointer to hash function + * \param name Name of the algorithm + */ + rsa(const std::string& public_key, const std::string& private_key, const std::string& public_key_password, + const std::string& private_key_password, const EVP_MD* (*md)(), std::string name) + : md(md), alg_name(std::move(name)) { + if (!private_key.empty()) { + pkey = helper::load_private_key_from_string(private_key, private_key_password); + } else if (!public_key.empty()) { + pkey = helper::load_public_key_from_string(public_key, public_key_password); + } else + throw error::rsa_exception(error::rsa_error::no_key_provided); + } + /** + * Sign jwt data + * \param data The data to sign + * \param ec error_code filled with details on error + * \return RSA signature for the given data + */ + std::string sign(const std::string& data, std::error_code& ec) const { + ec.clear(); + auto ctx = helper::make_evp_md_ctx(); + if (!ctx) { + ec = error::signature_generation_error::create_context_failed; + return {}; + } + if (!EVP_SignInit(ctx.get(), md())) { + ec = error::signature_generation_error::signinit_failed; + return {}; + } + + std::string res(EVP_PKEY_size(pkey.get()), '\0'); + unsigned int len = 0; + + if (!EVP_SignUpdate(ctx.get(), data.data(), data.size())) { + ec = error::signature_generation_error::signupdate_failed; + return {}; + } + if (EVP_SignFinal(ctx.get(), (unsigned char*)res.data(), &len, pkey.get()) == 0) { + ec = error::signature_generation_error::signfinal_failed; + return {}; + } + + res.resize(len); + return res; + } + /** + * Check if signature is valid + * \param data The data to check signature against + * \param signature Signature provided by the jwt + * \param ec Filled with details on failure + */ + void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { + ec.clear(); + auto ctx = helper::make_evp_md_ctx(); + if (!ctx) { + ec = error::signature_verification_error::create_context_failed; + return; + } + if (!EVP_VerifyInit(ctx.get(), md())) { + ec = error::signature_verification_error::verifyinit_failed; + return; + } + if (!EVP_VerifyUpdate(ctx.get(), data.data(), data.size())) { + ec = error::signature_verification_error::verifyupdate_failed; + return; + } + auto res = EVP_VerifyFinal(ctx.get(), reinterpret_cast(signature.data()), + static_cast(signature.size()), pkey.get()); + if (res != 1) { + ec = error::signature_verification_error::verifyfinal_failed; + return; + } + } + /** + * Returns the algorithm name provided to the constructor + * \return algorithm's name + */ + std::string name() const { return alg_name; } + + private: + /// OpenSSL structure containing converted keys + helper::evp_pkey_handle pkey; + /// Hash generator + const EVP_MD* (*md)(); + /// algorithm's name + const std::string alg_name; + }; + /** + * \brief Base class for ECDSA family of algorithms + */ + struct ecdsa { + /** + * Construct new ecdsa algorithm + * + * \param public_key ECDSA public key in PEM format + * \param private_key ECDSA private key or empty string if not available. If empty, signing will always fail + * \param public_key_password Password to decrypt public key pem + * \param private_key_password Password to decrypt private key pem + * \param md Pointer to hash function + * \param name Name of the algorithm + * \param siglen The bit length of the signature + */ + ecdsa(const std::string& public_key, const std::string& private_key, const std::string& public_key_password, + const std::string& private_key_password, const EVP_MD* (*md)(), std::string name, size_t siglen) + : md(md), alg_name(std::move(name)), signature_length(siglen) { + if (!private_key.empty()) { + pkey = helper::load_private_ec_key_from_string(private_key, private_key_password); + check_private_key(pkey.get()); + } else if (!public_key.empty()) { + pkey = helper::load_public_ec_key_from_string(public_key, public_key_password); + check_public_key(pkey.get()); + } else { + throw error::ecdsa_exception(error::ecdsa_error::no_key_provided); + } + if (!pkey) throw error::ecdsa_exception(error::ecdsa_error::invalid_key); + + size_t keysize = EVP_PKEY_bits(pkey.get()); + if (keysize != signature_length * 4 && (signature_length != 132 || keysize != 521)) + throw error::ecdsa_exception(error::ecdsa_error::invalid_key_size); + } + + /** + * Sign jwt data + * \param data The data to sign + * \param ec error_code filled with details on error + * \return ECDSA signature for the given data + */ + std::string sign(const std::string& data, std::error_code& ec) const { + ec.clear(); + auto ctx = helper::make_evp_md_ctx(); + if (!ctx) { + ec = error::signature_generation_error::create_context_failed; + return {}; + } + if (!EVP_DigestSignInit(ctx.get(), nullptr, md(), nullptr, pkey.get())) { + ec = error::signature_generation_error::signinit_failed; + return {}; + } + if (!EVP_DigestUpdate(ctx.get(), data.data(), data.size())) { + ec = error::signature_generation_error::digestupdate_failed; + return {}; + } + + size_t len = 0; + if (!EVP_DigestSignFinal(ctx.get(), nullptr, &len)) { + ec = error::signature_generation_error::signfinal_failed; + return {}; + } + std::string res(len, '\0'); + if (!EVP_DigestSignFinal(ctx.get(), (unsigned char*)res.data(), &len)) { + ec = error::signature_generation_error::signfinal_failed; + return {}; + } + + res.resize(len); + return der_to_p1363_signature(res, ec); + } + + /** + * Check if signature is valid + * \param data The data to check signature against + * \param signature Signature provided by the jwt + * \param ec Filled with details on error + */ + void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { + ec.clear(); + std::string der_signature = p1363_to_der_signature(signature, ec); + if (ec) { return; } + + auto ctx = helper::make_evp_md_ctx(); + if (!ctx) { + ec = error::signature_verification_error::create_context_failed; + return; + } + if (!EVP_DigestVerifyInit(ctx.get(), nullptr, md(), nullptr, pkey.get())) { + ec = error::signature_verification_error::verifyinit_failed; + return; + } + if (!EVP_DigestUpdate(ctx.get(), data.data(), data.size())) { + ec = error::signature_verification_error::verifyupdate_failed; + return; + } + +#if OPENSSL_VERSION_NUMBER < 0x10002000L + unsigned char* der_sig_data = reinterpret_cast(const_cast(der_signature.data())); +#else + const unsigned char* der_sig_data = reinterpret_cast(der_signature.data()); +#endif + auto res = + EVP_DigestVerifyFinal(ctx.get(), der_sig_data, static_cast(der_signature.length())); + if (res == 0) { + ec = error::signature_verification_error::invalid_signature; + return; + } + if (res == -1) { + ec = error::signature_verification_error::verifyfinal_failed; + return; + } + } + /** + * Returns the algorithm name provided to the constructor + * \return algorithm's name + */ + std::string name() const { return alg_name; } + + private: + static void check_public_key(EVP_PKEY* pkey) { +#ifdef JWT_OPENSSL_3_0 + std::unique_ptr ctx( + EVP_PKEY_CTX_new_from_pkey(nullptr, pkey, nullptr), EVP_PKEY_CTX_free); + if (!ctx) { throw error::ecdsa_exception(error::ecdsa_error::create_context_failed); } + if (EVP_PKEY_public_check(ctx.get()) != 1) { + throw error::ecdsa_exception(error::ecdsa_error::invalid_key); + } +#else + std::unique_ptr eckey(EVP_PKEY_get1_EC_KEY(pkey), EC_KEY_free); + if (!eckey) { throw error::ecdsa_exception(error::ecdsa_error::invalid_key); } + if (EC_KEY_check_key(eckey.get()) == 0) throw error::ecdsa_exception(error::ecdsa_error::invalid_key); +#endif + } + + static void check_private_key(EVP_PKEY* pkey) { +#ifdef JWT_OPENSSL_3_0 + std::unique_ptr ctx( + EVP_PKEY_CTX_new_from_pkey(nullptr, pkey, nullptr), EVP_PKEY_CTX_free); + if (!ctx) { throw error::ecdsa_exception(error::ecdsa_error::create_context_failed); } + if (EVP_PKEY_private_check(ctx.get()) != 1) { + throw error::ecdsa_exception(error::ecdsa_error::invalid_key); + } +#else + std::unique_ptr eckey(EVP_PKEY_get1_EC_KEY(pkey), EC_KEY_free); + if (!eckey) { throw error::ecdsa_exception(error::ecdsa_error::invalid_key); } + if (EC_KEY_check_key(eckey.get()) == 0) throw error::ecdsa_exception(error::ecdsa_error::invalid_key); +#endif + } + + std::string der_to_p1363_signature(const std::string& der_signature, std::error_code& ec) const { + const unsigned char* possl_signature = reinterpret_cast(der_signature.data()); + std::unique_ptr sig( + d2i_ECDSA_SIG(nullptr, &possl_signature, static_cast(der_signature.length())), + ECDSA_SIG_free); + if (!sig) { + ec = error::signature_generation_error::signature_decoding_failed; + return {}; + } + +#ifdef JWT_OPENSSL_1_0_0 + + auto rr = helper::bn2raw(sig->r); + auto rs = helper::bn2raw(sig->s); +#else + const BIGNUM* r; + const BIGNUM* s; + ECDSA_SIG_get0(sig.get(), &r, &s); + auto rr = helper::bn2raw(r); + auto rs = helper::bn2raw(s); +#endif + if (rr.size() > signature_length / 2 || rs.size() > signature_length / 2) + throw std::logic_error("bignum size exceeded expected length"); + rr.insert(0, signature_length / 2 - rr.size(), '\0'); + rs.insert(0, signature_length / 2 - rs.size(), '\0'); + return rr + rs; + } + + std::string p1363_to_der_signature(const std::string& signature, std::error_code& ec) const { + ec.clear(); + auto r = helper::raw2bn(signature.substr(0, signature.size() / 2)); + auto s = helper::raw2bn(signature.substr(signature.size() / 2)); + + ECDSA_SIG* psig; +#ifdef JWT_OPENSSL_1_0_0 + ECDSA_SIG sig; + sig.r = r.get(); + sig.s = s.get(); + psig = &sig; +#else + std::unique_ptr sig(ECDSA_SIG_new(), ECDSA_SIG_free); + if (!sig) { + ec = error::signature_verification_error::create_context_failed; + return {}; + } + ECDSA_SIG_set0(sig.get(), r.release(), s.release()); + psig = sig.get(); +#endif + + int length = i2d_ECDSA_SIG(psig, nullptr); + if (length < 0) { + ec = error::signature_verification_error::signature_encoding_failed; + return {}; + } + std::string der_signature(length, '\0'); + unsigned char* psbuffer = (unsigned char*)der_signature.data(); + length = i2d_ECDSA_SIG(psig, &psbuffer); + if (length < 0) { + ec = error::signature_verification_error::signature_encoding_failed; + return {}; + } + der_signature.resize(length); + return der_signature; + } + + /// OpenSSL struct containing keys + helper::evp_pkey_handle pkey; + /// Hash generator function + const EVP_MD* (*md)(); + /// algorithm's name + const std::string alg_name; + /// Length of the resulting signature + const size_t signature_length; + }; + +#if !defined(JWT_OPENSSL_1_0_0) && !defined(JWT_OPENSSL_1_1_0) + /** + * \brief Base class for EdDSA family of algorithms + * + * https://tools.ietf.org/html/rfc8032 + * + * The EdDSA algorithms were introduced in [OpenSSL v1.1.1](https://www.openssl.org/news/openssl-1.1.1-notes.html), + * so these algorithms are only available when building against this version or higher. + */ + struct eddsa { + /** + * Construct new eddsa algorithm + * \param public_key EdDSA public key in PEM format + * \param private_key EdDSA private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password + * to decrypt private key pem. + * \param name Name of the algorithm + */ + eddsa(const std::string& public_key, const std::string& private_key, const std::string& public_key_password, + const std::string& private_key_password, std::string name) + : alg_name(std::move(name)) { + if (!private_key.empty()) { + pkey = helper::load_private_key_from_string(private_key, private_key_password); + } else if (!public_key.empty()) { + pkey = helper::load_public_key_from_string(public_key, public_key_password); + } else + throw error::ecdsa_exception(error::ecdsa_error::load_key_bio_read); + } + /** + * Sign jwt data + * \param data The data to sign + * \param ec error_code filled with details on error + * \return EdDSA signature for the given data + */ + std::string sign(const std::string& data, std::error_code& ec) const { + ec.clear(); + auto ctx = helper::make_evp_md_ctx(); + if (!ctx) { + ec = error::signature_generation_error::create_context_failed; + return {}; + } + if (!EVP_DigestSignInit(ctx.get(), nullptr, nullptr, nullptr, pkey.get())) { + ec = error::signature_generation_error::signinit_failed; + return {}; + } + + size_t len = EVP_PKEY_size(pkey.get()); + std::string res(len, '\0'); + +// LibreSSL is the special kid in the block, as it does not support EVP_DigestSign. +// OpenSSL on the otherhand does not support using EVP_DigestSignUpdate for eddsa, which is why we end up with this +// mess. +#if defined(LIBRESSL_VERSION_NUMBER) || defined(LIBWOLFSSL_VERSION_HEX) + ERR_clear_error(); + if (EVP_DigestSignUpdate(ctx.get(), reinterpret_cast(data.data()), data.size()) != + 1) { + std::cout << ERR_error_string(ERR_get_error(), NULL) << std::endl; + ec = error::signature_generation_error::signupdate_failed; + return {}; + } + if (EVP_DigestSignFinal(ctx.get(), reinterpret_cast(&res[0]), &len) != 1) { + ec = error::signature_generation_error::signfinal_failed; + return {}; + } +#else + if (EVP_DigestSign(ctx.get(), reinterpret_cast(&res[0]), &len, + reinterpret_cast(data.data()), data.size()) != 1) { + ec = error::signature_generation_error::signfinal_failed; + return {}; + } +#endif + + res.resize(len); + return res; + } + + /** + * Check if signature is valid + * \param data The data to check signature against + * \param signature Signature provided by the jwt + * \param ec Filled with details on error + */ + void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { + ec.clear(); + auto ctx = helper::make_evp_md_ctx(); + if (!ctx) { + ec = error::signature_verification_error::create_context_failed; + return; + } + if (!EVP_DigestVerifyInit(ctx.get(), nullptr, nullptr, nullptr, pkey.get())) { + ec = error::signature_verification_error::verifyinit_failed; + return; + } +// LibreSSL is the special kid in the block, as it does not support EVP_DigestVerify. +// OpenSSL on the otherhand does not support using EVP_DigestVerifyUpdate for eddsa, which is why we end up with this +// mess. +#if defined(LIBRESSL_VERSION_NUMBER) || defined(LIBWOLFSSL_VERSION_HEX) + if (EVP_DigestVerifyUpdate(ctx.get(), reinterpret_cast(data.data()), + data.size()) != 1) { + ec = error::signature_verification_error::verifyupdate_failed; + return; + } + if (EVP_DigestVerifyFinal(ctx.get(), reinterpret_cast(signature.data()), + signature.size()) != 1) { + ec = error::signature_verification_error::verifyfinal_failed; + return; + } +#else + auto res = EVP_DigestVerify(ctx.get(), reinterpret_cast(signature.data()), + signature.size(), reinterpret_cast(data.data()), + data.size()); + if (res != 1) { + ec = error::signature_verification_error::verifyfinal_failed; + return; + } +#endif + } + /** + * Returns the algorithm name provided to the constructor + * \return algorithm's name + */ + std::string name() const { return alg_name; } + + private: + /// OpenSSL struct containing keys + helper::evp_pkey_handle pkey; + /// algorithm's name + const std::string alg_name; + }; +#endif + /** + * \brief Base class for PSS-RSA family of algorithms + */ + struct pss { + /** + * Construct new pss algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + * \param md Pointer to hash function + * \param name Name of the algorithm + */ + pss(const std::string& public_key, const std::string& private_key, const std::string& public_key_password, + const std::string& private_key_password, const EVP_MD* (*md)(), std::string name) + : md(md), alg_name(std::move(name)) { + if (!private_key.empty()) { + pkey = helper::load_private_key_from_string(private_key, private_key_password); + } else if (!public_key.empty()) { + pkey = helper::load_public_key_from_string(public_key, public_key_password); + } else + throw error::rsa_exception(error::rsa_error::no_key_provided); + } + + /** + * Sign jwt data + * \param data The data to sign + * \param ec error_code filled with details on error + * \return ECDSA signature for the given data + */ + std::string sign(const std::string& data, std::error_code& ec) const { + ec.clear(); + auto md_ctx = helper::make_evp_md_ctx(); + if (!md_ctx) { + ec = error::signature_generation_error::create_context_failed; + return {}; + } + EVP_PKEY_CTX* ctx = nullptr; + if (EVP_DigestSignInit(md_ctx.get(), &ctx, md(), nullptr, pkey.get()) != 1) { + ec = error::signature_generation_error::signinit_failed; + return {}; + } + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PSS_PADDING) <= 0) { + ec = error::signature_generation_error::rsa_padding_failed; + return {}; + } +// wolfSSL does not require EVP_PKEY_CTX_set_rsa_pss_saltlen. The default behavior +// sets the salt length to the hash length. Unlike OpenSSL which exposes this functionality. +#ifndef LIBWOLFSSL_VERSION_HEX + if (EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, -1) <= 0) { + ec = error::signature_generation_error::set_rsa_pss_saltlen_failed; + return {}; + } +#endif + if (EVP_DigestUpdate(md_ctx.get(), data.data(), data.size()) != 1) { + ec = error::signature_generation_error::digestupdate_failed; + return {}; + } + + size_t size = EVP_PKEY_size(pkey.get()); + std::string res(size, 0x00); + if (EVP_DigestSignFinal( + md_ctx.get(), + (unsigned char*)res.data(), // NOLINT(google-readability-casting) requires `const_cast` + &size) <= 0) { + ec = error::signature_generation_error::signfinal_failed; + return {}; + } + + return res; + } + + /** + * Check if signature is valid + * \param data The data to check signature against + * \param signature Signature provided by the jwt + * \param ec Filled with error details + */ + void verify(const std::string& data, const std::string& signature, std::error_code& ec) const { + ec.clear(); + + auto md_ctx = helper::make_evp_md_ctx(); + if (!md_ctx) { + ec = error::signature_verification_error::create_context_failed; + return; + } + EVP_PKEY_CTX* ctx = nullptr; + if (EVP_DigestVerifyInit(md_ctx.get(), &ctx, md(), nullptr, pkey.get()) != 1) { + ec = error::signature_verification_error::verifyinit_failed; + return; + } + if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_PSS_PADDING) <= 0) { + ec = error::signature_generation_error::rsa_padding_failed; + return; + } +// wolfSSL does not require EVP_PKEY_CTX_set_rsa_pss_saltlen. The default behavior +// sets the salt length to the hash length. Unlike OpenSSL which exposes this functionality. +#ifndef LIBWOLFSSL_VERSION_HEX + if (EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, -1) <= 0) { + ec = error::signature_verification_error::set_rsa_pss_saltlen_failed; + return; + } +#endif + if (EVP_DigestUpdate(md_ctx.get(), data.data(), data.size()) != 1) { + ec = error::signature_verification_error::verifyupdate_failed; + return; + } + + if (EVP_DigestVerifyFinal(md_ctx.get(), (unsigned char*)signature.data(), signature.size()) <= 0) { + ec = error::signature_verification_error::verifyfinal_failed; + return; + } + } + /** + * Returns the algorithm name provided to the constructor + * \return algorithm's name + */ + std::string name() const { return alg_name; } + + private: + /// OpenSSL structure containing keys + helper::evp_pkey_handle pkey; + /// Hash generator function + const EVP_MD* (*md)(); + /// algorithm's name + const std::string alg_name; + }; + + /** + * HS256 algorithm + */ + struct hs256 : public hmacsha { + /** + * Construct new instance of algorithm + * \param key HMAC signing key + */ + explicit hs256(std::string key) : hmacsha(std::move(key), EVP_sha256, "HS256") {} + }; + /** + * HS384 algorithm + */ + struct hs384 : public hmacsha { + /** + * Construct new instance of algorithm + * \param key HMAC signing key + */ + explicit hs384(std::string key) : hmacsha(std::move(key), EVP_sha384, "HS384") {} + }; + /** + * HS512 algorithm + */ + struct hs512 : public hmacsha { + /** + * Construct new instance of algorithm + * \param key HMAC signing key + */ + explicit hs512(std::string key) : hmacsha(std::move(key), EVP_sha512, "HS512") {} + }; + /** + * RS256 algorithm + */ + struct rs256 : public rsa { + /** + * Construct new instance of algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit rs256(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : rsa(public_key, private_key, public_key_password, private_key_password, EVP_sha256, "RS256") {} + }; + /** + * RS384 algorithm + */ + struct rs384 : public rsa { + /** + * Construct new instance of algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit rs384(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : rsa(public_key, private_key, public_key_password, private_key_password, EVP_sha384, "RS384") {} + }; + /** + * RS512 algorithm + */ + struct rs512 : public rsa { + /** + * Construct new instance of algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit rs512(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : rsa(public_key, private_key, public_key_password, private_key_password, EVP_sha512, "RS512") {} + }; + /** + * ES256 algorithm + */ + struct es256 : public ecdsa { + /** + * Construct new instance of algorithm + * \param public_key ECDSA public key in PEM format + * \param private_key ECDSA private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password + * to decrypt private key pem. + */ + explicit es256(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : ecdsa(public_key, private_key, public_key_password, private_key_password, EVP_sha256, "ES256", 64) {} + }; + /** + * ES384 algorithm + */ + struct es384 : public ecdsa { + /** + * Construct new instance of algorithm + * \param public_key ECDSA public key in PEM format + * \param private_key ECDSA private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password + * to decrypt private key pem. + */ + explicit es384(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : ecdsa(public_key, private_key, public_key_password, private_key_password, EVP_sha384, "ES384", 96) {} + }; + /** + * ES512 algorithm + */ + struct es512 : public ecdsa { + /** + * Construct new instance of algorithm + * \param public_key ECDSA public key in PEM format + * \param private_key ECDSA private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password + * to decrypt private key pem. + */ + explicit es512(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : ecdsa(public_key, private_key, public_key_password, private_key_password, EVP_sha512, "ES512", 132) {} + }; + /** + * ES256K algorithm + */ + struct es256k : public ecdsa { + /** + * Construct new instance of algorithm + * \param public_key ECDSA public key in PEM format + * \param private_key ECDSA private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit es256k(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : ecdsa(public_key, private_key, public_key_password, private_key_password, EVP_sha256, "ES256K", 64) {} + }; + +#if !defined(JWT_OPENSSL_1_0_0) && !defined(JWT_OPENSSL_1_1_0) + /** + * Ed25519 algorithm + * + * https://en.wikipedia.org/wiki/EdDSA#Ed25519 + * + * Requires at least OpenSSL 1.1.1. + */ + struct ed25519 : public eddsa { + /** + * Construct new instance of algorithm + * \param public_key Ed25519 public key in PEM format + * \param private_key Ed25519 private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password + * to decrypt private key pem. + */ + explicit ed25519(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : eddsa(public_key, private_key, public_key_password, private_key_password, "EdDSA") {} + }; + + /** + * Ed448 algorithm + * + * https://en.wikipedia.org/wiki/EdDSA#Ed448 + * + * Requires at least OpenSSL 1.1.1. + */ + struct ed448 : public eddsa { + /** + * Construct new instance of algorithm + * \param public_key Ed448 public key in PEM format + * \param private_key Ed448 private key or empty string if not available. If empty, signing will always + * fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password + * to decrypt private key pem. + */ + explicit ed448(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : eddsa(public_key, private_key, public_key_password, private_key_password, "EdDSA") {} + }; +#endif + + /** + * PS256 algorithm + */ + struct ps256 : public pss { + /** + * Construct new instance of algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit ps256(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : pss(public_key, private_key, public_key_password, private_key_password, EVP_sha256, "PS256") {} + }; + /** + * PS384 algorithm + */ + struct ps384 : public pss { + /** + * Construct new instance of algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit ps384(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : pss(public_key, private_key, public_key_password, private_key_password, EVP_sha384, "PS384") {} + }; + /** + * PS512 algorithm + */ + struct ps512 : public pss { + /** + * Construct new instance of algorithm + * \param public_key RSA public key in PEM format + * \param private_key RSA private key or empty string if not available. If empty, signing will always fail. + * \param public_key_password Password to decrypt public key pem. + * \param private_key_password Password to decrypt private key pem. + */ + explicit ps512(const std::string& public_key, const std::string& private_key = "", + const std::string& public_key_password = "", const std::string& private_key_password = "") + : pss(public_key, private_key, public_key_password, private_key_password, EVP_sha512, "PS512") {} + }; + } // namespace algorithm + + /** + * \brief JSON Abstractions for working with any library + */ + namespace json { + /** + * \brief Generic JSON types used in JWTs + * + * This enum is to abstract the third party underlying types + */ + enum class type { boolean, integer, number, string, array, object }; + } // namespace json + + namespace details { +#ifdef __cpp_lib_void_t + template + using void_t = std::void_t; +#else + // https://en.cppreference.com/w/cpp/types/void_t + template + struct make_void { + using type = void; + }; + + template + using void_t = typename make_void::type; +#endif + +#ifdef __cpp_lib_experimental_detect + template class _Op, typename... _Args> + using is_detected = std::experimental::is_detected<_Op, _Args...>; +#else + struct nonesuch { + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + nonesuch(nonesuch const&&) = delete; + void operator=(nonesuch const&) = delete; + void operator=(nonesuch&&) = delete; + }; + + // https://en.cppreference.com/w/cpp/experimental/is_detected + template class Op, class... Args> + struct detector { + using value = std::false_type; + using type = Default; + }; + + template class Op, class... Args> + struct detector>, Op, Args...> { + using value = std::true_type; + using type = Op; + }; + + template class Op, class... Args> + using is_detected = typename detector::value; +#endif + + template + using is_signature = typename std::is_same; + + template class Op, typename Signature> + struct is_function_signature_detected { + using type = Op; + static constexpr auto value = is_detected::value && std::is_function::value && + is_signature::value; + }; + + template + struct supports_get_type { + template + using get_type_t = decltype(T::get_type); + + static constexpr auto value = + is_function_signature_detected::value; + + // Internal assertions for better feedback + static_assert(value, "traits implementation must provide `jwt::json::type get_type(const value_type&)`"); + }; + +#define JWT_CPP_JSON_TYPE_TYPE(TYPE) json_##TYPE_type +#define JWT_CPP_AS_TYPE_T(TYPE) as_##TYPE_t +#define JWT_CPP_SUPPORTS_AS(TYPE) \ + template \ + struct supports_as_##TYPE { \ + template \ + using JWT_CPP_AS_TYPE_T(TYPE) = decltype(T::as_##TYPE); \ + \ + static constexpr auto value = \ + is_function_signature_detected::value; \ + \ + static_assert(value, "traits implementation must provide `" #TYPE "_type as_" #TYPE "(const value_type&)`"); \ + } + + JWT_CPP_SUPPORTS_AS(object); + JWT_CPP_SUPPORTS_AS(array); + JWT_CPP_SUPPORTS_AS(string); + JWT_CPP_SUPPORTS_AS(number); + JWT_CPP_SUPPORTS_AS(integer); + JWT_CPP_SUPPORTS_AS(boolean); + +#undef JWT_CPP_JSON_TYPE_TYPE +#undef JWT_CPP_AS_TYPE_T +#undef JWT_CPP_SUPPORTS_AS + + template + struct is_valid_traits { + static constexpr auto value = + supports_get_type::value && + supports_as_object::value && + supports_as_array::value && + supports_as_string::value && + supports_as_number::value && + supports_as_integer::value && + supports_as_boolean::value; + }; + + template + struct is_valid_json_value { + static constexpr auto value = + std::is_default_constructible::value && + std::is_constructible::value && // a more generic is_copy_constructible + std::is_move_constructible::value && std::is_assignable::value && + std::is_copy_assignable::value && std::is_move_assignable::value; + // TODO(prince-chrismc): Stream operators + }; + + // https://stackoverflow.com/a/53967057/8480874 + template + struct is_iterable : std::false_type {}; + + template + struct is_iterable())), decltype(std::end(std::declval())), +#if __cplusplus > 201402L + decltype(std::cbegin(std::declval())), decltype(std::cend(std::declval())) +#else + decltype(std::begin(std::declval())), + decltype(std::end(std::declval())) +#endif + >> : std::true_type { + }; + +#if __cplusplus > 201703L + template + inline constexpr bool is_iterable_v = is_iterable::value; +#endif + + template + using is_count_signature = typename std::is_integral().count( + std::declval()))>; + + template + struct is_subcription_operator_signature : std::false_type {}; + + template + struct is_subcription_operator_signature< + object_type, string_type, + void_t().operator[](std::declval()))>> : std::true_type { + // TODO(prince-chrismc): I am not convienced this is meaningful anymore + static_assert( + value, + "object_type must implementate the subscription operator '[]' taking string_type as an argument"); + }; + + template + using is_at_const_signature = + typename std::is_same().at(std::declval())), + const value_type&>; + + template + struct is_valid_json_object { + template + using mapped_type_t = typename T::mapped_type; + template + using key_type_t = typename T::key_type; + template + using iterator_t = typename T::iterator; + template + using const_iterator_t = typename T::const_iterator; + + static constexpr auto value = + std::is_constructible::value && + is_detected::value && + std::is_same::value && + is_detected::value && + (std::is_same::value || + std::is_constructible::value) && + is_detected::value && is_detected::value && + is_iterable::value && is_count_signature::value && + is_subcription_operator_signature::value && + is_at_const_signature::value; + }; + + template + struct is_valid_json_array { + template + using value_type_t = typename T::value_type; + + static constexpr auto value = std::is_constructible::value && + is_iterable::value && + is_detected::value && + std::is_same::value; + }; + + template + using is_substr_start_end_index_signature = + typename std::is_same().substr(std::declval(), + std::declval())), + string_type>; + + template + using is_substr_start_index_signature = + typename std::is_same().substr(std::declval())), + string_type>; + + template + using is_std_operate_plus_signature = + typename std::is_same(), std::declval())), + string_type>; + + template + struct is_valid_json_string { + static constexpr auto substr = is_substr_start_end_index_signature::value && + is_substr_start_index_signature::value; + static_assert(substr, "string_type must have a substr method taking only a start index and an overload " + "taking a start and end index, both must return a string_type"); + + static constexpr auto operator_plus = is_std_operate_plus_signature::value; + static_assert(operator_plus, + "string_type must have a '+' operator implemented which returns the concatenated string"); + + static constexpr auto value = + std::is_constructible::value && substr && operator_plus; + }; + + template + struct is_valid_json_number { + static constexpr auto value = + std::is_floating_point::value && std::is_constructible::value; + }; + + template + struct is_valid_json_integer { + static constexpr auto value = std::is_signed::value && + !std::is_floating_point::value && + std::is_constructible::value; + }; + template + struct is_valid_json_boolean { + static constexpr auto value = std::is_convertible::value && + std::is_constructible::value; + }; + + template + struct is_valid_json_types { + // Internal assertions for better feedback + static_assert(is_valid_json_value::value, + "value_type must meet basic requirements, default constructor, copyable, moveable"); + static_assert(is_valid_json_object::value, + "object_type must be a string_type to value_type container"); + static_assert(is_valid_json_array::value, + "array_type must be a container of value_type"); + + static constexpr auto value = is_valid_json_value::value && + is_valid_json_object::value && + is_valid_json_array::value && + is_valid_json_string::value && + is_valid_json_number::value && + is_valid_json_integer::value && + is_valid_json_boolean::value; + }; + } // namespace details + + /** + * \brief a class to store a generic JSON value as claim + * + * \tparam json_traits : JSON implementation traits + * + * \see [RFC 7519: JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) + */ + template + class basic_claim { + /** + * The reason behind this is to provide an expressive abstraction without + * over complexifying the API. For more information take the time to read + * https://github.com/nlohmann/json/issues/774. It maybe be expanded to + * support custom string types. + */ + static_assert(std::is_same::value || + std::is_convertible::value || + std::is_constructible::value, + "string_type must be a std::string, convertible to a std::string, or construct a std::string."); + + static_assert( + details::is_valid_json_types::value, + "must staisfy json container requirements"); + static_assert(details::is_valid_traits::value, "traits must satisfy requirements"); + + typename json_traits::value_type val; + + public: + using set_t = std::set; + + basic_claim() = default; + basic_claim(const basic_claim&) = default; + basic_claim(basic_claim&&) = default; + basic_claim& operator=(const basic_claim&) = default; + basic_claim& operator=(basic_claim&&) = default; + ~basic_claim() = default; + + JWT_CLAIM_EXPLICIT basic_claim(typename json_traits::string_type s) : val(std::move(s)) {} + JWT_CLAIM_EXPLICIT basic_claim(const date& d) + : val(typename json_traits::integer_type(std::chrono::system_clock::to_time_t(d))) {} + JWT_CLAIM_EXPLICIT basic_claim(typename json_traits::array_type a) : val(std::move(a)) {} + JWT_CLAIM_EXPLICIT basic_claim(typename json_traits::value_type v) : val(std::move(v)) {} + JWT_CLAIM_EXPLICIT basic_claim(const set_t& s) : val(typename json_traits::array_type(s.begin(), s.end())) {} + template + basic_claim(Iterator begin, Iterator end) : val(typename json_traits::array_type(begin, end)) {} + + /** + * Get wrapped JSON value + * \return Wrapped JSON value + */ + typename json_traits::value_type to_json() const { return val; } + + /** + * Parse input stream into underlying JSON value + * \return input stream + */ + std::istream& operator>>(std::istream& is) { return is >> val; } + + /** + * Serialize claim to output stream from wrapped JSON value + * \return output stream + */ + std::ostream& operator<<(std::ostream& os) { return os << val; } + + /** + * Get type of contained JSON value + * \return Type + * \throw std::logic_error An internal error occurred + */ + json::type get_type() const { return json_traits::get_type(val); } + + /** + * Get the contained JSON value as a string + * \return content as string + * \throw std::bad_cast Content was not a string + */ + typename json_traits::string_type as_string() const { return json_traits::as_string(val); } + + /** + * \brief Get the contained JSON value as a date + * + * If the value is a decimal, it is rounded up to the closest integer + * + * \return content as date + * \throw std::bad_cast Content was not a date + */ + date as_date() const { + using std::chrono::system_clock; + if (get_type() == json::type::number) return system_clock::from_time_t(std::round(as_number())); + return system_clock::from_time_t(as_integer()); + } + + /** + * Get the contained JSON value as an array + * \return content as array + * \throw std::bad_cast Content was not an array + */ + typename json_traits::array_type as_array() const { return json_traits::as_array(val); } + + /** + * Get the contained JSON value as a set of strings + * \return content as set of strings + * \throw std::bad_cast Content was not an array of string + */ + set_t as_set() const { + set_t res; + for (const auto& e : json_traits::as_array(val)) { + res.insert(json_traits::as_string(e)); + } + return res; + } + + /** + * Get the contained JSON value as an integer + * \return content as int + * \throw std::bad_cast Content was not an int + */ + typename json_traits::integer_type as_integer() const { return json_traits::as_integer(val); } + + /** + * Get the contained JSON value as a bool + * \return content as bool + * \throw std::bad_cast Content was not a bool + */ + typename json_traits::boolean_type as_boolean() const { return json_traits::as_boolean(val); } + + /** + * Get the contained JSON value as a number + * \return content as double + * \throw std::bad_cast Content was not a number + */ + typename json_traits::number_type as_number() const { return json_traits::as_number(val); } + }; + + namespace error { + /** + * Attempt to parse JSON was unsuccessful + */ + struct invalid_json_exception : public std::runtime_error { + invalid_json_exception() : runtime_error("invalid json") {} + }; + /** + * Attempt to access claim was unsuccessful + */ + struct claim_not_present_exception : public std::out_of_range { + claim_not_present_exception() : out_of_range("claim not found") {} + }; + } // namespace error + + namespace details { + template + struct map_of_claims { + typename json_traits::object_type claims; + using basic_claim_t = basic_claim; + using iterator = typename json_traits::object_type::iterator; + using const_iterator = typename json_traits::object_type::const_iterator; + + map_of_claims() = default; + map_of_claims(const map_of_claims&) = default; + map_of_claims(map_of_claims&&) = default; + map_of_claims& operator=(const map_of_claims&) = default; + map_of_claims& operator=(map_of_claims&&) = default; + + map_of_claims(typename json_traits::object_type json) : claims(std::move(json)) {} + + iterator begin() { return claims.begin(); } + iterator end() { return claims.end(); } + const_iterator cbegin() const { return claims.begin(); } + const_iterator cend() const { return claims.end(); } + const_iterator begin() const { return claims.begin(); } + const_iterator end() const { return claims.end(); } + + /** + * \brief Parse a JSON string into a map of claims + * + * The implication is that a "map of claims" is identic to a JSON object + * + * \param str JSON data to be parse as an object + * \return content as JSON object + */ + static typename json_traits::object_type parse_claims(const typename json_traits::string_type& str) { + typename json_traits::value_type val; + if (!json_traits::parse(val, str)) throw error::invalid_json_exception(); + + return json_traits::as_object(val); + }; + + /** + * Check if a claim is present in the map + * \return true if claim was present, false otherwise + */ + bool has_claim(const typename json_traits::string_type& name) const noexcept { + return claims.count(name) != 0; + } + + /** + * Get a claim by name + * + * \param name the name of the desired claim + * \return Requested claim + * \throw jwt::error::claim_not_present_exception if the claim was not present + */ + basic_claim_t get_claim(const typename json_traits::string_type& name) const { + if (!has_claim(name)) throw error::claim_not_present_exception(); + return basic_claim_t{claims.at(name)}; + } + }; + } // namespace details + + /** + * Base class that represents a token payload. + * Contains Convenience accessors for common claims. + */ + template + class payload { + protected: + details::map_of_claims payload_claims; + + public: + using basic_claim_t = basic_claim; + + /** + * Check if issuer is present ("iss") + * \return true if present, false otherwise + */ + bool has_issuer() const noexcept { return has_payload_claim("iss"); } + /** + * Check if subject is present ("sub") + * \return true if present, false otherwise + */ + bool has_subject() const noexcept { return has_payload_claim("sub"); } + /** + * Check if audience is present ("aud") + * \return true if present, false otherwise + */ + bool has_audience() const noexcept { return has_payload_claim("aud"); } + /** + * Check if expires is present ("exp") + * \return true if present, false otherwise + */ + bool has_expires_at() const noexcept { return has_payload_claim("exp"); } + /** + * Check if not before is present ("nbf") + * \return true if present, false otherwise + */ + bool has_not_before() const noexcept { return has_payload_claim("nbf"); } + /** + * Check if issued at is present ("iat") + * \return true if present, false otherwise + */ + bool has_issued_at() const noexcept { return has_payload_claim("iat"); } + /** + * Check if token id is present ("jti") + * \return true if present, false otherwise + */ + bool has_id() const noexcept { return has_payload_claim("jti"); } + /** + * Get issuer claim + * \return issuer as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_issuer() const { return get_payload_claim("iss").as_string(); } + /** + * Get subject claim + * \return subject as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_subject() const { return get_payload_claim("sub").as_string(); } + /** + * Get audience claim + * \return audience as a set of strings + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a set (Should not happen in a valid token) + */ + typename basic_claim_t::set_t get_audience() const { + auto aud = get_payload_claim("aud"); + if (aud.get_type() == json::type::string) return {aud.as_string()}; + + return aud.as_set(); + } + /** + * Get expires claim + * \return expires as a date in utc + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a date (Should not happen in a valid token) + */ + date get_expires_at() const { return get_payload_claim("exp").as_date(); } + /** + * Get not valid before claim + * \return nbf date in utc + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a date (Should not happen in a valid token) + */ + date get_not_before() const { return get_payload_claim("nbf").as_date(); } + /** + * Get issued at claim + * \return issued at as date in utc + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a date (Should not happen in a valid token) + */ + date get_issued_at() const { return get_payload_claim("iat").as_date(); } + /** + * Get id claim + * \return id as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_id() const { return get_payload_claim("jti").as_string(); } + /** + * Check if a payload claim is present + * \return true if claim was present, false otherwise + */ + bool has_payload_claim(const typename json_traits::string_type& name) const noexcept { + return payload_claims.has_claim(name); + } + /** + * Get payload claim + * \return Requested claim + * \throw std::runtime_error If claim was not present + */ + basic_claim_t get_payload_claim(const typename json_traits::string_type& name) const { + return payload_claims.get_claim(name); + } + }; + + /** + * Base class that represents a token header. + * Contains Convenience accessors for common claims. + */ + template + class header { + protected: + details::map_of_claims header_claims; + + public: + using basic_claim_t = basic_claim; + /** + * Check if algorithm is present ("alg") + * \return true if present, false otherwise + */ + bool has_algorithm() const noexcept { return has_header_claim("alg"); } + /** + * Check if type is present ("typ") + * \return true if present, false otherwise + */ + bool has_type() const noexcept { return has_header_claim("typ"); } + /** + * Check if content type is present ("cty") + * \return true if present, false otherwise + */ + bool has_content_type() const noexcept { return has_header_claim("cty"); } + /** + * Check if key id is present ("kid") + * \return true if present, false otherwise + */ + bool has_key_id() const noexcept { return has_header_claim("kid"); } + /** + * Get algorithm claim + * \return algorithm as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_algorithm() const { return get_header_claim("alg").as_string(); } + /** + * Get type claim + * \return type as a string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_type() const { return get_header_claim("typ").as_string(); } + /** + * Get content type claim + * \return content type as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_content_type() const { return get_header_claim("cty").as_string(); } + /** + * Get key id claim + * \return key id as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_key_id() const { return get_header_claim("kid").as_string(); } + /** + * Check if a header claim is present + * \return true if claim was present, false otherwise + */ + bool has_header_claim(const typename json_traits::string_type& name) const noexcept { + return header_claims.has_claim(name); + } + /** + * Get header claim + * \return Requested claim + * \throw std::runtime_error If claim was not present + */ + basic_claim_t get_header_claim(const typename json_traits::string_type& name) const { + return header_claims.get_claim(name); + } + }; + + /** + * Class containing all information about a decoded token + */ + template + class decoded_jwt : public header, public payload { + protected: + /// Unmodified token, as passed to constructor + typename json_traits::string_type token; + /// Header part decoded from base64 + typename json_traits::string_type header; + /// Unmodified header part in base64 + typename json_traits::string_type header_base64; + /// Payload part decoded from base64 + typename json_traits::string_type payload; + /// Unmodified payload part in base64 + typename json_traits::string_type payload_base64; + /// Signature part decoded from base64 + typename json_traits::string_type signature; + /// Unmodified signature part in base64 + typename json_traits::string_type signature_base64; + + public: + using basic_claim_t = basic_claim; +#ifndef JWT_DISABLE_BASE64 + /** + * \brief Parses a given token + * + * \note Decodes using the jwt::base64url which supports an std::string + * + * \param token The token to parse + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + JWT_CLAIM_EXPLICIT decoded_jwt(const typename json_traits::string_type& token) + : decoded_jwt(token, [](const typename json_traits::string_type& str) { + return base::decode(base::pad(str)); + }) {} +#endif + /** + * \brief Parses a given token + * + * \tparam Decode is callabled, taking a string_type and returns a string_type. + * It should ensure the padding of the input and then base64url decode and + * return the results. + * \param token The token to parse + * \param decode The function to decode the token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + template + decoded_jwt(const typename json_traits::string_type& token, Decode decode) : token(token) { + auto hdr_end = token.find('.'); + if (hdr_end == json_traits::string_type::npos) throw std::invalid_argument("invalid token supplied"); + auto payload_end = token.find('.', hdr_end + 1); + if (payload_end == json_traits::string_type::npos) throw std::invalid_argument("invalid token supplied"); + header_base64 = token.substr(0, hdr_end); + payload_base64 = token.substr(hdr_end + 1, payload_end - hdr_end - 1); + signature_base64 = token.substr(payload_end + 1); + + header = decode(header_base64); + payload = decode(payload_base64); + signature = decode(signature_base64); + + this->header_claims = details::map_of_claims::parse_claims(header); + this->payload_claims = details::map_of_claims::parse_claims(payload); + } + + /** + * Get token string, as passed to constructor + * \return token as passed to constructor + */ + const typename json_traits::string_type& get_token() const noexcept { return token; } + /** + * Get header part as json string + * \return header part after base64 decoding + */ + const typename json_traits::string_type& get_header() const noexcept { return header; } + /** + * Get payload part as json string + * \return payload part after base64 decoding + */ + const typename json_traits::string_type& get_payload() const noexcept { return payload; } + /** + * Get signature part as json string + * \return signature part after base64 decoding + */ + const typename json_traits::string_type& get_signature() const noexcept { return signature; } + /** + * Get header part as base64 string + * \return header part before base64 decoding + */ + const typename json_traits::string_type& get_header_base64() const noexcept { return header_base64; } + /** + * Get payload part as base64 string + * \return payload part before base64 decoding + */ + const typename json_traits::string_type& get_payload_base64() const noexcept { return payload_base64; } + /** + * Get signature part as base64 string + * \return signature part before base64 decoding + */ + const typename json_traits::string_type& get_signature_base64() const noexcept { return signature_base64; } + /** + * Get all payload as JSON object + * \return map of claims + */ + typename json_traits::object_type get_payload_json() const { return this->payload_claims.claims; } + /** + * Get all header as JSON object + * \return map of claims + */ + typename json_traits::object_type get_header_json() const { return this->header_claims.claims; } + /** + * Get a payload claim by name + * + * \param name the name of the desired claim + * \return Requested claim + * \throw jwt::error::claim_not_present_exception if the claim was not present + */ + basic_claim_t get_payload_claim(const typename json_traits::string_type& name) const { + return this->payload_claims.get_claim(name); + } + /** + * Get a header claim by name + * + * \param name the name of the desired claim + * \return Requested claim + * \throw jwt::error::claim_not_present_exception if the claim was not present + */ + basic_claim_t get_header_claim(const typename json_traits::string_type& name) const { + return this->header_claims.get_claim(name); + } + }; + + /** + * Builder class to build and sign a new token + * Use jwt::create() to get an instance of this class. + */ + template + class builder { + typename json_traits::object_type header_claims; + typename json_traits::object_type payload_claims; + + public: + builder() = default; + /** + * Set a header claim. + * \param id Name of the claim + * \param c Claim to add + * \return *this to allow for method chaining + */ + builder& set_header_claim(const typename json_traits::string_type& id, typename json_traits::value_type c) { + header_claims[id] = std::move(c); + return *this; + } + + /** + * Set a header claim. + * \param id Name of the claim + * \param c Claim to add + * \return *this to allow for method chaining + */ + builder& set_header_claim(const typename json_traits::string_type& id, basic_claim c) { + header_claims[id] = c.to_json(); + return *this; + } + /** + * Set a payload claim. + * \param id Name of the claim + * \param c Claim to add + * \return *this to allow for method chaining + */ + builder& set_payload_claim(const typename json_traits::string_type& id, typename json_traits::value_type c) { + payload_claims[id] = std::move(c); + return *this; + } + /** + * Set a payload claim. + * \param id Name of the claim + * \param c Claim to add + * \return *this to allow for method chaining + */ + builder& set_payload_claim(const typename json_traits::string_type& id, basic_claim c) { + payload_claims[id] = c.to_json(); + return *this; + } + /** + * \brief Set algorithm claim + * You normally don't need to do this, as the algorithm is automatically set if you don't change it. + * + * \param str Name of algorithm + * \return *this to allow for method chaining + */ + builder& set_algorithm(typename json_traits::string_type str) { + return set_header_claim("alg", typename json_traits::value_type(str)); + } + /** + * Set type claim + * \param str Type to set + * \return *this to allow for method chaining + */ + builder& set_type(typename json_traits::string_type str) { + return set_header_claim("typ", typename json_traits::value_type(str)); + } + /** + * Set content type claim + * \param str Type to set + * \return *this to allow for method chaining + */ + builder& set_content_type(typename json_traits::string_type str) { + return set_header_claim("cty", typename json_traits::value_type(str)); + } + /** + * \brief Set key id claim + * + * \param str Key id to set + * \return *this to allow for method chaining + */ + builder& set_key_id(typename json_traits::string_type str) { + return set_header_claim("kid", typename json_traits::value_type(str)); + } + /** + * Set issuer claim + * \param str Issuer to set + * \return *this to allow for method chaining + */ + builder& set_issuer(typename json_traits::string_type str) { + return set_payload_claim("iss", typename json_traits::value_type(str)); + } + /** + * Set subject claim + * \param str Subject to set + * \return *this to allow for method chaining + */ + builder& set_subject(typename json_traits::string_type str) { + return set_payload_claim("sub", typename json_traits::value_type(str)); + } + /** + * Set audience claim + * \param a Audience set + * \return *this to allow for method chaining + */ + builder& set_audience(typename json_traits::array_type a) { + return set_payload_claim("aud", typename json_traits::value_type(a)); + } + /** + * Set audience claim + * \param aud Single audience + * \return *this to allow for method chaining + */ + builder& set_audience(typename json_traits::string_type aud) { + return set_payload_claim("aud", typename json_traits::value_type(aud)); + } + /** + * Set expires at claim + * \param d Expires time + * \return *this to allow for method chaining + */ + builder& set_expires_at(const date& d) { return set_payload_claim("exp", basic_claim(d)); } + /** + * Set not before claim + * \param d First valid time + * \return *this to allow for method chaining + */ + builder& set_not_before(const date& d) { return set_payload_claim("nbf", basic_claim(d)); } + /** + * Set issued at claim + * \param d Issued at time, should be current time + * \return *this to allow for method chaining + */ + builder& set_issued_at(const date& d) { return set_payload_claim("iat", basic_claim(d)); } + /** + * Set id claim + * \param str ID to set + * \return *this to allow for method chaining + */ + builder& set_id(const typename json_traits::string_type& str) { + return set_payload_claim("jti", typename json_traits::value_type(str)); + } + + /** + * Sign token and return result + * \tparam Algo Callable method which takes a string_type and return the signed input as a string_type + * \tparam Encode Callable method which takes a string_type and base64url safe encodes it, + * MUST return the result with no padding; trim the result. + * \param algo Instance of an algorithm to sign the token with + * \param encode Callable to transform the serialized json to base64 with no padding + * \return Final token as a string + * + * \note If the 'alg' header in not set in the token it will be set to `algo.name()` + */ + template + typename json_traits::string_type sign(const Algo& algo, Encode encode) const { + std::error_code ec; + auto res = sign(algo, encode, ec); + error::throw_if_error(ec); + return res; + } +#ifndef JWT_DISABLE_BASE64 + /** + * Sign token and return result + * + * using the `jwt::base` functions provided + * + * \param algo Instance of an algorithm to sign the token with + * \return Final token as a string + */ + template + typename json_traits::string_type sign(const Algo& algo) const { + std::error_code ec; + auto res = sign(algo, ec); + error::throw_if_error(ec); + return res; + } +#endif + + /** + * Sign token and return result + * \tparam Algo Callable method which takes a string_type and return the signed input as a string_type + * \tparam Encode Callable method which takes a string_type and base64url safe encodes it, + * MUST return the result with no padding; trim the result. + * \param algo Instance of an algorithm to sign the token with + * \param encode Callable to transform the serialized json to base64 with no padding + * \param ec error_code filled with details on error + * \return Final token as a string + * + * \note If the 'alg' header in not set in the token it will be set to `algo.name()` + */ + template + typename json_traits::string_type sign(const Algo& algo, Encode encode, std::error_code& ec) const { + // make a copy such that a builder can be re-used + typename json_traits::object_type obj_header = header_claims; + if (header_claims.count("alg") == 0) obj_header["alg"] = typename json_traits::value_type(algo.name()); + + const auto header = encode(json_traits::serialize(typename json_traits::value_type(obj_header))); + const auto payload = encode(json_traits::serialize(typename json_traits::value_type(payload_claims))); + const auto token = header + "." + payload; + + auto signature = algo.sign(token, ec); + if (ec) return {}; + + return token + "." + encode(signature); + } +#ifndef JWT_DISABLE_BASE64 + /** + * Sign token and return result + * + * using the `jwt::base` functions provided + * + * \param algo Instance of an algorithm to sign the token with + * \param ec error_code filled with details on error + * \return Final token as a string + */ + template + typename json_traits::string_type sign(const Algo& algo, std::error_code& ec) const { + return sign( + algo, + [](const typename json_traits::string_type& data) { + return base::trim(base::encode(data)); + }, + ec); + } +#endif + }; + + namespace verify_ops { + /** + * This is the base container which holds the token that need to be verified + */ + template + struct verify_context { + verify_context(date ctime, const decoded_jwt& j, size_t l) + : current_time(ctime), jwt(j), default_leeway(l) {} + // Current time, retrieved from the verifiers clock and cached for performance and consistency + date current_time; + // The jwt passed to the verifier + const decoded_jwt& jwt; + // The configured default leeway for this verification + size_t default_leeway{0}; + + // The claim key to apply this comparison on + typename json_traits::string_type claim_key{}; + + // Helper method to get a claim from the jwt in this context + basic_claim get_claim(bool in_header, std::error_code& ec) const { + if (in_header) { + if (!jwt.has_header_claim(claim_key)) { + ec = error::token_verification_error::missing_claim; + return {}; + } + return jwt.get_header_claim(claim_key); + } else { + if (!jwt.has_payload_claim(claim_key)) { + ec = error::token_verification_error::missing_claim; + return {}; + } + return jwt.get_payload_claim(claim_key); + } + } + basic_claim get_claim(bool in_header, json::type t, std::error_code& ec) const { + auto c = get_claim(in_header, ec); + if (ec) return {}; + if (c.get_type() != t) { + ec = error::token_verification_error::claim_type_missmatch; + return {}; + } + return c; + } + basic_claim get_claim(std::error_code& ec) const { return get_claim(false, ec); } + basic_claim get_claim(json::type t, std::error_code& ec) const { + return get_claim(false, t, ec); + } + }; + + /** + * This is the default operation and does case sensitive matching + */ + template + struct equals_claim { + const basic_claim expected; + void operator()(const verify_context& ctx, std::error_code& ec) const { + auto jc = ctx.get_claim(in_header, expected.get_type(), ec); + if (ec) return; + const bool matches = [&]() { + switch (expected.get_type()) { + case json::type::boolean: return expected.as_boolean() == jc.as_boolean(); + case json::type::integer: return expected.as_integer() == jc.as_integer(); + case json::type::number: return expected.as_number() == jc.as_number(); + case json::type::string: return expected.as_string() == jc.as_string(); + case json::type::array: + case json::type::object: + return json_traits::serialize(expected.to_json()) == json_traits::serialize(jc.to_json()); + default: throw std::logic_error("internal error, should be unreachable"); + } + }(); + if (!matches) { + ec = error::token_verification_error::claim_value_missmatch; + return; + } + } + }; + + /** + * Checks that the current time is before the time specified in the given + * claim. This is identical to how the "exp" check works. + */ + template + struct date_before_claim { + const size_t leeway; + void operator()(const verify_context& ctx, std::error_code& ec) const { + auto jc = ctx.get_claim(in_header, json::type::integer, ec); + if (ec) return; + auto c = jc.as_date(); + if (ctx.current_time > c + std::chrono::seconds(leeway)) { + ec = error::token_verification_error::token_expired; + } + } + }; + + /** + * Checks that the current time is after the time specified in the given + * claim. This is identical to how the "nbf" and "iat" check works. + */ + template + struct date_after_claim { + const size_t leeway; + void operator()(const verify_context& ctx, std::error_code& ec) const { + auto jc = ctx.get_claim(in_header, json::type::integer, ec); + if (ec) return; + auto c = jc.as_date(); + if (ctx.current_time < c - std::chrono::seconds(leeway)) { + ec = error::token_verification_error::token_expired; + } + } + }; + + /** + * Checks if the given set is a subset of the set inside the token. + * If the token value is a string it is traited as a set of a single element. + * The comparison is case sensitive. + */ + template + struct is_subset_claim { + const typename basic_claim::set_t expected; + void operator()(const verify_context& ctx, std::error_code& ec) const { + auto c = ctx.get_claim(in_header, ec); + if (ec) return; + if (c.get_type() == json::type::string) { + if (expected.size() != 1 || *expected.begin() != c.as_string()) { + ec = error::token_verification_error::audience_missmatch; + return; + } + } else if (c.get_type() == json::type::array) { + auto jc = c.as_set(); + for (auto& e : expected) { + if (jc.find(e) == jc.end()) { + ec = error::token_verification_error::audience_missmatch; + return; + } + } + } else { + ec = error::token_verification_error::claim_type_missmatch; + return; + } + } + }; + + /** + * Checks if the claim is a string and does an case insensitive comparison. + */ + template + struct insensitive_string_claim { + const typename json_traits::string_type expected; + std::locale locale; + insensitive_string_claim(const typename json_traits::string_type& e, std::locale loc) + : expected(to_lower_unicode(e, loc)), locale(loc) {} + + void operator()(const verify_context& ctx, std::error_code& ec) const { + const auto c = ctx.get_claim(in_header, json::type::string, ec); + if (ec) return; + if (to_lower_unicode(c.as_string(), locale) != expected) { + ec = error::token_verification_error::claim_value_missmatch; + } + } + + static std::string to_lower_unicode(const std::string& str, const std::locale& loc) { + std::mbstate_t state = std::mbstate_t(); + const char* in_next = str.data(); + const char* in_end = str.data() + str.size(); + std::wstring wide; + wide.reserve(str.size()); + + while (in_next != in_end) { + wchar_t wc; + std::size_t result = std::mbrtowc(&wc, in_next, in_end - in_next, &state); + if (result == static_cast(-1)) { + throw std::runtime_error("encoding error: " + std::string(std::strerror(errno))); + } else if (result == static_cast(-2)) { + throw std::runtime_error("conversion error: next bytes constitute an incomplete, but so far " + "valid, multibyte character."); + } + in_next += result; + wide.push_back(wc); + } + + auto& f = std::use_facet>(loc); + f.tolower(&wide[0], &wide[0] + wide.size()); + + std::string out; + out.reserve(wide.size()); + for (wchar_t wc : wide) { + char mb[MB_LEN_MAX]; + std::size_t n = std::wcrtomb(mb, wc, &state); + if (n != static_cast(-1)) out.append(mb, n); + } + + return out; + } + }; + } // namespace verify_ops + + /** + * Verifier class used to check if a decoded token contains all claims required by your application and has a valid + * signature. + */ + template + class verifier { + public: + using basic_claim_t = basic_claim; + /** + * Verification function + * + * This gets passed the current verifier, a reference to the decoded jwt, a reference to the key of this claim, + * as well as a reference to an error_code. + * The function checks if the actual value matches certain rules (e.g. equality to value x) and sets the error_code if + * it does not. Once a non zero error_code is encountered the verification stops and this error_code becomes the result + * returned from verify + */ + using verify_check_fn_t = + std::function&, std::error_code& ec)>; + + private: + struct algo_base { + virtual ~algo_base() = default; + virtual void verify(const std::string& data, const std::string& sig, std::error_code& ec) = 0; + }; + template + struct algo : public algo_base { + T alg; + explicit algo(T a) : alg(a) {} + void verify(const std::string& data, const std::string& sig, std::error_code& ec) override { + alg.verify(data, sig, ec); + } + }; + /// Required claims + std::unordered_map claims; + /// Leeway time for exp, nbf and iat + size_t default_leeway = 0; + /// Instance of clock type + Clock clock; + /// Supported algorithms + std::unordered_map> algs; + + public: + /** + * Constructor for building a new verifier instance + * \param c Clock instance + */ + explicit verifier(Clock c) : clock(c) { + claims["exp"] = [](const verify_ops::verify_context& ctx, std::error_code& ec) { + if (!ctx.jwt.has_expires_at()) return; + auto exp = ctx.jwt.get_expires_at(); + if (ctx.current_time > exp + std::chrono::seconds(ctx.default_leeway)) { + ec = error::token_verification_error::token_expired; + } + }; + claims["iat"] = [](const verify_ops::verify_context& ctx, std::error_code& ec) { + if (!ctx.jwt.has_issued_at()) return; + auto iat = ctx.jwt.get_issued_at(); + if (ctx.current_time < iat - std::chrono::seconds(ctx.default_leeway)) { + ec = error::token_verification_error::token_expired; + } + }; + claims["nbf"] = [](const verify_ops::verify_context& ctx, std::error_code& ec) { + if (!ctx.jwt.has_not_before()) return; + auto nbf = ctx.jwt.get_not_before(); + if (ctx.current_time < nbf - std::chrono::seconds(ctx.default_leeway)) { + ec = error::token_verification_error::token_expired; + } + }; + } + + /** + * Set default leeway to use. + * \param leeway Default leeway to use if not specified otherwise + * \return *this to allow chaining + */ + verifier& leeway(size_t leeway) { + default_leeway = leeway; + return *this; + } + /** + * Set leeway for expires at. + * If not specified the default leeway will be used. + * \param leeway Set leeway to use for expires at. + * \return *this to allow chaining + */ + verifier& expires_at_leeway(size_t leeway) { + claims["exp"] = verify_ops::date_before_claim{leeway}; + return *this; + } + /** + * Set leeway for not before. + * If not specified the default leeway will be used. + * \param leeway Set leeway to use for not before. + * \return *this to allow chaining + */ + verifier& not_before_leeway(size_t leeway) { + claims["nbf"] = verify_ops::date_after_claim{leeway}; + return *this; + } + /** + * Set leeway for issued at. + * If not specified the default leeway will be used. + * \param leeway Set leeway to use for issued at. + * \return *this to allow chaining + */ + verifier& issued_at_leeway(size_t leeway) { + claims["iat"] = verify_ops::date_after_claim{leeway}; + return *this; + } + + /** + * Set an type to check for. + * + * According to [RFC 7519 Section 5.1](https://datatracker.ietf.org/doc/html/rfc7519#section-5.1), + * This parameter is ignored by JWT implementations; any processing of this parameter is performed by the JWT application. + * Check is casesensitive. + * + * \param type Type Header Parameter to check for. + * \param locale Localization functionality to use when comparing + * \return *this to allow chaining + */ + verifier& with_type(const typename json_traits::string_type& type, std::locale locale = std::locale{}) { + return with_claim("typ", verify_ops::insensitive_string_claim{type, std::move(locale)}); + } + + /** + * Set an issuer to check for. + * Check is casesensitive. + * \param iss Issuer to check for. + * \return *this to allow chaining + */ + verifier& with_issuer(const typename json_traits::string_type& iss) { + return with_claim("iss", basic_claim_t(iss)); + } + + /** + * Set a subject to check for. + * Check is casesensitive. + * \param sub Subject to check for. + * \return *this to allow chaining + */ + verifier& with_subject(const typename json_traits::string_type& sub) { + return with_claim("sub", basic_claim_t(sub)); + } + /** + * Set an audience to check for. + * If any of the specified audiences is not present in the token the check fails. + * \param aud Audience to check for. + * \return *this to allow chaining + */ + verifier& with_audience(const typename basic_claim_t::set_t& aud) { + claims["aud"] = verify_ops::is_subset_claim{aud}; + return *this; + } + /** + * Set an audience to check for. + * If the specified audiences is not present in the token the check fails. + * \param aud Audience to check for. + * \return *this to allow chaining + */ + verifier& with_audience(const typename json_traits::string_type& aud) { + typename basic_claim_t::set_t s; + s.insert(aud); + return with_audience(s); + } + /** + * Set an id to check for. + * Check is casesensitive. + * \param id ID to check for. + * \return *this to allow chaining + */ + verifier& with_id(const typename json_traits::string_type& id) { return with_claim("jti", basic_claim_t(id)); } + + /** + * Specify a claim to check for using the specified operation. + * \param name Name of the claim to check for + * \param fn Function to use for verifying the claim + * \return *this to allow chaining + */ + verifier& with_claim(const typename json_traits::string_type& name, verify_check_fn_t fn) { + claims[name] = fn; + return *this; + } + + /** + * Specify a claim to check for equality (both type & value). + * \param name Name of the claim to check for + * \param c Claim to check for + * \return *this to allow chaining + */ + verifier& with_claim(const typename json_traits::string_type& name, basic_claim_t c) { + return with_claim(name, verify_ops::equals_claim{c}); + } + + /** + * Add an algorithm available for checking. + * \param alg Algorithm to allow + * \return *this to allow chaining + */ + template + verifier& allow_algorithm(Algorithm alg) { + algs[alg.name()] = std::make_shared>(alg); + return *this; + } + + /** + * Verify the given token. + * \param jwt Token to check + * \throw token_verification_exception Verification failed + */ + void verify(const decoded_jwt& jwt) const { + std::error_code ec; + verify(jwt, ec); + error::throw_if_error(ec); + } + /** + * Verify the given token. + * \param jwt Token to check + * \param ec error_code filled with details on error + */ + void verify(const decoded_jwt& jwt, std::error_code& ec) const { + ec.clear(); + const typename json_traits::string_type data = jwt.get_header_base64() + "." + jwt.get_payload_base64(); + const typename json_traits::string_type sig = jwt.get_signature(); + const std::string algo = jwt.get_algorithm(); + if (algs.count(algo) == 0) { + ec = error::token_verification_error::wrong_algorithm; + return; + } + algs.at(algo)->verify(data, sig, ec); + if (ec) return; + + verify_ops::verify_context ctx{clock.now(), jwt, default_leeway}; + for (auto& c : claims) { + ctx.claim_key = c.first; + c.second(ctx, ec); + if (ec) return; + } + } + }; + + /** + * \brief JSON Web Key + * + * https://tools.ietf.org/html/rfc7517 + * + * A JSON object that represents a cryptographic key. The members of + * the object represent properties of the key, including its value. + */ + template + class jwk { + using basic_claim_t = basic_claim; + const details::map_of_claims jwk_claims; + + public: + JWT_CLAIM_EXPLICIT jwk(const typename json_traits::string_type& str) + : jwk_claims(details::map_of_claims::parse_claims(str)) {} + + JWT_CLAIM_EXPLICIT jwk(const typename json_traits::value_type& json) + : jwk_claims(json_traits::as_object(json)) {} + + /** + * Get key type claim + * + * This returns the general type (e.g. RSA or EC), not a specific algorithm value. + * \return key type as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_key_type() const { return get_jwk_claim("kty").as_string(); } + + /** + * Get public key usage claim + * \return usage parameter as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_use() const { return get_jwk_claim("use").as_string(); } + + /** + * Get key operation types claim + * \return key operation types as a set of strings + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename basic_claim_t::set_t get_key_operations() const { return get_jwk_claim("key_ops").as_set(); } + + /** + * Get algorithm claim + * \return algorithm as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_algorithm() const { return get_jwk_claim("alg").as_string(); } + + /** + * Get key id claim + * \return key id as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_key_id() const { return get_jwk_claim("kid").as_string(); } + + /** + * \brief Get curve claim + * + * https://www.rfc-editor.org/rfc/rfc7518.html#section-6.2.1.1 + * https://www.iana.org/assignments/jose/jose.xhtml#table-web-key-elliptic-curve + * + * \return curve as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_curve() const { return get_jwk_claim("crv").as_string(); } + + /** + * Get x5c claim + * \return x5c as an array + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a array (Should not happen in a valid token) + */ + typename json_traits::array_type get_x5c() const { return get_jwk_claim("x5c").as_array(); }; + + /** + * Get X509 URL claim + * \return x5u as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_x5u() const { return get_jwk_claim("x5u").as_string(); }; + + /** + * Get X509 thumbprint claim + * \return x5t as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_x5t() const { return get_jwk_claim("x5t").as_string(); }; + + /** + * Get X509 SHA256 thumbprint claim + * \return x5t#S256 as string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_x5t_sha256() const { return get_jwk_claim("x5t#S256").as_string(); }; + + /** + * Get x5c claim as a string + * \return x5c as an string + * \throw std::runtime_error If claim was not present + * \throw std::bad_cast Claim was present but not a string (Should not happen in a valid token) + */ + typename json_traits::string_type get_x5c_key_value() const { + auto x5c_array = get_jwk_claim("x5c").as_array(); + if (x5c_array.size() == 0) throw error::claim_not_present_exception(); + + return json_traits::as_string(x5c_array.front()); + }; + + /** + * Check if a key type is present ("kty") + * \return true if present, false otherwise + */ + bool has_key_type() const noexcept { return has_jwk_claim("kty"); } + + /** + * Check if a public key usage indication is present ("use") + * \return true if present, false otherwise + */ + bool has_use() const noexcept { return has_jwk_claim("use"); } + + /** + * Check if a key operations parameter is present ("key_ops") + * \return true if present, false otherwise + */ + bool has_key_operations() const noexcept { return has_jwk_claim("key_ops"); } + + /** + * Check if algorithm is present ("alg") + * \return true if present, false otherwise + */ + bool has_algorithm() const noexcept { return has_jwk_claim("alg"); } + + /** + * Check if curve is present ("crv") + * \return true if present, false otherwise + */ + bool has_curve() const noexcept { return has_jwk_claim("crv"); } + + /** + * Check if key id is present ("kid") + * \return true if present, false otherwise + */ + bool has_key_id() const noexcept { return has_jwk_claim("kid"); } + + /** + * Check if X509 URL is present ("x5u") + * \return true if present, false otherwise + */ + bool has_x5u() const noexcept { return has_jwk_claim("x5u"); } + + /** + * Check if X509 Chain is present ("x5c") + * \return true if present, false otherwise + */ + bool has_x5c() const noexcept { return has_jwk_claim("x5c"); } + + /** + * Check if a X509 thumbprint is present ("x5t") + * \return true if present, false otherwise + */ + bool has_x5t() const noexcept { return has_jwk_claim("x5t"); } + + /** + * Check if a X509 SHA256 thumbprint is present ("x5t#S256") + * \return true if present, false otherwise + */ + bool has_x5t_sha256() const noexcept { return has_jwk_claim("x5t#S256"); } + + /** + * Check if a jwks claim is present + * \return true if claim was present, false otherwise + */ + bool has_jwk_claim(const typename json_traits::string_type& name) const noexcept { + return jwk_claims.has_claim(name); + } + + /** + * Get jwks claim + * \return Requested claim + * \throw std::runtime_error If claim was not present + */ + basic_claim_t get_jwk_claim(const typename json_traits::string_type& name) const { + return jwk_claims.get_claim(name); + } + + bool empty() const noexcept { return jwk_claims.empty(); } + + /** + * Get all jwk claims + * \return Map of claims + */ + typename json_traits::object_type get_claims() const { return this->jwk_claims.claims; } + }; + + /** + * \brief JWK Set + * + * https://tools.ietf.org/html/rfc7517 + * + * A JSON object that represents a set of JWKs. The JSON object MUST + * have a "keys" member, which is an array of JWKs. + * + * This container takes a JWKs and simplifies it to a vector of JWKs + */ + template + class jwks { + public: + using jwk_t = jwk; + using jwt_vector_t = std::vector; + using iterator = typename jwt_vector_t::iterator; + using const_iterator = typename jwt_vector_t::const_iterator; + + JWT_CLAIM_EXPLICIT jwks(const typename json_traits::string_type& str) { + typename json_traits::value_type parsed_val; + if (!json_traits::parse(parsed_val, str)) throw error::invalid_json_exception(); + + const details::map_of_claims jwks_json = json_traits::as_object(parsed_val); + if (!jwks_json.has_claim("keys")) throw error::invalid_json_exception(); + + auto jwk_list = jwks_json.get_claim("keys").as_array(); + std::transform(jwk_list.begin(), jwk_list.end(), std::back_inserter(jwk_claims), + [](const typename json_traits::value_type& val) { return jwk_t{val}; }); + } + + iterator begin() { return jwk_claims.begin(); } + iterator end() { return jwk_claims.end(); } + const_iterator cbegin() const { return jwk_claims.begin(); } + const_iterator cend() const { return jwk_claims.end(); } + const_iterator begin() const { return jwk_claims.begin(); } + const_iterator end() const { return jwk_claims.end(); } + + /** + * Check if a jwk with the kid is present + * \return true if jwk was present, false otherwise + */ + bool has_jwk(const typename json_traits::string_type& key_id) const noexcept { + return find_by_kid(key_id) != end(); + } + + /** + * Get jwk + * \return Requested jwk by key_id + * \throw std::runtime_error If jwk was not present + */ + jwk_t get_jwk(const typename json_traits::string_type& key_id) const { + const auto maybe = find_by_kid(key_id); + if (maybe == end()) throw error::claim_not_present_exception(); + return *maybe; + } + + private: + jwt_vector_t jwk_claims; + + const_iterator find_by_kid(const typename json_traits::string_type& key_id) const noexcept { + return std::find_if(cbegin(), cend(), [key_id](const jwk_t& jwk) { + if (!jwk.has_key_id()) { return false; } + return jwk.get_key_id() == key_id; + }); + } + }; + + /** + * Create a verifier using the given clock + * \param c Clock instance to use + * \return verifier instance + */ + template + verifier verify(Clock c) { + return verifier(c); + } + + /** + * Default clock class using std::chrono::system_clock as a backend. + */ + struct default_clock { + date now() const { return date::clock::now(); } + }; + + /** + * Create a verifier using the given clock + * \param c Clock instance to use + * \return verifier instance + */ + template + verifier verify(default_clock c = {}) { + return verifier(c); + } + + /** + * Return a builder instance to create a new token + */ + template + builder create() { + return builder(); + } + + /** + * Decode a token + * \param token Token to decode + * \param decode function that will pad and base64url decode the token + * \return Decoded token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + template + decoded_jwt decode(const typename json_traits::string_type& token, Decode decode) { + return decoded_jwt(token, decode); + } + + /** + * Decode a token + * \param token Token to decode + * \return Decoded token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + template + decoded_jwt decode(const typename json_traits::string_type& token) { + return decoded_jwt(token); + } + + template + jwk parse_jwk(const typename json_traits::string_type& token) { + return jwk(token); + } + + template + jwks parse_jwks(const typename json_traits::string_type& token) { + return jwks(token); + } +} // namespace jwt + +template +std::istream& operator>>(std::istream& is, jwt::basic_claim& c) { + return c.operator>>(is); +} + +template +std::ostream& operator<<(std::ostream& os, const jwt::basic_claim& c) { + return os << c.to_json(); +} + +#ifndef JWT_DISABLE_PICOJSON +#include "traits/kazuho-picojson/defaults.h" +#endif + +#endif diff --git a/Development/third_party/jwt-cpp/traits/nlohmann-json/defaults.h b/Development/third_party/jwt-cpp/traits/nlohmann-json/defaults.h new file mode 100644 index 000000000..c324075f8 --- /dev/null +++ b/Development/third_party/jwt-cpp/traits/nlohmann-json/defaults.h @@ -0,0 +1,88 @@ +#ifndef JWT_CPP_NLOHMANN_JSON_DEFAULTS_H +#define JWT_CPP_NLOHMANN_JSON_DEFAULTS_H + +#ifndef JWT_DISABLE_PICOJSON +#define JWT_DISABLE_PICOJSON +#endif + +#include "traits.h" + +namespace jwt { + /** + * \brief a class to store a generic [JSON for Modern C++](https://github.com/nlohmann/json) value as claim + * + * This type is the specialization of the \ref basic_claim class which + * uses the standard template types. + */ + using claim = basic_claim; + + /** + * Create a verifier using the default clock + * \return verifier instance + */ + inline verifier verify() { + return verify(default_clock{}); + } + + /** + * Return a builder instance to create a new token + */ + inline builder create() { return builder(); } + +#ifndef JWT_DISABLE_BASE64 + /** + * Decode a token + * \param token Token to decode + * \return Decoded token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + inline decoded_jwt decode(const std::string& token) { + return decoded_jwt(token); + } +#endif + + /** + * Decode a token + * \tparam Decode is callabled, taking a string_type and returns a string_type. + * It should ensure the padding of the input and then base64url decode and + * return the results. + * \param token Token to decode + * \param decode The token to parse + * \return Decoded token + * \throw std::invalid_argument Token is not in correct format + * \throw std::runtime_error Base64 decoding failed or invalid json + */ + template + decoded_jwt decode(const std::string& token, Decode decode) { + return decoded_jwt(token, decode); + } + + /** + * Parse a jwk + * \param token JWK Token to parse + * \return Parsed JWK + * \throw std::runtime_error Token is not in correct format + */ + inline jwk parse_jwk(const traits::nlohmann_json::string_type& token) { + return jwk(token); + } + + /** + * Parse a jwks + * \param token JWKs Token to parse + * \return Parsed JWKs + * \throw std::runtime_error Token is not in correct format + */ + inline jwks parse_jwks(const traits::nlohmann_json::string_type& token) { + return jwks(token); + } + + /** + * This type is the specialization of the \ref verify_ops::verify_context class which + * uses the standard template types. + */ + using verify_context = verify_ops::verify_context; +} // namespace jwt + +#endif // JWT_CPP_NLOHMANN_JSON_DEFAULTS_H diff --git a/Development/third_party/jwt-cpp/traits/nlohmann-json/traits.h b/Development/third_party/jwt-cpp/traits/nlohmann-json/traits.h new file mode 100644 index 000000000..7cf486902 --- /dev/null +++ b/Development/third_party/jwt-cpp/traits/nlohmann-json/traits.h @@ -0,0 +1,77 @@ +#ifndef JWT_CPP_NLOHMANN_JSON_TRAITS_H +#define JWT_CPP_NLOHMANN_JSON_TRAITS_H + +#include "jwt-cpp/jwt.h" +#include "nlohmann/json.hpp" + +namespace jwt { + namespace traits { + struct nlohmann_json { + using json = nlohmann::json; + using value_type = json; + using object_type = json::object_t; + using array_type = json::array_t; + using string_type = std::string; // current limitation of traits implementation + using number_type = json::number_float_t; + using integer_type = json::number_integer_t; + using boolean_type = json::boolean_t; + + static jwt::json::type get_type(const json& val) { + using jwt::json::type; + + if (val.type() == json::value_t::boolean) return type::boolean; + // nlohmann internally tracks two types of integers + if (val.type() == json::value_t::number_integer) return type::integer; + if (val.type() == json::value_t::number_unsigned) return type::integer; + if (val.type() == json::value_t::number_float) return type::number; + if (val.type() == json::value_t::string) return type::string; + if (val.type() == json::value_t::array) return type::array; + if (val.type() == json::value_t::object) return type::object; + + throw std::logic_error("invalid type"); + } + + static json::object_t as_object(const json& val) { + if (val.type() != json::value_t::object) throw std::bad_cast(); + return val.get(); + } + + static std::string as_string(const json& val) { + if (val.type() != json::value_t::string) throw std::bad_cast(); + return val.get(); + } + + static json::array_t as_array(const json& val) { + if (val.type() != json::value_t::array) throw std::bad_cast(); + return val.get(); + } + + static int64_t as_integer(const json& val) { + switch (val.type()) { + case json::value_t::number_integer: + case json::value_t::number_unsigned: return val.get(); + default: throw std::bad_cast(); + } + } + + static bool as_boolean(const json& val) { + if (val.type() != json::value_t::boolean) throw std::bad_cast(); + return val.get(); + } + + static double as_number(const json& val) { + if (val.type() != json::value_t::number_float) throw std::bad_cast(); + return val.get(); + } + + static bool parse(json& val, std::string str) { + val = json::parse(str.begin(), str.end()); + return true; + } + + static std::string serialize(const json& val) { return val.dump(); } + }; + } // namespace traits +} // namespace jwt + +#endif diff --git a/Development/third_party/nlohmann/README.md b/Development/third_party/nlohmann/README.md index 0a9210222..712836bd9 100644 --- a/Development/third_party/nlohmann/README.md +++ b/Development/third_party/nlohmann/README.md @@ -7,7 +7,7 @@ This directory contains the single header version of the [JSON for Modern C++](h Original source code: - Licensed under the MIT License . -- Copyright (c) 2013-2018 Niels Lohmann . +- Copyright (c) 2013-2022 Niels Lohmann . ## Modern C++ JSON schema validator diff --git a/Development/third_party/nlohmann/json-patch.cpp b/Development/third_party/nlohmann/json-patch.cpp new file mode 100644 index 000000000..3203543a9 --- /dev/null +++ b/Development/third_party/nlohmann/json-patch.cpp @@ -0,0 +1,115 @@ +#include "json-patch.hpp" + +#include + +namespace +{ + +// originally from http://jsonpatch.com/, http://json.schemastore.org/json-patch +// with fixes +const nlohmann::json patch_schema = R"patch({ + "title": "JSON schema for JSONPatch files", + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "array", + + "items": { + "oneOf": [ + { + "additionalProperties": false, + "required": [ "value", "op", "path"], + "properties": { + "path" : { "$ref": "#/definitions/path" }, + "op": { + "description": "The operation to perform.", + "type": "string", + "enum": [ "add", "replace", "test" ] + }, + "value": { + "description": "The value to add, replace or test." + } + } + }, + { + "additionalProperties": false, + "required": [ "op", "path"], + "properties": { + "path" : { "$ref": "#/definitions/path" }, + "op": { + "description": "The operation to perform.", + "type": "string", + "enum": [ "remove" ] + } + } + }, + { + "additionalProperties": false, + "required": [ "from", "op", "path" ], + "properties": { + "path" : { "$ref": "#/definitions/path" }, + "op": { + "description": "The operation to perform.", + "type": "string", + "enum": [ "move", "copy" ] + }, + "from": { + "$ref": "#/definitions/path", + "description": "A JSON Pointer path pointing to the location to move/copy from." + } + } + } + ] + }, + "definitions": { + "path": { + "description": "A JSON Pointer path.", + "type": "string" + } + } +})patch"_json; +} // namespace + +namespace nlohmann +{ + +json_patch::json_patch(json &&patch) + : j_(std::move(patch)) +{ + validateJsonPatch(j_); +} + +json_patch::json_patch(const json &patch) + : j_(std::move(patch)) +{ + validateJsonPatch(j_); +} + +json_patch &json_patch::add(const json::json_pointer &ptr, json value) +{ + j_.push_back(json{{"op", "add"}, {"path", ptr.to_string()}, {"value", std::move(value)}}); + return *this; +} + +json_patch &json_patch::replace(const json::json_pointer &ptr, json value) +{ + j_.push_back(json{{"op", "replace"}, {"path", ptr.to_string()}, {"value", std::move(value)}}); + return *this; +} + +json_patch &json_patch::remove(const json::json_pointer &ptr) +{ + j_.push_back(json{{"op", "remove"}, {"path", ptr.to_string()}}); + return *this; +} + +void json_patch::validateJsonPatch(json const &patch) +{ + // static put here to have it created at the first usage of validateJsonPatch + static nlohmann::json_schema::json_validator patch_validator(patch_schema); + + patch_validator.validate(patch); + + for (auto const &op : patch) + json::json_pointer(op["path"].get()); +} + +} // namespace nlohmann diff --git a/Development/third_party/nlohmann/json-patch.hpp b/Development/third_party/nlohmann/json-patch.hpp new file mode 100644 index 000000000..39a579b16 --- /dev/null +++ b/Development/third_party/nlohmann/json-patch.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include + +namespace nlohmann +{ +class JsonPatchFormatException : public std::exception +{ +public: + explicit JsonPatchFormatException(std::string msg) + : ex_{std::move(msg)} {} + + inline const char *what() const noexcept override final { return ex_.c_str(); } + +private: + std::string ex_; +}; + +class json_patch +{ +public: + json_patch() = default; + json_patch(json &&patch); + json_patch(const json &patch); + + json_patch &add(const json::json_pointer &, json value); + json_patch &replace(const json::json_pointer &, json value); + json_patch &remove(const json::json_pointer &); + + json &get_json() { return j_; } + const json &get_json() const { return j_; } + + operator json() const { return j_; } + +private: + json j_ = nlohmann::json::array(); + + static void validateJsonPatch(json const &patch); +}; +} // namespace nlohmann diff --git a/Development/third_party/nlohmann/json-schema.hpp b/Development/third_party/nlohmann/json-schema.hpp index 165a9c90f..07befd347 100644 --- a/Development/third_party/nlohmann/json-schema.hpp +++ b/Development/third_party/nlohmann/json-schema.hpp @@ -24,11 +24,11 @@ #include #ifdef NLOHMANN_JSON_VERSION_MAJOR -# if (NLOHMANN_JSON_VERSION_MAJOR * 10000 + NLOHMANN_JSON_VERSION_MINOR * 100 + NLOHMANN_JSON_VERSION_PATCH) < 30600 -# error "Please use this library with NLohmann's JSON version 3.6.0 or higher" +# if (NLOHMANN_JSON_VERSION_MAJOR * 10000 + NLOHMANN_JSON_VERSION_MINOR * 100 + NLOHMANN_JSON_VERSION_PATCH) < 30800 +# error "Please use this library with NLohmann's JSON version 3.8.0 or higher" # endif #else -# error "expected existing NLOHMANN_JSON_VERSION_MAJOR preproc variable, please update to NLohmann's JSON 3.6.0" +# error "expected existing NLOHMANN_JSON_VERSION_MAJOR preproc variable, please update to NLohmann's JSON 3.8.0" #endif // make yourself a home - welcome to nlohmann's namespace @@ -48,18 +48,20 @@ class JSON_SCHEMA_VALIDATOR_API json_uri { std::string urn_; - std::string proto_; - std::string hostname_; + std::string scheme_; + std::string authority_; std::string path_; - json::json_pointer pointer_; + + json::json_pointer pointer_; // fragment part if JSON-Pointer + std::string identifier_; // fragment part if Locatation Independent ID protected: // decodes a JSON uri and replaces all or part of the currently stored values void update(const std::string &uri); - std::tuple tie() const + std::tuple as_tuple() const { - return std::tie(urn_, proto_, hostname_, path_, pointer_); + return std::make_tuple(urn_, scheme_, authority_, path_, identifier_ != "" ? identifier_ : pointer_.to_string()); } public: @@ -68,14 +70,23 @@ class JSON_SCHEMA_VALIDATOR_API json_uri update(uri); } - const std::string protocol() const { return proto_; } - const std::string hostname() const { return hostname_; } - const std::string path() const { return path_; } + const std::string &scheme() const { return scheme_; } + const std::string &authority() const { return authority_; } + const std::string &path() const { return path_; } + + const json::json_pointer &pointer() const { return pointer_; } + const std::string &identifier() const { return identifier_; } - const json::json_pointer pointer() const { return pointer_; } + std::string fragment() const + { + if (identifier_ == "") + return pointer_.to_string(); + else + return identifier_; + } - const std::string url() const { return location(); } - const std::string location() const; + std::string url() const { return location(); } + std::string location() const; static std::string escape(const std::string &); @@ -91,6 +102,9 @@ class JSON_SCHEMA_VALIDATOR_API json_uri // append a pointer-field to the pointer-part of this uri json_uri append(const std::string &field) const { + if (identifier_ != "") + return *this; + json_uri u = *this; u.pointer_ /= field; return u; @@ -100,12 +114,12 @@ class JSON_SCHEMA_VALIDATOR_API json_uri friend bool operator<(const json_uri &l, const json_uri &r) { - return l.tie() < r.tie(); + return l.as_tuple() < r.as_tuple(); } friend bool operator==(const json_uri &l, const json_uri &r) { - return l.tie() == r.tie(); + return l.as_tuple() == r.as_tuple(); } friend std::ostream &operator<<(std::ostream &os, const json_uri &u); @@ -118,6 +132,7 @@ extern json draft7_schema_builtin; typedef std::function schema_loader; typedef std::function format_checker; +typedef std::function content_checker; // Interface for validation error handlers class JSON_SCHEMA_VALIDATOR_API error_handler @@ -141,6 +156,11 @@ class JSON_SCHEMA_VALIDATOR_API basic_error_handler : public error_handler operator bool() const { return error_; } }; +/** + * Checks validity of JSON schema built-in string format specifiers like 'date-time', 'ipv4', ... + */ +void JSON_SCHEMA_VALIDATOR_API default_string_format_check(const std::string &format, const std::string &value); + class root_schema; class JSON_SCHEMA_VALIDATOR_API json_validator @@ -148,19 +168,28 @@ class JSON_SCHEMA_VALIDATOR_API json_validator std::unique_ptr root_; public: - json_validator(schema_loader = nullptr, format_checker = nullptr); + json_validator(schema_loader = nullptr, format_checker = nullptr, content_checker = nullptr); + + json_validator(const json &, schema_loader = nullptr, format_checker = nullptr, content_checker = nullptr); + json_validator(json &&, schema_loader = nullptr, format_checker = nullptr, content_checker = nullptr); + json_validator(json_validator &&); - ~json_validator(); json_validator &operator=(json_validator &&); - // insert and set thea root-schema + json_validator(json_validator const &) = delete; + json_validator &operator=(json_validator const &) = delete; + + ~json_validator(); + + // insert and set the root-schema void set_root_schema(const json &); + void set_root_schema(json &&); // validate a json-document based on the root-schema - void validate(const json &) const; + json validate(const json &) const; // validate a json-document based on the root-schema with a custom error-handler - void validate(const json &, error_handler &) const; + json validate(const json &, error_handler &, const json_uri &initial_uri = json_uri("#")) const; }; } // namespace json_schema diff --git a/Development/third_party/nlohmann/json-uri.cpp b/Development/third_party/nlohmann/json-uri.cpp index c0e37cb76..260255613 100644 --- a/Development/third_party/nlohmann/json-uri.cpp +++ b/Development/third_party/nlohmann/json-uri.cpp @@ -6,7 +6,7 @@ * SPDX-License-Identifier: MIT * */ -#include "json-schema.hpp" +#include #include @@ -35,7 +35,7 @@ void json_uri::update(const std::string &uri) } std::string hex = pointer.substr(pos + 1, 2); - char ascii = (char) std::strtoul(hex.c_str(), nullptr, 16); + char ascii = static_cast(std::strtoul(hex.c_str(), nullptr, 16)); pointer.replace(pos, 3, 1, ascii); pos--; @@ -44,16 +44,15 @@ void json_uri::update(const std::string &uri) auto location = uri.substr(0, pointer_separator); - if (location.size()) { // a location part has been found - pointer_ = ""_json_pointer; // if a location is given, the pointer is emptied + if (location.size()) { // a location part has been found // if it is an URN take it as it is if (location.find("urn:") == 0) { urn_ = location; // and clear URL members - proto_ = ""; - hostname_ = ""; + scheme_ = ""; + authority_ = ""; path_ = ""; } else { // it is an URL @@ -65,13 +64,13 @@ void json_uri::update(const std::string &uri) urn_ = ""; // clear URN-member if URL is parsed - proto_ = location.substr(pos, proto - pos); + scheme_ = location.substr(pos, proto - pos); pos = 3 + proto; // 3 == "://" - auto hostname = location.find("/", pos); - if (hostname != std::string::npos) { // and the hostname (no proto without hostname) - hostname_ = location.substr(pos, hostname - pos); - pos = hostname; + auto authority = location.find("/", pos); + if (authority != std::string::npos) { // and the hostname (no proto without hostname) + authority_ = location.substr(pos, authority - pos); + pos = authority; } } @@ -91,20 +90,26 @@ void json_uri::update(const std::string &uri) } } - pointer_ = json::json_pointer(pointer); + pointer_ = ""_json_pointer; + identifier_ = ""; + + if (pointer[0] == '/') + pointer_ = json::json_pointer(pointer); + else + identifier_ = pointer; } -const std::string json_uri::location() const +std::string json_uri::location() const { if (urn_.size()) return urn_; std::stringstream s; - if (proto_.size() > 0) - s << proto_ << "://"; + if (scheme_.size() > 0) + s << scheme_ << "://"; - s << hostname_ + s << authority_ << path_; return s.str(); @@ -114,7 +119,12 @@ std::string json_uri::to_string() const { std::stringstream s; - s << location() << " # " << pointer_.to_string(); + s << location() << " # "; + + if (identifier_ == "") + s << pointer_.to_string(); + else + s << identifier_; return s.str(); } diff --git a/Development/third_party/nlohmann/json-validator.cpp b/Development/third_party/nlohmann/json-validator.cpp index 2a7e063e6..1fd0de1ac 100644 --- a/Development/third_party/nlohmann/json-validator.cpp +++ b/Development/third_party/nlohmann/json-validator.cpp @@ -6,13 +6,18 @@ * SPDX-License-Identifier: MIT * */ -#include +#include +#include "json-patch.hpp" + +#include #include #include #include +#include using nlohmann::json; +using nlohmann::json_patch; using nlohmann::json_uri; using nlohmann::json_schema::root_schema; using namespace nlohmann::json_schema; @@ -34,12 +39,32 @@ class schema { protected: root_schema *root_; + json default_value_ = nullptr; + +protected: + virtual std::shared_ptr make_for_default_( + std::shared_ptr<::schema> & /* sch */, + root_schema * /* root */, + std::vector & /* uris */, + nlohmann::json & /* default_value */) const + { + return nullptr; + }; public: + virtual ~schema() = default; + schema(root_schema *root) : root_(root) {} - virtual void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const = 0; + virtual void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const = 0; + + virtual const json &default_value(const json::json_pointer &, const json &, error_handler &) const + { + return default_value_; + } + + void set_default_value(const json &v) { default_value_ = v; } static std::shared_ptr make(json &schema, root_schema *root, @@ -50,22 +75,61 @@ class schema class schema_ref : public schema { const std::string id_; - std::shared_ptr target_; + std::weak_ptr target_; + std::shared_ptr target_strong_; // for references to references keep also the shared_ptr because + // no one else might use it after resolving - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final { - if (target_) - target_->validate(ptr, instance, e); + auto target = target_.lock(); + + if (target) + target->validate(ptr, instance, patch, e); else - e.error(ptr, instance, "unresolved schema-reference " + id_); + e.error(ptr, instance, "unresolved or freed schema-reference " + id_); + } + + const json &default_value(const json::json_pointer &ptr, const json &instance, error_handler &e) const override final + { + if (!default_value_.is_null()) + return default_value_; + + auto target = target_.lock(); + if (target) + return target->default_value(ptr, instance, e); + + e.error(ptr, instance, "unresolved or freed schema-reference " + id_); + + return default_value_; } +protected: + virtual std::shared_ptr make_for_default_( + std::shared_ptr<::schema> &sch, + root_schema *root, + std::vector &uris, + nlohmann::json &default_value) const override + { + // create a new reference schema using the original reference (which will be resolved later) + // to store this overloaded default value #209 + auto result = std::make_shared(uris[0].to_string(), root); + result->set_target(sch, true); + result->set_default_value(default_value); + return result; + }; + public: schema_ref(const std::string &id, root_schema *root) : schema(root), id_(id) {} const std::string &id() const { return id_; } - void set_target(std::shared_ptr target) { target_ = target; } + + void set_target(const std::shared_ptr &target, bool strong = false) + { + target_ = target; + if (strong) + target_strong_ = target; + } }; } // namespace @@ -75,16 +139,17 @@ namespace nlohmann namespace json_schema { -class root_schema : public schema +class root_schema { schema_loader loader_; format_checker format_check_; + content_checker content_check_; std::shared_ptr root_; struct schema_file { - std::map> schemas; - std::map> unresolved; // contains all unresolved references from any other file seen during parsing + std::map> schemas; + std::map> unresolved; // contains all unresolved references from any other file seen during parsing json unknown_keywords; }; @@ -101,25 +166,32 @@ class root_schema : public schema } public: - root_schema(schema_loader loader, - format_checker format) - : schema(this), loader_(loader), format_check_(format) {} + root_schema(schema_loader &&loader, + format_checker &&format, + content_checker &&content) + + : loader_(std::move(loader)), + format_check_(std::move(format)), + content_check_(std::move(content)) + { + } format_checker &format_check() { return format_check_; } + content_checker &content_check() { return content_check_; } void insert(const json_uri &uri, const std::shared_ptr &s) { auto &file = get_or_create_file(uri.location()); - auto schema = file.schemas.lower_bound(uri.pointer()); - if (schema != file.schemas.end() && !(file.schemas.key_comp()(uri.pointer(), schema->first))) { + auto sch = file.schemas.lower_bound(uri.fragment()); + if (sch != file.schemas.end() && !(file.schemas.key_comp()(uri.fragment(), sch->first))) { throw std::invalid_argument("schema with " + uri.to_string() + " already inserted"); return; } - file.schemas.insert({uri.pointer(), s}); + file.schemas.insert({uri.fragment(), s}); // was someone referencing this newly inserted schema? - auto unresolved = file.unresolved.find(uri.pointer()); + auto unresolved = file.unresolved.find(uri.fragment()); if (unresolved != file.unresolved.end()) { unresolved->second->set_target(s); file.unresolved.erase(unresolved); @@ -130,14 +202,42 @@ class root_schema : public schema { auto &file = get_or_create_file(uri.location()); auto new_uri = uri.append(key); - auto pointer = new_uri.pointer(); + auto fragment = new_uri.pointer(); // is there a reference looking for this unknown-keyword, which is thus no longer a unknown keyword but a schema - auto unresolved = file.unresolved.find(pointer); + auto unresolved = file.unresolved.find(fragment.to_string()); if (unresolved != file.unresolved.end()) schema::make(value, this, {}, {{new_uri}}); - else // no, nothing ref'd it - file.unknown_keywords[pointer] = value; + else { // no, nothing ref'd it, keep for later + + // need to create an object for each reference-token in the + // JSON-Pointer When not existing, a stringified integer reference + // token (e.g. "123") in the middle of the pointer will be + // interpreted a an array-index and an array will be created. + + // json_pointer's reference_tokens is private - get them + std::deque ref_tokens; + auto uri_pointer = uri.pointer(); + while (!uri_pointer.empty()) { + ref_tokens.push_front(uri_pointer.back()); + uri_pointer.pop_back(); + } + + // for each token create an object, if not already existing + auto unk_kw = &file.unknown_keywords; + for (auto &rt : ref_tokens) { + auto existing_object = unk_kw->find(rt); + if (existing_object == unk_kw->end()) + (*unk_kw)[rt] = json::object(); + unk_kw = &(*unk_kw)[rt]; + } + (*unk_kw)[key] = value; + } + + // recursively add possible subschemas of unknown keywords + if (value.type() == json::value_t::object) + for (auto &subsch : value.items()) + insert_unknown_keyword(new_uri, subsch.key(), subsch.value()); } std::shared_ptr get_or_create_ref(const json_uri &uri) @@ -145,33 +245,41 @@ class root_schema : public schema auto &file = get_or_create_file(uri.location()); // existing schema - auto schema = file.schemas.find(uri.pointer()); - if (schema != file.schemas.end()) - return schema->second; + auto sch = file.schemas.find(uri.fragment()); + if (sch != file.schemas.end()) + return sch->second; // referencing an unknown keyword, turn it into schema - try { - auto &subschema = file.unknown_keywords.at(uri.pointer()); - auto s = schema::make(subschema, this, {}, {{uri}}); - file.unknown_keywords.erase(uri.pointer()); - return s; - } catch (...) { + // + // an unknown keyword can only be referenced by a json-pointer, + // not by a plain name fragment + if (uri.pointer().to_string() != "") { + try { + auto &subschema = file.unknown_keywords.at(uri.pointer()); // null is returned if not existing + auto s = schema::make(subschema, this, {}, {{uri}}); // A JSON Schema MUST be an object or a boolean. + if (s) { // nullptr if invalid schema, e.g. null + file.unknown_keywords.erase(uri.fragment()); + return s; + } + } catch (nlohmann::detail::out_of_range &) { // at() did not find it + } } // get or create a schema_ref - auto r = file.unresolved.lower_bound(uri.pointer()); - if (r != file.unresolved.end() && !(file.unresolved.key_comp()(uri.pointer(), r->first))) { - return r->second; + auto r = file.unresolved.lower_bound(uri.fragment()); + if (r != file.unresolved.end() && !(file.unresolved.key_comp()(uri.fragment(), r->first))) { + return r->second; // unresolved, already seen previously - use existing reference } else { return file.unresolved.insert(r, - {uri.pointer(), std::make_shared(uri.to_string(), this)}) - ->second; + {uri.fragment(), std::make_shared(uri.to_string(), this)}) + ->second; // unresolved, create reference } } - void set_root_schema(json schema) + void set_root_schema(json sch) { - root_ = schema::make(schema, this, {}, {{"#"}}); + files_.clear(); + root_ = schema::make(sch, this, {}, {{"#"}}); // load all files which have not yet been loaded do { @@ -185,11 +293,11 @@ class root_schema : public schema for (auto &loc : locations) { if (files_[loc].schemas.size() == 0) { // nothing has been loaded for this file if (loader_) { - json sch; + json loaded_schema; - loader_(loc, sch); + loader_(loc, loaded_schema); - schema::make(sch, this, {}, {{loc}}); + schema::make(loaded_schema, this, {}, {{loc}}); new_schema_loaded = true; } else { throw std::invalid_argument("external schema reference '" + loc + "' needs loading, but no loader callback given"); @@ -200,14 +308,59 @@ class root_schema : public schema if (!new_schema_loaded) // if no new schema loaded, no need to try again break; } while (1); + + for (const auto &file : files_) { + if (file.second.unresolved.size() != 0) { + // Build a representation of the undefined + // references as a list of comma-separated strings. + auto n_urefs = file.second.unresolved.size(); + std::string urefs = "["; + + decltype(n_urefs) counter = 0; + for (const auto &p : file.second.unresolved) { + urefs += p.first; + + if (counter != n_urefs - 1u) { + urefs += ", "; + } + + ++counter; + } + + urefs += "]"; + + throw std::invalid_argument("after all files have been parsed, '" + + (file.first == "" ? "" : file.first) + + "' has still the following undefined references: " + urefs); + } + } } - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const final + void validate(const json::json_pointer &ptr, + const json &instance, + json_patch &patch, + error_handler &e, + const json_uri &initial) const { - if (root_) - root_->validate(ptr, instance, e); - else + if (!root_) { e.error(ptr, "", "no root schema has yet been set for validating an instance"); + return; + } + + auto file_entry = files_.find(initial.location()); + if (file_entry == files_.end()) { + e.error(ptr, "", "no file found serving requested root-URI. " + initial.location()); + return; + } + + auto &file = file_entry->second; + auto sch = file.schemas.find(initial.fragment()); + if (sch == file.schemas.end()) { + e.error(ptr, "", "no schema find for request initial URI: " + initial.to_string()); + return; + } + + sch->second->validate(ptr, instance, patch, e); } }; @@ -225,7 +378,7 @@ class first_error_handler : public error_handler json instance_; std::string message_; - void error(const json::json_pointer & ptr, const json & instance, const std::string & message) override + void error(const json::json_pointer &ptr, const json &instance, const std::string &message) override { if (*this) return; @@ -242,15 +395,20 @@ class logical_not : public schema { std::shared_ptr subschema_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final { first_error_handler esub; - subschema_->validate(ptr, instance, esub); + subschema_->validate(ptr, instance, patch, esub); if (!esub) e.error(ptr, instance, "the subschema has succeeded, but it is required to not validate"); } + const json &default_value(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + { + return subschema_->default_value(ptr, instance, e); + } + public: logical_not(json &sch, root_schema *root, @@ -272,15 +430,18 @@ class logical_combination : public schema { std::vector> subschemata_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const final { size_t count = 0; for (auto &s : subschemata_) { first_error_handler esub; - s->validate(ptr, instance, esub); + auto oldPatchSize = patch.get_json().size(); + s->validate(ptr, instance, patch, esub); if (!esub) count++; + else + patch.get_json().get_ref().resize(oldPatchSize); if (is_validate_complete(instance, ptr, e, esub, count)) return; @@ -319,7 +480,7 @@ template <> const std::string logical_combination::key = "oneOf"; template <> -bool logical_combination::is_validate_complete(const json &instance, const json::json_pointer &ptr, error_handler &e, const first_error_handler &esub, size_t) +bool logical_combination::is_validate_complete(const json &, const json::json_pointer &, error_handler &e, const first_error_handler &esub, size_t) { if (esub) e.error(esub.ptr_, esub.instance_, "at least one subschema has failed, but all of them are required to validate - " + esub.message_); @@ -354,13 +515,13 @@ class type_schema : public schema std::shared_ptr if_, then_, else_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override final { // depending on the type of instance run the type specific validator - if present - auto type = type_[(uint8_t) instance.type()]; + auto type = type_[static_cast(instance.type())]; if (type) - type->validate(ptr, instance, e); + type->validate(ptr, instance, patch, e); else e.error(ptr, instance, "unexpected instance type"); @@ -381,27 +542,42 @@ class type_schema : public schema e.error(ptr, instance, "instance not const"); for (auto l : logic_) - l->validate(ptr, instance, e); + l->validate(ptr, instance, patch, e); if (if_) { first_error_handler err; - if_->validate(ptr, instance, err); + if_->validate(ptr, instance, patch, err); if (!err) { if (then_) - then_->validate(ptr, instance, e); + then_->validate(ptr, instance, patch, e); } else { if (else_) - else_->validate(ptr, instance, e); + else_->validate(ptr, instance, patch, e); } } + if (instance.is_null()) { + patch.add(nlohmann::json::json_pointer{}, default_value_); + } } +protected: + virtual std::shared_ptr make_for_default_( + std::shared_ptr<::schema> & /* sch */, + root_schema * /* root */, + std::vector & /* uris */, + nlohmann::json &default_value) const override + { + auto result = std::make_shared(*this); + result->set_default_value(default_value); + return result; + }; + public: type_schema(json &sch, root_schema *root, const std::vector &uris) - : schema(root), type_((uint8_t) json::value_t::discarded + 1) + : schema(root), type_(static_cast(json::value_t::discarded) + 1) { // association between JSON-schema-type and NLohmann-types static const std::vector> schema_types = { @@ -411,7 +587,6 @@ class type_schema : public schema {"string", json::value_t::string}, {"boolean", json::value_t::boolean}, {"integer", json::value_t::number_integer}, - {"integer", json::value_t::number_unsigned}, {"number", json::value_t::number_float}, }; @@ -420,7 +595,7 @@ class type_schema : public schema auto attr = sch.find("type"); if (attr == sch.end()) // no type field means all sub-types possible for (auto &t : schema_types) - type_[(uint8_t) t.second] = type_schema::make(sch, t.second, root, uris, known_keywords); + type_[static_cast(t.second)] = type_schema::make(sch, t.second, root, uris, known_keywords); else { switch (attr.value().type()) { // "type": "type" @@ -428,14 +603,16 @@ class type_schema : public schema auto schema_type = attr.value().get(); for (auto &t : schema_types) if (t.first == schema_type) - type_[(uint8_t) t.second] = type_schema::make(sch, t.second, root, uris, known_keywords); + type_[static_cast(t.second)] = type_schema::make(sch, t.second, root, uris, known_keywords); } break; case json::value_t::array: // "type": ["type1", "type2"] - for (auto &schema_type : attr.value()) + for (auto &array_value : attr.value()) { + auto schema_type = array_value.get(); for (auto &t : schema_types) if (t.first == schema_type) - type_[(uint8_t) t.second] = type_schema::make(sch, t.second, root, uris, known_keywords); + type_[static_cast(t.second)] = type_schema::make(sch, t.second, root, uris, known_keywords); + } break; default: @@ -445,15 +622,28 @@ class type_schema : public schema sch.erase(attr); } + attr = sch.find("default"); + if (attr != sch.end()) { + set_default_value(attr.value()); + sch.erase(attr); + } + for (auto &key : known_keywords) sch.erase(key); - // with nlohmann::json floats can be seen as unsigned or integer - reuse the number-validator for - // integer values as well, if they have not been specified - if (type_[(uint8_t) json::value_t::number_float] && !type_[(uint8_t) json::value_t::number_integer]) - type_[(uint8_t) json::value_t::number_integer] = - type_[(uint8_t) json::value_t::number_unsigned] = - type_[(uint8_t) json::value_t::number_float]; + // with nlohmann::json float instance (but number in schema-definition) can be seen as unsigned or integer - + // reuse the number-validator for integer values as well, if they have not been specified explicitly + if (type_[static_cast(json::value_t::number_float)] && !type_[static_cast(json::value_t::number_integer)]) + type_[static_cast(json::value_t::number_integer)] = type_[static_cast(json::value_t::number_float)]; + + // #54: JSON-schema does not differentiate between unsigned and signed integer - nlohmann::json does + // we stick with JSON-schema: use the integer-validator if instance-value is unsigned + type_[static_cast(json::value_t::number_unsigned)] = type_[static_cast(json::value_t::number_integer)]; + + // special for binary types + if (type_[static_cast(json::value_t::string)]) { + type_[static_cast(json::value_t::binary)] = type_[static_cast(json::value_t::string)]; + } attr = sch.find("enum"); if (attr != sch.end()) { @@ -525,20 +715,21 @@ class string : public schema #endif std::pair format_; + std::tuple content_{false, "", ""}; std::size_t utf8_length(const std::string &s) const { size_t len = 0; - for (const unsigned char &c : s) + for (auto c : s) if ((c & 0xc0) != 0x80) len++; return len; } - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override { if (minLength_.first) { - if (utf8_length(instance) < minLength_.second) { + if (utf8_length(instance.get()) < minLength_.second) { std::ostringstream s; s << "instance is too short as per minLength:" << minLength_.second; e.error(ptr, instance, s.str()); @@ -546,13 +737,31 @@ class string : public schema } if (maxLength_.first) { - if (utf8_length(instance) > maxLength_.second) { + if (utf8_length(instance.get()) > maxLength_.second) { std::ostringstream s; s << "instance is too long as per maxLength: " << maxLength_.second; e.error(ptr, instance, s.str()); } } + if (std::get<0>(content_)) { + if (root_->content_check() == nullptr) + e.error(ptr, instance, std::string("a content checker was not provided but a contentEncoding or contentMediaType for this string have been present: '") + std::get<1>(content_) + "' '" + std::get<2>(content_) + "'"); + else { + try { + root_->content_check()(std::get<1>(content_), std::get<2>(content_), instance); + } catch (const std::exception &ex) { + e.error(ptr, instance, std::string("content-checking failed: ") + ex.what()); + } + } + } else if (instance.type() == json::value_t::binary) { + e.error(ptr, instance, "expected string, but get binary data"); + } + + if (instance.type() != json::value_t::string) { + return; // next checks only for strings + } + #ifndef NO_STD_REGEX if (pattern_.first && !REGEX_NAMESPACE::regex_search(instance.get(), pattern_.second)) @@ -564,7 +773,7 @@ class string : public schema e.error(ptr, instance, std::string("a format checker was not provided but a format keyword for this string is present: ") + format_.second); else { try { - root_->format_check()(format_.second, instance); + root_->format_check()(format_.second, instance.get()); } catch (const std::exception &ex) { e.error(ptr, instance, std::string("format-checking failed: ") + ex.what()); } @@ -578,20 +787,51 @@ class string : public schema { auto attr = sch.find("maxLength"); if (attr != sch.end()) { - maxLength_ = {true, attr.value()}; + maxLength_ = {true, attr.value().get()}; sch.erase(attr); } attr = sch.find("minLength"); if (attr != sch.end()) { - minLength_ = {true, attr.value()}; + minLength_ = {true, attr.value().get()}; + sch.erase(attr); + } + + attr = sch.find("contentEncoding"); + if (attr != sch.end()) { + std::get<0>(content_) = true; + std::get<1>(content_) = attr.value().get(); + + // special case for nlohmann::json-binary-types + // + // https://github.com/pboettch/json-schema-validator/pull/114 + // + // We cannot use explicitly in a schema: {"type": "binary"} or + // "type": ["binary", "number"] we have to be implicit. For a + // schema where "contentEncoding" is set to "binary", an instance + // of type json::value_t::binary is accepted. If a + // contentEncoding-callback has to be provided and is called + // accordingly. For encoding=binary, no other type validations are done + + sch.erase(attr); + } + + attr = sch.find("contentMediaType"); + if (attr != sch.end()) { + std::get<0>(content_) = true; + std::get<2>(content_) = attr.value().get(); + sch.erase(attr); } + if (std::get<0>(content_) == true && root_->content_check() == nullptr) { + throw std::invalid_argument{"schema contains contentEncoding/contentMediaType but content checker was not set"}; + } + #ifndef NO_STD_REGEX attr = sch.find("pattern"); if (attr != sch.end()) { - patternString_ = attr.value(); + patternString_ = attr.value().get(); pattern_ = {true, REGEX_NAMESPACE::regex(attr.value().get(), REGEX_NAMESPACE::regex::ECMAScript)}; sch.erase(attr); @@ -600,7 +840,10 @@ class string : public schema attr = sch.find("format"); if (attr != sch.end()) { - format_ = {true, attr.value()}; + if (root_->format_check() == nullptr) + throw std::invalid_argument{"a format checker was not provided but a format keyword for this string is present: " + format_.second}; + + format_ = {true, attr.value().get()}; sch.erase(attr); } } @@ -621,11 +864,16 @@ class numeric : public schema bool violates_multiple_of(T x) const { double res = std::remainder(x, multipleOf_.second); - double eps = std::nextafter(x, 0) - x; + double multiple = std::fabs(x / multipleOf_.second); + if (multiple > 1) { + res = res / multiple; + } + double eps = std::nextafter(x, 0) - static_cast(x); + return std::fabs(res) > std::fabs(eps); } - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override { T value = instance; // conversion of json to value_type @@ -633,15 +881,19 @@ class numeric : public schema if (violates_multiple_of(value)) e.error(ptr, instance, "instance is not a multiple of " + std::to_string(multipleOf_.second)); - if (maximum_.first) - if ((exclusiveMaximum_ && value >= maximum_.second) || - value > maximum_.second) + if (maximum_.first) { + if (exclusiveMaximum_ && value >= maximum_.second) + e.error(ptr, instance, "instance exceeds or equals maximum of " + std::to_string(maximum_.second)); + else if (value > maximum_.second) e.error(ptr, instance, "instance exceeds maximum of " + std::to_string(maximum_.second)); + } - if (minimum_.first) - if ((exclusiveMinimum_ && value <= minimum_.second) || - value < minimum_.second) + if (minimum_.first) { + if (exclusiveMinimum_ && value <= minimum_.second) + e.error(ptr, instance, "instance is below or equals minimum of " + std::to_string(minimum_.second)); + else if (value < minimum_.second) e.error(ptr, instance, "instance is below minimum of " + std::to_string(minimum_.second)); + } } public: @@ -650,33 +902,33 @@ class numeric : public schema { auto attr = sch.find("maximum"); if (attr != sch.end()) { - maximum_ = {true, attr.value()}; + maximum_ = {true, attr.value().get()}; kw.insert("maximum"); } attr = sch.find("minimum"); if (attr != sch.end()) { - minimum_ = {true, attr.value()}; + minimum_ = {true, attr.value().get()}; kw.insert("minimum"); } attr = sch.find("exclusiveMaximum"); if (attr != sch.end()) { exclusiveMaximum_ = true; - maximum_ = {true, attr.value()}; + maximum_ = {true, attr.value().get()}; kw.insert("exclusiveMaximum"); } attr = sch.find("exclusiveMinimum"); if (attr != sch.end()) { - minimum_ = {true, attr.value()}; exclusiveMinimum_ = true; + minimum_ = {true, attr.value().get()}; kw.insert("exclusiveMinimum"); } attr = sch.find("multipleOf"); if (attr != sch.end()) { - multipleOf_ = {true, attr.value()}; + multipleOf_ = {true, attr.value().get()}; kw.insert("multipleOf"); } } @@ -684,7 +936,7 @@ class numeric : public schema class null : public schema { - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override { if (!instance.is_null()) e.error(ptr, instance, "expected to be null"); @@ -697,7 +949,7 @@ class null : public schema class boolean_type : public schema { - void validate(const json::json_pointer &, const json &, error_handler &) const override {} + void validate(const json::json_pointer &, const json &, json_patch &, error_handler &) const override {} public: boolean_type(json &, root_schema *root) @@ -707,12 +959,12 @@ class boolean_type : public schema class boolean : public schema { bool true_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override { if (!true_) { // false schema // empty array - //switch (instance.type()) { - //case json::value_t::array: + // switch (instance.type()) { + // case json::value_t::array: // if (instance.size() != 0) // valid false-schema // e.error(ptr, instance, "false-schema required empty array"); // return; @@ -731,7 +983,7 @@ class required : public schema { const std::vector required_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override final + void validate(const json::json_pointer &ptr, const json &instance, json_patch &, error_handler &e) const override final { for (auto &r : required_) if (instance.find(r) == instance.end()) @@ -759,7 +1011,7 @@ class object : public schema std::shared_ptr propertyNames_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override { if (maxProperties_.first && instance.size() > maxProperties_.second) e.error(ptr, instance, "too many properties"); @@ -774,31 +1026,49 @@ class object : public schema // for each property in instance for (auto &p : instance.items()) { if (propertyNames_) - propertyNames_->validate(ptr, p.key(), e); + propertyNames_->validate(ptr, p.key(), patch, e); bool a_prop_or_pattern_matched = false; auto schema_p = properties_.find(p.key()); // check if it is in "properties" if (schema_p != properties_.end()) { a_prop_or_pattern_matched = true; - schema_p->second->validate(ptr / p.key(), p.value(), e); + schema_p->second->validate(ptr / p.key(), p.value(), patch, e); } +#ifndef NO_STD_REGEX // check all matching patternProperties for (auto &schema_pp : patternProperties_) if (REGEX_NAMESPACE::regex_search(p.key(), schema_pp.first)) { a_prop_or_pattern_matched = true; - schema_pp.second->validate(ptr / p.key(), p.value(), e); + schema_pp.second->validate(ptr / p.key(), p.value(), patch, e); } +#endif + // check additionalProperties as a last resort - if (!a_prop_or_pattern_matched && additionalProperties_) - additionalProperties_->validate(ptr / p.key(), p.value(), e); + if (!a_prop_or_pattern_matched && additionalProperties_) { + first_error_handler additional_prop_err; + additionalProperties_->validate(ptr / p.key(), p.value(), patch, additional_prop_err); + if (additional_prop_err) + e.error(ptr, instance, "validation failed for additional property '" + p.key() + "': " + additional_prop_err.message_); + } + } + + // reverse search + for (auto const &prop : properties_) { + const auto finding = instance.find(prop.first); + if (instance.end() == finding) { // if the prop is not in the instance + const auto &default_value = prop.second->default_value(ptr, instance, e); + if (!default_value.is_null()) { // if default value is available + patch.add((ptr / prop.first), default_value); + } + } } for (auto &dep : dependencies_) { auto prop = instance.find(dep.first); - if (prop != instance.end()) // if dependency-property is present in instance - dep.second->validate(ptr / dep.first, instance, e); // validate + if (prop != instance.end()) // if dependency-property is present in instance + dep.second->validate(ptr / dep.first, instance, patch, e); // validate } } @@ -810,13 +1080,13 @@ class object : public schema { auto attr = sch.find("maxProperties"); if (attr != sch.end()) { - maxProperties_ = {true, attr.value()}; + maxProperties_ = {true, attr.value().get()}; sch.erase(attr); } attr = sch.find("minProperties"); if (attr != sch.end()) { - minProperties_ = {true, attr.value()}; + minProperties_ = {true, attr.value().get()}; sch.erase(attr); } @@ -877,6 +1147,11 @@ class object : public schema propertyNames_ = schema::make(attr.value(), root, {"propertyNames"}, uris); sch.erase(attr); } + + attr = sch.find("default"); + if (attr != sch.end()) { + set_default_value(*attr); + } } }; @@ -893,7 +1168,7 @@ class array : public schema std::shared_ptr contains_; - void validate(const json::json_pointer &ptr, const json &instance, error_handler &e) const override + void validate(const json::json_pointer &ptr, const json &instance, json_patch &patch, error_handler &e) const override { if (maxItems_.first && instance.size() > maxItems_.second) e.error(ptr, instance, "array has too many items"); @@ -912,7 +1187,7 @@ class array : public schema size_t index = 0; if (items_schema_) for (auto &i : instance) { - items_schema_->validate(ptr / index, i, e); + items_schema_->validate(ptr / index, i, patch, e); index++; } else { @@ -929,7 +1204,7 @@ class array : public schema if (!item_validator) break; - item_validator->validate(ptr / index, i, e); + item_validator->validate(ptr / index, i, patch, e); } } @@ -937,7 +1212,7 @@ class array : public schema bool contained = false; for (auto &item : instance) { first_error_handler local_e; - contains_->validate(ptr, item, local_e); + contains_->validate(ptr, item, patch, local_e); if (!local_e) { contained = true; break; @@ -954,19 +1229,19 @@ class array : public schema { auto attr = sch.find("maxItems"); if (attr != sch.end()) { - maxItems_ = {true, attr.value()}; + maxItems_ = {true, attr.value().get()}; sch.erase(attr); } attr = sch.find("minItems"); if (attr != sch.end()) { - minItems_ = {true, attr.value()}; + minItems_ = {true, attr.value().get()}; sch.erase(attr); } attr = sch.find("uniqueItems"); if (attr != sch.end()) { - uniqueItems_ = attr.value(); + uniqueItems_ = attr.value().get(); sch.erase(attr); } @@ -1008,8 +1283,8 @@ std::shared_ptr type_schema::make(json &schema, switch (type) { case json::value_t::null: return std::make_shared(schema, root); + case json::value_t::number_unsigned: - return std::make_shared>(schema, root, kw); case json::value_t::number_integer: return std::make_shared>(schema, root, kw); case json::value_t::number_float: @@ -1025,10 +1300,14 @@ std::shared_ptr type_schema::make(json &schema, case json::value_t::discarded: // not a real type - silence please break; + + case json::value_t::binary: + break; } return nullptr; } } // namespace + namespace { @@ -1037,6 +1316,13 @@ std::shared_ptr schema::make(json &schema, const std::vector &keys, std::vector uris) { + // remove URIs which contain plain name identifiers, as sub-schemas cannot be referenced + for (auto uri = uris.begin(); uri != uris.end();) + if (uri->identifier() != "") + uri = uris.erase(uri); + else + uri++; + // append to all URIs the keys for this sub-schema for (auto &key : keys) for (auto &uri : uris) @@ -1055,7 +1341,7 @@ std::shared_ptr schema::make(json &schema, if (std::find(uris.begin(), uris.end(), attr.value().get()) == uris.end()) - uris.push_back(uris.back().derive(attr.value())); // so add it to the list if it is not there already + uris.push_back(uris.back().derive(attr.value().get())); // so add it to the list if it is not there already schema.erase(attr); } @@ -1070,27 +1356,37 @@ std::shared_ptr schema::make(json &schema, if (attr != schema.end()) { // this schema is a reference // the last one on the uri-stack is the last id seen before coming here, // so this is the origial URI for this reference, the $ref-value has thus be resolved from it - auto id = uris.back().derive(attr.value()); + auto id = uris.back().derive(attr.value().get()); sch = root->get_or_create_ref(id); + schema.erase(attr); + + // special case where we break draft-7 and allow overriding of properties when a $ref is used + attr = schema.find("default"); + if (attr != schema.end()) { + // copy the referenced schema depending on the underlying type and modify the default value + if (auto new_sch = sch->make_for_default_(sch, root, uris, attr.value())) { + sch = new_sch; + } + schema.erase(attr); + } } else { sch = std::make_shared(schema, root, uris); } schema.erase("$schema"); - schema.erase("default"); schema.erase("title"); schema.erase("description"); } else { - return nullptr; // TODO error/throw? when schema is invalid + throw std::invalid_argument("invalid JSON-type for a schema for " + uris[0].to_string() + ", expected: boolean or object"); } - for (auto &uri : uris) { // for all URI references this schema + for (auto &uri : uris) { // for all URIs this schema is referenced by root->insert(uri, sch); if (schema.type() == json::value_t::object) for (auto &u : schema.items()) - root->insert_unknown_keyword(uri, u.key(), u.value()); + root->insert_unknown_keyword(uri, u.key(), u.value()); // insert unknown keywords for later reference } return sch; } @@ -1111,9 +1407,35 @@ namespace json_schema { json_validator::json_validator(schema_loader loader, - format_checker format) - : root_(std::unique_ptr(new root_schema(loader, format))) + format_checker format, + content_checker content) + : root_(std::unique_ptr(new root_schema(std::move(loader), + std::move(format), + std::move(content)))) +{ +} + +json_validator::json_validator(const json &schema, + schema_loader loader, + format_checker format, + content_checker content) + : json_validator(std::move(loader), + std::move(format), + std::move(content)) +{ + set_root_schema(schema); +} + +json_validator::json_validator(json &&schema, + schema_loader loader, + format_checker format, + content_checker content) + + : json_validator(std::move(loader), + std::move(format), + std::move(content)) { + set_root_schema(std::move(schema)); } // move constructor, destructor and move assignment operator can be defaulted here @@ -1127,16 +1449,23 @@ void json_validator::set_root_schema(const json &schema) root_->set_root_schema(schema); } -void json_validator::validate(const json &instance) const +void json_validator::set_root_schema(json &&schema) +{ + root_->set_root_schema(std::move(schema)); +} + +json json_validator::validate(const json &instance) const { throwing_error_handler err; - validate(instance, err); + return validate(instance, err); } -void json_validator::validate(const json &instance, error_handler &err) const +json json_validator::validate(const json &instance, error_handler &err, const json_uri &initial_uri) const { json::json_pointer ptr; - root_->validate(ptr, instance, err); + json_patch patch; + root_->validate(ptr, instance, patch, err, initial_uri); + return patch; } } // namespace json_schema diff --git a/Development/third_party/nlohmann/json.hpp b/Development/third_party/nlohmann/json.hpp index a2feec0ff..8b72ea653 100644 --- a/Development/third_party/nlohmann/json.hpp +++ b/Development/third_party/nlohmann/json.hpp @@ -1,64 +1,164 @@ -/* - __ _____ _____ _____ - __| | __| | | | JSON for Modern C++ -| | |__ | | | | | | version 3.6.1 -|_____|_____|_____|_|___| https://github.com/nlohmann/json - -Licensed under the MIT License . -SPDX-License-Identifier: MIT -Copyright (c) 2013-2019 Niels Lohmann . - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -*/ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + +/****************************************************************************\ + * Note on documentation: The source files contain links to the online * + * documentation of the public API at https://json.nlohmann.me. This URL * + * contains the most recent documentation and should also be applicable to * + * previous versions; documentation for deprecated functions is not * + * removed, but marked deprecated. See "Generate documentation" section in * + * file docs/README.md. * +\****************************************************************************/ #ifndef INCLUDE_NLOHMANN_JSON_HPP_ #define INCLUDE_NLOHMANN_JSON_HPP_ -#define NLOHMANN_JSON_VERSION_MAJOR 3 -#define NLOHMANN_JSON_VERSION_MINOR 6 -#define NLOHMANN_JSON_VERSION_PATCH 1 - #include // all_of, find, for_each -#include // assert -#include // and, not, or #include // nullptr_t, ptrdiff_t, size_t #include // hash, less #include // initializer_list -#include // istream, ostream +#ifndef JSON_NO_IO + #include // istream, ostream +#endif // JSON_NO_IO #include // random_access_iterator_tag #include // unique_ptr -#include // accumulate #include // string, stoi, to_string #include // declval, forward, move, pair, swap #include // vector // #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + #include +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// This file contains all macro definitions affecting or depending on the ABI + +#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK + #if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH) + #if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 11 || NLOHMANN_JSON_VERSION_PATCH != 3 + #warning "Already included a different version of the library!" + #endif + #endif +#endif + +#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum) +#define NLOHMANN_JSON_VERSION_MINOR 11 // NOLINT(modernize-macro-to-enum) +#define NLOHMANN_JSON_VERSION_PATCH 3 // NOLINT(modernize-macro-to-enum) + +#ifndef JSON_DIAGNOSTICS + #define JSON_DIAGNOSTICS 0 +#endif + +#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON + #define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0 +#endif + +#if JSON_DIAGNOSTICS + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag +#else + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS +#endif + +#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON + #define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp +#else + #define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION + #define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0 +#endif + +// Construct the namespace ABI tags component +#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b) json_abi ## a ## b +#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b) \ + NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b) + +#define NLOHMANN_JSON_ABI_TAGS \ + NLOHMANN_JSON_ABI_TAGS_CONCAT( \ + NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \ + NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON) + +// Construct the namespace version component +#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \ + _v ## major ## _ ## minor ## _ ## patch +#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \ + NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) + +#if NLOHMANN_JSON_NAMESPACE_NO_VERSION +#define NLOHMANN_JSON_NAMESPACE_VERSION +#else +#define NLOHMANN_JSON_NAMESPACE_VERSION \ + NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \ + NLOHMANN_JSON_VERSION_MINOR, \ + NLOHMANN_JSON_VERSION_PATCH) +#endif + +// Combine namespace components +#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b +#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \ + NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) + +#ifndef NLOHMANN_JSON_NAMESPACE +#define NLOHMANN_JSON_NAMESPACE \ + nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \ + NLOHMANN_JSON_ABI_TAGS, \ + NLOHMANN_JSON_NAMESPACE_VERSION) +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN +#define NLOHMANN_JSON_NAMESPACE_BEGIN \ + namespace nlohmann \ + { \ + inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \ + NLOHMANN_JSON_ABI_TAGS, \ + NLOHMANN_JSON_NAMESPACE_VERSION) \ + { +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_END +#define NLOHMANN_JSON_NAMESPACE_END \ + } /* namespace (inline namespace) NOLINT(readability/namespace) */ \ + } // namespace nlohmann +#endif + // #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + #include // transform #include // array -#include // and, not #include // forward_list #include // inserter, front_inserter, end #include // map @@ -70,1174 +170,2692 @@ SOFTWARE. #include // valarray // #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include // nullptr_t #include // exception +#if JSON_DIAGNOSTICS + #include // accumulate +#endif #include // runtime_error #include // to_string +#include // vector -// #include +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include // array #include // size_t +#include // uint8_t +#include // string -namespace nlohmann -{ -namespace detail -{ -/// struct to capture the start position of the current token -struct position_t -{ - /// the total number of characters read - std::size_t chars_read_total = 0; - /// the number of characters read in the current line - std::size_t chars_read_current_line = 0; - /// the number of lines read - std::size_t lines_read = 0; +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT - /// conversion to size_t to preserve SAX interface - constexpr operator size_t() const - { - return chars_read_total; - } -}; -} // namespace detail -} // namespace nlohmann +#include // declval, pair +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT -namespace nlohmann -{ -namespace detail -{ -//////////////// -// exceptions // -//////////////// -/*! -@brief general exception of the @ref basic_json class -This class is an extension of `std::exception` objects with a member @a id for -exception ids. It is used as the base class for all exceptions thrown by the -@ref basic_json class. This class can hence be used as "wildcard" to catch -exceptions. +#include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + -Subclasses: -- @ref parse_error for exceptions indicating a parse error -- @ref invalid_iterator for exceptions indicating errors with iterators -- @ref type_error for exceptions indicating executing a member function with - a wrong type -- @ref out_of_range for exceptions indicating access out of the defined range -- @ref other_error for exceptions indicating other library errors -@internal -@note To have nothrow-copy-constructible exceptions, we internally use - `std::runtime_error` which can cope with arbitrary-length error messages. - Intermediate strings are built with static functions and then passed to - the actual constructor. -@endinternal +// #include -@liveexample{The following code shows how arbitrary library exceptions can be -caught.,exception} -@since version 3.0.0 -*/ -class exception : public std::exception +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail { - public: - /// returns the explanatory string - const char* what() const noexcept override - { - return m.what(); - } - /// the id of the exception - const int id; +template struct make_void +{ + using type = void; +}; +template using void_t = typename make_void::type; - protected: - exception(int id_, const char* what_arg) : id(id_), m(what_arg) {} +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END - static std::string name(const std::string& ename, int id_) - { - return "[json.exception." + ename + "." + std::to_string(id_) + "] "; - } - private: - /// an exception object as storage for error messages - std::runtime_error m; +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +// https://en.cppreference.com/w/cpp/experimental/is_detected +struct nonesuch +{ + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + nonesuch(nonesuch const&&) = delete; + void operator=(nonesuch const&) = delete; + void operator=(nonesuch&&) = delete; }; -/*! -@brief exception indicating a parse error - -This exception is thrown by the library when a parse error occurs. Parse errors -can occur during the deserialization of JSON text, CBOR, MessagePack, as well -as when using JSON Patch. - -Member @a byte holds the byte index of the last read character in the input -file. - -Exceptions have ids 1xx. - -name / id | example message | description ------------------------------- | --------------- | ------------------------- -json.exception.parse_error.101 | parse error at 2: unexpected end of input; expected string literal | This error indicates a syntax error while deserializing a JSON text. The error message describes that an unexpected token (character) was encountered, and the member @a byte indicates the error position. -json.exception.parse_error.102 | parse error at 14: missing or wrong low surrogate | JSON uses the `\uxxxx` format to describe Unicode characters. Code points above above 0xFFFF are split into two `\uxxxx` entries ("surrogate pairs"). This error indicates that the surrogate pair is incomplete or contains an invalid code point. -json.exception.parse_error.103 | parse error: code points above 0x10FFFF are invalid | Unicode supports code points up to 0x10FFFF. Code points above 0x10FFFF are invalid. -json.exception.parse_error.104 | parse error: JSON patch must be an array of objects | [RFC 6902](https://tools.ietf.org/html/rfc6902) requires a JSON Patch document to be a JSON document that represents an array of objects. -json.exception.parse_error.105 | parse error: operation must have string member 'op' | An operation of a JSON Patch document must contain exactly one "op" member, whose value indicates the operation to perform. Its value must be one of "add", "remove", "replace", "move", "copy", or "test"; other values are errors. -json.exception.parse_error.106 | parse error: array index '01' must not begin with '0' | An array index in a JSON Pointer ([RFC 6901](https://tools.ietf.org/html/rfc6901)) may be `0` or any number without a leading `0`. -json.exception.parse_error.107 | parse error: JSON pointer must be empty or begin with '/' - was: 'foo' | A JSON Pointer must be a Unicode string containing a sequence of zero or more reference tokens, each prefixed by a `/` character. -json.exception.parse_error.108 | parse error: escape character '~' must be followed with '0' or '1' | In a JSON Pointer, only `~0` and `~1` are valid escape sequences. -json.exception.parse_error.109 | parse error: array index 'one' is not a number | A JSON Pointer array index must be a number. -json.exception.parse_error.110 | parse error at 1: cannot read 2 bytes from vector | When parsing CBOR or MessagePack, the byte vector ends before the complete value has been read. -json.exception.parse_error.112 | parse error at 1: error reading CBOR; last byte: 0xF8 | Not all types of CBOR or MessagePack are supported. This exception occurs if an unsupported byte was read. -json.exception.parse_error.113 | parse error at 2: expected a CBOR string; last byte: 0x98 | While parsing a map key, a value that is not a string has been read. -json.exception.parse_error.114 | parse error: Unsupported BSON record type 0x0F | The parsing of the corresponding BSON record type is not implemented (yet). - -@note For an input with n bytes, 1 is the index of the first character and n+1 - is the index of the terminating null byte or the end of file. This also - holds true when reading a byte vector (CBOR or MessagePack). - -@liveexample{The following code shows how a `parse_error` exception can be -caught.,parse_error} - -@sa - @ref exception for the base class of the library exceptions -@sa - @ref invalid_iterator for exceptions indicating errors with iterators -@sa - @ref type_error for exceptions indicating executing a member function with - a wrong type -@sa - @ref out_of_range for exceptions indicating access out of the defined range -@sa - @ref other_error for exceptions indicating other library errors - -@since version 3.0.0 -*/ -class parse_error : public exception +template class Op, + class... Args> +struct detector { - public: - /*! - @brief create a parse error exception - @param[in] id_ the id of the exception - @param[in] pos the position where the error occurred (or with - chars_read_total=0 if the position cannot be - determined) - @param[in] what_arg the explanatory string - @return parse_error object - */ - static parse_error create(int id_, const position_t& pos, const std::string& what_arg) - { - std::string w = exception::name("parse_error", id_) + "parse error" + - position_string(pos) + ": " + what_arg; - return parse_error(id_, pos.chars_read_total, w.c_str()); - } + using value_t = std::false_type; + using type = Default; +}; - static parse_error create(int id_, std::size_t byte_, const std::string& what_arg) - { - std::string w = exception::name("parse_error", id_) + "parse error" + - (byte_ != 0 ? (" at byte " + std::to_string(byte_)) : "") + - ": " + what_arg; - return parse_error(id_, byte_, w.c_str()); - } +template class Op, class... Args> +struct detector>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op; +}; - /*! - @brief byte index of the parse error +template class Op, class... Args> +using is_detected = typename detector::value_t; - The byte index of the last read character in the input file. +template class Op, class... Args> +struct is_detected_lazy : is_detected { }; - @note For an input with n bytes, 1 is the index of the first character and - n+1 is the index of the terminating null byte or the end of file. - This also holds true when reading a byte vector (CBOR or MessagePack). - */ - const std::size_t byte; +template class Op, class... Args> +using detected_t = typename detector::type; - private: - parse_error(int id_, std::size_t byte_, const char* what_arg) - : exception(id_, what_arg), byte(byte_) {} +template class Op, class... Args> +using detected_or = detector; - static std::string position_string(const position_t& pos) - { - return " at line " + std::to_string(pos.lines_read + 1) + - ", column " + std::to_string(pos.chars_read_current_line); - } -}; +template class Op, class... Args> +using detected_or_t = typename detected_or::type; -/*! -@brief exception indicating errors with iterators - -This exception is thrown if iterators passed to a library function do not match -the expected semantics. - -Exceptions have ids 2xx. - -name / id | example message | description ------------------------------------ | --------------- | ------------------------- -json.exception.invalid_iterator.201 | iterators are not compatible | The iterators passed to constructor @ref basic_json(InputIT first, InputIT last) are not compatible, meaning they do not belong to the same container. Therefore, the range (@a first, @a last) is invalid. -json.exception.invalid_iterator.202 | iterator does not fit current value | In an erase or insert function, the passed iterator @a pos does not belong to the JSON value for which the function was called. It hence does not define a valid position for the deletion/insertion. -json.exception.invalid_iterator.203 | iterators do not fit current value | Either iterator passed to function @ref erase(IteratorType first, IteratorType last) does not belong to the JSON value from which values shall be erased. It hence does not define a valid range to delete values from. -json.exception.invalid_iterator.204 | iterators out of range | When an iterator range for a primitive type (number, boolean, or string) is passed to a constructor or an erase function, this range has to be exactly (@ref begin(), @ref end()), because this is the only way the single stored value is expressed. All other ranges are invalid. -json.exception.invalid_iterator.205 | iterator out of range | When an iterator for a primitive type (number, boolean, or string) is passed to an erase function, the iterator has to be the @ref begin() iterator, because it is the only way to address the stored value. All other iterators are invalid. -json.exception.invalid_iterator.206 | cannot construct with iterators from null | The iterators passed to constructor @ref basic_json(InputIT first, InputIT last) belong to a JSON null value and hence to not define a valid range. -json.exception.invalid_iterator.207 | cannot use key() for non-object iterators | The key() member function can only be used on iterators belonging to a JSON object, because other types do not have a concept of a key. -json.exception.invalid_iterator.208 | cannot use operator[] for object iterators | The operator[] to specify a concrete offset cannot be used on iterators belonging to a JSON object, because JSON objects are unordered. -json.exception.invalid_iterator.209 | cannot use offsets with object iterators | The offset operators (+, -, +=, -=) cannot be used on iterators belonging to a JSON object, because JSON objects are unordered. -json.exception.invalid_iterator.210 | iterators do not fit | The iterator range passed to the insert function are not compatible, meaning they do not belong to the same container. Therefore, the range (@a first, @a last) is invalid. -json.exception.invalid_iterator.211 | passed iterators may not belong to container | The iterator range passed to the insert function must not be a subrange of the container to insert to. -json.exception.invalid_iterator.212 | cannot compare iterators of different containers | When two iterators are compared, they must belong to the same container. -json.exception.invalid_iterator.213 | cannot compare order of object iterators | The order of object iterators cannot be compared, because JSON objects are unordered. -json.exception.invalid_iterator.214 | cannot get value | Cannot get value for iterator: Either the iterator belongs to a null value or it is an iterator to a primitive type (number, boolean, or string), but the iterator is different to @ref begin(). - -@liveexample{The following code shows how an `invalid_iterator` exception can be -caught.,invalid_iterator} - -@sa - @ref exception for the base class of the library exceptions -@sa - @ref parse_error for exceptions indicating a parse error -@sa - @ref type_error for exceptions indicating executing a member function with - a wrong type -@sa - @ref out_of_range for exceptions indicating access out of the defined range -@sa - @ref other_error for exceptions indicating other library errors - -@since version 3.0.0 -*/ -class invalid_iterator : public exception -{ - public: - static invalid_iterator create(int id_, const std::string& what_arg) - { - std::string w = exception::name("invalid_iterator", id_) + what_arg; - return invalid_iterator(id_, w.c_str()); - } +template class Op, class... Args> +using is_detected_exact = std::is_same>; - private: - invalid_iterator(int id_, const char* what_arg) - : exception(id_, what_arg) {} -}; +template class Op, class... Args> +using is_detected_convertible = + std::is_convertible, To>; -/*! -@brief exception indicating executing a member function with a wrong type - -This exception is thrown in case of a type error; that is, a library function is -executed on a JSON value whose type does not match the expected semantics. - -Exceptions have ids 3xx. - -name / id | example message | description ------------------------------ | --------------- | ------------------------- -json.exception.type_error.301 | cannot create object from initializer list | To create an object from an initializer list, the initializer list must consist only of a list of pairs whose first element is a string. When this constraint is violated, an array is created instead. -json.exception.type_error.302 | type must be object, but is array | During implicit or explicit value conversion, the JSON type must be compatible to the target type. For instance, a JSON string can only be converted into string types, but not into numbers or boolean types. -json.exception.type_error.303 | incompatible ReferenceType for get_ref, actual type is object | To retrieve a reference to a value stored in a @ref basic_json object with @ref get_ref, the type of the reference must match the value type. For instance, for a JSON array, the @a ReferenceType must be @ref array_t &. -json.exception.type_error.304 | cannot use at() with string | The @ref at() member functions can only be executed for certain JSON types. -json.exception.type_error.305 | cannot use operator[] with string | The @ref operator[] member functions can only be executed for certain JSON types. -json.exception.type_error.306 | cannot use value() with string | The @ref value() member functions can only be executed for certain JSON types. -json.exception.type_error.307 | cannot use erase() with string | The @ref erase() member functions can only be executed for certain JSON types. -json.exception.type_error.308 | cannot use push_back() with string | The @ref push_back() and @ref operator+= member functions can only be executed for certain JSON types. -json.exception.type_error.309 | cannot use insert() with | The @ref insert() member functions can only be executed for certain JSON types. -json.exception.type_error.310 | cannot use swap() with number | The @ref swap() member functions can only be executed for certain JSON types. -json.exception.type_error.311 | cannot use emplace_back() with string | The @ref emplace_back() member function can only be executed for certain JSON types. -json.exception.type_error.312 | cannot use update() with string | The @ref update() member functions can only be executed for certain JSON types. -json.exception.type_error.313 | invalid value to unflatten | The @ref unflatten function converts an object whose keys are JSON Pointers back into an arbitrary nested JSON value. The JSON Pointers must not overlap, because then the resulting value would not be well defined. -json.exception.type_error.314 | only objects can be unflattened | The @ref unflatten function only works for an object whose keys are JSON Pointers. -json.exception.type_error.315 | values in object must be primitive | The @ref unflatten function only works for an object whose keys are JSON Pointers and whose values are primitive. -json.exception.type_error.316 | invalid UTF-8 byte at index 10: 0x7E | The @ref dump function only works with UTF-8 encoded strings; that is, if you assign a `std::string` to a JSON value, make sure it is UTF-8 encoded. | -json.exception.type_error.317 | JSON value cannot be serialized to requested format | The dynamic type of the object cannot be represented in the requested serialization format (e.g. a raw `true` or `null` JSON object cannot be serialized to BSON) | - -@liveexample{The following code shows how a `type_error` exception can be -caught.,type_error} - -@sa - @ref exception for the base class of the library exceptions -@sa - @ref parse_error for exceptions indicating a parse error -@sa - @ref invalid_iterator for exceptions indicating errors with iterators -@sa - @ref out_of_range for exceptions indicating access out of the defined range -@sa - @ref other_error for exceptions indicating other library errors - -@since version 3.0.0 -*/ -class type_error : public exception -{ - public: - static type_error create(int id_, const std::string& what_arg) - { - std::string w = exception::name("type_error", id_) + what_arg; - return type_error(id_, w.c_str()); - } +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END - private: - type_error(int id_, const char* what_arg) : exception(id_, what_arg) {} -}; +// #include -/*! -@brief exception indicating access out of the defined range - -This exception is thrown in case a library function is called on an input -parameter that exceeds the expected range, for instance in case of array -indices or nonexisting object keys. - -Exceptions have ids 4xx. - -name / id | example message | description -------------------------------- | --------------- | ------------------------- -json.exception.out_of_range.401 | array index 3 is out of range | The provided array index @a i is larger than @a size-1. -json.exception.out_of_range.402 | array index '-' (3) is out of range | The special array index `-` in a JSON Pointer never describes a valid element of the array, but the index past the end. That is, it can only be used to add elements at this position, but not to read it. -json.exception.out_of_range.403 | key 'foo' not found | The provided key was not found in the JSON object. -json.exception.out_of_range.404 | unresolved reference token 'foo' | A reference token in a JSON Pointer could not be resolved. -json.exception.out_of_range.405 | JSON pointer has no parent | The JSON Patch operations 'remove' and 'add' can not be applied to the root element of the JSON value. -json.exception.out_of_range.406 | number overflow parsing '10E1000' | A parsed number could not be stored as without changing it to NaN or INF. -json.exception.out_of_range.407 | number overflow serializing '9223372036854775808' | UBJSON and BSON only support integer numbers up to 9223372036854775807. | -json.exception.out_of_range.408 | excessive array size: 8658170730974374167 | The size (following `#`) of an UBJSON array or object exceeds the maximal capacity. | -json.exception.out_of_range.409 | BSON key cannot contain code point U+0000 (at byte 2) | Key identifiers to be serialized to BSON cannot contain code point U+0000, since the key is stored as zero-terminated c-string | - -@liveexample{The following code shows how an `out_of_range` exception can be -caught.,out_of_range} - -@sa - @ref exception for the base class of the library exceptions -@sa - @ref parse_error for exceptions indicating a parse error -@sa - @ref invalid_iterator for exceptions indicating errors with iterators -@sa - @ref type_error for exceptions indicating executing a member function with - a wrong type -@sa - @ref other_error for exceptions indicating other library errors - -@since version 3.0.0 -*/ -class out_of_range : public exception -{ - public: - static out_of_range create(int id_, const std::string& what_arg) - { - std::string w = exception::name("out_of_range", id_) + what_arg; - return out_of_range(id_, w.c_str()); - } - private: - out_of_range(int id_, const char* what_arg) : exception(id_, what_arg) {} -}; +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-FileCopyrightText: 2016-2021 Evan Nemerson +// SPDX-License-Identifier: MIT -/*! -@brief exception indicating other library errors +/* Hedley - https://nemequ.github.io/hedley + * Created by Evan Nemerson + */ -This exception is thrown in case of errors that cannot be classified with the -other exception types. +#if !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < 15) +#if defined(JSON_HEDLEY_VERSION) + #undef JSON_HEDLEY_VERSION +#endif +#define JSON_HEDLEY_VERSION 15 -Exceptions have ids 5xx. +#if defined(JSON_HEDLEY_STRINGIFY_EX) + #undef JSON_HEDLEY_STRINGIFY_EX +#endif +#define JSON_HEDLEY_STRINGIFY_EX(x) #x -name / id | example message | description ------------------------------- | --------------- | ------------------------- -json.exception.other_error.501 | unsuccessful: {"op":"test","path":"/baz", "value":"bar"} | A JSON Patch operation 'test' failed. The unsuccessful operation is also printed. +#if defined(JSON_HEDLEY_STRINGIFY) + #undef JSON_HEDLEY_STRINGIFY +#endif +#define JSON_HEDLEY_STRINGIFY(x) JSON_HEDLEY_STRINGIFY_EX(x) -@sa - @ref exception for the base class of the library exceptions -@sa - @ref parse_error for exceptions indicating a parse error -@sa - @ref invalid_iterator for exceptions indicating errors with iterators -@sa - @ref type_error for exceptions indicating executing a member function with - a wrong type -@sa - @ref out_of_range for exceptions indicating access out of the defined range +#if defined(JSON_HEDLEY_CONCAT_EX) + #undef JSON_HEDLEY_CONCAT_EX +#endif +#define JSON_HEDLEY_CONCAT_EX(a,b) a##b -@liveexample{The following code shows how an `other_error` exception can be -caught.,other_error} +#if defined(JSON_HEDLEY_CONCAT) + #undef JSON_HEDLEY_CONCAT +#endif +#define JSON_HEDLEY_CONCAT(a,b) JSON_HEDLEY_CONCAT_EX(a,b) -@since version 3.0.0 -*/ -class other_error : public exception -{ - public: - static other_error create(int id_, const std::string& what_arg) - { - std::string w = exception::name("other_error", id_) + what_arg; - return other_error(id_, w.c_str()); - } +#if defined(JSON_HEDLEY_CONCAT3_EX) + #undef JSON_HEDLEY_CONCAT3_EX +#endif +#define JSON_HEDLEY_CONCAT3_EX(a,b,c) a##b##c - private: - other_error(int id_, const char* what_arg) : exception(id_, what_arg) {} -}; -} // namespace detail -} // namespace nlohmann +#if defined(JSON_HEDLEY_CONCAT3) + #undef JSON_HEDLEY_CONCAT3 +#endif +#define JSON_HEDLEY_CONCAT3(a,b,c) JSON_HEDLEY_CONCAT3_EX(a,b,c) -// #include +#if defined(JSON_HEDLEY_VERSION_ENCODE) + #undef JSON_HEDLEY_VERSION_ENCODE +#endif +#define JSON_HEDLEY_VERSION_ENCODE(major,minor,revision) (((major) * 1000000) + ((minor) * 1000) + (revision)) +#if defined(JSON_HEDLEY_VERSION_DECODE_MAJOR) + #undef JSON_HEDLEY_VERSION_DECODE_MAJOR +#endif +#define JSON_HEDLEY_VERSION_DECODE_MAJOR(version) ((version) / 1000000) -#include // pair +#if defined(JSON_HEDLEY_VERSION_DECODE_MINOR) + #undef JSON_HEDLEY_VERSION_DECODE_MINOR +#endif +#define JSON_HEDLEY_VERSION_DECODE_MINOR(version) (((version) % 1000000) / 1000) -// This file contains all internal macro definitions -// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them +#if defined(JSON_HEDLEY_VERSION_DECODE_REVISION) + #undef JSON_HEDLEY_VERSION_DECODE_REVISION +#endif +#define JSON_HEDLEY_VERSION_DECODE_REVISION(version) ((version) % 1000) -// exclude unsupported compilers -#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK) - #if defined(__clang__) - #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400 - #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers" - #endif - #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER)) - #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40800 - #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers" - #endif - #endif +#if defined(JSON_HEDLEY_GNUC_VERSION) + #undef JSON_HEDLEY_GNUC_VERSION +#endif +#if defined(__GNUC__) && defined(__GNUC_PATCHLEVEL__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#elif defined(__GNUC__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, 0) #endif -// disable float-equal warnings on GCC/clang -#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wfloat-equal" +#if defined(JSON_HEDLEY_GNUC_VERSION_CHECK) + #undef JSON_HEDLEY_GNUC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GNUC_VERSION) + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GNUC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (0) #endif -// disable documentation warnings on clang -#if defined(__clang__) - #pragma GCC diagnostic push - #pragma GCC diagnostic ignored "-Wdocumentation" +#if defined(JSON_HEDLEY_MSVC_VERSION) + #undef JSON_HEDLEY_MSVC_VERSION +#endif +#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 140000000) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 10000000, (_MSC_FULL_VER % 10000000) / 100000, (_MSC_FULL_VER % 100000) / 100) +#elif defined(_MSC_FULL_VER) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 1000000, (_MSC_FULL_VER % 1000000) / 10000, (_MSC_FULL_VER % 10000) / 10) +#elif defined(_MSC_VER) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_VER / 100, _MSC_VER % 100, 0) #endif -// allow for portable deprecation warnings -#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) - #define JSON_DEPRECATED __attribute__((deprecated)) -#elif defined(_MSC_VER) - #define JSON_DEPRECATED __declspec(deprecated) +#if defined(JSON_HEDLEY_MSVC_VERSION_CHECK) + #undef JSON_HEDLEY_MSVC_VERSION_CHECK +#endif +#if !defined(JSON_HEDLEY_MSVC_VERSION) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (0) +#elif defined(_MSC_VER) && (_MSC_VER >= 1400) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 10000000) + (minor * 100000) + (patch))) +#elif defined(_MSC_VER) && (_MSC_VER >= 1200) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 1000000) + (minor * 10000) + (patch))) #else - #define JSON_DEPRECATED + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_VER >= ((major * 100) + (minor))) #endif -// allow for portable nodiscard warnings -#if defined(__has_cpp_attribute) - #if __has_cpp_attribute(nodiscard) - #define JSON_NODISCARD [[nodiscard]] - #elif __has_cpp_attribute(gnu::warn_unused_result) - #define JSON_NODISCARD [[gnu::warn_unused_result]] - #else - #define JSON_NODISCARD - #endif +#if defined(JSON_HEDLEY_INTEL_VERSION) + #undef JSON_HEDLEY_INTEL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && !defined(__ICL) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, __INTEL_COMPILER_UPDATE) +#elif defined(__INTEL_COMPILER) && !defined(__ICL) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) +#endif + +#if defined(JSON_HEDLEY_INTEL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_INTEL_VERSION) + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) #else - #define JSON_NODISCARD + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (0) #endif -// allow to disable exceptions -#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) - #define JSON_THROW(exception) throw exception - #define JSON_TRY try - #define JSON_CATCH(exception) catch(exception) - #define JSON_INTERNAL_CATCH(exception) catch(exception) -#else - #include - #define JSON_THROW(exception) std::abort() - #define JSON_TRY if(true) - #define JSON_CATCH(exception) if(false) - #define JSON_INTERNAL_CATCH(exception) if(false) +#if defined(JSON_HEDLEY_INTEL_CL_VERSION) + #undef JSON_HEDLEY_INTEL_CL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && defined(__ICL) + #define JSON_HEDLEY_INTEL_CL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER, __INTEL_COMPILER_UPDATE, 0) #endif -// override exception macros -#if defined(JSON_THROW_USER) - #undef JSON_THROW - #define JSON_THROW JSON_THROW_USER +#if defined(JSON_HEDLEY_INTEL_CL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_CL_VERSION_CHECK #endif -#if defined(JSON_TRY_USER) - #undef JSON_TRY - #define JSON_TRY JSON_TRY_USER +#if defined(JSON_HEDLEY_INTEL_CL_VERSION) + #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_CL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (0) #endif -#if defined(JSON_CATCH_USER) - #undef JSON_CATCH - #define JSON_CATCH JSON_CATCH_USER - #undef JSON_INTERNAL_CATCH - #define JSON_INTERNAL_CATCH JSON_CATCH_USER + +#if defined(JSON_HEDLEY_PGI_VERSION) + #undef JSON_HEDLEY_PGI_VERSION #endif -#if defined(JSON_INTERNAL_CATCH_USER) - #undef JSON_INTERNAL_CATCH - #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER +#if defined(__PGI) && defined(__PGIC__) && defined(__PGIC_MINOR__) && defined(__PGIC_PATCHLEVEL__) + #define JSON_HEDLEY_PGI_VERSION JSON_HEDLEY_VERSION_ENCODE(__PGIC__, __PGIC_MINOR__, __PGIC_PATCHLEVEL__) #endif -// manual branch prediction -#if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) - #define JSON_LIKELY(x) __builtin_expect(x, 1) - #define JSON_UNLIKELY(x) __builtin_expect(x, 0) +#if defined(JSON_HEDLEY_PGI_VERSION_CHECK) + #undef JSON_HEDLEY_PGI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PGI_VERSION) + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PGI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) #else - #define JSON_LIKELY(x) x - #define JSON_UNLIKELY(x) x + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (0) #endif -// C++ language standard detection -#if (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 - #define JSON_HAS_CPP_17 - #define JSON_HAS_CPP_14 -#elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) - #define JSON_HAS_CPP_14 +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #undef JSON_HEDLEY_SUNPRO_VERSION +#endif +#if defined(__SUNPRO_C) && (__SUNPRO_C > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_C >> 16) & 0xf) * 10) + ((__SUNPRO_C >> 12) & 0xf), (((__SUNPRO_C >> 8) & 0xf) * 10) + ((__SUNPRO_C >> 4) & 0xf), (__SUNPRO_C & 0xf) * 10) +#elif defined(__SUNPRO_C) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_C >> 8) & 0xf, (__SUNPRO_C >> 4) & 0xf, (__SUNPRO_C) & 0xf) +#elif defined(__SUNPRO_CC) && (__SUNPRO_CC > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_CC >> 16) & 0xf) * 10) + ((__SUNPRO_CC >> 12) & 0xf), (((__SUNPRO_CC >> 8) & 0xf) * 10) + ((__SUNPRO_CC >> 4) & 0xf), (__SUNPRO_CC & 0xf) * 10) +#elif defined(__SUNPRO_CC) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_CC >> 8) & 0xf, (__SUNPRO_CC >> 4) & 0xf, (__SUNPRO_CC) & 0xf) #endif -/*! -@brief macro to briefly define a mapping between an enum and JSON -@def NLOHMANN_JSON_SERIALIZE_ENUM -@since version 3.4.0 -*/ -#define NLOHMANN_JSON_SERIALIZE_ENUM(ENUM_TYPE, ...) \ - template \ - inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \ - { \ - static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ - static const std::pair m[] = __VA_ARGS__; \ - auto it = std::find_if(std::begin(m), std::end(m), \ - [e](const std::pair& ej_pair) -> bool \ - { \ - return ej_pair.first == e; \ - }); \ - j = ((it != std::end(m)) ? it : std::begin(m))->second; \ - } \ - template \ - inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ - { \ - static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ - static const std::pair m[] = __VA_ARGS__; \ - auto it = std::find_if(std::begin(m), std::end(m), \ - [j](const std::pair& ej_pair) -> bool \ - { \ - return ej_pair.second == j; \ - }); \ - e = ((it != std::end(m)) ? it : std::begin(m))->first; \ - } +#if defined(JSON_HEDLEY_SUNPRO_VERSION_CHECK) + #undef JSON_HEDLEY_SUNPRO_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_SUNPRO_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (0) +#endif -// Ugly macros to avoid uglier copy-paste when specializing basic_json. They -// may be removed in the future once the class is split. +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION +#endif +#if defined(__EMSCRIPTEN__) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION JSON_HEDLEY_VERSION_ENCODE(__EMSCRIPTEN_major__, __EMSCRIPTEN_minor__, __EMSCRIPTEN_tiny__) +#endif -#define NLOHMANN_BASIC_JSON_TPL_DECLARATION \ - template class ObjectType, \ - template class ArrayType, \ - class StringType, class BooleanType, class NumberIntegerType, \ - class NumberUnsignedType, class NumberFloatType, \ - template class AllocatorType, \ - template class JSONSerializer> +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_EMSCRIPTEN_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (0) +#endif -#define NLOHMANN_BASIC_JSON_TPL \ - basic_json +#if defined(JSON_HEDLEY_ARM_VERSION) + #undef JSON_HEDLEY_ARM_VERSION +#endif +#if defined(__CC_ARM) && defined(__ARMCOMPILER_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCOMPILER_VERSION / 1000000, (__ARMCOMPILER_VERSION % 1000000) / 10000, (__ARMCOMPILER_VERSION % 10000) / 100) +#elif defined(__CC_ARM) && defined(__ARMCC_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCC_VERSION / 1000000, (__ARMCC_VERSION % 1000000) / 10000, (__ARMCC_VERSION % 10000) / 100) +#endif -// #include +#if defined(JSON_HEDLEY_ARM_VERSION_CHECK) + #undef JSON_HEDLEY_ARM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_ARM_VERSION) + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_ARM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (0) +#endif +#if defined(JSON_HEDLEY_IBM_VERSION) + #undef JSON_HEDLEY_IBM_VERSION +#endif +#if defined(__ibmxl__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ibmxl_version__, __ibmxl_release__, __ibmxl_modification__) +#elif defined(__xlC__) && defined(__xlC_ver__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, (__xlC_ver__ >> 8) & 0xff) +#elif defined(__xlC__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, 0) +#endif -#include // not -#include // size_t -#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type +#if defined(JSON_HEDLEY_IBM_VERSION_CHECK) + #undef JSON_HEDLEY_IBM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IBM_VERSION) + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IBM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (0) +#endif -namespace nlohmann -{ -namespace detail -{ -// alias templates to reduce boilerplate -template -using enable_if_t = typename std::enable_if::type; +#if defined(JSON_HEDLEY_TI_VERSION) + #undef JSON_HEDLEY_TI_VERSION +#endif +#if \ + defined(__TI_COMPILER_VERSION__) && \ + ( \ + defined(__TMS470__) || defined(__TI_ARM__) || \ + defined(__MSP430__) || \ + defined(__TMS320C2000__) \ + ) +#if (__TI_COMPILER_VERSION__ >= 16000000) + #define JSON_HEDLEY_TI_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif +#endif -template -using uncvref_t = typename std::remove_cv::type>::type; +#if defined(JSON_HEDLEY_TI_VERSION_CHECK) + #undef JSON_HEDLEY_TI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_VERSION) + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (0) +#endif -// implementation of C++14 index_sequence and affiliates -// source: https://stackoverflow.com/a/32223343 -template -struct index_sequence -{ - using type = index_sequence; - using value_type = std::size_t; - static constexpr std::size_t size() noexcept - { - return sizeof...(Ints); - } -}; +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #undef JSON_HEDLEY_TI_CL2000_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C2000__) + #define JSON_HEDLEY_TI_CL2000_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif -template -struct merge_and_renumber; +#if defined(JSON_HEDLEY_TI_CL2000_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL2000_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL2000_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (0) +#endif -template -struct merge_and_renumber, index_sequence> - : index_sequence < I1..., (sizeof...(I1) + I2)... > {}; +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #undef JSON_HEDLEY_TI_CL430_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__MSP430__) + #define JSON_HEDLEY_TI_CL430_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif -template -struct make_index_sequence - : merge_and_renumber < typename make_index_sequence < N / 2 >::type, - typename make_index_sequence < N - N / 2 >::type > {}; +#if defined(JSON_HEDLEY_TI_CL430_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL430_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL430_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (0) +#endif -template<> struct make_index_sequence<0> : index_sequence<> {}; -template<> struct make_index_sequence<1> : index_sequence<0> {}; +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #undef JSON_HEDLEY_TI_ARMCL_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && (defined(__TMS470__) || defined(__TI_ARM__)) + #define JSON_HEDLEY_TI_ARMCL_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif -template -using index_sequence_for = make_index_sequence; +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION_CHECK) + #undef JSON_HEDLEY_TI_ARMCL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_ARMCL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (0) +#endif -// dispatch utility (taken from ranges-v3) -template struct priority_tag : priority_tag < N - 1 > {}; -template<> struct priority_tag<0> {}; +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #undef JSON_HEDLEY_TI_CL6X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C6X__) + #define JSON_HEDLEY_TI_CL6X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif -// taken from ranges-v3 -template -struct static_const -{ - static constexpr T value{}; -}; +#if defined(JSON_HEDLEY_TI_CL6X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL6X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL6X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (0) +#endif -template -constexpr T static_const::value; -} // namespace detail -} // namespace nlohmann +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #undef JSON_HEDLEY_TI_CL7X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__C7000__) + #define JSON_HEDLEY_TI_CL7X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif -// #include +#if defined(JSON_HEDLEY_TI_CL7X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL7X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL7X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (0) +#endif +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #undef JSON_HEDLEY_TI_CLPRU_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__PRU__) + #define JSON_HEDLEY_TI_CLPRU_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif -#include // not -#include // numeric_limits -#include // false_type, is_constructible, is_integral, is_same, true_type -#include // declval +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CLPRU_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CLPRU_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (0) +#endif -// #include +#if defined(JSON_HEDLEY_CRAY_VERSION) + #undef JSON_HEDLEY_CRAY_VERSION +#endif +#if defined(_CRAYC) + #if defined(_RELEASE_PATCHLEVEL) + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, _RELEASE_PATCHLEVEL) + #else + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, 0) + #endif +#endif +#if defined(JSON_HEDLEY_CRAY_VERSION_CHECK) + #undef JSON_HEDLEY_CRAY_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_CRAY_VERSION) + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_CRAY_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (0) +#endif -#include // random_access_iterator_tag +#if defined(JSON_HEDLEY_IAR_VERSION) + #undef JSON_HEDLEY_IAR_VERSION +#endif +#if defined(__IAR_SYSTEMS_ICC__) + #if __VER__ > 1000 + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE((__VER__ / 1000000), ((__VER__ / 1000) % 1000), (__VER__ % 1000)) + #else + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE(__VER__ / 100, __VER__ % 100, 0) + #endif +#endif -// #include +#if defined(JSON_HEDLEY_IAR_VERSION_CHECK) + #undef JSON_HEDLEY_IAR_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IAR_VERSION) + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IAR_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (0) +#endif +#if defined(JSON_HEDLEY_TINYC_VERSION) + #undef JSON_HEDLEY_TINYC_VERSION +#endif +#if defined(__TINYC__) + #define JSON_HEDLEY_TINYC_VERSION JSON_HEDLEY_VERSION_ENCODE(__TINYC__ / 1000, (__TINYC__ / 100) % 10, __TINYC__ % 100) +#endif -namespace nlohmann -{ -namespace detail -{ -template struct make_void -{ - using type = void; -}; -template using void_t = typename make_void::type; -} // namespace detail -} // namespace nlohmann +#if defined(JSON_HEDLEY_TINYC_VERSION_CHECK) + #undef JSON_HEDLEY_TINYC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TINYC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (0) +#endif -// #include +#if defined(JSON_HEDLEY_DMC_VERSION) + #undef JSON_HEDLEY_DMC_VERSION +#endif +#if defined(__DMC__) + #define JSON_HEDLEY_DMC_VERSION JSON_HEDLEY_VERSION_ENCODE(__DMC__ >> 8, (__DMC__ >> 4) & 0xf, __DMC__ & 0xf) +#endif +#if defined(JSON_HEDLEY_DMC_VERSION_CHECK) + #undef JSON_HEDLEY_DMC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_DMC_VERSION) + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_DMC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (0) +#endif -namespace nlohmann -{ -namespace detail -{ -template -struct iterator_types {}; +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #undef JSON_HEDLEY_COMPCERT_VERSION +#endif +#if defined(__COMPCERT_VERSION__) + #define JSON_HEDLEY_COMPCERT_VERSION JSON_HEDLEY_VERSION_ENCODE(__COMPCERT_VERSION__ / 10000, (__COMPCERT_VERSION__ / 100) % 100, __COMPCERT_VERSION__ % 100) +#endif -template -struct iterator_types < - It, - void_t> -{ - using difference_type = typename It::difference_type; - using value_type = typename It::value_type; - using pointer = typename It::pointer; - using reference = typename It::reference; - using iterator_category = typename It::iterator_category; -}; +#if defined(JSON_HEDLEY_COMPCERT_VERSION_CHECK) + #undef JSON_HEDLEY_COMPCERT_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_COMPCERT_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (0) +#endif -// This is required as some compilers implement std::iterator_traits in a way that -// doesn't work with SFINAE. See https://github.com/nlohmann/json/issues/1341. -template -struct iterator_traits -{ -}; +#if defined(JSON_HEDLEY_PELLES_VERSION) + #undef JSON_HEDLEY_PELLES_VERSION +#endif +#if defined(__POCC__) + #define JSON_HEDLEY_PELLES_VERSION JSON_HEDLEY_VERSION_ENCODE(__POCC__ / 100, __POCC__ % 100, 0) +#endif -template -struct iterator_traits < T, enable_if_t < !std::is_pointer::value >> - : iterator_types -{ -}; +#if defined(JSON_HEDLEY_PELLES_VERSION_CHECK) + #undef JSON_HEDLEY_PELLES_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PELLES_VERSION) + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PELLES_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (0) +#endif -template -struct iterator_traits::value>> -{ - using iterator_category = std::random_access_iterator_tag; - using value_type = T; - using difference_type = ptrdiff_t; - using pointer = T*; - using reference = T&; -}; -} // namespace detail -} // namespace nlohmann +#if defined(JSON_HEDLEY_MCST_LCC_VERSION) + #undef JSON_HEDLEY_MCST_LCC_VERSION +#endif +#if defined(__LCC__) && defined(__LCC_MINOR__) + #define JSON_HEDLEY_MCST_LCC_VERSION JSON_HEDLEY_VERSION_ENCODE(__LCC__ / 100, __LCC__ % 100, __LCC_MINOR__) +#endif -// #include +#if defined(JSON_HEDLEY_MCST_LCC_VERSION_CHECK) + #undef JSON_HEDLEY_MCST_LCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_MCST_LCC_VERSION) + #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_MCST_LCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (0) +#endif -// #include +#if defined(JSON_HEDLEY_GCC_VERSION) + #undef JSON_HEDLEY_GCC_VERSION +#endif +#if \ + defined(JSON_HEDLEY_GNUC_VERSION) && \ + !defined(__clang__) && \ + !defined(JSON_HEDLEY_INTEL_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_ARM_VERSION) && \ + !defined(JSON_HEDLEY_CRAY_VERSION) && \ + !defined(JSON_HEDLEY_TI_VERSION) && \ + !defined(JSON_HEDLEY_TI_ARMCL_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL430_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL2000_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL6X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL7X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CLPRU_VERSION) && \ + !defined(__COMPCERT__) && \ + !defined(JSON_HEDLEY_MCST_LCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION JSON_HEDLEY_GNUC_VERSION +#endif -// #include +#if defined(JSON_HEDLEY_GCC_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (0) +#endif +#if defined(JSON_HEDLEY_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_ATTRIBUTE +#endif +#if \ + defined(__has_attribute) && \ + ( \ + (!defined(JSON_HEDLEY_IAR_VERSION) || JSON_HEDLEY_IAR_VERSION_CHECK(8,5,9)) \ + ) +# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) __has_attribute(attribute) +#else +# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) (0) +#endif -#include +#if defined(JSON_HEDLEY_GNUC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif -// #include +#if defined(JSON_HEDLEY_GCC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE +#endif +#if \ + defined(__has_cpp_attribute) && \ + defined(__cplusplus) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) (0) +#endif -// http://en.cppreference.com/w/cpp/experimental/is_detected -namespace nlohmann -{ -namespace detail -{ -struct nonesuch -{ - nonesuch() = delete; - ~nonesuch() = delete; - nonesuch(nonesuch const&) = delete; - nonesuch(nonesuch const&&) = delete; - void operator=(nonesuch const&) = delete; - void operator=(nonesuch&&) = delete; -}; +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS +#endif +#if !defined(__cplusplus) || !defined(__has_cpp_attribute) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#elif \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) && \ + (!defined(JSON_HEDLEY_MSVC_VERSION) || JSON_HEDLEY_MSVC_VERSION_CHECK(19,20,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(ns::attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#endif -template class Op, - class... Args> -struct detector -{ - using value_t = std::false_type; - using type = Default; -}; +#if defined(JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif -template class Op, class... Args> -struct detector>, Op, Args...> -{ - using value_t = std::true_type; - using type = Op; -}; +#if defined(JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif -template