diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index ed8737899..337d75115 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -12,7 +12,7 @@ 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 }}, dns-sd mode: ${{ matrix.dns_sd_mode}})' + 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 @@ -22,8 +22,15 @@ jobs: 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: macos-11 + enable_authorization: false + - os: windows-2019 + enable_authorization: false + - os: ubuntu-20.04 + enable_authorization: false - os: macos-11 install_mdns: true - os: windows-2019 @@ -38,17 +45,32 @@ jobs: - os: ubuntu-20.04 install_mdns: true dns_sd_mode: unicast + enable_authorization: true include: - - os: windows-2019 + - os: windows-2022 + install_mdns: false + use_conan: 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 dns_sd_mode: multicast + enable_authorization: true + - os: ubuntu-22.04 + install_mdns: false + use_conan: true + force_cpprest_asio: false + dns_sd_mode: multicast + enable_authorization: false steps: - uses: actions/checkout@v3 @@ -56,16 +78,22 @@ jobs: - 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_${{ matrix.dns_sd_mode }}" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_mdns_${{ matrix.dns_sd_mode }}_$authorization_mode" >> $GITHUB_ENV else - echo "BUILD_NAME=${{ matrix.os }}_avahi_${{ matrix.dns_sd_mode }}" >> $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 @@ -93,12 +121,16 @@ jobs: - name: install conan if: matrix.use_conan == true run: | - pip install conan~=1.47 - conan config set general.revisions_enabled=1 + pip install conan~=2.4.1 - - name: install cmake + - name: 'ubuntu-14.04: install cmake' + if: matrix.os == 'ubuntu-14.04' uses: lukka/get-cmake@v3.24.2 + - name: install cmake + if: matrix.os != 'ubuntu-14.04' + uses: lukka/get-cmake@v3.28.3 + - name: setup bash path working-directory: ${{ env.GITHUB_WORKSPACE }} shell: bash @@ -111,7 +143,7 @@ jobs: 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 @@ -234,20 +266,18 @@ jobs: libssl-dev cd ${{ env.RUNNER_WORKSPACE }} - git clone --recurse-submodules --branch v2.10.18 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 }} -DNMOS_CPP_USE_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 @@ -264,19 +294,37 @@ jobs: 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 + + - name: setup developer command prompt for Microsoft Visual C++ + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 - - uses: ilammy/msvc-dev-cmd@v1 - name: build 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 -DCMAKE_INSTALL_PREFIX="${{ env.RUNNER_WORKSPACE }}/install" ${{ 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: | @@ -313,10 +361,10 @@ jobs: cmakeAppendedArgs: '-GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_FIND_PACKAGE_PREFER_CONFIG="1" - -DCMAKE_MODULE_PATH="${{ env.CMAKE_WORKSPACE }}/build" + -DCMAKE_MODULE_PATH="${{ env.CMAKE_WORKSPACE }}/build/conan" -DCMAKE_PREFIX_PATH="${{ env.CMAKE_WORKSPACE }}/install" - -DCMAKE_INSTALL_PREFIX="${{ env.CMAKE_WORKSPACE }}/build" - ${{ env.CMAKE_EXTRA_ARGS }}' + -DCMAKE_INSTALL_PREFIX="${{ env.CMAKE_WORKSPACE }}/build/conan" + ${{ env.CMAKE_COMPILER_ARGS }}' - name: install test log run: | @@ -344,10 +392,23 @@ jobs: 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 @@ -386,20 +447,21 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; 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}" + # 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 [[ "${{ runner.os }}" == "macOS" ]]; then @@ -534,7 +596,7 @@ jobs: build_and_test_ubuntu_14: - 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}})' + 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: ubuntu-20.04 container: image: ubuntu:14.04 @@ -546,6 +608,7 @@ jobs: use_conan: [false] force_cpprest_asio: [false] dns_sd_mode: [multicast] + enable_authorization: [true] steps: - uses: actions/checkout@v3 @@ -579,13 +642,25 @@ jobs: apt-get --allow-unauthenticated install -y curl g++ git make patch zlib1g-dev libssl-dev bsdmainutils dnsutils unzip # ubuntu-14.04 ca-certificates are out of date git config --global http.sslVerify false - curl -sS https://www.python.org/ftp/python/3.6.9/Python-3.6.9.tar.xz | tar -xJ - cd Python-3.6.9 - ./configure + # build and install openssl + curl -OsSk https://www.openssl.org/source/openssl-1.1.1v.tar.gz + tar xzf openssl-1.1.1v.tar.gz + cd openssl-1.1.1v + ./config --prefix=/usr/local/custom-openssl --libdir=lib --openssldir=/etc/ssl + make -j1 depend + make -j8 + make install_sw + cd .. + # install ffi.h, which is required for python build + apt install libffi-dev + # build and install python + curl -sSk https://www.python.org/ftp/python/3.11.5/Python-3.11.5.tar.xz | tar -xJ + cd Python-3.11.5 + ./configure -C --with-openssl=/usr/local/custom-openssl --with-openssl-rpath=auto make -j8 make install - update-alternatives --install /usr/bin/python3 python3 /usr/local/bin/python3.6 3 - ln -s /usr/local/bin/python3.6 /usr/bin/python + update-alternatives --install /usr/bin/python3 python3 /usr/local/bin/python3.11 3 + ln -s /usr/local/bin/python3.11 /usr/bin/python curl -sS https://bootstrap.pypa.io/pip/3.6/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 @@ -602,12 +677,16 @@ jobs: - name: install conan if: matrix.use_conan == true run: | - pip install conan~=1.47 - conan config set general.revisions_enabled=1 + pip install conan~=2.4.1 - - name: install cmake + - name: 'ubuntu-14.04: install cmake' + if: matrix.os == 'ubuntu-14.04' uses: lukka/get-cmake@v3.24.2 + - name: install cmake + if: matrix.os != 'ubuntu-14.04' + uses: lukka/get-cmake@v3.28.3 + - name: setup bash path working-directory: ${{ env.GITHUB_WORKSPACE }} shell: bash @@ -620,7 +699,7 @@ jobs: 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 @@ -743,20 +822,18 @@ jobs: libssl-dev cd ${{ env.RUNNER_WORKSPACE }} - git clone --recurse-submodules --branch v2.10.18 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 }} -DNMOS_CPP_USE_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 @@ -773,19 +850,37 @@ jobs: 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 + + - name: setup developer command prompt for Microsoft Visual C++ + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 - - uses: ilammy/msvc-dev-cmd@v1 - name: build 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 -DCMAKE_INSTALL_PREFIX="${{ env.RUNNER_WORKSPACE }}/install" ${{ 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: | @@ -822,10 +917,10 @@ jobs: cmakeAppendedArgs: '-GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_FIND_PACKAGE_PREFER_CONFIG="1" - -DCMAKE_MODULE_PATH="${{ env.CMAKE_WORKSPACE }}/build" + -DCMAKE_MODULE_PATH="${{ env.CMAKE_WORKSPACE }}/build/conan" -DCMAKE_PREFIX_PATH="${{ env.CMAKE_WORKSPACE }}/install" - -DCMAKE_INSTALL_PREFIX="${{ env.CMAKE_WORKSPACE }}/build" - ${{ env.CMAKE_EXTRA_ARGS }}' + -DCMAKE_INSTALL_PREFIX="${{ env.CMAKE_WORKSPACE }}/build/conan" + ${{ env.CMAKE_COMPILER_ARGS }}' - name: install test log run: | @@ -853,10 +948,23 @@ jobs: 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 @@ -895,20 +1003,21 @@ jobs: pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; 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}" + # 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 [[ "${{ runner.os }}" == "macOS" ]]; then @@ -1064,7 +1173,7 @@ jobs: - name: make badges run: | # combine badges from all builds, exclude macos-11 - ${{ github.workspace }}/Sandbox/make_badges.sh ${{ github.workspace }} ${{ runner.workspace }}/artifacts macos-11 + ${{ github.workspace }}/Sandbox/make_badges.sh ${{ github.workspace }} ${{ runner.workspace }}/artifacts macos-11_auth macos-11_noauth # force push to github onto an orphan 'badges' branch cd ${{ github.workspace }} @@ -1075,4 +1184,4 @@ jobs: git config --global user.name 'test-results-uploader' git config --global user.email 'test-results-uploader@nmos-cpp.iam.gserviceaccount.com' git commit -qm "Badges for README at ${{ env.GITHUB_COMMIT }}" - git push -f `git remote` badges-${{ env.GITHUB_COMMIT }}:badges + git push -f `git remote` badges-${{ env.GITHUB_COMMIT }}:badges \ No newline at end of file diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index cca0fa18d..89396a152 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -19,11 +19,24 @@ 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/v3.0.7.tar.gz -s | tar -xvzf - --strip-components=1 > /dev/null @@ -61,20 +74,21 @@ pip install -r utilities/run-test-suites/gsheetsImport/requirements.txt if [[ "${{ runner.os }}" == "Windows" ]]; 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}" + # 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 [[ "${{ runner.os }}" == "macOS" ]]; then diff --git a/.github/workflows/src/build-setup.yml b/.github/workflows/src/build-setup.yml index 2559b3092..525d9554d 100644 --- a/.github/workflows/src/build-setup.yml +++ b/.github/workflows/src/build-setup.yml @@ -1,12 +1,16 @@ - name: install conan if: matrix.use_conan == true run: | - pip install conan~=1.47 - conan config set general.revisions_enabled=1 + pip install conan~=2.4.1 -- name: install cmake +- name: 'ubuntu-14.04: install cmake' + if: matrix.os == 'ubuntu-14.04' uses: lukka/get-cmake@v3.24.2 +- name: install cmake + if: matrix.os != 'ubuntu-14.04' + uses: lukka/get-cmake@v3.28.3 + - name: setup bash path working-directory: ${{ env.GITHUB_WORKSPACE }} shell: bash @@ -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 @@ -142,20 +146,18 @@ libssl-dev cd ${{ env.RUNNER_WORKSPACE }} - git clone --recurse-submodules --branch v2.10.18 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 }} -DNMOS_CPP_USE_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 @@ -172,7 +174,17 @@ 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 0b5663ff3..424273970 100644 --- a/.github/workflows/src/build-test.yml +++ b/.github/workflows/src/build-test.yml @@ -12,7 +12,7 @@ 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 }}, dns-sd mode: ${{ matrix.dns_sd_mode}})' + 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 @@ -22,8 +22,15 @@ jobs: 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: macos-11 + enable_authorization: false + - os: windows-2019 + enable_authorization: false + - os: ubuntu-20.04 + enable_authorization: false - os: macos-11 install_mdns: true - os: windows-2019 @@ -38,17 +45,32 @@ jobs: - os: ubuntu-20.04 install_mdns: true dns_sd_mode: unicast + enable_authorization: true include: - - os: windows-2019 + - os: windows-2022 install_mdns: false use_conan: 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 + dns_sd_mode: multicast + enable_authorization: true - os: ubuntu-22.04 install_mdns: false use_conan: true force_cpprest_asio: false dns_sd_mode: multicast + enable_authorization: false steps: - uses: actions/checkout@v3 @@ -56,16 +78,22 @@ jobs: - 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_${{ matrix.dns_sd_mode }}" >> $GITHUB_ENV + echo "BUILD_NAME=${{ matrix.os }}_mdns_${{ matrix.dns_sd_mode }}_$authorization_mode" >> $GITHUB_ENV else - echo "BUILD_NAME=${{ matrix.os }}_avahi_${{ matrix.dns_sd_mode }}" >> $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 @@ -84,7 +112,7 @@ jobs: @import build-and-test build_and_test_ubuntu_14: - 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}})' + 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: ubuntu-20.04 container: image: ubuntu:14.04 @@ -96,6 +124,7 @@ jobs: use_conan: [false] force_cpprest_asio: [false] dns_sd_mode: [multicast] + enable_authorization: [true] steps: - uses: actions/checkout@v3 @@ -129,13 +158,25 @@ jobs: apt-get --allow-unauthenticated install -y curl g++ git make patch zlib1g-dev libssl-dev bsdmainutils dnsutils unzip # ubuntu-14.04 ca-certificates are out of date git config --global http.sslVerify false - curl -sS https://www.python.org/ftp/python/3.6.9/Python-3.6.9.tar.xz | tar -xJ - cd Python-3.6.9 - ./configure + # build and install openssl + curl -OsSk https://www.openssl.org/source/openssl-1.1.1v.tar.gz + tar xzf openssl-1.1.1v.tar.gz + cd openssl-1.1.1v + ./config --prefix=/usr/local/custom-openssl --libdir=lib --openssldir=/etc/ssl + make -j1 depend + make -j8 + make install_sw + cd .. + # install ffi.h, which is required for python build + apt install libffi-dev + # build and install python + curl -sSk https://www.python.org/ftp/python/3.11.5/Python-3.11.5.tar.xz | tar -xJ + cd Python-3.11.5 + ./configure -C --with-openssl=/usr/local/custom-openssl --with-openssl-rpath=auto make -j8 make install - update-alternatives --install /usr/bin/python3 python3 /usr/local/bin/python3.6 3 - ln -s /usr/local/bin/python3.6 /usr/bin/python + update-alternatives --install /usr/bin/python3 python3 /usr/local/bin/python3.11 3 + ln -s /usr/local/bin/python3.11 /usr/bin/python curl -sS https://bootstrap.pypa.io/pip/3.6/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 @@ -164,7 +205,7 @@ jobs: - name: make badges run: | # combine badges from all builds, exclude macos-11 - ${{ github.workspace }}/Sandbox/make_badges.sh ${{ github.workspace }} ${{ runner.workspace }}/artifacts macos-11 + ${{ github.workspace }}/Sandbox/make_badges.sh ${{ github.workspace }} ${{ runner.workspace }}/artifacts macos-11_auth macos-11_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 bbf5856f1..80b669d62 100644 --- a/.github/workflows/src/build.yml +++ b/.github/workflows/src/build.yml @@ -1,8 +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@v3.4 with: cmakeListsOrSettingsJson: CMakeListsTxtAdvanced cmakeListsTxtPath: '${{ env.GITHUB_WORKSPACE }}/Development/CMakeLists.txt' buildDirectory: '${{ env.RUNNER_WORKSPACE }}/build/' - cmakeAppendedArgs: '-GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="${{ env.RUNNER_WORKSPACE }}/install" ${{ 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/install-test.yml b/.github/workflows/src/install-test.yml index daf0be850..fb8dde85b 100644 --- a/.github/workflows/src/install-test.yml +++ b/.github/workflows/src/install-test.yml @@ -13,10 +13,10 @@ cmakeAppendedArgs: '-GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_FIND_PACKAGE_PREFER_CONFIG="1" - -DCMAKE_MODULE_PATH="${{ env.CMAKE_WORKSPACE }}/build" + -DCMAKE_MODULE_PATH="${{ env.CMAKE_WORKSPACE }}/build/conan" -DCMAKE_PREFIX_PATH="${{ env.CMAKE_WORKSPACE }}/install" - -DCMAKE_INSTALL_PREFIX="${{ env.CMAKE_WORKSPACE }}/build" - ${{ env.CMAKE_EXTRA_ARGS }}' + -DCMAKE_INSTALL_PREFIX="${{ env.CMAKE_WORKSPACE }}/build/conan" + ${{ env.CMAKE_COMPILER_ARGS }}' - name: install test log run: | diff --git a/Development/CMakeLists.txt b/Development/CMakeLists.txt index bb4184340..a221b8df0 100644 --- a/Development/CMakeLists.txt +++ b/Development/CMakeLists.txt @@ -1,4 +1,12 @@ -cmake_minimum_required(VERSION 3.17 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) diff --git a/Development/README.md b/Development/README.md index e98f03c4e..c4464e9ec 100644 --- a/Development/README.md +++ b/Development/README.md @@ -12,6 +12,8 @@ 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) diff --git a/Development/boost/asio/ssl/use_tmp_ecdh.hpp b/Development/boost/asio/ssl/use_tmp_ecdh.hpp index a9e2e8a09..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(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(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(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 diff --git a/Development/cmake/NmosCppCommon.cmake b/Development/cmake/NmosCppCommon.cmake index 584f6908c..b9518b7e9 100644 --- a/Development/cmake/NmosCppCommon.cmake +++ b/Development/cmake/NmosCppCommon.cmake @@ -1,9 +1,6 @@ -set(NMOS_CPP_USE_CONAN ON CACHE BOOL "Use Conan to acquire dependencies") -mark_as_advanced(FORCE NMOS_CPP_USE_CONAN) - -if(NMOS_CPP_USE_CONAN) - include(cmake/NmosCppConan.cmake) -endif() +# 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) include(GNUInstallDirs) diff --git a/Development/cmake/NmosCppConan.cmake b/Development/cmake/NmosCppConan.cmake deleted file mode 100644 index e88f61a2a..000000000 --- a/Development/cmake/NmosCppConan.cmake +++ /dev/null @@ -1,56 +0,0 @@ -# the Conan recipe should use its own wrapper CMakeLists.txt to call include(conanbuildinfo.cmake) and conan_basic_setup() -# see https://github.com/conan-io/cmake-conan#creating-packages -if(CONAN_EXPORTED) - return() -endif() - -if(NOT EXISTS "${CMAKE_CURRENT_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/0.18.1/conan.cmake" - "${CMAKE_CURRENT_BINARY_DIR}/conan.cmake") -endif() - -include(${CMAKE_CURRENT_BINARY_DIR}/conan.cmake) - -# checking the Conan version produces a more helpful message than the confusing errors -# that are reported when some dependency's recipe uses new features; Conan moves fast! -# it would be nice to output a message if its a more recent version than tested, like: -# "Found Conan version 99.99 that is higher than the current tested version: " ${CONAN_VERSION_CUR}) -set(CONAN_VERSION_MIN "1.47.0") -set(CONAN_VERSION_CUR "1.59.0") -conan_check(VERSION ${CONAN_VERSION_MIN} REQUIRED) - -set(NMOS_CPP_CONAN_BUILD_LIBS "missing" CACHE STRING "Semicolon separated list of libraries to build rather than download") -mark_as_advanced(FORCE NMOS_CPP_CONAN_BUILD_LIBS) -set(NMOS_CPP_CONAN_OPTIONS "" CACHE STRING "Semicolon separated list of Conan options") -mark_as_advanced(FORCE NMOS_CPP_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}) - - # tell find_package() to try "Config" mode before "Module" mode if no mode was specified - # so a FindXXXX.cmake file in CMake's default modules directory isn't used instead of - # the Config.cmake generated by Conan in the current binary directory - # see https://docs.conan.io/en/1.39/integrations/build_system/cmake/cmake_find_package_multi_generator.html - set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE) - - # ensure Config.cmake config files generated by Conan can be found - list(APPEND CMAKE_PREFIX_PATH ${CMAKE_CURRENT_BINARY_DIR}) -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}) - - # ensure Find.cmake module files generated by Conan can be found - list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_BINARY_DIR}) -endif() diff --git a/Development/cmake/NmosCppDependencies.cmake b/Development/cmake/NmosCppDependencies.cmake index cf3a939c6..56e36dbe1 100644 --- a/Development/cmake/NmosCppDependencies.cmake +++ b/Development/cmake/NmosCppDependencies.cmake @@ -1,7 +1,7 @@ # Boost set(BOOST_VERSION_MIN "1.54.0") -set(BOOST_VERSION_CUR "1.80.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 @@ -95,7 +95,7 @@ add_library(nmos-cpp::Boost ALIAS Boost) # 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.18") +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}) @@ -187,9 +187,10 @@ add_library(nmos-cpp::OpenSSL ALIAS OpenSSL) # json schema validator library -if(NMOS_CPP_USE_CONAN) +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.2.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}) @@ -202,7 +203,7 @@ if(NMOS_CPP_USE_CONAN) endif() set(NLOHMANN_JSON_VERSION_MIN "3.6.0") - set(NLOHMANN_JSON_VERSION_CUR "3.11.2") + 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}) @@ -456,3 +457,64 @@ if(NMOS_CPP_BUILD_LLDP) 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.") + + set(JWT_SOURCES + ) + + set(JWT_HEADERS + third_party/jwt-cpp/base.h + third_party/jwt-cpp/jwt.h + third_party/jwt-cpp/traits/nlohmann-json/defaults.h + third_party/jwt-cpp/traits/nlohmann-json/traits.h + ) + + # hm, header-only so should be INTERFACE library? + add_library( + jwt-cpp STATIC + ${JWT_SOURCES} + ${JWT_HEADERS} + ) + + source_group("Source Files" FILES ${JWT_SOURCES}) + source_group("Header Files" FILES ${JWT_HEADERS}) + + target_link_libraries( + jwt-cpp PRIVATE + nmos-cpp::compile-settings + ) + target_include_directories(jwt-cpp PUBLIC + $ + $ + ) +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/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index c255d6c15..3ebf4939b 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -686,6 +686,154 @@ target_include_directories(nmos_is09_schemas PUBLIC list(APPEND NMOS_CPP_TARGETS nmos_is09_schemas) add_library(nmos-cpp::nmos_is09_schemas ALIAS nmos_is09_schemas) +# nmos_is10_schemas library + +set(NMOS_IS10_SCHEMAS_HEADERS + nmos/is10_schemas/is10_schemas.h + ) + +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( + nmos_is10_schemas STATIC + ${NMOS_IS10_SCHEMAS_HEADERS} + ${NMOS_IS10_V1_0_SCHEMAS_SOURCES} + ) + +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( + 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) + +# 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_is13_schemas library set(NMOS_IS13_SCHEMAS_HEADERS @@ -787,8 +935,12 @@ if(MSVC) endif() set(NMOS_CPP_CPPREST_HEADERS + 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 @@ -799,6 +951,9 @@ set(NMOS_CPP_CPPREST_HEADERS 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 @@ -811,12 +966,25 @@ set(NMOS_CPP_CPPREST_DETAILS_HEADERS cpprest/details/system_error.h ) +set(NMOS_CPP_JWK_HEADERS + jwk/algorithm.h + jwk/public_key_use.h + ) + set(NMOS_CPP_NMOS_SOURCES nmos/activation_utils.cpp nmos/admin_ui.cpp nmos/annotation_api.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 @@ -829,6 +997,13 @@ set(NMOS_CPP_NMOS_SOURCES 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 @@ -840,6 +1015,10 @@ set(NMOS_CPP_NMOS_SOURCES 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 @@ -867,6 +1046,7 @@ set(NMOS_CPP_NMOS_SOURCES 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 @@ -875,6 +1055,7 @@ set(NMOS_CPP_NMOS_SOURCES nmos/system_api.cpp nmos/system_resources.cpp nmos/video_jxsv.cpp + nmos/ws_api_utils.cpp ) set(NMOS_CPP_NMOS_HEADERS nmos/activation_mode.h @@ -885,6 +1066,14 @@ set(NMOS_CPP_NMOS_HEADERS 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 @@ -902,6 +1091,16 @@ set(NMOS_CPP_NMOS_HEADERS 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 @@ -920,9 +1119,16 @@ set(NMOS_CPP_NMOS_HEADERS nmos/is07_versions.h nmos/is08_versions.h nmos/is09_versions.h + nmos/is10_versions.h + nmos/is12_versions.h nmos/is13_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 @@ -961,6 +1167,8 @@ set(NMOS_CPP_NMOS_HEADERS 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 @@ -982,6 +1190,7 @@ set(NMOS_CPP_NMOS_HEADERS nmos/video_jxsv.h nmos/vpid_code.h nmos/websockets.h + nmos/ws_api_utils.h ) set(NMOS_CPP_PPLX_SOURCES @@ -1023,6 +1232,7 @@ add_library( ${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} @@ -1043,6 +1253,7 @@ 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}) @@ -1059,12 +1270,15 @@ target_link_libraries( 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::nmos_is13_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 @@ -1100,6 +1314,7 @@ target_include_directories(nmos-cpp PUBLIC 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) diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index f33f98e40..10de269e9 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -14,6 +14,7 @@ set(NMOS_CPP_TEST_BST_TEST_HEADERS set(NMOS_CPP_TEST_CPPREST_TEST_SOURCES 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 @@ -43,16 +44,20 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/api_utils_test.cpp nmos/test/capabilities_test.cpp nmos/test/channels_test.cpp + nmos/test/control_protocol_test.cpp nmos/test/did_sdid_test.cpp nmos/test/event_type_test.cpp nmos/test/json_validator_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/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 @@ -122,6 +127,7 @@ target_link_libraries( nmos-cpp::mdns nmos-cpp::cpprestsdk nmos-cpp::Boost + nmos-cpp::jwt-cpp ) if(NMOS_CPP_BUILD_LLDP) target_link_libraries( diff --git a/Development/cmake/nmos-cpp-config.cmake.in b/Development/cmake/nmos-cpp-config.cmake.in index 13da90ff3..9f49527bc 100644 --- a/Development/cmake/nmos-cpp-config.cmake.in +++ b/Development/cmake/nmos-cpp-config.cmake.in @@ -9,9 +9,12 @@ include(CMakeFindDependencyMacro) find_dependency(Boost COMPONENTS @FIND_BOOST_COMPONENTS@) find_dependency(cpprestsdk) find_dependency(OpenSSL) -if(@NMOS_CPP_USE_CONAN@) +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) diff --git a/Development/conanfile.txt b/Development/conanfile.txt index be2f189c3..aa55298b1 100644 --- a/Development/conanfile.txt +++ b/Development/conanfile.txt @@ -1,11 +1,12 @@ [requires] -boost/1.80.0 -cpprestsdk/2.10.18 +boost/1.83.0 +cpprestsdk/2.10.19 websocketpp/0.8.2 -openssl/1.1.1s -json-schema-validator/2.2.0 -nlohmann_json/3.11.2 +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 @@ -13,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 a86d8782d..c0bceb3f3 100644 --- a/Development/cpprest/api_router.cpp +++ b/Development/cpprest/api_router.cpp @@ -198,6 +198,11 @@ namespace web 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) { impl->exception_handler = handler; diff --git a/Development/cpprest/api_router.h b/Development/cpprest/api_router.h index 59d18fb2d..38b53e6c0 100644 --- a/Development/cpprest/api_router.h +++ b/Development/cpprest/api_router.h @@ -59,6 +59,9 @@ 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); 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/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/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/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/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/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 d3700d74a..7d89f50d3 100644 --- a/Development/mdns/service_discovery.h +++ b/Development/mdns/service_discovery.h @@ -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 18934076e..aaf8a2f8a 100644 --- a/Development/mdns/service_discovery_impl.cpp +++ b/Development/mdns/service_discovery_impl.cpp @@ -200,19 +200,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 { @@ -520,6 +507,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; }; @@ -562,4 +563,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 f9a8b4814..63abf856d 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -101,22 +101,35 @@ // 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"], + // is13_versions [node]: used to specify the enabled API versions for a version-locked configuration //"is13_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, + // 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, or OCSP servers + // 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, @@ -138,6 +151,8 @@ //"annotation_port": 3212, // 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, @@ -229,9 +244,6 @@ // proxy_port [registry, node]: forward proxy port //"proxy_port": 8080, - // discovery_mode [node]: whether the discovered host name (1) or resolved addresses (2) are used to construct request URLs for Registration APIs or System APIs - //"discovery_mode": 1, - // href_mode [registry, node]: whether the host name (1), addresses (2) or both (3) are used to construct response headers, and host and URL fields in the data model //"href_mode": 1, @@ -286,5 +298,94 @@ // 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 d2b65923f..e4b420fa2 100644 --- a/Development/nmos-cpp-node/main.cpp +++ b/Development/nmos-cpp-node/main.cpp @@ -1,5 +1,13 @@ #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" @@ -8,6 +16,7 @@ #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[]) @@ -107,23 +116,44 @@ int main(int argc, char* argv[]) } #endif - // Set up the node server - - auto node_server = nmos::experimental::make_node_server(node_model, node_implementation, log_model, gate); - - if (!nmos::experimental::fields::http_trace(node_model.settings)) + // 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)) { - // Disable TRACE method + 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 + } - for (auto& http_listener : node_server.http_listeners) - { - http_listener.support(web::http::methods::TRCE, [](web::http::http_request req) { req.reply(web::http::status_codes::MethodNotAllowed); }); - } + 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, gate); }); + 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 @@ -136,6 +166,90 @@ int main(int argc, char* argv[]) } #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 + + for (auto& http_listener : node_server.http_listeners) + { + http_listener.support(web::http::methods::TRCE, [](web::http::http_request req) { req.reply(web::http::status_codes::MethodNotAllowed); }); + } + } + + // 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) slog::log(gate, SLOG_FLF) << "Preparing for connections"; diff --git a/Development/nmos-cpp-node/node_implementation.cpp b/Development/nmos-cpp-node/node_implementation.cpp index b1c252891..1ebc56be6 100644 --- a/Development/nmos-cpp-node/node_implementation.cpp +++ b/Development/nmos-cpp-node/node_implementation.cpp @@ -21,10 +21,15 @@ #include "nmos/colorspace.h" #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 @@ -186,7 +191,7 @@ namespace impl } // forward declarations for node_implementation_thread -void node_implementation_init(nmos::node_model& model, slog::base_gate& gate); +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); @@ -196,13 +201,13 @@ 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_) +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, gate); + node_implementation_init(model, control_protocol_state, gate); node_implementation_run(model, gate); } catch (const node_implementation_init_exception&) @@ -232,7 +237,7 @@ void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) } } -void node_implementation_init(nmos::node_model& model, slog::base_gate& gate) +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; @@ -294,6 +299,27 @@ void node_implementation_init(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); @@ -894,6 +920,374 @@ void node_implementation_init(nmos::node_model& model, slog::base_gate& gate) 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)) 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::stringstream_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) @@ -911,6 +1305,7 @@ void node_implementation_run(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, ws_sender_ports, events_engine, &gate, token] { @@ -956,6 +1351,33 @@ void node_implementation_run(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(); @@ -970,6 +1392,7 @@ void node_implementation_run(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 }; + events.wait(); } @@ -1241,13 +1664,17 @@ nmos::connection_activation_handler make_node_implementation_connection_activati 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_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); }; } @@ -1290,6 +1717,24 @@ nmos::annotation_patch_merger make_node_implementation_annotation_patch_merger(c }; } +// 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) @@ -1444,5 +1889,6 @@ nmos::experimental::node_implementation make_node_implementation(nmos::node_mode .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_merge_annotation_patch(make_node_implementation_annotation_patch_merger(model.settings, gate)); // may be omitted if not required + .on_merge_annotation_patch(make_node_implementation_annotation_patch_merger(model.settings, gate)) // may be omitted if not required + .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 421769f38..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); +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 940eb0645..4b4a3e89b 100644 --- a/Development/nmos-cpp-registry/config.json +++ b/Development/nmos-cpp-registry/config.json @@ -34,15 +34,25 @@ // 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, or OCSP servers + // 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, this becomes the default port for each HTTP API and the next higher port becomes the default for each WebSocket API @@ -236,5 +246,38 @@ // 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 df15bf185..a98b253b7 100644 --- a/Development/nmos-cpp-registry/main.cpp +++ b/Development/nmos-cpp-registry/main.cpp @@ -1,5 +1,7 @@ #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" @@ -104,10 +106,23 @@ int main(int argc, char* argv[]) 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)); + 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, registry_implementation, log_model, gate); @@ -135,6 +150,14 @@ int main(int argc, char* argv[]) } #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/api_utils.cpp b/Development/nmos/api_utils.cpp index bf352c77c..899d759d6 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -5,10 +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" @@ -156,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); } @@ -175,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); } @@ -751,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 976d7f316..53958eb2f 100644 --- a/Development/nmos/api_utils.h +++ b/Development/nmos/api_utils.h @@ -8,6 +8,7 @@ #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 @@ -19,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?) @@ -46,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")); // IS-13 Annotation API @@ -195,6 +202,23 @@ namespace nmos 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); + } + } } #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..f5bdeead3 --- /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; + } + auto now = std::chrono::system_clock::now(); + 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/certificate_handlers.cpp b/Development/nmos/certificate_handlers.cpp index 81064b37e..ed52a73ab 100644 --- a/Development/nmos/certificate_handlers.cpp +++ b/Development/nmos/certificate_handlers.cpp @@ -121,4 +121,61 @@ namespace nmos 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::stringstream_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 index bb853fef5..043a50e40 100644 --- a/Development/nmos/certificate_handlers.h +++ b/Development/nmos/certificate_handlers.h @@ -65,6 +65,9 @@ namespace nmos // 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); @@ -73,6 +76,9 @@ namespace nmos // 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/channelmapping_api.cpp b/Development/nmos/channelmapping_api.cpp index dc45d5ece..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&) { diff --git a/Development/nmos/channelmapping_api.h b/Development/nmos/channelmapping_api.h index c3c79d2fb..04cad01df 100644 --- a/Development/nmos/channelmapping_api.h +++ b/Development/nmos/channelmapping_api.h @@ -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/client_utils.cpp b/Development/nmos/client_utils.cpp index 784a5183b..681c520da 100644 --- a/Development/nmos/client_utils.cpp +++ b/Development/nmos/client_utils.cpp @@ -11,8 +11,10 @@ #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" @@ -125,7 +127,7 @@ namespace nmos } #ifdef CPPRESTSDK_ENABLE_BIND_WEBSOCKET_CLIENT - // The current version of the C++ REST SDK 2.10.18 does not provide the callback to enable the custom websocket setting + // 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 {}; @@ -177,6 +179,43 @@ namespace nmos 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(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) @@ -202,6 +241,56 @@ namespace nmos 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) { diff --git a/Development/nmos/client_utils.h b/Development/nmos/client_utils.h index 826d9e23c..c1f83e3f7 100644 --- a/Development/nmos/client_utils.h +++ b/Development/nmos/client_utils.h @@ -2,6 +2,7 @@ #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" @@ -17,14 +18,25 @@ namespace nmos // 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); + // 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, 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()); pplx::task api_request(web::http::client::http_client client, const web::http::method& mtd, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()); diff --git a/Development/nmos/connection_api.cpp b/Development/nmos/connection_api.cpp index 385a31c76..eb63bcafb 100644 --- a/Development/nmos/connection_api.cpp +++ b/Development/nmos/connection_api.cpp @@ -24,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; @@ -42,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&) { @@ -54,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) diff --git a/Development/nmos/connection_api.h b/Development/nmos/connection_api.h index 6a7cd47be..bc6b24182 100644 --- a/Development/nmos/connection_api.h +++ b/Development/nmos/connection_api.h @@ -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); diff --git a/Development/nmos/connection_events_activation.cpp b/Development/nmos/connection_events_activation.cpp index 17b014332..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(load_ca_certificates_handler load_ca_certificates, events_ws_message_handler message_handler, 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, load_ca_certificates, gate), 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); diff --git a/Development/nmos/connection_events_activation.h b/Development/nmos/connection_events_activation.h index 1014f2400..4367ab2de 100644 --- a/Development/nmos/connection_events_activation.h +++ b/Development/nmos/connection_events_activation.h @@ -1,6 +1,7 @@ #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. @@ -11,7 +12,12 @@ 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::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); + 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) { 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..d9d781294 --- /dev/null +++ b/Development/nmos/control_protocol_methods.cpp @@ -0,0 +1,628 @@ +#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::stringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << U(" to do Get"); + 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)) + { + return details::make_nc_method_result({ nc_method_status::read_only }); + } + + 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))) + { + return details::make_nc_method_result({ nc_method_status::parameter_error }); + } + + 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) + { + slog::log(gate, SLOG_FLF) << "Set property: " << property_id.serialize() << " value: " << val.serialize() << " error: " << e.what(); + + return details::make_nc_method_result({ nc_method_status::parameter_error }); + } + } + + // unknown property + utility::stringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << " to do Set"; + 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::stringstream_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::stringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is outside the available range to do GetSequenceItem"); + return details::make_nc_method_result_error({ nc_method_status::index_out_of_bounds }, ss.str()); + } + + // unknown property + utility::stringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << U(" to do GetSequenceItem"); + 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::stringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is not a sequence to do SetSequenceItem"); + 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) + { + slog::log(gate, SLOG_FLF) << "Set sequence item: " << property_id.serialize() << " index: " << index << " value: " << val.serialize() << " error: " << e.what(); + + return details::make_nc_method_result({ nc_method_status::parameter_error }); + } + } + + // out of bound + utility::stringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is outside the available range to do SetSequenceItem"); + return details::make_nc_method_result_error({ nc_method_status::index_out_of_bounds }, ss.str()); + } + + // unknown property + utility::stringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << U(" to do SetSequenceItem"); + 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::stringstream_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) + { + slog::log(gate, SLOG_FLF) << "Add sequence item: " << property_id.serialize() << " value: " << val.serialize() << " error: " << e.what(); + + return details::make_nc_method_result({ nc_method_status::parameter_error }); + } + } + + // unknown property + utility::stringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << U(" to do AddSequenceItem"); + 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, 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()) + { + 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::stringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is not a sequence to do RemoveSequenceItem"); + 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); + + }, 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::stringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is outside the available range to do RemoveSequenceItem"); + return details::make_nc_method_result_error({ nc_method_status::index_out_of_bounds }, ss.str()); + } + + // unknown property + utility::stringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << U(" to do RemoveSequenceItem"); + 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::stringstream_t ss; + ss << U("property: ") << property_id.serialize() << U(" is not a sequence to do GetSequenceLength"); + 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::stringstream_t ss; + ss << U("property: ") << property_id.serialize() << " is a null sequence to do GetSequenceLength"; + 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::stringstream_t ss; + ss << U("unknown property: ") << property_id.serialize() << " to do GetSequenceLength"; + 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::stringstream_t ss; + ss << U("role: ") << role.as_string() << U(" not found to do FindMembersByPath"); + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, ss.str()); + } + } + else + { + // no members + return details::make_nc_method_result_error({ nc_method_status::parameter_error }, U("no members to do FindMembersByPath")); + } + } + + 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..957f93627 --- /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, 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..dd4b1911a --- /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) + { + 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 remove_sequence_item(resources, resource, arguments, is_deprecated, get_control_protocol_class_descriptor, 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) }, + { 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..d22718bef --- /dev/null +++ b/Development/nmos/control_protocol_utils.cpp @@ -0,0 +1,651 @@ +#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) + { + // 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::min)() + && 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 (data.is_array()) + { + for (const auto& value : data.as_array()) + { + if (!primitive_validation(name, value)) + { + throw control_protocol_exception("value is not a " + utility::us2s(name) + " type"); + } + } + } + else + { + if (!primitive_validation(name, data)) + { + throw control_protocol_exception("value is not a " + utility::us2s(name) + " type");; + } + } + } + else + { + constraints_validation(data, 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(data, { details::get_datatype_descriptor(type_name, params.get_control_protocol_datatype_descriptor), params.get_control_protocol_datatype_descriptor }); + } + else + { + constraints_validation(data, 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) == data; })) + { + 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 (!data.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) != data.is_null()) { throw control_protocol_exception(utility::us2s(field_name) + " is not nullable"); } + + // is field sequenceable + if (nmos::fields::nc::is_sequence(nc_field_descriptor) != data.is_array()) { throw control_protocol_exception(utility::us2s(field_name) + " is not sequenceable"); } + + // check against field constraints if presented + const auto& constraints = nmos::fields::nc::constraints(nc_field_descriptor); + if (constraints.is_null()) + { + // no field constraints, move to check the 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 = data.at(field_name); + + if (value.is_array()) + { + for (const auto& val : value.as_array()) + { + // do typename constraints validation + datatype_constraints_validation(val, { details::get_datatype_descriptor(field_type_name, params.get_control_protocol_datatype_descriptor), params.get_control_protocol_datatype_descriptor }); + } + } + else + { + // 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 }); + } + } + } + else + { + // do field constraints validation + const auto& value = data.at(field_name); + constraints_validation(value, constraints); + } + } + + return; + } + + // unsupported datatype_type, no validation is required + } + + // 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..b777064ba --- /dev/null +++ b/Development/nmos/control_protocol_ws_api.cpp @@ -0,0 +1,533 @@ +#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 + slog::log(gate, SLOG_FLF) << "invalid argument: " << arguments.serialize() << " error: " << e.what(); + nc_method_result = details::make_nc_method_result({ nmos::nc_method_status::parameter_error }); + } + } + else + { + // unknown methodId + utility::stringstream_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(); + 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::stringstream_t ss; + ss << U("unknown oid: ") << oid; + 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/events_api.cpp b/Development/nmos/events_api.cpp index 9567f58a7..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&) { diff --git a/Development/nmos/events_api.h b/Development/nmos/events_api.h index a6ef568a7..810b6563c 100644 --- a/Development/nmos/events_api.h +++ b/Development/nmos/events_api.h @@ -14,7 +14,12 @@ 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_ws_api.cpp b/Development/nmos/events_ws_api.cpp index cd5485e33..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(); diff --git a/Development/nmos/events_ws_api.h b/Development/nmos/events_ws_api.h index 74a9e603e..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 { @@ -15,15 +17,15 @@ 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) 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 d04d57923..7374c5a90 100644 --- a/Development/nmos/json_fields.h +++ b/Development/nmos/json_fields.h @@ -230,6 +230,114 @@ namespace nmos 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 @@ -238,6 +346,71 @@ namespace nmos 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 2a4e6bbe0..2b7076d2b 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/is13_versions.h" #include "nmos/is13_schemas/is13_schemas.h" #include "nmos/type.h" @@ -129,6 +132,47 @@ namespace nmos } } + 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 is13_schemas { web::uri make_schema_uri(const utility::string_t& tag, const utility::string_t& ref = {}) @@ -329,6 +373,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) } + }; + } + static std::map make_is13_schemas() { using namespace nmos::is13_schemas; @@ -351,6 +432,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()); merge(result, make_is13_schemas()); return result; } @@ -413,6 +496,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; + } + web::uri make_annotationapi_resource_core_patch_request_schema_uri(const nmos::api_version& version) { return is13_schemas::v1_0::annotationapi_resource_core_patch_request_uri; diff --git a/Development/nmos/json_schema.h b/Development/nmos/json_schema.h index 7eda1598c..e2f5a983b 100644 --- a/Development/nmos/json_schema.h +++ b/Development/nmos/json_schema.h @@ -31,6 +31,17 @@ namespace nmos web::uri make_annotationapi_resource_core_patch_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..b03b3e486 --- /dev/null +++ b/Development/nmos/jwt_generator_impl.cpp @@ -0,0 +1,49 @@ +#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; + + // 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(std::chrono::system_clock::now()) + .set_expires_at(std::chrono::system_clock::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) + { + 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/mdns.cpp b/Development/nmos/mdns.cpp index 73b17f1ca..2549e5f43 100644 --- a/Development/nmos/mdns.cpp +++ b/Development/nmos/mdns.cpp @@ -13,6 +13,7 @@ #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 @@ -106,13 +107,22 @@ namespace nmos } bool get_service_authorization(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 + // 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; - - const auto client_authorization = false; - return client_authorization; + + return nmos::experimental::fields::server_authorization(settings); } namespace details @@ -175,8 +185,27 @@ 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) { @@ -215,12 +244,12 @@ namespace nmos 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 - // hm, IS-10 Authorization may also need an 'api_selector' TXT record 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::pri, details::make_pri_value(pri) }, + { txt_record_keys::api_selector, details::make_api_selector_value(selector) } }; } else if (service == nmos::service_types::mqtt) @@ -281,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; } @@ -291,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); } @@ -312,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 @@ -347,7 +380,7 @@ 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(service, settings), nmos::get_service_authorization(service, 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) @@ -425,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(service, settings), nmos::get_service_authorization(service, 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); @@ -462,46 +495,11 @@ namespace nmos update_service(advertiser, service, domain, settings, std::move(add_records)); } - enum discovery_mode - { - discovery_mode_default = 0, - discovery_mode_name = 1, - discovery_mode_addresses = 2 - }; - namespace details { - typedef std::pair api_ver_pri; - typedef std::pair resolved_service; typedef std::vector resolved_services; - std::vector get_resolved_hosts(const mdns::resolve_result& resolved, const nmos::service_protocol& resolved_proto, discovery_mode mode) - { - std::vector results; - - // by default, use the host name if secure communications are in use - if (mode == discovery_mode_name || (mode == discovery_mode_default && is_service_protocol_secure(resolved_proto))) - { - 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(host_name); - } - - if (mode == discovery_mode_addresses || (mode == discovery_mode_default && !is_service_protocol_secure(resolved_proto))) - { - for (const auto& ip_address : resolved.ip_addresses) - { - results.push_back(utility::s2us(ip_address)); - } - } - - return results; - } - - pplx::task resolve_service(std::shared_ptr results, mdns::service_discovery& discovery, discovery_mode discovery_mode, 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) + 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) { return discovery.browse([=, &discovery](const mdns::browse_result& resolving) { @@ -515,7 +513,7 @@ namespace nmos // 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) { @@ -538,17 +536,36 @@ 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); - auto resolved_hosts = get_resolved_hosts(resolved, resolved_proto, discovery_mode); + auto resolved_uri = web::uri_builder(); + if (service == nmos::service_types::authorization) + { + 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 + { + resolved_uri + .set_scheme(utility::s2us(resolved_proto)) + .set_port(resolved.port) + .set_path(U("/x-nmos/") + utility::s2us(details::service_api(service))); + } - for (const auto& host : resolved_hosts) + 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_host(host) + .set_user_info(host_name) + .set_host(utility::s2us(ip_address)) .to_uri() }); } @@ -568,11 +585,14 @@ namespace nmos 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) }; } } - pplx::task> resolve_service(mdns::service_discovery& discovery, discovery_mode mode, 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) + // 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) { const auto absolute_timeout = std::chrono::steady_clock::now() + timeout; @@ -600,8 +620,8 @@ namespace nmos }; const std::vector> both_tasks{ - details::resolve_service(both_results[0], discovery, mode, nmos::service_types::register_, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, linked_token), - details::resolve_service(both_results[1], discovery, mode, service, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, linked_token) + details::resolve_service(both_results[0], discovery, nmos::service_types::register_, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, linked_token), + details::resolve_service(both_results[1], discovery, service, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, linked_token) }; // when either task is completed, cancel and wait for the other to be completed @@ -629,23 +649,23 @@ namespace nmos } else { - resolve_task = details::resolve_service(results, discovery, mode, nmos::service_types::register_, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, token); + resolve_task = details::resolve_service(results, discovery, nmos::service_types::register_, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, token); } } else { - resolve_task = details::resolve_service(results, discovery, mode, service, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, token); + resolve_task = details::resolve_service(results, discovery, service, browse_domain, api_ver, priorities, api_proto, api_auth, absolute_timeout, token); } return resolve_task.then([results, randomize](bool) { // 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()); @@ -660,32 +680,40 @@ 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); }); - // add the version to each uri - return boost::copy_range>(*results | boost::adaptors::transformed([](const details::resolved_service& s) + // return the randomized services + std::list resolved_services; + for (const auto& result : *results) { - return web::uri_builder(s.second).append_path(U("/") + make_api_version(s.first.first)).to_uri(); - })); + 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 (by default) services with the same priority ordered randomly + // 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, discovery_mode_default, service, browse_domain, api_ver, priorities, api_proto, api_auth, randomize, timeout, 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>(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 mode = discovery_mode(nmos::experimental::fields::discovery_mode(settings)); 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); @@ -695,8 +723,25 @@ namespace nmos // 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, mode, service, browse_domain, versions, priorities, protocols, authorization, true, std::chrono::seconds(timeout), token); + + 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 9a257a1a3..8267ab850 100644 --- a/Development/nmos/mdns.h +++ b/Development/nmos/mdns.h @@ -61,7 +61,7 @@ namespace nmos // 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" }; @@ -122,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 @@ -172,6 +172,25 @@ namespace nmos // 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/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 9c5ee8526..2d457172c 100644 --- a/Development/nmos/node_api.h +++ b/Development/nmos/node_api.h @@ -10,7 +10,12 @@ 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 725df294c..a98d26967 100644 --- a/Development/nmos/node_api_target_handler.cpp +++ b/Development/nmos/node_api_target_handler.cpp @@ -8,15 +8,16 @@ #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, load_ca_certificates_handler load_ca_certificates, 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, load_ca_certificates, 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; @@ -29,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, [&, load_ca_certificates] { return nmos::make_http_client_config(model.settings, load_ca_certificates, gate); })); + 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) diff --git a/Development/nmos/node_api_target_handler.h b/Development/nmos/node_api_target_handler.h index 076cb7574..2b22990d7 100644 --- a/Development/nmos/node_api_target_handler.h +++ b/Development/nmos/node_api_target_handler.h @@ -1,6 +1,7 @@ #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" @@ -21,7 +22,12 @@ namespace nmos // 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, load_ca_certificates_handler load_ca_certificates, 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) { diff --git a/Development/nmos/node_behaviour.cpp b/Development/nmos/node_behaviour.cpp index 2b009e301..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, load_ca_certificates_handler load_ca_certificates, 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, load_ca_certificates_handler load_ca_certificates, 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, 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,6 +43,17 @@ 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, 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)); + + 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), 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)); @@ -51,31 +63,35 @@ namespace nmos mdns::service_discovery discovery(gate); - details::node_behaviour_thread(model, std::move(load_ca_certificates), std::move(registration_changed), advertiser, 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, load_ca_certificates_handler load_ca_certificates, 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(load_ca_certificates), 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, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { - node_behaviour_thread(model, load_ca_certificates, {}, gate); + node_behaviour_thread(model, load_ca_certificates, {}, {}, gate); } // uses the specified DNS-SD implementation 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, load_ca_certificates, {}, advertiser, discovery, gate); + node_behaviour_thread(model, load_ca_certificates, {}, {}, advertiser, discovery, gate); } - void details::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) + 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". @@ -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, load_ca_certificates, 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, load_ca_certificates, 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)) { @@ -261,7 +277,7 @@ namespace nmos }).get(); with_write_lock(model.mutex, [&] - { + { if (!registration_services.empty()) { slog::log(gate, SLOG_FLF) << "Discovered " << registration_services.size() << " Registration API(s)"; @@ -470,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, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + 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, std::move(load_ca_certificates), gate); + 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, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + 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, std::move(load_ca_certificates), gate); + 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; } @@ -538,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 { @@ -657,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, load_ca_certificates_handler load_ca_certificates, 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"; @@ -744,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, load_ca_certificates, gate))); + 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(); @@ -767,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) @@ -815,7 +833,7 @@ namespace nmos request.wait(); } - 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, 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"; @@ -838,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(); @@ -881,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, load_ca_certificates, gate))); - heartbeat_client.reset(new web::http::client::http_client(base_uri, make_heartbeat_client_config(model.settings, load_ca_certificates, gate))); + 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; @@ -916,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); @@ -969,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 diff --git a/Development/nmos/node_behaviour.h b/Development/nmos/node_behaviour.h index f7b3f2480..c98abf854 100644 --- a/Development/nmos/node_behaviour.h +++ b/Development/nmos/node_behaviour.h @@ -2,6 +2,7 @@ #define NMOS_NODE_BEHAVIOUR_H #include +#include "nmos/authorization_handlers.h" #include "nmos/certificate_handlers.h" namespace web @@ -35,11 +36,13 @@ 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, 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, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); diff --git a/Development/nmos/node_resource.cpp b/Development/nmos/node_resource.cpp index 85f78efc6..b12c12b39 100644 --- a/Development/nmos/node_resource.cpp +++ b/Development/nmos/node_resource.cpp @@ -36,7 +36,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) } })); } diff --git a/Development/nmos/node_resources.cpp b/Development/nmos/node_resources.cpp index 69056819d..34cab371c 100644 --- a/Development/nmos/node_resources.cpp +++ b/Development/nmos/node_resources.cpp @@ -16,6 +16,7 @@ #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 @@ -57,7 +58,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) } })); } } @@ -77,7 +79,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) } })); } } @@ -102,7 +105,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) } })); } } @@ -122,11 +126,34 @@ 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 }; } diff --git a/Development/nmos/node_resources.h b/Development/nmos/node_resources.h index 2598867bb..a1b9f6555 100644 --- a/Development/nmos/node_resources.h +++ b/Development/nmos/node_resources.h @@ -113,7 +113,7 @@ namespace nmos { 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://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_core.html diff --git a/Development/nmos/node_server.cpp b/Development/nmos/node_server.cpp index 7c92f38e8..0e67de64e 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -1,16 +1,18 @@ #include "nmos/node_server.h" #include "cpprest/ws_utils.h" +#include "nmos/annotation_api.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" #include "nmos/node_api.h" #include "nmos/node_behaviour.h" -#include "nmos/annotation_api.h" #include "nmos/server.h" #include "nmos/server_utils.h" #include "nmos/settings_api.h" @@ -20,13 +22,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 }; @@ -50,23 +58,37 @@ namespace nmos // Configure the Node API - 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_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::fields::annotation_port(node_model.settings) }].mount({}, nmos::make_annotation_api(node_model, node_implementation.merge_annotation_patch, 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 @@ -86,6 +108,10 @@ namespace nmos 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) { // if IP address isn't specified for this router, use default server address or wildcard address @@ -93,9 +119,21 @@ namespace nmos // 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) @@ -105,8 +143,9 @@ namespace nmos 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({ - [&, load_ca_certificates, registration_changed] { nmos::node_behaviour_thread(node_model, load_ca_certificates, 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); }, @@ -119,6 +158,13 @@ namespace nmos 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; } diff --git a/Development/nmos/node_server.h b/Development/nmos/node_server.h index bace14793..fd7f195cc 100644 --- a/Development/nmos/node_server.h +++ b/Development/nmos/node_server.h @@ -1,15 +1,18 @@ #ifndef NMOS_NODE_SERVER_H #define NMOS_NODE_SERVER_H +#include "nmos/annotation_api.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/annotation_api.h" +#include "nmos/ws_api_utils.h" namespace nmos { @@ -25,7 +28,7 @@ namespace nmos // underlying implementation into the server instance for the NMOS Node struct node_implementation { - 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) + 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)) @@ -37,6 +40,17 @@ namespace nmos , 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 @@ -59,6 +73,17 @@ namespace nmos node_implementation& on_channelmapping_activated(nmos::channelmapping_activation_handler channelmapping_activated) { this->channelmapping_activated = std::move(channelmapping_activated); return *this; } node_implementation& on_merge_annotation_patch(nmos::annotation_patch_merger merge_annotation_patch) { this->merge_annotation_patch = std::move(merge_annotation_patch); 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)); } @@ -90,6 +115,19 @@ namespace nmos nmos::annotation_patch_merger merge_annotation_patch; 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 diff --git a/Development/nmos/node_system_behaviour.cpp b/Development/nmos/node_system_behaviour.cpp index 2e215824b..bc29d0da5 100644 --- a/Development/nmos/node_system_behaviour.cpp +++ b/Development/nmos/node_system_behaviour.cpp @@ -396,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, load_ca_certificates, gate))); + 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/ocsp_behaviour.cpp b/Development/nmos/ocsp_behaviour.cpp index f16140be6..a835d5937 100644 --- a/Development/nmos/ocsp_behaviour.cpp +++ b/Development/nmos/ocsp_behaviour.cpp @@ -347,7 +347,7 @@ namespace nmos { const auto ocsp_uri = ocsp_uris.front(); const auto secure = web::is_secure_uri_scheme(ocsp_uri.scheme()); - state.client.reset(new web::http::client::http_client(ocsp_uri, make_ocsp_client_config(secure, model.settings, state.load_ca_certificates, gate))); + 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(); diff --git a/Development/nmos/query_api.cpp b/Development/nmos/query_api.cpp index 0e4da6cb4..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&) { @@ -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 2c372d7b1..ec1200368 100644 --- a/Development/nmos/query_api.h +++ b/Development/nmos/query_api.h @@ -15,7 +15,12 @@ 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 14e381200..62977ff09 100644 --- a/Development/nmos/query_utils.cpp +++ b/Development/nmos/query_utils.cpp @@ -577,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 91addbe46..fcfbe9c0b 100644 --- a/Development/nmos/query_utils.h +++ b/Development/nmos/query_utils.h @@ -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 2497f39ad..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; diff --git a/Development/nmos/query_ws_api.h b/Development/nmos/query_ws_api.h index b5034ff8f..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 { @@ -14,17 +16,17 @@ 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/registration_api.cpp b/Development/nmos/registration_api.cpp index 1f7f704ab..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 @@ -404,9 +417,19 @@ 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); @@ -414,6 +437,20 @@ namespace nmos 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); @@ -426,13 +463,17 @@ namespace nmos }); } - // 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) { diff --git a/Development/nmos/registration_api.h b/Development/nmos/registration_api.h index d69fe4844..f254e02cd 100644 --- a/Development/nmos/registration_api.h +++ b/Development/nmos/registration_api.h @@ -16,7 +16,12 @@ namespace nmos 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 bcd3d024a..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::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 }; @@ -64,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://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; diff --git a/Development/nmos/registry_server.h b/Development/nmos/registry_server.h index 590c57c9d..82dce35ce 100644 --- a/Development/nmos/registry_server.h +++ b/Development/nmos/registry_server.h @@ -1,8 +1,10 @@ #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 { @@ -23,11 +25,13 @@ namespace nmos // 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) + 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 @@ -39,6 +43,8 @@ namespace nmos 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 @@ -51,6 +57,9 @@ namespace nmos 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 diff --git a/Development/nmos/resource.h b/Development/nmos/resource.h index fedfde3a4..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 @@ -71,6 +72,11 @@ namespace nmos // 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 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 2bc863982..22f693b9a 100644 --- a/Development/nmos/sdp_utils.cpp +++ b/Development/nmos/sdp_utils.cpp @@ -341,7 +341,7 @@ namespace nmos // hm, TM not indicated in IS-04 if (tm) params.tm = *tm; - params.ssn = params.tm.empty() ? sdp::smpte_standard_numbers::ST2110_40_2018 : sdp::smpte_standard_numbers::ST2110_40_2022; + 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 @@ -793,11 +793,11 @@ namespace nmos // 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 (0 != params.troff) fmtp.push_back({ sdp::fields::TROFF, utility::ostringstreamed(params.troff) }); + 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 (0 != params.tsdelay) fmtp.push_back({ sdp::fields::timestamp_delay, utility::ostringstreamed(params.tsdelay) }); + 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 }; } @@ -814,7 +814,7 @@ namespace nmos 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 (0 != params.tsdelay) fmtp.push_back({ sdp::fields::timestamp_delay, utility::ostringstreamed(params.tsdelay) }); + if (params.tsdelay) fmtp.push_back({ sdp::fields::timestamp_delay, utility::ostringstreamed(*params.tsdelay) }); return{ session_name, sdp::media_types::audio, rtpmap, fmtp, {}, params.packet_time, {}, {}, media_stream_ids, ts_refclk }; } @@ -832,13 +832,13 @@ namespace nmos { 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(params.vpid_code) }); - if (!params.exactframerate) fmtp.push_back({ sdp::fields::exactframerate, nmos::details::make_exactframerate(params.exactframerate) }); + 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 (0 != params.troff) fmtp.push_back({ sdp::fields::TROFF, utility::ostringstreamed(params.troff) }); + 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 (0 != params.tsdelay) fmtp.push_back({ sdp::fields::timestamp_delay, utility::ostringstreamed(params.tsdelay) }); + 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 }; } @@ -854,7 +854,7 @@ namespace nmos // See https://tools.ietf.org/html/rfc4566#section-6 sdp_parameters::fmtp_t fmtp = {}; if (!params.tp.empty()) fmtp.push_back({ sdp::fields::type_parameter, params.tp.name }); - if (0 != params.troff) fmtp.push_back({ sdp::fields::TROFF, utility::ostringstreamed(params.troff) }); + if (params.troff) fmtp.push_back({ sdp::fields::TROFF, utility::ostringstreamed(*params.troff) }); return{ session_name, sdp::media_types::video, rtpmap, fmtp, {}, {}, {}, {}, media_stream_ids, ts_refclk }; } diff --git a/Development/nmos/sdp_utils.h b/Development/nmos/sdp_utils.h index cb03c7bde..c4ce30e68 100644 --- a/Development/nmos/sdp_utils.h +++ b/Development/nmos/sdp_utils.h @@ -190,7 +190,7 @@ namespace nmos // 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 @@ -316,13 +316,13 @@ namespace nmos // additional fmtp parameters from ST 2110-21:2022 sdp::type_parameter tp; - uint32_t troff; // if omitted (zero), assume default + 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 - uint32_t tsdelay; + bst::optional tsdelay; video_raw_parameters() : depth(), width(), height(), interlace(), segmented(), troff(), cmax(), maxudp(), tsdelay() {} @@ -341,11 +341,11 @@ namespace nmos sdp::packing_mode pm, sdp::smpte_standard_number ssn, sdp::type_parameter tp, - uint32_t troff, + bst::optional troff, uint32_t cmax, uint32_t maxudp, sdp::timestamp_mode tsmode, - uint32_t tsdelay + bst::optional tsdelay ) : sampling(std::move(sampling)) , depth(depth) @@ -393,7 +393,7 @@ namespace nmos // additional fmtp parameters from ST 2110-10:2022 sdp::timestamp_mode tsmode; // if omitted (empty), assume sdp::timestamp_modes::NEW - uint32_t tsdelay; + bst::optional tsdelay; // ptime double packet_time; @@ -406,7 +406,7 @@ namespace nmos uint64_t sample_rate, utility::string_t channel_order, sdp::timestamp_mode tsmode, - uint32_t tsdelay, + bst::optional tsdelay, double packet_time ) : channel_count(channel_count) @@ -425,7 +425,7 @@ namespace nmos }; // Additional "video/smpte291" data payload parameters - // See SMPTE ST 2110-40:2022 + // 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 @@ -434,19 +434,19 @@ namespace nmos 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:2022 + // fmtp is required to indicate frame rate, since ST 2110-40:2023 nmos::rational exactframerate; - // fmtp optionally indicates TM, since ST 2110-40:2022 + // 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:2022 + // fmtp is required to indicate SSN, since ST 2110-40:2023 sdp::smpte_standard_number ssn; // additional fmtp parameters from ST 2110-21:2022 - uint32_t troff; // if omitted (zero), assume default + 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 - uint32_t tsdelay; + bst::optional tsdelay; video_smpte291_parameters() : vpid_code(), troff(), tsdelay() {} @@ -456,9 +456,9 @@ namespace nmos nmos::rational exactframerate, sdp::transmission_model tm, sdp::smpte_standard_number ssn, - uint32_t troff, + bst::optional troff, sdp::timestamp_mode tsmode, - uint32_t tsdelay + bst::optional tsdelay ) : did_sdids(std::move(did_sdids)) , vpid_code(vpid_code) @@ -482,13 +482,13 @@ namespace nmos { // additional fmtp parameters from ST 2110-21:2017 sdp::type_parameter tp; - uint32_t troff; // if omitted (zero), assume default + bst::optional troff; // if omitted, assume default video_SMPTE2022_6_parameters() : troff() {} video_SMPTE2022_6_parameters( sdp::type_parameter tp, - uint32_t troff + bst::optional troff ) : tp(std::move(tp)) , troff(troff) diff --git a/Development/nmos/settings.cpp b/Development/nmos/settings.cpp index 9c0fe67cc..51f0c96c5 100644 --- a/Development/nmos/settings.cpp +++ b/Development/nmos/settings.cpp @@ -66,6 +66,8 @@ namespace nmos 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)); 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)); @@ -82,6 +84,9 @@ namespace nmos 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)); } } } diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 37229e978..267f9c358 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -101,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 + // is13_versions [node]: used to specify the enabled API versions for a version-locked configuration const web::json::field_as_array is13_versions{ U("is13_versions") }; // when omitted, nmos::is13_versions::all is used @@ -111,12 +117,19 @@ 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 + // 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") }; @@ -140,6 +153,8 @@ namespace nmos const web::json::field_as_integer_or annotation_port{ U("annotation_port"), 3212 }; // 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 }; @@ -288,9 +303,6 @@ namespace nmos // proxy_port [registry, node]: forward proxy port const web::json::field_as_integer_or proxy_port{ U("proxy_port"), 8080 }; - // discovery_mode [node]: whether the discovered host name (1) or resolved addresses (2) are used to construct request URLs for Registration APIs or System APIs - const web::json::field_as_integer_or discovery_mode{ U("discovery_mode"), 0 }; // when omitted, a default heuristic is used - // href_mode [registry, node]: whether the host name (1), addresses (2) or both (3) are used to construct response headers, and host and URL fields in the data model const web::json::field_as_integer_or href_mode{ U("href_mode"), 0 }; // when omitted, a default heuristic is used @@ -360,6 +372,102 @@ namespace nmos // 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 d3fc150fd..13a1a67b8 100644 --- a/Development/nmos/slog.h +++ b/Development/nmos/slog.h @@ -44,6 +44,8 @@ namespace nmos 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/test/control_protocol_test.cpp b/Development/nmos/test/control_protocol_test.cpp new file mode 100644 index 000000000..7150accd6 --- /dev/null +++ b/Development/nmos/test/control_protocol_test.cpp @@ -0,0 +1,1464 @@ +// 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 } + }); + + // propertry 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, 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("Sequence string property example"), U("sequenceStringProperty"), U("NcString"), false, false, datatype_string_constraints)); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Sequence number property example"), U("sequenceNumberProperty"), U("NcInt32"), false, false, datatype_int32_constraints)); + web::json::push_back(fields, nmos::details::make_nc_field_descriptor(U("Sequence boolean property example"), U("sequenceBooleanProperty"), 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("Sequence struct property example"), U("sequenceStructProperty"), U("simpleStructDatatype"), false, false, 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::min(), 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::min(), 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)); + // 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_struct = 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 } + }) }) } + }); + // 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 } + }) }) } + }); + // 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 } + }) }) } + }); + 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 + }) }) } + }); + + 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_struct, 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); +} 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/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 index a2b3db618..edf850a23 100644 --- a/Development/nmos/test/sdp_utils_test.cpp +++ b/Development/nmos/test/sdp_utils_test.cpp @@ -9,6 +9,7 @@ #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" //////////////////////////////////////////////////////////////////////////////////////////// @@ -498,3 +499,442 @@ BST_TEST_CASE(testSdpTransportParamsMulticast) 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/video_jxsv_test.cpp b/Development/nmos/test/video_jxsv_test.cpp index fc92ba85a..cfbab508c 100644 --- a/Development/nmos/test/video_jxsv_test.cpp +++ b/Development/nmos/test/video_jxsv_test.cpp @@ -3,6 +3,7 @@ #include "bst/test/test.h" #include "nmos/json_fields.h" +#include "nmos/test/sdp_test_utils.h" #include "sdp/sdp.h" //////////////////////////////////////////////////////////////////////////////////////////// @@ -56,3 +57,189 @@ a=fmtp:112 packetmode=0; profile=High444.12; level=1k-1; sublevel=Sublev3bpp; de 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/type.h b/Development/nmos/type.h index d58734f81..8da37f685 100644 --- a/Development/nmos/type.h +++ b/Development/nmos/type.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/video_jxsv.cpp b/Development/nmos/video_jxsv.cpp index 05440fecc..bde7df1e2 100644 --- a/Development/nmos/video_jxsv.cpp +++ b/Development/nmos/video_jxsv.cpp @@ -176,11 +176,11 @@ namespace nmos // 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 (0 != params.troff) fmtp.push_back({ sdp::fields::TROFF, utility::ostringstreamed(params.troff) }); + 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 (0 != params.tsdelay) fmtp.push_back({ sdp::fields::timestamp_delay, utility::ostringstreamed(params.tsdelay) }); + 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 }; } diff --git a/Development/nmos/video_jxsv.h b/Development/nmos/video_jxsv.h index c954267d0..90d0b740b 100644 --- a/Development/nmos/video_jxsv.h +++ b/Development/nmos/video_jxsv.h @@ -283,13 +283,13 @@ namespace nmos // additional fmtp parameters from ST 2110-21:2022 sdp::type_parameter tp; - uint32_t troff; // if omitted (zero), assume default + 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 - uint32_t tsdelay; + bst::optional tsdelay; // bandwidth uint64_t bit_rate; // transport bit rate @@ -314,11 +314,11 @@ namespace nmos sdp::range range, sdp::smpte_standard_number ssn, sdp::type_parameter tp, - uint32_t troff, + bst::optional troff, uint32_t cmax, uint32_t maxudp, sdp::timestamp_mode tsmode, - uint32_t tsdelay, + bst::optional tsdelay, uint64_t bit_rate ) : packetmode(packetmode) 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 3b90a0ce3..fd20aa2f5 100644 --- a/Development/pplx/pplx_utils.h +++ b/Development/pplx/pplx_utils.h @@ -11,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()) { @@ -45,42 +29,24 @@ 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) { @@ -99,7 +65,7 @@ namespace pplx catch (...) {} } } - + struct exception_observer { template @@ -109,13 +75,9 @@ namespace pplx } }; - /// - /// 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. - /// + // 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(); @@ -153,26 +115,18 @@ namespace pplx std::vector> 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. - /// + // 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. - /// + // 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) { @@ -244,9 +198,7 @@ namespace pplx } } - /// - /// RAII helper for classes that have asynchronous open/close member functions. - /// + // RAII helper for classes that have asynchronous open/close member functions. template struct open_close_guard { diff --git a/Development/sdp/json.h b/Development/sdp/json.h index 6eefc206d..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 @@ -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) @@ -299,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 @@ -424,7 +453,8 @@ namespace sdp // See https://tools.ietf.org/html/rfc4175 // and SMPTE ST 2110-20:2022 Section 7 Session Description Protocol (SDP) Considerations - // and VSF TR-05:2018 + // 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 @@ -467,7 +497,7 @@ namespace sdp // and https://tools.ietf.org/html/rfc3190 const web::json::field_as_string channel_order{ U("channel-order") }; // ".", e.g. "SMPTE2110.(ST)", see nmos/channels.h - // See SMPTE ST 2110-40:2022 + // 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}", see nmos::did_sdid const web::json::field VPID_Code{ U("VPID_Code") }; // 1..255, see nmos::vpid_code @@ -570,7 +600,7 @@ namespace sdp // SMPTE Standard Number // 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:2022 Section 7 Session Description Protocol (SDP) + // and SMPTE ST 2110-40:2023 Section 7 Session Description Protocol (SDP) DEFINE_STRING_ENUM(smpte_standard_number) namespace smpte_standard_numbers { @@ -587,9 +617,11 @@ namespace sdp 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:2022." + // which case they shall signal the value ST2110-40:2021." // Note that SSN was not actually specified in ST 2110-40:2018... - const smpte_standard_number ST2110_40_2022{ U("ST2110-40:2022") }; + // ...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) @@ -646,7 +678,7 @@ namespace sdp } // TM (Transmission Model) - // See SMPTE ST 2110-40:2022 Section 7 Session Description Protocol (SDP) + // See SMPTE ST 2110-40:2023 Section 7 Session Description Protocol (SDP) DEFINE_STRING_ENUM(transmission_model) namespace transmission_models { diff --git a/Development/sdp/sdp_grammar.cpp b/Development/sdp/sdp_grammar.cpp index 0c565791a..06086d10e 100644 --- a/Development/sdp/sdp_grammar.cpp +++ b/Development/sdp/sdp_grammar.cpp @@ -40,10 +40,33 @@ namespace sdp { // use web::json::basic_ostream_visitor rather than web::json::value::serialize // to avoid, for example, 59.94 being output as 59.939999999999998 - std::stringstream os; + 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) { auto v = web::json::value::parse(utility::s2us(s)); return v.as_number(), v; } + 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 @@ -71,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) { @@ -199,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); } }; @@ -256,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 @@ -265,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 } @@ -328,7 +353,7 @@ namespace sdp 'b', object_converter({ { sdp::fields::bandwidth_type, string_converter }, - { sdp::fields::bandwidth, number_converter } + { sdp::fields::bandwidth, digits_converter } }, ":") ); @@ -469,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 } }) @@ -714,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 }, + }) } }; } diff --git a/Development/sdp/test/sdp_test.cpp b/Development/sdp/test/sdp_test.cpp index 33a056e5f..6d216b2a3 100644 --- a/Development/sdp/test/sdp_test.cpp +++ b/Development/sdp/test/sdp_test.cpp @@ -14,6 +14,12 @@ 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 @@ -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({ @@ -324,6 +384,40 @@ a=framerate:59.94 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) { @@ -363,6 +457,35 @@ BST_TEST_CASE(testSdpParseErrors) 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) { diff --git a/Development/third_party/README.md b/Development/third_party/README.md index fe42ecd63..5f29fd91c 100644 --- a/Development/third_party/README.md +++ b/Development/third_party/README.md @@ -6,6 +6,8 @@ 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) @@ -18,6 +20,8 @@ Third-party source files used by the nmos-cpp libraries 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 - [is-13](is-13) The JSON Schema files used for validation of Annotation API requests and responses - [WpdPack](WpdPack) diff --git a/Development/third_party/cmake/README.md b/Development/third_party/cmake/README.md index e27f9f2c9..11b068e73 100644 --- a/Development/third_party/cmake/README.md +++ b/Development/third_party/cmake/README.md @@ -29,3 +29,12 @@ 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/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/json-patch.hpp b/Development/third_party/nlohmann/json-patch.hpp index 80fe1dfc2..39a579b16 100644 --- a/Development/third_party/nlohmann/json-patch.hpp +++ b/Development/third_party/nlohmann/json-patch.hpp @@ -28,10 +28,13 @@ class json_patch 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_; + json j_ = nlohmann::json::array(); static void validateJsonPatch(json const &patch); }; diff --git a/Development/third_party/nlohmann/json-validator.cpp b/Development/third_party/nlohmann/json-validator.cpp index 7f34553e1..1fd0de1ac 100644 --- a/Development/third_party/nlohmann/json-validator.cpp +++ b/Development/third_party/nlohmann/json-validator.cpp @@ -436,9 +436,12 @@ class logical_combination : public schema for (auto &s : subschemata_) { first_error_handler 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; @@ -553,6 +556,9 @@ class type_schema : public schema else_->validate(ptr, instance, patch, e); } } + if (instance.is_null()) { + patch.add(nlohmann::json::json_pointer{}, default_value_); + } } protected: @@ -601,10 +607,12 @@ class type_schema : public schema } 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_[static_cast(t.second)] = type_schema::make(sch, t.second, root, uris, known_keywords); + } break; default: @@ -856,7 +864,12 @@ class numeric : public schema bool violates_multiple_of(T x) const { double res = std::remainder(x, multipleOf_.second); + 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); } @@ -1134,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); + } } }; diff --git a/Development/third_party/nlohmann/json.hpp b/Development/third_party/nlohmann/json.hpp index 5dd555f0c..8b72ea653 100644 --- a/Development/third_party/nlohmann/json.hpp +++ b/Development/third_party/nlohmann/json.hpp @@ -1,9 +1,9 @@ // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT /****************************************************************************\ @@ -27,7 +27,6 @@ #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 @@ -35,10 +34,10 @@ // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -48,10 +47,10 @@ // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -60,7 +59,7 @@ #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 != 2 + #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 @@ -68,7 +67,7 @@ #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 2 // NOLINT(modernize-macro-to-enum) +#define NLOHMANN_JSON_VERSION_PATCH 3 // NOLINT(modernize-macro-to-enum) #ifndef JSON_DIAGNOSTICS #define JSON_DIAGNOSTICS 0 @@ -150,10 +149,10 @@ // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -173,16 +172,19 @@ // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// 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 @@ -190,10 +192,10 @@ // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -206,10 +208,10 @@ // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -218,10 +220,10 @@ // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -231,10 +233,10 @@ // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -318,10 +320,10 @@ NLOHMANN_JSON_NAMESPACE_END // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-FileCopyrightText: 2016-2021 Evan Nemerson // SPDX-License-Identifier: MIT @@ -2483,6 +2485,14 @@ JSON_HEDLEY_DIAGNOSTIC_POP #endif #endif +#ifndef JSON_HAS_STATIC_RTTI + #if !defined(_HAS_STATIC_RTTI) || _HAS_STATIC_RTTI != 0 + #define JSON_HAS_STATIC_RTTI 1 + #else + #define JSON_HAS_STATIC_RTTI 0 + #endif +#endif + #ifdef JSON_HAS_CPP_17 #define JSON_INLINE_VARIABLE inline #else @@ -2590,12 +2600,13 @@ JSON_HEDLEY_DIAGNOSTIC_POP class NumberUnsignedType, class NumberFloatType, \ template class AllocatorType, \ template class JSONSerializer, \ - class BinaryType> + class BinaryType, \ + class CustomBaseClass> #define NLOHMANN_BASIC_JSON_TPL \ basic_json + AllocatorType, JSONSerializer, BinaryType, CustomBaseClass> // Macros to simplify conversion from/to types @@ -2745,7 +2756,10 @@ JSON_HEDLEY_DIAGNOSTIC_POP #define NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Type, ...) \ friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ - friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { Type nlohmann_json_default_obj; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ + friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } /*! @brief macro @@ -2756,10 +2770,12 @@ JSON_HEDLEY_DIAGNOSTIC_POP inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ + inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + #define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Type, ...) \ inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ - inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { Type nlohmann_json_default_obj; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } - + inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } // inspired from https://stackoverflow.com/a/26745591 // allows to call any std function as if (e.g. with begin): @@ -2923,10 +2939,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -2998,10 +3014,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -3040,10 +3056,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-FileCopyrightText: 2018 The Abseil Authors // SPDX-License-Identifier: MIT @@ -3214,10 +3230,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -3226,14 +3242,15 @@ NLOHMANN_JSON_NAMESPACE_END #include // false_type, is_constructible, is_integral, is_same, true_type #include // declval #include // tuple +#include // char_traits // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -3298,10 +3315,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -3318,10 +3335,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -3342,10 +3359,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT #ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_ @@ -3389,7 +3406,8 @@ NLOHMANN_JSON_NAMESPACE_END template class AllocatorType = std::allocator, template class JSONSerializer = adl_serializer, - class BinaryType = std::vector> + class BinaryType = std::vector, // cppcheck-suppress syntaxError + class CustomBaseClass = void> class basic_json; /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document @@ -3577,6 +3595,63 @@ struct actual_object_comparator template using actual_object_comparator_t = typename actual_object_comparator::type; +///////////////// +// char_traits // +///////////////// + +// Primary template of char_traits calls std char_traits +template +struct char_traits : std::char_traits +{}; + +// Explicitly define char traits for unsigned char since it is not standard +template<> +struct char_traits : std::char_traits +{ + using char_type = unsigned char; + using int_type = uint64_t; + + // Redefine to_int_type function + static int_type to_int_type(char_type c) noexcept + { + return static_cast(c); + } + + static char_type to_char_type(int_type i) noexcept + { + return static_cast(i); + } + + static constexpr int_type eof() noexcept + { + return static_cast(EOF); + } +}; + +// Explicitly define char traits for signed char since it is not standard +template<> +struct char_traits : std::char_traits +{ + using char_type = signed char; + using int_type = uint64_t; + + // Redefine to_int_type function + static int_type to_int_type(char_type c) noexcept + { + return static_cast(c); + } + + static char_type to_char_type(int_type i) noexcept + { + return static_cast(i); + } + + static constexpr int_type eof() noexcept + { + return static_cast(EOF); + } +}; + /////////////////// // is_ functions // /////////////////// @@ -3613,7 +3688,6 @@ template struct is_default_constructible> : conjunction...> {}; - template struct is_constructible : std::is_constructible {}; @@ -3629,7 +3703,6 @@ struct is_constructible> : is_default_constructible struct is_constructible> : is_default_constructible> {}; - template struct is_iterator_traits : std::false_type {}; @@ -4039,7 +4112,6 @@ struct value_in_range_of_impl2 } }; - template struct value_in_range_of_impl2 { @@ -4138,10 +4210,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -4165,28 +4237,28 @@ inline std::size_t concat_length() } template -inline std::size_t concat_length(const char* cstr, Args&& ... rest); +inline std::size_t concat_length(const char* cstr, const Args& ... rest); template -inline std::size_t concat_length(const StringType& str, Args&& ... rest); +inline std::size_t concat_length(const StringType& str, const Args& ... rest); template -inline std::size_t concat_length(const char /*c*/, Args&& ... rest) +inline std::size_t concat_length(const char /*c*/, const Args& ... rest) { - return 1 + concat_length(std::forward(rest)...); + return 1 + concat_length(rest...); } template -inline std::size_t concat_length(const char* cstr, Args&& ... rest) +inline std::size_t concat_length(const char* cstr, const Args& ... rest) { // cppcheck-suppress ignoredReturnValue - return ::strlen(cstr) + concat_length(std::forward(rest)...); + return ::strlen(cstr) + concat_length(rest...); } template -inline std::size_t concat_length(const StringType& str, Args&& ... rest) +inline std::size_t concat_length(const StringType& str, const Args& ... rest) { - return str.size() + concat_length(std::forward(rest)...); + return str.size() + concat_length(rest...); } template @@ -4277,7 +4349,7 @@ template inline OutStringType concat(Args && ... args) { OutStringType str; - str.reserve(concat_length(std::forward(args)...)); + str.reserve(concat_length(args...)); concat_into(str, std::forward(args)...); return str; } @@ -4286,7 +4358,6 @@ inline OutStringType concat(Args && ... args) NLOHMANN_JSON_NAMESPACE_END - NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail { @@ -4334,9 +4405,9 @@ class exception : public std::exception { case value_t::array: { - for (std::size_t i = 0; i < current->m_parent->m_value.array->size(); ++i) + for (std::size_t i = 0; i < current->m_parent->m_data.m_value.array->size(); ++i) { - if (¤t->m_parent->m_value.array->operator[](i) == current) + if (¤t->m_parent->m_data.m_value.array->operator[](i) == current) { tokens.emplace_back(std::to_string(i)); break; @@ -4347,7 +4418,7 @@ class exception : public std::exception case value_t::object: { - for (const auto& element : *current->m_parent->m_value.object) + for (const auto& element : *current->m_parent->m_data.m_value.object) { if (&element.second == current) { @@ -4410,17 +4481,17 @@ class parse_error : public exception template::value, int> = 0> static parse_error create(int id_, const position_t& pos, const std::string& what_arg, BasicJsonContext context) { - std::string w = concat(exception::name("parse_error", id_), "parse error", - position_string(pos), ": ", exception::diagnostics(context), what_arg); + const std::string w = concat(exception::name("parse_error", id_), "parse error", + position_string(pos), ": ", exception::diagnostics(context), what_arg); return {id_, pos.chars_read_total, w.c_str()}; } template::value, int> = 0> static parse_error create(int id_, std::size_t byte_, const std::string& what_arg, BasicJsonContext context) { - std::string w = concat(exception::name("parse_error", id_), "parse error", - (byte_ != 0 ? (concat(" at byte ", std::to_string(byte_))) : ""), - ": ", exception::diagnostics(context), what_arg); + const std::string w = concat(exception::name("parse_error", id_), "parse error", + (byte_ != 0 ? (concat(" at byte ", std::to_string(byte_))) : ""), + ": ", exception::diagnostics(context), what_arg); return {id_, byte_, w.c_str()}; } @@ -4454,7 +4525,7 @@ class invalid_iterator : public exception template::value, int> = 0> static invalid_iterator create(int id_, const std::string& what_arg, BasicJsonContext context) { - std::string w = concat(exception::name("invalid_iterator", id_), exception::diagnostics(context), what_arg); + const std::string w = concat(exception::name("invalid_iterator", id_), exception::diagnostics(context), what_arg); return {id_, w.c_str()}; } @@ -4472,7 +4543,7 @@ class type_error : public exception template::value, int> = 0> static type_error create(int id_, const std::string& what_arg, BasicJsonContext context) { - std::string w = concat(exception::name("type_error", id_), exception::diagnostics(context), what_arg); + const std::string w = concat(exception::name("type_error", id_), exception::diagnostics(context), what_arg); return {id_, w.c_str()}; } @@ -4489,7 +4560,7 @@ class out_of_range : public exception template::value, int> = 0> static out_of_range create(int id_, const std::string& what_arg, BasicJsonContext context) { - std::string w = concat(exception::name("out_of_range", id_), exception::diagnostics(context), what_arg); + const std::string w = concat(exception::name("out_of_range", id_), exception::diagnostics(context), what_arg); return {id_, w.c_str()}; } @@ -4506,7 +4577,7 @@ class other_error : public exception template::value, int> = 0> static other_error create(int id_, const std::string& what_arg, BasicJsonContext context) { - std::string w = concat(exception::name("other_error", id_), exception::diagnostics(context), what_arg); + const std::string w = concat(exception::name("other_error", id_), exception::diagnostics(context), what_arg); return {id_, w.c_str()}; } @@ -4525,10 +4596,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -4549,10 +4620,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -5055,10 +5126,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -5075,10 +5146,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -5147,10 +5218,10 @@ template class iteration_proxy_value // older GCCs are a bit fussy and require explicit noexcept specifiers on defaulted functions iteration_proxy_value(iteration_proxy_value&&) noexcept(std::is_nothrow_move_constructible::value - && std::is_nothrow_move_constructible::value) = default; + && std::is_nothrow_move_constructible::value) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor,cppcoreguidelines-noexcept-move-operations) iteration_proxy_value& operator=(iteration_proxy_value&&) noexcept(std::is_nothrow_move_assignable::value - && std::is_nothrow_move_assignable::value) = default; + && std::is_nothrow_move_assignable::value) = default; // NOLINT(hicpp-noexcept-move,performance-noexcept-move-constructor,cppcoreguidelines-noexcept-move-operations) ~iteration_proxy_value() = default; /// dereference operator (needed for range-based for) @@ -5297,11 +5368,11 @@ namespace std #pragma clang diagnostic ignored "-Wmismatched-tags" #endif template -class tuple_size<::nlohmann::detail::iteration_proxy_value> +class tuple_size<::nlohmann::detail::iteration_proxy_value> // NOLINT(cert-dcl58-cpp) : public std::integral_constant {}; template -class tuple_element> +class tuple_element> // NOLINT(cert-dcl58-cpp) { public: using type = decltype( @@ -5340,7 +5411,7 @@ namespace detail /* * Note all external_constructor<>::construct functions need to call - * j.m_value.destroy(j.m_type) to avoid a memory leak in case j contains an + * j.m_data.m_value.destroy(j.m_data.m_type) to avoid a memory leak in case j contains an * allocated value (e.g., a string). See bug issue * https://github.com/nlohmann/json/issues/2865 for more information. */ @@ -5353,9 +5424,9 @@ struct external_constructor template static void construct(BasicJsonType& j, typename BasicJsonType::boolean_t b) noexcept { - j.m_value.destroy(j.m_type); - j.m_type = value_t::boolean; - j.m_value = b; + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::boolean; + j.m_data.m_value = b; j.assert_invariant(); } }; @@ -5366,18 +5437,18 @@ struct external_constructor template static void construct(BasicJsonType& j, const typename BasicJsonType::string_t& s) { - j.m_value.destroy(j.m_type); - j.m_type = value_t::string; - j.m_value = s; + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::string; + j.m_data.m_value = s; j.assert_invariant(); } template static void construct(BasicJsonType& j, typename BasicJsonType::string_t&& s) { - j.m_value.destroy(j.m_type); - j.m_type = value_t::string; - j.m_value = std::move(s); + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::string; + j.m_data.m_value = std::move(s); j.assert_invariant(); } @@ -5386,9 +5457,9 @@ struct external_constructor int > = 0 > static void construct(BasicJsonType& j, const CompatibleStringType& str) { - j.m_value.destroy(j.m_type); - j.m_type = value_t::string; - j.m_value.string = j.template create(str); + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::string; + j.m_data.m_value.string = j.template create(str); j.assert_invariant(); } }; @@ -5399,18 +5470,18 @@ struct external_constructor template static void construct(BasicJsonType& j, const typename BasicJsonType::binary_t& b) { - j.m_value.destroy(j.m_type); - j.m_type = value_t::binary; - j.m_value = typename BasicJsonType::binary_t(b); + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::binary; + j.m_data.m_value = typename BasicJsonType::binary_t(b); j.assert_invariant(); } template static void construct(BasicJsonType& j, typename BasicJsonType::binary_t&& b) { - j.m_value.destroy(j.m_type); - j.m_type = value_t::binary; - j.m_value = typename BasicJsonType::binary_t(std::move(b)); + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::binary; + j.m_data.m_value = typename BasicJsonType::binary_t(std::move(b)); j.assert_invariant(); } }; @@ -5421,9 +5492,9 @@ struct external_constructor template static void construct(BasicJsonType& j, typename BasicJsonType::number_float_t val) noexcept { - j.m_value.destroy(j.m_type); - j.m_type = value_t::number_float; - j.m_value = val; + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::number_float; + j.m_data.m_value = val; j.assert_invariant(); } }; @@ -5434,9 +5505,9 @@ struct external_constructor template static void construct(BasicJsonType& j, typename BasicJsonType::number_unsigned_t val) noexcept { - j.m_value.destroy(j.m_type); - j.m_type = value_t::number_unsigned; - j.m_value = val; + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::number_unsigned; + j.m_data.m_value = val; j.assert_invariant(); } }; @@ -5447,9 +5518,9 @@ struct external_constructor template static void construct(BasicJsonType& j, typename BasicJsonType::number_integer_t val) noexcept { - j.m_value.destroy(j.m_type); - j.m_type = value_t::number_integer; - j.m_value = val; + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::number_integer; + j.m_data.m_value = val; j.assert_invariant(); } }; @@ -5460,9 +5531,9 @@ struct external_constructor template static void construct(BasicJsonType& j, const typename BasicJsonType::array_t& arr) { - j.m_value.destroy(j.m_type); - j.m_type = value_t::array; - j.m_value = arr; + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::array; + j.m_data.m_value = arr; j.set_parents(); j.assert_invariant(); } @@ -5470,9 +5541,9 @@ struct external_constructor template static void construct(BasicJsonType& j, typename BasicJsonType::array_t&& arr) { - j.m_value.destroy(j.m_type); - j.m_type = value_t::array; - j.m_value = std::move(arr); + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::array; + j.m_data.m_value = std::move(arr); j.set_parents(); j.assert_invariant(); } @@ -5485,9 +5556,9 @@ struct external_constructor using std::begin; using std::end; - j.m_value.destroy(j.m_type); - j.m_type = value_t::array; - j.m_value.array = j.template create(begin(arr), end(arr)); + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::array; + j.m_data.m_value.array = j.template create(begin(arr), end(arr)); j.set_parents(); j.assert_invariant(); } @@ -5495,14 +5566,14 @@ struct external_constructor template static void construct(BasicJsonType& j, const std::vector& arr) { - j.m_value.destroy(j.m_type); - j.m_type = value_t::array; - j.m_value = value_t::array; - j.m_value.array->reserve(arr.size()); + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::array; + j.m_data.m_value = value_t::array; + j.m_data.m_value.array->reserve(arr.size()); for (const bool x : arr) { - j.m_value.array->push_back(x); - j.set_parent(j.m_value.array->back()); + j.m_data.m_value.array->push_back(x); + j.set_parent(j.m_data.m_value.array->back()); } j.assert_invariant(); } @@ -5511,13 +5582,13 @@ struct external_constructor enable_if_t::value, int> = 0> static void construct(BasicJsonType& j, const std::valarray& arr) { - j.m_value.destroy(j.m_type); - j.m_type = value_t::array; - j.m_value = value_t::array; - j.m_value.array->resize(arr.size()); + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::array; + j.m_data.m_value = value_t::array; + j.m_data.m_value.array->resize(arr.size()); if (arr.size() > 0) { - std::copy(std::begin(arr), std::end(arr), j.m_value.array->begin()); + std::copy(std::begin(arr), std::end(arr), j.m_data.m_value.array->begin()); } j.set_parents(); j.assert_invariant(); @@ -5530,9 +5601,9 @@ struct external_constructor template static void construct(BasicJsonType& j, const typename BasicJsonType::object_t& obj) { - j.m_value.destroy(j.m_type); - j.m_type = value_t::object; - j.m_value = obj; + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::object; + j.m_data.m_value = obj; j.set_parents(); j.assert_invariant(); } @@ -5540,9 +5611,9 @@ struct external_constructor template static void construct(BasicJsonType& j, typename BasicJsonType::object_t&& obj) { - j.m_value.destroy(j.m_type); - j.m_type = value_t::object; - j.m_value = std::move(obj); + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::object; + j.m_data.m_value = std::move(obj); j.set_parents(); j.assert_invariant(); } @@ -5554,9 +5625,9 @@ struct external_constructor using std::begin; using std::end; - j.m_value.destroy(j.m_type); - j.m_type = value_t::object; - j.m_value.object = j.template create(begin(obj), end(obj)); + j.m_data.m_value.destroy(j.m_data.m_type); + j.m_data.m_type = value_t::object; + j.m_data.m_value.object = j.template create(begin(obj), end(obj)); j.set_parents(); j.assert_invariant(); } @@ -5796,10 +5867,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -5908,10 +5979,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -6041,10 +6112,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -6067,10 +6138,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -6094,6 +6165,8 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -6140,7 +6213,6 @@ class file_input_adapter std::FILE* m_file; }; - /*! Input adapter for a (caching) istream. Ignores a UFT Byte Order Mark at beginning of input. Does not support changing the underlying std::streambuf @@ -6214,16 +6286,16 @@ class iterator_input_adapter : current(std::move(first)), end(std::move(last)) {} - typename std::char_traits::int_type get_character() + typename char_traits::int_type get_character() { if (JSON_HEDLEY_LIKELY(current != end)) { - auto result = std::char_traits::to_int_type(*current); + auto result = char_traits::to_int_type(*current); std::advance(current, 1); return result; } - return std::char_traits::eof(); + return char_traits::eof(); } private: @@ -6239,7 +6311,6 @@ class iterator_input_adapter } }; - template struct wide_string_input_helper; @@ -6363,7 +6434,7 @@ struct wide_string_input_helper } }; -// Wraps another input apdater to convert wide character types into individual bytes. +// Wraps another input adapter to convert wide character types into individual bytes. template class wide_string_input_adapter { @@ -6408,7 +6479,6 @@ class wide_string_input_adapter std::size_t utf8_bytes_filled = 0; }; - template struct iterator_input_adapter_factory { @@ -6565,10 +6635,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -6710,7 +6780,6 @@ struct json_sax virtual ~json_sax() = default; }; - namespace detail { /*! @@ -6812,7 +6881,7 @@ class json_sax_dom_parser JSON_ASSERT(ref_stack.back()->is_object()); // add null at given key and store the reference for later - object_element = &(ref_stack.back()->m_value.object->operator[](val)); + object_element = &(ref_stack.back()->m_data.m_value.object->operator[](val)); return true; } @@ -6887,8 +6956,8 @@ class json_sax_dom_parser if (ref_stack.back()->is_array()) { - ref_stack.back()->m_value.array->emplace_back(std::forward(v)); - return &(ref_stack.back()->m_value.array->back()); + ref_stack.back()->m_data.m_value.array->emplace_back(std::forward(v)); + return &(ref_stack.back()->m_data.m_value.array->back()); } JSON_ASSERT(ref_stack.back()->is_object()); @@ -7007,7 +7076,7 @@ class json_sax_dom_callback_parser // add discarded value at given key and store the reference for later if (keep && ref_stack.back()) { - object_element = &(ref_stack.back()->m_value.object->operator[](val) = discarded); + object_element = &(ref_stack.back()->m_data.m_value.object->operator[](val) = discarded); } return true; @@ -7092,7 +7161,7 @@ class json_sax_dom_callback_parser // remove discarded value if (!keep && !ref_stack.empty() && ref_stack.back()->is_array()) { - ref_stack.back()->m_value.array->pop_back(); + ref_stack.back()->m_data.m_value.array->pop_back(); } return true; @@ -7159,7 +7228,7 @@ class json_sax_dom_callback_parser if (ref_stack.empty()) { root = std::move(value); - return {true, &root}; + return {true, & root}; } // skip this value if we already decided to skip the parent @@ -7175,8 +7244,8 @@ class json_sax_dom_callback_parser // array if (ref_stack.back()->is_array()) { - ref_stack.back()->m_value.array->emplace_back(std::move(value)); - return {true, &(ref_stack.back()->m_value.array->back())}; + ref_stack.back()->m_data.m_value.array->emplace_back(std::move(value)); + return {true, & (ref_stack.back()->m_data.m_value.array->back())}; } // object @@ -7298,10 +7367,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -7322,6 +7391,8 @@ NLOHMANN_JSON_NAMESPACE_END // #include +// #include + NLOHMANN_JSON_NAMESPACE_BEGIN namespace detail @@ -7416,7 +7487,7 @@ class lexer : public lexer_base using number_float_t = typename BasicJsonType::number_float_t; using string_t = typename BasicJsonType::string_t; using char_type = typename InputAdapterType::char_type; - using char_int_type = typename std::char_traits::int_type; + using char_int_type = typename char_traits::int_type; public: using token_type = typename lexer_base::token_type; @@ -7523,7 +7594,7 @@ class lexer : public lexer_base for (auto range = ranges.begin(); range != ranges.end(); ++range) { get(); - if (JSON_HEDLEY_LIKELY(*range <= current && current <= *(++range))) + if (JSON_HEDLEY_LIKELY(*range <= current && current <= *(++range))) // NOLINT(bugprone-inc-dec-in-conditions) { add(current); } @@ -7566,7 +7637,7 @@ class lexer : public lexer_base switch (get()) { // end of file while parsing string - case std::char_traits::eof(): + case char_traits::eof(): { error_message = "invalid string: missing closing quote"; return token_type::parse_error; @@ -8155,7 +8226,7 @@ class lexer : public lexer_base { case '\n': case '\r': - case std::char_traits::eof(): + case char_traits::eof(): case '\0': return true; @@ -8172,7 +8243,7 @@ class lexer : public lexer_base { switch (get()) { - case std::char_traits::eof(): + case char_traits::eof(): case '\0': { error_message = "invalid comment; missing closing '*/'"; @@ -8601,10 +8672,10 @@ class lexer : public lexer_base token_type scan_literal(const char_type* literal_text, const std::size_t length, token_type return_type) { - JSON_ASSERT(std::char_traits::to_char_type(current) == literal_text[0]); + JSON_ASSERT(char_traits::to_char_type(current) == literal_text[0]); for (std::size_t i = 1; i < length; ++i) { - if (JSON_HEDLEY_UNLIKELY(std::char_traits::to_char_type(get()) != literal_text[i])) + if (JSON_HEDLEY_UNLIKELY(char_traits::to_char_type(get()) != literal_text[i])) { error_message = "invalid literal"; return token_type::parse_error; @@ -8622,7 +8693,7 @@ class lexer : public lexer_base { token_buffer.clear(); token_string.clear(); - token_string.push_back(std::char_traits::to_char_type(current)); + token_string.push_back(char_traits::to_char_type(current)); } /* @@ -8630,7 +8701,7 @@ class lexer : public lexer_base This function provides the interface to the used input adapter. It does not throw in case the input reached EOF, but returns a - `std::char_traits::eof()` in that case. Stores the scanned characters + `char_traits::eof()` in that case. Stores the scanned characters for use in error messages. @return character read from the input @@ -8650,9 +8721,9 @@ class lexer : public lexer_base current = ia.get_character(); } - if (JSON_HEDLEY_LIKELY(current != std::char_traits::eof())) + if (JSON_HEDLEY_LIKELY(current != char_traits::eof())) { - token_string.push_back(std::char_traits::to_char_type(current)); + token_string.push_back(char_traits::to_char_type(current)); } if (current == '\n') @@ -8691,7 +8762,7 @@ class lexer : public lexer_base --position.chars_read_current_line; } - if (JSON_HEDLEY_LIKELY(current != std::char_traits::eof())) + if (JSON_HEDLEY_LIKELY(current != char_traits::eof())) { JSON_ASSERT(!token_string.empty()); token_string.pop_back(); @@ -8885,7 +8956,7 @@ class lexer : public lexer_base // end of input (the null byte is needed when parsing from // string literals) case '\0': - case std::char_traits::eof(): + case char_traits::eof(): return token_type::end_of_input; // error @@ -8903,7 +8974,7 @@ class lexer : public lexer_base const bool ignore_comments = false; /// the current character - char_int_type current = std::char_traits::eof(); + char_int_type current = char_traits::eof(); /// whether the next get() call should just return current bool next_unget = false; @@ -8937,10 +9008,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -9129,7 +9200,6 @@ static inline bool little_endianness(int num = 1) noexcept return *reinterpret_cast(&num) == 1; } - /////////////////// // binary reader // /////////////////// @@ -9147,7 +9217,7 @@ class binary_reader using binary_t = typename BasicJsonType::binary_t; using json_sax_t = SAX; using char_type = typename InputAdapterType::char_type; - using char_int_type = typename std::char_traits::int_type; + using char_int_type = typename char_traits::int_type; public: /*! @@ -9220,7 +9290,7 @@ class binary_reader get(); } - if (JSON_HEDLEY_UNLIKELY(current != std::char_traits::eof())) + if (JSON_HEDLEY_UNLIKELY(current != char_traits::eof())) { return sax->parse_error(chars_read, get_token_string(), parse_error::create(110, chars_read, exception_message(input_format, concat("expected end of input; last byte: 0x", get_token_string()), "value"), nullptr)); @@ -9303,7 +9373,7 @@ class binary_reader exception_message(input_format_t::bson, concat("string length must be at least 1, is ", std::to_string(len)), "string"), nullptr)); } - return get_string(input_format_t::bson, len - static_cast(1), result) && get() != std::char_traits::eof(); + return get_string(input_format_t::bson, len - static_cast(1), result) && get() != char_traits::eof(); } /*! @@ -9404,7 +9474,7 @@ class binary_reader { std::array cr{{}}; static_cast((std::snprintf)(cr.data(), cr.size(), "%.2hhX", static_cast(element_type))); // NOLINT(cppcoreguidelines-pro-type-vararg,hicpp-vararg) - std::string cr_str{cr.data()}; + const std::string cr_str{cr.data()}; return sax->parse_error(element_type_parse_position, cr_str, parse_error::create(114, element_type_parse_position, concat("Unsupported BSON record type 0x", cr_str), nullptr)); } @@ -9497,7 +9567,7 @@ class binary_reader switch (get_char ? get() : current) { // EOF - case std::char_traits::eof(): + case char_traits::eof(): return unexpect_eof(input_format_t::cbor, "value"); // Integer 0x00..0x17 (0..23) @@ -10272,7 +10342,7 @@ class binary_reader switch (get()) { // EOF - case std::char_traits::eof(): + case char_traits::eof(): return unexpect_eof(input_format_t::msgpack, "value"); // positive fixint @@ -11227,7 +11297,7 @@ class binary_reader } if (is_ndarray) // ndarray dimensional vector can only contain integers, and can not embed another array { - return sax->parse_error(chars_read, get_token_string(), parse_error::create(113, chars_read, exception_message(input_format, "ndarray dimentional vector is not allowed", "size"), nullptr)); + return sax->parse_error(chars_read, get_token_string(), parse_error::create(113, chars_read, exception_message(input_format, "ndarray dimensional vector is not allowed", "size"), nullptr)); } std::vector dim; if (JSON_HEDLEY_UNLIKELY(!get_ubjson_ndarray_size(dim))) @@ -11339,7 +11409,7 @@ class binary_reader exception_message(input_format, concat("expected '#' after type information; last byte: 0x", last_token), "size"), nullptr)); } - bool is_error = get_ubjson_size_value(result.first, is_ndarray); + const bool is_error = get_ubjson_size_value(result.first, is_ndarray); if (input_format == input_format_t::bjdata && is_ndarray) { if (inside_ndarray) @@ -11354,7 +11424,7 @@ class binary_reader if (current == '#') { - bool is_error = get_ubjson_size_value(result.first, is_ndarray); + const bool is_error = get_ubjson_size_value(result.first, is_ndarray); if (input_format == input_format_t::bjdata && is_ndarray) { return sax->parse_error(chars_read, get_token_string(), parse_error::create(112, chars_read, @@ -11374,7 +11444,7 @@ class binary_reader { switch (prefix) { - case std::char_traits::eof(): // EOF + case char_traits::eof(): // EOF return unexpect_eof(input_format, "value"); case 'T': // true @@ -11819,7 +11889,7 @@ class binary_reader This function provides the interface to the used input adapter. It does not throw in case the input reached EOF, but returns a -'ve valued - `std::char_traits::eof()` in that case. + `char_traits::eof()` in that case. @return character read from the input */ @@ -11961,7 +12031,7 @@ class binary_reader JSON_HEDLEY_NON_NULL(3) bool unexpect_eof(const input_format_t format, const char* context) const { - if (JSON_HEDLEY_UNLIKELY(current == std::char_traits::eof())) + if (JSON_HEDLEY_UNLIKELY(current == char_traits::eof())) { return sax->parse_error(chars_read, "", parse_error::create(110, chars_read, exception_message(format, "unexpected end of input", context), nullptr)); @@ -12028,7 +12098,7 @@ class binary_reader InputAdapterType ia; /// the current character - char_int_type current = std::char_traits::eof(); + char_int_type current = char_traits::eof(); /// the number of characters read std::size_t chars_read = 0; @@ -12090,10 +12160,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -12439,13 +12509,25 @@ class parser m_lexer.get_token_string(), parse_error::create(101, m_lexer.get_position(), exception_message(token_type::uninitialized, "value"), nullptr)); } + case token_type::end_of_input: + { + if (JSON_HEDLEY_UNLIKELY(m_lexer.get_position().chars_read_total == 1)) + { + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), + "attempting to parse an empty input; check that your input string or stream contains the expected JSON", nullptr)); + } + return sax->parse_error(m_lexer.get_position(), + m_lexer.get_token_string(), + parse_error::create(101, m_lexer.get_position(), exception_message(token_type::literal_or_value, "value"), nullptr)); + } case token_type::uninitialized: case token_type::end_array: case token_type::end_object: case token_type::name_separator: case token_type::value_separator: - case token_type::end_of_input: case token_type::literal_or_value: default: // the last token was unexpected { @@ -12607,10 +12689,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -12620,10 +12702,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -12779,10 +12861,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -12887,7 +12969,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_type) + switch (m_object->m_data.m_type) { case value_t::object: { @@ -12984,17 +13066,17 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_type) + switch (m_object->m_data.m_type) { case value_t::object: { - m_it.object_iterator = m_object->m_value.object->begin(); + m_it.object_iterator = m_object->m_data.m_value.object->begin(); break; } case value_t::array: { - m_it.array_iterator = m_object->m_value.array->begin(); + m_it.array_iterator = m_object->m_data.m_value.array->begin(); break; } @@ -13028,17 +13110,17 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_type) + switch (m_object->m_data.m_type) { case value_t::object: { - m_it.object_iterator = m_object->m_value.object->end(); + m_it.object_iterator = m_object->m_data.m_value.object->end(); break; } case value_t::array: { - m_it.array_iterator = m_object->m_value.array->end(); + m_it.array_iterator = m_object->m_data.m_value.array->end(); break; } @@ -13067,17 +13149,17 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_type) + switch (m_object->m_data.m_type) { case value_t::object: { - JSON_ASSERT(m_it.object_iterator != m_object->m_value.object->end()); + JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end()); return m_it.object_iterator->second; } case value_t::array: { - JSON_ASSERT(m_it.array_iterator != m_object->m_value.array->end()); + JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end()); return *m_it.array_iterator; } @@ -13111,17 +13193,17 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_type) + switch (m_object->m_data.m_type) { case value_t::object: { - JSON_ASSERT(m_it.object_iterator != m_object->m_value.object->end()); + JSON_ASSERT(m_it.object_iterator != m_object->m_data.m_value.object->end()); return &(m_it.object_iterator->second); } case value_t::array: { - JSON_ASSERT(m_it.array_iterator != m_object->m_value.array->end()); + JSON_ASSERT(m_it.array_iterator != m_object->m_data.m_value.array->end()); return &*m_it.array_iterator; } @@ -13164,7 +13246,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_type) + switch (m_object->m_data.m_type) { case value_t::object: { @@ -13215,7 +13297,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_type) + switch (m_object->m_data.m_type) { case value_t::object: { @@ -13262,7 +13344,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci JSON_ASSERT(m_object != nullptr); - switch (m_object->m_type) + switch (m_object->m_data.m_type) { case value_t::object: return (m_it.object_iterator == other.m_it.object_iterator); @@ -13307,7 +13389,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci JSON_ASSERT(m_object != nullptr); - switch (m_object->m_type) + switch (m_object->m_data.m_type) { case value_t::object: JSON_THROW(invalid_iterator::create(213, "cannot compare order of object iterators", m_object)); @@ -13363,7 +13445,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_type) + switch (m_object->m_data.m_type) { case value_t::object: JSON_THROW(invalid_iterator::create(209, "cannot use offsets with object iterators", m_object)); @@ -13442,7 +13524,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_type) + switch (m_object->m_data.m_type) { case value_t::object: JSON_THROW(invalid_iterator::create(209, "cannot use offsets with object iterators", m_object)); @@ -13471,7 +13553,7 @@ class iter_impl // NOLINT(cppcoreguidelines-special-member-functions,hicpp-speci { JSON_ASSERT(m_object != nullptr); - switch (m_object->m_type) + switch (m_object->m_data.m_type) { case value_t::object: JSON_THROW(invalid_iterator::create(208, "cannot use operator[] for object iterators", m_object)); @@ -13541,10 +13623,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -13673,13 +13755,55 @@ NLOHMANN_JSON_NAMESPACE_END // #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 // conditional, is_same + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/*! +@brief Default base class of the @ref basic_json class. + +So that the correct implementations of the copy / move ctors / assign operators +of @ref basic_json do not require complex case distinctions +(no base class / custom base class used as customization point), +@ref basic_json always has a base class. +By default, this class is used because it is empty and thus has no effect +on the behavior of @ref basic_json. +*/ +struct json_default_base {}; + +template +using json_base_class = typename std::conditional < + std::is_same::value, + json_default_base, + T + >::type; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -13911,7 +14035,7 @@ class json_pointer const char* p = s.c_str(); char* p_end = nullptr; errno = 0; // strtoull doesn't reset errno - unsigned long long res = std::strtoull(p, &p_end, 10); // NOLINT(runtime/int) + const unsigned long long res = std::strtoull(p, &p_end, 10); // NOLINT(runtime/int) if (p == p_end // invalid input or empty string || errno == ERANGE // out of range || JSON_HEDLEY_UNLIKELY(static_cast(p_end - p) != s.size())) // incomplete read @@ -14067,7 +14191,7 @@ class json_pointer if (reference_token == "-") { // explicitly treat "-" as index beyond the end - ptr = &ptr->operator[](ptr->m_value.array->size()); + ptr = &ptr->operator[](ptr->m_data.m_value.array->size()); } else { @@ -14119,7 +14243,7 @@ class json_pointer { // "-" always fails the range check JSON_THROW(detail::out_of_range::create(402, detail::concat( - "array index '-' (", std::to_string(ptr->m_value.array->size()), + "array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr)); } @@ -14176,7 +14300,7 @@ class json_pointer if (JSON_HEDLEY_UNLIKELY(reference_token == "-")) { // "-" cannot be used for const access - JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_value.array->size()), ") is out of range"), ptr)); + JSON_THROW(detail::out_of_range::create(402, detail::concat("array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr)); } // use unchecked array access @@ -14226,7 +14350,7 @@ class json_pointer { // "-" always fails the range check JSON_THROW(detail::out_of_range::create(402, detail::concat( - "array index '-' (", std::to_string(ptr->m_value.array->size()), + "array index '-' (", std::to_string(ptr->m_data.m_value.array->size()), ") is out of range"), ptr)); } @@ -14421,7 +14545,7 @@ class json_pointer { case detail::value_t::array: { - if (value.m_value.array->empty()) + if (value.m_data.m_value.array->empty()) { // flatten empty array as null result[reference_string] = nullptr; @@ -14429,10 +14553,10 @@ class json_pointer else { // iterate array and use index as reference string - for (std::size_t i = 0; i < value.m_value.array->size(); ++i) + for (std::size_t i = 0; i < value.m_data.m_value.array->size(); ++i) { flatten(detail::concat(reference_string, '/', std::to_string(i)), - value.m_value.array->operator[](i), result); + value.m_data.m_value.array->operator[](i), result); } } break; @@ -14440,7 +14564,7 @@ class json_pointer case detail::value_t::object: { - if (value.m_value.object->empty()) + if (value.m_data.m_value.object->empty()) { // flatten empty object as null result[reference_string] = nullptr; @@ -14448,7 +14572,7 @@ class json_pointer else { // iterate object and use keys as reference string - for (const auto& element : *value.m_value.object) + for (const auto& element : *value.m_data.m_value.object) { flatten(detail::concat(reference_string, '/', detail::escape(element.first)), element.second, result); } @@ -14495,7 +14619,7 @@ class json_pointer BasicJsonType result; // iterate the JSON object values - for (const auto& element : *value.m_value.object) + for (const auto& element : *value.m_data.m_value.object) { if (JSON_HEDLEY_UNLIKELY(!element.second.is_primitive())) { @@ -14671,10 +14795,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -14763,10 +14887,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -14789,10 +14913,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -14978,7 +15102,7 @@ class binary_writer { case value_t::object: { - write_bson_object(*j.m_value.object); + write_bson_object(*j.m_data.m_value.object); break; } @@ -15013,7 +15137,7 @@ class binary_writer case value_t::boolean: { - oa->write_character(j.m_value.boolean + oa->write_character(j.m_data.m_value.boolean ? to_char_type(0xF5) : to_char_type(0xF4)); break; @@ -15021,42 +15145,42 @@ class binary_writer case value_t::number_integer: { - if (j.m_value.number_integer >= 0) + if (j.m_data.m_value.number_integer >= 0) { // CBOR does not differentiate between positive signed // integers and unsigned integers. Therefore, we used the // code from the value_t::number_unsigned case here. - if (j.m_value.number_integer <= 0x17) + if (j.m_data.m_value.number_integer <= 0x17) { - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_integer <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) { oa->write_character(to_char_type(0x18)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_integer <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) { oa->write_character(to_char_type(0x19)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_integer <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) { oa->write_character(to_char_type(0x1A)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } else { oa->write_character(to_char_type(0x1B)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } } else { // The conversions below encode the sign in the first // byte, and the value is converted to a positive number. - const auto positive_number = -1 - j.m_value.number_integer; - if (j.m_value.number_integer >= -24) + const auto positive_number = -1 - j.m_data.m_value.number_integer; + if (j.m_data.m_value.number_integer >= -24) { write_number(static_cast(0x20 + positive_number)); } @@ -15086,52 +15210,52 @@ class binary_writer case value_t::number_unsigned: { - if (j.m_value.number_unsigned <= 0x17) + if (j.m_data.m_value.number_unsigned <= 0x17) { - write_number(static_cast(j.m_value.number_unsigned)); + write_number(static_cast(j.m_data.m_value.number_unsigned)); } - else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) { oa->write_character(to_char_type(0x18)); - write_number(static_cast(j.m_value.number_unsigned)); + write_number(static_cast(j.m_data.m_value.number_unsigned)); } - else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) { oa->write_character(to_char_type(0x19)); - write_number(static_cast(j.m_value.number_unsigned)); + write_number(static_cast(j.m_data.m_value.number_unsigned)); } - else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) { oa->write_character(to_char_type(0x1A)); - write_number(static_cast(j.m_value.number_unsigned)); + write_number(static_cast(j.m_data.m_value.number_unsigned)); } else { oa->write_character(to_char_type(0x1B)); - write_number(static_cast(j.m_value.number_unsigned)); + write_number(static_cast(j.m_data.m_value.number_unsigned)); } break; } case value_t::number_float: { - if (std::isnan(j.m_value.number_float)) + if (std::isnan(j.m_data.m_value.number_float)) { // NaN is 0xf97e00 in CBOR oa->write_character(to_char_type(0xF9)); oa->write_character(to_char_type(0x7E)); oa->write_character(to_char_type(0x00)); } - else if (std::isinf(j.m_value.number_float)) + else if (std::isinf(j.m_data.m_value.number_float)) { // Infinity is 0xf97c00, -Infinity is 0xf9fc00 oa->write_character(to_char_type(0xf9)); - oa->write_character(j.m_value.number_float > 0 ? to_char_type(0x7C) : to_char_type(0xFC)); + oa->write_character(j.m_data.m_value.number_float > 0 ? to_char_type(0x7C) : to_char_type(0xFC)); oa->write_character(to_char_type(0x00)); } else { - write_compact_float(j.m_value.number_float, detail::input_format_t::cbor); + write_compact_float(j.m_data.m_value.number_float, detail::input_format_t::cbor); } break; } @@ -15139,7 +15263,7 @@ class binary_writer case value_t::string: { // step 1: write control byte and the string length - const auto N = j.m_value.string->size(); + const auto N = j.m_data.m_value.string->size(); if (N <= 0x17) { write_number(static_cast(0x60 + N)); @@ -15169,15 +15293,15 @@ class binary_writer // step 2: write the string oa->write_characters( - reinterpret_cast(j.m_value.string->c_str()), - j.m_value.string->size()); + reinterpret_cast(j.m_data.m_value.string->c_str()), + j.m_data.m_value.string->size()); break; } case value_t::array: { // step 1: write control byte and the array size - const auto N = j.m_value.array->size(); + const auto N = j.m_data.m_value.array->size(); if (N <= 0x17) { write_number(static_cast(0x80 + N)); @@ -15206,7 +15330,7 @@ class binary_writer // LCOV_EXCL_STOP // step 2: write each element - for (const auto& el : *j.m_value.array) + for (const auto& el : *j.m_data.m_value.array) { write_cbor(el); } @@ -15215,32 +15339,32 @@ class binary_writer case value_t::binary: { - if (j.m_value.binary->has_subtype()) + if (j.m_data.m_value.binary->has_subtype()) { - if (j.m_value.binary->subtype() <= (std::numeric_limits::max)()) + if (j.m_data.m_value.binary->subtype() <= (std::numeric_limits::max)()) { write_number(static_cast(0xd8)); - write_number(static_cast(j.m_value.binary->subtype())); + write_number(static_cast(j.m_data.m_value.binary->subtype())); } - else if (j.m_value.binary->subtype() <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.binary->subtype() <= (std::numeric_limits::max)()) { write_number(static_cast(0xd9)); - write_number(static_cast(j.m_value.binary->subtype())); + write_number(static_cast(j.m_data.m_value.binary->subtype())); } - else if (j.m_value.binary->subtype() <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.binary->subtype() <= (std::numeric_limits::max)()) { write_number(static_cast(0xda)); - write_number(static_cast(j.m_value.binary->subtype())); + write_number(static_cast(j.m_data.m_value.binary->subtype())); } - else if (j.m_value.binary->subtype() <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.binary->subtype() <= (std::numeric_limits::max)()) { write_number(static_cast(0xdb)); - write_number(static_cast(j.m_value.binary->subtype())); + write_number(static_cast(j.m_data.m_value.binary->subtype())); } } // step 1: write control byte and the binary array size - const auto N = j.m_value.binary->size(); + const auto N = j.m_data.m_value.binary->size(); if (N <= 0x17) { write_number(static_cast(0x40 + N)); @@ -15270,7 +15394,7 @@ class binary_writer // step 2: write each element oa->write_characters( - reinterpret_cast(j.m_value.binary->data()), + reinterpret_cast(j.m_data.m_value.binary->data()), N); break; @@ -15279,7 +15403,7 @@ class binary_writer case value_t::object: { // step 1: write control byte and the object size - const auto N = j.m_value.object->size(); + const auto N = j.m_data.m_value.object->size(); if (N <= 0x17) { write_number(static_cast(0xA0 + N)); @@ -15308,7 +15432,7 @@ class binary_writer // LCOV_EXCL_STOP // step 2: write each element - for (const auto& el : *j.m_value.object) + for (const auto& el : *j.m_data.m_value.object) { write_cbor(el.first); write_cbor(el.second); @@ -15337,7 +15461,7 @@ class binary_writer case value_t::boolean: // true and false { - oa->write_character(j.m_value.boolean + oa->write_character(j.m_data.m_value.boolean ? to_char_type(0xC3) : to_char_type(0xC2)); break; @@ -15345,75 +15469,75 @@ class binary_writer case value_t::number_integer: { - if (j.m_value.number_integer >= 0) + if (j.m_data.m_value.number_integer >= 0) { // MessagePack does not differentiate between positive // signed integers and unsigned integers. Therefore, we used // the code from the value_t::number_unsigned case here. - if (j.m_value.number_unsigned < 128) + if (j.m_data.m_value.number_unsigned < 128) { // positive fixnum - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) { // uint 8 oa->write_character(to_char_type(0xCC)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) { // uint 16 oa->write_character(to_char_type(0xCD)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) { // uint 32 oa->write_character(to_char_type(0xCE)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) { // uint 64 oa->write_character(to_char_type(0xCF)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } } else { - if (j.m_value.number_integer >= -32) + if (j.m_data.m_value.number_integer >= -32) { // negative fixnum - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_integer >= (std::numeric_limits::min)() && - j.m_value.number_integer <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) { // int 8 oa->write_character(to_char_type(0xD0)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_integer >= (std::numeric_limits::min)() && - j.m_value.number_integer <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) { // int 16 oa->write_character(to_char_type(0xD1)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_integer >= (std::numeric_limits::min)() && - j.m_value.number_integer <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) { // int 32 oa->write_character(to_char_type(0xD2)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_integer >= (std::numeric_limits::min)() && - j.m_value.number_integer <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_integer >= (std::numeric_limits::min)() && + j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) { // int 64 oa->write_character(to_char_type(0xD3)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } } break; @@ -15421,48 +15545,48 @@ class binary_writer case value_t::number_unsigned: { - if (j.m_value.number_unsigned < 128) + if (j.m_data.m_value.number_unsigned < 128) { // positive fixnum - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) { // uint 8 oa->write_character(to_char_type(0xCC)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) { // uint 16 oa->write_character(to_char_type(0xCD)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) { // uint 32 oa->write_character(to_char_type(0xCE)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } - else if (j.m_value.number_unsigned <= (std::numeric_limits::max)()) + else if (j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) { // uint 64 oa->write_character(to_char_type(0xCF)); - write_number(static_cast(j.m_value.number_integer)); + write_number(static_cast(j.m_data.m_value.number_integer)); } break; } case value_t::number_float: { - write_compact_float(j.m_value.number_float, detail::input_format_t::msgpack); + write_compact_float(j.m_data.m_value.number_float, detail::input_format_t::msgpack); break; } case value_t::string: { // step 1: write control byte and the string length - const auto N = j.m_value.string->size(); + const auto N = j.m_data.m_value.string->size(); if (N <= 31) { // fixstr @@ -15489,15 +15613,15 @@ class binary_writer // step 2: write the string oa->write_characters( - reinterpret_cast(j.m_value.string->c_str()), - j.m_value.string->size()); + reinterpret_cast(j.m_data.m_value.string->c_str()), + j.m_data.m_value.string->size()); break; } case value_t::array: { // step 1: write control byte and the array size - const auto N = j.m_value.array->size(); + const auto N = j.m_data.m_value.array->size(); if (N <= 15) { // fixarray @@ -15517,7 +15641,7 @@ class binary_writer } // step 2: write each element - for (const auto& el : *j.m_value.array) + for (const auto& el : *j.m_data.m_value.array) { write_msgpack(el); } @@ -15528,10 +15652,10 @@ class binary_writer { // step 0: determine if the binary type has a set subtype to // determine whether or not to use the ext or fixext types - const bool use_ext = j.m_value.binary->has_subtype(); + const bool use_ext = j.m_data.m_value.binary->has_subtype(); // step 1: write control byte and the byte string length - const auto N = j.m_value.binary->size(); + const auto N = j.m_data.m_value.binary->size(); if (N <= (std::numeric_limits::max)()) { std::uint8_t output_type{}; @@ -15576,18 +15700,18 @@ class binary_writer } else if (N <= (std::numeric_limits::max)()) { - std::uint8_t output_type = use_ext - ? 0xC8 // ext 16 - : 0xC5; // bin 16 + const std::uint8_t output_type = use_ext + ? 0xC8 // ext 16 + : 0xC5; // bin 16 oa->write_character(to_char_type(output_type)); write_number(static_cast(N)); } else if (N <= (std::numeric_limits::max)()) { - std::uint8_t output_type = use_ext - ? 0xC9 // ext 32 - : 0xC6; // bin 32 + const std::uint8_t output_type = use_ext + ? 0xC9 // ext 32 + : 0xC6; // bin 32 oa->write_character(to_char_type(output_type)); write_number(static_cast(N)); @@ -15596,12 +15720,12 @@ class binary_writer // step 1.5: if this is an ext type, write the subtype if (use_ext) { - write_number(static_cast(j.m_value.binary->subtype())); + write_number(static_cast(j.m_data.m_value.binary->subtype())); } // step 2: write the byte string oa->write_characters( - reinterpret_cast(j.m_value.binary->data()), + reinterpret_cast(j.m_data.m_value.binary->data()), N); break; @@ -15610,7 +15734,7 @@ class binary_writer case value_t::object: { // step 1: write control byte and the object size - const auto N = j.m_value.object->size(); + const auto N = j.m_data.m_value.object->size(); if (N <= 15) { // fixmap @@ -15630,7 +15754,7 @@ class binary_writer } // step 2: write each element - for (const auto& el : *j.m_value.object) + for (const auto& el : *j.m_data.m_value.object) { write_msgpack(el.first); write_msgpack(el.second); @@ -15670,7 +15794,7 @@ class binary_writer { if (add_prefix) { - oa->write_character(j.m_value.boolean + oa->write_character(j.m_data.m_value.boolean ? to_char_type('T') : to_char_type('F')); } @@ -15679,19 +15803,19 @@ class binary_writer case value_t::number_integer: { - write_number_with_ubjson_prefix(j.m_value.number_integer, add_prefix, use_bjdata); + write_number_with_ubjson_prefix(j.m_data.m_value.number_integer, add_prefix, use_bjdata); break; } case value_t::number_unsigned: { - write_number_with_ubjson_prefix(j.m_value.number_unsigned, add_prefix, use_bjdata); + write_number_with_ubjson_prefix(j.m_data.m_value.number_unsigned, add_prefix, use_bjdata); break; } case value_t::number_float: { - write_number_with_ubjson_prefix(j.m_value.number_float, add_prefix, use_bjdata); + write_number_with_ubjson_prefix(j.m_data.m_value.number_float, add_prefix, use_bjdata); break; } @@ -15701,10 +15825,10 @@ class binary_writer { oa->write_character(to_char_type('S')); } - write_number_with_ubjson_prefix(j.m_value.string->size(), true, use_bjdata); + write_number_with_ubjson_prefix(j.m_data.m_value.string->size(), true, use_bjdata); oa->write_characters( - reinterpret_cast(j.m_value.string->c_str()), - j.m_value.string->size()); + reinterpret_cast(j.m_data.m_value.string->c_str()), + j.m_data.m_value.string->size()); break; } @@ -15716,7 +15840,7 @@ class binary_writer } bool prefix_required = true; - if (use_type && !j.m_value.array->empty()) + if (use_type && !j.m_data.m_value.array->empty()) { JSON_ASSERT(use_count); const CharType first_prefix = ubjson_prefix(j.front(), use_bjdata); @@ -15739,10 +15863,10 @@ class binary_writer if (use_count) { oa->write_character(to_char_type('#')); - write_number_with_ubjson_prefix(j.m_value.array->size(), true, use_bjdata); + write_number_with_ubjson_prefix(j.m_data.m_value.array->size(), true, use_bjdata); } - for (const auto& el : *j.m_value.array) + for (const auto& el : *j.m_data.m_value.array) { write_ubjson(el, use_count, use_type, prefix_required, use_bjdata); } @@ -15762,7 +15886,7 @@ class binary_writer oa->write_character(to_char_type('[')); } - if (use_type && !j.m_value.binary->empty()) + if (use_type && !j.m_data.m_value.binary->empty()) { JSON_ASSERT(use_count); oa->write_character(to_char_type('$')); @@ -15772,21 +15896,21 @@ class binary_writer if (use_count) { oa->write_character(to_char_type('#')); - write_number_with_ubjson_prefix(j.m_value.binary->size(), true, use_bjdata); + write_number_with_ubjson_prefix(j.m_data.m_value.binary->size(), true, use_bjdata); } if (use_type) { oa->write_characters( - reinterpret_cast(j.m_value.binary->data()), - j.m_value.binary->size()); + reinterpret_cast(j.m_data.m_value.binary->data()), + j.m_data.m_value.binary->size()); } else { - for (size_t i = 0; i < j.m_value.binary->size(); ++i) + for (size_t i = 0; i < j.m_data.m_value.binary->size(); ++i) { oa->write_character(to_char_type('U')); - oa->write_character(j.m_value.binary->data()[i]); + oa->write_character(j.m_data.m_value.binary->data()[i]); } } @@ -15800,9 +15924,9 @@ class binary_writer case value_t::object: { - if (use_bjdata && j.m_value.object->size() == 3 && j.m_value.object->find("_ArrayType_") != j.m_value.object->end() && j.m_value.object->find("_ArraySize_") != j.m_value.object->end() && j.m_value.object->find("_ArrayData_") != j.m_value.object->end()) + if (use_bjdata && j.m_data.m_value.object->size() == 3 && j.m_data.m_value.object->find("_ArrayType_") != j.m_data.m_value.object->end() && j.m_data.m_value.object->find("_ArraySize_") != j.m_data.m_value.object->end() && j.m_data.m_value.object->find("_ArrayData_") != j.m_data.m_value.object->end()) { - if (!write_bjdata_ndarray(*j.m_value.object, use_count, use_type)) // decode bjdata ndarray in the JData format (https://github.com/NeuroJSON/jdata) + if (!write_bjdata_ndarray(*j.m_data.m_value.object, use_count, use_type)) // decode bjdata ndarray in the JData format (https://github.com/NeuroJSON/jdata) { break; } @@ -15814,7 +15938,7 @@ class binary_writer } bool prefix_required = true; - if (use_type && !j.m_value.object->empty()) + if (use_type && !j.m_data.m_value.object->empty()) { JSON_ASSERT(use_count); const CharType first_prefix = ubjson_prefix(j.front(), use_bjdata); @@ -15837,10 +15961,10 @@ class binary_writer if (use_count) { oa->write_character(to_char_type('#')); - write_number_with_ubjson_prefix(j.m_value.object->size(), true, use_bjdata); + write_number_with_ubjson_prefix(j.m_data.m_value.object->size(), true, use_bjdata); } - for (const auto& el : *j.m_value.object) + for (const auto& el : *j.m_data.m_value.object) { write_number_with_ubjson_prefix(el.first.size(), true, use_bjdata); oa->write_characters( @@ -15990,19 +16114,19 @@ class binary_writer void write_bson_unsigned(const string_t& name, const BasicJsonType& j) { - if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) { write_bson_entry_header(name, 0x10 /* int32 */); - write_number(static_cast(j.m_value.number_unsigned), true); + write_number(static_cast(j.m_data.m_value.number_unsigned), true); } - else if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + else if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) { write_bson_entry_header(name, 0x12 /* int64 */); - write_number(static_cast(j.m_value.number_unsigned), true); + write_number(static_cast(j.m_data.m_value.number_unsigned), true); } else { - JSON_THROW(out_of_range::create(407, concat("integer number ", std::to_string(j.m_value.number_unsigned), " cannot be represented by BSON as it does not fit int64"), &j)); + JSON_THROW(out_of_range::create(407, concat("integer number ", std::to_string(j.m_data.m_value.number_unsigned), " cannot be represented by BSON as it does not fit int64"), &j)); } } @@ -16083,13 +16207,13 @@ class binary_writer switch (j.type()) { case value_t::object: - return header_size + calc_bson_object_size(*j.m_value.object); + return header_size + calc_bson_object_size(*j.m_data.m_value.object); case value_t::array: - return header_size + calc_bson_array_size(*j.m_value.array); + return header_size + calc_bson_array_size(*j.m_data.m_value.array); case value_t::binary: - return header_size + calc_bson_binary_size(*j.m_value.binary); + return header_size + calc_bson_binary_size(*j.m_data.m_value.binary); case value_t::boolean: return header_size + 1ul; @@ -16098,13 +16222,13 @@ class binary_writer return header_size + 8ul; case value_t::number_integer: - return header_size + calc_bson_integer_size(j.m_value.number_integer); + return header_size + calc_bson_integer_size(j.m_data.m_value.number_integer); case value_t::number_unsigned: - return header_size + calc_bson_unsigned_size(j.m_value.number_unsigned); + return header_size + calc_bson_unsigned_size(j.m_data.m_value.number_unsigned); case value_t::string: - return header_size + calc_bson_string_size(*j.m_value.string); + return header_size + calc_bson_string_size(*j.m_data.m_value.string); case value_t::null: return header_size + 0ul; @@ -16130,28 +16254,28 @@ class binary_writer switch (j.type()) { case value_t::object: - return write_bson_object_entry(name, *j.m_value.object); + return write_bson_object_entry(name, *j.m_data.m_value.object); case value_t::array: - return write_bson_array(name, *j.m_value.array); + return write_bson_array(name, *j.m_data.m_value.array); case value_t::binary: - return write_bson_binary(name, *j.m_value.binary); + return write_bson_binary(name, *j.m_data.m_value.binary); case value_t::boolean: - return write_bson_boolean(name, j.m_value.boolean); + return write_bson_boolean(name, j.m_data.m_value.boolean); case value_t::number_float: - return write_bson_double(name, j.m_value.number_float); + return write_bson_double(name, j.m_data.m_value.number_float); case value_t::number_integer: - return write_bson_integer(name, j.m_value.number_integer); + return write_bson_integer(name, j.m_data.m_value.number_integer); case value_t::number_unsigned: return write_bson_unsigned(name, j); case value_t::string: - return write_bson_string(name, *j.m_value.string); + return write_bson_string(name, *j.m_data.m_value.string); case value_t::null: return write_bson_null(name); @@ -16173,8 +16297,8 @@ class binary_writer */ static std::size_t calc_bson_object_size(const typename BasicJsonType::object_t& value) { - std::size_t document_size = std::accumulate(value.begin(), value.end(), static_cast(0), - [](size_t result, const typename BasicJsonType::object_t::value_type & el) + const std::size_t document_size = std::accumulate(value.begin(), value.end(), static_cast(0), + [](size_t result, const typename BasicJsonType::object_t::value_type & el) { return result += calc_bson_element_size(el.first, el.second); }); @@ -16424,35 +16548,35 @@ class binary_writer return 'Z'; case value_t::boolean: - return j.m_value.boolean ? 'T' : 'F'; + return j.m_data.m_value.boolean ? 'T' : 'F'; case value_t::number_integer: { - if ((std::numeric_limits::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits::max)()) + if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) { return 'i'; } - if ((std::numeric_limits::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits::max)()) + if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) { return 'U'; } - if ((std::numeric_limits::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits::max)()) + if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) { return 'I'; } - if (use_bjdata && ((std::numeric_limits::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits::max)())) + if (use_bjdata && ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)())) { return 'u'; } - if ((std::numeric_limits::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits::max)()) + if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) { return 'l'; } - if (use_bjdata && ((std::numeric_limits::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits::max)())) + if (use_bjdata && ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)())) { return 'm'; } - if ((std::numeric_limits::min)() <= j.m_value.number_integer && j.m_value.number_integer <= (std::numeric_limits::max)()) + if ((std::numeric_limits::min)() <= j.m_data.m_value.number_integer && j.m_data.m_value.number_integer <= (std::numeric_limits::max)()) { return 'L'; } @@ -16462,35 +16586,35 @@ class binary_writer case value_t::number_unsigned: { - if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) { return 'i'; } - if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) { return 'U'; } - if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) { return 'I'; } - if (use_bjdata && j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + if (use_bjdata && j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) { return 'u'; } - if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) { return 'l'; } - if (use_bjdata && j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + if (use_bjdata && j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) { return 'm'; } - if (j.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) + if (j.m_data.m_value.number_unsigned <= static_cast((std::numeric_limits::max)())) { return 'L'; } - if (use_bjdata && j.m_value.number_unsigned <= (std::numeric_limits::max)()) + if (use_bjdata && j.m_data.m_value.number_unsigned <= (std::numeric_limits::max)()) { return 'M'; } @@ -16499,7 +16623,7 @@ class binary_writer } case value_t::number_float: - return get_ubjson_float_prefix(j.m_value.number_float); + return get_ubjson_float_prefix(j.m_data.m_value.number_float); case value_t::string: return 'S'; @@ -16548,7 +16672,7 @@ class binary_writer std::size_t len = (value.at(key).empty() ? 0 : 1); for (const auto& el : value.at(key)) { - len *= static_cast(el.m_value.number_unsigned); + len *= static_cast(el.m_data.m_value.number_unsigned); } key = "_ArrayData_"; @@ -16570,70 +16694,70 @@ class binary_writer { for (const auto& el : value.at(key)) { - write_number(static_cast(el.m_value.number_unsigned), true); + write_number(static_cast(el.m_data.m_value.number_unsigned), true); } } else if (dtype == 'i') { for (const auto& el : value.at(key)) { - write_number(static_cast(el.m_value.number_integer), true); + write_number(static_cast(el.m_data.m_value.number_integer), true); } } else if (dtype == 'u') { for (const auto& el : value.at(key)) { - write_number(static_cast(el.m_value.number_unsigned), true); + write_number(static_cast(el.m_data.m_value.number_unsigned), true); } } else if (dtype == 'I') { for (const auto& el : value.at(key)) { - write_number(static_cast(el.m_value.number_integer), true); + write_number(static_cast(el.m_data.m_value.number_integer), true); } } else if (dtype == 'm') { for (const auto& el : value.at(key)) { - write_number(static_cast(el.m_value.number_unsigned), true); + write_number(static_cast(el.m_data.m_value.number_unsigned), true); } } else if (dtype == 'l') { for (const auto& el : value.at(key)) { - write_number(static_cast(el.m_value.number_integer), true); + write_number(static_cast(el.m_data.m_value.number_integer), true); } } else if (dtype == 'M') { for (const auto& el : value.at(key)) { - write_number(static_cast(el.m_value.number_unsigned), true); + write_number(static_cast(el.m_data.m_value.number_unsigned), true); } } else if (dtype == 'L') { for (const auto& el : value.at(key)) { - write_number(static_cast(el.m_value.number_integer), true); + write_number(static_cast(el.m_data.m_value.number_integer), true); } } else if (dtype == 'd') { for (const auto& el : value.at(key)) { - write_number(static_cast(el.m_value.number_float), true); + write_number(static_cast(el.m_data.m_value.number_float), true); } } else if (dtype == 'D') { for (const auto& el : value.at(key)) { - write_number(static_cast(el.m_value.number_float), true); + write_number(static_cast(el.m_data.m_value.number_float), true); } } return false; @@ -16757,11 +16881,11 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2008-2009 Björn Hoehrmann -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2008-2009 Björn Hoehrmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -16782,11 +16906,11 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // // SPDX-FileCopyrightText: 2009 Florian Loitsch -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -17692,7 +17816,7 @@ void grisu2(char* buf, int& len, int& decimal_exponent, FloatType value) // NB: If the neighbors are computed for single-precision numbers, there is a single float // (7.0385307e-26f) which can't be recovered using strtod. The resulting double precision // value is off by 1 ulp. -#if 0 +#if 0 // NOLINT(readability-avoid-unconditional-preprocessor-if) const boundaries w = compute_boundaries(static_cast(value)); #else const boundaries w = compute_boundaries(value); @@ -17994,11 +18118,11 @@ class serializer const unsigned int indent_step, const unsigned int current_indent = 0) { - switch (val.m_type) + switch (val.m_data.m_type) { case value_t::object: { - if (val.m_value.object->empty()) + if (val.m_data.m_value.object->empty()) { o->write_characters("{}", 2); return; @@ -18016,8 +18140,8 @@ class serializer } // first n-1 elements - auto i = val.m_value.object->cbegin(); - for (std::size_t cnt = 0; cnt < val.m_value.object->size() - 1; ++cnt, ++i) + auto i = val.m_data.m_value.object->cbegin(); + for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) { o->write_characters(indent_string.c_str(), new_indent); o->write_character('\"'); @@ -18028,8 +18152,8 @@ class serializer } // last element - JSON_ASSERT(i != val.m_value.object->cend()); - JSON_ASSERT(std::next(i) == val.m_value.object->cend()); + JSON_ASSERT(i != val.m_data.m_value.object->cend()); + JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); o->write_characters(indent_string.c_str(), new_indent); o->write_character('\"'); dump_escaped(i->first, ensure_ascii); @@ -18045,8 +18169,8 @@ class serializer o->write_character('{'); // first n-1 elements - auto i = val.m_value.object->cbegin(); - for (std::size_t cnt = 0; cnt < val.m_value.object->size() - 1; ++cnt, ++i) + auto i = val.m_data.m_value.object->cbegin(); + for (std::size_t cnt = 0; cnt < val.m_data.m_value.object->size() - 1; ++cnt, ++i) { o->write_character('\"'); dump_escaped(i->first, ensure_ascii); @@ -18056,8 +18180,8 @@ class serializer } // last element - JSON_ASSERT(i != val.m_value.object->cend()); - JSON_ASSERT(std::next(i) == val.m_value.object->cend()); + JSON_ASSERT(i != val.m_data.m_value.object->cend()); + JSON_ASSERT(std::next(i) == val.m_data.m_value.object->cend()); o->write_character('\"'); dump_escaped(i->first, ensure_ascii); o->write_characters("\":", 2); @@ -18071,7 +18195,7 @@ class serializer case value_t::array: { - if (val.m_value.array->empty()) + if (val.m_data.m_value.array->empty()) { o->write_characters("[]", 2); return; @@ -18089,8 +18213,8 @@ class serializer } // first n-1 elements - for (auto i = val.m_value.array->cbegin(); - i != val.m_value.array->cend() - 1; ++i) + for (auto i = val.m_data.m_value.array->cbegin(); + i != val.m_data.m_value.array->cend() - 1; ++i) { o->write_characters(indent_string.c_str(), new_indent); dump(*i, true, ensure_ascii, indent_step, new_indent); @@ -18098,9 +18222,9 @@ class serializer } // last element - JSON_ASSERT(!val.m_value.array->empty()); + JSON_ASSERT(!val.m_data.m_value.array->empty()); o->write_characters(indent_string.c_str(), new_indent); - dump(val.m_value.array->back(), true, ensure_ascii, indent_step, new_indent); + dump(val.m_data.m_value.array->back(), true, ensure_ascii, indent_step, new_indent); o->write_character('\n'); o->write_characters(indent_string.c_str(), current_indent); @@ -18111,16 +18235,16 @@ class serializer o->write_character('['); // first n-1 elements - for (auto i = val.m_value.array->cbegin(); - i != val.m_value.array->cend() - 1; ++i) + for (auto i = val.m_data.m_value.array->cbegin(); + i != val.m_data.m_value.array->cend() - 1; ++i) { dump(*i, false, ensure_ascii, indent_step, current_indent); o->write_character(','); } // last element - JSON_ASSERT(!val.m_value.array->empty()); - dump(val.m_value.array->back(), false, ensure_ascii, indent_step, current_indent); + JSON_ASSERT(!val.m_data.m_value.array->empty()); + dump(val.m_data.m_value.array->back(), false, ensure_ascii, indent_step, current_indent); o->write_character(']'); } @@ -18131,7 +18255,7 @@ class serializer case value_t::string: { o->write_character('\"'); - dump_escaped(*val.m_value.string, ensure_ascii); + dump_escaped(*val.m_data.m_value.string, ensure_ascii); o->write_character('\"'); return; } @@ -18153,24 +18277,24 @@ class serializer o->write_characters("\"bytes\": [", 10); - if (!val.m_value.binary->empty()) + if (!val.m_data.m_value.binary->empty()) { - for (auto i = val.m_value.binary->cbegin(); - i != val.m_value.binary->cend() - 1; ++i) + for (auto i = val.m_data.m_value.binary->cbegin(); + i != val.m_data.m_value.binary->cend() - 1; ++i) { dump_integer(*i); o->write_characters(", ", 2); } - dump_integer(val.m_value.binary->back()); + dump_integer(val.m_data.m_value.binary->back()); } o->write_characters("],\n", 3); o->write_characters(indent_string.c_str(), new_indent); o->write_characters("\"subtype\": ", 11); - if (val.m_value.binary->has_subtype()) + if (val.m_data.m_value.binary->has_subtype()) { - dump_integer(val.m_value.binary->subtype()); + dump_integer(val.m_data.m_value.binary->subtype()); } else { @@ -18184,21 +18308,21 @@ class serializer { o->write_characters("{\"bytes\":[", 10); - if (!val.m_value.binary->empty()) + if (!val.m_data.m_value.binary->empty()) { - for (auto i = val.m_value.binary->cbegin(); - i != val.m_value.binary->cend() - 1; ++i) + for (auto i = val.m_data.m_value.binary->cbegin(); + i != val.m_data.m_value.binary->cend() - 1; ++i) { dump_integer(*i); o->write_character(','); } - dump_integer(val.m_value.binary->back()); + dump_integer(val.m_data.m_value.binary->back()); } o->write_characters("],\"subtype\":", 12); - if (val.m_value.binary->has_subtype()) + if (val.m_data.m_value.binary->has_subtype()) { - dump_integer(val.m_value.binary->subtype()); + dump_integer(val.m_data.m_value.binary->subtype()); o->write_character('}'); } else @@ -18211,7 +18335,7 @@ class serializer case value_t::boolean: { - if (val.m_value.boolean) + if (val.m_data.m_value.boolean) { o->write_characters("true", 4); } @@ -18224,19 +18348,19 @@ class serializer case value_t::number_integer: { - dump_integer(val.m_value.number_integer); + dump_integer(val.m_data.m_value.number_integer); return; } case value_t::number_unsigned: { - dump_integer(val.m_value.number_unsigned); + dump_integer(val.m_data.m_value.number_unsigned); return; } case value_t::number_float: { - dump_float(val.m_value.number_float); + dump_float(val.m_data.m_value.number_float); return; } @@ -18810,8 +18934,8 @@ class serializer ? (byte & 0x3fu) | (codep << 6u) : (0xFFu >> type) & (byte); - std::size_t index = 256u + static_cast(state) * 16u + static_cast(type); - JSON_ASSERT(index < 400); + const std::size_t index = 256u + static_cast(state) * 16u + static_cast(type); + JSON_ASSERT(index < utf8d.size()); state = utf8d[index]; return state; } @@ -18878,10 +19002,10 @@ NLOHMANN_JSON_NAMESPACE_END // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -18998,7 +19122,7 @@ template , template::value, int> = 0> - T & at(KeyType && key) + T & at(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) { for (auto it = this->begin(); it != this->end(); ++it) { @@ -19026,7 +19150,7 @@ template , template::value, int> = 0> - const T & at(KeyType && key) const + const T & at(KeyType && key) const // NOLINT(cppcoreguidelines-missing-std-forward) { for (auto it = this->begin(); it != this->end(); ++it) { @@ -19060,7 +19184,7 @@ template , template::value, int> = 0> - size_type erase(KeyType && key) + size_type erase(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) { for (auto it = this->begin(); it != this->end(); ++it) { @@ -19151,7 +19275,7 @@ template , template::value, int> = 0> - size_type count(KeyType && key) const + size_type count(KeyType && key) const // NOLINT(cppcoreguidelines-missing-std-forward) { for (auto it = this->begin(); it != this->end(); ++it) { @@ -19177,7 +19301,7 @@ template , template::value, int> = 0> - iterator find(KeyType && key) + iterator find(KeyType && key) // NOLINT(cppcoreguidelines-missing-std-forward) { for (auto it = this->begin(); it != this->end(); ++it) { @@ -19240,7 +19364,9 @@ NLOHMANN_JSON_NAMESPACE_END #if defined(JSON_HAS_CPP_17) - #include + #if JSON_HAS_STATIC_RTTI + #include + #endif #include #endif @@ -19271,6 +19397,7 @@ The invariants are checked by member function assert_invariant(). */ NLOHMANN_BASIC_JSON_TPL_DECLARATION class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-special-member-functions) + : public ::nlohmann::detail::json_base_class { private: template friend struct detail::external_constructor; @@ -19297,6 +19424,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// workaround type for MSVC using basic_json_t = NLOHMANN_BASIC_JSON_TPL; + using json_base_class_t = ::nlohmann::detail::json_base_class; JSON_PRIVATE_UNLESS_TESTED: // convenience aliases for types residing in namespace detail; @@ -19368,7 +19496,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @} - ///////////////////// // container types // ///////////////////// @@ -19410,7 +19537,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @} - /// @brief returns the allocator associated with the container /// @sa https://json.nlohmann.me/api/basic_json/get_allocator/ static allocator_type get_allocator() @@ -19425,7 +19551,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec { basic_json result; - result["copyright"] = "(C) 2013-2022 Niels Lohmann"; + result["copyright"] = "(C) 2013-2023 Niels Lohmann"; result["name"] = "JSON for Modern C++"; result["url"] = "https://github.com/nlohmann/json"; result["version"]["string"] = @@ -19473,7 +19599,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec result["compiler"] = {{"family", "unknown"}, {"version", "unknown"}}; #endif - #if defined(_MSVC_LANG) result["compiler"]["c++"] = std::to_string(_MSVC_LANG); #elif defined(__cplusplus) @@ -19484,7 +19609,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return result; } - /////////////////////////// // JSON value data types // /////////////////////////// @@ -19692,7 +19816,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec object = nullptr; // silence warning, see #821 if (JSON_HEDLEY_UNLIKELY(t == value_t::null)) { - JSON_THROW(other_error::create(500, "961c151d2e87f2686a955a9be24d316f1362bf21 3.11.2", nullptr)); // LCOV_EXCL_LINE + JSON_THROW(other_error::create(500, "961c151d2e87f2686a955a9be24d316f1362bf21 3.11.3", nullptr)); // LCOV_EXCL_LINE } break; } @@ -19731,6 +19855,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec void destroy(value_t t) { + if ( + (t == value_t::object && object == nullptr) || + (t == value_t::array && array == nullptr) || + (t == value_t::string && string == nullptr) || + (t == value_t::binary && binary == nullptr) + ) + { + //not initialized (e.g. due to exception in the ctor) + return; + } if (t == value_t::array || t == value_t::object) { // flatten the current json_value to a heap-allocated stack @@ -19761,18 +19895,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // its children to the stack to be processed later if (current_item.is_array()) { - std::move(current_item.m_value.array->begin(), current_item.m_value.array->end(), std::back_inserter(stack)); + std::move(current_item.m_data.m_value.array->begin(), current_item.m_data.m_value.array->end(), std::back_inserter(stack)); - current_item.m_value.array->clear(); + current_item.m_data.m_value.array->clear(); } else if (current_item.is_object()) { - for (auto&& it : *current_item.m_value.object) + for (auto&& it : *current_item.m_data.m_value.object) { stack.push_back(std::move(it.second)); } - current_item.m_value.object->clear(); + current_item.m_data.m_value.object->clear(); } // it's now safe that current_item get destructed @@ -19849,10 +19983,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec */ void assert_invariant(bool check_parents = true) const noexcept { - JSON_ASSERT(m_type != value_t::object || m_value.object != nullptr); - JSON_ASSERT(m_type != value_t::array || m_value.array != nullptr); - JSON_ASSERT(m_type != value_t::string || m_value.string != nullptr); - JSON_ASSERT(m_type != value_t::binary || m_value.binary != nullptr); + JSON_ASSERT(m_data.m_type != value_t::object || m_data.m_value.object != nullptr); + JSON_ASSERT(m_data.m_type != value_t::array || m_data.m_value.array != nullptr); + JSON_ASSERT(m_data.m_type != value_t::string || m_data.m_value.string != nullptr); + JSON_ASSERT(m_data.m_type != value_t::binary || m_data.m_value.binary != nullptr); #if JSON_DIAGNOSTICS JSON_TRY @@ -19871,11 +20005,11 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec void set_parents() { #if JSON_DIAGNOSTICS - switch (m_type) + switch (m_data.m_type) { case value_t::array: { - for (auto& element : *m_value.array) + for (auto& element : *m_data.m_value.array) { element.m_parent = this; } @@ -19884,7 +20018,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec case value_t::object: { - for (auto& element : *m_value.object) + for (auto& element : *m_data.m_value.object) { element.second.m_parent = this; } @@ -19925,7 +20059,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec { // see https://github.com/nlohmann/json/issues/2838 JSON_ASSERT(type() == value_t::array); - if (JSON_HEDLEY_UNLIKELY(m_value.array->capacity() != old_capacity)) + if (JSON_HEDLEY_UNLIKELY(m_data.m_value.array->capacity() != old_capacity)) { // capacity has changed: update all parents set_parents(); @@ -19981,7 +20115,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @brief create an empty value with a given type /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ basic_json(const value_t v) - : m_type(v), m_value(v) + : m_data(v) { assert_invariant(); } @@ -20055,12 +20189,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec *this = nullptr; break; case value_t::discarded: - m_type = value_t::discarded; + m_data.m_type = value_t::discarded; break; default: // LCOV_EXCL_LINE JSON_ASSERT(false); // NOLINT(cert-dcl03-c,hicpp-static-assert,misc-static-assert) LCOV_EXCL_LINE } - JSON_ASSERT(m_type == val.type()); + JSON_ASSERT(m_data.m_type == val.type()); set_parents(); assert_invariant(); } @@ -20076,7 +20210,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec bool is_an_object = std::all_of(init.begin(), init.end(), [](const detail::json_ref& element_ref) { - return element_ref->is_array() && element_ref->size() == 2 && (*element_ref)[0].is_string(); + // The cast is to ensure op[size_type] is called, bearing in mind size_type may not be int; + // (many string types can be constructed from 0 via its null-pointer guise, so we get a + // broken call to op[key_type], the wrong semantics and a 4804 warning on Windows) + return element_ref->is_array() && element_ref->size() == 2 && (*element_ref)[static_cast(0)].is_string(); }); // adjust type if type deduction is not wanted @@ -20098,22 +20235,22 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_an_object) { // the initializer list is a list of pairs -> create object - m_type = value_t::object; - m_value = value_t::object; + m_data.m_type = value_t::object; + m_data.m_value = value_t::object; for (auto& element_ref : init) { auto element = element_ref.moved_or_copied(); - m_value.object->emplace( - std::move(*((*element.m_value.array)[0].m_value.string)), - std::move((*element.m_value.array)[1])); + m_data.m_value.object->emplace( + std::move(*((*element.m_data.m_value.array)[0].m_data.m_value.string)), + std::move((*element.m_data.m_value.array)[1])); } } else { // the initializer list describes an array -> create array - m_type = value_t::array; - m_value.array = create(init.begin(), init.end()); + m_data.m_type = value_t::array; + m_data.m_value.array = create(init.begin(), init.end()); } set_parents(); @@ -20126,8 +20263,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json binary(const typename binary_t::container_type& init) { auto res = basic_json(); - res.m_type = value_t::binary; - res.m_value = init; + res.m_data.m_type = value_t::binary; + res.m_data.m_value = init; return res; } @@ -20137,8 +20274,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json binary(const typename binary_t::container_type& init, typename binary_t::subtype_type subtype) { auto res = basic_json(); - res.m_type = value_t::binary; - res.m_value = binary_t(init, subtype); + res.m_data.m_type = value_t::binary; + res.m_data.m_value = binary_t(init, subtype); return res; } @@ -20148,8 +20285,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json binary(typename binary_t::container_type&& init) { auto res = basic_json(); - res.m_type = value_t::binary; - res.m_value = std::move(init); + res.m_data.m_type = value_t::binary; + res.m_data.m_value = std::move(init); return res; } @@ -20159,8 +20296,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec static basic_json binary(typename binary_t::container_type&& init, typename binary_t::subtype_type subtype) { auto res = basic_json(); - res.m_type = value_t::binary; - res.m_value = binary_t(std::move(init), subtype); + res.m_data.m_type = value_t::binary; + res.m_data.m_value = binary_t(std::move(init), subtype); return res; } @@ -20182,10 +20319,9 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @brief construct an array with count copies of given value /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ - basic_json(size_type cnt, const basic_json& val) - : m_type(value_t::array) + basic_json(size_type cnt, const basic_json& val): + m_data{cnt, val} { - m_value.array = create(cnt, val); set_parents(); assert_invariant(); } @@ -20207,10 +20343,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } // copy type from first iterator - m_type = first.m_object->m_type; + m_data.m_type = first.m_object->m_data.m_type; // check if iterator range is complete for primitive values - switch (m_type) + switch (m_data.m_type) { case value_t::boolean: case value_t::number_float: @@ -20235,55 +20371,55 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec break; } - switch (m_type) + switch (m_data.m_type) { case value_t::number_integer: { - m_value.number_integer = first.m_object->m_value.number_integer; + m_data.m_value.number_integer = first.m_object->m_data.m_value.number_integer; break; } case value_t::number_unsigned: { - m_value.number_unsigned = first.m_object->m_value.number_unsigned; + m_data.m_value.number_unsigned = first.m_object->m_data.m_value.number_unsigned; break; } case value_t::number_float: { - m_value.number_float = first.m_object->m_value.number_float; + m_data.m_value.number_float = first.m_object->m_data.m_value.number_float; break; } case value_t::boolean: { - m_value.boolean = first.m_object->m_value.boolean; + m_data.m_value.boolean = first.m_object->m_data.m_value.boolean; break; } case value_t::string: { - m_value = *first.m_object->m_value.string; + m_data.m_value = *first.m_object->m_data.m_value.string; break; } case value_t::object: { - m_value.object = create(first.m_it.object_iterator, - last.m_it.object_iterator); + m_data.m_value.object = create(first.m_it.object_iterator, + last.m_it.object_iterator); break; } case value_t::array: { - m_value.array = create(first.m_it.array_iterator, - last.m_it.array_iterator); + m_data.m_value.array = create(first.m_it.array_iterator, + last.m_it.array_iterator); break; } case value_t::binary: { - m_value = *first.m_object->m_value.binary; + m_data.m_value = *first.m_object->m_data.m_value.binary; break; } @@ -20297,7 +20433,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec assert_invariant(); } - /////////////////////////////////////// // other constructors and destructor // /////////////////////////////////////// @@ -20310,58 +20445,59 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @brief copy constructor /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ basic_json(const basic_json& other) - : m_type(other.m_type) + : json_base_class_t(other) { + m_data.m_type = other.m_data.m_type; // check of passed value is valid other.assert_invariant(); - switch (m_type) + switch (m_data.m_type) { case value_t::object: { - m_value = *other.m_value.object; + m_data.m_value = *other.m_data.m_value.object; break; } case value_t::array: { - m_value = *other.m_value.array; + m_data.m_value = *other.m_data.m_value.array; break; } case value_t::string: { - m_value = *other.m_value.string; + m_data.m_value = *other.m_data.m_value.string; break; } case value_t::boolean: { - m_value = other.m_value.boolean; + m_data.m_value = other.m_data.m_value.boolean; break; } case value_t::number_integer: { - m_value = other.m_value.number_integer; + m_data.m_value = other.m_data.m_value.number_integer; break; } case value_t::number_unsigned: { - m_value = other.m_value.number_unsigned; + m_data.m_value = other.m_data.m_value.number_unsigned; break; } case value_t::number_float: { - m_value = other.m_value.number_float; + m_data.m_value = other.m_data.m_value.number_float; break; } case value_t::binary: { - m_value = *other.m_value.binary; + m_data.m_value = *other.m_data.m_value.binary; break; } @@ -20378,15 +20514,15 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @brief move constructor /// @sa https://json.nlohmann.me/api/basic_json/basic_json/ basic_json(basic_json&& other) noexcept - : m_type(std::move(other.m_type)), - m_value(std::move(other.m_value)) + : json_base_class_t(std::forward(other)), + m_data(std::move(other.m_data)) { // check that passed value is valid other.assert_invariant(false); // invalidate payload - other.m_type = value_t::null; - other.m_value = {}; + other.m_data.m_type = value_t::null; + other.m_data.m_value = {}; set_parents(); assert_invariant(); @@ -20398,15 +20534,17 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec std::is_nothrow_move_constructible::value&& std::is_nothrow_move_assignable::value&& std::is_nothrow_move_constructible::value&& - std::is_nothrow_move_assignable::value + std::is_nothrow_move_assignable::value&& + std::is_nothrow_move_assignable::value ) { // check that passed value is valid other.assert_invariant(); using std::swap; - swap(m_type, other.m_type); - swap(m_value, other.m_value); + swap(m_data.m_type, other.m_data.m_type); + swap(m_data.m_value, other.m_data.m_value); + json_base_class_t::operator=(std::move(other)); set_parents(); assert_invariant(); @@ -20418,7 +20556,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec ~basic_json() noexcept { assert_invariant(false); - m_value.destroy(m_type); } /// @} @@ -20458,7 +20595,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/type/ constexpr value_t type() const noexcept { - return m_type; + return m_data.m_type; } /// @brief return whether type is primitive @@ -20479,14 +20616,14 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/is_null/ constexpr bool is_null() const noexcept { - return m_type == value_t::null; + return m_data.m_type == value_t::null; } /// @brief return whether value is a boolean /// @sa https://json.nlohmann.me/api/basic_json/is_boolean/ constexpr bool is_boolean() const noexcept { - return m_type == value_t::boolean; + return m_data.m_type == value_t::boolean; } /// @brief return whether value is a number @@ -20500,63 +20637,63 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/is_number_integer/ constexpr bool is_number_integer() const noexcept { - return m_type == value_t::number_integer || m_type == value_t::number_unsigned; + return m_data.m_type == value_t::number_integer || m_data.m_type == value_t::number_unsigned; } /// @brief return whether value is an unsigned integer number /// @sa https://json.nlohmann.me/api/basic_json/is_number_unsigned/ constexpr bool is_number_unsigned() const noexcept { - return m_type == value_t::number_unsigned; + return m_data.m_type == value_t::number_unsigned; } /// @brief return whether value is a floating-point number /// @sa https://json.nlohmann.me/api/basic_json/is_number_float/ constexpr bool is_number_float() const noexcept { - return m_type == value_t::number_float; + return m_data.m_type == value_t::number_float; } /// @brief return whether value is an object /// @sa https://json.nlohmann.me/api/basic_json/is_object/ constexpr bool is_object() const noexcept { - return m_type == value_t::object; + return m_data.m_type == value_t::object; } /// @brief return whether value is an array /// @sa https://json.nlohmann.me/api/basic_json/is_array/ constexpr bool is_array() const noexcept { - return m_type == value_t::array; + return m_data.m_type == value_t::array; } /// @brief return whether value is a string /// @sa https://json.nlohmann.me/api/basic_json/is_string/ constexpr bool is_string() const noexcept { - return m_type == value_t::string; + return m_data.m_type == value_t::string; } /// @brief return whether value is a binary array /// @sa https://json.nlohmann.me/api/basic_json/is_binary/ constexpr bool is_binary() const noexcept { - return m_type == value_t::binary; + return m_data.m_type == value_t::binary; } /// @brief return whether value is discarded /// @sa https://json.nlohmann.me/api/basic_json/is_discarded/ constexpr bool is_discarded() const noexcept { - return m_type == value_t::discarded; + return m_data.m_type == value_t::discarded; } /// @brief return the type of the JSON value (implicit) /// @sa https://json.nlohmann.me/api/basic_json/operator_value_t/ constexpr operator value_t() const noexcept { - return m_type; + return m_data.m_type; } /// @} @@ -20571,7 +20708,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec { if (JSON_HEDLEY_LIKELY(is_boolean())) { - return m_value.boolean; + return m_data.m_value.boolean; } JSON_THROW(type_error::create(302, detail::concat("type must be boolean, but is ", type_name()), this)); @@ -20580,97 +20717,97 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// get a pointer to the value (object) object_t* get_impl_ptr(object_t* /*unused*/) noexcept { - return is_object() ? m_value.object : nullptr; + return is_object() ? m_data.m_value.object : nullptr; } /// get a pointer to the value (object) constexpr const object_t* get_impl_ptr(const object_t* /*unused*/) const noexcept { - return is_object() ? m_value.object : nullptr; + return is_object() ? m_data.m_value.object : nullptr; } /// get a pointer to the value (array) array_t* get_impl_ptr(array_t* /*unused*/) noexcept { - return is_array() ? m_value.array : nullptr; + return is_array() ? m_data.m_value.array : nullptr; } /// get a pointer to the value (array) constexpr const array_t* get_impl_ptr(const array_t* /*unused*/) const noexcept { - return is_array() ? m_value.array : nullptr; + return is_array() ? m_data.m_value.array : nullptr; } /// get a pointer to the value (string) string_t* get_impl_ptr(string_t* /*unused*/) noexcept { - return is_string() ? m_value.string : nullptr; + return is_string() ? m_data.m_value.string : nullptr; } /// get a pointer to the value (string) constexpr const string_t* get_impl_ptr(const string_t* /*unused*/) const noexcept { - return is_string() ? m_value.string : nullptr; + return is_string() ? m_data.m_value.string : nullptr; } /// get a pointer to the value (boolean) boolean_t* get_impl_ptr(boolean_t* /*unused*/) noexcept { - return is_boolean() ? &m_value.boolean : nullptr; + return is_boolean() ? &m_data.m_value.boolean : nullptr; } /// get a pointer to the value (boolean) constexpr const boolean_t* get_impl_ptr(const boolean_t* /*unused*/) const noexcept { - return is_boolean() ? &m_value.boolean : nullptr; + return is_boolean() ? &m_data.m_value.boolean : nullptr; } /// get a pointer to the value (integer number) number_integer_t* get_impl_ptr(number_integer_t* /*unused*/) noexcept { - return is_number_integer() ? &m_value.number_integer : nullptr; + return is_number_integer() ? &m_data.m_value.number_integer : nullptr; } /// get a pointer to the value (integer number) constexpr const number_integer_t* get_impl_ptr(const number_integer_t* /*unused*/) const noexcept { - return is_number_integer() ? &m_value.number_integer : nullptr; + return is_number_integer() ? &m_data.m_value.number_integer : nullptr; } /// get a pointer to the value (unsigned number) number_unsigned_t* get_impl_ptr(number_unsigned_t* /*unused*/) noexcept { - return is_number_unsigned() ? &m_value.number_unsigned : nullptr; + return is_number_unsigned() ? &m_data.m_value.number_unsigned : nullptr; } /// get a pointer to the value (unsigned number) constexpr const number_unsigned_t* get_impl_ptr(const number_unsigned_t* /*unused*/) const noexcept { - return is_number_unsigned() ? &m_value.number_unsigned : nullptr; + return is_number_unsigned() ? &m_data.m_value.number_unsigned : nullptr; } /// get a pointer to the value (floating-point number) number_float_t* get_impl_ptr(number_float_t* /*unused*/) noexcept { - return is_number_float() ? &m_value.number_float : nullptr; + return is_number_float() ? &m_data.m_value.number_float : nullptr; } /// get a pointer to the value (floating-point number) constexpr const number_float_t* get_impl_ptr(const number_float_t* /*unused*/) const noexcept { - return is_number_float() ? &m_value.number_float : nullptr; + return is_number_float() ? &m_data.m_value.number_float : nullptr; } /// get a pointer to the value (binary) binary_t* get_impl_ptr(binary_t* /*unused*/) noexcept { - return is_binary() ? m_value.binary : nullptr; + return is_binary() ? m_data.m_value.binary : nullptr; } /// get a pointer to the value (binary) constexpr const binary_t* get_impl_ptr(const binary_t* /*unused*/) const noexcept { - return is_binary() ? m_value.binary : nullptr; + return is_binary() ? m_data.m_value.binary : nullptr; } /*! @@ -21053,7 +21190,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #if defined(JSON_HAS_CPP_17) && (defined(__GNUC__) || (defined(_MSC_VER) && _MSC_VER >= 1910 && _MSC_VER <= 1914)) detail::negation>, #endif -#if defined(JSON_HAS_CPP_17) +#if defined(JSON_HAS_CPP_17) && JSON_HAS_STATIC_RTTI detail::negation>, #endif detail::is_detected_lazy @@ -21090,7 +21227,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @} - //////////////////// // element access // //////////////////// @@ -21108,7 +21244,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec { JSON_TRY { - return set_parent(m_value.array->at(idx)); + return set_parent(m_data.m_value.array->at(idx)); } JSON_CATCH (std::out_of_range&) { @@ -21131,7 +21267,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec { JSON_TRY { - return m_value.array->at(idx); + return m_data.m_value.array->at(idx); } JSON_CATCH (std::out_of_range&) { @@ -21155,8 +21291,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this)); } - auto it = m_value.object->find(key); - if (it == m_value.object->end()) + auto it = m_data.m_value.object->find(key); + if (it == m_data.m_value.object->end()) { JSON_THROW(out_of_range::create(403, detail::concat("key '", key, "' not found"), this)); } @@ -21175,8 +21311,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this)); } - auto it = m_value.object->find(std::forward(key)); - if (it == m_value.object->end()) + auto it = m_data.m_value.object->find(std::forward(key)); + if (it == m_data.m_value.object->end()) { JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward(key)), "' not found"), this)); } @@ -21193,8 +21329,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this)); } - auto it = m_value.object->find(key); - if (it == m_value.object->end()) + auto it = m_data.m_value.object->find(key); + if (it == m_data.m_value.object->end()) { JSON_THROW(out_of_range::create(403, detail::concat("key '", key, "' not found"), this)); } @@ -21213,8 +21349,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(304, detail::concat("cannot use at() with ", type_name()), this)); } - auto it = m_value.object->find(std::forward(key)); - if (it == m_value.object->end()) + auto it = m_data.m_value.object->find(std::forward(key)); + if (it == m_data.m_value.object->end()) { JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(std::forward(key)), "' not found"), this)); } @@ -21228,8 +21364,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // implicitly convert null value to an empty array if (is_null()) { - m_type = value_t::array; - m_value.array = create(); + m_data.m_type = value_t::array; + m_data.m_value.array = create(); assert_invariant(); } @@ -21237,17 +21373,17 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (JSON_HEDLEY_LIKELY(is_array())) { // fill up array with null values if given idx is outside range - if (idx >= m_value.array->size()) + if (idx >= m_data.m_value.array->size()) { #if JSON_DIAGNOSTICS // remember array size & capacity before resizing - const auto old_size = m_value.array->size(); - const auto old_capacity = m_value.array->capacity(); + const auto old_size = m_data.m_value.array->size(); + const auto old_capacity = m_data.m_value.array->capacity(); #endif - m_value.array->resize(idx + 1); + m_data.m_value.array->resize(idx + 1); #if JSON_DIAGNOSTICS - if (JSON_HEDLEY_UNLIKELY(m_value.array->capacity() != old_capacity)) + if (JSON_HEDLEY_UNLIKELY(m_data.m_value.array->capacity() != old_capacity)) { // capacity has changed: update all parents set_parents(); @@ -21261,7 +21397,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec assert_invariant(); } - return m_value.array->operator[](idx); + return m_data.m_value.array->operator[](idx); } JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a numeric argument with ", type_name()), this)); @@ -21274,7 +21410,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // const operator[] only works for arrays if (JSON_HEDLEY_LIKELY(is_array())) { - return m_value.array->operator[](idx); + return m_data.m_value.array->operator[](idx); } JSON_THROW(type_error::create(305, detail::concat("cannot use operator[] with a numeric argument with ", type_name()), this)); @@ -21287,15 +21423,15 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // implicitly convert null value to an empty object if (is_null()) { - m_type = value_t::object; - m_value.object = create(); + m_data.m_type = value_t::object; + m_data.m_value.object = create(); assert_invariant(); } // operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { - auto result = m_value.object->emplace(std::move(key), nullptr); + auto result = m_data.m_value.object->emplace(std::move(key), nullptr); return set_parent(result.first->second); } @@ -21309,8 +21445,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // const operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { - auto it = m_value.object->find(key); - JSON_ASSERT(it != m_value.object->end()); + auto it = m_data.m_value.object->find(key); + JSON_ASSERT(it != m_data.m_value.object->end()); return it->second; } @@ -21340,15 +21476,15 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // implicitly convert null value to an empty object if (is_null()) { - m_type = value_t::object; - m_value.object = create(); + m_data.m_type = value_t::object; + m_data.m_value.object = create(); assert_invariant(); } // operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { - auto result = m_value.object->emplace(std::forward(key), nullptr); + auto result = m_data.m_value.object->emplace(std::forward(key), nullptr); return set_parent(result.first->second); } @@ -21364,8 +21500,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // const operator[] only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { - auto it = m_value.object->find(std::forward(key)); - JSON_ASSERT(it != m_value.object->end()); + auto it = m_data.m_value.object->find(std::forward(key)); + JSON_ASSERT(it != m_data.m_value.object->end()); return it->second; } @@ -21602,7 +21738,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec IteratorType result = end(); - switch (m_type) + switch (m_data.m_type) { case value_t::boolean: case value_t::number_float: @@ -21619,32 +21755,32 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_string()) { AllocatorType alloc; - std::allocator_traits::destroy(alloc, m_value.string); - std::allocator_traits::deallocate(alloc, m_value.string, 1); - m_value.string = nullptr; + std::allocator_traits::destroy(alloc, m_data.m_value.string); + std::allocator_traits::deallocate(alloc, m_data.m_value.string, 1); + m_data.m_value.string = nullptr; } else if (is_binary()) { AllocatorType alloc; - std::allocator_traits::destroy(alloc, m_value.binary); - std::allocator_traits::deallocate(alloc, m_value.binary, 1); - m_value.binary = nullptr; + std::allocator_traits::destroy(alloc, m_data.m_value.binary); + std::allocator_traits::deallocate(alloc, m_data.m_value.binary, 1); + m_data.m_value.binary = nullptr; } - m_type = value_t::null; + m_data.m_type = value_t::null; assert_invariant(); break; } case value_t::object: { - result.m_it.object_iterator = m_value.object->erase(pos.m_it.object_iterator); + result.m_it.object_iterator = m_data.m_value.object->erase(pos.m_it.object_iterator); break; } case value_t::array: { - result.m_it.array_iterator = m_value.array->erase(pos.m_it.array_iterator); + result.m_it.array_iterator = m_data.m_value.array->erase(pos.m_it.array_iterator); break; } @@ -21672,7 +21808,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec IteratorType result = end(); - switch (m_type) + switch (m_data.m_type) { case value_t::boolean: case value_t::number_float: @@ -21690,33 +21826,33 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_string()) { AllocatorType alloc; - std::allocator_traits::destroy(alloc, m_value.string); - std::allocator_traits::deallocate(alloc, m_value.string, 1); - m_value.string = nullptr; + std::allocator_traits::destroy(alloc, m_data.m_value.string); + std::allocator_traits::deallocate(alloc, m_data.m_value.string, 1); + m_data.m_value.string = nullptr; } else if (is_binary()) { AllocatorType alloc; - std::allocator_traits::destroy(alloc, m_value.binary); - std::allocator_traits::deallocate(alloc, m_value.binary, 1); - m_value.binary = nullptr; + std::allocator_traits::destroy(alloc, m_data.m_value.binary); + std::allocator_traits::deallocate(alloc, m_data.m_value.binary, 1); + m_data.m_value.binary = nullptr; } - m_type = value_t::null; + m_data.m_type = value_t::null; assert_invariant(); break; } case value_t::object: { - result.m_it.object_iterator = m_value.object->erase(first.m_it.object_iterator, + result.m_it.object_iterator = m_data.m_value.object->erase(first.m_it.object_iterator, last.m_it.object_iterator); break; } case value_t::array: { - result.m_it.array_iterator = m_value.array->erase(first.m_it.array_iterator, + result.m_it.array_iterator = m_data.m_value.array->erase(first.m_it.array_iterator, last.m_it.array_iterator); break; } @@ -21741,7 +21877,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); } - return m_value.object->erase(std::forward(key)); + return m_data.m_value.object->erase(std::forward(key)); } template < typename KeyType, detail::enable_if_t < @@ -21754,10 +21890,10 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this)); } - const auto it = m_value.object->find(std::forward(key)); - if (it != m_value.object->end()) + const auto it = m_data.m_value.object->find(std::forward(key)); + if (it != m_data.m_value.object->end()) { - m_value.object->erase(it); + m_data.m_value.object->erase(it); return 1; } return 0; @@ -21795,7 +21931,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(out_of_range::create(401, detail::concat("array index ", std::to_string(idx), " is out of range"), this)); } - m_value.array->erase(m_value.array->begin() + static_cast(idx)); + m_data.m_value.array->erase(m_data.m_value.array->begin() + static_cast(idx)); } else { @@ -21805,7 +21941,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @} - //////////// // lookup // //////////// @@ -21821,7 +21956,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_object()) { - result.m_it.object_iterator = m_value.object->find(key); + result.m_it.object_iterator = m_data.m_value.object->find(key); } return result; @@ -21835,7 +21970,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_object()) { - result.m_it.object_iterator = m_value.object->find(key); + result.m_it.object_iterator = m_data.m_value.object->find(key); } return result; @@ -21851,7 +21986,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_object()) { - result.m_it.object_iterator = m_value.object->find(std::forward(key)); + result.m_it.object_iterator = m_data.m_value.object->find(std::forward(key)); } return result; @@ -21867,7 +22002,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (is_object()) { - result.m_it.object_iterator = m_value.object->find(std::forward(key)); + result.m_it.object_iterator = m_data.m_value.object->find(std::forward(key)); } return result; @@ -21878,7 +22013,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec size_type count(const typename object_t::key_type& key) const { // return 0 for all nonobject types - return is_object() ? m_value.object->count(key) : 0; + return is_object() ? m_data.m_value.object->count(key) : 0; } /// @brief returns the number of occurrences of a key in a JSON object @@ -21888,14 +22023,14 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec size_type count(KeyType && key) const { // return 0 for all nonobject types - return is_object() ? m_value.object->count(std::forward(key)) : 0; + return is_object() ? m_data.m_value.object->count(std::forward(key)) : 0; } /// @brief check the existence of an element in a JSON object /// @sa https://json.nlohmann.me/api/basic_json/contains/ bool contains(const typename object_t::key_type& key) const { - return is_object() && m_value.object->find(key) != m_value.object->end(); + return is_object() && m_data.m_value.object->find(key) != m_data.m_value.object->end(); } /// @brief check the existence of an element in a JSON object @@ -21904,7 +22039,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec detail::is_usable_as_basic_json_key_type::value, int> = 0> bool contains(KeyType && key) const { - return is_object() && m_value.object->find(std::forward(key)) != m_value.object->end(); + return is_object() && m_data.m_value.object->find(std::forward(key)) != m_data.m_value.object->end(); } /// @brief check the existence of an element in a JSON object given a JSON pointer @@ -21923,7 +22058,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @} - /////////////// // iterators // /////////////// @@ -22062,7 +22196,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @} - ////////////// // capacity // ////////////// @@ -22074,7 +22207,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/empty/ bool empty() const noexcept { - switch (m_type) + switch (m_data.m_type) { case value_t::null: { @@ -22085,13 +22218,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec case value_t::array: { // delegate call to array_t::empty() - return m_value.array->empty(); + return m_data.m_value.array->empty(); } case value_t::object: { // delegate call to object_t::empty() - return m_value.object->empty(); + return m_data.m_value.object->empty(); } case value_t::string: @@ -22113,7 +22246,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/size/ size_type size() const noexcept { - switch (m_type) + switch (m_data.m_type) { case value_t::null: { @@ -22124,13 +22257,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec case value_t::array: { // delegate call to array_t::size() - return m_value.array->size(); + return m_data.m_value.array->size(); } case value_t::object: { // delegate call to object_t::size() - return m_value.object->size(); + return m_data.m_value.object->size(); } case value_t::string: @@ -22152,18 +22285,18 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/max_size/ size_type max_size() const noexcept { - switch (m_type) + switch (m_data.m_type) { case value_t::array: { // delegate call to array_t::max_size() - return m_value.array->max_size(); + return m_data.m_value.array->max_size(); } case value_t::object: { // delegate call to object_t::max_size() - return m_value.object->max_size(); + return m_data.m_value.object->max_size(); } case value_t::null: @@ -22184,7 +22317,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @} - /////////////// // modifiers // /////////////// @@ -22196,53 +22328,53 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @sa https://json.nlohmann.me/api/basic_json/clear/ void clear() noexcept { - switch (m_type) + switch (m_data.m_type) { case value_t::number_integer: { - m_value.number_integer = 0; + m_data.m_value.number_integer = 0; break; } case value_t::number_unsigned: { - m_value.number_unsigned = 0; + m_data.m_value.number_unsigned = 0; break; } case value_t::number_float: { - m_value.number_float = 0.0; + m_data.m_value.number_float = 0.0; break; } case value_t::boolean: { - m_value.boolean = false; + m_data.m_value.boolean = false; break; } case value_t::string: { - m_value.string->clear(); + m_data.m_value.string->clear(); break; } case value_t::binary: { - m_value.binary->clear(); + m_data.m_value.binary->clear(); break; } case value_t::array: { - m_value.array->clear(); + m_data.m_value.array->clear(); break; } case value_t::object: { - m_value.object->clear(); + m_data.m_value.object->clear(); break; } @@ -22266,15 +22398,15 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // transform null object into an array if (is_null()) { - m_type = value_t::array; - m_value = value_t::array; + m_data.m_type = value_t::array; + m_data.m_value = value_t::array; assert_invariant(); } // add element to array (move semantics) - const auto old_capacity = m_value.array->capacity(); - m_value.array->push_back(std::move(val)); - set_parent(m_value.array->back(), old_capacity); + const auto old_capacity = m_data.m_value.array->capacity(); + m_data.m_value.array->push_back(std::move(val)); + set_parent(m_data.m_value.array->back(), old_capacity); // if val is moved from, basic_json move constructor marks it null, so we do not call the destructor } @@ -22299,15 +22431,15 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // transform null object into an array if (is_null()) { - m_type = value_t::array; - m_value = value_t::array; + m_data.m_type = value_t::array; + m_data.m_value = value_t::array; assert_invariant(); } // add element to array - const auto old_capacity = m_value.array->capacity(); - m_value.array->push_back(val); - set_parent(m_value.array->back(), old_capacity); + const auto old_capacity = m_data.m_value.array->capacity(); + m_data.m_value.array->push_back(val); + set_parent(m_data.m_value.array->back(), old_capacity); } /// @brief add an object to an array @@ -22331,13 +22463,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // transform null object into an object if (is_null()) { - m_type = value_t::object; - m_value = value_t::object; + m_data.m_type = value_t::object; + m_data.m_value = value_t::object; assert_invariant(); } // add element to object - auto res = m_value.object->insert(val); + auto res = m_data.m_value.object->insert(val); set_parent(res.first->second); } @@ -22387,15 +22519,15 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // transform null object into an array if (is_null()) { - m_type = value_t::array; - m_value = value_t::array; + m_data.m_type = value_t::array; + m_data.m_value = value_t::array; assert_invariant(); } // add element to array (perfect forwarding) - const auto old_capacity = m_value.array->capacity(); - m_value.array->emplace_back(std::forward(args)...); - return set_parent(m_value.array->back(), old_capacity); + const auto old_capacity = m_data.m_value.array->capacity(); + m_data.m_value.array->emplace_back(std::forward(args)...); + return set_parent(m_data.m_value.array->back(), old_capacity); } /// @brief add an object to an object if key does not exist @@ -22412,13 +22544,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // transform null object into an object if (is_null()) { - m_type = value_t::object; - m_value = value_t::object; + m_data.m_type = value_t::object; + m_data.m_value = value_t::object; assert_invariant(); } // add element to array (perfect forwarding) - auto res = m_value.object->emplace(std::forward(args)...); + auto res = m_data.m_value.object->emplace(std::forward(args)...); set_parent(res.first->second); // create result iterator and set iterator to the result of emplace @@ -22436,14 +22568,14 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec iterator insert_iterator(const_iterator pos, Args&& ... args) { iterator result(this); - JSON_ASSERT(m_value.array != nullptr); + JSON_ASSERT(m_data.m_value.array != nullptr); - auto insert_pos = std::distance(m_value.array->begin(), pos.m_it.array_iterator); - m_value.array->insert(pos.m_it.array_iterator, std::forward(args)...); - result.m_it.array_iterator = m_value.array->begin() + insert_pos; + auto insert_pos = std::distance(m_data.m_value.array->begin(), pos.m_it.array_iterator); + m_data.m_value.array->insert(pos.m_it.array_iterator, std::forward(args)...); + result.m_it.array_iterator = m_data.m_value.array->begin() + insert_pos; // This could have been written as: - // result.m_it.array_iterator = m_value.array->insert(pos.m_it.array_iterator, cnt, val); + // result.m_it.array_iterator = m_data.m_value.array->insert(pos.m_it.array_iterator, cnt, val); // but the return value of insert is missing in GCC 4.8, so it is written this way instead. set_parents(); @@ -22570,7 +22702,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_THROW(invalid_iterator::create(202, "iterators first and last must point to objects", this)); } - m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator); + m_data.m_value.object->insert(first.m_it.object_iterator, last.m_it.object_iterator); } /// @brief updates a JSON object from another object, overwriting existing keys @@ -22587,8 +22719,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // implicitly convert null value to an empty object if (is_null()) { - m_type = value_t::object; - m_value.object = create(); + m_data.m_type = value_t::object; + m_data.m_value.object = create(); assert_invariant(); } @@ -22613,16 +22745,16 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec { if (merge_objects && it.value().is_object()) { - auto it2 = m_value.object->find(it.key()); - if (it2 != m_value.object->end()) + auto it2 = m_data.m_value.object->find(it.key()); + if (it2 != m_data.m_value.object->end()) { it2->second.update(it.value(), true); continue; } } - m_value.object->operator[](it.key()) = it.value(); + m_data.m_value.object->operator[](it.key()) = it.value(); #if JSON_DIAGNOSTICS - m_value.object->operator[](it.key()).m_parent = this; + m_data.m_value.object->operator[](it.key()).m_parent = this; #endif } } @@ -22632,12 +22764,12 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec void swap(reference other) noexcept ( std::is_nothrow_move_constructible::value&& std::is_nothrow_move_assignable::value&& - std::is_nothrow_move_constructible::value&& + std::is_nothrow_move_constructible::value&& // NOLINT(cppcoreguidelines-noexcept-swap,performance-noexcept-swap) std::is_nothrow_move_assignable::value ) { - std::swap(m_type, other.m_type); - std::swap(m_value, other.m_value); + std::swap(m_data.m_type, other.m_data.m_type); + std::swap(m_data.m_value, other.m_data.m_value); set_parents(); other.set_parents(); @@ -22649,7 +22781,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec friend void swap(reference left, reference right) noexcept ( std::is_nothrow_move_constructible::value&& std::is_nothrow_move_assignable::value&& - std::is_nothrow_move_constructible::value&& + std::is_nothrow_move_constructible::value&& // NOLINT(cppcoreguidelines-noexcept-swap,performance-noexcept-swap) std::is_nothrow_move_assignable::value ) { @@ -22658,13 +22790,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @brief exchanges the values /// @sa https://json.nlohmann.me/api/basic_json/swap/ - void swap(array_t& other) // NOLINT(bugprone-exception-escape) + void swap(array_t& other) // NOLINT(bugprone-exception-escape,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) { // swap only works for arrays if (JSON_HEDLEY_LIKELY(is_array())) { using std::swap; - swap(*(m_value.array), other); + swap(*(m_data.m_value.array), other); } else { @@ -22674,13 +22806,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @brief exchanges the values /// @sa https://json.nlohmann.me/api/basic_json/swap/ - void swap(object_t& other) // NOLINT(bugprone-exception-escape) + void swap(object_t& other) // NOLINT(bugprone-exception-escape,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) { // swap only works for objects if (JSON_HEDLEY_LIKELY(is_object())) { using std::swap; - swap(*(m_value.object), other); + swap(*(m_data.m_value.object), other); } else { @@ -22690,13 +22822,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @brief exchanges the values /// @sa https://json.nlohmann.me/api/basic_json/swap/ - void swap(string_t& other) // NOLINT(bugprone-exception-escape) + void swap(string_t& other) // NOLINT(bugprone-exception-escape,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) { // swap only works for strings if (JSON_HEDLEY_LIKELY(is_string())) { using std::swap; - swap(*(m_value.string), other); + swap(*(m_data.m_value.string), other); } else { @@ -22706,13 +22838,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec /// @brief exchanges the values /// @sa https://json.nlohmann.me/api/basic_json/swap/ - void swap(binary_t& other) // NOLINT(bugprone-exception-escape) + void swap(binary_t& other) // NOLINT(bugprone-exception-escape,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) { // swap only works for strings if (JSON_HEDLEY_LIKELY(is_binary())) { using std::swap; - swap(*(m_value.binary), other); + swap(*(m_data.m_value.binary), other); } else { @@ -22728,7 +22860,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (JSON_HEDLEY_LIKELY(is_binary())) { using std::swap; - swap(*(m_value.binary), other); + swap(*(m_data.m_value.binary), other); } else { @@ -22756,31 +22888,31 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec switch (lhs_type) \ { \ case value_t::array: \ - return (*lhs.m_value.array) op (*rhs.m_value.array); \ + return (*lhs.m_data.m_value.array) op (*rhs.m_data.m_value.array); \ \ case value_t::object: \ - return (*lhs.m_value.object) op (*rhs.m_value.object); \ + return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \ \ case value_t::null: \ return (null_result); \ \ case value_t::string: \ - return (*lhs.m_value.string) op (*rhs.m_value.string); \ + return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \ \ case value_t::boolean: \ - return (lhs.m_value.boolean) op (rhs.m_value.boolean); \ + return (lhs.m_data.m_value.boolean) op (rhs.m_data.m_value.boolean); \ \ case value_t::number_integer: \ - return (lhs.m_value.number_integer) op (rhs.m_value.number_integer); \ + return (lhs.m_data.m_value.number_integer) op (rhs.m_data.m_value.number_integer); \ \ case value_t::number_unsigned: \ - return (lhs.m_value.number_unsigned) op (rhs.m_value.number_unsigned); \ + return (lhs.m_data.m_value.number_unsigned) op (rhs.m_data.m_value.number_unsigned); \ \ case value_t::number_float: \ - return (lhs.m_value.number_float) op (rhs.m_value.number_float); \ + return (lhs.m_data.m_value.number_float) op (rhs.m_data.m_value.number_float); \ \ case value_t::binary: \ - return (*lhs.m_value.binary) op (*rhs.m_value.binary); \ + return (*lhs.m_data.m_value.binary) op (*rhs.m_data.m_value.binary); \ \ case value_t::discarded: \ default: \ @@ -22789,27 +22921,27 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } \ else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_float) \ { \ - return static_cast(lhs.m_value.number_integer) op rhs.m_value.number_float; \ + return static_cast(lhs.m_data.m_value.number_integer) op rhs.m_data.m_value.number_float; \ } \ else if (lhs_type == value_t::number_float && rhs_type == value_t::number_integer) \ { \ - return lhs.m_value.number_float op static_cast(rhs.m_value.number_integer); \ + return lhs.m_data.m_value.number_float op static_cast(rhs.m_data.m_value.number_integer); \ } \ else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_float) \ { \ - return static_cast(lhs.m_value.number_unsigned) op rhs.m_value.number_float; \ + return static_cast(lhs.m_data.m_value.number_unsigned) op rhs.m_data.m_value.number_float; \ } \ else if (lhs_type == value_t::number_float && rhs_type == value_t::number_unsigned) \ { \ - return lhs.m_value.number_float op static_cast(rhs.m_value.number_unsigned); \ + return lhs.m_data.m_value.number_float op static_cast(rhs.m_data.m_value.number_unsigned); \ } \ else if (lhs_type == value_t::number_unsigned && rhs_type == value_t::number_integer) \ { \ - return static_cast(lhs.m_value.number_unsigned) op rhs.m_value.number_integer; \ + return static_cast(lhs.m_data.m_value.number_unsigned) op rhs.m_data.m_value.number_integer; \ } \ else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_unsigned) \ { \ - return lhs.m_value.number_integer op static_cast(rhs.m_value.number_unsigned); \ + return lhs.m_data.m_value.number_integer op static_cast(rhs.m_data.m_value.number_unsigned); \ } \ else if(compares_unordered(lhs, rhs))\ {\ @@ -22826,8 +22958,8 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // an operation is computed as an odd number of inverses of others static bool compares_unordered(const_reference lhs, const_reference rhs, bool inverse = false) noexcept { - if ((lhs.is_number_float() && std::isnan(lhs.m_value.number_float) && rhs.is_number()) - || (rhs.is_number_float() && std::isnan(rhs.m_value.number_float) && lhs.is_number())) + if ((lhs.is_number_float() && std::isnan(lhs.m_data.m_value.number_float) && rhs.is_number()) + || (rhs.is_number_float() && std::isnan(rhs.m_data.m_value.number_float) && lhs.is_number())) { return true; } @@ -23171,7 +23303,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec #endif // JSON_NO_IO /// @} - ///////////////////// // deserialization // ///////////////////// @@ -23328,7 +23459,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_HEDLEY_RETURNS_NON_NULL const char* type_name() const noexcept { - switch (m_type) + switch (m_data.m_type) { case value_t::null: return "null"; @@ -23352,17 +23483,43 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } } - JSON_PRIVATE_UNLESS_TESTED: ////////////////////// // member variables // ////////////////////// - /// the type of the current element - value_t m_type = value_t::null; + struct data + { + /// the type of the current element + value_t m_type = value_t::null; + + /// the value of the current element + json_value m_value = {}; + + data(const value_t v) + : m_type(v), m_value(v) + { + } + + data(size_type cnt, const basic_json& val) + : m_type(value_t::array) + { + m_value.array = create(cnt, val); + } + + data() noexcept = default; + data(data&&) noexcept = default; + data(const data&) noexcept = delete; + data& operator=(data&&) noexcept = delete; + data& operator=(const data&) noexcept = delete; + + ~data() noexcept + { + m_value.destroy(m_type); + } + }; - /// the value of the current element - json_value m_value = {}; + data m_data = {}; #if JSON_DIAGNOSTICS /// a pointer to a parent value (for debugging purposes) @@ -23543,7 +23700,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return from_cbor(ptr, ptr + len, strict, allow_exceptions, tag_handler); } - JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DEPRECATED_FOR(3.8.0, from_cbor(ptr, ptr + len)) static basic_json from_cbor(detail::span_input_adapter&& i, @@ -23667,7 +23823,6 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return res ? result : basic_json(value_t::discarded); } - /// @brief create a JSON value from an input in BJData format /// @sa https://json.nlohmann.me/api/basic_json/from_bjdata/ template @@ -23890,7 +24045,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec } // make sure the top element of the pointer exists - json_pointer top_pointer = ptr.top(); + json_pointer const top_pointer = ptr.top(); if (top_pointer != ptr) { result.at(top_pointer); @@ -23902,7 +24057,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec // parent must exist when performing patch add per RFC6902 specs basic_json& parent = result.at(ptr); - switch (parent.m_type) + switch (parent.m_data.m_type) { case value_t::null: case value_t::object: @@ -23948,7 +24103,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec }; // wrapper for "remove" operation; remove value at ptr - const auto operation_remove = [this, &result](json_pointer & ptr) + const auto operation_remove = [this, & result](json_pointer & ptr) { // get reference to parent of JSON pointer ptr const auto last_path = ptr.back(); @@ -23991,13 +24146,13 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec bool string_type) -> basic_json & { // find value - auto it = val.m_value.object->find(member); + auto it = val.m_data.m_value.object->find(member); // context-sensitive error message const auto error_msg = (op == "op") ? "operation" : detail::concat("operation '", op, '\''); // check if desired value is present - if (JSON_HEDLEY_UNLIKELY(it == val.m_value.object->end())) + if (JSON_HEDLEY_UNLIKELY(it == val.m_data.m_value.object->end())) { // NOLINTNEXTLINE(performance-inefficient-string-concatenation) JSON_THROW(parse_error::create(105, 0, detail::concat(error_msg, " must have member '", member, "'"), &val)); @@ -24052,7 +24207,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec json_pointer from_ptr(from_path); // the "from" location must exist - use at() - basic_json v = result.at(from_ptr); + basic_json const v = result.at(from_ptr); // The move operation is functionally identical to a // "remove" operation on the "from" location, followed @@ -24069,7 +24224,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec const json_pointer from_ptr(from_path); // the "from" location must exist - use at() - basic_json v = result.at(from_ptr); + basic_json const v = result.at(from_ptr); // The copy is functionally identical to an "add" // operation at the target location using the value @@ -24311,7 +24466,11 @@ inline namespace json_literals /// @brief user-defined string literal for JSON values /// @sa https://json.nlohmann.me/api/basic_json/operator_literal_json/ JSON_HEDLEY_NON_NULL(1) -inline nlohmann::json operator "" _json(const char* s, std::size_t n) +#if !defined(JSON_HEDLEY_GCC_VERSION) || JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) + inline nlohmann::json operator ""_json(const char* s, std::size_t n) +#else + inline nlohmann::json operator "" _json(const char* s, std::size_t n) +#endif { return nlohmann::json::parse(s, s + n); } @@ -24319,7 +24478,11 @@ inline nlohmann::json operator "" _json(const char* s, std::size_t n) /// @brief user-defined string literal for JSON pointer /// @sa https://json.nlohmann.me/api/basic_json/operator_literal_json_pointer/ JSON_HEDLEY_NON_NULL(1) -inline nlohmann::json::json_pointer operator "" _json_pointer(const char* s, std::size_t n) +#if !defined(JSON_HEDLEY_GCC_VERSION) || JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) + inline nlohmann::json::json_pointer operator ""_json_pointer(const char* s, std::size_t n) +#else + inline nlohmann::json::json_pointer operator "" _json_pointer(const char* s, std::size_t n) +#endif { return nlohmann::json::json_pointer(std::string(s, n)); } @@ -24338,7 +24501,7 @@ namespace std // NOLINT(cert-dcl58-cpp) /// @brief hash value for JSON objects /// @sa https://json.nlohmann.me/api/basic_json/std_hash/ NLOHMANN_BASIC_JSON_TPL_DECLARATION -struct hash +struct hash // NOLINT(cert-dcl58-cpp) { std::size_t operator()(const nlohmann::NLOHMANN_BASIC_JSON_TPL& j) const { @@ -24371,8 +24534,8 @@ struct less< ::nlohmann::detail::value_t> // do not remove the space after '<', /// @brief exchanges the values of two JSON objects /// @sa https://json.nlohmann.me/api/basic_json/std_swap/ NLOHMANN_BASIC_JSON_TPL_DECLARATION -inline void swap(nlohmann::NLOHMANN_BASIC_JSON_TPL& j1, nlohmann::NLOHMANN_BASIC_JSON_TPL& j2) noexcept( // NOLINT(readability-inconsistent-declaration-parameter-name) - is_nothrow_move_constructible::value&& // NOLINT(misc-redundant-expression) +inline void swap(nlohmann::NLOHMANN_BASIC_JSON_TPL& j1, nlohmann::NLOHMANN_BASIC_JSON_TPL& j2) noexcept( // NOLINT(readability-inconsistent-declaration-parameter-name, cert-dcl58-cpp) + is_nothrow_move_constructible::value&& // NOLINT(misc-redundant-expression,cppcoreguidelines-noexcept-swap,performance-noexcept-swap) is_nothrow_move_assignable::value) { j1.swap(j2); @@ -24383,17 +24546,22 @@ inline void swap(nlohmann::NLOHMANN_BASIC_JSON_TPL& j1, nlohmann::NLOHMANN_BASIC } // namespace std #if JSON_USE_GLOBAL_UDLS - using nlohmann::literals::json_literals::operator "" _json; // NOLINT(misc-unused-using-decls,google-global-names-in-headers) - using nlohmann::literals::json_literals::operator "" _json_pointer; //NOLINT(misc-unused-using-decls,google-global-names-in-headers) + #if !defined(JSON_HEDLEY_GCC_VERSION) || JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) + using nlohmann::literals::json_literals::operator ""_json; // NOLINT(misc-unused-using-decls,google-global-names-in-headers) + using nlohmann::literals::json_literals::operator ""_json_pointer; //NOLINT(misc-unused-using-decls,google-global-names-in-headers) + #else + using nlohmann::literals::json_literals::operator "" _json; // NOLINT(misc-unused-using-decls,google-global-names-in-headers) + using nlohmann::literals::json_literals::operator "" _json_pointer; //NOLINT(misc-unused-using-decls,google-global-names-in-headers) + #endif #endif // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT @@ -24428,16 +24596,17 @@ inline void swap(nlohmann::NLOHMANN_BASIC_JSON_TPL& j1, nlohmann::NLOHMANN_BASIC #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM #undef JSON_HAS_THREE_WAY_COMPARISON #undef JSON_HAS_RANGES + #undef JSON_HAS_STATIC_RTTI #undef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON #endif // #include // __ _____ _____ _____ // __| | __| | | | JSON for Modern C++ -// | | |__ | | | | | | version 3.11.2 +// | | |__ | | | | | | version 3.11.3 // |_____|_____|_____|_|___| https://github.com/nlohmann/json // -// SPDX-FileCopyrightText: 2013-2022 Niels Lohmann +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann // SPDX-License-Identifier: MIT diff --git a/Development/third_party/nlohmann/smtp-address-validator.cpp b/Development/third_party/nlohmann/smtp-address-validator.cpp index 3903b51d2..5ddd81104 100644 --- a/Development/third_party/nlohmann/smtp-address-validator.cpp +++ b/Development/third_party/nlohmann/smtp-address-validator.cpp @@ -26,805 +26,767 @@ SOFTWARE. */ -#line 1 "smtp-address-validator.rl" #include "smtp-address-validator.hpp" -#line 110 "smtp-address-validator.rl" - -#line 7 "smtp-address-validator.cpp" static const signed char _address_actions[] = { 0, 1, 0, 1, 1, 0}; static const short _address_key_offsets[] = { - 0, 0, 24, 26, 50, 52, 54, 56, - 58, 60, 62, 86, 103, 105, 107, 109, - 111, 113, 115, 117, 134, 150, 161, 168, - 176, 180, 181, 190, 195, 196, 201, 202, - 207, 210, 213, 219, 222, 225, 228, 234, - 237, 240, 243, 249, 252, 261, 270, 282, - 293, 302, 311, 320, 328, 345, 353, 360, - 367, 368, 375, 382, 389, 396, 397, 404, - 411, 418, 425, 426, 433, 440, 447, 454, - 455, 462, 469, 476, 483, 484, 491, 498, - 505, 512, 513, 523, 531, 538, 545, 546, - 552, 559, 566, 573, 581, 589, 597, 608, - 618, 626, 634, 641, 649, 657, 665, 667, - 673, 681, 689, 697, 699, 705, 713, 721, - 729, 731, 737, 745, 753, 761, 763, 769, - 777, 785, 793, 795, 802, 812, 821, 829, - 837, 839, 848, 857, 865, 873, 875, 884, - 893, 901, 909, 911, 920, 929, 937, 945, - 947, 956, 965, 974, 983, 992, 1004, 1015, - 1024, 1033, 1042, 1051, 1060, 1072, 1083, 1092, - 1101, 1109, 1118, 1127, 1136, 1148, 1159, 1168, - 1177, 1185, 1194, 1203, 1212, 1224, 1235, 1244, - 1253, 1261, 1270, 1279, 1288, 1300, 1311, 1320, - 1329, 1337, 1339, 1353, 1355, 1357, 1359, 1361, - 1363, 1365, 1367, 1368, 1370, 1388, 0 -}; + 0, 0, 24, 26, 50, 52, 54, 56, + 58, 60, 62, 86, 103, 105, 107, 109, + 111, 113, 115, 117, 134, 150, 161, 168, + 176, 180, 181, 190, 195, 196, 201, 202, + 207, 210, 213, 219, 222, 225, 228, 234, + 237, 240, 243, 249, 252, 261, 270, 282, + 293, 302, 311, 320, 328, 345, 353, 360, + 367, 368, 375, 382, 389, 396, 397, 404, + 411, 418, 425, 426, 433, 440, 447, 454, + 455, 462, 469, 476, 483, 484, 491, 498, + 505, 512, 513, 523, 531, 538, 545, 546, + 552, 559, 566, 573, 581, 589, 597, 608, + 618, 626, 634, 641, 649, 657, 665, 667, + 673, 681, 689, 697, 699, 705, 713, 721, + 729, 731, 737, 745, 753, 761, 763, 769, + 777, 785, 793, 795, 802, 812, 821, 829, + 837, 839, 848, 857, 865, 873, 875, 884, + 893, 901, 909, 911, 920, 929, 937, 945, + 947, 956, 965, 974, 983, 992, 1004, 1015, + 1024, 1033, 1042, 1051, 1060, 1072, 1083, 1092, + 1101, 1109, 1118, 1127, 1136, 1148, 1159, 1168, + 1177, 1185, 1194, 1203, 1212, 1224, 1235, 1244, + 1253, 1261, 1270, 1279, 1288, 1300, 1311, 1320, + 1329, 1337, 1339, 1353, 1355, 1357, 1359, 1361, + 1363, 1365, 1367, 1368, 1370, 1388, 0}; static const signed char _address_trans_keys[] = { - -32, -19, -16, -12, 34, 45, 61, 63, - -62, -33, -31, -17, -15, -13, 33, 39, - 42, 43, 47, 57, 65, 90, 94, 126, - -128, -65, -32, -19, -16, -12, 33, 46, - 61, 64, -62, -33, -31, -17, -15, -13, - 35, 39, 42, 43, 45, 57, 63, 90, - 94, 126, -96, -65, -128, -65, -128, -97, - -112, -65, -128, -65, -128, -113, -32, -19, - -16, -12, 33, 45, 61, 63, -62, -33, - -31, -17, -15, -13, 35, 39, 42, 43, - 47, 57, 65, 90, 94, 126, -32, -19, - -16, -12, 91, -62, -33, -31, -17, -15, - -13, 48, 57, 65, 90, 97, 122, -128, - -65, -96, -65, -128, -65, -128, -97, -112, - -65, -128, -65, -128, -113, -32, -19, -16, - -12, 45, -62, -33, -31, -17, -15, -13, - 48, 57, 65, 90, 97, 122, -32, -19, - -16, -12, -62, -33, -31, -17, -15, -13, - 48, 57, 65, 90, 97, 122, 45, 48, - 49, 50, 73, 51, 57, 65, 90, 97, - 122, 45, 48, 57, 65, 90, 97, 122, - 45, 58, 48, 57, 65, 90, 97, 122, - 33, 90, 94, 126, 93, 45, 46, 58, - 48, 57, 65, 90, 97, 122, 48, 49, - 50, 51, 57, 46, 48, 49, 50, 51, - 57, 46, 48, 49, 50, 51, 57, 93, - 48, 57, 93, 48, 57, 53, 93, 48, - 52, 54, 57, 93, 48, 53, 46, 48, - 57, 46, 48, 57, 46, 53, 48, 52, - 54, 57, 46, 48, 53, 46, 48, 57, - 46, 48, 57, 46, 53, 48, 52, 54, - 57, 46, 48, 53, 45, 46, 58, 48, - 57, 65, 90, 97, 122, 45, 46, 58, - 48, 57, 65, 90, 97, 122, 45, 46, - 53, 58, 48, 52, 54, 57, 65, 90, - 97, 122, 45, 46, 58, 48, 53, 54, - 57, 65, 90, 97, 122, 45, 58, 80, - 48, 57, 65, 90, 97, 122, 45, 58, - 118, 48, 57, 65, 90, 97, 122, 45, - 54, 58, 48, 57, 65, 90, 97, 122, - 45, 58, 48, 57, 65, 90, 97, 122, - 58, 33, 47, 48, 57, 59, 64, 65, - 70, 71, 90, 94, 96, 97, 102, 103, - 126, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 48, 57, 65, 70, 97, 102, - 58, 48, 57, 65, 70, 97, 102, 58, - 58, 48, 57, 65, 70, 97, 102, 58, - 48, 57, 65, 70, 97, 102, 58, 48, - 57, 65, 70, 97, 102, 58, 48, 57, - 65, 70, 97, 102, 58, 58, 48, 57, - 65, 70, 97, 102, 58, 48, 57, 65, - 70, 97, 102, 58, 48, 57, 65, 70, - 97, 102, 58, 48, 57, 65, 70, 97, - 102, 58, 58, 48, 57, 65, 70, 97, - 102, 58, 48, 57, 65, 70, 97, 102, - 58, 48, 57, 65, 70, 97, 102, 58, - 48, 57, 65, 70, 97, 102, 58, 58, - 48, 57, 65, 70, 97, 102, 58, 48, - 57, 65, 70, 97, 102, 58, 48, 57, - 65, 70, 97, 102, 58, 48, 57, 65, - 70, 97, 102, 58, 58, 48, 57, 65, - 70, 97, 102, 58, 48, 57, 65, 70, - 97, 102, 58, 48, 57, 65, 70, 97, - 102, 58, 48, 57, 65, 70, 97, 102, - 58, 48, 49, 50, 58, 51, 57, 65, - 70, 97, 102, 46, 58, 48, 57, 65, - 70, 97, 102, 58, 48, 57, 65, 70, - 97, 102, 58, 48, 57, 65, 70, 97, - 102, 58, 48, 57, 65, 70, 97, 102, - 93, 48, 57, 65, 70, 97, 102, 93, - 48, 57, 65, 70, 97, 102, 93, 48, - 57, 65, 70, 97, 102, 46, 58, 48, - 57, 65, 70, 97, 102, 46, 58, 48, - 57, 65, 70, 97, 102, 46, 58, 48, - 57, 65, 70, 97, 102, 46, 53, 58, - 48, 52, 54, 57, 65, 70, 97, 102, - 46, 58, 48, 53, 54, 57, 65, 70, - 97, 102, 46, 58, 48, 57, 65, 70, - 97, 102, 46, 58, 48, 57, 65, 70, - 97, 102, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 58, 48, 57, 65, 70, - 97, 102, 48, 49, 50, 93, 51, 57, - 65, 70, 97, 102, 46, 58, 93, 48, - 57, 65, 70, 97, 102, 58, 93, 48, - 57, 65, 70, 97, 102, 58, 93, 48, - 57, 65, 70, 97, 102, 58, 93, 48, - 49, 50, 51, 57, 65, 70, 97, 102, - 46, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 49, 50, 51, 57, - 65, 70, 97, 102, 46, 58, 93, 48, - 57, 65, 70, 97, 102, 58, 93, 48, - 57, 65, 70, 97, 102, 58, 93, 48, - 57, 65, 70, 97, 102, 58, 93, 48, - 49, 50, 51, 57, 65, 70, 97, 102, - 46, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 49, 50, 51, 57, - 65, 70, 97, 102, 46, 58, 93, 48, - 57, 65, 70, 97, 102, 46, 58, 93, - 48, 57, 65, 70, 97, 102, 46, 58, - 93, 48, 57, 65, 70, 97, 102, 46, - 58, 93, 48, 57, 65, 70, 97, 102, - 46, 53, 58, 93, 48, 52, 54, 57, - 65, 70, 97, 102, 46, 58, 93, 48, - 53, 54, 57, 65, 70, 97, 102, 46, - 58, 93, 48, 57, 65, 70, 97, 102, - 46, 58, 93, 48, 57, 65, 70, 97, - 102, 46, 58, 93, 48, 57, 65, 70, - 97, 102, 46, 58, 93, 48, 57, 65, - 70, 97, 102, 46, 58, 93, 48, 57, - 65, 70, 97, 102, 46, 53, 58, 93, - 48, 52, 54, 57, 65, 70, 97, 102, - 46, 58, 93, 48, 53, 54, 57, 65, - 70, 97, 102, 46, 58, 93, 48, 57, - 65, 70, 97, 102, 46, 58, 93, 48, - 57, 65, 70, 97, 102, 58, 93, 48, - 57, 65, 70, 97, 102, 46, 58, 93, - 48, 57, 65, 70, 97, 102, 46, 58, - 93, 48, 57, 65, 70, 97, 102, 46, - 58, 93, 48, 57, 65, 70, 97, 102, - 46, 53, 58, 93, 48, 52, 54, 57, - 65, 70, 97, 102, 46, 58, 93, 48, - 53, 54, 57, 65, 70, 97, 102, 46, - 58, 93, 48, 57, 65, 70, 97, 102, - 46, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 46, 58, 93, 48, 57, 65, 70, - 97, 102, 46, 58, 93, 48, 57, 65, - 70, 97, 102, 46, 58, 93, 48, 57, - 65, 70, 97, 102, 46, 53, 58, 93, - 48, 52, 54, 57, 65, 70, 97, 102, - 46, 58, 93, 48, 53, 54, 57, 65, - 70, 97, 102, 46, 58, 93, 48, 57, - 65, 70, 97, 102, 46, 58, 93, 48, - 57, 65, 70, 97, 102, 58, 93, 48, - 57, 65, 70, 97, 102, 46, 58, 93, - 48, 57, 65, 70, 97, 102, 46, 58, - 93, 48, 57, 65, 70, 97, 102, 46, - 58, 93, 48, 57, 65, 70, 97, 102, - 46, 53, 58, 93, 48, 52, 54, 57, - 65, 70, 97, 102, 46, 58, 93, 48, - 53, 54, 57, 65, 70, 97, 102, 46, - 58, 93, 48, 57, 65, 70, 97, 102, - 46, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, 48, 57, 65, 70, 97, - 102, 58, 93, -32, -19, -16, -12, 34, - 92, -62, -33, -31, -17, -15, -13, 32, - 126, -128, -65, -96, -65, -128, -65, -128, - -97, -112, -65, -128, -65, -128, -113, 64, - 32, 126, -32, -19, -16, -12, 45, 46, - -62, -33, -31, -17, -15, -13, 48, 57, - 65, 90, 97, 122, 0 -}; + -32, -19, -16, -12, 34, 45, 61, 63, + -62, -33, -31, -17, -15, -13, 33, 39, + 42, 43, 47, 57, 65, 90, 94, 126, + -128, -65, -32, -19, -16, -12, 33, 46, + 61, 64, -62, -33, -31, -17, -15, -13, + 35, 39, 42, 43, 45, 57, 63, 90, + 94, 126, -96, -65, -128, -65, -128, -97, + -112, -65, -128, -65, -128, -113, -32, -19, + -16, -12, 33, 45, 61, 63, -62, -33, + -31, -17, -15, -13, 35, 39, 42, 43, + 47, 57, 65, 90, 94, 126, -32, -19, + -16, -12, 91, -62, -33, -31, -17, -15, + -13, 48, 57, 65, 90, 97, 122, -128, + -65, -96, -65, -128, -65, -128, -97, -112, + -65, -128, -65, -128, -113, -32, -19, -16, + -12, 45, -62, -33, -31, -17, -15, -13, + 48, 57, 65, 90, 97, 122, -32, -19, + -16, -12, -62, -33, -31, -17, -15, -13, + 48, 57, 65, 90, 97, 122, 45, 48, + 49, 50, 73, 51, 57, 65, 90, 97, + 122, 45, 48, 57, 65, 90, 97, 122, + 45, 58, 48, 57, 65, 90, 97, 122, + 33, 90, 94, 126, 93, 45, 46, 58, + 48, 57, 65, 90, 97, 122, 48, 49, + 50, 51, 57, 46, 48, 49, 50, 51, + 57, 46, 48, 49, 50, 51, 57, 93, + 48, 57, 93, 48, 57, 53, 93, 48, + 52, 54, 57, 93, 48, 53, 46, 48, + 57, 46, 48, 57, 46, 53, 48, 52, + 54, 57, 46, 48, 53, 46, 48, 57, + 46, 48, 57, 46, 53, 48, 52, 54, + 57, 46, 48, 53, 45, 46, 58, 48, + 57, 65, 90, 97, 122, 45, 46, 58, + 48, 57, 65, 90, 97, 122, 45, 46, + 53, 58, 48, 52, 54, 57, 65, 90, + 97, 122, 45, 46, 58, 48, 53, 54, + 57, 65, 90, 97, 122, 45, 58, 80, + 48, 57, 65, 90, 97, 122, 45, 58, + 118, 48, 57, 65, 90, 97, 122, 45, + 54, 58, 48, 57, 65, 90, 97, 122, + 45, 58, 48, 57, 65, 90, 97, 122, + 58, 33, 47, 48, 57, 59, 64, 65, + 70, 71, 90, 94, 96, 97, 102, 103, + 126, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 48, 57, 65, 70, 97, 102, + 58, 48, 57, 65, 70, 97, 102, 58, + 58, 48, 57, 65, 70, 97, 102, 58, + 48, 57, 65, 70, 97, 102, 58, 48, + 57, 65, 70, 97, 102, 58, 48, 57, + 65, 70, 97, 102, 58, 58, 48, 57, + 65, 70, 97, 102, 58, 48, 57, 65, + 70, 97, 102, 58, 48, 57, 65, 70, + 97, 102, 58, 48, 57, 65, 70, 97, + 102, 58, 58, 48, 57, 65, 70, 97, + 102, 58, 48, 57, 65, 70, 97, 102, + 58, 48, 57, 65, 70, 97, 102, 58, + 48, 57, 65, 70, 97, 102, 58, 58, + 48, 57, 65, 70, 97, 102, 58, 48, + 57, 65, 70, 97, 102, 58, 48, 57, + 65, 70, 97, 102, 58, 48, 57, 65, + 70, 97, 102, 58, 58, 48, 57, 65, + 70, 97, 102, 58, 48, 57, 65, 70, + 97, 102, 58, 48, 57, 65, 70, 97, + 102, 58, 48, 57, 65, 70, 97, 102, + 58, 48, 49, 50, 58, 51, 57, 65, + 70, 97, 102, 46, 58, 48, 57, 65, + 70, 97, 102, 58, 48, 57, 65, 70, + 97, 102, 58, 48, 57, 65, 70, 97, + 102, 58, 48, 57, 65, 70, 97, 102, + 93, 48, 57, 65, 70, 97, 102, 93, + 48, 57, 65, 70, 97, 102, 93, 48, + 57, 65, 70, 97, 102, 46, 58, 48, + 57, 65, 70, 97, 102, 46, 58, 48, + 57, 65, 70, 97, 102, 46, 58, 48, + 57, 65, 70, 97, 102, 46, 53, 58, + 48, 52, 54, 57, 65, 70, 97, 102, + 46, 58, 48, 53, 54, 57, 65, 70, + 97, 102, 46, 58, 48, 57, 65, 70, + 97, 102, 46, 58, 48, 57, 65, 70, + 97, 102, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 58, 48, 57, 65, 70, + 97, 102, 48, 49, 50, 93, 51, 57, + 65, 70, 97, 102, 46, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, + 49, 50, 51, 57, 65, 70, 97, 102, + 46, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 49, 50, 51, 57, + 65, 70, 97, 102, 46, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, + 49, 50, 51, 57, 65, 70, 97, 102, + 46, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 49, 50, 51, 57, + 65, 70, 97, 102, 46, 58, 93, 48, + 57, 65, 70, 97, 102, 46, 58, 93, + 48, 57, 65, 70, 97, 102, 46, 58, + 93, 48, 57, 65, 70, 97, 102, 46, + 58, 93, 48, 57, 65, 70, 97, 102, + 46, 53, 58, 93, 48, 52, 54, 57, + 65, 70, 97, 102, 46, 58, 93, 48, + 53, 54, 57, 65, 70, 97, 102, 46, + 58, 93, 48, 57, 65, 70, 97, 102, + 46, 58, 93, 48, 57, 65, 70, 97, + 102, 46, 58, 93, 48, 57, 65, 70, + 97, 102, 46, 58, 93, 48, 57, 65, + 70, 97, 102, 46, 58, 93, 48, 57, + 65, 70, 97, 102, 46, 53, 58, 93, + 48, 52, 54, 57, 65, 70, 97, 102, + 46, 58, 93, 48, 53, 54, 57, 65, + 70, 97, 102, 46, 58, 93, 48, 57, + 65, 70, 97, 102, 46, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 46, 58, 93, + 48, 57, 65, 70, 97, 102, 46, 58, + 93, 48, 57, 65, 70, 97, 102, 46, + 58, 93, 48, 57, 65, 70, 97, 102, + 46, 53, 58, 93, 48, 52, 54, 57, + 65, 70, 97, 102, 46, 58, 93, 48, + 53, 54, 57, 65, 70, 97, 102, 46, + 58, 93, 48, 57, 65, 70, 97, 102, + 46, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 46, 58, 93, 48, 57, 65, 70, + 97, 102, 46, 58, 93, 48, 57, 65, + 70, 97, 102, 46, 58, 93, 48, 57, + 65, 70, 97, 102, 46, 53, 58, 93, + 48, 52, 54, 57, 65, 70, 97, 102, + 46, 58, 93, 48, 53, 54, 57, 65, + 70, 97, 102, 46, 58, 93, 48, 57, + 65, 70, 97, 102, 46, 58, 93, 48, + 57, 65, 70, 97, 102, 58, 93, 48, + 57, 65, 70, 97, 102, 46, 58, 93, + 48, 57, 65, 70, 97, 102, 46, 58, + 93, 48, 57, 65, 70, 97, 102, 46, + 58, 93, 48, 57, 65, 70, 97, 102, + 46, 53, 58, 93, 48, 52, 54, 57, + 65, 70, 97, 102, 46, 58, 93, 48, + 53, 54, 57, 65, 70, 97, 102, 46, + 58, 93, 48, 57, 65, 70, 97, 102, + 46, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, 48, 57, 65, 70, 97, + 102, 58, 93, -32, -19, -16, -12, 34, + 92, -62, -33, -31, -17, -15, -13, 32, + 126, -128, -65, -96, -65, -128, -65, -128, + -97, -112, -65, -128, -65, -128, -113, 64, + 32, 126, -32, -19, -16, -12, 45, 46, + -62, -33, -31, -17, -15, -13, 48, 57, + 65, 90, 97, 122, 0}; static const signed char _address_single_lengths[] = { - 0, 8, 0, 8, 0, 0, 0, 0, - 0, 0, 8, 5, 0, 0, 0, 0, - 0, 0, 0, 5, 4, 5, 1, 2, - 0, 1, 3, 3, 1, 3, 1, 3, - 1, 1, 2, 1, 1, 1, 2, 1, - 1, 1, 2, 1, 3, 3, 4, 3, - 3, 3, 3, 2, 1, 2, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 4, 2, 1, 1, 1, 0, - 1, 1, 1, 2, 2, 2, 3, 2, - 2, 2, 1, 2, 2, 2, 2, 0, - 2, 2, 2, 2, 0, 2, 2, 2, - 2, 0, 2, 2, 2, 2, 0, 2, - 2, 2, 2, 1, 4, 3, 2, 2, - 2, 3, 3, 2, 2, 2, 3, 3, - 2, 2, 2, 3, 3, 2, 2, 2, - 3, 3, 3, 3, 3, 4, 3, 3, - 3, 3, 3, 3, 4, 3, 3, 3, - 2, 3, 3, 3, 4, 3, 3, 3, - 2, 3, 3, 3, 4, 3, 3, 3, - 2, 3, 3, 3, 4, 3, 3, 3, - 2, 2, 6, 0, 0, 0, 0, 0, - 0, 0, 1, 0, 6, 0, 0 -}; + 0, 8, 0, 8, 0, 0, 0, 0, + 0, 0, 8, 5, 0, 0, 0, 0, + 0, 0, 0, 5, 4, 5, 1, 2, + 0, 1, 3, 3, 1, 3, 1, 3, + 1, 1, 2, 1, 1, 1, 2, 1, + 1, 1, 2, 1, 3, 3, 4, 3, + 3, 3, 3, 2, 1, 2, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 4, 2, 1, 1, 1, 0, + 1, 1, 1, 2, 2, 2, 3, 2, + 2, 2, 1, 2, 2, 2, 2, 0, + 2, 2, 2, 2, 0, 2, 2, 2, + 2, 0, 2, 2, 2, 2, 0, 2, + 2, 2, 2, 1, 4, 3, 2, 2, + 2, 3, 3, 2, 2, 2, 3, 3, + 2, 2, 2, 3, 3, 2, 2, 2, + 3, 3, 3, 3, 3, 4, 3, 3, + 3, 3, 3, 3, 4, 3, 3, 3, + 2, 3, 3, 3, 4, 3, 3, 3, + 2, 3, 3, 3, 4, 3, 3, 3, + 2, 3, 3, 3, 4, 3, 3, 3, + 2, 2, 6, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 6, 0, 0}; static const signed char _address_range_lengths[] = { - 0, 8, 1, 8, 1, 1, 1, 1, - 1, 1, 8, 6, 1, 1, 1, 1, - 1, 1, 1, 6, 6, 3, 3, 3, - 2, 0, 3, 1, 0, 1, 0, 1, - 1, 1, 2, 1, 1, 1, 2, 1, - 1, 1, 2, 1, 3, 3, 4, 4, - 3, 3, 3, 3, 8, 3, 3, 3, - 0, 3, 3, 3, 3, 0, 3, 3, - 3, 3, 0, 3, 3, 3, 3, 0, - 3, 3, 3, 3, 0, 3, 3, 3, - 3, 0, 3, 3, 3, 3, 0, 3, - 3, 3, 3, 3, 3, 3, 4, 4, - 3, 3, 3, 3, 3, 3, 0, 3, - 3, 3, 3, 0, 3, 3, 3, 3, - 0, 3, 3, 3, 3, 0, 3, 3, - 3, 3, 0, 3, 3, 3, 3, 3, - 0, 3, 3, 3, 3, 0, 3, 3, - 3, 3, 0, 3, 3, 3, 3, 0, - 3, 3, 3, 3, 3, 4, 4, 3, - 3, 3, 3, 3, 4, 4, 3, 3, - 3, 3, 3, 3, 4, 4, 3, 3, - 3, 3, 3, 3, 4, 4, 3, 3, - 3, 3, 3, 3, 4, 4, 3, 3, - 3, 0, 4, 1, 1, 1, 1, 1, - 1, 1, 0, 1, 6, 0, 0 -}; + 0, 8, 1, 8, 1, 1, 1, 1, + 1, 1, 8, 6, 1, 1, 1, 1, + 1, 1, 1, 6, 6, 3, 3, 3, + 2, 0, 3, 1, 0, 1, 0, 1, + 1, 1, 2, 1, 1, 1, 2, 1, + 1, 1, 2, 1, 3, 3, 4, 4, + 3, 3, 3, 3, 8, 3, 3, 3, + 0, 3, 3, 3, 3, 0, 3, 3, + 3, 3, 0, 3, 3, 3, 3, 0, + 3, 3, 3, 3, 0, 3, 3, 3, + 3, 0, 3, 3, 3, 3, 0, 3, + 3, 3, 3, 3, 3, 3, 4, 4, + 3, 3, 3, 3, 3, 3, 0, 3, + 3, 3, 3, 0, 3, 3, 3, 3, + 0, 3, 3, 3, 3, 0, 3, 3, + 3, 3, 0, 3, 3, 3, 3, 3, + 0, 3, 3, 3, 3, 0, 3, 3, + 3, 3, 0, 3, 3, 3, 3, 0, + 3, 3, 3, 3, 3, 4, 4, 3, + 3, 3, 3, 3, 4, 4, 3, 3, + 3, 3, 3, 3, 4, 4, 3, 3, + 3, 3, 3, 3, 4, 4, 3, 3, + 3, 3, 3, 3, 4, 4, 3, 3, + 3, 0, 4, 1, 1, 1, 1, 1, + 1, 1, 0, 1, 6, 0, 0}; static const short _address_index_offsets[] = { - 0, 0, 17, 19, 36, 38, 40, 42, - 44, 46, 48, 65, 77, 79, 81, 83, - 85, 87, 89, 91, 103, 114, 123, 128, - 134, 137, 139, 146, 151, 153, 158, 160, - 165, 168, 171, 176, 179, 182, 185, 190, - 193, 196, 199, 204, 207, 214, 221, 230, - 238, 245, 252, 259, 265, 275, 281, 286, - 291, 293, 298, 303, 308, 313, 315, 320, - 325, 330, 335, 337, 342, 347, 352, 357, - 359, 364, 369, 374, 379, 381, 386, 391, - 396, 401, 403, 411, 417, 422, 427, 429, - 433, 438, 443, 448, 454, 460, 466, 474, - 481, 487, 493, 498, 504, 510, 516, 519, - 523, 529, 535, 541, 544, 548, 554, 560, - 566, 569, 573, 579, 585, 591, 594, 598, - 604, 610, 616, 619, 624, 632, 639, 645, - 651, 654, 661, 668, 674, 680, 683, 690, - 697, 703, 709, 712, 719, 726, 732, 738, - 741, 748, 755, 762, 769, 776, 785, 793, - 800, 807, 814, 821, 828, 837, 845, 852, - 859, 865, 872, 879, 886, 895, 903, 910, - 917, 923, 930, 937, 944, 953, 961, 968, - 975, 981, 988, 995, 1002, 1011, 1019, 1026, - 1033, 1039, 1042, 1053, 1055, 1057, 1059, 1061, - 1063, 1065, 1067, 1069, 1071, 1084, 0 -}; + 0, 0, 17, 19, 36, 38, 40, 42, + 44, 46, 48, 65, 77, 79, 81, 83, + 85, 87, 89, 91, 103, 114, 123, 128, + 134, 137, 139, 146, 151, 153, 158, 160, + 165, 168, 171, 176, 179, 182, 185, 190, + 193, 196, 199, 204, 207, 214, 221, 230, + 238, 245, 252, 259, 265, 275, 281, 286, + 291, 293, 298, 303, 308, 313, 315, 320, + 325, 330, 335, 337, 342, 347, 352, 357, + 359, 364, 369, 374, 379, 381, 386, 391, + 396, 401, 403, 411, 417, 422, 427, 429, + 433, 438, 443, 448, 454, 460, 466, 474, + 481, 487, 493, 498, 504, 510, 516, 519, + 523, 529, 535, 541, 544, 548, 554, 560, + 566, 569, 573, 579, 585, 591, 594, 598, + 604, 610, 616, 619, 624, 632, 639, 645, + 651, 654, 661, 668, 674, 680, 683, 690, + 697, 703, 709, 712, 719, 726, 732, 738, + 741, 748, 755, 762, 769, 776, 785, 793, + 800, 807, 814, 821, 828, 837, 845, 852, + 859, 865, 872, 879, 886, 895, 903, 910, + 917, 923, 930, 937, 944, 953, 961, 968, + 975, 981, 988, 995, 1002, 1011, 1019, 1026, + 1033, 1039, 1042, 1053, 1055, 1057, 1059, 1061, + 1063, 1065, 1067, 1069, 1071, 1084, 0}; static const short _address_cond_targs[] = { - 4, 6, 7, 9, 186, 3, 3, 3, - 2, 5, 8, 3, 3, 3, 3, 3, - 0, 3, 0, 4, 6, 7, 9, 3, - 10, 3, 11, 2, 5, 8, 3, 3, - 3, 3, 3, 0, 2, 0, 2, 0, - 2, 0, 5, 0, 5, 0, 5, 0, - 4, 6, 7, 9, 3, 3, 3, 3, - 2, 5, 8, 3, 3, 3, 3, 3, - 0, 13, 15, 16, 18, 21, 12, 14, - 17, 196, 196, 196, 0, 196, 0, 12, - 0, 12, 0, 12, 0, 14, 0, 14, - 0, 14, 0, 13, 15, 16, 18, 19, - 12, 14, 17, 196, 196, 196, 0, 13, - 15, 16, 18, 12, 14, 17, 196, 196, - 196, 0, 22, 26, 44, 46, 48, 45, - 23, 23, 0, 22, 23, 23, 23, 0, - 22, 24, 23, 23, 23, 0, 25, 25, - 0, 197, 0, 22, 27, 24, 23, 23, - 23, 0, 28, 40, 42, 41, 0, 29, - 0, 30, 36, 38, 37, 0, 31, 0, - 25, 32, 34, 33, 0, 197, 33, 0, - 197, 25, 0, 35, 197, 33, 25, 0, - 197, 25, 0, 31, 37, 0, 31, 30, - 0, 31, 39, 37, 30, 0, 31, 30, - 0, 29, 41, 0, 29, 28, 0, 29, - 43, 41, 28, 0, 29, 28, 0, 22, - 27, 24, 45, 23, 23, 0, 22, 27, - 24, 26, 23, 23, 0, 22, 27, 47, - 24, 45, 26, 23, 23, 0, 22, 27, - 24, 26, 23, 23, 23, 0, 22, 24, - 49, 23, 23, 23, 0, 22, 24, 50, - 23, 23, 23, 0, 22, 51, 24, 23, - 23, 23, 0, 22, 52, 23, 23, 23, - 0, 185, 25, 53, 25, 53, 25, 25, - 53, 25, 0, 57, 197, 54, 54, 54, - 0, 57, 55, 55, 55, 0, 57, 56, - 56, 56, 0, 57, 0, 124, 58, 58, - 58, 0, 62, 59, 59, 59, 0, 62, - 60, 60, 60, 0, 62, 61, 61, 61, - 0, 62, 0, 124, 63, 63, 63, 0, - 67, 64, 64, 64, 0, 67, 65, 65, - 65, 0, 67, 66, 66, 66, 0, 67, - 0, 124, 68, 68, 68, 0, 72, 69, - 69, 69, 0, 72, 70, 70, 70, 0, - 72, 71, 71, 71, 0, 72, 0, 124, - 73, 73, 73, 0, 77, 74, 74, 74, - 0, 77, 75, 75, 75, 0, 77, 76, - 76, 76, 0, 77, 0, 98, 78, 78, - 78, 0, 82, 79, 79, 79, 0, 82, - 80, 80, 80, 0, 82, 81, 81, 81, - 0, 82, 0, 83, 91, 94, 98, 97, - 123, 123, 0, 27, 87, 84, 84, 84, - 0, 87, 85, 85, 85, 0, 87, 86, - 86, 86, 0, 87, 0, 88, 88, 88, - 0, 197, 89, 89, 89, 0, 197, 90, - 90, 90, 0, 197, 25, 25, 25, 0, - 27, 87, 92, 84, 84, 0, 27, 87, - 93, 85, 85, 0, 27, 87, 86, 86, - 86, 0, 27, 95, 87, 92, 96, 84, - 84, 0, 27, 87, 93, 85, 85, 85, - 0, 27, 87, 85, 85, 85, 0, 27, - 87, 96, 84, 84, 0, 197, 99, 99, - 99, 0, 103, 197, 100, 100, 100, 0, - 103, 197, 101, 101, 101, 0, 103, 197, - 102, 102, 102, 0, 103, 197, 0, 104, - 104, 104, 0, 108, 197, 105, 105, 105, - 0, 108, 197, 106, 106, 106, 0, 108, - 197, 107, 107, 107, 0, 108, 197, 0, - 109, 109, 109, 0, 113, 197, 110, 110, - 110, 0, 113, 197, 111, 111, 111, 0, - 113, 197, 112, 112, 112, 0, 113, 197, - 0, 114, 114, 114, 0, 118, 197, 115, - 115, 115, 0, 118, 197, 116, 116, 116, - 0, 118, 197, 117, 117, 117, 0, 118, - 197, 0, 119, 119, 119, 0, 87, 197, - 120, 120, 120, 0, 87, 197, 121, 121, - 121, 0, 87, 197, 122, 122, 122, 0, - 87, 197, 0, 87, 84, 84, 84, 0, - 125, 177, 180, 197, 183, 184, 184, 0, - 27, 129, 197, 126, 126, 126, 0, 129, - 197, 127, 127, 127, 0, 129, 197, 128, - 128, 128, 0, 129, 197, 0, 130, 169, - 172, 175, 176, 176, 0, 27, 134, 197, - 131, 131, 131, 0, 134, 197, 132, 132, - 132, 0, 134, 197, 133, 133, 133, 0, - 134, 197, 0, 135, 161, 164, 167, 168, - 168, 0, 27, 139, 197, 136, 136, 136, - 0, 139, 197, 137, 137, 137, 0, 139, - 197, 138, 138, 138, 0, 139, 197, 0, - 140, 153, 156, 159, 160, 160, 0, 27, - 144, 197, 141, 141, 141, 0, 144, 197, - 142, 142, 142, 0, 144, 197, 143, 143, - 143, 0, 144, 197, 0, 145, 146, 149, - 152, 119, 119, 0, 27, 87, 197, 120, - 120, 120, 0, 27, 87, 197, 147, 120, - 120, 0, 27, 87, 197, 148, 121, 121, - 0, 27, 87, 197, 122, 122, 122, 0, - 27, 150, 87, 197, 147, 151, 120, 120, - 0, 27, 87, 197, 148, 121, 121, 121, - 0, 27, 87, 197, 121, 121, 121, 0, - 27, 87, 197, 151, 120, 120, 0, 27, - 144, 197, 154, 141, 141, 0, 27, 144, - 197, 155, 142, 142, 0, 27, 144, 197, - 143, 143, 143, 0, 27, 157, 144, 197, - 154, 158, 141, 141, 0, 27, 144, 197, - 155, 142, 142, 142, 0, 27, 144, 197, - 142, 142, 142, 0, 27, 144, 197, 158, - 141, 141, 0, 144, 197, 141, 141, 141, - 0, 27, 139, 197, 162, 136, 136, 0, - 27, 139, 197, 163, 137, 137, 0, 27, - 139, 197, 138, 138, 138, 0, 27, 165, - 139, 197, 162, 166, 136, 136, 0, 27, - 139, 197, 163, 137, 137, 137, 0, 27, - 139, 197, 137, 137, 137, 0, 27, 139, - 197, 166, 136, 136, 0, 139, 197, 136, - 136, 136, 0, 27, 134, 197, 170, 131, - 131, 0, 27, 134, 197, 171, 132, 132, - 0, 27, 134, 197, 133, 133, 133, 0, - 27, 173, 134, 197, 170, 174, 131, 131, - 0, 27, 134, 197, 171, 132, 132, 132, - 0, 27, 134, 197, 132, 132, 132, 0, - 27, 134, 197, 174, 131, 131, 0, 134, - 197, 131, 131, 131, 0, 27, 129, 197, - 178, 126, 126, 0, 27, 129, 197, 179, - 127, 127, 0, 27, 129, 197, 128, 128, - 128, 0, 27, 181, 129, 197, 178, 182, - 126, 126, 0, 27, 129, 197, 179, 127, - 127, 127, 0, 27, 129, 197, 127, 127, - 127, 0, 27, 129, 197, 182, 126, 126, - 0, 129, 197, 126, 126, 126, 0, 124, - 197, 0, 188, 190, 191, 193, 194, 195, - 187, 189, 192, 186, 0, 186, 0, 187, - 0, 187, 0, 187, 0, 189, 0, 189, - 0, 189, 0, 11, 0, 186, 0, 13, - 15, 16, 18, 19, 20, 12, 14, 17, - 196, 196, 196, 0, 0, 0, 1, 2, - 3, 4, 5, 6, 7, 8, 9, 10, - 11, 12, 13, 14, 15, 16, 17, 18, - 19, 20, 21, 22, 23, 24, 25, 26, - 27, 28, 29, 30, 31, 32, 33, 34, - 35, 36, 37, 38, 39, 40, 41, 42, - 43, 44, 45, 46, 47, 48, 49, 50, - 51, 52, 53, 54, 55, 56, 57, 58, - 59, 60, 61, 62, 63, 64, 65, 66, - 67, 68, 69, 70, 71, 72, 73, 74, - 75, 76, 77, 78, 79, 80, 81, 82, - 83, 84, 85, 86, 87, 88, 89, 90, - 91, 92, 93, 94, 95, 96, 97, 98, - 99, 100, 101, 102, 103, 104, 105, 106, - 107, 108, 109, 110, 111, 112, 113, 114, - 115, 116, 117, 118, 119, 120, 121, 122, - 123, 124, 125, 126, 127, 128, 129, 130, - 131, 132, 133, 134, 135, 136, 137, 138, - 139, 140, 141, 142, 143, 144, 145, 146, - 147, 148, 149, 150, 151, 152, 153, 154, - 155, 156, 157, 158, 159, 160, 161, 162, - 163, 164, 165, 166, 167, 168, 169, 170, - 171, 172, 173, 174, 175, 176, 177, 178, - 179, 180, 181, 182, 183, 184, 185, 186, - 187, 188, 189, 190, 191, 192, 193, 194, - 195, 196, 197, 0 -}; + 4, 6, 7, 9, 186, 3, 3, 3, + 2, 5, 8, 3, 3, 3, 3, 3, + 0, 3, 0, 4, 6, 7, 9, 3, + 10, 3, 11, 2, 5, 8, 3, 3, + 3, 3, 3, 0, 2, 0, 2, 0, + 2, 0, 5, 0, 5, 0, 5, 0, + 4, 6, 7, 9, 3, 3, 3, 3, + 2, 5, 8, 3, 3, 3, 3, 3, + 0, 13, 15, 16, 18, 21, 12, 14, + 17, 196, 196, 196, 0, 196, 0, 12, + 0, 12, 0, 12, 0, 14, 0, 14, + 0, 14, 0, 13, 15, 16, 18, 19, + 12, 14, 17, 196, 196, 196, 0, 13, + 15, 16, 18, 12, 14, 17, 196, 196, + 196, 0, 22, 26, 44, 46, 48, 45, + 23, 23, 0, 22, 23, 23, 23, 0, + 22, 24, 23, 23, 23, 0, 25, 25, + 0, 197, 0, 22, 27, 24, 23, 23, + 23, 0, 28, 40, 42, 41, 0, 29, + 0, 30, 36, 38, 37, 0, 31, 0, + 25, 32, 34, 33, 0, 197, 33, 0, + 197, 25, 0, 35, 197, 33, 25, 0, + 197, 25, 0, 31, 37, 0, 31, 30, + 0, 31, 39, 37, 30, 0, 31, 30, + 0, 29, 41, 0, 29, 28, 0, 29, + 43, 41, 28, 0, 29, 28, 0, 22, + 27, 24, 45, 23, 23, 0, 22, 27, + 24, 26, 23, 23, 0, 22, 27, 47, + 24, 45, 26, 23, 23, 0, 22, 27, + 24, 26, 23, 23, 23, 0, 22, 24, + 49, 23, 23, 23, 0, 22, 24, 50, + 23, 23, 23, 0, 22, 51, 24, 23, + 23, 23, 0, 22, 52, 23, 23, 23, + 0, 185, 25, 53, 25, 53, 25, 25, + 53, 25, 0, 57, 197, 54, 54, 54, + 0, 57, 55, 55, 55, 0, 57, 56, + 56, 56, 0, 57, 0, 124, 58, 58, + 58, 0, 62, 59, 59, 59, 0, 62, + 60, 60, 60, 0, 62, 61, 61, 61, + 0, 62, 0, 124, 63, 63, 63, 0, + 67, 64, 64, 64, 0, 67, 65, 65, + 65, 0, 67, 66, 66, 66, 0, 67, + 0, 124, 68, 68, 68, 0, 72, 69, + 69, 69, 0, 72, 70, 70, 70, 0, + 72, 71, 71, 71, 0, 72, 0, 124, + 73, 73, 73, 0, 77, 74, 74, 74, + 0, 77, 75, 75, 75, 0, 77, 76, + 76, 76, 0, 77, 0, 98, 78, 78, + 78, 0, 82, 79, 79, 79, 0, 82, + 80, 80, 80, 0, 82, 81, 81, 81, + 0, 82, 0, 83, 91, 94, 98, 97, + 123, 123, 0, 27, 87, 84, 84, 84, + 0, 87, 85, 85, 85, 0, 87, 86, + 86, 86, 0, 87, 0, 88, 88, 88, + 0, 197, 89, 89, 89, 0, 197, 90, + 90, 90, 0, 197, 25, 25, 25, 0, + 27, 87, 92, 84, 84, 0, 27, 87, + 93, 85, 85, 0, 27, 87, 86, 86, + 86, 0, 27, 95, 87, 92, 96, 84, + 84, 0, 27, 87, 93, 85, 85, 85, + 0, 27, 87, 85, 85, 85, 0, 27, + 87, 96, 84, 84, 0, 197, 99, 99, + 99, 0, 103, 197, 100, 100, 100, 0, + 103, 197, 101, 101, 101, 0, 103, 197, + 102, 102, 102, 0, 103, 197, 0, 104, + 104, 104, 0, 108, 197, 105, 105, 105, + 0, 108, 197, 106, 106, 106, 0, 108, + 197, 107, 107, 107, 0, 108, 197, 0, + 109, 109, 109, 0, 113, 197, 110, 110, + 110, 0, 113, 197, 111, 111, 111, 0, + 113, 197, 112, 112, 112, 0, 113, 197, + 0, 114, 114, 114, 0, 118, 197, 115, + 115, 115, 0, 118, 197, 116, 116, 116, + 0, 118, 197, 117, 117, 117, 0, 118, + 197, 0, 119, 119, 119, 0, 87, 197, + 120, 120, 120, 0, 87, 197, 121, 121, + 121, 0, 87, 197, 122, 122, 122, 0, + 87, 197, 0, 87, 84, 84, 84, 0, + 125, 177, 180, 197, 183, 184, 184, 0, + 27, 129, 197, 126, 126, 126, 0, 129, + 197, 127, 127, 127, 0, 129, 197, 128, + 128, 128, 0, 129, 197, 0, 130, 169, + 172, 175, 176, 176, 0, 27, 134, 197, + 131, 131, 131, 0, 134, 197, 132, 132, + 132, 0, 134, 197, 133, 133, 133, 0, + 134, 197, 0, 135, 161, 164, 167, 168, + 168, 0, 27, 139, 197, 136, 136, 136, + 0, 139, 197, 137, 137, 137, 0, 139, + 197, 138, 138, 138, 0, 139, 197, 0, + 140, 153, 156, 159, 160, 160, 0, 27, + 144, 197, 141, 141, 141, 0, 144, 197, + 142, 142, 142, 0, 144, 197, 143, 143, + 143, 0, 144, 197, 0, 145, 146, 149, + 152, 119, 119, 0, 27, 87, 197, 120, + 120, 120, 0, 27, 87, 197, 147, 120, + 120, 0, 27, 87, 197, 148, 121, 121, + 0, 27, 87, 197, 122, 122, 122, 0, + 27, 150, 87, 197, 147, 151, 120, 120, + 0, 27, 87, 197, 148, 121, 121, 121, + 0, 27, 87, 197, 121, 121, 121, 0, + 27, 87, 197, 151, 120, 120, 0, 27, + 144, 197, 154, 141, 141, 0, 27, 144, + 197, 155, 142, 142, 0, 27, 144, 197, + 143, 143, 143, 0, 27, 157, 144, 197, + 154, 158, 141, 141, 0, 27, 144, 197, + 155, 142, 142, 142, 0, 27, 144, 197, + 142, 142, 142, 0, 27, 144, 197, 158, + 141, 141, 0, 144, 197, 141, 141, 141, + 0, 27, 139, 197, 162, 136, 136, 0, + 27, 139, 197, 163, 137, 137, 0, 27, + 139, 197, 138, 138, 138, 0, 27, 165, + 139, 197, 162, 166, 136, 136, 0, 27, + 139, 197, 163, 137, 137, 137, 0, 27, + 139, 197, 137, 137, 137, 0, 27, 139, + 197, 166, 136, 136, 0, 139, 197, 136, + 136, 136, 0, 27, 134, 197, 170, 131, + 131, 0, 27, 134, 197, 171, 132, 132, + 0, 27, 134, 197, 133, 133, 133, 0, + 27, 173, 134, 197, 170, 174, 131, 131, + 0, 27, 134, 197, 171, 132, 132, 132, + 0, 27, 134, 197, 132, 132, 132, 0, + 27, 134, 197, 174, 131, 131, 0, 134, + 197, 131, 131, 131, 0, 27, 129, 197, + 178, 126, 126, 0, 27, 129, 197, 179, + 127, 127, 0, 27, 129, 197, 128, 128, + 128, 0, 27, 181, 129, 197, 178, 182, + 126, 126, 0, 27, 129, 197, 179, 127, + 127, 127, 0, 27, 129, 197, 127, 127, + 127, 0, 27, 129, 197, 182, 126, 126, + 0, 129, 197, 126, 126, 126, 0, 124, + 197, 0, 188, 190, 191, 193, 194, 195, + 187, 189, 192, 186, 0, 186, 0, 187, + 0, 187, 0, 187, 0, 189, 0, 189, + 0, 189, 0, 11, 0, 186, 0, 13, + 15, 16, 18, 19, 20, 12, 14, 17, + 196, 196, 196, 0, 0, 0, 1, 2, + 3, 4, 5, 6, 7, 8, 9, 10, + 11, 12, 13, 14, 15, 16, 17, 18, + 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, + 35, 36, 37, 38, 39, 40, 41, 42, + 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, + 59, 60, 61, 62, 63, 64, 65, 66, + 67, 68, 69, 70, 71, 72, 73, 74, + 75, 76, 77, 78, 79, 80, 81, 82, + 83, 84, 85, 86, 87, 88, 89, 90, + 91, 92, 93, 94, 95, 96, 97, 98, + 99, 100, 101, 102, 103, 104, 105, 106, + 107, 108, 109, 110, 111, 112, 113, 114, + 115, 116, 117, 118, 119, 120, 121, 122, + 123, 124, 125, 126, 127, 128, 129, 130, + 131, 132, 133, 134, 135, 136, 137, 138, + 139, 140, 141, 142, 143, 144, 145, 146, + 147, 148, 149, 150, 151, 152, 153, 154, + 155, 156, 157, 158, 159, 160, 161, 162, + 163, 164, 165, 166, 167, 168, 169, 170, + 171, 172, 173, 174, 175, 176, 177, 178, + 179, 180, 181, 182, 183, 184, 185, 186, + 187, 188, 189, 190, 191, 192, 193, 194, + 195, 196, 197, 0}; static const signed char _address_cond_actions[] = { - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 3, 0, 3, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 3, 0, 3, 0, 3, - 0, 3, 0, 3, 0, 3, 0, 3, - 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 3, 0, 0, 0, 0, 0, 0, 0, - 0, 1, 1, 1, 3, 1, 3, 0, - 3, 0, 3, 0, 3, 0, 3, 0, - 3, 0, 3, 0, 0, 0, 0, 0, - 0, 0, 0, 1, 1, 1, 3, 0, - 0, 0, 0, 0, 0, 0, 1, 1, - 1, 3, 0, 0, 0, 0, 0, 0, - 0, 0, 3, 0, 0, 0, 0, 3, - 0, 0, 0, 0, 0, 3, 0, 0, - 3, 1, 3, 0, 0, 0, 0, 0, - 0, 3, 0, 0, 0, 0, 3, 0, - 3, 0, 0, 0, 0, 3, 0, 3, - 0, 0, 0, 0, 3, 1, 0, 3, - 1, 0, 3, 0, 1, 0, 0, 3, - 1, 0, 3, 0, 0, 3, 0, 0, - 3, 0, 0, 0, 0, 3, 0, 0, - 3, 0, 0, 3, 0, 0, 3, 0, - 0, 0, 0, 3, 0, 0, 3, 0, - 0, 0, 0, 0, 0, 3, 0, 0, - 0, 0, 0, 0, 3, 0, 0, 0, - 0, 0, 0, 0, 0, 3, 0, 0, - 0, 0, 0, 0, 0, 3, 0, 0, - 0, 0, 0, 0, 3, 0, 0, 0, - 0, 0, 0, 3, 0, 0, 0, 0, - 0, 0, 3, 0, 0, 0, 0, 0, - 3, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 3, 0, 1, 0, 0, 0, - 3, 0, 0, 0, 0, 3, 0, 0, - 0, 0, 3, 0, 3, 0, 0, 0, - 0, 3, 0, 0, 0, 0, 3, 0, - 0, 0, 0, 3, 0, 0, 0, 0, - 3, 0, 3, 0, 0, 0, 0, 3, - 0, 0, 0, 0, 3, 0, 0, 0, - 0, 3, 0, 0, 0, 0, 3, 0, - 3, 0, 0, 0, 0, 3, 0, 0, - 0, 0, 3, 0, 0, 0, 0, 3, - 0, 0, 0, 0, 3, 0, 3, 0, - 0, 0, 0, 3, 0, 0, 0, 0, - 3, 0, 0, 0, 0, 3, 0, 0, - 0, 0, 3, 0, 3, 0, 0, 0, - 0, 3, 0, 0, 0, 0, 3, 0, - 0, 0, 0, 3, 0, 0, 0, 0, - 3, 0, 3, 0, 0, 0, 0, 0, - 0, 0, 3, 0, 0, 0, 0, 0, - 3, 0, 0, 0, 0, 3, 0, 0, - 0, 0, 3, 0, 3, 0, 0, 0, - 3, 1, 0, 0, 0, 3, 1, 0, - 0, 0, 3, 1, 0, 0, 0, 3, - 0, 0, 0, 0, 0, 3, 0, 0, - 0, 0, 0, 3, 0, 0, 0, 0, - 0, 3, 0, 0, 0, 0, 0, 0, - 0, 3, 0, 0, 0, 0, 0, 0, - 3, 0, 0, 0, 0, 0, 3, 0, - 0, 0, 0, 0, 3, 1, 0, 0, - 0, 3, 0, 1, 0, 0, 0, 3, - 0, 1, 0, 0, 0, 3, 0, 1, - 0, 0, 0, 3, 0, 1, 3, 0, - 0, 0, 3, 0, 1, 0, 0, 0, - 3, 0, 1, 0, 0, 0, 3, 0, - 1, 0, 0, 0, 3, 0, 1, 3, - 0, 0, 0, 3, 0, 1, 0, 0, - 0, 3, 0, 1, 0, 0, 0, 3, - 0, 1, 0, 0, 0, 3, 0, 1, - 3, 0, 0, 0, 3, 0, 1, 0, - 0, 0, 3, 0, 1, 0, 0, 0, - 3, 0, 1, 0, 0, 0, 3, 0, - 1, 3, 0, 0, 0, 3, 0, 1, - 0, 0, 0, 3, 0, 1, 0, 0, - 0, 3, 0, 1, 0, 0, 0, 3, - 0, 1, 3, 0, 0, 0, 0, 3, - 0, 0, 0, 1, 0, 0, 0, 3, - 0, 0, 1, 0, 0, 0, 3, 0, - 1, 0, 0, 0, 3, 0, 1, 0, - 0, 0, 3, 0, 1, 3, 0, 0, - 0, 0, 0, 0, 3, 0, 0, 1, - 0, 0, 0, 3, 0, 1, 0, 0, - 0, 3, 0, 1, 0, 0, 0, 3, - 0, 1, 3, 0, 0, 0, 0, 0, - 0, 3, 0, 0, 1, 0, 0, 0, - 3, 0, 1, 0, 0, 0, 3, 0, - 1, 0, 0, 0, 3, 0, 1, 3, - 0, 0, 0, 0, 0, 0, 3, 0, - 0, 1, 0, 0, 0, 3, 0, 1, - 0, 0, 0, 3, 0, 1, 0, 0, - 0, 3, 0, 1, 3, 0, 0, 0, - 0, 0, 0, 3, 0, 0, 1, 0, - 0, 0, 3, 0, 0, 1, 0, 0, - 0, 3, 0, 0, 1, 0, 0, 0, - 3, 0, 0, 1, 0, 0, 0, 3, - 0, 0, 0, 1, 0, 0, 0, 0, - 3, 0, 0, 1, 0, 0, 0, 0, - 3, 0, 0, 1, 0, 0, 0, 3, - 0, 0, 1, 0, 0, 0, 3, 0, - 0, 1, 0, 0, 0, 3, 0, 0, - 1, 0, 0, 0, 3, 0, 0, 1, - 0, 0, 0, 3, 0, 0, 0, 1, - 0, 0, 0, 0, 3, 0, 0, 1, - 0, 0, 0, 0, 3, 0, 0, 1, - 0, 0, 0, 3, 0, 0, 1, 0, - 0, 0, 3, 0, 1, 0, 0, 0, - 3, 0, 0, 1, 0, 0, 0, 3, - 0, 0, 1, 0, 0, 0, 3, 0, - 0, 1, 0, 0, 0, 3, 0, 0, - 0, 1, 0, 0, 0, 0, 3, 0, - 0, 1, 0, 0, 0, 0, 3, 0, - 0, 1, 0, 0, 0, 3, 0, 0, - 1, 0, 0, 0, 3, 0, 1, 0, - 0, 0, 3, 0, 0, 1, 0, 0, - 0, 3, 0, 0, 1, 0, 0, 0, - 3, 0, 0, 1, 0, 0, 0, 3, - 0, 0, 0, 1, 0, 0, 0, 0, - 3, 0, 0, 1, 0, 0, 0, 0, - 3, 0, 0, 1, 0, 0, 0, 3, - 0, 0, 1, 0, 0, 0, 3, 0, - 1, 0, 0, 0, 3, 0, 0, 1, - 0, 0, 0, 3, 0, 0, 1, 0, - 0, 0, 3, 0, 0, 1, 0, 0, - 0, 3, 0, 0, 0, 1, 0, 0, - 0, 0, 3, 0, 0, 1, 0, 0, - 0, 0, 3, 0, 0, 1, 0, 0, - 0, 3, 0, 0, 1, 0, 0, 0, - 3, 0, 1, 0, 0, 0, 3, 0, - 1, 3, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 3, 0, 3, 0, - 3, 0, 3, 0, 3, 0, 3, 0, - 3, 0, 3, 0, 3, 0, 3, 0, - 0, 0, 0, 0, 0, 0, 0, 0, - 1, 1, 1, 3, 3, 0, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 3, 3, 3, 3, 3, 3, 3, - 3, 0, 0, 0 -}; + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 3, 0, 3, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 3, 0, 3, 0, 3, + 0, 3, 0, 3, 0, 3, 0, 3, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 3, 1, 3, 0, + 3, 0, 3, 0, 3, 0, 3, 0, + 3, 0, 3, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 3, 0, + 0, 0, 0, 0, 0, 0, 1, 1, + 1, 3, 0, 0, 0, 0, 0, 0, + 0, 0, 3, 0, 0, 0, 0, 3, + 0, 0, 0, 0, 0, 3, 0, 0, + 3, 1, 3, 0, 0, 0, 0, 0, + 0, 3, 0, 0, 0, 0, 3, 0, + 3, 0, 0, 0, 0, 3, 0, 3, + 0, 0, 0, 0, 3, 1, 0, 3, + 1, 0, 3, 0, 1, 0, 0, 3, + 1, 0, 3, 0, 0, 3, 0, 0, + 3, 0, 0, 0, 0, 3, 0, 0, + 3, 0, 0, 3, 0, 0, 3, 0, + 0, 0, 0, 3, 0, 0, 3, 0, + 0, 0, 0, 0, 0, 3, 0, 0, + 0, 0, 0, 0, 3, 0, 0, 0, + 0, 0, 0, 0, 0, 3, 0, 0, + 0, 0, 0, 0, 0, 3, 0, 0, + 0, 0, 0, 0, 3, 0, 0, 0, + 0, 0, 0, 3, 0, 0, 0, 0, + 0, 0, 3, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 3, 0, 1, 0, 0, 0, + 3, 0, 0, 0, 0, 3, 0, 0, + 0, 0, 3, 0, 3, 0, 0, 0, + 0, 3, 0, 0, 0, 0, 3, 0, + 0, 0, 0, 3, 0, 0, 0, 0, + 3, 0, 3, 0, 0, 0, 0, 3, + 0, 0, 0, 0, 3, 0, 0, 0, + 0, 3, 0, 0, 0, 0, 3, 0, + 3, 0, 0, 0, 0, 3, 0, 0, + 0, 0, 3, 0, 0, 0, 0, 3, + 0, 0, 0, 0, 3, 0, 3, 0, + 0, 0, 0, 3, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 3, 0, 0, + 0, 0, 3, 0, 3, 0, 0, 0, + 0, 3, 0, 0, 0, 0, 3, 0, + 0, 0, 0, 3, 0, 0, 0, 0, + 3, 0, 3, 0, 0, 0, 0, 0, + 0, 0, 3, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 3, 0, 0, + 0, 0, 3, 0, 3, 0, 0, 0, + 3, 1, 0, 0, 0, 3, 1, 0, + 0, 0, 3, 1, 0, 0, 0, 3, + 0, 0, 0, 0, 0, 3, 0, 0, + 0, 0, 0, 3, 0, 0, 0, 0, + 0, 3, 0, 0, 0, 0, 0, 0, + 0, 3, 0, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 0, 3, 0, + 0, 0, 0, 0, 3, 1, 0, 0, + 0, 3, 0, 1, 0, 0, 0, 3, + 0, 1, 0, 0, 0, 3, 0, 1, + 0, 0, 0, 3, 0, 1, 3, 0, + 0, 0, 3, 0, 1, 0, 0, 0, + 3, 0, 1, 0, 0, 0, 3, 0, + 1, 0, 0, 0, 3, 0, 1, 3, + 0, 0, 0, 3, 0, 1, 0, 0, + 0, 3, 0, 1, 0, 0, 0, 3, + 0, 1, 0, 0, 0, 3, 0, 1, + 3, 0, 0, 0, 3, 0, 1, 0, + 0, 0, 3, 0, 1, 0, 0, 0, + 3, 0, 1, 0, 0, 0, 3, 0, + 1, 3, 0, 0, 0, 3, 0, 1, + 0, 0, 0, 3, 0, 1, 0, 0, + 0, 3, 0, 1, 0, 0, 0, 3, + 0, 1, 3, 0, 0, 0, 0, 3, + 0, 0, 0, 1, 0, 0, 0, 3, + 0, 0, 1, 0, 0, 0, 3, 0, + 1, 0, 0, 0, 3, 0, 1, 0, + 0, 0, 3, 0, 1, 3, 0, 0, + 0, 0, 0, 0, 3, 0, 0, 1, + 0, 0, 0, 3, 0, 1, 0, 0, + 0, 3, 0, 1, 0, 0, 0, 3, + 0, 1, 3, 0, 0, 0, 0, 0, + 0, 3, 0, 0, 1, 0, 0, 0, + 3, 0, 1, 0, 0, 0, 3, 0, + 1, 0, 0, 0, 3, 0, 1, 3, + 0, 0, 0, 0, 0, 0, 3, 0, + 0, 1, 0, 0, 0, 3, 0, 1, + 0, 0, 0, 3, 0, 1, 0, 0, + 0, 3, 0, 1, 3, 0, 0, 0, + 0, 0, 0, 3, 0, 0, 1, 0, + 0, 0, 3, 0, 0, 1, 0, 0, + 0, 3, 0, 0, 1, 0, 0, 0, + 3, 0, 0, 1, 0, 0, 0, 3, + 0, 0, 0, 1, 0, 0, 0, 0, + 3, 0, 0, 1, 0, 0, 0, 0, + 3, 0, 0, 1, 0, 0, 0, 3, + 0, 0, 1, 0, 0, 0, 3, 0, + 0, 1, 0, 0, 0, 3, 0, 0, + 1, 0, 0, 0, 3, 0, 0, 1, + 0, 0, 0, 3, 0, 0, 0, 1, + 0, 0, 0, 0, 3, 0, 0, 1, + 0, 0, 0, 0, 3, 0, 0, 1, + 0, 0, 0, 3, 0, 0, 1, 0, + 0, 0, 3, 0, 1, 0, 0, 0, + 3, 0, 0, 1, 0, 0, 0, 3, + 0, 0, 1, 0, 0, 0, 3, 0, + 0, 1, 0, 0, 0, 3, 0, 0, + 0, 1, 0, 0, 0, 0, 3, 0, + 0, 1, 0, 0, 0, 0, 3, 0, + 0, 1, 0, 0, 0, 3, 0, 0, + 1, 0, 0, 0, 3, 0, 1, 0, + 0, 0, 3, 0, 0, 1, 0, 0, + 0, 3, 0, 0, 1, 0, 0, 0, + 3, 0, 0, 1, 0, 0, 0, 3, + 0, 0, 0, 1, 0, 0, 0, 0, + 3, 0, 0, 1, 0, 0, 0, 0, + 3, 0, 0, 1, 0, 0, 0, 3, + 0, 0, 1, 0, 0, 0, 3, 0, + 1, 0, 0, 0, 3, 0, 0, 1, + 0, 0, 0, 3, 0, 0, 1, 0, + 0, 0, 3, 0, 0, 1, 0, 0, + 0, 3, 0, 0, 0, 1, 0, 0, + 0, 0, 3, 0, 0, 1, 0, 0, + 0, 0, 3, 0, 0, 1, 0, 0, + 0, 3, 0, 0, 1, 0, 0, 0, + 3, 0, 1, 0, 0, 0, 3, 0, + 1, 3, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 3, 0, 3, 0, + 3, 0, 3, 0, 3, 0, 3, 0, + 3, 0, 3, 0, 3, 0, 3, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 1, 1, 3, 3, 0, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, + 3, 0, 0, 0}; static const short _address_eof_trans[] = { - 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, - 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101, - 1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, - 1110, 1111, 1112, 1113, 1114, 1115, 1116, 1117, - 1118, 1119, 1120, 1121, 1122, 1123, 1124, 1125, - 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, - 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, - 1142, 1143, 1144, 1145, 1146, 1147, 1148, 1149, - 1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157, - 1158, 1159, 1160, 1161, 1162, 1163, 1164, 1165, - 1166, 1167, 1168, 1169, 1170, 1171, 1172, 1173, - 1174, 1175, 1176, 1177, 1178, 1179, 1180, 1181, - 1182, 1183, 1184, 1185, 1186, 1187, 1188, 1189, - 1190, 1191, 1192, 1193, 1194, 1195, 1196, 1197, - 1198, 1199, 1200, 1201, 1202, 1203, 1204, 1205, - 1206, 1207, 1208, 1209, 1210, 1211, 1212, 1213, - 1214, 1215, 1216, 1217, 1218, 1219, 1220, 1221, - 1222, 1223, 1224, 1225, 1226, 1227, 1228, 1229, - 1230, 1231, 1232, 1233, 1234, 1235, 1236, 1237, - 1238, 1239, 1240, 1241, 1242, 1243, 1244, 1245, - 1246, 1247, 1248, 1249, 1250, 1251, 1252, 1253, - 1254, 1255, 1256, 1257, 1258, 1259, 1260, 1261, - 1262, 1263, 1264, 1265, 1266, 1267, 1268, 1269, - 1270, 1271, 1272, 1273, 1274, 1275, 1276, 1277, - 1278, 1279, 1280, 1281, 1282, 1283, 0 -}; + 1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, + 1094, 1095, 1096, 1097, 1098, 1099, 1100, 1101, + 1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, + 1110, 1111, 1112, 1113, 1114, 1115, 1116, 1117, + 1118, 1119, 1120, 1121, 1122, 1123, 1124, 1125, + 1126, 1127, 1128, 1129, 1130, 1131, 1132, 1133, + 1134, 1135, 1136, 1137, 1138, 1139, 1140, 1141, + 1142, 1143, 1144, 1145, 1146, 1147, 1148, 1149, + 1150, 1151, 1152, 1153, 1154, 1155, 1156, 1157, + 1158, 1159, 1160, 1161, 1162, 1163, 1164, 1165, + 1166, 1167, 1168, 1169, 1170, 1171, 1172, 1173, + 1174, 1175, 1176, 1177, 1178, 1179, 1180, 1181, + 1182, 1183, 1184, 1185, 1186, 1187, 1188, 1189, + 1190, 1191, 1192, 1193, 1194, 1195, 1196, 1197, + 1198, 1199, 1200, 1201, 1202, 1203, 1204, 1205, + 1206, 1207, 1208, 1209, 1210, 1211, 1212, 1213, + 1214, 1215, 1216, 1217, 1218, 1219, 1220, 1221, + 1222, 1223, 1224, 1225, 1226, 1227, 1228, 1229, + 1230, 1231, 1232, 1233, 1234, 1235, 1236, 1237, + 1238, 1239, 1240, 1241, 1242, 1243, 1244, 1245, + 1246, 1247, 1248, 1249, 1250, 1251, 1252, 1253, + 1254, 1255, 1256, 1257, 1258, 1259, 1260, 1261, + 1262, 1263, 1264, 1265, 1266, 1267, 1268, 1269, + 1270, 1271, 1272, 1273, 1274, 1275, 1276, 1277, + 1278, 1279, 1280, 1281, 1282, 1283, 0}; static const int address_start = 1; -static const int address_first_final = 196; -static const int address_error = 0; - -static const int address_en_main = 1; - - -#line 112 "smtp-address-validator.rl" - -bool is_address(const char* p, const char* pe) +bool is_address(const char *p, const char *pe) { int cs = 0; - - const char* eof = pe; - + + const char *eof = pe; + bool result = false; - -#line 675 "smtp-address-validator.cpp" { - cs = (int)address_start; + cs = (int) address_start; } - -#line 124 "smtp-address-validator.rl" - - -#line 680 "smtp-address-validator.cpp" { int _klen; unsigned int _trans = 0; - const signed char * _keys; - const signed char * _acts; + const signed char *_keys; + const signed char *_acts; unsigned int _nacts; - _resume: {} - if ( p == pe && p != eof ) + _resume : { + } + if (p == pe && p != eof) goto _out; - if ( p == eof ) { - if ( _address_eof_trans[cs] > 0 ) { - _trans = (unsigned int)_address_eof_trans[cs] - 1; + if (p == eof) { + if (_address_eof_trans[cs] > 0) { + _trans = (unsigned int) _address_eof_trans[cs] - 1; } - } - else { - _keys = ( _address_trans_keys + (_address_key_offsets[cs])); - _trans = (unsigned int)_address_index_offsets[cs]; - - _klen = (int)_address_single_lengths[cs]; - if ( _klen > 0 ) { + } else { + _keys = (_address_trans_keys + (_address_key_offsets[cs])); + _trans = (unsigned int) _address_index_offsets[cs]; + + _klen = (int) _address_single_lengths[cs]; + if (_klen > 0) { const signed char *_lower = _keys; const signed char *_upper = _keys + _klen - 1; const signed char *_mid; - while ( 1 ) { - if ( _upper < _lower ) { + while (1) { + if (_upper < _lower) { _keys += _klen; - _trans += (unsigned int)_klen; + _trans += (unsigned int) _klen; break; } - - _mid = _lower + ((_upper-_lower) >> 1); - if ( ( (*( p))) < (*( _mid)) ) + + _mid = _lower + ((_upper - _lower) >> 1); + if (((*(p))) < (*(_mid))) _upper = _mid - 1; - else if ( ( (*( p))) > (*( _mid)) ) + else if (((*(p))) > (*(_mid))) _lower = _mid + 1; else { - _trans += (unsigned int)(_mid - _keys); + _trans += (unsigned int) (_mid - _keys); goto _match; } } } - - _klen = (int)_address_range_lengths[cs]; - if ( _klen > 0 ) { + + _klen = (int) _address_range_lengths[cs]; + if (_klen > 0) { const signed char *_lower = _keys; - const signed char *_upper = _keys + (_klen<<1) - 2; + const signed char *_upper = _keys + (_klen << 1) - 2; const signed char *_mid; - while ( 1 ) { - if ( _upper < _lower ) { - _trans += (unsigned int)_klen; + while (1) { + if (_upper < _lower) { + _trans += (unsigned int) _klen; break; } - - _mid = _lower + (((_upper-_lower) >> 1) & ~1); - if ( ( (*( p))) < (*( _mid)) ) + + _mid = _lower + (((_upper - _lower) >> 1) & ~1); + if (((*(p))) < (*(_mid))) _upper = _mid - 2; - else if ( ( (*( p))) > (*( _mid + 1)) ) + else if (((*(p))) > (*(_mid + 1))) _lower = _mid + 2; else { - _trans += (unsigned int)((_mid - _keys)>>1); + _trans += (unsigned int) ((_mid - _keys) >> 1); break; } } } - - _match: {} + + _match : { } - cs = (int)_address_cond_targs[_trans]; - - if ( _address_cond_actions[_trans] != 0 ) { - - _acts = ( _address_actions + (_address_cond_actions[_trans])); - _nacts = (unsigned int)(*( _acts)); - _acts += 1; - while ( _nacts > 0 ) { - switch ( (*( _acts)) ) - { - case 0: { - { -#line 108 "smtp-address-validator.rl" - result = true; } - -#line 762 "smtp-address-validator.cpp" + } + cs = (int) _address_cond_targs[_trans]; - break; - } - case 1: { - { -#line 108 "smtp-address-validator.rl" - result = false; } - -#line 770 "smtp-address-validator.cpp" + if (_address_cond_actions[_trans] != 0) { - break; + _acts = (_address_actions + (_address_cond_actions[_trans])); + _nacts = (unsigned int) (*(_acts)); + _acts += 1; + while (_nacts > 0) { + switch ((*(_acts))) { + case 0: { + { + result = true; } + break; + } + case 1: { + { + result = false; + } + break; + } } _nacts -= 1; _acts += 1; } - } - - if ( p == eof ) { - if ( cs >= 196 ) + + if (p == eof) { + if (cs >= 196) goto _out; - } - else { - if ( cs != 0 ) { + } else { + if (cs != 0) { p += 1; goto _resume; } } - _out: {} + _out : { + } } - -#line 125 "smtp-address-validator.rl" - - return result; } diff --git a/Development/third_party/nlohmann/smtp-address-validator.hpp b/Development/third_party/nlohmann/smtp-address-validator.hpp index 5552c3eee..8d3c12bc9 100644 --- a/Development/third_party/nlohmann/smtp-address-validator.hpp +++ b/Development/third_party/nlohmann/smtp-address-validator.hpp @@ -29,6 +29,6 @@ SOFTWARE. */ -bool is_address(const char* p, const char* pe); +bool is_address(const char *p, const char *pe); #endif // SMTP_ADDRESS_PARSER_HPP_INCLUDED diff --git a/Development/third_party/nlohmann/string-format-check.cpp b/Development/third_party/nlohmann/string-format-check.cpp index bd8952ffd..ecc428f38 100644 --- a/Development/third_party/nlohmann/string-format-check.cpp +++ b/Development/third_party/nlohmann/string-format-check.cpp @@ -11,6 +11,16 @@ #include #include +#ifdef JSON_SCHEMA_BOOST_REGEX +# include +# define REGEX_NAMESPACE boost +#elif defined(JSON_SCHEMA_NO_REGEX) +# define NO_STD_REGEX +#else +# include +# define REGEX_NAMESPACE std +#endif + /** * Many of the RegExes are from @see http://jmrware.com/articles/2009/uri_regexp/URI_regex.html */ @@ -30,10 +40,10 @@ void range_check(const T value, const T min, const T max) /** @see date_time_check */ void rfc3339_date_check(const std::string &value) { - const static std::regex dateRegex{R"(^([0-9]{4})\-([0-9]{2})\-([0-9]{2})$)"}; + const static REGEX_NAMESPACE::regex dateRegex{R"(^([0-9]{4})\-([0-9]{2})\-([0-9]{2})$)"}; - std::smatch matches; - if (!std::regex_match(value, matches, dateRegex)) { + REGEX_NAMESPACE::smatch matches; + if (!REGEX_NAMESPACE::regex_match(value, matches, dateRegex)) { throw std::invalid_argument(value + " is not a date string according to RFC 3339."); } @@ -56,10 +66,10 @@ void rfc3339_date_check(const std::string &value) /** @see date_time_check */ void rfc3339_time_check(const std::string &value) { - const static std::regex timeRegex{R"(^([0-9]{2})\:([0-9]{2})\:([0-9]{2})(\.[0-9]+)?(?:[Zz]|((?:\+|\-)[0-9]{2})\:([0-9]{2}))$)"}; + const static REGEX_NAMESPACE::regex timeRegex{R"(^([0-9]{2})\:([0-9]{2})\:([0-9]{2})(\.[0-9]+)?(?:[Zz]|((?:\+|\-)[0-9]{2})\:([0-9]{2}))$)"}; - std::smatch matches; - if (!std::regex_match(value, matches, timeRegex)) { + REGEX_NAMESPACE::smatch matches; + if (!REGEX_NAMESPACE::regex_match(value, matches, timeRegex)) { throw std::invalid_argument(value + " is not a time string according to RFC 3339."); } @@ -131,10 +141,10 @@ void rfc3339_time_check(const std::string &value) */ void rfc3339_date_time_check(const std::string &value) { - const static std::regex dateTimeRegex{R"(^([0-9]{4}\-[0-9]{2}\-[0-9]{2})[Tt]([0-9]{2}\:[0-9]{2}\:[0-9]{2}(?:\.[0-9]+)?(?:[Zz]|(?:\+|\-)[0-9]{2}\:[0-9]{2}))$)"}; + const static REGEX_NAMESPACE::regex dateTimeRegex{R"(^([0-9]{4}\-[0-9]{2}\-[0-9]{2})[Tt]([0-9]{2}\:[0-9]{2}\:[0-9]{2}(?:\.[0-9]+)?(?:[Zz]|(?:\+|\-)[0-9]{2}\:[0-9]{2}))$)"}; - std::smatch matches; - if (!std::regex_match(value, matches, dateTimeRegex)) { + REGEX_NAMESPACE::smatch matches; + if (!REGEX_NAMESPACE::regex_match(value, matches, dateTimeRegex)) { throw std::invalid_argument(value + " is not a date-time string according to RFC 3339."); } @@ -182,7 +192,7 @@ const std::string uuid{R"([0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a- // from http://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address const std::string hostname{R"(^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$)"}; -bool is_ascii(std::string const& value) +bool is_ascii(std::string const &value) { for (auto ch : value) { if (ch & 0x80) { @@ -320,9 +330,9 @@ void rfc3986_uri_check(const std::string &value) R"((?:\#((?:[A-Za-z0-9\-._~!$&'()*+,;=:@\/?]|%[0-9A-Fa-f]{2})*))?)"}; const static std::string uriFormat{scheme + hierPart + query + fragment}; - const static std::regex uriRegex{uriFormat}; + const static REGEX_NAMESPACE::regex uriRegex{uriFormat}; - if (!std::regex_match(value, uriRegex)) { + if (!REGEX_NAMESPACE::regex_match(value, uriRegex)) { throw std::invalid_argument(value + " is not a URI string according to RFC 3986."); } } @@ -362,28 +372,28 @@ void default_string_format_check(const std::string &format, const std::string &v throw std::invalid_argument(value + " is not a valid idn-email according to RFC 6531."); } } else if (format == "hostname") { - static const std::regex hostRegex{hostname}; - if (!std::regex_match(value, hostRegex)) { + static const REGEX_NAMESPACE::regex hostRegex{hostname}; + if (!REGEX_NAMESPACE::regex_match(value, hostRegex)) { throw std::invalid_argument(value + " is not a valid hostname according to RFC 3986 Appendix A."); } } else if (format == "ipv4") { - const static std::regex ipv4Regex{"^" + ipv4Address + "$"}; - if (!std::regex_match(value, ipv4Regex)) { + const static REGEX_NAMESPACE::regex ipv4Regex{"^" + ipv4Address + "$"}; + if (!REGEX_NAMESPACE::regex_match(value, ipv4Regex)) { throw std::invalid_argument(value + " is not an IPv4 string according to RFC 2673."); } } else if (format == "ipv6") { - static const std::regex ipv6Regex{ipv6Address}; - if (!std::regex_match(value, ipv6Regex)) { + static const REGEX_NAMESPACE::regex ipv6Regex{ipv6Address}; + if (!REGEX_NAMESPACE::regex_match(value, ipv6Regex)) { throw std::invalid_argument(value + " is not an IPv6 string according to RFC 5954."); } } else if (format == "uuid") { - static const std::regex uuidRegex{uuid}; - if (!std::regex_match(value, uuidRegex)) { + static const REGEX_NAMESPACE::regex uuidRegex{uuid}; + if (!REGEX_NAMESPACE::regex_match(value, uuidRegex)) { throw std::invalid_argument(value + " is not an uuid string according to RFC 4122."); } } else if (format == "regex") { try { - std::regex re(value, std::regex::ECMAScript); + REGEX_NAMESPACE::regex re(value, std::regex::ECMAScript); } catch (std::exception &exception) { throw exception; } diff --git a/Documents/Architecture.md b/Documents/Architecture.md index 2612cc225..6b550bf05 100644 --- a/Documents/Architecture.md +++ b/Documents/Architecture.md @@ -1,10 +1,10 @@ # Architecture of nmos-cpp -The [nmos](../Development/nmos/) module fundamentally provides three things. +The [nmos](../Development/nmos/) module fundamentally provides three things. -1. A C++ data model for the AMWA IS-04 and IS-05 NMOS resources which represent the logical functionality of a Node, or equally, for the resources of many Nodes held by a Registry. -2. An implementation of each of the REST APIs defined by the AMWA IS-04 and IS-05 NMOS specifications, in terms of the data model. -3. An implementation of the Node and Registry "active behaviours" defined by the specifications. +1. A C++ data model for the AMWA NMOS resources which represent the logical functionality of a Node, or equally, for the resources of many Nodes held by a Registry. +2. An implementation of each of the REST APIs defined by the AMWA NMOS specifications, in terms of the data model. +3. An implementation of the Node and Registry "active behaviours" defined by the specifications. The module also provides the concept of a server which combines the REST APIs and behaviours into a single object for simplicity. @@ -14,12 +14,13 @@ The module also provides the concept of a server which combines the REST APIs an The top-level data structures for an NMOS Node and Registry are ``nmos::node_model`` and ``nmos::registry_model`` respectively. -A ``node_model`` has three member variables which are containers, of IS-04 resources, IS-05 resources and IS-07 resources, respectively: +A ``node_model`` has four member variables which are containers, of IS-04 resources, IS-05 resources, IS-07 resources and IS-08 resources, respectively: ```C++ nmos::resources node_resources; nmos::resources connection_resources; nmos::resources events_resources; +nmos::resources channelmapping_resources; ``` A ``registry_model`` has two containers, this time for the Registry's own Node API "self" resource, and for the resources that have been registered with the Registration API: @@ -116,11 +117,13 @@ for (;;) > [nmos/node_api.cpp](../Development/nmos/node_api.cpp), > [nmos/connection_api.cpp](../Development/nmos/connection_api.cpp), > [nmos/events_api.cpp](../Development/nmos/events_api.cpp), +> [nmos/channelmapping_api.cpp](../Development/nmos/channelmapping_api.cpp), > [nmos/registration_api.cpp](../Development/nmos/registration_api.cpp), > [nmos/query_api.cpp](../Development/nmos/query_api.cpp), -> [nmos/system_api.cpp](../Development/nmos/system_api.cpp) +> [nmos/system_api.cpp](../Development/nmos/system_api.cpp), +> [nmos/authorization_redirect_api.cpp](../Development/nmos/authorization_redirect_api.cpp) -The ``nmos`` module also provides the implementation of each of the REST APIs defined by AMWA IS-04, IS-05, IS-07 and IS-09. +The ``nmos`` module also provides the implementation of each of the REST APIs defined by AMWA IS-04, IS-05, IS-07, IS-08, IS-09 and IS-10. The C++ REST SDK provides a general purpose HTTP listener, that accepts requests at a particular base URL and passes them to a user-specified request handler for processing. Therefore the ``nmos`` module implements each API as a request handler which reads and/or writes the relevant parts of the NMOS data model, and provides a convenience function, ``nmos::support_api``, for associating the API request handler with the HTTP listener. @@ -167,7 +170,7 @@ The required Node behaviour includes: The state machine implemented by the ``nmos::node_behaviour_thread`` is shown below: -![NMOS Node Behaviour](images/node-behaviour.png) +![NMOS Node Behaviour](images/node-behaviour.png)
More details... @@ -211,7 +214,7 @@ The diagram below shows a sequence of events within and between an **nmos-cpp** Resource events initiated in a resource-scheduling thread in the Node are propagated via the Registration API to the Registry model. Events in the Registry model are sent in WebSocket messages to each Client with a matching Query API subscription. -![Sequence Diagram](images/node-registry-sequence.png) +![Sequence Diagram](images/node-registry-sequence.png) ## Servers diff --git a/Documents/Authorization.md b/Documents/Authorization.md new file mode 100644 index 000000000..d08ea1049 --- /dev/null +++ b/Documents/Authorization.md @@ -0,0 +1,110 @@ +# Authorization in nmos-cpp + +Authorization in nmos-cpp is based on the IS-10 / BCP-003-02 specifications, which are themselves based on _OAuth 2.0_. Authorization allows NMOS Nodes and Registries to protect APIs by limiting their access by third-party applications. Third-party applications might include Broadcast Controllers and other Nodes accessing NMOS APIs such as IS-04 and IS-05. + +## Overview + +A client such as a Broadcast Controller provides credentials to the Authorization Server. The Authorization Server grants the required access token(s) to the Controller for accessing protected APIs on NMOS Node(s). A Node will verify that the access token has the necessary access rights, and once successfully verified will allow access to that API. + +The access token is time-limited, and must be refreshed before it expires. It is recommended to attempt to refresh the token at least 15 seconds before its expiry, or at the half-life of the access token. + +To speed up the token validation process, the Node periodically fetches the Authorization Server's public keys, typically once every hour. The public keys allow the Node to perform local token validation without having to contact the Authorization Server every time an API is accessed. + +Similarly the Registry obtains the public keys from the Authorization Server which can be used to validate access tokens a Node obtains from the Authorization Server, and uses for Node registration and Registry heartbeats. + +## Client Registration + +In this context the term Client is used to refer to clients of the Authorization Server. In this way Broadcast Controllers, Registries and Nodes are all referred to as Clients. + +Clients locate the Authorization Server's API endpoints via DNS-SD. The Authorization Server has a well-known endpoint for returning server metadata. + +Clients must be registered to the Authorization Server before using the _OAuth 2.0_ protocol. In the event of successful registration, the Authorization Server will return the `client_id` for a public client and `client_id` and `client_secret` for a confidential client. + +However, it is important that a public client which is using the Authorization Code grant register one or more redirect URLs for security purposes. These allow the Authorization Server to ensure any authorization request is genuine and only valid redirect URLs are used for returning authorization codes. While using Client Credentials grant, _Private Key JWT_ can be used in client authentication for extra security. + +See the client registration sequence diagram below on how a Node is registered to the Authorization Server. + +![Client-Registration](images/Authorization-Client-Registration.png) + +## Access Token + +There are two ways of requesting access tokens from the Authomrization Server according to the type of authorization grant used. The grant type depends on the location and the nature of the Client involved in obtaining the access token. + +A number of grant types are defined in _OAuth 2.0_, but the IS-10/BCP-003-02 specifications focus on using the following grant types: +- Authorization Code Grant. +- Client Credentials Grant. + +### Authorization Code Grant + +This is the recommended grant type and should be used if the Client runs within a web browser (for instance a Broadcast Controller). An authorization code is returned by the Authorization Server via the Client's redirect URI. The Client can then exchange this code for a time-limited access token, which can be renewed with the refresh token. + +For public clients, there is a risk of an attacker hijacking the authorization code. To prevent this _Proof Key for Code Exchange_ (PKCE) is used to further secure the Authorization Code grant. + +The PCKE steps are: + +Step 1. Create a high entropy cryptographic random string, ``code_verifier``. + +Step 2. Convert the ``code_verifier`` to ``code_challenge`` with the following logic: + +``` +code_challenge=BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) +``` + +Step 3. Include the ``code_challenge`` and the hashing method used to generate the ``code_challenge`` in the authorization code request. + +Step 4. Send the ``code_verifier`` and the ``authorization code`` in exchange for the token. The Authorization Server uses the ``code_verifier`` to recreate the matching ``code_challenge`` to verify the client. + +![Authorization-Code-Flow](images/Authorization-Code-Flow.png) + +### Client Credentials Grant + +This type of authorization is used by Clients to obtain the access token without user interaction. This is for use by Nodes with no user interface. + +For extra security the Node uses _Private Key JWT_ to authenticate with the Authorization Server when requesting the access token. + +![Client-Credentials-Flow](images/Authorization-Client-Credentials-Flow.png) + +## Authorization Server Public Keys + +Public keys are used by the Node for validating the access token before allowing access to its protected APIs. The Client must periodically poll the Authorization Server for its public keys, typically once every hour. In the event that the Authorization Server is no longer available, the last fetched public keys will be kept in use until the Authorization Server connection is restored. + +Token validation is done by regenerating the matching token signature. This is done by signing the token header and the token payload. + +![Public-Keys](images/Authorization-Public-Keys.png) + +## Authorization Behaviour + +> [nmos/authorization_behaviour.cpp](../../Development/nmos/authorization_behaviour.cpp) + +The required Authorization behaviour includes: + +- Discovery of the Authorization Server. +- Fetch Authorization Server metadata for Authorization Server endpoints and supported features. +- Authorization Client registration. +- Fetch Authorization Server public keys. +- Fetch Bearer token for accessing protected endpoints. + +The state machine implemented by the ```nmos::experimental::authorization_behaviour_thread``` is shown below: + +![Authorization-behaviour](images/Authorization-behaviour.png) + +## Validating Access Tokens When Public Keys Are Missing + +> [nmos/authorization_handlers.cpp](../../Development/nmos/authorization_handlers.cpp) +> [nmos/authorization_behaviour.cpp](../../Development/nmos/authorization_behaviour.cpp) + +If no matching public key is available to validate the incoming access token the validation handler will trigger the authorization token issuer thread to fetch and cache the public keys from this token's issuer, which can then be used to validate any token issued by this issuer. + +The state machine implemented by the ```nmos::experimental::validate_authorization_handler``` and the ```nmos::experimental::authorization_token_issuer_thread``` are shown below: + +![missing-public-keys](images/Authorization-Missing-Public-Keys.png) + +In addition, if the Authorization behaviour thread is excluded, the Node/Registry can easily be configured as a headless _OAuth 2.0_ enabled device. + +In this case the access token will be fed in externally via the ```nmos::experimental::get_authorization_bearer_token_handler``` callback and the access token validation will be happening on the ```nmos::experimental::validate_authorization_token_handler``` callback. + +## OAuth 2.0 Node Registration Example + +The following is an overview of how an _OAuth 2.0_ Node registers to an _OAuth 2.0_ enabled Registry. + +![Node-Registration](images/Authorization-Node-Registration.png) diff --git a/Documents/Dependencies.md b/Documents/Dependencies.md index 4c31f4243..5c8490a8a 100644 --- a/Documents/Dependencies.md +++ b/Documents/Dependencies.md @@ -12,6 +12,7 @@ More details are given below. - This library incorporates some third-party material including WebSocket++, and also relies on e.g. some of the Boost C++ Libraries and [OpenSSL](https://www.openssl.org/). - The [WebSocket++](https://github.com/zaphoyd/websocketpp) header-only C++ websocket client/server library, to implement Query API websocket subscriptions - For JSON Schema validation, the [Modern C++ JSON schema validator](https://github.com/pboettch/json-schema-validator) library, which is implemented on top of [JSON for Modern C++](https://github.com/nlohmann/json) +- For creating and validating JSON Web Tokens, the [Thalhammer/jwt-cpp](https://github.com/Thalhammer/jwt-cpp) header only library - For DNS Service Discovery (DNS-SD), the [Bonjour SDK](https://developer.apple.com/bonjour/) on Windows, and on Linux either [Avahi](https://www.avahi.org/) or Apple's [mDNSResponder](https://opensource.apple.com/tarballs/mDNSResponder/) (another name for Bonjour) - The [Catch](https://github.com/philsquared/Catch) automated test framework, for unit testing @@ -33,7 +34,7 @@ Specific instructions for [cross-compiling for Raspberry Pi](Raspberry-Pi.md) ar 1. Download and install a recent [CMake stable release](https://cmake.org/download/#latest) for your platform Notes: - - Currently, CMake 3.17 or higher is required; version 3.24.2 (latest release at the time) has been tested + - Currently, CMake 3.24 or higher is required in order to use the Conan package manager; version 3.28.3 (latest release at the time) has been tested - Pre-built binary distributions are available for many platforms - On Linux distributions, e.g. Ubuntu 14.04 LTS (long-term support), the pre-built binary version available via ``apt-get`` may be too out-of-date Fetch, build and install a suitable version: @@ -46,7 +47,7 @@ Specific instructions for [cross-compiling for Raspberry Pi](Raspberry-Pi.md) ar sudo make install cd .. ``` - - Some CMake modules derived from third-party sources are included in the [third_party/cmake](../Development/third_party/cmake) directory + - Some CMake modules derived from third-party sources are supplied in the [third_party/cmake](../Development/third_party/cmake) directory ### Conan @@ -54,23 +55,16 @@ By default nmos-cpp uses [Conan](https://conan.io) to download most of its depen 1. Install Python 3 if necessary Note: The Python scripts directory needs to be added to the `PATH`, so the Conan executable can be found -2. Install or upgrade Conan using `pip install --upgrade conan~=1.47` +2. Install or upgrade Conan using `pip install --upgrade conan~=2.0.5` Notes: - - On some platforms with Python 2 and Python 3 both installed this may need to be `pip3 install --upgrade conan~=1.47` - - Currently, Conan 1.47 or higher (and lower than version 2.0) is required by the nmos-cpp recipe; dependencies may require a higher version; version 1.59.0 has been tested - - Conan evolves fairly quickly, so it's worth running `pip install --upgrade conan~=1.47` regularly - - By default [Conan assumes semver compatibility](https://docs.conan.io/en/1.42/creating_packages/define_abi_compatibility.html#versioning-schema). - Boost and other C++ libraries do not meet this expectation and break ABI compatibility between e.g. minor versions. - Unfortunately, the recipes in Conan Center Index do not generally customize their `package_id` method to take this into account. - Therefore it is strongly recommended to change Conan's default package id mode to `minor_mode` or a stricter mode such as `recipe_revision_mode`. - ```sh - conan config set general.default_package_id_mode=minor_mode - ``` + - On some platforms with Python 2 and Python 3 both installed this may need to be `pip3 install --upgrade conan~=2.0.5` + - Conan 2.0.5 or higher is required; dependencies may require a higher version; version 2.0.17 (latest release at the time) has been tested + - Conan 1.X is no longer supported 3. Install a [DNS Service Discovery](#dns-service-discovery) implementation, since this isn't currently handled by Conan Now follow the [Getting Started](Getting-Started.md) instructions directly. Conan is used to download the rest of the dependencies. -If you prefer not to use Conan, you must install Boost, WebSocket++, OpenSSL and C++ REST SDK as detailed below then call CMake with `-DNMOS_CPP_USE_CONAN:BOOL="0"` when building nmos-cpp. +If you prefer not to use Conan, you must install Boost, WebSocket++, OpenSSL and C++ REST SDK as detailed below. ### Boost C++ Libraries @@ -80,9 +74,9 @@ If using Conan, this section can be skipped. 1. Download a [recent release](http://www.boost.org/users/download/) Notes: - - Several Boost releases have been tested, including Version 1.80.0 (latest release at the time) and Version 1.54.0 + - Several Boost releases have been tested, including Version 1.83.0 (latest release at the time) and Version 1.54.0 - On Linux distributions, a Boost libraries package may already be installed, e.g. Ubuntu 14.04 LTS has Version 1.54.0 -2. Expand the archive so that, for example, the boost\_1\_80\_0 directory is at the same level as the nmos-cpp directory +2. Expand the archive so that, for example, the boost\_1\_83\_0 directory is at the same level as the nmos-cpp directory 3. Build and stage (or install) the following Boost libraries for your platform/toolset: - atomic - chrono @@ -133,11 +127,11 @@ If using Conan, this section can be skipped. If not using Conan... 1. Get the source code - - Clone the [repo](https://github.com/Microsoft/cpprestsdk/) and its submodules, and check out the v2.10.18 tag + - Clone the [repo](https://github.com/Microsoft/cpprestsdk/) and its submodules, and check out the v2.10.19 tag The ``git clone`` command option ``--recurse-submodules`` (formerly ``--recursive``) simplifies [cloning a project with submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules#_cloning_submodules). For example: ``` - git clone --recurse-submodules --branch v2.10.18 https://github.com/Microsoft/cpprestsdk /cpprestsdk + git clone --recurse-submodules --branch v2.10.19 https://github.com/Microsoft/cpprestsdk /cpprestsdk ``` Note: The downloadable archives created by GitHub cannot be used on their own since they don't include submodules. 2. Use CMake to configure for your platform @@ -152,8 +146,8 @@ If using Conan, this section can be skipped. - Set ``Boost_USE_STATIC_LIBS`` (BOOL) to ``1`` (true) - If CMake cannot find it automatically, set hints for [finding Boost](https://cmake.org/cmake/help/latest/module/FindBoost.html), for example: - *Either* set ``Boost_DIR`` (PATH) to the location of the installed BoostConfig.cmake (since Boost 1.70.0) - - *Or* set ``BOOST_INCLUDEDIR`` (PATH) and ``BOOST_LIBRARYDIR`` (PATH) to the appropriate full paths, e.g. *````*``/boost_1_80_0`` - and *````*``/boost_1_80_0/x64/lib`` respectively to match the suggested ``b2`` command + - *Or* set ``BOOST_INCLUDEDIR`` (PATH) and ``BOOST_LIBRARYDIR`` (PATH) to the appropriate full paths, e.g. *````*``/boost_1_83_0`` + and *````*``/boost_1_83_0/x64/lib`` respectively to match the suggested ``b2`` command - Due to interactions with other dependencies, it may also be necessary to explicitly set ``WERROR`` (BOOL) to ``0`` so that compiler warnings are not treated as errors - To speed up the build by omitting the C++ REST SDK sample apps and test suite, set ``BUILD_SAMPLES`` and ``BUILD_TESTS`` (BOOL) to ``0`` (false) 3. Use CMake to generate build/project files, and then build *and* install @@ -172,8 +166,8 @@ cmake .. ^ -DCPPREST_EXCLUDE_COMPRESSION:BOOL="1" ^ -DCMAKE_CONFIGURATION_TYPES:STRING="Debug;Release" ^ -DBoost_USE_STATIC_LIBS:BOOL="1" ^ - -DBOOST_INCLUDEDIR:PATH="/boost_1_80_0" ^ - -DBOOST_LIBRARYDIR:PATH="/boost_1_80_0/x64/lib" ^ + -DBOOST_INCLUDEDIR:PATH="/boost_1_83_0" ^ + -DBOOST_LIBRARYDIR:PATH="/boost_1_83_0/x64/lib" ^ -DWERROR:BOOL="0" ^ -DBUILD_SAMPLES:BOOL="0" ^ -DBUILD_TESTS:BOOL="0" @@ -234,15 +228,17 @@ If using Conan, this section can be skipped. If not using Conan... The C++ REST SDK depends on [OpenSSL](https://www.openssl.org/) (to implement secure HTTP and/or secure WebSockets). -It is compatible with the OpenSSL 1.1 API, so the 1.1.1 Long Term Support (LTS) release is recommended. -It is also possible to use OpenSSL 1.0, but the OpenSSL team announced that [users of that release are strongly advised to upgrade to OpenSSL 1.1.1](https://www.openssl.org/blog/blog/2018/09/11/release111/). +The nmos-cpp codebase also uses OpenSSL directly to implement the specific requirements of [AMWA BCP-003-01 Secure Communication in NMOS Systems](https://specs.amwa.tv/bcp-003-01/) and [AMWA BCP-003-02 Authorization in NMOS Systems](https://specs.amwa.tv/bcp-003-02/). -1. Download and install a recent release +OpenSSL version 3 is recommended, and version 3.2.1 (latest release at the time) has been tested. +It is currently also possible to use OpenSSL v1.1.1, although [this OpenSSL release is now end of life](https://www.openssl.org/blog/blog/2023/09/11/eol-111/index.html). + +1. Download and install a recent release Notes: - On Windows, an installer can be downloaded from [Shining Light Productions - Win32 OpenSSL](https://slproweb.com/products/Win32OpenSSL.html) - The Win64 OpenSSL v1.1.1s installer (latest release at the time) has been tested + The Win64 OpenSSL v3.2.1 installer (latest release at the time) has been tested - On Linux distributions, an OpenSSL package may already be available - The Ubuntu team announced an [OpenSSL 1.1.1 stable release update (SRU) for Ubuntu 18.04 LTS](https://lists.ubuntu.com/archives/ubuntu-devel/2018-December/040567.html) + For example, Ubuntu 22.04 LTS includes OpenSSL v3.0.2
@@ -252,9 +248,24 @@ If using Conan, this section can be skipped.
If not using Conan... -A copy of the source code necessary to use this library is included in the [third_party/nlohmann](../Development/third_party/nlohmann) directory. +A copy of the source code necessary to use this library is supplied in the [third_party/nlohmann](../Development/third_party/nlohmann) directory. +No installation is necessary. + +(The [Getting Started](Getting-Started.md) instructions explain how to set ``NMOS_CPP_USE_SUPPLIED_JSON_SCHEMA_VALIDATOR`` in order to use the supplied version when building nmos-cpp.) + +
+ +### jwt-cpp + +If using Conan, this section can be skipped. +
+If not using Conan... + +A copy of the source code necessary to use this library is supplied in the [third_party/jwt-cpp](../Development/third_party/jwt-cpp) directory. No installation is necessary. +(The [Getting Started](Getting-Started.md) instructions explain how to set ``NMOS_CPP_USE_SUPPLIED_JWT_CPP`` in order to use the supplied version when building nmos-cpp.) +
### DNS Service Discovery @@ -306,7 +317,7 @@ Notes: ### Catch -A copy of the single header version (v1.10.0) is included in the [third_party/catch](../Development/third_party/catch) directory. +A copy of the single header version (v1.10.0) is supplied in the [third_party/catch](../Development/third_party/catch) directory. No installation is necessary. # What Next? diff --git a/Documents/Getting-Started.md b/Documents/Getting-Started.md index f5bd80f2a..6b5e92bcc 100644 --- a/Documents/Getting-Started.md +++ b/Documents/Getting-Started.md @@ -32,12 +32,15 @@ Notes: - If CMake cannot find it automatically, set hints for [finding Boost](https://cmake.org/cmake/help/latest/module/FindBoost.html), for example: - *Either* set ``Boost_DIR`` (PATH) to the location of the installed *BoostConfig.cmake* (since Boost 1.70.0) - - *Or* set ``BOOST_INCLUDEDIR`` (PATH) and ``BOOST_LIBRARYDIR`` (PATH) to the appropriate full paths, e.g. *````*``/boost_1_80_0`` - and *````*``/boost_1_80_0/x64/lib`` respectively to match the suggested ``b2`` command + - *Or* set ``BOOST_INCLUDEDIR`` (PATH) and ``BOOST_LIBRARYDIR`` (PATH) to the appropriate full paths, e.g. *````*``/boost_1_83_0`` + and *````*``/boost_1_83_0/x64/lib`` respectively to match the suggested ``b2`` command - If CMake cannot find them automatically, set hints for finding the C++ REST SDK and WebSocket++, for example: - Set ``cpprestsdk_DIR`` (PATH) to the location of the installed *cpprestsdk-config.cmake* - *Either* set ``websocketpp_DIR`` (PATH) to the location of the installed *websocketpp-config.cmake* - *Or* set ``WEBSOCKETPP_INCLUDE_DIR`` (PATH) to the location of the WebSocket++ include files, e.g. *````*``/cpprestsdk/Release/libs/websocketpp`` to use the copy within the C++ REST SDK source tree + - Set flags to use the supplied Modern C++ JSON schema validator and jwt-cpp libraries + - Set ``NMOS_CPP_USE_SUPPLIED_JSON_SCHEMA_VALIDATOR`` (BOOL) to ``1`` (true) + - Set ``NMOS_CPP_USE_SUPPLIED_JWT_CPP`` (BOOL) to ``1`` (true) 3. Use CMake to generate build/project files, and then build @@ -49,7 +52,8 @@ Cache Variable | Default | Description -|-|- `NMOS_CPP_BUILD_EXAMPLES` | `ON` | Build example applications `NMOS_CPP_BUILD_TESTS` | `ON` | Build test suite application -`NMOS_CPP_USE_CONAN` | `ON` | Use Conan to acquire dependencies +`NMOS_CPP_USE_SUPPLIED_JSON_SCHEMA_VALIDATOR` | `OFF` | Use supplied third_party/nlohmann +`NMOS_CPP_USE_SUPPLIED_JWT_CPP` | `OFF` | Use supplied third_party/jwt-cpp `NMOS_CPP_USE_AVAHI` | `ON` | Use Avahi compatibility library rather than mDNSResponder **Windows** @@ -61,7 +65,8 @@ mkdir build cd build cmake .. ^ -G "Visual Studio 16 2019" ^ - -DCMAKE_CONFIGURATION_TYPES:STRING="Debug;Release" + -DCMAKE_CONFIGURATION_TYPES:STRING="Debug;Release" ^ + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES:STRING="third_party/cmake/conan_provider.cmake" ```
@@ -75,9 +80,11 @@ cmake .. ^ -G "Visual Studio 16 2019" ^ -DCMAKE_CONFIGURATION_TYPES:STRING="Debug;Release" ^ -DBoost_USE_STATIC_LIBS:BOOL="1" ^ - -DBOOST_INCLUDEDIR:PATH="/boost_1_80_0" ^ - -DBOOST_LIBRARYDIR:PATH="/boost_1_80_0/x64/lib" ^ - -DWEBSOCKETPP_INCLUDE_DIR:PATH="/cpprestsdk/Release/libs/websocketpp" + -DBOOST_INCLUDEDIR:PATH="/boost_1_83_0" ^ + -DBOOST_LIBRARYDIR:PATH="/boost_1_83_0/x64/lib" ^ + -DWEBSOCKETPP_INCLUDE_DIR:PATH="/cpprestsdk/Release/libs/websocketpp" ^ + -DNMOS_CPP_USE_SUPPLIED_JSON_SCHEMA_VALIDATOR:BOOL="1" ^ + -DNMOS_CPP_USE_SUPPLIED_JWT_CPP:BOOL="1" ```
@@ -97,7 +104,8 @@ cd /nmos-cpp/Development mkdir build cd build cmake .. \ - -DCMAKE_BUILD_TYPE:STRING="" + -DCMAKE_BUILD_TYPE:STRING="" \ + -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES:STRING="third_party/cmake/conan_provider.cmake" make ``` @@ -110,7 +118,9 @@ mkdir build cd build cmake .. \ -DCMAKE_BUILD_TYPE:STRING="" \ - -DWEBSOCKETPP_INCLUDE_DIR:PATH="/cpprestsdk/Release/libs/websocketpp" + -DWEBSOCKETPP_INCLUDE_DIR:PATH="/cpprestsdk/Release/libs/websocketpp" \ + -DNMOS_CPP_USE_SUPPLIED_JSON_SCHEMA_VALIDATOR:BOOL="1" \ + -DNMOS_CPP_USE_SUPPLIED_JWT_CPP:BOOL="1" make ``` diff --git a/Documents/Installation-with-Conan.md b/Documents/Installation-with-Conan.md new file mode 100644 index 000000000..13c106ed6 --- /dev/null +++ b/Documents/Installation-with-Conan.md @@ -0,0 +1,72 @@ +## Install nmos-cpp Using Conan + +The following steps describe how to install and set up nmos-cpp using the Conan package manager. +For many platforms, a binary package is available from Conan Center Index so it isn't necessary to build nmos-cpp or any of its dependencies. + +1. Install conan: + ```sh + pip install --upgrade conan~=2.0.5 + ``` + + `pip` is the package installer for Python. Install Python 3 if necessary. + + If the python Scripts directory is not on the PATH you will get a warning like: + > WARNING: The script conan.exe is installed in 'C:\Users\\%USERNAME%\\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.12_qbz5n2kfra8p0\LocalCache\local-packages\Python312\Scripts' which is not on PATH. + > Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location. + + On Windows, you can use _System Properties \> Environment Variables..._ to permanently add the directory to the user PATH. Restart the Command Prompt and run `conan --help` to ensure Conan is found. + +2. Detect profile: + ```sh + conan profile detect + ``` + This will create a Conan profile with values based on your current platform, compiler, etc. + Ideally the profile should be similar to these: + + Windows + ```ini + [settings] + arch=x86_64 + build_type=Release + compiler=msvc + compiler.cppstd=14 + compiler.runtime=dynamic + compiler.version=193 + os=Windows + ``` + Linux + ```ini + [settings] + arch=x86_64 + build_type=Release + compiler=gcc + compiler.cppstd=17 + compiler.version=11 + os=Linux + ``` + +3. Install nmos-cpp: + ```sh + conan install --tool-requires=nmos-cpp/cci.20240223 + ``` + This installs the **nmos-cpp-registry** and **nmos-cpp-node** applications in the Conan cache, and generates a script to make these executables available in the current session. + On Windows, run `.\conanbuild.bat` to add the install directory to the PATH. + On Linux, run `source conanbuild.sh`. + + Alternatively the nmos-cpp installation can be copied to the current working directory using a Conan deployer: + ```sh + conan install --requires=nmos-cpp/cci.20240223 --deployer=direct_deploy + ``` + + On Windows, the executables are then found in the _.\direct_deploy\nmos-cpp\bin\Release_ directory. + On Linux, the executables are found in the _./direct_deploy/nmos-cpp/bin_ directory. + +4. Try starting nmos-cpp-registry and/or nmos-cpp-node: + ```sh + nmos-cpp-registry + ``` + or + ```sh + nmos-cpp-node + ``` + For more information about running these applications and the JSON configuration file that can be passed on the command-line, see the [tutorial](Tutorial.md). diff --git a/Documents/Raspberry-Pi.md b/Documents/Raspberry-Pi.md index 74a521fc7..4f6cae53f 100644 --- a/Documents/Raspberry-Pi.md +++ b/Documents/Raspberry-Pi.md @@ -217,9 +217,10 @@ cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_TOOLCHAIN_FILE=${RPI_ROOT}/Toolchain-rpi.cmake \ -DWEBSOCKETPP_INCLUDE_DIR:PATH="~/cpprestsdk/Release/libs/websocketpp" \ + -DNMOS_CPP_USE_SUPPLIED_JSON_SCHEMA_VALIDATOR=1 \ + -DNMOS_CPP_USE_SUPPLIED_JWT_CPP=1 \ -DNMOS_CPP_USE_AVAHI=1 \ - -DAvahi_INCLUDE_DIR=${RPI_LIBS}/include/avahi-compat-libdns_sd \ - -DNMOS_CPP_USE_CONAN=0 + -DAvahi_INCLUDE_DIR=${RPI_LIBS}/include/avahi-compat-libdns_sd # Cross-compile the library. make diff --git a/Documents/images/Authorization-Client-Credentials-Flow.png b/Documents/images/Authorization-Client-Credentials-Flow.png new file mode 100644 index 000000000..58541ed15 Binary files /dev/null and b/Documents/images/Authorization-Client-Credentials-Flow.png differ diff --git a/Documents/images/Authorization-Client-Registration.png b/Documents/images/Authorization-Client-Registration.png new file mode 100644 index 000000000..27f6d7133 Binary files /dev/null and b/Documents/images/Authorization-Client-Registration.png differ diff --git a/Documents/images/Authorization-Code-Flow.png b/Documents/images/Authorization-Code-Flow.png new file mode 100644 index 000000000..ed5dc9842 Binary files /dev/null and b/Documents/images/Authorization-Code-Flow.png differ diff --git a/Documents/images/Authorization-Missing-Public-Keys.png b/Documents/images/Authorization-Missing-Public-Keys.png new file mode 100644 index 000000000..90a6d8ad6 Binary files /dev/null and b/Documents/images/Authorization-Missing-Public-Keys.png differ diff --git a/Documents/images/Authorization-Node-Registration.png b/Documents/images/Authorization-Node-Registration.png new file mode 100644 index 000000000..b1d0a611b Binary files /dev/null and b/Documents/images/Authorization-Node-Registration.png differ diff --git a/Documents/images/Authorization-Public-Keys.png b/Documents/images/Authorization-Public-Keys.png new file mode 100644 index 000000000..cb9e2df78 Binary files /dev/null and b/Documents/images/Authorization-Public-Keys.png differ diff --git a/Documents/images/Authorization-behaviour.png b/Documents/images/Authorization-behaviour.png new file mode 100644 index 000000000..2b69502ef Binary files /dev/null and b/Documents/images/Authorization-behaviour.png differ diff --git a/README.md b/README.md index 214e2f458..9feba6026 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,17 @@ This repository contains an implementation of the [AMWA Networked Media Open Spe - [AMWA IS-07 NMOS Event & Tally Specification](https://specs.amwa.tv/is-07/) - [AMWA IS-08 NMOS Audio Channel Mapping Specification](https://specs.amwa.tv/is-08/) - [AMWA IS-09 NMOS System Parameters Specification](https://specs.amwa.tv/is-09/) (originally defined in JT-NM TR-1001-1:2018 Annex A) +- [AMWA IS-10 NMOS Authorization Specification](https://specs.amwa.tv/is-10/) +- [AMWA IS-12 NMOS Control Protocol](https://specs.amwa.tv/is-12/) - [AMWA IS-13 NMOS Annotation Specification](https://specs.amwa.tv/is-13/) - [AMWA BCP-002-01 NMOS Grouping Recommendations - Natural Grouping](https://specs.amwa.tv/bcp-002-01/) - [AMWA BCP-002-02 NMOS Asset Distinguishing Information](https://specs.amwa.tv/bcp-002-02/) - [AMWA BCP-003-01 Secure Communication in NMOS Systems](https://specs.amwa.tv/bcp-003-01/) +- [AMWA BCP-003-02 Authorization in NMOS Systems](https://specs.amwa.tv/bcp-003-02/) - [AMWA BCP-004-01 NMOS Receiver Capabilities](https://specs.amwa.tv/bcp-004-01/) - [AMWA BCP-006-01 NMOS With JPEG XS](https://specs.amwa.tv/bcp-006-01/) +- [AMWA MS-05-01 NMOS Control Architecture](https://specs.amwa.tv/ms-05-01/) +- [AMWA MS-05-02 NMOS Control Framework](https://specs.amwa.tv/ms-05-02/) For more information about AMWA, NMOS and the Networked Media Incubator, please refer to . @@ -29,7 +34,10 @@ Some information about the overall design of **nmos-cpp** is also included in th ### Getting Started With NMOS The [Easy-NMOS](https://github.com/rhastie/easy-nmos) starter kit allows the user to launch a simple NMOS setup with minimal installation steps. -It relies on nmos-cpp to provide an NMOS Registry and a virtual NMOS Node in a Docker Compose network, along with the AMWA NMOS Testing Tool and supporting services. +It relies on a containerized nmos-cpp build to provide an NMOS Registry and a virtual NMOS Node in a Docker Compose network, along with the AMWA NMOS Testing Tool and supporting services. + +Alternatively, it is possible to install a pre-built package for many platforms. +See the instructions for [installing with Conan](Documents/Installation-with-Conan.md). ### Getting Started For Developers @@ -41,7 +49,7 @@ After setting up the dependencies, follow these [instructions](Documents/Getting Next, try out the registry and node applications in the [tutorial](Documents/Tutorial.md). -An [nmos-cpp Conan package](https://conan.io/center/nmos-cpp) is published at Conan Center Index. +An [nmos-cpp Conan package](https://conan.io/center/recipes/nmos-cpp) is occasionally published at Conan Center Index. ## Agile Development @@ -55,16 +63,18 @@ Several vendors have deployed JT-NM Tested badged products, using nmos-cpp, to t The following configurations, defined by the [build-test](.github/workflows/src/build-test.yml) jobs, are built and unit tested automatically via continuous integration. -| Platform | Version | Build Options | Test Options | -|----------|---------------------------|------------------------------------|--------------------------------------------| -| Linux | Ubuntu 22.04 (GCC 11.2.0) | Avahi | Secure Communications
Multicast DNS-SD | -| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
Multicast DNS-SD | -| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
Unicast DNS-SD | -| Linux | Ubuntu 20.04 (GCC 9.4.0) | mDNSResponder | Secure Communications
Multicast DNS-SD | -| Linux | Ubuntu 14.04 (GCC 4.8.4) | mDNSResponder, not using Conan | Secure Communications
Multicast DNS-SD | -| Windows | Server 2019 (VS 2019) | Bonjour (mDNSResponder), WinHTTP | Secure Communications
Multicast DNS-SD | -| Windows | Server 2019 (VS 2019) | Bonjour (mDNSResponder), ASIO | Secure Communications
Multicast DNS-SD | -| macOS | 11 (AppleClang 13.0) | Bonjour (mDNSResponder) | Secure Communications
Multicast DNS-SD | +| Platform | Version | Build Options | Test Options | +|----------|---------------------------|------------------------------------|--------------------------------------------------------------------| +| Linux | Ubuntu 22.04 (GCC 11.2.0) | Avahi | Secure Communications
Multicast DNS-SD | +| Linux | Ubuntu 22.04 (GCC 11.2.0) | Avahi | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Linux | Ubuntu 20.04 (GCC 9.4.0) | Avahi | Secure Communications
IS-10 Authorization
Unicast DNS-SD | +| Linux | Ubuntu 20.04 (GCC 9.4.0) | mDNSResponder | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Linux | Ubuntu 14.04 (GCC 4.8.4) | mDNSResponder, not using Conan | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Windows | Server 2019 (VS 2019) | Bonjour (mDNSResponder), WinHTTP | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| Windows | Server 2022 (VS 2022) | Bonjour (mDNSResponder), ASIO | Secure Communications
Multicast DNS-SD | +| Windows | Server 2022 (VS 2022) | Bonjour (mDNSResponder), ASIO | Secure Communications
IS-10 Authorization
Multicast DNS-SD | +| macOS | 11 (AppleClang 13.0) | Bonjour (mDNSResponder) | Secure Communications
IS-10 Authorization
Multicast DNS-SD | The [AMWA NMOS API Testing Tool](https://github.com/AMWA-TV/nmos-testing) is automatically run against the APIs of the **nmos-cpp-node** and **nmos-cpp-registry** applications. @@ -114,6 +124,9 @@ The implementation is designed to be extended. Development is ongoing, following Recent activity on the project (newest first): - Added support for IS-13 v1.0-dev +- Added support for the IS-12 NMOS Control Protocol +- Update to Conan 2; Conan 1.X is no longer supported +- Added support for IS-10 Authorization - Added support for HSTS and OCSP stapling - Added support for BCP-006-01 v1.0-dev, which can be demonstrated with **nmos-cpp-node** by using `"video_type": "video/jxsv"` - Updates to the GitHub Actions build-test workflow for better coverage of platforms and to include unicast DNS-SD tests diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index 9e45dcc6a..f22cf5804 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -59,7 +59,13 @@ common_params=",\ \"host_address\":\"${host_ip}\",\ \"host_addresses\":[\"${host_ip}\"]\ " - +registry_params=",\ + \"label\":\"nmos-cpp-registry\"\ + " +node_params=",\ + \"label\":\"nmos-cpp-node\"\ + " + if [[ "${config_secure}" == "True" ]]; then secure=true echo "Running TLS tests" @@ -110,6 +116,18 @@ fi if [[ "${config_auth}" == "True" ]]; then echo "Running Auth tests" auth=true + common_params+=",\ + \"server_authorization\":true,\ + \"service_unavailable_retry_after\":25\ + " + node_params+=",\ + \"client_authorization\":true,\ + \"authorization_flow\":\"client_credentials\",\ + \"authorization_scopes\":[\"registration\"],\ + \"token_endpoint_auth_method\":\"private_key_jwt\"\ + " + # 7 test cases test_06 to test_12 + (( expected_disabled_IS_09_01+=7 )) else echo "Running non-Auth tests" auth=false @@ -127,7 +145,7 @@ else (( expected_disabled_IS_09_01+=7 )) fi -"${node_command}" "{\"how_many\":6,\"http_port\":1080 ${common_params}}" > ${results_dir}/nodeoutput 2>&1 & +"${node_command}" "{\"how_many\":6,\"http_port\":1080 ${common_params} ${node_params}}" > ${results_dir}/nodeoutput 2>&1 & NODE_PID=$! function do_run_test() { @@ -194,7 +212,7 @@ do_run_test IS-08-02 $expected_disabled_IS_08_02 --host "${host}" "${host}" --po do_run_test IS-09-02 $expected_disabled_IS_09_02 --host "${host}" null --port 0 0 --version null v1.0 # Run Registry tests (leave Node running) -"${registry_command}" "{\"pri\":0,\"http_port\":8088 ${common_params}}" > ${results_dir}/registryoutput 2>&1 & +"${registry_command}" "{\"pri\":0,\"http_port\":8088 ${common_params} ${registry_params}}" > ${results_dir}/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