diff --git a/third_party/angular-material/CMakeLists.txt b/.github/dependabot.yml similarity index 81% rename from third_party/angular-material/CMakeLists.txt rename to .github/dependabot.yml index a772e21457c..f81e6a9dde3 100644 --- a/third_party/angular-material/CMakeLists.txt +++ b/.github/dependabot.yml @@ -1,5 +1,5 @@ # -# Copyright (c) 2020, The OpenThread Authors. +# Copyright (c) 2021, The OpenThread Authors. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -26,9 +26,15 @@ # POSSIBILITY OF SUCH DAMAGE. # - -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/repo/angular-material.min.js - DESTINATION ${OTBR_WEB_DATADIR}/frontend/res/js) - -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/repo/angular-material.min.css - DESTINATION ${OTBR_WEB_DATADIR}/frontend/res/css) +version: 2 +updates: + - package-ecosystem: "gitsubmodule" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-name: "third_party/openthread/repo" + commit-message: + prefix: "submodule" + rebase-strategy: "disabled" + open-pull-requests-limit: 1 diff --git a/.github/workflows/border_router.yml b/.github/workflows/border_router.yml new file mode 100644 index 00000000000..d87f82667b2 --- /dev/null +++ b/.github/workflows/border_router.yml @@ -0,0 +1,118 @@ +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +name: Border Router + +on: + push: + branches-ignore: + - 'dependabot/**' + pull_request: + branches: + - 'main' + +jobs: + + cancel-previous-runs: + runs-on: ubuntu-20.04 + steps: + - uses: rokroskar/workflow-run-cleanup-action@master + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + if: "github.ref != 'refs/heads/main'" + + border-router: + runs-on: ubuntu-20.04 + + strategy: + matrix: + include: + - name: "Border Router" + otbr_options: "-DOT_TREL=OFF -DOT_MLR=ON -DOTBR_COVERAGE=ON -DOT_SRP_SERVER=ON -DOT_ECDSA=ON -DOT_SERVICE=ON -DOTBR_DUA_ROUTING=OFF" + border_routing: 1 + cert_scripts: ./tests/scripts/thread-cert/border_router/*.py + - name: "Backbone Router" + otbr_options: "-DOT_TREL=OFF -DOT_DUA=ON -DOT_MLR=ON -DOTBR_COVERAGE=ON -DOT_SRP_SERVER=ON -DOT_ECDSA=ON -DOT_SERVICE=ON -DOTBR_DUA_ROUTING=ON" + border_routing: 0 + cert_scripts: ./tests/scripts/thread-cert/backbone/*.py + name: ${{ matrix.name }} + env: + PACKET_VERIFICATION: 1 + THREAD_VERSION: 1.2 + VIRTUAL_TIME: 0 + PYTHONUNBUFFERED: 1 + REFERENCE_DEVICE: 1 + OTBR_COVERAGE: 1 + READLINE: readline + INTER_OP: 0 + INTER_OP_BBR: 0 + BORDER_ROUTING: ${{ matrix.border_routing }} + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Build OTBR Docker Image + run: | + # We need the `-DOT_SRP_SERVER=ON` option because the `bbr_5_11_01.py` script is referring SRP server. + # This should be fixed by enhancing the test script to handle SRP server situations properly. + otbr_options="${{ matrix.otbr_options }}" + otbr_image_name="otbr-ot12-backbone-ci" + docker build -t "${otbr_image_name}" -f etc/docker/Dockerfile . \ + --build-arg BORDER_ROUTING=${{ matrix.border_routing }} \ + --build-arg INFRA_IF_NAME=eth0 \ + --build-arg BACKBONE_ROUTER=1 \ + --build-arg REFERENCE_DEVICE=1 \ + --build-arg OT_BACKBONE_CI=1 \ + --build-arg NAT64=0 \ + --build-arg OTBR_OPTIONS="${otbr_options}" + - name: Bootstrap OpenThread Test + run: | + sudo rm /etc/apt/sources.list.d/* && sudo apt-get update + sudo apt-get --no-install-recommends install -y python3-setuptools python3-wheel ninja-build socat nodejs npm + python3 -m pip install -r third_party/openthread/repo/tests/scripts/thread-cert/requirements.txt + - name: Build OpenThread + run: | + (cd third_party/openthread/repo && ./script/test build) + - name: Get Thread-Wireshark + run: | + (cd third_party/openthread/repo && ./script/test get_thread_wireshark) + - name: Run Test + run: | + export CI_ENV="$(bash <(curl -s https://codecov.io/env)) -e GITHUB_ACTIONS -e OTBR_COVERAGE" + echo "CI_ENV=${CI_ENV}" + (cd third_party/openthread/repo && sudo -E ./script/test cert_suite ${{ matrix.cert_scripts }} || (sudo chmod a+r *.log *.json *.pcap && false)) + - uses: actions/upload-artifact@v2 + if: ${{ failure() }} + with: + name: thread-1-2-backbone-results + path: | + third_party/openthread/repo/*.pcap + third_party/openthread/repo/*.json + third_party/openthread/repo/*.log + - name: Codecov + uses: codecov/codecov-action@v1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac6b27f6628..8d4f4546a76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,56 +1,62 @@ # -## Copyright (c) 2020, The OpenThread Authors. -## All rights reserved. -## -## Redistribution and use in source and binary forms, with or without -## modification, are permitted provided that the following conditions are met: -## 1. Redistributions of source code must retain the above copyright -## notice, this list of conditions and the following disclaimer. -## 2. Redistributions in binary form must reproduce the above copyright -## notice, this list of conditions and the following disclaimer in the -## documentation and/or other materials provided with the distribution. -## 3. Neither the name of the copyright holder nor the -## names of its contributors may be used to endorse or promote products -## derived from this software without specific prior written permission. -## -## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -## AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -## IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -## ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -## LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -## CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -## SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -## INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -## CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -## ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -## POSSIBILITY OF SUCH DAMAGE. -## +# Copyright (c) 2020, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# name: Build -on: [push, pull_request] +on: + push: + branches-ignore: + - 'dependabot/**' + pull_request: + branches: + - 'main' jobs: cancel-previous-runs: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: rokroskar/workflow-run-cleanup-action@master env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - if: "github.ref != 'refs/heads/master'" + if: "github.ref != 'refs/heads/main'" pretty: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Bootstrap - run: sudo BUILD_TARGET=pretty-check tests/scripts/bootstrap.sh + run: BUILD_TARGET=pretty-check tests/scripts/bootstrap.sh - name: Check run: script/make-pretty check android-check: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 strategy: matrix: mdns: ["mDNSResponder", ""] @@ -62,12 +68,12 @@ jobs: env: OTBR_MDNS: ${{ matrix.mdns }} run: > - docker run --rm -v $PWD:/build/ot-br-posix openthread/android-trusty bash -c + docker run --rm -v $PWD:/build/ot-br-posix -v $PWD/third_party/openthread/repo:/build/external/openthread openthread/android-trusty bash -c "BUILD_TARGET=android-check ot-br-posix/tests/scripts/bootstrap.sh && \ ot-br-posix/tests/scripts/check-android-build" check: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 strategy: matrix: mdns: ["mDNSResponder", "avahi"] @@ -107,7 +113,7 @@ jobs: uses: codecov/codecov-action@v1 script-check: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 env: BUILD_TARGET: script-check OTBR_COVERAGE: 1 @@ -123,7 +129,7 @@ jobs: uses: codecov/codecov-action@v1 scan-build: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 env: BUILD_TARGET: scan-build CC: clang @@ -152,49 +158,3 @@ jobs: cmake --version | grep 3.10.3 - name: Build run: script/test package - - thread-1-2-backbone: - runs-on: ubuntu-18.04 - env: - PACKET_VERIFICATION: 1 - REFERENCE_DEVICE: 1 - THREAD_VERSION: 1.2 - INTER_OP: 1 - VIRTUAL_TIME: 0 - PYTHONUNBUFFERED: 1 - OTBR_COVERAGE: 1 - steps: - - uses: actions/checkout@v2 - with: - submodules: true - - name: Build OTBR Docker Image - run: | - otbr_options="-DOTBR_BACKBONE_ROUTER=ON -DOT_DUA=ON -DOT_MLR=ON -DOTBR_COVERAGE=ON" - otbr_image_name="otbr-ot12-backbone-ci" - docker build -t "${otbr_image_name}" -f etc/docker/Dockerfile . --build-arg REFERENCE_DEVICE=1 --build-arg OT_BACKBONE_CI=1 --build-arg OTBR_OPTIONS="${otbr_options}" - - name: Bootstrap OpenThread Test - run: | - sudo rm /etc/apt/sources.list.d/* && sudo apt-get update - sudo apt-get --no-install-recommends install -y python3-setuptools python3-wheel ninja-build socat - python3 -m pip install -r third_party/openthread/repo/tests/scripts/thread-cert/requirements.txt - - name: Build OpenThread - run: | - (cd third_party/openthread/repo && ./script/test build) - - name: Get Thread-Wireshark - run: | - (cd third_party/openthread/repo && ./script/test get_thread_wireshark) - - name: Run Test - run: | - export CI_ENV="$(bash <(curl -s https://codecov.io/env)) -e GITHUB_ACTIONS -e OTBR_COVERAGE" - echo "CI_ENV=${CI_ENV}" - (cd third_party/openthread/repo && sudo -E ./script/test cert_bbr ./tests/scripts/thread-cert/backbone/*.py || (sudo chmod a+r *.log *.json *.pcap && false)) - - uses: actions/upload-artifact@v2 - if: ${{ failure() }} - with: - name: thread-1-2-backbone-results - path: | - third_party/openthread/repo/*.pcap - third_party/openthread/repo/*.json - third_party/openthread/repo/*.log - - name: Codecov - uses: codecov/codecov-action@v1 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index c60a6959c8e..baf7d41ac91 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -28,78 +28,129 @@ name: Docker -on: [push, pull_request] +on: + push: + branches-ignore: + - 'dependabot/**' + pull_request: + branches: + - 'main' jobs: cancel-previous-runs: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: rokroskar/workflow-run-cleanup-action@master env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - if: "github.ref != 'refs/heads/master'" + if: "github.ref != 'refs/heads/main'" - buildx: + docker-check: runs-on: ubuntu-latest + strategy: + matrix: + rcp_bus: ["UART", "SPI"] + env: + OT_POSIX_CONFIG_RCP_BUS: ${{matrix.rcp_bus}} + OTBR_COVERAGE: 1 + VERBOSE: 1 + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Bootstrap + env: + BUILD_TARGET: "docker-check" + run: tests/scripts/bootstrap.sh + - name: Check + run: tests/scripts/check-docker + + buildx: + runs-on: ubuntu-20.04 strategy: matrix: include: - - build_args: "" + - image_tag: "latest" + base_image: "ubuntu:bionic" + build_args: "" + platforms: "linux/amd64,linux/arm/v7,linux/arm64" + push: yes + - image_tag: "focal" + base_image: "ubuntu:focal" + build_args: "" + platforms: "linux/amd64,linux/arm64" + push: yes + - image_tag: "reference-device" + base_image: "ubuntu:bionic" + build_args: >- + --build-arg REFERENCE_DEVICE=1 + --build-arg BORDER_ROUTING=0 + --build-arg BACKBONE_ROUTER=1 + --build-arg NAT64=0 + --build-arg WEB_GUI=0 + --build-arg REST_API=0 + --build-arg OTBR_OPTIONS='-DOTBR_DUA_ROUTING=ON -DOT_DUA=ON -DOT_MLR=ON' + platforms: "linux/amd64,linux/arm/v7,linux/arm64" push: yes - - build_args: "--build-arg OT_BACKBONE_CI=1" + - image_tag: "test" + base_image: "ubuntu:bionic" + build_args: >- + --build-arg OT_BACKBONE_CI=1 + --build-arg REFERENCE_DEVICE=1 + --build-arg BACKBONE_ROUTER=1 + --build-arg OTBR_OPTIONS='-DOTBR_DUA_ROUTING=ON -DOT_DUA=ON -DOT_MLR=ON' + platforms: "linux/amd64,linux/arm/v7,linux/arm64" + push: no steps: - uses: actions/checkout@v2 with: submodules: true - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - with: - platforms: all - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 + - name: Prepare id: prepare run: | DOCKER_IMAGE=openthread/otbr - DOCKER_PLATFORMS=linux/amd64,linux/arm/v7,linux/arm64 - VERSION=latest + DOCKER_PLATFORMS=${{ matrix.platforms }} + VERSION=${{ matrix.image_tag }} TAGS="--tag ${DOCKER_IMAGE}:${VERSION}" echo ::set-output name=docker_image::${DOCKER_IMAGE} echo ::set-output name=version::${VERSION} - echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \ + echo ::set-output name=buildx_args::"--platform ${DOCKER_PLATFORMS} \ + --build-arg BASE_IMAGE=${{ matrix.base_image }} \ --build-arg VERSION=${VERSION} \ --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \ --build-arg VCS_REF=${GITHUB_SHA::8} \ ${{ matrix.build_args }} \ - ${TAGS} --file etc/docker/Dockerfile . + ${TAGS} --file etc/docker/Dockerfile ." + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + with: + platforms: all + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 - name: Docker Buildx (build) run: | docker buildx build --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args }} - - name: Docker Login + - name: Login to DockerHub if: success() && github.repository == 'openthread/ot-br-posix' && github.event_name != 'pull_request' && matrix.push - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: | - echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} - name: Docker Buildx (push) if: success() && github.repository == 'openthread/ot-br-posix' && github.event_name != 'pull_request' && matrix.push run: | docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }} - - name: Docker Check Manifest - if: always() && github.repository == 'openthread/ot-br-posix' && github.event_name != 'pull_request' && matrix.push - run: | - docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }} - - - name: Clear + - name: Inspect Image if: always() && github.repository == 'openthread/ot-br-posix' && github.event_name != 'pull_request' && matrix.push run: | - rm -f ${HOME}/.docker/config.json + docker buildx imagetools inspect ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }} diff --git a/.travis/after_success.sh b/.github/workflows/documentation.yml old mode 100755 new mode 100644 similarity index 62% rename from .travis/after_success.sh rename to .github/workflows/documentation.yml index 9f64999716c..6eb63f6f5ec --- a/.travis/after_success.sh +++ b/.github/workflows/documentation.yml @@ -1,6 +1,5 @@ -#!/bin/bash # -# Copyright (c) 2019, The OpenThread Authors. +# Copyright (c) 2021, The OpenThread Authors. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -27,28 +26,40 @@ # POSSIBILITY OF SUCH DAMAGE. # -[ -n "$BUILD_TARGET" ] || exit 0 +name: Documentation -set -e +on: + push: + branches: + - 'main' -codecov_upload() -{ - curl -s https://codecov.io/bash >codecov - chmod a+x codecov +jobs: + cancel-previous-runs: + runs-on: ubuntu-latest + steps: + - uses: rokroskar/workflow-run-cleanup-action@master + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + if: "github.ref != 'refs/heads/main'" - # Assume gcov by default, and llvm-cov if CC is clang - if [[ -z $CC ]]; then - ./codecov - elif "$CC" --version | grep -q gcc; then - ./codecov - elif "$CC" --version | grep -q clang; then - ./codecov -x "llvm-cov gcov" - fi -} - -main() -{ - codecov_upload -} - -main "$@" + doxygen: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Bootstrap + run: | + sudo apt-get update + sudo apt-get install -y doxygen libdbus-1-dev libglib2.0-dev-bin xmlto + - name: Generate + run: | + mkdir build-doc + cd build-doc + cmake -DBUILD_TESTING=OFF -DOTBR_DOC=ON -DOTBR_DBUS=ON .. + make otbr-doc + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./build-doc/doc/html diff --git a/.travis/script.sh b/.github/workflows/macOS.yml old mode 100755 new mode 100644 similarity index 68% rename from .travis/script.sh rename to .github/workflows/macOS.yml index 5432e6a884a..7980872f2ea --- a/.travis/script.sh +++ b/.github/workflows/macOS.yml @@ -1,6 +1,5 @@ -#!/bin/bash # -# Copyright (c) 2017, The OpenThread Authors. +# Copyright (c) 2021, The OpenThread Authors. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -27,40 +26,36 @@ # POSSIBILITY OF SUCH DAMAGE. # -set -e -set -x - -export PATH=/usr/local/bin:$PATH - -case $BUILD_TARGET in - meshcop) - ./script/bootstrap - ./script/test build - - OT_CLI="ot-cli-mtd" ./script/test meshcop - OT_CLI="ot-cli-ftd" ./script/test meshcop - OTBR_USE_WEB_COMMISSIONER=1 ./script/test meshcop - ;; - - openwrt-check) - ./script/test openwrt - ;; - - docker-check) - .travis/check-docker - ;; - - otbr-dbus-check) - .travis/check-otbr-dbus - ;; - - macOS) - # On Travis, brew install fails when a package is already installed, so use reinstall here instead of ./script/bootstrap - brew unlink python@2 || true +name: macOS + +on: + push: + branches-ignore: + - 'dependabot/**' + pull_request: + branches: + - 'main' + +jobs: + + cancel-previous-runs: + runs-on: ubuntu-20.04 + steps: + - uses: rokroskar/workflow-run-cleanup-action@master + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + if: "github.ref != 'refs/heads/main'" + + build-check: + runs-on: macos-10.15 + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Bootstrap + run: | + brew update brew reinstall boost cmake cpputest dbus jsoncpp ninja + - name: Build + run: | OTBR_OPTIONS='-DOTBR_MDNS=OFF' ./script/test build - ;; - *) - false - ;; -esac diff --git a/.github/workflows/meshcop.yml b/.github/workflows/meshcop.yml new file mode 100644 index 00000000000..949dd3d17d1 --- /dev/null +++ b/.github/workflows/meshcop.yml @@ -0,0 +1,80 @@ +# +# Copyright (c) 2020, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +name: MeshCoP + +on: + push: + branches-ignore: + - 'dependabot/**' + pull_request: + branches: + - 'main' + +jobs: + + cancel-previous-runs: + runs-on: ubuntu-latest + steps: + - uses: rokroskar/workflow-run-cleanup-action@master + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + if: "github.ref != 'refs/heads/main'" + + meshcop: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Bootstrap + env: + BUILD_TARGET: "meshcop" + run: tests/scripts/bootstrap.sh + - name: Build + run: | + script/bootstrap + script/test build + - name: mDNS service + env: + TEST_CASE: "mdns_service" + run: script/test meshcop + - name: MTD + env: + OT_CLI: "ot-cli-mtd" + run: script/test meshcop + - name: FTD + env: + OT_CLI: "ot-cli-ftd" + run: script/test meshcop + - name: Web Commissioner + env: + OTBR_USE_WEB_COMMISSIONER: 1 + run: script/test meshcop + - name: Codecov + uses: codecov/codecov-action@v1 diff --git a/.github/workflows/openwrt.yml b/.github/workflows/openwrt.yml new file mode 100644 index 00000000000..a6c48af5547 --- /dev/null +++ b/.github/workflows/openwrt.yml @@ -0,0 +1,60 @@ +# +# Copyright (c) 2020, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +name: OpenWrt + +on: + push: + branches-ignore: + - 'dependabot/**' + pull_request: + branches: + - 'main' + +jobs: + + cancel-previous-runs: + runs-on: ubuntu-latest + steps: + - uses: rokroskar/workflow-run-cleanup-action@master + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + if: "github.ref != 'refs/heads/main'" + + openwrt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Bootstrap + env: + BUILD_TARGET: "openwrt-check" + run: tests/scripts/bootstrap.sh + - name: Check + run: script/test openwrt diff --git a/.github/workflows/raspbian.yml b/.github/workflows/raspbian.yml index ed798b39a17..b4a180dde6d 100644 --- a/.github/workflows/raspbian.yml +++ b/.github/workflows/raspbian.yml @@ -1,54 +1,60 @@ # -## Copyright (c) 2020, The OpenThread Authors. -## All rights reserved. -## -## Redistribution and use in source and binary forms, with or without -## modification, are permitted provided that the following conditions are met: -## 1. Redistributions of source code must retain the above copyright -## notice, this list of conditions and the following disclaimer. -## 2. Redistributions in binary form must reproduce the above copyright -## notice, this list of conditions and the following disclaimer in the -## documentation and/or other materials provided with the distribution. -## 3. Neither the name of the copyright holder nor the -## names of its contributors may be used to endorse or promote products -## derived from this software without specific prior written permission. -## -## THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -## AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -## IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -## ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE -## LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -## CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -## SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -## INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -## CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -## ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -## POSSIBILITY OF SUCH DAMAGE. -## +# Copyright (c) 2020, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# name: Raspbian -on: [push, pull_request] +on: + push: + branches-ignore: + - 'dependabot/**' + pull_request: + branches: + - 'main' jobs: cancel-previous-runs: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: rokroskar/workflow-run-cleanup-action@master env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" - if: "github.ref != 'refs/heads/master'" + if: "github.ref != 'refs/heads/main'" raspbian-check: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 env: - IMAGE_URL: http://director.downloads.raspberrypi.org/raspbian_lite/images/raspbian_lite-2018-04-19/2018-04-18-raspbian-stretch-lite.zip + IMAGE_URL: https://downloads.raspberrypi.org/raspios_lite_armhf/images/raspios_lite_armhf-2021-01-12/2021-01-11-raspios-buster-armhf-lite.zip BUILD_TARGET: raspbian-gcc steps: - uses: actions/checkout@v2 with: - submodules: true + submodules: recursive - name: Bootstrap run: tests/scripts/bootstrap.sh - name: Build diff --git a/.gitignore b/.gitignore index 96311abbd3b..15c86358b23 100644 --- a/.gitignore +++ b/.gitignore @@ -105,6 +105,10 @@ tests/unit/unittest # Vim auto complete helper .ycm_extra_conf.py +# CLion +.idea/** +cmake-build-*/** + # Thread local storage ./tmp/ *.data diff --git a/.gitmodules b/.gitmodules index b6c5ee50011..78cdcf5b2e8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,13 +1,10 @@ [submodule "third_party/openthread/repo"] path = third_party/openthread/repo url = https://github.com/openthread/openthread.git - branch = master + branch = main [submodule "third_party/cJSON/repo"] path = third_party/cJSON/repo url = https://github.com/DaveGamble/cJSON.git [submodule "third_party/http-parser/repo"] path = third_party/http-parser/repo url = https://github.com/nodejs/http-parser.git -[submodule "third_party/d3js/repo"] - path = third_party/d3js/repo - url = https://github.com/d3/d3.git diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 85f96c0c582..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -language: python -python: "3.6" - -dist: bionic -os: linux - -cache: - directories: - - $HOME/.cache/tools - -before_install: ./tests/scripts/bootstrap.sh -script: .travis/script.sh -after_success: - - .travis/after_success.sh - -jobs: - include: - - env: BUILD_TARGET="docker-check" OTBR_COVERAGE=1 VERBOSE=1 - - env: BUILD_TARGET="docker-check" OT_POSIX_CONFIG_RCP_BUS=SPI OTBR_COVERAGE=1 VERBOSE=1 - - env: BUILD_TARGET="openwrt-check" VERBOSE=1 - - env: BUILD_TARGET="meshcop" OTBR_COVERAGE=1 VERBOSE=1 - - env: BUILD_TARGET="macOS" - os: osx - language: "generic" diff --git a/Android.mk b/Android.mk index e89bae9a9eb..b1ec60ca2e2 100644 --- a/Android.mk +++ b/Android.mk @@ -28,6 +28,12 @@ LOCAL_PATH := $(call my-dir) +ifeq ($(OTBR_ENABLE_ANDROID_MK),1) + +ifneq ($(OTBR_PROJECT_ANDROID_MK),) +include $(OTBR_PROJECT_ANDROID_MK) +endif + include $(CLEAR_VARS) LOCAL_MODULE_CLASS := STATIC_LIBRARIES @@ -58,18 +64,25 @@ include $(BUILD_STATIC_LIBRARY) include $(CLEAR_VARS) -$(LOCAL_PATH)/src/dbus/server/dbus_thread_object.cpp: $(LOCAL_PATH)/src/dbus/server/introspect.hpp +LOCAL_MODULE_CLASS := EXECUTABLES +LOCAL_MODULE := otbr-agent +LOCAL_MODULE_TAGS := eng +LOCAL_SHARED_LIBRARIES := libdbus -$(LOCAL_PATH)/src/dbus/server/introspect.hpp: $(LOCAL_PATH)/src/dbus/server/introspect.xml +OTBR_GEN_HEADER_DIR := $(local-intermediates-dir)/gen +OTBR_GEN_DBUS_INTROSPECT_HEADER := $(OTBR_GEN_HEADER_DIR)/dbus/server/introspect.hpp + +$(OTBR_GEN_DBUS_INTROSPECT_HEADER): $(LOCAL_PATH)/src/dbus/server/introspect.xml + mkdir -p $(OTBR_GEN_HEADER_DIR)/dbus/server echo 'R"INTROSPECT(' > $@ cat $+ >> $@ echo ')INTROSPECT"' >> $@ +$(LOCAL_PATH)/src/dbus/server/dbus_thread_object.cpp: $(OTBR_GEN_HEADER_DIR)/dbus/server/introspect.hpp -LOCAL_MODULE_CLASS := EXECUTABLES -LOCAL_MODULE := otbr-agent -LOCAL_MODULE_TAGS := eng -LOCAL_SHARED_LIBRARIES := libdbus +ifneq ($(ANDROID_NDK),1) +LOCAL_SHARED_LIBRARIES += libcutils +endif LOCAL_C_INCLUDES := \ $(LOCAL_PATH)/include \ @@ -79,6 +92,7 @@ LOCAL_C_INCLUDES := \ external/openthread/include \ external/openthread/src \ external/openthread/src/posix/platform/include \ + $(OTBR_GEN_HEADER_DIR) \ $(OTBR_PROJECT_INCLUDES) LOCAL_CFLAGS += -Wall -Wextra -Wno-unused-parameter @@ -90,13 +104,21 @@ LOCAL_CFLAGS += \ LOCAL_CPPFLAGS += -std=c++14 +LOCAL_GENERATED_SOURCES = $(OTBR_GEN_DBUS_INTROSPECT_HEADER) + LOCAL_SRC_FILES := \ + src/agent/advertising_proxy.cpp \ src/agent/agent_instance.cpp \ + src/agent/instance_params.cpp \ src/agent/border_agent.cpp \ + src/agent/discovery_proxy.cpp \ src/agent/main.cpp \ src/agent/ncp_openthread.cpp \ src/agent/thread_helper.cpp \ + src/common/dns_utils.cpp \ src/common/logging.cpp \ + src/common/task_runner.cpp \ + src/common/types.cpp \ src/dbus/common/dbus_message_dump.cpp \ src/dbus/common/dbus_message_helper.cpp \ src/dbus/common/dbus_message_helper_openthread.cpp \ @@ -105,12 +127,12 @@ LOCAL_SRC_FILES := \ src/dbus/server/dbus_object.cpp \ src/dbus/server/dbus_thread_object.cpp \ src/dbus/server/error_helper.cpp \ - src/utils/event_emitter.cpp \ + src/mdns/mdns.cpp \ src/utils/hex.cpp \ src/utils/strcpy_utils.cpp \ LOCAL_STATIC_LIBRARIES += \ - libopenthread-ncp \ + ot-core \ libopenthread-cli \ ot-core \ @@ -124,6 +146,10 @@ LOCAL_SRC_FILES += \ LOCAL_SHARED_LIBRARIES += libmdnssd endif +LOCAL_SRC_FILES += $(OTBR_PROJECT_SRC_FILES) +LOCAL_STATIC_LIBRARIES += $(OTBR_PROJECT_STATIC_LIBRARIES) +LOCAL_SHARED_LIBRARIES += $(OTBR_PROJECT_SHARED_LIBRARIES) + include $(BUILD_EXECUTABLE) include $(CLEAR_VARS) @@ -132,9 +158,18 @@ LOCAL_MODULE_CLASS := ETC LOCAL_MODULE := otbr-agent.conf LOCAL_MODULE_TAGS := eng +OTBR_AGENT_USER ?= root +OTBR_AGENT_GROUP ?= root + LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/dbus-1/system.d -LOCAL_SRC_FILES := src/agent/otbr-agent.conf -$(LOCAL_PATH)/src/agent/otbr-agent.conf: $(LOCAL_PATH)/src/agent/otbr-agent.conf.in - sed -e 's/@OTBR_AGENT_USER@/root/g' -e 's/@OTBR_AGENT_GROUP@/root/g' $< > $@ +OTBR_GEN_DBUS_CONF_DIR := $(local-intermediates-dir)/gen +$(OTBR_GEN_DBUS_CONF_DIR)/otbr-agent.conf: $(LOCAL_PATH)/src/agent/otbr-agent.conf.in + mkdir -p $(OTBR_GEN_DBUS_CONF_DIR) + sed -e 's/@OTBR_AGENT_USER@/$(OTBR_AGENT_USER)/g' -e 's/@OTBR_AGENT_GROUP@/$(OTBR_AGENT_GROUP)/g' $< > $@ + +# Dirty hack for Android.mk to copy config files from the intermediate directory. +LOCAL_PATH := $(local-intermediates-dir) +LOCAL_SRC_FILES := gen/otbr-agent.conf include $(BUILD_PREBUILT) +endif # ifeq ($(OTBR_ENABLE_ANDROID_MK),1) diff --git a/CMakeLists.txt b/CMakeLists.txt index 48ffe9ec2c9..09fd174d3f9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,18 +27,14 @@ # cmake_minimum_required(VERSION 3.10.2) -project(openthread-br VERSION 0.2.0) +project(openthread-br VERSION 0.3.0) add_library(otbr-config INTERFACE) -option(OTBR_BACKBONE_ROUTER "Build Backbone Router" OFF) -option(OTBR_DBUS "Build DBus support" OFF) -option(OTBR_OPENWRT "Build OpenWrt support" OFF) -option(OTBR_UNSECURE_JOIN "Enable unsecure joining" OFF) -option(OTBR_WEB "Build Web GUI" OFF) -option(OTBR_REST "Build Rest Server" OFF) +set(OTBR_INFRA_IF_NAME "wlan0" CACHE STRING "The infrastructure interface name") +include("${PROJECT_SOURCE_DIR}/etc/cmake/options.cmake") if(NOT CMAKE_C_STANDARD) set(CMAKE_C_STANDARD 99) @@ -60,17 +56,24 @@ endif() add_compile_options(-Wall -Wextra -Werror -Wfatal-errors -Wno-missing-braces) +if(NOT OTBR_NAME) + set(OTBR_NAME "OPENTHREAD_BR") +endif() -execute_process( - COMMAND git describe --dirty --always - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - OUTPUT_VARIABLE OTBR_GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE -) +message(STATUS "Name: ${OTBR_NAME}") + +if(NOT OTBR_VERSION) + execute_process( + COMMAND git describe --dirty --always 2>/dev/null + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + OUTPUT_VARIABLE OTBR_GIT_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE + ) -if(OTBR_GIT_VERSION) - set(OTBR_VERSION "${PROJECT_VERSION}-${OTBR_GIT_VERSION}") -else() - set(OTBR_VERSION "${PROJECT_VERSION}") + if(OTBR_GIT_VERSION) + set(OTBR_VERSION "${PROJECT_VERSION}-${OTBR_GIT_VERSION}") + else() + set(OTBR_VERSION "${PROJECT_VERSION}") + endif() endif() message(STATUS "Version: ${OTBR_VERSION}") @@ -81,7 +84,7 @@ target_include_directories(otbr-config INTERFACE ${PROJECT_SOURCE_DIR}/src ) target_compile_definitions(otbr-config INTERFACE - "OTBR_PACKAGE_NAME=\"OPENTHREAD_BR\"" + "OTBR_PACKAGE_NAME=\"${OTBR_NAME}\"" "OTBR_PACKAGE_VERSION=\"${OTBR_VERSION}\"" ) @@ -89,54 +92,8 @@ if(BUILD_SHARED_LIBS) target_link_libraries(otbr-config INTERFACE -Wl,--unresolved-symbols=ignore-in-shared-libs) endif() -find_package(PkgConfig) include(GNUInstallDirs) -if (OTBR_BACKBONE_ROUTER) - target_compile_definitions(otbr-config INTERFACE - OTBR_ENABLE_BACKBONE_ROUTER=1 - ) - set(OT_THREAD_VERSION 1.2 CACHE STRING "Backbone Router requires Thread 1.2 or higher" FORCE) - set(OT_BACKBONE_ROUTER ON CACHE BOOL "Enable Backbone Router feature in OpenThread" FORCE) - set(OT_SERVICE ON CACHE BOOL "Backbone Router requires Thread network service" FORCE) -endif() - -if(OTBR_DBUS) - pkg_check_modules(DBUS REQUIRED dbus-1) - pkg_get_variable(OTBR_DBUS_SYSTEM_BUS_SERVICES_DIR dbus-1 system_bus_services_dir) - target_compile_definitions(otbr-config INTERFACE - OTBR_ENABLE_DBUS_SERVER=1 - ) -endif() - -if(OTBR_REST) - target_compile_definitions(otbr-config INTERFACE - OTBR_ENABLE_REST_SERVER=1 - ) -endif() - -if(OTBR_WEB) - pkg_check_modules(JSONCPP jsoncpp REQUIRED) - set(Boost_USE_STATIC_LIBS ON) - set(Boost_USE_MULTITHREADED ON) - set(Boost_USE_STATIC_RUNTIME OFF) - find_package(Boost REQUIRED - COMPONENTS filesystem system) - set(OTBR_WEB_DATADIR ${CMAKE_INSTALL_FULL_DATADIR}/otbr-web) -endif() - -if(OTBR_OPENWRT) - target_compile_definitions(otbr-config INTERFACE - OTBR_ENABLE_OPENWRT=1 - ) -endif() - -if(OTBR_UNSECURE_JOIN) - target_compile_definitions(otbr-config INTERFACE - OTBR_ENABLE_UNSECURE_JOIN=1 - ) -endif() - set(OTBR_MDNS "avahi" CACHE STRING "MDNS service provider") set_property(CACHE OTBR_MDNS PROPERTY STRINGS "avahi" "mDNSResponder") @@ -165,3 +122,7 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) set(CPACK_PACKAGE_CONTACT "OpenThread Authors origin/master +git branch --track origin/main # Checkout the branch git checkout @@ -85,16 +85,16 @@ This will open up a text editor where you can craft your commit message. Prior to submitting your pull request, you might want to do a few things to clean up your branch and make it as simple as possible for the original repo's maintainer to test, accept, and merge your work. -If any commits have been made to the upstream master branch, you should rebase your development branch so that merging it will be a simple fast-forward that won't require any conflict resolution work. +If any commits have been made to the upstream main branch, you should rebase your development branch so that merging it will be a simple fast-forward that won't require any conflict resolution work. ```bash -# Fetch upstream master and merge with your repo's master branch -git checkout master -git pull upstream master +# Fetch upstream main and merge with your repo's main branch +git checkout main +git pull upstream main # If there were any new commits, rebase your development branch git checkout -git rebase master +git rebase main ``` Now, it may be desirable to squash some of your smaller commits down into a small number of larger more cohesive commits. You can do this with an interactive rebase: @@ -102,14 +102,14 @@ Now, it may be desirable to squash some of your smaller commits down into a smal ```bash # Rebase all commits on your development branch git checkout -git rebase -i master +git rebase -i main ``` This will open up a text editor where you can specify which commits to squash. #### Coding Conventions and Style -OpenThread uses and enforces the [OpenThread Coding Conventions and Style](STYLE_GUIDE.md) on all code, except for code located in [third_party](third_party). Use `script/make-pretty` and `script/make-pretty check` to automatically reformat code and check for code-style compliance, respectively. OpenThread currently requires [clang-format v10.0.0](http://releases.llvm.org/download.html#10.0.0) for C/C++ and [yapf](https://github.com/google/yapf) for Python. +OpenThread uses and enforces the [OpenThread Coding Conventions and Style](STYLE_GUIDE.md) on all code, except for code located in [third_party](third_party). Use `script/make-pretty` and `script/make-pretty check` to automatically reformat code and check for code-style compliance, respectively. OpenThread currently requires [clang-format v9.0.0](https://releases.llvm.org/download.html#9.0.0) for C/C++ and [yapf](https://github.com/google/yapf) for Python. As part of the cleanup process, you should also run `script/make-pretty check` to ensure that your code passes the baseline code style checks. diff --git a/README.md b/README.md index 5f8c3f2d13e..a542e7d6c10 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,14 @@ OTBR includes a number of features, including: More information about Thread can be found at [threadgroup.org](http://threadgroup.org/). Thread is a registered trademark of the Thread Group, Inc. -[ot-gh-action-build]: https://github.com/openthread/ot-br-posix/actions?query=workflow%3ABuild+branch%3Amaster+event%3Apush -[ot-gh-action-build-svg]: https://github.com/openthread/ot-br-posix/workflows/Build/badge.svg?branch=master&event=push -[ot-gh-action-docker]: https://github.com/openthread/ot-br-posix/actions?query=workflow%3ADocker+branch%3Amaster+event%3Apush -[ot-gh-action-docker-svg]: https://github.com/openthread/ot-br-posix/workflows/Docker/badge.svg?branch=master&event=push +[ot-gh-action-build]: https://github.com/openthread/ot-br-posix/actions?query=workflow%3ABuild+branch%3Amain+event%3Apush +[ot-gh-action-build-svg]: https://github.com/openthread/ot-br-posix/workflows/Build/badge.svg?branch=main&event=push +[ot-gh-action-docker]: https://github.com/openthread/ot-br-posix/actions?query=workflow%3ADocker+branch%3Amain+event%3Apush +[ot-gh-action-docker-svg]: https://github.com/openthread/ot-br-posix/workflows/Docker/badge.svg?branch=main&event=push [otbr-travis]: https://travis-ci.org/openthread/ot-br-posix -[otbr-travis-svg]: https://travis-ci.org/openthread/ot-br-posix.svg?branch=master +[otbr-travis-svg]: https://travis-ci.org/openthread/ot-br-posix.svg?branch=main [otbr-codecov]: https://codecov.io/gh/openthread/ot-br-posix -[otbr-codecov-svg]: https://codecov.io/gh/openthread/ot-br-posix/branch/master/graph/badge.svg +[otbr-codecov-svg]: https://codecov.io/gh/openthread/ot-br-posix/branch/main/graph/badge.svg ## Getting started @@ -49,9 +49,9 @@ If you're interested in contributing to OpenThread Border Router, read on. # Contributing -We would love for you to contribute to OpenThread Border Router and help make it even better than it is today! See our [Contributing Guidelines](https://github.com/openthread/ot-br-posix/blob/master/CONTRIBUTING.md) for more information. +We would love for you to contribute to OpenThread Border Router and help make it even better than it is today! See our [Contributing Guidelines](https://github.com/openthread/ot-br-posix/blob/main/CONTRIBUTING.md) for more information. -Contributors are required to abide by our [Code of Conduct](https://github.com/openthread/ot-br-posix/blob/master/CODE_OF_CONDUCT.md) and [Coding Conventions and Style Guide](https://github.com/openthread/ot-br-posix/blob/master/STYLE_GUIDE.md). +Contributors are required to abide by our [Code of Conduct](https://github.com/openthread/ot-br-posix/blob/main/CODE_OF_CONDUCT.md) and [Coding Conventions and Style Guide](https://github.com/openthread/ot-br-posix/blob/main/STYLE_GUIDE.md). We follow the philosophy of [Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all). @@ -61,7 +61,7 @@ OpenThread Border Router follows the [Semantic Versioning guidelines](http://sem # License -OpenThread Border Router is released under the [BSD 3-Clause license](https://github.com/openthread/ot-br-posix/blob/master/LICENSE). See the [`LICENSE`](https://github.com/openthread/ot-br-posix/blob/master/LICENSE) file for more information. +OpenThread Border Router is released under the [BSD 3-Clause license](https://github.com/openthread/ot-br-posix/blob/main/LICENSE). See the [`LICENSE`](https://github.com/openthread/ot-br-posix/blob/main/LICENSE) file for more information. Please only use the OpenThread name and marks when accurately referencing this software distribution. Do not use the marks in a way that suggests you are endorsed by or otherwise affiliated with Nest, Google, or The Thread Group. diff --git a/STYLE_GUIDE.md b/STYLE_GUIDE.md index 68f6edbb8c6..0c42f78efbf 100644 --- a/STYLE_GUIDE.md +++ b/STYLE_GUIDE.md @@ -111,7 +111,7 @@ - OpenThread uses `script/make-pretty` to reformat code and enforce code format and style. `script/make-pretty check` is included in OpenThread's continuous integration and must pass before a pull request is merged. -- `script/make-pretty` requires [clang-format v10.0.0](http://releases.llvm.org/download.html#10.0.0) for C/C++ and [yapf](https://github.com/google/yapf) for Python. +- `script/make-pretty` requires [clang-format v9.0.0](https://releases.llvm.org/download.html#9.0.0) for C/C++ and [yapf](https://github.com/google/yapf) for Python. ### File Names diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt new file mode 100644 index 00000000000..86ccb0a34c3 --- /dev/null +++ b/doc/CMakeLists.txt @@ -0,0 +1,59 @@ +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +find_package(Doxygen) + +if (DOXYGEN_FOUND) + set(DOXYGEN_IN ${CMAKE_CURRENT_SOURCE_DIR}/Doxyfile.in) + set(DOXYGEN_OUT ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile) + + configure_file(${DOXYGEN_IN} ${DOXYGEN_OUT} @ONLY) + message("Doxygen build started") + + add_custom_target(otbr-doc) + + add_custom_target(doxygen ALL + COMMAND ${DOXYGEN_EXECUTABLE} ${DOXYGEN_OUT} + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + COMMENT "Generating documentation with Doxygen" + VERBATIM) + add_dependencies(otbr-doc doxygen) + + if(OTBR_DBUS) + set(DBUS_DOC_SRC ${CMAKE_BINARY_DIR}/src/dbus/server/index.html) + set(DBUS_DOC_TARGET ${CMAKE_CURRENT_BINARY_DIR}/html/dbus-api.html) + add_custom_target(otbr-dbus-server-doc-copy ALL + COMMAND cp ${DBUS_DOC_SRC} ${DBUS_DOC_TARGET} + VERBATIM + ) + add_dependencies(otbr-dbus-server-doc-copy otbr-dbus-server-doc) + add_dependencies(otbr-doc otbr-dbus-server-doc-copy) + endif() +else() + message("Doxygen must be installed to generate doxygen documentation") +endif() diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in index 8b71d49251c..67c4c4f524b 100644 --- a/doc/Doxyfile.in +++ b/doc/Doxyfile.in @@ -1,4 +1,4 @@ -# Doxyfile 1.8.6 +# Doxyfile 1.8.20 # # Copyright (c) 2017, The OpenThread Authors. @@ -28,36 +28,28 @@ # POSSIBILITY OF SUCH DAMAGE. # +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. # -# Description: -# This file describes the settings to be used by the -# documentation system # doxygen (www.doxygen.org) -# -# This was initially autogenerated 'doxywizard' and then hand-tuned. -# -# All text after a hash (#) is considered a comment and will be -# ignored. -# -# The format is: -# -# TAG = value [value, ...] -# -# For lists items can also be appended using: -# -# TAG += value [value, ...] -# -# Values that contain spaces should be placed between quotes (" ") +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. # +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). #--------------------------------------------------------------------------- # Project related configuration options #--------------------------------------------------------------------------- -# This tag specifies the encoding used for all characters in the config file -# that follow. The default is UTF-8 which is also the encoding used for all text -# before the first occurrence of this tag. Doxygen uses libiconv (or the iconv -# built into libc) for the transcoding. See http://www.gnu.org/software/libiconv -# for the list of possible encodings. +# This tag specifies the encoding used for all characters in the configuration +# file that follow. The default is UTF-8 which is also the encoding used for all +# text before the first occurrence of this tag. Doxygen uses libiconv (or the +# iconv built into libc) for the transcoding. See +# https://www.gnu.org/software/libiconv/ for the list of possible encodings. # The default value is: UTF-8. DOXYFILE_ENCODING = UTF-8 @@ -68,13 +60,13 @@ DOXYFILE_ENCODING = UTF-8 # title of most generated pages and in a few other places. # The default value is: My Project. -PROJECT_NAME = @OTBR_PACKAGE_NAME@ +PROJECT_NAME = @PROJECT_NAME@ # The PROJECT_NUMBER tag can be used to enter a project or revision number. This # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = @OTBR_PACKAGE_VERSION@ +PROJECT_NUMBER = @OTBR_VERSION@ # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a @@ -82,10 +74,10 @@ PROJECT_NUMBER = @OTBR_PACKAGE_VERSION@ PROJECT_BRIEF = -# With the PROJECT_LOGO tag one can specify an logo or icon that is included in -# the documentation. The maximum height of the logo should not exceed 55 pixels -# and the maximum width should not exceed 200 pixels. Doxygen will copy the logo -# to the output directory. +# With the PROJECT_LOGO tag one can specify a logo or an icon that is included +# in the documentation. The maximum height of the logo should not exceed 55 +# pixels and the maximum width should not exceed 200 pixels. Doxygen will copy +# the logo to the output directory. PROJECT_LOGO = @@ -94,9 +86,9 @@ PROJECT_LOGO = # entered, it will be relative to the location where doxygen was started. If # left blank the current directory will be used. -OUTPUT_DIRECTORY = @abs_builddir@ +OUTPUT_DIRECTORY = @CMAKE_CURRENT_BINARY_DIR@ -# If the CREATE_SUBDIRS tag is set to YES, then doxygen will create 4096 sub- +# If the CREATE_SUBDIRS tag is set to YES then doxygen will create 4096 sub- # directories (in 2 levels) under the output directory of each output format and # will distribute the generated files over these directories. Enabling this # option can be useful when feeding doxygen a huge amount of source files, where @@ -106,6 +98,14 @@ OUTPUT_DIRECTORY = @abs_builddir@ CREATE_SUBDIRS = YES +# If the ALLOW_UNICODE_NAMES tag is set to YES, doxygen will allow non-ASCII +# characters to appear in the names of generated files. If set to NO, non-ASCII +# characters will be escaped, for example _xE3_x81_x84 will be used for Unicode +# U+3044. +# The default value is: NO. + +ALLOW_UNICODE_NAMES = NO + # The OUTPUT_LANGUAGE tag is used to specify the language in which all # documentation generated by doxygen is written. Doxygen will use this # information to generate all constant output in the proper language. @@ -121,14 +121,22 @@ CREATE_SUBDIRS = YES OUTPUT_LANGUAGE = English -# If the BRIEF_MEMBER_DESC tag is set to YES doxygen will include brief member +# The OUTPUT_TEXT_DIRECTION tag is used to specify the direction in which all +# documentation generated by doxygen is written. Doxygen will use this +# information to generate all generated output in the proper direction. +# Possible values are: None, LTR, RTL and Context. +# The default value is: None. + +OUTPUT_TEXT_DIRECTION = None + +# If the BRIEF_MEMBER_DESC tag is set to YES, doxygen will include brief member # descriptions after the members that are listed in the file and class # documentation (similar to Javadoc). Set to NO to disable this. # The default value is: YES. BRIEF_MEMBER_DESC = YES -# If the REPEAT_BRIEF tag is set to YES doxygen will prepend the brief +# If the REPEAT_BRIEF tag is set to YES, doxygen will prepend the brief # description of a member or function before the detailed description # # Note: If both HIDE_UNDOC_MEMBERS and BRIEF_MEMBER_DESC are set to NO, the @@ -173,7 +181,7 @@ ALWAYS_DETAILED_SEC = NO INLINE_INHERITED_MEMB = NO -# If the FULL_PATH_NAMES tag is set to YES doxygen will prepend the full path +# If the FULL_PATH_NAMES tag is set to YES, doxygen will prepend the full path # before files name in the file list and in the header files. If set to NO the # shortest path that makes the file name unique will be used # The default value is: YES. @@ -190,8 +198,8 @@ FULL_PATH_NAMES = YES # will be relative from the directory where doxygen is started. # This tag requires that the tag FULL_PATH_NAMES is set to YES. -STRIP_FROM_PATH = @abs_top_srcdir@ \ - @abs_top_builddir@ +STRIP_FROM_PATH = @PROJECT_SOURCE_DIR@ \ + @PROJECT_BINARY_DIR@ # The STRIP_FROM_INC_PATH tag can be used to strip a user-defined part of the # path mentioned in the documentation of a class, which tells the reader which @@ -200,7 +208,7 @@ STRIP_FROM_PATH = @abs_top_srcdir@ \ # specify the list of include paths that are normally passed to the compiler # using the -I flag. -STRIP_FROM_INC_PATH = @abs_top_srcdir@ +STRIP_FROM_INC_PATH = @PROJECT_SOURCE_DIR@ # If the SHORT_NAMES tag is set to YES, doxygen will generate much shorter (but # less readable) file names. This can be useful is your file systems doesn't @@ -216,7 +224,17 @@ SHORT_NAMES = NO # description.) # The default value is: NO. -JAVADOC_AUTOBRIEF = YES +JAVADOC_AUTOBRIEF = NO + +# If the JAVADOC_BANNER tag is set to YES then doxygen will interpret a line +# such as +# /*************** +# as being the beginning of a Javadoc-style comment "banner". If set to NO, the +# Javadoc-style will behave just like regular comments and it will not be +# interpreted by doxygen. +# The default value is: NO. + +JAVADOC_BANNER = NO # If the QT_AUTOBRIEF tag is set to YES then doxygen will interpret the first # line (until the first dot) of a Qt-style comment as the brief description. If @@ -238,15 +256,23 @@ QT_AUTOBRIEF = NO MULTILINE_CPP_IS_BRIEF = NO +# By default Python docstrings are displayed as preformatted text and doxygen's +# special commands cannot be used. By setting PYTHON_DOCSTRING to NO the +# doxygen's special commands can be used and the contents of the docstring +# documentation blocks is shown as doxygen documentation. +# The default value is: YES. + +PYTHON_DOCSTRING = YES + # If the INHERIT_DOCS tag is set to YES then an undocumented member inherits the # documentation from any documented member that it re-implements. # The default value is: YES. INHERIT_DOCS = YES -# If the SEPARATE_MEMBER_PAGES tag is set to YES, then doxygen will produce a -# new page for each member. If set to NO, the documentation of a member will be -# part of the file/class/namespace that contains it. +# If the SEPARATE_MEMBER_PAGES tag is set to YES then doxygen will produce a new +# page for each member. If set to NO, the documentation of a member will be part +# of the file/class/namespace that contains it. # The default value is: NO. SEPARATE_MEMBER_PAGES = NO @@ -265,16 +291,15 @@ TAB_SIZE = 4 # will allow you to put the command \sideeffect (or @sideeffect) in the # documentation, which will result in a user-defined paragraph with heading # "Side Effects:". You can put \n's in the value part of an alias to insert -# newlines. +# newlines (in the resulting output). You can put ^^ in the value part of an +# alias to insert a newline as if a physical newline was in the original file. +# When you need a literal { or } or , in the value part of an alias you have to +# escape them by means of a backslash (\), this can lead to conflicts with the +# commands \{ and \} for these it is advised to use the version @{ and @} or use +# a double escape (\\{ and \\}) ALIASES = -# This tag can be used to specify a number of word-keyword mappings (TCL only). -# A mapping has the form "name=value". For example adding "class=itcl::class" -# will allow you to use the command class in the itcl::class meaning. - -TCL_SUBST = - # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. For # instance, some of the names that are used will be different. The list of all @@ -303,16 +328,28 @@ OPTIMIZE_FOR_FORTRAN = NO OPTIMIZE_OUTPUT_VHDL = NO +# Set the OPTIMIZE_OUTPUT_SLICE tag to YES if your project consists of Slice +# sources only. Doxygen will then generate output that is more tailored for that +# language. For instance, namespaces will be presented as modules, types will be +# separated into more groups, etc. +# The default value is: NO. + +OPTIMIZE_OUTPUT_SLICE = NO + # Doxygen selects the parser to use depending on the extension of the files it # parses. With this tag you can assign which parser to use for a given # extension. Doxygen has a built-in mapping, but you can override or extend it # using this tag. The format is ext=language, where ext is a file extension, and -# language is one of the parsers supported by doxygen: IDL, Java, Javascript, -# C#, C, C++, D, PHP, Objective-C, Python, Fortran, VHDL. For instance to make -# doxygen treat .inc files as Fortran files (default is PHP), and .f files as C -# (default is Fortran), use: inc=Fortran f=C. +# language is one of the parsers supported by doxygen: IDL, Java, JavaScript, +# Csharp (C#), C, C++, D, PHP, md (Markdown), Objective-C, Python, Slice, VHDL, +# Fortran (fixed format Fortran: FortranFixed, free formatted Fortran: +# FortranFree, unknown formatted Fortran: Fortran. In the later case the parser +# tries to guess whether the code is fixed or free formatted code, this is the +# default for Fortran type files). For instance to make doxygen treat .inc files +# as Fortran files (default is PHP), and .f files as C (default is Fortran), +# use: inc=Fortran f=C. # -# Note For files without extension you can use no_extension as a placeholder. +# Note: For files without extension you can use no_extension as a placeholder. # # Note that for custom extensions you also need to set FILE_PATTERNS otherwise # the files are not read by doxygen. @@ -321,7 +358,7 @@ EXTENSION_MAPPING = # If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments # according to the Markdown format, which allows for more readable -# documentation. See http://daringfireball.net/projects/markdown/ for details. +# documentation. See https://daringfireball.net/projects/markdown/ for details. # The output of markdown processing is further processed by doxygen, so you can # mix doxygen, HTML, and XML commands with Markdown formatting. Disable only in # case of backward compatibilities issues. @@ -329,10 +366,19 @@ EXTENSION_MAPPING = MARKDOWN_SUPPORT = YES +# When the TOC_INCLUDE_HEADINGS tag is set to a non-zero value, all headings up +# to that level are automatically included in the table of contents, even if +# they do not have an id attribute. +# Note: This feature currently applies only to Markdown headings. +# Minimum value: 0, maximum value: 99, default value: 5. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +TOC_INCLUDE_HEADINGS = 5 + # When enabled doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can -# be prevented in individual cases by by putting a % sign in front of the word -# or globally by setting AUTOLINK_SUPPORT to NO. +# be prevented in individual cases by putting a % sign in front of the word or +# globally by setting AUTOLINK_SUPPORT to NO. # The default value is: YES. AUTOLINK_SUPPORT = YES @@ -354,7 +400,7 @@ BUILTIN_STL_SUPPORT = NO CPP_CLI_SUPPORT = NO # Set the SIP_SUPPORT tag to YES if your project consists of sip (see: -# http://www.riverbankcomputing.co.uk/software/sip/intro) sources only. Doxygen +# https://www.riverbankcomputing.com/software/sip/intro) sources only. Doxygen # will parse them like normal C++ but will assume all classes use public instead # of private inheritance when no explicit protection keyword is present. # The default value is: NO. @@ -372,13 +418,20 @@ SIP_SUPPORT = NO IDL_PROPERTY_SUPPORT = YES # If member grouping is used in the documentation and the DISTRIBUTE_GROUP_DOC -# tag is set to YES, then doxygen will reuse the documentation of the first +# tag is set to YES then doxygen will reuse the documentation of the first # member in the group (if any) for the other members of the group. By default # all members of a group must be documented explicitly. # The default value is: NO. DISTRIBUTE_GROUP_DOC = NO +# If one adds a struct or class to a group and this option is enabled, then also +# any nested class or struct is added to the same group. By default this option +# is disabled and one has to add nested compounds explicitly via \ingroup. +# The default value is: NO. + +GROUP_NESTED_COMPOUNDS = NO + # Set the SUBGROUPING tag to YES to allow class member groups of the same type # (for instance a group of public functions) to be put as a subgroup of that # type (e.g. under the Public Functions section). Set it to NO to prevent @@ -433,11 +486,24 @@ TYPEDEF_HIDES_STRUCT = NO LOOKUP_CACHE_SIZE = 0 +# The NUM_PROC_THREADS specifies the number threads doxygen is allowed to use +# during processing. When set to 0 doxygen will based this on the number of +# cores available in the system. You can set it explicitly to a value larger +# than 0 to get more control over the balance between CPU load and processing +# speed. At this moment only the input processing can be done using multiple +# threads. Since this is still an experimental feature the default is set to 1, +# which efficively disables parallel processing. Please report any issues you +# encounter. Generating dot graphs in parallel is controlled by the +# DOT_NUM_THREADS setting. +# Minimum value: 0, maximum value: 32, default value: 1. + +NUM_PROC_THREADS = 1 + #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- -# If the EXTRACT_ALL tag is set to YES doxygen will assume all entities in +# If the EXTRACT_ALL tag is set to YES, doxygen will assume all entities in # documentation are documented, even if no documentation was available. Private # class members and static file members will be hidden unless the # EXTRACT_PRIVATE respectively EXTRACT_STATIC tags are set to YES. @@ -447,35 +513,41 @@ LOOKUP_CACHE_SIZE = 0 EXTRACT_ALL = NO -# If the EXTRACT_PRIVATE tag is set to YES all private members of a class will +# If the EXTRACT_PRIVATE tag is set to YES, all private members of a class will # be included in the documentation. # The default value is: NO. EXTRACT_PRIVATE = NO -# If the EXTRACT_PACKAGE tag is set to YES all members with package or internal +# If the EXTRACT_PRIV_VIRTUAL tag is set to YES, documented private virtual +# methods of a class will be included in the documentation. +# The default value is: NO. + +EXTRACT_PRIV_VIRTUAL = NO + +# If the EXTRACT_PACKAGE tag is set to YES, all members with package or internal # scope will be included in the documentation. # The default value is: NO. EXTRACT_PACKAGE = NO -# If the EXTRACT_STATIC tag is set to YES all static members of a file will be +# If the EXTRACT_STATIC tag is set to YES, all static members of a file will be # included in the documentation. # The default value is: NO. EXTRACT_STATIC = NO -# If the EXTRACT_LOCAL_CLASSES tag is set to YES classes (and structs) defined -# locally in source files will be included in the documentation. If set to NO +# If the EXTRACT_LOCAL_CLASSES tag is set to YES, classes (and structs) defined +# locally in source files will be included in the documentation. If set to NO, # only classes defined in header files are included. Does not have any effect # for Java sources. # The default value is: YES. EXTRACT_LOCAL_CLASSES = YES -# This flag is only useful for Objective-C code. When set to YES local methods, +# This flag is only useful for Objective-C code. If set to YES, local methods, # which are defined in the implementation section but not in the interface are -# included in the documentation. If set to NO only methods in the interface are +# included in the documentation. If set to NO, only methods in the interface are # included. # The default value is: NO. @@ -500,21 +572,21 @@ HIDE_UNDOC_MEMBERS = NO # If the HIDE_UNDOC_CLASSES tag is set to YES, doxygen will hide all # undocumented classes that are normally visible in the class hierarchy. If set -# to NO these classes will be included in the various overviews. This option has -# no effect if EXTRACT_ALL is enabled. +# to NO, these classes will be included in the various overviews. This option +# has no effect if EXTRACT_ALL is enabled. # The default value is: NO. HIDE_UNDOC_CLASSES = NO # If the HIDE_FRIEND_COMPOUNDS tag is set to YES, doxygen will hide all friend -# (class|struct|union) declarations. If set to NO these declarations will be -# included in the documentation. +# declarations. If set to NO, these declarations will be included in the +# documentation. # The default value is: NO. HIDE_FRIEND_COMPOUNDS = NO # If the HIDE_IN_BODY_DOCS tag is set to YES, doxygen will hide any -# documentation blocks found inside the body of a function. If set to NO these +# documentation blocks found inside the body of a function. If set to NO, these # blocks will be appended to the function's detailed documentation block. # The default value is: NO. @@ -528,21 +600,28 @@ HIDE_IN_BODY_DOCS = NO INTERNAL_DOCS = NO # If the CASE_SENSE_NAMES tag is set to NO then doxygen will only generate file -# names in lower-case letters. If set to YES upper-case letters are also +# names in lower-case letters. If set to YES, upper-case letters are also # allowed. This is useful if you have classes or files whose names only differ # in case and if your file system supports case sensitive file names. Windows -# and Mac users are advised to set this option to NO. +# (including Cygwin) and Mac users are advised to set this option to NO. # The default value is: system dependent. CASE_SENSE_NAMES = YES # If the HIDE_SCOPE_NAMES tag is set to NO then doxygen will show members with -# their full class and namespace scopes in the documentation. If set to YES the +# their full class and namespace scopes in the documentation. If set to YES, the # scope will be hidden. # The default value is: NO. HIDE_SCOPE_NAMES = NO +# If the HIDE_COMPOUND_REFERENCE tag is set to NO (default) then doxygen will +# append additional text to a page's title, such as Class Reference. If set to +# YES the compound reference will be hidden. +# The default value is: NO. + +HIDE_COMPOUND_REFERENCE= NO + # If the SHOW_INCLUDE_FILES tag is set to YES then doxygen will put a list of # the files that are included by a file in the documentation of that file. # The default value is: YES. @@ -570,14 +649,14 @@ INLINE_INFO = YES # If the SORT_MEMBER_DOCS tag is set to YES then doxygen will sort the # (detailed) documentation of file and class members alphabetically by member -# name. If set to NO the members will appear in declaration order. +# name. If set to NO, the members will appear in declaration order. # The default value is: YES. SORT_MEMBER_DOCS = YES # If the SORT_BRIEF_DOCS tag is set to YES then doxygen will sort the brief # descriptions of file, namespace and class members alphabetically by member -# name. If set to NO the members will appear in declaration order. Note that +# name. If set to NO, the members will appear in declaration order. Note that # this will also influence the order of the classes in the class list. # The default value is: NO. @@ -622,27 +701,25 @@ SORT_BY_SCOPE_NAME = NO STRICT_PROTO_MATCHING = NO -# The GENERATE_TODOLIST tag can be used to enable ( YES) or disable ( NO) the -# todo list. This list is created by putting \todo commands in the -# documentation. +# The GENERATE_TODOLIST tag can be used to enable (YES) or disable (NO) the todo +# list. This list is created by putting \todo commands in the documentation. # The default value is: YES. GENERATE_TODOLIST = YES -# The GENERATE_TESTLIST tag can be used to enable ( YES) or disable ( NO) the -# test list. This list is created by putting \test commands in the -# documentation. +# The GENERATE_TESTLIST tag can be used to enable (YES) or disable (NO) the test +# list. This list is created by putting \test commands in the documentation. # The default value is: YES. GENERATE_TESTLIST = YES -# The GENERATE_BUGLIST tag can be used to enable ( YES) or disable ( NO) the bug +# The GENERATE_BUGLIST tag can be used to enable (YES) or disable (NO) the bug # list. This list is created by putting \bug commands in the documentation. # The default value is: YES. GENERATE_BUGLIST = YES -# The GENERATE_DEPRECATEDLIST tag can be used to enable ( YES) or disable ( NO) +# The GENERATE_DEPRECATEDLIST tag can be used to enable (YES) or disable (NO) # the deprecated list. This list is created by putting \deprecated commands in # the documentation. # The default value is: YES. @@ -667,8 +744,8 @@ ENABLED_SECTIONS = MAX_INITIALIZER_LINES = 30 # Set the SHOW_USED_FILES tag to NO to disable the list of files generated at -# the bottom of the documentation of classes and structs. If set to YES the list -# will mention the files that were used to generate the documentation. +# the bottom of the documentation of classes and structs. If set to YES, the +# list will mention the files that were used to generate the documentation. # The default value is: YES. SHOW_USED_FILES = YES @@ -713,11 +790,10 @@ LAYOUT_FILE = # The CITE_BIB_FILES tag can be used to specify one or more bib files containing # the reference definitions. This must be a list of .bib files. The .bib # extension is automatically appended if omitted. This requires the bibtex tool -# to be installed. See also http://en.wikipedia.org/wiki/BibTeX for more info. +# to be installed. See also https://en.wikipedia.org/wiki/BibTeX for more info. # For LaTeX the style of the bibliography can be controlled using # LATEX_BIB_STYLE. To use this feature you need bibtex and perl available in the -# search path. Do not use file names with spaces, bibtex cannot handle them. See -# also \cite for info how to create references. +# search path. See also \cite for info how to create references. CITE_BIB_FILES = @@ -733,7 +809,7 @@ CITE_BIB_FILES = QUIET = NO # The WARNINGS tag can be used to turn on/off the warning messages that are -# generated to standard error ( stderr) by doxygen. If WARNINGS is set to YES +# generated to standard error (stderr) by doxygen. If WARNINGS is set to YES # this implies that the warnings are on. # # Tip: Turn warnings on while writing the documentation. @@ -741,7 +817,7 @@ QUIET = NO WARNINGS = YES -# If the WARN_IF_UNDOCUMENTED tag is set to YES, then doxygen will generate +# If the WARN_IF_UNDOCUMENTED tag is set to YES then doxygen will generate # warnings for undocumented members. If EXTRACT_ALL is set to YES then this flag # will automatically be disabled. # The default value is: YES. @@ -758,12 +834,19 @@ WARN_IF_DOC_ERROR = YES # This WARN_NO_PARAMDOC option can be enabled to get warnings for functions that # are documented, but have no documentation for their parameters or return -# value. If set to NO doxygen will only warn about wrong or incomplete parameter -# documentation, but not about the absence of documentation. +# value. If set to NO, doxygen will only warn about wrong or incomplete +# parameter documentation, but not about the absence of documentation. If +# EXTRACT_ALL is set to YES then this flag will automatically be disabled. # The default value is: NO. WARN_NO_PARAMDOC = NO +# If the WARN_AS_ERROR tag is set to YES then doxygen will immediately stop when +# a warning is encountered. +# The default value is: NO. + +WARN_AS_ERROR = NO + # The WARN_FORMAT tag determines the format of the warning messages that doxygen # can produce. The string should contain the $file, $line, and $text tags, which # will be replaced by the file and line number from which the warning originated @@ -787,17 +870,20 @@ WARN_LOGFILE = # The INPUT tag is used to specify the files and/or directories that contain # documented source files. You may enter file names like myfile.cpp or # directories like /usr/src/myproject. Separate the files or directories with -# spaces. +# spaces. See also FILE_PATTERNS and EXTENSION_MAPPING # Note: If this tag is empty the current directory is searched. -INPUT = @abs_top_builddir@/src \ - @abs_top_srcdir@/include \ - @abs_top_srcdir@/doc +INPUT = @PROJECT_BINARY_DIR@/src \ + @PROJECT_SOURCE_DIR@/doc \ + @PROJECT_SOURCE_DIR@/include \ + @PROJECT_SOURCE_DIR@/README.md \ + @PROJECT_SOURCE_DIR@/src \ + @PROJECT_SOURCE_DIR@/tools # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding. Doxygen uses # libiconv (or the iconv built into libc) for the transcoding. See the libiconv -# documentation (see: http://www.gnu.org/software/libiconv) for the list of +# documentation (see: https://www.gnu.org/software/libiconv/) for the list of # possible encodings. # The default value is: UTF-8. @@ -805,45 +891,67 @@ INPUT_ENCODING = UTF-8 # If the value of the INPUT tag contains directories, you can use the # FILE_PATTERNS tag to specify one or more wildcard patterns (like *.cpp and -# *.h) to filter out the source-files in the directories. If left blank the -# following patterns are tested:*.c, *.cc, *.cxx, *.cpp, *.c++, *.java, *.ii, -# *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, -# *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, -# *.md, *.mm, *.dox, *.py, *.f90, *.f, *.for, *.tcl, *.vhd, *.vhdl, *.ucf, -# *.qsf, *.as and *.js. +# *.h) to filter out the source-files in the directories. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# read by doxygen. +# +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, +# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, +# *.hh, *.hxx, *.hpp, *.h++, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, *.inc, +# *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C comment), +# *.doc (to be provided as doxygen C comment), *.txt (to be provided as doxygen +# C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, +# *.vhdl, *.ucf, *.qsf and *.ice. FILE_PATTERNS = *.c \ *.cc \ *.cxx \ *.cpp \ *.c++ \ - *.d \ *.java \ *.ii \ *.ixx \ *.ipp \ *.i++ \ *.inl \ + *.idl \ + *.ddl \ + *.odl \ *.h \ *.hh \ *.hxx \ *.hpp \ *.h++ \ - *.idl \ - *.odl \ *.cs \ + *.d \ *.php \ - *.php3 \ + *.php4 \ + *.php5 \ + *.phtml \ *.inc \ *.m \ + *.markdown \ + *.md \ *.mm \ *.dox \ + *.doc \ + *.txt \ *.py \ + *.pyw \ *.f90 \ + *.f95 \ + *.f03 \ + *.f08 \ + *.f18 \ *.f \ *.for \ *.vhd \ - *.vhdl + *.vhdl \ + *.ucf \ + *.qsf \ + *.ice # The RECURSIVE tag can be used to specify whether or not subdirectories should # be searched for input files as well. @@ -927,6 +1035,10 @@ IMAGE_PATH = # Note that the filter must not add or remove lines; it is applied before the # code is scanned, but not when the output code is generated. If lines are added # or removed, the anchors will not be placed correctly. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. INPUT_FILTER = @@ -936,11 +1048,15 @@ INPUT_FILTER = # (like *.cpp=my_cpp_filter). See INPUT_FILTER for further information on how # filters are used. If the FILTER_PATTERNS tag is empty or if none of the # patterns match the file name, INPUT_FILTER is applied. +# +# Note that for custom extensions or not directly supported extensions you also +# need to set EXTENSION_MAPPING for the extension otherwise the files are not +# properly processed by doxygen. FILTER_PATTERNS = # If the FILTER_SOURCE_FILES tag is set to YES, the input filter (if set using -# INPUT_FILTER ) will also be used to filter the input files that are used for +# INPUT_FILTER) will also be used to filter the input files that are used for # producing the source files to browse (i.e. when SOURCE_BROWSER is set to YES). # The default value is: NO. @@ -959,7 +1075,7 @@ FILTER_SOURCE_PATTERNS = # (index.html). This can be useful if you have a project on for instance GitHub # and want to reuse the introduction page also for the doxygen output. -USE_MDFILE_AS_MAINPAGE = +USE_MDFILE_AS_MAINPAGE = README.md #--------------------------------------------------------------------------- # Configuration options related to source browsing @@ -988,7 +1104,7 @@ INLINE_SOURCES = NO STRIP_CODE_COMMENTS = YES # If the REFERENCED_BY_RELATION tag is set to YES then for each documented -# function all documented functions referencing it will be listed. +# entity all documented functions referencing it will be listed. # The default value is: NO. REFERENCED_BY_RELATION = NO @@ -1000,7 +1116,7 @@ REFERENCED_BY_RELATION = NO REFERENCES_RELATION = NO # If the REFERENCES_LINK_SOURCE tag is set to YES and SOURCE_BROWSER tag is set -# to YES, then the hyperlinks from functions in REFERENCES_RELATION and +# to YES then the hyperlinks from functions in REFERENCES_RELATION and # REFERENCED_BY_RELATION lists will link to the source code. Otherwise they will # link to the documentation. # The default value is: YES. @@ -1020,12 +1136,12 @@ SOURCE_TOOLTIPS = YES # If the USE_HTAGS tag is set to YES then the references to source code will # point to the HTML generated by the htags(1) tool instead of doxygen built-in # source browser. The htags tool is part of GNU's global source tagging system -# (see http://www.gnu.org/software/global/global.html). You will need version +# (see https://www.gnu.org/software/global/global.html). You will need version # 4.8.6 or higher. # # To use it do the following: # - Install the latest version of global -# - Enable SOURCE_BROWSER and USE_HTAGS in the config file +# - Enable SOURCE_BROWSER and USE_HTAGS in the configuration file # - Make sure the INPUT points to the root of the source tree # - Run doxygen as normal # @@ -1047,6 +1163,38 @@ USE_HTAGS = NO VERBATIM_HEADERS = YES +# If the CLANG_ASSISTED_PARSING tag is set to YES then doxygen will use the +# clang parser (see: http://clang.llvm.org/) for more accurate parsing at the +# cost of reduced performance. This can be particularly helpful with template +# rich C++ code for which doxygen's built-in parser lacks the necessary type +# information. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. +# The default value is: NO. + +CLANG_ASSISTED_PARSING = NO + +# If clang assisted parsing is enabled you can provide the compiler with command +# line options that you would normally use when invoking the compiler. Note that +# the include paths will already be set by doxygen for the files and directories +# specified with INPUT and INCLUDE_PATH. +# This tag requires that the tag CLANG_ASSISTED_PARSING is set to YES. + +CLANG_OPTIONS = + +# If clang assisted parsing is enabled you can provide the clang parser with the +# path to the directory containing a file called compile_commands.json. This +# file is the compilation database (see: +# http://clang.llvm.org/docs/HowToSetupToolingForLLVM.html) containing the +# options used when the source files were built. This is equivalent to +# specifying the "-p" option to a clang tool, such as clang-check. These options +# will then be passed to the parser. Any options specified with CLANG_OPTIONS +# will be added as well. +# Note: The availability of this option depends on whether or not doxygen was +# generated with the -Duse_libclang=ON option for CMake. + +CLANG_DATABASE_PATH = + #--------------------------------------------------------------------------- # Configuration options related to the alphabetical class index #--------------------------------------------------------------------------- @@ -1077,7 +1225,7 @@ IGNORE_PREFIX = # Configuration options related to the HTML output #--------------------------------------------------------------------------- -# If the GENERATE_HTML tag is set to YES doxygen will generate HTML output +# If the GENERATE_HTML tag is set to YES, doxygen will generate HTML output # The default value is: YES. GENERATE_HTML = YES @@ -1139,13 +1287,15 @@ HTML_FOOTER = HTML_STYLESHEET = -# The HTML_EXTRA_STYLESHEET tag can be used to specify an additional user- -# defined cascading style sheet that is included after the standard style sheets +# The HTML_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# cascading style sheets that are included after the standard style sheets # created by doxygen. Using this option one can overrule certain style aspects. # This is preferred over using HTML_STYLESHEET since it does not replace the -# standard style sheet and is therefor more robust against future updates. -# Doxygen will copy the style sheet file to the output directory. For an example -# see the documentation. +# standard style sheet and is therefore more robust against future updates. +# Doxygen will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). For an example see the documentation. # This tag requires that the tag GENERATE_HTML is set to YES. HTML_EXTRA_STYLESHEET = @@ -1161,9 +1311,9 @@ HTML_EXTRA_STYLESHEET = HTML_EXTRA_FILES = # The HTML_COLORSTYLE_HUE tag controls the color of the HTML output. Doxygen -# will adjust the colors in the stylesheet and background images according to +# will adjust the colors in the style sheet and background images according to # this color. Hue is specified as an angle on a colorwheel, see -# http://en.wikipedia.org/wiki/Hue for more information. For instance the value +# https://en.wikipedia.org/wiki/Hue for more information. For instance the value # 0 represents red, 60 is yellow, 120 is green, 180 is cyan, 240 is blue, 300 # purple, and 360 is red again. # Minimum value: 0, maximum value: 359, default value: 220. @@ -1192,11 +1342,23 @@ HTML_COLORSTYLE_GAMMA = 80 # If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML # page will contain the date and time when the page was generated. Setting this -# to NO can help when comparing the output of multiple runs. +# to YES can help to show when doxygen was last run and thus if the +# documentation is up to date. +# The default value is: NO. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_TIMESTAMP = NO + +# If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML +# documentation will contain a main index with vertical navigation menus that +# are dynamically created via JavaScript. If disabled, the navigation index will +# consists of multiple levels of tabs that are statically embedded in every HTML +# page. Disable this option to support browsers that do not have JavaScript, +# like the Qt help browser. # The default value is: YES. # This tag requires that the tag GENERATE_HTML is set to YES. -HTML_TIMESTAMP = YES +HTML_DYNAMIC_MENUS = YES # If the HTML_DYNAMIC_SECTIONS tag is set to YES then the generated HTML # documentation will contain sections that can be hidden and shown after the @@ -1221,13 +1383,13 @@ HTML_INDEX_NUM_ENTRIES = 100 # If the GENERATE_DOCSET tag is set to YES, additional index files will be # generated that can be used as input for Apple's Xcode 3 integrated development -# environment (see: http://developer.apple.com/tools/xcode/), introduced with -# OSX 10.5 (Leopard). To create a documentation set, doxygen will generate a +# environment (see: https://developer.apple.com/xcode/), introduced with OSX +# 10.5 (Leopard). To create a documentation set, doxygen will generate a # Makefile in the HTML output directory. Running make will produce the docset in # that directory and running make install will install the docset in # ~/Library/Developer/Shared/Documentation/DocSets so that Xcode will find it at -# startup. See http://developer.apple.com/tools/creatingdocsetswithdoxygen.html -# for more information. +# startup. See https://developer.apple.com/library/archive/featuredarticles/Doxy +# genXcode/_index.html for more information. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. @@ -1266,7 +1428,7 @@ DOCSET_PUBLISHER_NAME = Publisher # If the GENERATE_HTMLHELP tag is set to YES then doxygen generates three # additional HTML index files: index.hhp, index.hhc, and index.hhk. The # index.hhp is a project file that can be read by Microsoft's HTML Help Workshop -# (see: http://www.microsoft.com/en-us/download/details.aspx?id=21138) on +# (see: https://www.microsoft.com/en-us/download/details.aspx?id=21138) on # Windows. # # The HTML Help Workshop contains a compiler that can convert all HTML output @@ -1289,28 +1451,29 @@ GENERATE_HTMLHELP = NO CHM_FILE = # The HHC_LOCATION tag can be used to specify the location (absolute path -# including file name) of the HTML help compiler ( hhc.exe). If non-empty +# including file name) of the HTML help compiler (hhc.exe). If non-empty, # doxygen will try to run the HTML help compiler on the generated index.hhp. # The file has to be specified with full path. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. HHC_LOCATION = -# The GENERATE_CHI flag controls if a separate .chi index file is generated ( -# YES) or that it should be included in the master .chm file ( NO). +# The GENERATE_CHI flag controls if a separate .chi index file is generated +# (YES) or that it should be included in the main .chm file (NO). # The default value is: NO. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. GENERATE_CHI = NO -# The CHM_INDEX_ENCODING is used to encode HtmlHelp index ( hhk), content ( hhc) +# The CHM_INDEX_ENCODING is used to encode HtmlHelp index (hhk), content (hhc) # and project file content. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. CHM_INDEX_ENCODING = -# The BINARY_TOC flag controls whether a binary table of contents is generated ( -# YES) or a normal table of contents ( NO) in the .chm file. +# The BINARY_TOC flag controls whether a binary table of contents is generated +# (YES) or a normal table of contents (NO) in the .chm file. Furthermore it +# enables the Previous and Next buttons. # The default value is: NO. # This tag requires that the tag GENERATE_HTMLHELP is set to YES. @@ -1341,7 +1504,7 @@ QCH_FILE = # The QHP_NAMESPACE tag specifies the namespace to use when generating Qt Help # Project output. For more information please see Qt Help Project / Namespace -# (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#namespace). +# (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#namespace). # The default value is: org.doxygen.Project. # This tag requires that the tag GENERATE_QHP is set to YES. @@ -1349,7 +1512,7 @@ QHP_NAMESPACE = org.doxygen.Project # The QHP_VIRTUAL_FOLDER tag specifies the namespace to use when generating Qt # Help Project output. For more information please see Qt Help Project / Virtual -# Folders (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#virtual- +# Folders (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#virtual- # folders). # The default value is: doc. # This tag requires that the tag GENERATE_QHP is set to YES. @@ -1358,7 +1521,7 @@ QHP_VIRTUAL_FOLDER = doc # If the QHP_CUST_FILTER_NAME tag is set, it specifies the name of a custom # filter to add. For more information please see Qt Help Project / Custom -# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- # filters). # This tag requires that the tag GENERATE_QHP is set to YES. @@ -1366,7 +1529,7 @@ QHP_CUST_FILTER_NAME = # The QHP_CUST_FILTER_ATTRS tag specifies the list of the attributes of the # custom filter to add. For more information please see Qt Help Project / Custom -# Filters (see: http://qt-project.org/doc/qt-4.8/qthelpproject.html#custom- +# Filters (see: https://doc.qt.io/archives/qt-4.8/qthelpproject.html#custom- # filters). # This tag requires that the tag GENERATE_QHP is set to YES. @@ -1374,7 +1537,7 @@ QHP_CUST_FILTER_ATTRS = # The QHP_SECT_FILTER_ATTRS tag specifies the list of the attributes this # project's filter section matches. Qt Help Project / Filter Attributes (see: -# http://qt-project.org/doc/qt-4.8/qthelpproject.html#filter-attributes). +# https://doc.qt.io/archives/qt-4.8/qthelpproject.html#filter-attributes). # This tag requires that the tag GENERATE_QHP is set to YES. QHP_SECT_FILTER_ATTRS = @@ -1423,7 +1586,7 @@ DISABLE_INDEX = NO # index structure (just like the one that is generated for HTML Help). For this # to work a browser that supports JavaScript, DHTML, CSS and frames is required # (i.e. any modern browser). Windows users are probably better off using the -# HTML help feature. Via custom stylesheets (see HTML_EXTRA_STYLESHEET) one can +# HTML help feature. Via custom style sheets (see HTML_EXTRA_STYLESHEET) one can # further fine-tune the look of the index. As an example, the default style # sheet generated by doxygen has an example that shows how to put an image at # the root of the tree instead of the PROJECT_NAME. Since the tree basically has @@ -1432,7 +1595,7 @@ DISABLE_INDEX = NO # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. -GENERATE_TREEVIEW = YES +GENERATE_TREEVIEW = NO # The ENUM_VALUES_PER_LINE tag can be used to set the number of enum values that # doxygen will group on one line in the generated HTML documentation. @@ -1442,7 +1605,7 @@ GENERATE_TREEVIEW = YES # Minimum value: 0, maximum value: 20, default value: 4. # This tag requires that the tag GENERATE_HTML is set to YES. -ENUM_VALUES_PER_LINE = 1 +ENUM_VALUES_PER_LINE = 4 # If the treeview is enabled (see GENERATE_TREEVIEW) then this tag can be used # to set the initial width (in pixels) of the frame in which the tree is shown. @@ -1451,13 +1614,24 @@ ENUM_VALUES_PER_LINE = 1 TREEVIEW_WIDTH = 250 -# When the EXT_LINKS_IN_WINDOW option is set to YES doxygen will open links to +# If the EXT_LINKS_IN_WINDOW option is set to YES, doxygen will open links to # external symbols imported via tag files in a separate window. # The default value is: NO. # This tag requires that the tag GENERATE_HTML is set to YES. EXT_LINKS_IN_WINDOW = NO +# If the HTML_FORMULA_FORMAT option is set to svg, doxygen will use the pdf2svg +# tool (see https://github.com/dawbarton/pdf2svg) or inkscape (see +# https://inkscape.org) to generate formulas as SVG images instead of PNGs for +# the HTML output. These images will generally look nicer at scaled resolutions. +# Possible values are: png (the default) and svg (looks nicer but requires the +# pdf2svg or inkscape tool). +# The default value is: png. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_FORMULA_FORMAT = png + # Use this tag to change the font size of LaTeX formulas included as images in # the HTML documentation. When you change the font size after a successful # doxygen run you need to manually remove any form_*.png images from the HTML @@ -1467,7 +1641,7 @@ EXT_LINKS_IN_WINDOW = NO FORMULA_FONTSIZE = 10 -# Use the FORMULA_TRANPARENT tag to determine whether or not the images +# Use the FORMULA_TRANSPARENT tag to determine whether or not the images # generated for formulas are transparent PNGs. Transparent PNGs are not # supported properly for IE 6.0, but are supported on all modern browsers. # @@ -1478,9 +1652,15 @@ FORMULA_FONTSIZE = 10 FORMULA_TRANSPARENT = YES +# The FORMULA_MACROFILE can contain LaTeX \newcommand and \renewcommand commands +# to create new LaTeX commands to be used in formulas as building blocks. See +# the section "Including formulas" for details. + +FORMULA_MACROFILE = + # Enable the USE_MATHJAX option to render LaTeX formulas using MathJax (see -# http://www.mathjax.org) which uses client side Javascript for the rendering -# instead of using prerendered bitmaps. Use this if you do not have LaTeX +# https://www.mathjax.org) which uses client side JavaScript for the rendering +# instead of using pre-rendered bitmaps. Use this if you do not have LaTeX # installed or if you want to formulas look prettier in the HTML output. When # enabled you may also need to install MathJax separately and configure the path # to it using the MATHJAX_RELPATH option. @@ -1506,11 +1686,11 @@ MATHJAX_FORMAT = HTML-CSS # MATHJAX_RELPATH should be ../mathjax. The default value points to the MathJax # Content Delivery Network so you can quickly see the result without installing # MathJax. However, it is strongly recommended to install a local copy of -# MathJax from http://www.mathjax.org before deployment. -# The default value is: http://cdn.mathjax.org/mathjax/latest. +# MathJax from https://www.mathjax.org before deployment. +# The default value is: https://cdn.jsdelivr.net/npm/mathjax@2. # This tag requires that the tag USE_MATHJAX is set to YES. -MATHJAX_RELPATH = http://cdn.mathjax.org/mathjax/latest +MATHJAX_RELPATH = https://cdn.jsdelivr.net/npm/mathjax@2 # The MATHJAX_EXTENSIONS tag can be used to specify one or more MathJax # extension names that should be enabled during MathJax rendering. For example @@ -1549,12 +1729,12 @@ MATHJAX_CODEFILE = SEARCHENGINE = YES # When the SERVER_BASED_SEARCH tag is enabled the search engine will be -# implemented using a web server instead of a web client using Javascript. There -# are two flavours of web server based searching depending on the -# EXTERNAL_SEARCH setting. When disabled, doxygen will generate a PHP script for -# searching and an index file used by the script. When EXTERNAL_SEARCH is -# enabled the indexing and searching needs to be provided by external tools. See -# the section "External Indexing and Searching" for details. +# implemented using a web server instead of a web client using JavaScript. There +# are two flavors of web server based searching depending on the EXTERNAL_SEARCH +# setting. When disabled, doxygen will generate a PHP script for searching and +# an index file used by the script. When EXTERNAL_SEARCH is enabled the indexing +# and searching needs to be provided by external tools. See the section +# "External Indexing and Searching" for details. # The default value is: NO. # This tag requires that the tag SEARCHENGINE is set to YES. @@ -1566,9 +1746,9 @@ SERVER_BASED_SEARCH = NO # external search engine pointed to by the SEARCHENGINE_URL option to obtain the # search results. # -# Doxygen ships with an example indexer ( doxyindexer) and search engine +# Doxygen ships with an example indexer (doxyindexer) and search engine # (doxysearch.cgi) which are based on the open source search engine library -# Xapian (see: http://xapian.org/). +# Xapian (see: https://xapian.org/). # # See the section "External Indexing and Searching" for details. # The default value is: NO. @@ -1579,9 +1759,9 @@ EXTERNAL_SEARCH = NO # The SEARCHENGINE_URL should point to a search engine hosted by a web server # which will return the search results when EXTERNAL_SEARCH is enabled. # -# Doxygen ships with an example indexer ( doxyindexer) and search engine +# Doxygen ships with an example indexer (doxyindexer) and search engine # (doxysearch.cgi) which are based on the open source search engine library -# Xapian (see: http://xapian.org/). See the section "External Indexing and +# Xapian (see: https://xapian.org/). See the section "External Indexing and # Searching" for details. # This tag requires that the tag SEARCHENGINE is set to YES. @@ -1617,7 +1797,7 @@ EXTRA_SEARCH_MAPPINGS = # Configuration options related to the LaTeX output #--------------------------------------------------------------------------- -# If the GENERATE_LATEX tag is set to YES doxygen will generate LaTeX output. +# If the GENERATE_LATEX tag is set to YES, doxygen will generate LaTeX output. # The default value is: YES. GENERATE_LATEX = NO @@ -1633,22 +1813,36 @@ LATEX_OUTPUT = latex # The LATEX_CMD_NAME tag can be used to specify the LaTeX command name to be # invoked. # -# Note that when enabling USE_PDFLATEX this option is only used for generating -# bitmaps for formulas in the HTML output, but not in the Makefile that is -# written to the output directory. -# The default file is: latex. +# Note that when not enabling USE_PDFLATEX the default is latex when enabling +# USE_PDFLATEX the default is pdflatex and when in the later case latex is +# chosen this is overwritten by pdflatex. For specific output languages the +# default can have been set differently, this depends on the implementation of +# the output language. # This tag requires that the tag GENERATE_LATEX is set to YES. -LATEX_CMD_NAME = latex +LATEX_CMD_NAME = # The MAKEINDEX_CMD_NAME tag can be used to specify the command name to generate # index for LaTeX. +# Note: This tag is used in the Makefile / make.bat. +# See also: LATEX_MAKEINDEX_CMD for the part in the generated output file +# (.tex). # The default file is: makeindex. # This tag requires that the tag GENERATE_LATEX is set to YES. MAKEINDEX_CMD_NAME = makeindex -# If the COMPACT_LATEX tag is set to YES doxygen generates more compact LaTeX +# The LATEX_MAKEINDEX_CMD tag can be used to specify the command name to +# generate index for LaTeX. In case there is no backslash (\) as first character +# it will be automatically added in the LaTeX code. +# Note: This tag is used in the generated output file (.tex). +# See also: MAKEINDEX_CMD_NAME for the part in the Makefile / make.bat. +# The default value is: makeindex. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_MAKEINDEX_CMD = makeindex + +# If the COMPACT_LATEX tag is set to YES, doxygen generates more compact LaTeX # documents. This may be useful for small projects and may help to save some # trees in general. # The default value is: NO. @@ -1663,12 +1857,15 @@ COMPACT_LATEX = NO # The default value is: a4. # This tag requires that the tag GENERATE_LATEX is set to YES. -PAPER_TYPE = letter +PAPER_TYPE = a4 # The EXTRA_PACKAGES tag can be used to specify one or more LaTeX package names -# that should be included in the LaTeX output. To get the times font for -# instance you can specify -# EXTRA_PACKAGES=times +# that should be included in the LaTeX output. The package can be specified just +# by its name or with the correct syntax as to be used with the LaTeX +# \usepackage command. To get the times font for instance you can specify : +# EXTRA_PACKAGES=times or EXTRA_PACKAGES={times} +# To use the option intlimits with the amsmath package you can specify: +# EXTRA_PACKAGES=[intlimits]{amsmath} # If left blank no extra packages will be included. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -1682,23 +1879,36 @@ EXTRA_PACKAGES = # # Note: Only use a user-defined header if you know what you are doing! The # following commands have a special meaning inside the header: $title, -# $datetime, $date, $doxygenversion, $projectname, $projectnumber. Doxygen will -# replace them by respectively the title of the page, the current date and time, -# only the current date, the version number of doxygen, the project name (see -# PROJECT_NAME), or the project number (see PROJECT_NUMBER). +# $datetime, $date, $doxygenversion, $projectname, $projectnumber, +# $projectbrief, $projectlogo. Doxygen will replace $title with the empty +# string, for the replacement values of the other commands the user is referred +# to HTML_HEADER. # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_HEADER = # The LATEX_FOOTER tag can be used to specify a personal LaTeX footer for the # generated LaTeX document. The footer should contain everything after the last -# chapter. If it is left blank doxygen will generate a standard footer. +# chapter. If it is left blank doxygen will generate a standard footer. See +# LATEX_HEADER for more information on how to generate a default footer and what +# special commands can be used inside the footer. # # Note: Only use a user-defined footer if you know what you are doing! # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_FOOTER = +# The LATEX_EXTRA_STYLESHEET tag can be used to specify additional user-defined +# LaTeX style sheets that are included after the standard style sheets created +# by doxygen. Using this option one can overrule certain style aspects. Doxygen +# will copy the style sheet files to the output directory. +# Note: The order of the extra style sheet files is of importance (e.g. the last +# style sheet in the list overrules the setting of the previous ones in the +# list). +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_EXTRA_STYLESHEET = + # The LATEX_EXTRA_FILES tag can be used to specify one or more extra images or # other source files which should be copied to the LATEX_OUTPUT output # directory. Note that the files will be copied as-is; there are no commands or @@ -1716,9 +1926,11 @@ LATEX_EXTRA_FILES = PDF_HYPERLINKS = YES -# If the LATEX_PDFLATEX tag is set to YES, doxygen will use pdflatex to generate -# the PDF file directly from the LaTeX files. Set this option to YES to get a -# higher quality PDF documentation. +# If the USE_PDFLATEX tag is set to YES, doxygen will use the engine as +# specified with LATEX_CMD_NAME to generate the PDF file directly from the LaTeX +# files. Set this option to YES, to get a higher quality PDF documentation. +# +# See also section LATEX_CMD_NAME for selecting the engine. # The default value is: YES. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -1752,17 +1964,33 @@ LATEX_SOURCE_CODE = NO # The LATEX_BIB_STYLE tag can be used to specify the style to use for the # bibliography, e.g. plainnat, or ieeetr. See -# http://en.wikipedia.org/wiki/BibTeX and \cite for more info. +# https://en.wikipedia.org/wiki/BibTeX and \cite for more info. # The default value is: plain. # This tag requires that the tag GENERATE_LATEX is set to YES. LATEX_BIB_STYLE = plain +# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated +# page will contain the date and time when the page was generated. Setting this +# to NO can help when comparing the output of multiple runs. +# The default value is: NO. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_TIMESTAMP = NO + +# The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) +# path from which the emoji images will be read. If a relative path is entered, +# it will be relative to the LATEX_OUTPUT directory. If left blank the +# LATEX_OUTPUT directory will be used. +# This tag requires that the tag GENERATE_LATEX is set to YES. + +LATEX_EMOJI_DIRECTORY = + #--------------------------------------------------------------------------- # Configuration options related to the RTF output #--------------------------------------------------------------------------- -# If the GENERATE_RTF tag is set to YES doxygen will generate RTF output. The +# If the GENERATE_RTF tag is set to YES, doxygen will generate RTF output. The # RTF output is optimized for Word 97 and may not look too pretty with other RTF # readers/editors. # The default value is: NO. @@ -1777,7 +2005,7 @@ GENERATE_RTF = NO RTF_OUTPUT = rtf -# If the COMPACT_RTF tag is set to YES doxygen generates more compact RTF +# If the COMPACT_RTF tag is set to YES, doxygen generates more compact RTF # documents. This may be useful for small projects and may help to save some # trees in general. # The default value is: NO. @@ -1797,9 +2025,9 @@ COMPACT_RTF = NO RTF_HYPERLINKS = NO -# Load stylesheet definitions from file. Syntax is similar to doxygen's config -# file, i.e. a series of assignments. You only have to provide replacements, -# missing definitions are set to their default value. +# Load stylesheet definitions from file. Syntax is similar to doxygen's +# configuration file, i.e. a series of assignments. You only have to provide +# replacements, missing definitions are set to their default value. # # See also section "Doxygen usage" for information on how to generate the # default style sheet that doxygen normally uses. @@ -1808,17 +2036,27 @@ RTF_HYPERLINKS = NO RTF_STYLESHEET_FILE = # Set optional variables used in the generation of an RTF document. Syntax is -# similar to doxygen's config file. A template extensions file can be generated -# using doxygen -e rtf extensionFile. +# similar to doxygen's configuration file. A template extensions file can be +# generated using doxygen -e rtf extensionFile. # This tag requires that the tag GENERATE_RTF is set to YES. RTF_EXTENSIONS_FILE = +# If the RTF_SOURCE_CODE tag is set to YES then doxygen will include source code +# with syntax highlighting in the RTF output. +# +# Note that which sources are shown also depends on other settings such as +# SOURCE_BROWSER. +# The default value is: NO. +# This tag requires that the tag GENERATE_RTF is set to YES. + +RTF_SOURCE_CODE = NO + #--------------------------------------------------------------------------- # Configuration options related to the man page output #--------------------------------------------------------------------------- -# If the GENERATE_MAN tag is set to YES doxygen will generate man pages for +# If the GENERATE_MAN tag is set to YES, doxygen will generate man pages for # classes and files. # The default value is: NO. @@ -1842,6 +2080,13 @@ MAN_OUTPUT = man MAN_EXTENSION = .3 +# The MAN_SUBDIR tag determines the name of the directory created within +# MAN_OUTPUT in which the man pages are placed. If defaults to man followed by +# MAN_EXTENSION with the initial . removed. +# This tag requires that the tag GENERATE_MAN is set to YES. + +MAN_SUBDIR = + # If the MAN_LINKS tag is set to YES and doxygen generates man output, then it # will generate one additional man file for each entity documented in the real # man page(s). These additional files only source the real man page, but without @@ -1855,7 +2100,7 @@ MAN_LINKS = NO # Configuration options related to the XML output #--------------------------------------------------------------------------- -# If the GENERATE_XML tag is set to YES doxygen will generate an XML file that +# If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that # captures the structure of the code including all documentation. # The default value is: NO. @@ -1869,7 +2114,7 @@ GENERATE_XML = NO XML_OUTPUT = xml -# If the XML_PROGRAMLISTING tag is set to YES doxygen will dump the program +# If the XML_PROGRAMLISTING tag is set to YES, doxygen will dump the program # listings (including syntax highlighting and cross-referencing information) to # the XML output. Note that enabling this will significantly increase the size # of the XML output. @@ -1878,11 +2123,18 @@ XML_OUTPUT = xml XML_PROGRAMLISTING = YES +# If the XML_NS_MEMB_FILE_SCOPE tag is set to YES, doxygen will include +# namespace members in file scope as well, matching the HTML output. +# The default value is: NO. +# This tag requires that the tag GENERATE_XML is set to YES. + +XML_NS_MEMB_FILE_SCOPE = NO + #--------------------------------------------------------------------------- # Configuration options related to the DOCBOOK output #--------------------------------------------------------------------------- -# If the GENERATE_DOCBOOK tag is set to YES doxygen will generate Docbook files +# If the GENERATE_DOCBOOK tag is set to YES, doxygen will generate Docbook files # that can be used to generate PDF. # The default value is: NO. @@ -1896,14 +2148,23 @@ GENERATE_DOCBOOK = NO DOCBOOK_OUTPUT = docbook +# If the DOCBOOK_PROGRAMLISTING tag is set to YES, doxygen will include the +# program listings (including syntax highlighting and cross-referencing +# information) to the DOCBOOK output. Note that enabling this will significantly +# increase the size of the DOCBOOK output. +# The default value is: NO. +# This tag requires that the tag GENERATE_DOCBOOK is set to YES. + +DOCBOOK_PROGRAMLISTING = NO + #--------------------------------------------------------------------------- # Configuration options for the AutoGen Definitions output #--------------------------------------------------------------------------- -# If the GENERATE_AUTOGEN_DEF tag is set to YES doxygen will generate an AutoGen -# Definitions (see http://autogen.sf.net) file that captures the structure of -# the code including all documentation. Note that this feature is still -# experimental and incomplete at the moment. +# If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an +# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures +# the structure of the code including all documentation. Note that this feature +# is still experimental and incomplete at the moment. # The default value is: NO. GENERATE_AUTOGEN_DEF = NO @@ -1912,7 +2173,7 @@ GENERATE_AUTOGEN_DEF = NO # Configuration options related to the Perl module output #--------------------------------------------------------------------------- -# If the GENERATE_PERLMOD tag is set to YES doxygen will generate a Perl module +# If the GENERATE_PERLMOD tag is set to YES, doxygen will generate a Perl module # file that captures the structure of the code including all documentation. # # Note that this feature is still experimental and incomplete at the moment. @@ -1920,7 +2181,7 @@ GENERATE_AUTOGEN_DEF = NO GENERATE_PERLMOD = NO -# If the PERLMOD_LATEX tag is set to YES doxygen will generate the necessary +# If the PERLMOD_LATEX tag is set to YES, doxygen will generate the necessary # Makefile rules, Perl scripts and LaTeX code to be able to generate PDF and DVI # output from the Perl module output. # The default value is: NO. @@ -1928,9 +2189,9 @@ GENERATE_PERLMOD = NO PERLMOD_LATEX = NO -# If the PERLMOD_PRETTY tag is set to YES the Perl module output will be nicely +# If the PERLMOD_PRETTY tag is set to YES, the Perl module output will be nicely # formatted so it can be parsed by a human reader. This is useful if you want to -# understand what is going on. On the other hand, if this tag is set to NO the +# understand what is going on. On the other hand, if this tag is set to NO, the # size of the Perl module output will be much smaller and Perl will parse it # just the same. # The default value is: YES. @@ -1950,14 +2211,14 @@ PERLMOD_MAKEVAR_PREFIX = # Configuration options related to the preprocessor #--------------------------------------------------------------------------- -# If the ENABLE_PREPROCESSING tag is set to YES doxygen will evaluate all +# If the ENABLE_PREPROCESSING tag is set to YES, doxygen will evaluate all # C-preprocessor directives found in the sources and include files. # The default value is: YES. ENABLE_PREPROCESSING = YES -# If the MACRO_EXPANSION tag is set to YES doxygen will expand all macro names -# in the source code. If set to NO only conditional compilation will be +# If the MACRO_EXPANSION tag is set to YES, doxygen will expand all macro names +# in the source code. If set to NO, only conditional compilation will be # performed. Macro expansion can be done in a controlled way by setting # EXPAND_ONLY_PREDEF to YES. # The default value is: NO. @@ -1973,7 +2234,7 @@ MACRO_EXPANSION = NO EXPAND_ONLY_PREDEF = NO -# If the SEARCH_INCLUDES tag is set to YES the includes files in the +# If the SEARCH_INCLUDES tag is set to YES, the include files in the # INCLUDE_PATH will be searched if a #include is found. # The default value is: YES. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. @@ -2015,9 +2276,9 @@ PREDEFINED = EXPAND_AS_DEFINED = # If the SKIP_FUNCTION_MACROS tag is set to YES then doxygen's preprocessor will -# remove all refrences to function-like macros that are alone on a line, have an -# all uppercase name, and do not end with a semicolon. Such function macros are -# typically used for boiler-plate code, and will confuse the parser if not +# remove all references to function-like macros that are alone on a line, have +# an all uppercase name, and do not end with a semicolon. Such function macros +# are typically used for boiler-plate code, and will confuse the parser if not # removed. # The default value is: YES. # This tag requires that the tag ENABLE_PREPROCESSING is set to YES. @@ -2037,7 +2298,7 @@ SKIP_FUNCTION_MACROS = YES # where loc1 and loc2 can be relative or absolute paths or URLs. See the # section "Linking to external documentation" for more information about the use # of tag files. -# Note: Each tag file must have an unique name (where the name does NOT include +# Note: Each tag file must have a unique name (where the name does NOT include # the path). If a tag file is not located in the directory in which doxygen is # run, you must also specify the path to the tagfile here. @@ -2049,37 +2310,32 @@ TAGFILES = GENERATE_TAGFILE = -# If the ALLEXTERNALS tag is set to YES all external class will be listed in the -# class index. If set to NO only the inherited external classes will be listed. +# If the ALLEXTERNALS tag is set to YES, all external class will be listed in +# the class index. If set to NO, only the inherited external classes will be +# listed. # The default value is: NO. ALLEXTERNALS = NO -# If the EXTERNAL_GROUPS tag is set to YES all external groups will be listed in -# the modules index. If set to NO, only the current project's groups will be +# If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed +# in the modules index. If set to NO, only the current project's groups will be # listed. # The default value is: YES. EXTERNAL_GROUPS = YES -# If the EXTERNAL_PAGES tag is set to YES all external pages will be listed in +# If the EXTERNAL_PAGES tag is set to YES, all external pages will be listed in # the related pages index. If set to NO, only the current project's pages will # be listed. # The default value is: YES. EXTERNAL_PAGES = YES -# The PERL_PATH should be the absolute path and name of the perl script -# interpreter (i.e. the result of 'which perl'). -# The default file (with absolute path) is: /usr/bin/perl. - -PERL_PATH = @PERL@ - #--------------------------------------------------------------------------- # Configuration options related to the dot tool #--------------------------------------------------------------------------- -# If the CLASS_DIAGRAMS tag is set to YES doxygen will generate a class diagram +# If the CLASS_DIAGRAMS tag is set to YES, doxygen will generate a class diagram # (in HTML and LaTeX) for classes with base or super classes. Setting the tag to # NO turns the diagrams off. Note that this option also works with HAVE_DOT # disabled, but it is recommended to install and use dot, since it yields more @@ -2088,15 +2344,6 @@ PERL_PATH = @PERL@ CLASS_DIAGRAMS = YES -# You can define message sequence charts within doxygen comments using the \msc -# command. Doxygen will then run the mscgen tool (see: -# http://www.mcternan.me.uk/mscgen/)) to produce the chart and insert it in the -# documentation. The MSCGEN_PATH tag allows you to specify the directory where -# the mscgen tool resides. If left empty the tool is assumed to be found in the -# default search path. - -MSCGEN_PATH = - # You can include diagrams made with dia in doxygen documentation. Doxygen will # then run dia to produce the diagram and insert it in the documentation. The # DIA_PATH tag allows you to specify the directory where the dia binary resides. @@ -2104,7 +2351,7 @@ MSCGEN_PATH = DIA_PATH = -# If set to YES, the inheritance and collaboration graphs will hide inheritance +# If set to YES the inheritance and collaboration graphs will hide inheritance # and usage relations if the target is undocumented or is not a class. # The default value is: YES. @@ -2115,9 +2362,9 @@ HIDE_UNDOC_RELATIONS = YES # http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # Bell Labs. The other options in this section have no effect if this option is # set to NO -# The default value is: NO. +# The default value is: YES. -HAVE_DOT = @DOXYGEN_USE_DOT@ +HAVE_DOT = NO # The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed # to run in parallel. When set to 0 doxygen will base this on the number of @@ -2129,7 +2376,7 @@ HAVE_DOT = @DOXYGEN_USE_DOT@ DOT_NUM_THREADS = 0 -# When you want a differently looking font n the dot files that doxygen +# When you want a differently looking font in the dot files that doxygen # generates you can specify the font name using DOT_FONTNAME. You need to make # sure dot is able to find the font, which can be done by putting it in a # standard location or by setting the DOTFONTPATH environment variable or by @@ -2177,7 +2424,7 @@ COLLABORATION_GRAPH = YES GROUP_GRAPHS = YES -# If the UML_LOOK tag is set to YES doxygen will generate inheritance and +# If the UML_LOOK tag is set to YES, doxygen will generate inheritance and # collaboration diagrams in a style similar to the OMG's Unified Modeling # Language. # The default value is: NO. @@ -2229,22 +2476,24 @@ INCLUDED_BY_GRAPH = YES # # Note that enabling this option will significantly increase the time of a run. # So in most cases it will be better to enable call graphs for selected -# functions only using the \callgraph command. +# functions only using the \callgraph command. Disabling a call graph can be +# accomplished by means of the command \hidecallgraph. # The default value is: NO. # This tag requires that the tag HAVE_DOT is set to YES. -CALL_GRAPH = YES +CALL_GRAPH = NO # If the CALLER_GRAPH tag is set to YES then doxygen will generate a caller # dependency graph for every global function or class method. # # Note that enabling this option will significantly increase the time of a run. # So in most cases it will be better to enable caller graphs for selected -# functions only using the \callergraph command. +# functions only using the \callergraph command. Disabling a caller graph can be +# accomplished by means of the command \hidecallergraph. # The default value is: NO. # This tag requires that the tag HAVE_DOT is set to YES. -CALLER_GRAPH = YES +CALLER_GRAPH = NO # If the GRAPHICAL_HIERARCHY tag is set to YES then doxygen will graphical # hierarchy of all classes instead of a textual one. @@ -2263,11 +2512,17 @@ GRAPHICAL_HIERARCHY = YES DIRECTORY_GRAPH = YES # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images -# generated by dot. +# generated by dot. For an explanation of the image formats see the section +# output formats in the documentation of the dot tool (Graphviz (see: +# http://www.graphviz.org/)). # Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order # to make the SVG files visible in IE 9+ (other browsers do not have this # requirement). -# Possible values are: png, jpg, gif and svg. +# Possible values are: png, png:cairo, png:cairo:cairo, png:cairo:gd, png:gd, +# png:gd:gd, jpg, jpg:cairo, jpg:cairo:gd, jpg:gd, jpg:gd:gd, gif, gif:cairo, +# gif:cairo:gd, gif:gd, gif:gd:gd, svg, png:gd, png:gd:gd, png:cairo, +# png:cairo:gd, png:cairo:cairo, png:cairo:gdiplus, png:gdiplus and +# png:gdiplus:gdiplus. # The default value is: png. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2310,6 +2565,24 @@ MSCFILE_DIRS = DIAFILE_DIRS = +# When using plantuml, the PLANTUML_JAR_PATH tag should be used to specify the +# path where java can find the plantuml.jar file. If left blank, it is assumed +# PlantUML is not used or called during a preprocessing step. Doxygen will +# generate a warning when it encounters a \startuml command in this case and +# will not generate output for the diagram. + +PLANTUML_JAR_PATH = + +# When using plantuml, the PLANTUML_CFG_FILE tag can be used to specify a +# configuration file for plantuml. + +PLANTUML_CFG_FILE = + +# When using plantuml, the specified paths are searched for files specified by +# the !include statement in a plantuml block. + +PLANTUML_INCLUDE_PATH = + # The DOT_GRAPH_MAX_NODES tag can be used to set the maximum number of nodes # that will be shown in the graph. If the number of nodes in a graph becomes # larger than this value, doxygen will truncate the graph, which is visualized @@ -2346,14 +2619,14 @@ MAX_DOT_GRAPH_DEPTH = 0 DOT_TRANSPARENT = NO -# Set the DOT_MULTI_TARGETS tag to YES allow dot to generate multiple output +# Set the DOT_MULTI_TARGETS tag to YES to allow dot to generate multiple output # files in one run (i.e. multiple -o and -T options on the command line). This # makes dot run faster, but since only newer versions of dot (>1.8.10) support # this, this feature is disabled by default. # The default value is: NO. # This tag requires that the tag HAVE_DOT is set to YES. -DOT_MULTI_TARGETS = YES +DOT_MULTI_TARGETS = NO # If the GENERATE_LEGEND tag is set to YES doxygen will generate a legend page # explaining the meaning of the various boxes and arrows in the dot generated @@ -2363,7 +2636,7 @@ DOT_MULTI_TARGETS = YES GENERATE_LEGEND = YES -# If the DOT_CLEANUP tag is set to YES doxygen will remove the intermediate dot +# If the DOT_CLEANUP tag is set to YES, doxygen will remove the intermediate dot # files that are used to generate the various graphs. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. diff --git a/etc/cmake/options.cmake b/etc/cmake/options.cmake new file mode 100644 index 00000000000..d4e74e2607a --- /dev/null +++ b/etc/cmake/options.cmake @@ -0,0 +1,77 @@ +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +find_package(PkgConfig) + +option(OTBR_DOC "Build documentation" OFF) + +option(OTBR_BACKBONE_ROUTER "Enable Backbone Router" OFF) +if (OTBR_BACKBONE_ROUTER) + target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_BACKBONE_ROUTER=1) +endif() + +option(OTBR_BORDER_ROUTING "Enable Border Routing Manager" OFF) + +option(OTBR_DBUS "Enable DBus support" OFF) +if(OTBR_DBUS) + pkg_check_modules(DBUS REQUIRED dbus-1) + pkg_get_variable(OTBR_DBUS_SYSTEM_BUS_SERVICES_DIR dbus-1 system_bus_services_dir) + target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_DBUS_SERVER=1) +endif() + +option(OTBR_DUA_ROUTING "Enable Backbone Router DUA Routing" OFF) +if (OTBR_DUA_ROUTING) + target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_DUA_ROUTING=1) +endif() + +option(OTBR_OPENWRT "Enable OpenWrt support" OFF) +if(OTBR_OPENWRT) + target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_OPENWRT=1) +endif() + +option(OTBR_REST "Enable Rest Server" OFF) +if(OTBR_REST) + target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_REST_SERVER=1) +endif() + +option(OTBR_SRP_ADVERTISING_PROXY "Enable Advertising Proxy" OFF) +if (OTBR_SRP_ADVERTISING_PROXY) + target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_SRP_ADVERTISING_PROXY=1) +endif() + +option(OTBR_DNSSD_DISCOVERY_PROXY "Enable DNS-SD Discovery Proxy support" OFF) +if (OTBR_DNSSD_DISCOVERY_PROXY) + target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_DNSSD_DISCOVERY_PROXY=1) +endif() + +option(OTBR_UNSECURE_JOIN "Enable unsecure joining" OFF) +if(OTBR_UNSECURE_JOIN) + target_compile_definitions(otbr-config INTERFACE OTBR_ENABLE_UNSECURE_JOIN=1) +endif() + +option(OTBR_WEB "Enable Web GUI" OFF) diff --git a/etc/docker/Dockerfile b/etc/docker/Dockerfile index 96411c35222..62840261d4a 100644 --- a/etc/docker/Dockerfile +++ b/etc/docker/Dockerfile @@ -25,15 +25,24 @@ # POSSIBILITY OF SUCH DAMAGE. # -FROM ubuntu:bionic +ARG BASE_IMAGE=ubuntu:bionic +FROM ${BASE_IMAGE} +ARG INFRA_IF_NAME +ARG BORDER_ROUTING +ARG BACKBONE_ROUTER ARG OT_BACKBONE_CI ARG OTBR_OPTIONS ARG DNS64 ARG NAT64 ARG REFERENCE_DEVICE ARG RELEASE +ARG REST_API +ARG WEB_GUI +ENV INFRA_IF_NAME=${INFRA_IF_NAME:-eth0} +ENV BORDER_ROUTING=${BORDER_ROUTING:-1} +ENV BACKBONE_ROUTER=${BACKBONE_ROUTER:-1} ENV OT_BACKBONE_CI=${OT_BACKBONE_CI:-0} ENV OTBR_OPTIONS=${OTBR_OPTIONS} ENV DEBIAN_FRONTEND noninteractive @@ -42,6 +51,8 @@ ENV REFERENCE_DEVICE=${REFERENCE_DEVICE:-0} ENV RELEASE=${RELEASE:-1} ENV NAT64=${NAT64:-1} ENV DNS64=${DNS64:-1} +ENV WEB_GUI=${WEB_GUI:-1} +ENV REST_API=${REST_API:-1} ENV DOCKER 1 RUN env @@ -58,10 +69,11 @@ ENV OTBR_DOCKER_DEPS git ca-certificates # Required and installed during build (script/bootstrap), could be removed ENV OTBR_BUILD_DEPS apt-utils build-essential psmisc ninja-build cmake wget ca-certificates \ libreadline-dev libncurses-dev libcpputest-dev libdbus-1-dev libavahi-common-dev \ - libavahi-client-dev libboost-dev libboost-filesystem-dev libboost-system-dev libjsoncpp-dev + libavahi-client-dev libboost-dev libboost-filesystem-dev libboost-system-dev libjsoncpp-dev \ + libnetfilter-queue-dev # Required for OpenThread Backbone CI -ENV OTBR_OT_BACKBONE_CI_DEPS curl ca-certificates +ENV OTBR_OT_BACKBONE_CI_DEPS curl lcov wget build-essential # Required and installed during build (script/bootstrap) when RELEASE=1, could be removed ENV OTBR_NORELEASE_DEPS \ @@ -71,11 +83,12 @@ ENV OTBR_NORELEASE_DEPS \ RUN apt-get update \ && apt-get install --no-install-recommends -y $OTBR_DOCKER_REQS $OTBR_DOCKER_DEPS \ + && ([ "${OT_BACKBONE_CI}" != "1" ] || apt-get install --no-install-recommends -y $OTBR_OT_BACKBONE_CI_DEPS) \ && ln -fs /usr/share/zoneinfo/UTC /etc/localtime \ && ./script/bootstrap \ && ./script/setup \ && chmod 644 /etc/bind/named.conf.options \ - && [ "${OT_BACKBONE_CI}" = "1" ] || ( \ + && ([ "${OT_BACKBONE_CI}" = "1" ] || ( \ mv ./script /tmp \ && mv ./etc /tmp \ && find . -delete \ @@ -86,8 +99,7 @@ RUN apt-get update \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $OTBR_BUILD_DEPS \ && ([ "${RELEASE}" = 1 ] || apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false "$OTBR_NORELEASE_DEPS";) \ && rm -rf /var/lib/apt/lists/* \ - ) \ - && [ "${OT_BACKBONE_CI}" != "1" ] || apt-get install --no-install-recommends -y $OTBR_OT_BACKBONE_CI_DEPS + )) ENTRYPOINT ["/app/etc/docker/docker_entrypoint.sh"] diff --git a/etc/docker/docker_entrypoint.sh b/etc/docker/docker_entrypoint.sh index 1ac169db256..5e38ba63866 100755 --- a/etc/docker/docker_entrypoint.sh +++ b/etc/docker/docker_entrypoint.sh @@ -44,7 +44,7 @@ function parse_args() shift ;; --backbone-interface | -B) - BACKBONE_INTERFACE_ARG="-B $2" + BACKBONE_INTERFACE=$2 shift shift ;; @@ -72,13 +72,14 @@ parse_args "$@" [ -n "$RADIO_URL" ] || RADIO_URL="spinel+hdlc+uart:///dev/ttyUSB0" [ -n "$TUN_INTERFACE_NAME" ] || TUN_INTERFACE_NAME="wpan0" +[ -n "$BACKBONE_INTERFACE" ] || BACKBONE_INTERFACE="eth0" [ -n "$AUTO_PREFIX_ROUTE" ] || AUTO_PREFIX_ROUTE=true [ -n "$AUTO_PREFIX_SLAAC" ] || AUTO_PREFIX_SLAAC=true [ -n "$NAT64_PREFIX" ] || NAT64_PREFIX="64:ff9b::/96" echo "RADIO_URL:" $RADIO_URL echo "TUN_INTERFACE_NAME:" $TUN_INTERFACE_NAME -echo "BACKBONE_INTERFACE: $BACKBONE_INTERFACE_ARG" +echo "BACKBONE_INTERFACE: $BACKBONE_INTERFACE" echo "NAT64_PREFIX:" $NAT64_PREFIX echo "AUTO_PREFIX_ROUTE:" $AUTO_PREFIX_ROUTE echo "AUTO_PREFIX_SLAAC:" $AUTO_PREFIX_SLAAC @@ -88,7 +89,7 @@ NAT64_PREFIX=${NAT64_PREFIX/\//\\\/} sed -i "s/^prefix.*$/prefix $NAT64_PREFIX/" /etc/tayga.conf sed -i "s/dns64.*$/dns64 $NAT64_PREFIX {};/" /etc/bind/named.conf.options -echo "OTBR_AGENT_OPTS=\"-I $TUN_INTERFACE_NAME $BACKBONE_INTERFACE_ARG -d7 $RADIO_URL\"" >/etc/default/otbr-agent +echo "OTBR_AGENT_OPTS=\"-I $TUN_INTERFACE_NAME -B $BACKBONE_INTERFACE -d7 $RADIO_URL\"" >/etc/default/otbr-agent echo "OTBR_WEB_OPTS=\"-I $TUN_INTERFACE_NAME -d7 -p 80\"" >/etc/default/otbr-web /app/script/server diff --git a/examples/platforms/beagleboneblack/default b/examples/platforms/beagleboneblack/default index 6fe536ae60a..e92e238e562 100644 --- a/examples/platforms/beagleboneblack/default +++ b/examples/platforms/beagleboneblack/default @@ -34,7 +34,11 @@ SWAP_REQUIRED=false NAT64=1 DNS64=1 -DHCPV6_PD=1 +DHCPV6_PD=0 NETWORK_MANAGER=1 # disabled unless specifically added for the device NETWORK_MANAGER_WIFI=0 +BACKBONE_ROUTER=0 +BORDER_ROUTING=0 +WEB_GUI=1 +REST_API=1 diff --git a/third_party/mdl/CMakeLists.txt b/examples/platforms/debian/default similarity index 84% rename from third_party/mdl/CMakeLists.txt rename to examples/platforms/debian/default index 41646616654..c76b5423e6c 100644 --- a/third_party/mdl/CMakeLists.txt +++ b/examples/platforms/debian/default @@ -1,5 +1,6 @@ +#!/bin/sh # -# Copyright (c) 2020, The OpenThread Authors. +# Copyright (c) 2021, The OpenThread Authors. # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -26,9 +27,11 @@ # POSSIBILITY OF SUCH DAMAGE. # - -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/repo/material.min.js - DESTINATION ${OTBR_WEB_DATADIR}/frontend/res/js) - -install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/repo/material.min.css - DESTINATION ${OTBR_WEB_DATADIR}/frontend/res/css) +# shellcheck disable=SC2034 +NAT64=0 +DNS64=0 +DHCPV6_PD=0 +NETWORK_MANAGER=0 +BACKBONE_ROUTER=1 +BORDER_ROUTING=1 +REST_API=1 diff --git a/examples/platforms/fedora/default b/examples/platforms/fedora/default index 7c4468e7316..079cc6165d5 100644 --- a/examples/platforms/fedora/default +++ b/examples/platforms/fedora/default @@ -32,3 +32,7 @@ NAT64=0 DNS64=0 DHCPV6_PD=0 NETWORK_MANAGER=0 +BACKBONE_ROUTER=0 +BORDER_ROUTING=0 +WEB_GUI=1 +REST_API=1 diff --git a/examples/platforms/raspbian/default b/examples/platforms/raspbian/default index bc8dce30cbe..29df9cfae7e 100644 --- a/examples/platforms/raspbian/default +++ b/examples/platforms/raspbian/default @@ -30,5 +30,9 @@ # shellcheck disable=SC2034 NAT64=1 DNS64=1 -DHCPV6_PD=1 -NETWORK_MANAGER=1 +DHCPV6_PD=0 +NETWORK_MANAGER=0 +BACKBONE_ROUTER=1 +BORDER_ROUTING=1 +WEB_GUI=1 +REST_API=1 diff --git a/examples/platforms/ubuntu/default b/examples/platforms/ubuntu/default index fce15a150e1..29df9cfae7e 100644 --- a/examples/platforms/ubuntu/default +++ b/examples/platforms/ubuntu/default @@ -32,3 +32,7 @@ NAT64=1 DNS64=1 DHCPV6_PD=0 NETWORK_MANAGER=0 +BACKBONE_ROUTER=1 +BORDER_ROUTING=1 +WEB_GUI=1 +REST_API=1 diff --git a/include/openthread-br/config.h b/include/openthread-br/config.h index 4929710576f..a663fd9d297 100644 --- a/include/openthread-br/config.h +++ b/include/openthread-br/config.h @@ -26,6 +26,10 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes config file if defined. + */ #ifndef OTBR_CONFIG_H_ #define OTBR_CONFIG_H_ diff --git a/script/_border_routing b/script/_border_routing new file mode 100644 index 00000000000..cd9e2dddfb4 --- /dev/null +++ b/script/_border_routing @@ -0,0 +1,111 @@ +#!/bin/bash +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# +# Description: +# This script sets up border routing configurations. +# + +readonly INFRA_IF_NAME="${INFRA_IF_NAME:-wlan0}" +readonly SYSCTL_ACCEPT_RA_FILE="/etc/sysctl.d/60-otbr-accept-ra.conf" +readonly DHCPCD_CONF_FILE="/etc/dhcpcd.conf" +readonly DHCPCD_CONF_BACKUP_FILE="$DHCPCD_CONF_FILE.orig" + +accept_ra_install() +{ + sudo tee $SYSCTL_ACCEPT_RA_FILE <&2 ' *** WARNING: systemctl not found. otbr cannot start on boot.' fi - - install_reference_device_deps } otbr_update() diff --git a/third_party/angular/CMakeLists.txt b/script/_rt_tables similarity index 81% rename from third_party/angular/CMakeLists.txt rename to script/_rt_tables index 00317eb5359..ef1437d50c4 100644 --- a/third_party/angular/CMakeLists.txt +++ b/script/_rt_tables @@ -1,3 +1,4 @@ +#!/bin/bash # # Copyright (c) 2020, The OpenThread Authors. # All rights reserved. @@ -25,10 +26,22 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # +# Description: +# This script manipulates router tables. +# + +rt_tables_uninstall() +{ + with BACKBONE_ROUTER || return 0 + + sudo sed -i.bak '/88\s\+openthread/d' /etc/iproute2/rt_tables +} + +rt_tables_install() +{ + with BACKBONE_ROUTER || return 0 + + rt_tables_uninstall -install(FILES - ${CMAKE_CURRENT_SOURCE_DIR}/repo/angular.min.js - ${CMAKE_CURRENT_SOURCE_DIR}/repo/angular-animate.min.js - ${CMAKE_CURRENT_SOURCE_DIR}/repo/angular-aria.min.js - ${CMAKE_CURRENT_SOURCE_DIR}/repo/angular-messages.min.js - DESTINATION ${OTBR_WEB_DATADIR}/frontend/res/js) + sudo sh -c 'echo "88 openthread" >>/etc/iproute2/rt_tables' +} diff --git a/script/bootstrap b/script/bootstrap index 3f3cb6de5a4..3e2decfb47b 100755 --- a/script/bootstrap +++ b/script/bootstrap @@ -54,6 +54,12 @@ install_packages_apt() # mDNS sudo apt-get install --no-install-recommends -y libavahi-client3 libavahi-common-dev libavahi-client-dev avahi-daemon + (MDNS_RESPONDER_SOURCE_NAME=mDNSResponder-878.30.4 \ + && cd /tmp \ + && wget -4 --no-check-certificate https://opensource.apple.com/tarballs/mDNSResponder/$MDNS_RESPONDER_SOURCE_NAME.tar.gz \ + && tar xvf $MDNS_RESPONDER_SOURCE_NAME.tar.gz \ + && cd $MDNS_RESPONDER_SOURCE_NAME/mDNSPosix \ + && make os=linux && sudo make install os=linux) # Boost sudo apt-get install --no-install-recommends -y libboost-dev libboost-filesystem-dev libboost-system-dev @@ -74,18 +80,19 @@ install_packages_apt() without NETWORK_MANAGER || sudo apt-get install --no-install-recommends -y dnsmasq network-manager # dhcpcd5 - without DHCPV6_PD || { - # More details can be found in issue: #122 - if [ "$PLATFORM" = 'raspbian' ]; then - # TODO should figure out a better way to deal with this - sudo dpkg -i third_party/dhcpcd/dhcpcd5_6.11.5-1+rpt2_armhf.deb - else - sudo apt-get install --no-install-recommends -y dhcpcd5 - fi - } + without DHCPV6_PD || sudo apt-get install --no-install-recommends -y dhcpcd5 # libjsoncpp sudo apt-get install --no-install-recommends -y libjsoncpp1 libjsoncpp-dev + + # reference device + without REFERENCE_DEVICE || sudo apt-get install --no-install-recommends -y radvd dnsutils + + # backbone-router + without BACKBONE_ROUTER || sudo apt-get install --no-install-recommends -y iptables libnetfilter-queue1 libnetfilter-queue-dev + + # web dependencies + without WEB_GUI || command -v npm || sudo apt-get install -y nodejs npm } install_packages_opkg() diff --git a/script/clang-format b/script/clang-format index addb7fa47fc..01b9535e0c2 100755 --- a/script/clang-format +++ b/script/clang-format @@ -27,7 +27,7 @@ # POSSIBILITY OF SUCH DAMAGE. # -CLANG_FORMAT_VERSION="clang-format version 10.0" +CLANG_FORMAT_VERSION="clang-format version 9.0" die() { @@ -39,18 +39,18 @@ die() # expand_aliases shell option is set using shopt. shopt -s expand_aliases -if command -v clang-format-10 >/dev/null; then - alias clang-format=clang-format-10 +if command -v clang-format-9 >/dev/null; then + alias clang-format=clang-format-9 elif command -v clang-format >/dev/null; then case "$(clang-format --version)" in "$CLANG_FORMAT_VERSION"*) ;; *) - die "$(clang-format --version); clang-format 10.0 required" + die "$(clang-format --version); clang-format 9.0 required" ;; esac else - die "clang-format 10.0 required" + die "clang-format 9.0 required" fi clang-format "$@" || die diff --git a/script/cmake-build b/script/cmake-build new file mode 100755 index 00000000000..799e955e972 --- /dev/null +++ b/script/cmake-build @@ -0,0 +1,85 @@ +#!/bin/bash +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +# +# This script calls cmake and ninja to compile otbr-agent. +# +# Compile with default build options: +# +# script/cmake-build +# +# Compile with the specified build option enabled: +# +# script/cmake-build -D${option}=ON +# +# Compile with the specified ninja build target: +# +# OTBR_TARGET="${target}" script/cmake-build +# +# Compile with the specified build directory: +# +# OTBR_BUILD_DIR="./build/temp" script/cmake-build +# +# Examples: +# +# script/cmake-build +# +# script/cmake-build -DOTBR_DBUS=ON +# +# OTBR_BUILD_DIR="./build/temp" OTBR_TARGET="otbr-agent" script/cmake-build -DOTBR_DBUS=ON +# + +# shellcheck source=script/_initrc +. "$(dirname "$0")"/_initrc + +readonly OTBR_TOP_SRCDIR="$PWD" +readonly OTBR_TOP_BUILD_DIR="${BUILD_DIR}/otbr" + +OTBR_TARGET=${OTBR_TARGET:-} + +main() +{ + local builddir="${OTBR_BUILD_DIR:-${OTBR_TOP_BUILD_DIR}}" + + mkdir -p "${builddir}" + + ( + cd "${builddir}" || die "Failed to enter ${builddir}" + + cmake -GNinja "${OTBR_TOP_SRCDIR}" "$@" + + if [[ -n ${OTBR_TARGET[*]} ]]; then + ninja "${OTBR_TARGET[@]}" + else + ninja + fi + ) +} + +main "$@" diff --git a/script/console b/script/console index 232e51aecd0..912c163759d 100755 --- a/script/console +++ b/script/console @@ -34,6 +34,8 @@ . "$(dirname "$0")"/_initrc # shellcheck source=script/_ipforward . script/_ipforward +# shellcheck source=script/_border_routing +. script/_border_routing readonly TUN="${TUN:-wpan0}" readonly RADIO_URL="${RADIO_URL:-spinel+hdlc+uart:///dev/ttyUSB0}" @@ -59,12 +61,13 @@ main() killall_services trap on_exit INT TERM EXIT + accept_ra_enable ipforward_enable - sudo sh -s <:otbr-mdns> $<$:otbr-ubus> $<$:otbr-rest> + $<$:otbr-backbone-router> + openthread-posix openthread-cli-ftd openthread-ftd openthread-posix @@ -84,7 +91,8 @@ elseif(NOT OTBR_OPENWRT) RENAME otbr-agent) endif() -install(FILES otbr-agent.default +configure_file(otbr-agent.default.in otbr-agent.default) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/otbr-agent.default DESTINATION ${CMAKE_INSTALL_FULL_SYSCONFDIR}/default RENAME otbr-agent ) diff --git a/src/agent/advertising_proxy.cpp b/src/agent/advertising_proxy.cpp new file mode 100644 index 00000000000..18a8f3ed96b --- /dev/null +++ b/src/agent/advertising_proxy.cpp @@ -0,0 +1,331 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * The file implements the Advertising Proxy. + */ + +#define OTBR_LOG_TAG "ADPROXY" + +#include "agent/advertising_proxy.hpp" + +#if OTBR_ENABLE_SRP_ADVERTISING_PROXY + +#if !OTBR_ENABLE_MDNS_AVAHI && !OTBR_ENABLE_MDNS_MDNSSD && !OTBR_ENABLE_MDNS_MOJO +#error "The Advertising Proxy requires OTBR_ENABLE_MDNS_AVAHI, OTBR_ENABLE_MDNS_MDNSSD or OTBR_ENABLE_MDNS_MOJO" +#endif + +#include + +#include + +#include "common/code_utils.hpp" +#include "common/dns_utils.hpp" +#include "common/logging.hpp" + +namespace otbr { + +static otError OtbrErrorToOtError(otbrError aError) +{ + otError error; + + switch (aError) + { + case OTBR_ERROR_NONE: + error = OT_ERROR_NONE; + break; + + case OTBR_ERROR_NOT_FOUND: + error = OT_ERROR_NOT_FOUND; + break; + + case OTBR_ERROR_PARSE: + error = OT_ERROR_PARSE; + break; + + case OTBR_ERROR_NOT_IMPLEMENTED: + error = OT_ERROR_NOT_IMPLEMENTED; + break; + + case OTBR_ERROR_INVALID_ARGS: + error = OT_ERROR_INVALID_ARGS; + break; + + case OTBR_ERROR_DUPLICATED: + error = OT_ERROR_DUPLICATED; + break; + + default: + error = OT_ERROR_FAILED; + break; + } + + return error; +} + +AdvertisingProxy::AdvertisingProxy(Ncp::ControllerOpenThread &aNcp, Mdns::Publisher &aPublisher) + : mNcp(aNcp) + , mPublisher(aPublisher) +{ +} + +otbrError AdvertisingProxy::Start(void) +{ + otSrpServerSetServiceUpdateHandler(GetInstance(), AdvertisingHandler, this); + + mPublisher.SetPublishServiceHandler(PublishServiceHandler, this); + mPublisher.SetPublishHostHandler(PublishHostHandler, this); + + otbrLogInfo("Started"); + + return OTBR_ERROR_NONE; +} + +void AdvertisingProxy::Stop() +{ + mPublisher.SetPublishServiceHandler(nullptr, nullptr); + mPublisher.SetPublishHostHandler(nullptr, nullptr); + + // Outstanding updates will fail on the SRP server because of timeout. + // TODO: handle this case gracefully. + + // Stop receiving SRP server events. + if (GetInstance() != nullptr) + { + otSrpServerSetServiceUpdateHandler(GetInstance(), nullptr, nullptr); + } + + otbrLogInfo("Stopped"); +} + +void AdvertisingProxy::AdvertisingHandler(otSrpServerServiceUpdateId aId, + const otSrpServerHost * aHost, + uint32_t aTimeout, + void * aContext) +{ + static_cast(aContext)->AdvertisingHandler(aId, aHost, aTimeout); +} + +void AdvertisingProxy::AdvertisingHandler(otSrpServerServiceUpdateId aId, + const otSrpServerHost * aHost, + uint32_t aTimeout) +{ + // TODO: There are corner cases that the `aHost` is freed by SRP server because + // of timeout, but this `aHost` is passed back to SRP server and matches a newly + // allocated otSrpServerHost object which has the same pointer value as this + // `aHost`. This results in mismatching of the outstanding SRP updates. Solutions + // are cleaning up the outstanding update entries before timing out or using + // incremental ID to match oustanding SRP updates. + OTBR_UNUSED_VARIABLE(aTimeout); + + otbrError error = OTBR_ERROR_NONE; + const char * fullHostName; + std::string hostName; + std::string hostDomain; + const otIp6Address * hostAddress; + uint8_t hostAddressNum; + bool hostDeleted; + const otSrpServerService *service; + OutstandingUpdate * update; + + mOutstandingUpdates.resize(mOutstandingUpdates.size() + 1); + update = &mOutstandingUpdates.back(); + + fullHostName = otSrpServerHostGetFullName(aHost); + + otbrLogInfo("Advertise SRP service updates: host=%s", fullHostName); + + SuccessOrExit(error = SplitFullHostName(fullHostName, hostName, hostDomain)); + hostAddress = otSrpServerHostGetAddresses(aHost, &hostAddressNum); + hostDeleted = otSrpServerHostIsDeleted(aHost); + + update->mId = aId; + update->mCallbackCount += !hostDeleted; + update->mHostName = hostName; + + service = nullptr; + while ((service = otSrpServerHostGetNextService(aHost, service)) != nullptr) + { + update->mCallbackCount += !hostDeleted && !otSrpServerServiceIsDeleted(service); + } + + if (!hostDeleted) + { + // TODO: select a preferred address or advertise all addresses from SRP client. + otbrLogInfo("Publish SRP host: %s", fullHostName); + SuccessOrExit(error = + mPublisher.PublishHost(hostName.c_str(), hostAddress[0].mFields.m8, sizeof(hostAddress[0]))); + } + else + { + otbrLogInfo("Unpublish SRP host: %s", fullHostName); + SuccessOrExit(error = mPublisher.UnpublishHost(hostName.c_str())); + } + + service = nullptr; + while ((service = otSrpServerHostGetNextService(aHost, service)) != nullptr) + { + const char *fullServiceName = otSrpServerServiceGetFullName(service); + std::string serviceName; + std::string serviceType; + std::string serviceDomain; + + SuccessOrExit(error = SplitFullServiceInstanceName(fullServiceName, serviceName, serviceType, serviceDomain)); + + update->mServiceNames.emplace_back(serviceName, serviceType); + + if (!hostDeleted && !otSrpServerServiceIsDeleted(service)) + { + Mdns::Publisher::TxtList txtList = MakeTxtList(service); + + otbrLogInfo("Publish SRP service: %s", fullServiceName); + SuccessOrExit(error = mPublisher.PublishService(hostName.c_str(), otSrpServerServiceGetPort(service), + serviceName.c_str(), serviceType.c_str(), txtList)); + } + else + { + otbrLogInfo("Unpublish SRP service: %s", fullServiceName); + SuccessOrExit(error = mPublisher.UnpublishService(serviceName.c_str(), serviceType.c_str())); + } + } + +exit: + if (error != OTBR_ERROR_NONE || update->mCallbackCount == 0) + { + if (error != OTBR_ERROR_NONE) + { + otbrLogInfo("Failed to advertise SRP service updates %p", aHost); + } + + mOutstandingUpdates.pop_back(); + otSrpServerHandleServiceUpdateResult(GetInstance(), aId, OtbrErrorToOtError(error)); + } +} + +void AdvertisingProxy::PublishServiceHandler(const char *aName, const char *aType, otbrError aError, void *aContext) +{ + static_cast(aContext)->PublishServiceHandler(aName, aType, aError); +} + +void AdvertisingProxy::PublishServiceHandler(const char *aName, const char *aType, otbrError aError) +{ + otbrError error = OTBR_ERROR_NONE; + + otbrLogInfo("Handle publish service '%s.%s' result: %d", aName, aType, aError); + + // TODO: there may be same names between two SRP updates. + for (auto update = mOutstandingUpdates.begin(); update != mOutstandingUpdates.end(); ++update) + { + for (const auto &nameAndType : update->mServiceNames) + { + if (aName != nameAndType.first || !Mdns::Publisher::IsServiceTypeEqual(aType, nameAndType.second.c_str())) + { + continue; + } + + if (aError != OTBR_ERROR_NONE || update->mCallbackCount == 1) + { + otSrpServerHandleServiceUpdateResult(GetInstance(), update->mId, OtbrErrorToOtError(aError)); + mOutstandingUpdates.erase(update); + } + else + { + --update->mCallbackCount; + } + ExitNow(); + } + } + +exit: + if (error != OTBR_ERROR_NONE) + { + otbrLogWarning("Failed to handle result of service %s", aName); + } +} + +void AdvertisingProxy::PublishHostHandler(const char *aName, otbrError aError, void *aContext) +{ + static_cast(aContext)->PublishHostHandler(aName, aError); +} + +void AdvertisingProxy::PublishHostHandler(const char *aName, otbrError aError) +{ + otbrError error = OTBR_ERROR_NONE; + + otbrLogInfo("Handle publish host '%s' result: %d", aName, aError); + + for (auto update = mOutstandingUpdates.begin(); update != mOutstandingUpdates.end(); ++update) + { + if (aName != update->mHostName) + { + continue; + } + + if (aError != OTBR_ERROR_NONE || update->mCallbackCount == 1) + { + otSrpServerHandleServiceUpdateResult(GetInstance(), update->mId, OtbrErrorToOtError(aError)); + mOutstandingUpdates.erase(update); + } + else + { + --update->mCallbackCount; + } + ExitNow(); + } + +exit: + if (error != OTBR_ERROR_NONE) + { + otbrLogWarning("Failed to handle result of host %s", aName); + } +} + +Mdns::Publisher::TxtList AdvertisingProxy::MakeTxtList(const otSrpServerService *aSrpService) +{ + const uint8_t * txtData; + uint16_t txtDataLength = 0; + otDnsTxtEntryIterator iterator; + otDnsTxtEntry txtEntry; + Mdns::Publisher::TxtList txtList; + + txtData = otSrpServerServiceGetTxtData(aSrpService, &txtDataLength); + + otDnsInitTxtEntryIterator(&iterator, txtData, txtDataLength); + + while (otDnsGetNextTxtEntry(&iterator, &txtEntry) == OT_ERROR_NONE) + { + txtList.emplace_back(txtEntry.mKey, txtEntry.mValue, txtEntry.mValueLength); + } + + return txtList; +} + +} // namespace otbr + +#endif // OTBR_ENABLE_SRP_ADVERTISING_PROXY diff --git a/src/agent/advertising_proxy.hpp b/src/agent/advertising_proxy.hpp new file mode 100644 index 00000000000..dbe3f3906d9 --- /dev/null +++ b/src/agent/advertising_proxy.hpp @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definitions for Advertising Proxy. + */ + +#ifndef OTBR_SRP_ADVERTISING_PROXY_HPP_ +#define OTBR_SRP_ADVERTISING_PROXY_HPP_ + +#if OTBR_ENABLE_SRP_ADVERTISING_PROXY + +#include + +#include +#include + +#include "agent/ncp_openthread.hpp" +#include "mdns/mdns.hpp" + +namespace otbr { + +/** + * This class implements the Advertising Proxy. + * + */ +class AdvertisingProxy +{ +public: + /** + * This constructor initializes the Advertising Proxy object. + * + * @param[in] aNcp A reference to the NCP controller. + * @param[in] aPublisher A reference to the mDNS publisher. + * + */ + explicit AdvertisingProxy(Ncp::ControllerOpenThread &aNcp, Mdns::Publisher &aPublisher); + + /** + * This method starts the Advertising Proxy. + * + * @retval OTBR_ERROR_NONE Successfully started the Advertising Proxy. + * @retval ... Failed to start the Advertising Proxy. + * + */ + otbrError Start(void); + + /** + * This method stops the Advertising Proxy. + * + */ + void Stop(); + +private: + struct OutstandingUpdate + { + typedef std::vector> ServiceNameList; + + otSrpServerServiceUpdateId mId; // The ID of the SRP service update transaction. + std::string mHostName; // The host name. + ServiceNameList mServiceNames; // The list of service instance and name pair. + uint32_t mCallbackCount = 0; // The number of callbacks which we are waiting for. + }; + + static void AdvertisingHandler(otSrpServerServiceUpdateId aId, + const otSrpServerHost * aHost, + uint32_t aTimeout, + void * aContext); + void AdvertisingHandler(otSrpServerServiceUpdateId aId, const otSrpServerHost *aHost, uint32_t aTimeout); + + static Mdns::Publisher::TxtList MakeTxtList(const otSrpServerService *aSrpService); + + static void PublishServiceHandler(const char *aName, const char *aType, otbrError aError, void *aContext); + void PublishServiceHandler(const char *aName, const char *aType, otbrError aError); + static void PublishHostHandler(const char *aName, otbrError aError, void *aContext); + void PublishHostHandler(const char *aName, otbrError aError); + + otInstance *GetInstance(void) { return mNcp.GetInstance(); } + + // A reference to the NCP controller, has no ownership. + Ncp::ControllerOpenThread &mNcp; + + // A reference to the mDNS publisher, has no ownership. + Mdns::Publisher &mPublisher; + + // A vector that tracks outstanding updates. + std::vector mOutstandingUpdates; +}; + +} // namespace otbr + +#endif // OTBR_ENABLE_SRP_ADVERTISING_PROXY + +#endif // OTBR_SRP_ADVERTISING_PROXY_HPP_ diff --git a/src/agent/agent_instance.cpp b/src/agent/agent_instance.cpp index 1fd7962e32c..72fb60ea722 100644 --- a/src/agent/agent_instance.cpp +++ b/src/agent/agent_instance.cpp @@ -31,6 +31,8 @@ * This file includes implementation for Thread border router agent instance. */ +#define OTBR_LOG_TAG "AGENT" + #include "agent/agent_instance.hpp" #include @@ -40,7 +42,7 @@ namespace otbr { -AgentInstance::AgentInstance(Ncp::Controller *aNcp) +AgentInstance::AgentInstance(Ncp::ControllerOpenThread &aNcp) : mNcp(aNcp) , mBorderAgent(aNcp) { @@ -50,31 +52,25 @@ otbrError AgentInstance::Init(void) { otbrError error = OTBR_ERROR_NONE; - SuccessOrExit(error = mNcp->Init()); + SuccessOrExit(error = mNcp.Init()); mBorderAgent.Init(); exit: - otbrLogResult("Initialize OpenThread Border Router Agent", error); + otbrLogResult(error, "Initialize OpenThread Border Router Agent"); return error; } -void AgentInstance::UpdateFdSet(otSysMainloopContext &aMainloop) -{ - mNcp->UpdateFdSet(aMainloop); - mBorderAgent.UpdateFdSet(aMainloop.mReadFdSet, aMainloop.mWriteFdSet, aMainloop.mErrorFdSet, aMainloop.mMaxFd, - aMainloop.mTimeout); -} - -void AgentInstance::Process(const otSysMainloopContext &aMainloop) +void AgentInstance::Update(MainloopContext &aMainloop) { - mNcp->Process(aMainloop); - mBorderAgent.Process(aMainloop.mReadFdSet, aMainloop.mWriteFdSet, aMainloop.mErrorFdSet); + mNcp.Update(aMainloop); + mBorderAgent.Update(aMainloop); } -AgentInstance::~AgentInstance(void) +void AgentInstance::Process(const MainloopContext &aMainloop) { - Ncp::Controller::Destroy(mNcp); + mNcp.Process(aMainloop); + mBorderAgent.Process(aMainloop); } } // namespace otbr diff --git a/src/agent/agent_instance.hpp b/src/agent/agent_instance.hpp index 27853de8a26..92d13dd7537 100644 --- a/src/agent/agent_instance.hpp +++ b/src/agent/agent_instance.hpp @@ -42,7 +42,9 @@ #include #include "agent/border_agent.hpp" -#include "agent/ncp.hpp" +#include "agent/instance_params.hpp" +#include "agent/ncp_openthread.hpp" +#include "common/mainloop.hpp" namespace otbr { @@ -50,18 +52,16 @@ namespace otbr { * This class implements an instance to host services used by border router. * */ -class AgentInstance +class AgentInstance : public MainloopProcessor { public: /** * The constructor to initialize the Thread border router agent instance. * - * @param[in] aNcp A pointer to the NCP controller. + * @param[in] aNcp A reference to the NCP controller. * */ - AgentInstance(Ncp::Controller *aNcp); - - ~AgentInstance(void); + AgentInstance(Ncp::ControllerOpenThread &aNcp); /** * This method initialize the agent. @@ -73,32 +73,32 @@ class AgentInstance otbrError Init(void); /** - * This method updates the file descriptor sets and timeout for mainloop. + * This method updates the mainloop context. * - * @param[inout] aMainloop A reference to OpenThread mainloop context. + * @param[inout] aMainloop A reference to the mainloop to be updated. * */ - void UpdateFdSet(otSysMainloopContext &aMainloop); + void Update(MainloopContext &aMainloop) override; /** - * This method performs processing. + * This method processes mainloop events. * - * @param[in] aMainloop A reference to OpenThread mainloop context. + * @param[in] aMainloop A reference to the mainloop context. * */ - void Process(const otSysMainloopContext &aMainloop); + void Process(const MainloopContext &aMainloop) override; /** - * This method return mNcp pointer. + * This method returns the NCP controller. * - * @retval the pointer of mNcp. + * @retval the pointer of the NCP controller. * */ - Ncp::Controller &GetNcp(void) { return *mNcp; } + otbr::Ncp::ControllerOpenThread &GetNcp(void) { return mNcp; } private: - Ncp::Controller *mNcp; - BorderAgent mBorderAgent; + otbr::Ncp::ControllerOpenThread &mNcp; + BorderAgent mBorderAgent; }; } // namespace otbr diff --git a/src/agent/border_agent.cpp b/src/agent/border_agent.cpp index 9df1fb5ffd1..0908f58272a 100644 --- a/src/agent/border_agent.cpp +++ b/src/agent/border_agent.cpp @@ -31,6 +31,8 @@ * The file implements the Thread border agent. */ +#define OTBR_LOG_TAG "AGENT" + #include "agent/border_agent.hpp" #include @@ -42,11 +44,16 @@ #include #include +#include +#include #include -#include "agent/border_agent.hpp" -#include "agent/ncp.hpp" +#include "agent/ncp_openthread.hpp" #include "agent/uris.hpp" +#if OTBR_ENABLE_BACKBONE_ROUTER +#include "backbone_router/backbone_agent.hpp" +#endif +#include "common/byteswap.hpp" #include "common/code_utils.hpp" #include "common/logging.hpp" #include "common/tlv.hpp" @@ -56,10 +63,7 @@ namespace otbr { -static const uint16_t kThreadVersion11 = 2; ///< Thread Version 1.1 -static const uint16_t kThreadVersion12 = 3; ///< Thread Version 1.2 - -static const char kBorderAgentServiceType[] = "_meshcop._udp."; ///< Border agent service type of mDNS +static const char kBorderAgentServiceType[] = "_meshcop._udp"; ///< Border agent service type of mDNS /** * Locators @@ -71,75 +75,102 @@ enum kInvalidLocator = 0xffff, ///< invalid locator. }; -/** - * UDP ports - * - */ -enum +uint32_t BorderAgent::StateBitmap::ToUint32(void) const { - kBorderAgentUdpPort = 49191, ///< Thread commissioning port. -}; + uint32_t bitmap = 0; + + bitmap |= mConnectionMode << 0; + bitmap |= mThreadIfStatus << 3; + bitmap |= mAvailability << 5; + bitmap |= mBbrIsActive << 7; + bitmap |= mBbrIsPrimary << 8; + + return bitmap; +} -BorderAgent::BorderAgent(Ncp::Controller *aNcp) +BorderAgent::BorderAgent(otbr::Ncp::ControllerOpenThread &aNcp) + : mNcp(aNcp) #if OTBR_ENABLE_MDNS_AVAHI || OTBR_ENABLE_MDNS_MDNSSD || OTBR_ENABLE_MDNS_MOJO - : mPublisher(Mdns::Publisher::Create(AF_UNSPEC, nullptr, nullptr, HandleMdnsState, this)) + , mPublisher(Mdns::Publisher::Create(AF_UNSPEC, /* aDomain */ nullptr, HandleMdnsState, this)) +#if OTBR_ENABLE_SRP_ADVERTISING_PROXY + , mAdvertisingProxy(aNcp, *mPublisher) +#endif #else - : mPublisher(nullptr) + , mPublisher(nullptr) +#endif +#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY + , mDiscoveryProxy(aNcp, *mPublisher) +#endif +#if OTBR_ENABLE_BACKBONE_ROUTER + , mBackboneAgent(aNcp) #endif - , mNcp(aNcp) - , mThreadStarted(false) { } void BorderAgent::Init(void) { - memset(mNetworkName, 0, sizeof(mNetworkName)); - memset(mExtPanId, 0, sizeof(mExtPanId)); - mExtPanIdInitialized = false; - mThreadVersion = 0; + mNcp.AddThreadStateChangedCallback([this](otChangedFlags aFlags) { HandleThreadStateChanged(aFlags); }); -#if OTBR_ENABLE_MDNS_AVAHI || OTBR_ENABLE_MDNS_MDNSSD || OTBR_ENABLE_MDNS_MOJO - mNcp->On(Ncp::kEventExtPanId, HandleExtPanId, this); - mNcp->On(Ncp::kEventNetworkName, HandleNetworkName, this); - mNcp->On(Ncp::kEventThreadVersion, HandleThreadVersion, this); +#if OTBR_ENABLE_BACKBONE_ROUTER + mBackboneAgent.Init(); #endif - mNcp->On(Ncp::kEventThreadState, HandleThreadState, this); - mNcp->On(Ncp::kEventPSKc, HandlePSKc, this); - otbrLogResult("Check if Thread is up", mNcp->RequestEvent(Ncp::kEventThreadState)); - otbrLogResult("Check if PSKc is initialized", mNcp->RequestEvent(Ncp::kEventPSKc)); + if (IsThreadStarted()) + { + Start(); + } + else + { + Stop(); + } } otbrError BorderAgent::Start(void) { otbrError error = OTBR_ERROR_NONE; - VerifyOrExit(mThreadStarted && mPSKcInitialized, errno = EAGAIN, error = OTBR_ERROR_ERRNO); + VerifyOrExit(IsThreadStarted(), errno = EAGAIN, error = OTBR_ERROR_ERRNO); // In case we didn't receive Thread down event. Stop(); #if OTBR_ENABLE_MDNS_AVAHI || OTBR_ENABLE_MDNS_MDNSSD || OTBR_ENABLE_MDNS_MOJO - SuccessOrExit(error = mNcp->RequestEvent(Ncp::kEventNetworkName)); - SuccessOrExit(error = mNcp->RequestEvent(Ncp::kEventExtPanId)); +#if OTBR_ENABLE_SRP_ADVERTISING_PROXY + mAdvertisingProxy.Start(); +#endif + UpdateMeshCopService(); - SuccessOrExit(error = mNcp->RequestEvent(Ncp::kEventThreadVersion)); - StartPublishService(); -#endif // OTBR_ENABLE_MDNS_AVAHI || OTBR_ENABLE_MDNS_MDNSSD || OTBR_ENABLE_MDNS_MOJO +#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY + mDiscoveryProxy.Start(); +#endif - // Suppress unused warning of label exit - ExitNow(); +#endif // OTBR_ENABLE_MDNS_AVAHI || OTBR_ENABLE_MDNS_MDNSSD || OTBR_ENABLE_MDNS_MOJO exit: - otbrLogResult("Start Thread Border Agent", error); + otbrLogResult(error, "Start Thread Border Agent"); return error; } void BorderAgent::Stop(void) { + otbrLogInfo("Stop Thread Border Agent"); + #if OTBR_ENABLE_MDNS_AVAHI || OTBR_ENABLE_MDNS_MDNSSD || OTBR_ENABLE_MDNS_MOJO - StopPublishService(); + mPublisher->Stop(); +#if OTBR_ENABLE_SRP_ADVERTISING_PROXY + mAdvertisingProxy.Stop(); +#endif + +#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY + mDiscoveryProxy.Stop(); #endif + +#endif +} + +void BorderAgent::HandleMdnsState(void *aContext, Mdns::Publisher::State aState) +{ + static_cast(aContext)->HandleMdnsState(aState); } BorderAgent::~BorderAgent(void) @@ -153,232 +184,214 @@ BorderAgent::~BorderAgent(void) } } -void BorderAgent::HandleMdnsState(Mdns::State aState) +void BorderAgent::HandleMdnsState(Mdns::Publisher::State aState) { switch (aState) { - case Mdns::kStateReady: - PublishService(); + case Mdns::Publisher::State::kReady: + UpdateMeshCopService(); break; default: - otbrLog(OTBR_LOG_WARNING, "MDNS service not available!"); + otbrLogWarning("MDNS service not available!"); break; } } -void BorderAgent::UpdateFdSet(fd_set & aReadFdSet, - fd_set & aWriteFdSet, - fd_set & aErrorFdSet, - int & aMaxFd, - timeval &aTimeout) +void BorderAgent::Update(MainloopContext &aMainloop) { +#if OTBR_ENABLE_BACKBONE_ROUTER + mBackboneAgent.Update(aMainloop); +#endif + if (mPublisher != nullptr) { - mPublisher->UpdateFdSet(aReadFdSet, aWriteFdSet, aErrorFdSet, aMaxFd, aTimeout); + mPublisher->Update(aMainloop); } } -void BorderAgent::Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const fd_set &aErrorFdSet) +void BorderAgent::Process(const MainloopContext &aMainloop) { +#if OTBR_ENABLE_BACKBONE_ROUTER + mBackboneAgent.Process(aMainloop); +#endif if (mPublisher != nullptr) { - mPublisher->Process(aReadFdSet, aWriteFdSet, aErrorFdSet); + mPublisher->Process(aMainloop); } } -static const char *ThreadVersionToString(uint16_t aThreadVersion) +void BorderAgent::PublishMeshCopService(void) { - switch (aThreadVersion) + StateBitmap state; + uint32_t stateUint32; + otInstance * instance = mNcp.GetInstance(); + const otExtendedPanId * extPanId = otThreadGetExtendedPanId(instance); + const otExtAddress * extAddr = otLinkGetExtendedAddress(instance); + const char * networkName = otThreadGetNetworkName(instance); + Mdns::Publisher::TxtList txtList{{"rv", "1"}}; + + otbrLogInfo("Publish meshcop service %s.%s.local.", networkName, kBorderAgentServiceType); + + txtList.emplace_back("nn", networkName); + txtList.emplace_back("xp", extPanId->m8, sizeof(extPanId->m8)); + txtList.emplace_back("tv", mNcp.GetThreadVersion()); + + // "dd" represents for device discriminator which + // should always be the IEEE 802.15.4 extended address. + txtList.emplace_back("dd", extAddr->m8, sizeof(extAddr->m8)); + + state.mConnectionMode = kConnectionModePskc; + state.mAvailability = kAvailabilityHigh; +#if OTBR_ENABLE_BACKBONE_ROUTER + state.mBbrIsActive = otBackboneRouterGetState(instance) != OT_BACKBONE_ROUTER_STATE_DISABLED; + state.mBbrIsPrimary = otBackboneRouterGetState(instance) == OT_BACKBONE_ROUTER_STATE_PRIMARY; +#endif + switch (otThreadGetDeviceRole(instance)) { - case kThreadVersion11: - return "1.1.1"; - case kThreadVersion12: - return "1.2.0"; + case OT_DEVICE_ROLE_DISABLED: + state.mThreadIfStatus = kThreadIfStatusNotInitialized; + break; + case OT_DEVICE_ROLE_DETACHED: + state.mThreadIfStatus = kThreadIfStatusInitialized; + break; default: - otbrLog(OTBR_LOG_ERR, "unexpected thread version %hu", aThreadVersion); - abort(); + state.mThreadIfStatus = kThreadIfStatusActive; } -} - -void BorderAgent::PublishService(void) -{ - const char *versionString = ThreadVersionToString(mThreadVersion); - - assert(mNetworkName[0] != '\0'); - assert(mExtPanIdInitialized); - - assert(mThreadVersion != 0); - // clang-format off - mPublisher->PublishService(kBorderAgentUdpPort, mNetworkName, kBorderAgentServiceType, - "nn", mNetworkName, strlen(mNetworkName), - "xp", &mExtPanId, sizeof(mExtPanId), - "tv", versionString, strlen(versionString), - nullptr); - // clang-format on -} -void BorderAgent::StartPublishService(void) -{ - VerifyOrExit(mNetworkName[0] != '\0'); - VerifyOrExit(mExtPanIdInitialized); - VerifyOrExit(mThreadVersion != 0); + stateUint32 = htobe32(state.ToUint32()); + txtList.emplace_back("sb", reinterpret_cast(&stateUint32), sizeof(stateUint32)); - if (mPublisher->IsStarted()) - { - PublishService(); - } - else + if (state.mThreadIfStatus == kThreadIfStatusActive) { - mPublisher->Start(); - } - -exit: - otbrLog(OTBR_LOG_INFO, "Start publishing service"); -} + otError error; + otOperationalDataset activeDataset; + uint64_t activeTimestamp; + uint32_t partitionId; -void BorderAgent::StopPublishService(void) -{ - VerifyOrExit(mPublisher != nullptr); + if ((error = otDatasetGetActive(instance, &activeDataset)) != OT_ERROR_NONE) + { + otbrLogWarning("Failed to get active dataset: %s", otThreadErrorToString(error)); + } + else + { + activeTimestamp = htobe64(activeDataset.mActiveTimestamp); + txtList.emplace_back("at", reinterpret_cast(&activeTimestamp), sizeof(activeTimestamp)); + } - if (mPublisher->IsStarted()) - { - mPublisher->Stop(); + partitionId = otThreadGetPartitionId(instance); + txtList.emplace_back("pt", reinterpret_cast(&partitionId), sizeof(partitionId)); } -exit: - otbrLog(OTBR_LOG_INFO, "Stop publishing service"); -} - -void BorderAgent::SetNetworkName(const char *aNetworkName) -{ - strcpy_safe(mNetworkName, sizeof(mNetworkName), aNetworkName); - -#if OTBR_ENABLE_MDNS_AVAHI || OTBR_ENABLE_MDNS_MDNSSD || OTBR_ENABLE_MDNS_MOJO - if (mThreadStarted) +#if OTBR_ENABLE_BACKBONE_ROUTER + if (state.mBbrIsActive) { - // Restart publisher to publish new service name. - mPublisher->Stop(); - StartPublishService(); - } -#endif -} + otBackboneRouterConfig bbrConfig; + uint16_t bbrPort = htobe16(BackboneRouter::BackboneAgent::kBackboneUdpPort); -void BorderAgent::SetExtPanId(const uint8_t *aExtPanId) -{ - memcpy(mExtPanId, aExtPanId, sizeof(mExtPanId)); - mExtPanIdInitialized = true; -#if OTBR_ENABLE_MDNS_AVAHI || OTBR_ENABLE_MDNS_MDNSSD || OTBR_ENABLE_MDNS_MOJO - if (mThreadStarted) - { - StartPublishService(); + otBackboneRouterGetConfig(instance, &bbrConfig); + txtList.emplace_back("sq", &bbrConfig.mSequenceNumber, sizeof(bbrConfig.mSequenceNumber)); + txtList.emplace_back("bb", reinterpret_cast(&bbrPort), sizeof(bbrPort)); } -#endif -} -void BorderAgent::SetThreadVersion(uint16_t aThreadVersion) -{ - mThreadVersion = aThreadVersion; -#if OTBR_ENABLE_MDNS_AVAHI || OTBR_ENABLE_MDNS_MDNSSD || OTBR_ENABLE_MDNS_MOJO - if (mThreadStarted) - { - StartPublishService(); - } + txtList.emplace_back("dn", otThreadGetDomainName(instance)); #endif + + mPublisher->PublishService(/* aHostName */ nullptr, otBorderAgentGetUdpPort(instance), networkName, + kBorderAgentServiceType, txtList); } -void BorderAgent::HandlePSKc(void *aContext, int aEvent, va_list aArguments) +void BorderAgent::UnpublishMeshCopService(void) { - OT_UNUSED_VARIABLE(aEvent); + assert(IsThreadStarted()); + VerifyOrExit(!mNetworkName.empty()); - assert(aEvent == Ncp::kEventPSKc); + otbrLogInfo("Unpublish meshcop service %s.%s.local.", mNetworkName.c_str(), kBorderAgentServiceType); - static_cast(aContext)->HandlePSKc(va_arg(aArguments, const uint8_t *)); + mPublisher->UnpublishService(mNetworkName.c_str(), kBorderAgentServiceType); + +exit: + return; } -void BorderAgent::HandlePSKc(const uint8_t *aPSKc) +void BorderAgent::UpdateMeshCopService(void) { - mPSKcInitialized = false; + assert(IsThreadStarted()); - for (size_t i = 0; i < kSizePSKc; ++i) - { - if (aPSKc[i] != 0) - { - mPSKcInitialized = true; - break; - } - } + const char *networkName = otThreadGetNetworkName(mNcp.GetInstance()); - if (mPSKcInitialized) - { - Start(); - } - else + VerifyOrExit(mPublisher->IsStarted(), mPublisher->Start()); + VerifyOrExit(IsPskcInitialized(), UnpublishMeshCopService()); + + // In case the Thread network name changes, we need to unpublish + // current meshcop service. + // + // TODO: don't use Thread network name as service instance name. + // The network name is a TXT entry of the `_meshcop._udp` service + // and we should only update the mDNS service entry when network name + // changes but not republish a new service instance. + if (mNetworkName != networkName) { - Stop(); + UnpublishMeshCopService(); } - otbrLog(OTBR_LOG_INFO, "PSKc is %s", (mPSKcInitialized ? "initialized" : "not initialized")); + PublishMeshCopService(); + mNetworkName = networkName; + +exit: + return; } -void BorderAgent::HandleThreadState(bool aStarted) +void BorderAgent::HandleThreadStateChanged(otChangedFlags aFlags) { - VerifyOrExit(mThreadStarted != aStarted); - - mThreadStarted = aStarted; + VerifyOrExit(mPublisher != nullptr); + VerifyOrExit(aFlags & (OT_CHANGED_THREAD_ROLE | OT_CHANGED_THREAD_EXT_PANID | OT_CHANGED_THREAD_NETWORK_NAME | + OT_CHANGED_ACTIVE_DATASET | OT_CHANGED_THREAD_PARTITION_ID | + OT_CHANGED_THREAD_BACKBONE_ROUTER_STATE | OT_CHANGED_PSKC)); - if (aStarted) + if (aFlags & OT_CHANGED_THREAD_ROLE) { - SuccessOrExit(mNcp->RequestEvent(Ncp::kEventPSKc)); - Start(); + otbrLogInfo("Thread is %s", (IsThreadStarted() ? "up" : "down")); + + if (IsThreadStarted()) + { + Start(); + } + else + { + Stop(); + } } - else + else if (IsThreadStarted()) { - Stop(); + UpdateMeshCopService(); } exit: - otbrLog(OTBR_LOG_INFO, "Thread is %s", (aStarted ? "up" : "down")); -} - -void BorderAgent::HandleThreadState(void *aContext, int aEvent, va_list aArguments) -{ - OT_UNUSED_VARIABLE(aEvent); - - assert(aEvent == Ncp::kEventThreadState); - - int started = va_arg(aArguments, int); - static_cast(aContext)->HandleThreadState(started); + return; } -void BorderAgent::HandleNetworkName(void *aContext, int aEvent, va_list aArguments) +bool BorderAgent::IsThreadStarted(void) const { - OT_UNUSED_VARIABLE(aEvent); + otDeviceRole role = otThreadGetDeviceRole(mNcp.GetInstance()); - assert(aEvent == Ncp::kEventNetworkName); - - const char *networkName = va_arg(aArguments, const char *); - static_cast(aContext)->SetNetworkName(networkName); + return role == OT_DEVICE_ROLE_CHILD || role == OT_DEVICE_ROLE_ROUTER || role == OT_DEVICE_ROLE_LEADER; } -void BorderAgent::HandleExtPanId(void *aContext, int aEvent, va_list aArguments) +bool BorderAgent::IsPskcInitialized(void) const { - OT_UNUSED_VARIABLE(aEvent); - - assert(aEvent == Ncp::kEventExtPanId); - - const uint8_t *xpanid = va_arg(aArguments, const uint8_t *); - static_cast(aContext)->SetExtPanId(xpanid); -} + bool initialized = false; + const otPskc *pskc = otThreadGetPskc(mNcp.GetInstance()); -void BorderAgent::HandleThreadVersion(void *aContext, int aEvent, va_list aArguments) -{ - OT_UNUSED_VARIABLE(aEvent); - - assert(aEvent == Ncp::kEventThreadVersion); + for (uint8_t byte : pskc->m8) + { + if (byte != 0x00) + { + initialized = true; + break; + } + } - // `uint16_t` has been promoted to `int`. - uint16_t threadVersion = static_cast(va_arg(aArguments, int)); - static_cast(aContext)->SetThreadVersion(threadVersion); + return initialized; } } // namespace otbr diff --git a/src/agent/border_agent.hpp b/src/agent/border_agent.hpp index ae58a4fde00..a14a0fb5736 100644 --- a/src/agent/border_agent.hpp +++ b/src/agent/border_agent.hpp @@ -34,11 +34,21 @@ #ifndef OTBR_AGENT_BORDER_AGENT_HPP_ #define OTBR_AGENT_BORDER_AGENT_HPP_ +#include + #include -#include "agent/ncp.hpp" +#include "agent/advertising_proxy.hpp" +#include "agent/discovery_proxy.hpp" +#include "agent/instance_params.hpp" +#include "agent/ncp_openthread.hpp" +#include "common/mainloop.hpp" #include "mdns/mdns.hpp" +#if OTBR_ENABLE_BACKBONE_ROUTER +#include "backbone_router/backbone_agent.hpp" +#endif + namespace otbr { /** @@ -54,18 +64,18 @@ namespace otbr { * This class implements Thread border agent functionality. * */ -class BorderAgent +class BorderAgent : public MainloopProcessor { public: /** * The constructor to initialize the Thread border agent. * - * @param[in] aNcp A pointer to the NCP controller. + * @param[in] aNcp A reference to the NCP controller. * */ - BorderAgent(Ncp::Controller *aNcp); + BorderAgent(otbr::Ncp::ControllerOpenThread &aNcp); - ~BorderAgent(void); + ~BorderAgent(void) override; /** * This method initialize border agent service. @@ -74,73 +84,90 @@ class BorderAgent void Init(void); /** - * This method updates the fd_set and timeout for mainloop. + * This method updates the mainloop context. * - * @param[inout] aReadFdSet A reference to read file descriptors. - * @param[inout] aWriteFdSet A reference to write file descriptors. - * @param[inout] aErrorFdSet A reference to error file descriptors. - * @param[inout] aMaxFd A reference to the max file descriptor. - * @param[inout] aTimeout A reference to timeout. + * @param[inout] aMainloop A reference to the mainloop to be updated. * */ - void UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, fd_set &aErrorFdSet, int &aMaxFd, timeval &aTimeout); + void Update(MainloopContext &aMainloop) override; /** - * This method performs border agent processing. + * This method processes mainloop events. * - * @param[in] aReadFdSet A reference to read file descriptors. - * @param[in] aWriteFdSet A reference to write file descriptors. - * @param[in] aErrorFdSet A reference to error file descriptors. + * @param[in] aMainloop A reference to the mainloop context. * */ - void Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const fd_set &aErrorFdSet); + void Process(const MainloopContext &aMainloop) override; private: - /** - * This method starts border agent service. - * - * @retval OTBR_ERROR_NONE Successfully started border agent. - * @retval OTBR_ERROR_ERRNO Failed to start border agent. - * - */ - otbrError Start(void); + enum : uint8_t + { + kConnectionModeDisabled = 0, + kConnectionModePskc = 1, + kConnectionModePskd = 2, + kConnectionModeVendor = 3, + kConnectionModeX509 = 4, + }; + + enum : uint8_t + { + kThreadIfStatusNotInitialized = 0, + kThreadIfStatusInitialized = 1, + kThreadIfStatusActive = 2, + }; - /** - * This method stops border agent service. - * - */ - void Stop(void); + enum : uint8_t + { + kAvailabilityInfrequent = 0, + kAvailabilityHigh = 1, + }; - static void HandleMdnsState(void *aContext, Mdns::State aState) + struct StateBitmap { - static_cast(aContext)->HandleMdnsState(aState); - } - void HandleMdnsState(Mdns::State aState); - void PublishService(void); - void StartPublishService(void); - void StopPublishService(void); - - void SetNetworkName(const char *aNetworkName); - void SetExtPanId(const uint8_t *aExtPanId); - void SetThreadVersion(uint16_t aThreadVersion); - void HandleThreadState(bool aStarted); - void HandlePSKc(const uint8_t *aPSKc); - - static void HandlePSKc(void *aContext, int aEvent, va_list aArguments); - static void HandleThreadState(void *aContext, int aEvent, va_list aArguments); - static void HandleNetworkName(void *aContext, int aEvent, va_list aArguments); - static void HandleExtPanId(void *aContext, int aEvent, va_list aArguments); - static void HandleThreadVersion(void *aContext, int aEvent, va_list aArguments); - - Mdns::Publisher *mPublisher; - Ncp::Controller *mNcp; - - uint8_t mExtPanId[kSizeExtPanId]; - bool mExtPanIdInitialized; - uint16_t mThreadVersion; - char mNetworkName[kSizeNetworkName + 1]; - bool mThreadStarted; - bool mPSKcInitialized; + uint32_t mConnectionMode : 3; + uint32_t mThreadIfStatus : 2; + uint32_t mAvailability : 2; + uint32_t mBbrIsActive : 1; + uint32_t mBbrIsPrimary : 1; + + StateBitmap(void) + : mConnectionMode(0) + , mThreadIfStatus(0) + , mAvailability(0) + , mBbrIsActive(0) + , mBbrIsPrimary(0) + { + } + + uint32_t ToUint32(void) const; + }; + + otbrError Start(void); + void Stop(void); + static void HandleMdnsState(void *aContext, Mdns::Publisher::State aState); + void HandleMdnsState(Mdns::Publisher::State aState); + void PublishMeshCopService(void); + void UnpublishMeshCopService(void); + void UpdateMeshCopService(void); + + void HandleThreadStateChanged(otChangedFlags aFlags); + + bool IsThreadStarted(void) const; + bool IsPskcInitialized(void) const; + + otbr::Ncp::ControllerOpenThread &mNcp; + Mdns::Publisher * mPublisher; + std::string mNetworkName; + +#if OTBR_ENABLE_SRP_ADVERTISING_PROXY + AdvertisingProxy mAdvertisingProxy; +#endif +#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY + Dnssd::DiscoveryProxy mDiscoveryProxy; +#endif +#if OTBR_ENABLE_BACKBONE_ROUTER + BackboneRouter::BackboneAgent mBackboneAgent; +#endif }; /** diff --git a/src/agent/discovery_proxy.cpp b/src/agent/discovery_proxy.cpp new file mode 100644 index 00000000000..6928caad660 --- /dev/null +++ b/src/agent/discovery_proxy.cpp @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * The file implements the DNS-SD Discovery Proxy. + */ + +#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY + +#define OTBR_LOG_TAG "DPROXY" + +#include "agent/discovery_proxy.hpp" + +#include +#include + +#include + +#include + +#include "common/code_utils.hpp" +#include "common/dns_utils.hpp" +#include "common/logging.hpp" + +namespace otbr { +namespace Dnssd { + +DiscoveryProxy::DiscoveryProxy(Ncp::ControllerOpenThread &aNcp, Mdns::Publisher &aPublisher) + : mNcp(aNcp) + , mMdnsPublisher(aPublisher) +{ +} + +void DiscoveryProxy::Start(void) +{ + otDnssdQuerySetCallbacks(mNcp.GetInstance(), &DiscoveryProxy::OnDiscoveryProxySubscribe, + &DiscoveryProxy::OnDiscoveryProxyUnsubscribe, this); + + mMdnsPublisher.SetSubscriptionCallbacks( + [this](const std::string &aType, const Mdns::Publisher::DiscoveredInstanceInfo &aInstanceInfo) { + OnServiceDiscovered(aType, aInstanceInfo); + }, + + [this](const std::string &aHostName, const Mdns::Publisher::DiscoveredHostInfo &aHostInfo) { + OnHostDiscovered(aHostName, aHostInfo); + }); + + otbrLogInfo("started"); +} + +void DiscoveryProxy::Stop(void) +{ + otDnssdQuerySetCallbacks(mNcp.GetInstance(), nullptr, nullptr, nullptr); + mMdnsPublisher.SetSubscriptionCallbacks(nullptr, nullptr); + + otbrLogInfo("stopped"); +} + +void DiscoveryProxy::OnDiscoveryProxySubscribe(void *aContext, const char *aFullName) +{ + reinterpret_cast(aContext)->OnDiscoveryProxySubscribe(aFullName); +} + +void DiscoveryProxy::OnDiscoveryProxySubscribe(const char *aFullName) +{ + std::string fullName(aFullName); + otbrError error = OTBR_ERROR_NONE; + MdnsSubscriptionList::iterator it; + DnsNameInfo nameInfo = SplitFullDnsName(fullName); + + otbrLogInfo("subscribe: %s", fullName.c_str()); + + it = std::find_if(mSubscriptions.begin(), mSubscriptions.end(), [&](const MdnsSubscription &aSubscription) { + return aSubscription.Matches(nameInfo.mInstanceName, nameInfo.mServiceName, nameInfo.mHostName, + nameInfo.mDomain); + }); + + VerifyOrExit(it == mSubscriptions.end(), it->mSubscriptionCount++); + + mSubscriptions.emplace_back(nameInfo.mInstanceName, nameInfo.mServiceName, nameInfo.mHostName, nameInfo.mDomain); + + { + MdnsSubscription &subscription = mSubscriptions.back(); + + otbrLogDebug("subscriptions: %sx%d", subscription.ToString().c_str(), subscription.mSubscriptionCount); + + if (GetServiceSubscriptionCount(nameInfo.mInstanceName, nameInfo.mServiceName, nameInfo.mHostName) == 1) + { + if (subscription.mHostName.empty()) + { + mMdnsPublisher.SubscribeService(nameInfo.mServiceName, nameInfo.mInstanceName); + } + else + { + mMdnsPublisher.SubscribeHost(nameInfo.mHostName); + } + } + } + +exit: + if (error != OTBR_ERROR_NONE) + { + otbrLogWarning("failed to subscribe %s: %s", fullName.c_str(), otbrErrorString(error)); + } +} + +void DiscoveryProxy::OnDiscoveryProxyUnsubscribe(void *aContext, const char *aFullName) +{ + reinterpret_cast(aContext)->OnDiscoveryProxyUnsubscribe(aFullName); +} + +void DiscoveryProxy::OnDiscoveryProxyUnsubscribe(const char *aFullName) +{ + std::string fullName(aFullName); + otbrError error = OTBR_ERROR_NONE; + MdnsSubscriptionList::iterator it; + DnsNameInfo nameInfo = SplitFullDnsName(fullName); + + otbrLogInfo("unsubscribe: %s", fullName.c_str()); + + it = std::find_if(mSubscriptions.begin(), mSubscriptions.end(), [&](const MdnsSubscription &aSubscription) { + return aSubscription.Matches(nameInfo.mInstanceName, nameInfo.mServiceName, nameInfo.mHostName, + nameInfo.mDomain); + }); + + VerifyOrExit(it != mSubscriptions.end(), error = OTBR_ERROR_NOT_FOUND); + + { + MdnsSubscription &subscription = *it; + + subscription.mSubscriptionCount--; + assert(subscription.mSubscriptionCount >= 0); + + if (subscription.mSubscriptionCount == 0) + { + mSubscriptions.erase(it); + } + + otbrLogDebug("service subscriptions: %sx%d", it->ToString().c_str(), it->mSubscriptionCount); + + if (GetServiceSubscriptionCount(nameInfo.mInstanceName, nameInfo.mServiceName, nameInfo.mHostName) == 0) + { + if (subscription.mHostName.empty()) + { + mMdnsPublisher.UnsubscribeService(nameInfo.mServiceName, nameInfo.mInstanceName); + } + else + { + mMdnsPublisher.UnsubscribeHost(nameInfo.mHostName); + } + } + } +exit: + if (error != OTBR_ERROR_NONE) + { + otbrLogWarning("failed to unsubscribe %s: %s", fullName.c_str(), otbrErrorString(error)); + } +} + +void DiscoveryProxy::OnServiceDiscovered(const std::string & aType, + const Mdns::Publisher::DiscoveredInstanceInfo &aInstanceInfo) +{ + otDnssdServiceInstanceInfo instanceInfo; + + otbrLogInfo("service discovered: %s, instance %s hostname %s addresses %zu port %d priority %d " + "weight %d", + aType.c_str(), aInstanceInfo.mName.c_str(), aInstanceInfo.mHostName.c_str(), + aInstanceInfo.mAddresses.size(), aInstanceInfo.mPort, aInstanceInfo.mPriority, aInstanceInfo.mWeight); + + CheckServiceNameSanity(aType); + CheckHostnameSanity(aInstanceInfo.mHostName); + + instanceInfo.mAddressNum = aInstanceInfo.mAddresses.size(); + + if (!aInstanceInfo.mAddresses.empty()) + { + instanceInfo.mAddresses = reinterpret_cast(&aInstanceInfo.mAddresses[0]); + } + else + { + instanceInfo.mAddresses = nullptr; + } + + instanceInfo.mPort = aInstanceInfo.mPort; + instanceInfo.mPriority = aInstanceInfo.mPriority; + instanceInfo.mWeight = aInstanceInfo.mWeight; + instanceInfo.mTxtLength = static_cast(aInstanceInfo.mTxtData.size()); + instanceInfo.mTxtData = aInstanceInfo.mTxtData.data(); + instanceInfo.mTtl = CapTtl(aInstanceInfo.mTtl); + + std::for_each(mSubscriptions.begin(), mSubscriptions.end(), [&](const MdnsSubscription &aSubscription) { + if (aSubscription.MatchesServiceInstance(aType, aInstanceInfo.mName)) + { + std::string serviceFullName = aType + "." + aSubscription.mDomain; + std::string hostName = TranslateDomain(aInstanceInfo.mHostName, aSubscription.mDomain); + std::string instanceFullName = aInstanceInfo.mName + "." + serviceFullName; + + instanceInfo.mFullName = instanceFullName.c_str(); + instanceInfo.mHostName = hostName.c_str(); + + otDnssdQueryHandleDiscoveredServiceInstance(mNcp.GetInstance(), serviceFullName.c_str(), &instanceInfo); + } + }); +} + +void DiscoveryProxy::OnHostDiscovered(const std::string & aHostName, + const Mdns::Publisher::DiscoveredHostInfo &aHostInfo) +{ + otDnssdHostInfo hostInfo; + + otbrLogInfo("host discovered: %s hostname %s addresses %zu", aHostName.c_str(), aHostInfo.mHostName.c_str(), + aHostInfo.mAddresses.size()); + + CheckHostnameSanity(aHostInfo.mHostName); + + hostInfo.mAddressNum = aHostInfo.mAddresses.size(); + if (!aHostInfo.mAddresses.empty()) + { + hostInfo.mAddresses = reinterpret_cast(&aHostInfo.mAddresses[0]); + } + else + { + hostInfo.mAddresses = nullptr; + } + + hostInfo.mTtl = CapTtl(aHostInfo.mTtl); + + std::for_each(mSubscriptions.begin(), mSubscriptions.end(), [&](const MdnsSubscription &aSubscription) { + if (aSubscription.MatchesHost(aHostName)) + { + std::string hostFullName = TranslateDomain(aHostInfo.mHostName, aSubscription.mDomain); + + otDnssdQueryHandleDiscoveredHost(mNcp.GetInstance(), hostFullName.c_str(), &hostInfo); + } + }); +} + +std::string DiscoveryProxy::TranslateDomain(const std::string &aName, const std::string &aTargetDomain) +{ + std::string targetName; + std::string hostName, domain; + + VerifyOrExit(OTBR_ERROR_NONE == SplitFullHostName(aName, hostName, domain), targetName = aName); + VerifyOrExit(domain == "local.", targetName = aName); + + targetName = hostName + "." + aTargetDomain; + +exit: + otbrLogDebug("translate domain: %s => %s", aName.c_str(), targetName.c_str()); + return targetName; +} + +int DiscoveryProxy::GetServiceSubscriptionCount(const std::string &aInstanceName, + const std::string &aServiceName, + const std::string &aHostName) +{ + return std::accumulate( + mSubscriptions.begin(), mSubscriptions.end(), 0, [&](int aAccum, const MdnsSubscription &aSubscription) { + return aAccum + ((aSubscription.mInstanceName == aInstanceName && + aSubscription.mServiceName == aServiceName && aSubscription.mHostName == aHostName) + ? aSubscription.mSubscriptionCount + : 0); + }); +} + +void DiscoveryProxy::CheckServiceNameSanity(const std::string &aType) +{ + size_t dotpos; + + OTBR_UNUSED_VARIABLE(aType); + OTBR_UNUSED_VARIABLE(dotpos); + + assert(aType.length() > 0); + assert(aType[aType.length() - 1] != '.'); + dotpos = aType.find_first_of('.'); + assert(dotpos != std::string::npos); + assert(dotpos == aType.find_last_of('.')); +} + +void DiscoveryProxy::CheckHostnameSanity(const std::string &aHostName) +{ + OTBR_UNUSED_VARIABLE(aHostName); + + assert(aHostName.length() > 0); + assert(aHostName[aHostName.length() - 1] == '.'); +} + +uint32_t DiscoveryProxy::CapTtl(uint32_t aTtl) +{ + return std::min(aTtl, static_cast(kServiceTtlCapLimit)); +} + +std::string DiscoveryProxy::MdnsSubscription::ToString(void) const +{ + std::string str; + + if (!mHostName.empty()) + { + str = mHostName + "." + mDomain; + } + else if (!mInstanceName.empty()) + { + str = mInstanceName + "." + mServiceName + "." + mDomain; + } + else + { + str = mServiceName + "." + mDomain; + } + + return str; +} + +bool DiscoveryProxy::MdnsSubscription::Matches(const std::string &aInstanceName, + const std::string &aServiceName, + const std::string &aHostName, + const std::string &aDomain) const +{ + return mInstanceName == aInstanceName && mServiceName == aServiceName && mHostName == aHostName && + mDomain == aDomain; +} + +bool DiscoveryProxy::MdnsSubscription::MatchesServiceInstance(const std::string &aType, + const std::string &aInstanceName) const +{ + return mServiceName == aType && (mInstanceName.empty() || mInstanceName == aInstanceName); +} + +bool DiscoveryProxy::MdnsSubscription::MatchesHost(const std::string &aHostName) const +{ + return mHostName == aHostName; +} + +} // namespace Dnssd +} // namespace otbr + +#endif // OTBR_ENABLE_DNSSD_DISCOVERY_PROXY diff --git a/src/agent/discovery_proxy.hpp b/src/agent/discovery_proxy.hpp new file mode 100644 index 00000000000..2f7bc586f12 --- /dev/null +++ b/src/agent/discovery_proxy.hpp @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definition for DNS-SD Discovery Proxy. + */ + +#ifndef OTBR_AGENT_DISCOVERY_PROXY_HPP_ +#define OTBR_AGENT_DISCOVERY_PROXY_HPP_ + +#if OTBR_ENABLE_DNSSD_DISCOVERY_PROXY + +#include +#include + +#include + +#include +#include + +#include "agent/ncp_openthread.hpp" +#include "mdns/mdns.hpp" + +namespace otbr { +namespace Dnssd { + +/** + * This class implements the DNS-SD Discovery Proxy. + * + */ +class DiscoveryProxy +{ +public: + /** + * This constructor initializes the Discovery Proxy instance. + * + * @param[in] aNcp A reference to the OpenThread Controller instance. + * @param[in] aPublisher A reference to the mDNS Publisher. + * + */ + explicit DiscoveryProxy(Ncp::ControllerOpenThread &aNcp, Mdns::Publisher &aPublisher); + + /** + * This method starts the Discovery Proxy. + * + */ + void Start(void); + + /** + * This method stops the Discovery Proxy. + * + */ + void Stop(void); + +private: + enum : uint32_t + { + kServiceTtlCapLimit = 10, // TTL cap limit for Discovery Proxy (in seconds). + }; + + struct MdnsSubscription + { + explicit MdnsSubscription() + : mSubscriptionCount(0) + { + } + + MdnsSubscription(std::string aInstanceName, + std::string aServiceName, + std::string aHostName, + std::string aDomain) + : mInstanceName(std::move(aInstanceName)) + , mServiceName(std::move(aServiceName)) + , mHostName(std::move(aHostName)) + , mDomain(std::move(aDomain)) + , mSubscriptionCount(1) + { + } + + std::string ToString(void) const; + + std::string mInstanceName; + std::string mServiceName; + std::string mHostName; + std::string mDomain; + int mSubscriptionCount; + bool Matches(const std::string &aInstanceName, + const std::string &aServiceName, + const std::string &aHostName, + const std::string &aDomain) const; + bool MatchesServiceInstance(const std::string &aType, const std::string &aInstanceName) const; + bool MatchesHost(const std::string &aHostName) const; + }; + + typedef std::vector MdnsSubscriptionList; + + static void OnDiscoveryProxySubscribe(void *aContext, const char *aFullName); + void OnDiscoveryProxySubscribe(const char *aSubscription); + static void OnDiscoveryProxyUnsubscribe(void *aContext, const char *aFullName); + void OnDiscoveryProxyUnsubscribe(const char *aSubscription); + int GetServiceSubscriptionCount(const std::string &aInstanceName, + const std::string &aServiceName, + const std::string &aHostName); + static std::string TranslateDomain(const std::string &aName, const std::string &aTargetDomain); + static void CheckServiceNameSanity(const std::string &aType); + static void CheckHostnameSanity(const std::string &aHostName); + void OnServiceDiscovered(const std::string & aSubscription, + const Mdns::Publisher::DiscoveredInstanceInfo &aInstanceInfo); + void OnHostDiscovered(const std::string &aHostName, const Mdns::Publisher::DiscoveredHostInfo &aHostInfo); + static uint32_t CapTtl(uint32_t aTtl); + + Ncp::ControllerOpenThread &mNcp; + Mdns::Publisher & mMdnsPublisher; + MdnsSubscriptionList mSubscriptions; +}; + +} // namespace Dnssd +} // namespace otbr + +#endif // OTBR_ENABLE_DNSSD_DISCOVERY_PROXY + +#endif // OTBR_AGENT_DISCOVERY_PROXY_HPP_ diff --git a/src/utils/event_emitter.cpp b/src/agent/instance_params.cpp similarity index 55% rename from src/utils/event_emitter.cpp rename to src/agent/instance_params.cpp index cba8c25a9b9..776193b6fd6 100644 --- a/src/utils/event_emitter.cpp +++ b/src/agent/instance_params.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, The OpenThread Authors. + * Copyright (c) 2020, The OpenThread Authors. * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -26,69 +26,20 @@ * POSSIBILITY OF SUCH DAMAGE. */ -#include "utils/event_emitter.hpp" +/** + * @file + * The file implements the agent instance parameters. + */ -#include +#include "agent/instance_params.hpp" namespace otbr { -void EventEmitter::On(int aEvent, Callback aCallback, void *aContext) -{ - assert(aCallback); - - Handlers &handlers = mEvents[aEvent]; - - handlers.push_back(Handler(aCallback, aContext)); -} - -void EventEmitter::Off(int aEvent, Callback aCallback, void *aContext) +InstanceParams &InstanceParams::Get(void) { - assert(aCallback); - - if (!mEvents.count(aEvent)) - { - return; - } - - Handler handler = Handler(aCallback, aContext); - Handlers &handlers = mEvents[aEvent]; - - for (Handlers::iterator it = handlers.begin(); it != handlers.end(); ++it) - { - if (*it == handler) - { - handlers.erase(it); - if (handlers.empty()) - { - mEvents.erase(aEvent); - } - break; - } - } -} - -void EventEmitter::Emit(int aEvent, ...) -{ - if (!mEvents.count(aEvent)) - { - return; - } - - va_list args; - va_start(args, aEvent); - Handlers &handlers = mEvents[aEvent]; - - assert(!handlers.empty()); - - for (Handlers::iterator it = handlers.begin(); it != handlers.end(); ++it) - { - va_list tmpArgs; - va_copy(tmpArgs, args); - it->first(it->second, aEvent, tmpArgs); - va_end(tmpArgs); - } + static InstanceParams sInstanceParams; - va_end(args); + return sInstanceParams; } } // namespace otbr diff --git a/src/utils/event_emitter.hpp b/src/agent/instance_params.hpp similarity index 52% rename from src/utils/event_emitter.hpp rename to src/agent/instance_params.hpp index d2268f0a199..107bd61e861 100644 --- a/src/utils/event_emitter.hpp +++ b/src/agent/instance_params.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, The OpenThread Authors. + * Copyright (c) 2020, The OpenThread Authors. * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -26,70 +26,74 @@ * POSSIBILITY OF SUCH DAMAGE. */ -#ifndef OTBR_COMMON_EVENT_EMITTER_HPP_ -#define OTBR_COMMON_EVENT_EMITTER_HPP_ - -#include "openthread-br/config.h" - -#include -#include +/** + * @file + * This file includes definition for Thread border router agent instance parameters. + */ -#include +#ifndef OTBR_AGENT_INSATNCE_PARAMS_HPP_ +#define OTBR_AGENT_INSATNCE_PARAMS_HPP_ namespace otbr { /** - * This class implements the basic functionality of an event emitter. + * This class represents the agent instance parameters. * */ -class EventEmitter +class InstanceParams { +public: /** - * This function pointer will be called when the @p aEvent is emitted. + * This method gets the single `InstanceParams` instance. * - * @param[in] aContext A pointer to application-specific context. - * @param[in] aEvent The emitted event id. - * @param[in] aArguments The arguments associated with this event. + * @returns The single `InstanceParams` instance. * */ - typedef void (*Callback)(void *aContext, int aEvent, va_list aArguments); + static InstanceParams &Get(void); -public: /** - * This method register an event handler for @p aEvent. + * This method sets the Thread network interface name. * - * @param[in] aEvent The event id. - * @param[in] aCallback The function poiner to be called. - * @param[in] aContext A pointer to application-specific context. + * @param[in] aName The Thread network interface name. * */ - void On(int aEvent, Callback aCallback, void *aContext); + void SetThreadIfName(const char *aName) { mThreadIfName = aName; } /** - * This method deregister an event handler for @p aEvent. + * This method gets the Thread network interface name. * - * @param[in] aEvent The event id. - * @param[in] aCallback The function poiner to be called. - * @param[in] aContext A pointer to application-specific context. + * @returns The Thread network interface name. * */ - void Off(int aEvent, Callback aCallback, void *aContext); + const char *GetThreadIfName(void) const { return mThreadIfName; } /** - * This method emits an event. + * This method sets the Backbone network interface name. * - * @param[in] aEvent The event id. + * @param[in] aName The Backbone network interface name. * */ - void Emit(int aEvent, ...); + void SetBackboneIfName(const char *aName) { mBackboneIfName = aName; } + + /** + * This method gets the Backbone network interface name. + * + * @returns The Backbone network interface name. + * + */ + const char *GetBackboneIfName(void) const { return mBackboneIfName; } private: - typedef std::pair Handler; - typedef std::list Handlers; - typedef std::map Events; - Events mEvents; + InstanceParams() + : mThreadIfName(nullptr) + , mBackboneIfName(nullptr) + { + } + + const char *mThreadIfName; + const char *mBackboneIfName; }; } // namespace otbr -#endif // OTBR_COMMON_EVENT_EMITTER_HPP_ +#endif // OTBR_AGENT_INSATNCE_PARAMS_HPP_ diff --git a/src/agent/main.cpp b/src/agent/main.cpp index a60c28d7904..a429a5b240d 100644 --- a/src/agent/main.cpp +++ b/src/agent/main.cpp @@ -28,7 +28,9 @@ #include +#include #include +#include #include #include @@ -39,14 +41,15 @@ #include #include +#include #include #include #include "agent/agent_instance.hpp" -#include "agent/ncp.hpp" #include "agent/ncp_openthread.hpp" #include "common/code_utils.hpp" #include "common/logging.hpp" +#include "common/mainloop.hpp" #include "common/types.hpp" #if OTBR_ENABLE_REST_SERVER #include "rest/rest_web_server.hpp" @@ -71,24 +74,25 @@ static const char kDefaultInterfaceName[] = "wpan0"; enum { -#if OTBR_ENABLE_BACKBONE_ROUTER OTBR_OPT_BACKBONE_INTERFACE_NAME = 'B', -#endif - OTBR_OPT_DEBUG_LEVEL = 'd', - OTBR_OPT_HELP = 'h', - OTBR_OPT_INTERFACE_NAME = 'I', - OTBR_OPT_VERBOSE = 'v', - OTBR_OPT_VERSION = 'V', - OTBR_OPT_SHORTMAX = 128, + OTBR_OPT_DEBUG_LEVEL = 'd', + OTBR_OPT_HELP = 'h', + OTBR_OPT_INTERFACE_NAME = 'I', + OTBR_OPT_VERBOSE = 'v', + OTBR_OPT_VERSION = 'V', + OTBR_OPT_SHORTMAX = 128, OTBR_OPT_RADIO_VERSION, }; +static jmp_buf sResetJump; +static bool sShouldTerminate = false; + +void __gcov_flush(); + // Default poll timeout. static const struct timeval kPollTimeout = {10, 0}; static const struct option kOptions[] = { -#if OTBR_ENABLE_BACKBONE_ROUTER {"backbone-ifname", required_argument, nullptr, OTBR_OPT_BACKBONE_INTERFACE_NAME}, -#endif {"debug-level", required_argument, nullptr, OTBR_OPT_DEBUG_LEVEL}, {"help", no_argument, nullptr, OTBR_OPT_HELP}, {"thread-ifname", required_argument, nullptr, OTBR_OPT_INTERFACE_NAME}, @@ -99,32 +103,35 @@ static const struct option kOptions[] = { static void HandleSignal(int aSignal) { + sShouldTerminate = true; signal(aSignal, SIG_DFL); } static int Mainloop(otbr::AgentInstance &aInstance, const char *aInterfaceName) { - int error = EXIT_FAILURE; + int error = EXIT_SUCCESS; + ControllerOpenThread &ncpOpenThread = static_cast(aInstance.GetNcp()); + + OT_UNUSED_VARIABLE(ncpOpenThread); + #if OTBR_ENABLE_DBUS_SERVER - ControllerOpenThread * ncpOpenThread = reinterpret_cast(&aInstance.GetNcp()); - std::unique_ptr dbusAgent = std::unique_ptr(new DBusAgent(aInterfaceName, ncpOpenThread)); + std::unique_ptr dbusAgent = std::unique_ptr(new DBusAgent(aInterfaceName, &ncpOpenThread)); dbusAgent->Init(); #else - (void)aInterfaceName; + OTBR_UNUSED_VARIABLE(aInterfaceName); #endif #if OTBR_ENABLE_REST_SERVER - ControllerOpenThread *ncpOpenThreadRest = reinterpret_cast(&aInstance.GetNcp()); - RestWebServer * restServer = RestWebServer::GetRestWebServer(ncpOpenThreadRest); + RestWebServer *restServer = RestWebServer::GetRestWebServer(&ncpOpenThread); restServer->Init(); #endif - otbrLog(OTBR_LOG_INFO, "Border router agent started."); + otbrLogInfo("Border router agent started."); // allow quitting elegantly signal(SIGTERM, HandleSignal); - while (true) + while (!sShouldTerminate) { - otSysMainloopContext mainloop; - int rval; + otbr::MainloopContext mainloop; + int rval; mainloop.mMaxFd = -1; mainloop.mTimeout = kPollTimeout; @@ -133,15 +140,14 @@ static int Mainloop(otbr::AgentInstance &aInstance, const char *aInterfaceName) FD_ZERO(&mainloop.mWriteFdSet); FD_ZERO(&mainloop.mErrorFdSet); - aInstance.UpdateFdSet(mainloop); + aInstance.Update(mainloop); #if OTBR_ENABLE_DBUS_SERVER - dbusAgent->UpdateFdSet(mainloop.mReadFdSet, mainloop.mWriteFdSet, mainloop.mErrorFdSet, mainloop.mMaxFd, - mainloop.mTimeout); + dbusAgent->Update(mainloop); #endif #if OTBR_ENABLE_REST_SERVER - restServer->UpdateFdSet(mainloop); + restServer->Update(mainloop); #endif #if OTBR_ENABLE_OPENWRT @@ -152,14 +158,6 @@ static int Mainloop(otbr::AgentInstance &aInstance, const char *aInterfaceName) rval = select(mainloop.mMaxFd + 1, &mainloop.mReadFdSet, &mainloop.mWriteFdSet, &mainloop.mErrorFdSet, &mainloop.mTimeout); -#if OTBR_ENABLE_DBUS_SERVER - if (ncpOpenThread->IsResetRequested()) - { - ncpOpenThread->Reset(); - continue; - } -#endif - if (rval >= 0) { #if OTBR_ENABLE_OPENWRT @@ -174,16 +172,16 @@ static int Mainloop(otbr::AgentInstance &aInstance, const char *aInterfaceName) aInstance.Process(mainloop); #if OTBR_ENABLE_DBUS_SERVER - dbusAgent->Process(mainloop.mReadFdSet, mainloop.mWriteFdSet, mainloop.mErrorFdSet); + dbusAgent->Process(mainloop); #endif } - else + else if (errno != EINTR) { #if OTBR_ENABLE_OPENWRT sThreadMutex.lock(); #endif error = OTBR_ERROR_ERRNO; - otbrLog(OTBR_LOG_ERR, "select() failed", strerror(errno)); + otbrLogErr("select() failed: %s", strerror(errno)); break; } } @@ -193,7 +191,8 @@ static int Mainloop(otbr::AgentInstance &aInstance, const char *aInterfaceName) static void PrintHelp(const char *aProgramName) { - fprintf(stderr, "Usage: %s [-I interfaceName] [-d DEBUG_LEVEL] [-v] RADIO_URL\n", aProgramName); + fprintf(stderr, "Usage: %s [-I interfaceName] [-B backboneIfName] [-d DEBUG_LEVEL] [-v] RADIO_URL [RADIO_URL]\n", + aProgramName); fprintf(stderr, "%s", otSysGetRadioUrlHelpString()); } @@ -209,39 +208,33 @@ static void PrintRadioVersion(otInstance *aInstance) static void OnAllocateFailed(void) { - otbrLog(OTBR_LOG_CRIT, "Allocate failure, exiting..."); + otbrLogCrit("Allocate failure, exiting..."); exit(1); } -int main(int argc, char *argv[]) +static int realmain(int argc, char *argv[]) { - int logLevel = OTBR_LOG_INFO; - int opt; - int ret = EXIT_SUCCESS; - const char * interfaceName = kDefaultInterfaceName; - const char * backboneInterfaceName = nullptr; - otbr::Ncp::Controller *ncp = nullptr; - bool verbose = false; - bool printRadioVersion = false; + otbrLogLevel logLevel = OTBR_LOG_INFO; + int opt; + int ret = EXIT_SUCCESS; + const char * interfaceName = kDefaultInterfaceName; + const char * backboneInterfaceName = ""; + bool verbose = false; + bool printRadioVersion = false; + std::vector radioUrls; std::set_new_handler(OnAllocateFailed); - while ((opt = getopt_long(argc, argv, -#if OTBR_ENABLE_BACKBONE_ROUTER - "B:" -#endif - "d:hI:Vv", - kOptions, nullptr)) != -1) + while ((opt = getopt_long(argc, argv, "B:d:hI:Vv", kOptions, nullptr)) != -1) { switch (opt) { -#if OTBR_ENABLE_BACKBONE_ROUTER case OTBR_OPT_BACKBONE_INTERFACE_NAME: backboneInterfaceName = optarg; break; -#endif + case OTBR_OPT_DEBUG_LEVEL: - logLevel = atoi(optarg); + logLevel = static_cast(atoi(optarg)); VerifyOrExit(logLevel >= OTBR_LOG_EMERG && logLevel <= OTBR_LOG_DEBUG, ret = EXIT_FAILURE); break; @@ -275,33 +268,34 @@ int main(int argc, char *argv[]) } otbrLogInit(kSyslogIdent, logLevel, verbose); - otbrLog(OTBR_LOG_INFO, "Running %s", OTBR_PACKAGE_VERSION); - VerifyOrExit(optind < argc, ret = EXIT_FAILURE); + otbrLogInfo("Running %s", OTBR_PACKAGE_VERSION); + otbrLogInfo("Thread version: %s", otbr::Ncp::ControllerOpenThread::GetThreadVersion()); + otbrLogInfo("Thread interface: %s", interfaceName); + otbrLogInfo("Backbone interface: %s", backboneInterfaceName); - ncp = otbr::Ncp::Controller::Create(interfaceName, argv[optind], backboneInterfaceName); - VerifyOrExit(ncp != nullptr, ret = EXIT_FAILURE); - - otbrLog(OTBR_LOG_INFO, "Thread interface %s", interfaceName); - otbrLog(OTBR_LOG_INFO, "Backbone interface %s", - backboneInterfaceName == nullptr ? "(null)" : backboneInterfaceName); + for (int i = optind; i < argc; i++) + { + otbrLogInfo("Radio URL: %s", argv[i]); + radioUrls.push_back(argv[i]); + } { - otbr::AgentInstance instance(ncp); + otbr::Ncp::ControllerOpenThread ncpOpenThread{interfaceName, radioUrls, backboneInterfaceName}; + otbr::AgentInstance instance(ncpOpenThread); + + otbr::InstanceParams::Get().SetThreadIfName(interfaceName); + otbr::InstanceParams::Get().SetBackboneIfName(backboneInterfaceName); SuccessOrExit(ret = instance.Init()); if (printRadioVersion) { - ControllerOpenThread *ncpOpenThread = reinterpret_cast(ncp); - - PrintRadioVersion(ncpOpenThread->GetInstance()); + PrintRadioVersion(ncpOpenThread.GetInstance()); ExitNow(ret = EXIT_SUCCESS); } #if OTBR_ENABLE_OPENWRT - ControllerOpenThread *ncpThread = reinterpret_cast(ncp); - - UbusServerInit(ncpThread, &sThreadMutex); + UbusServerInit(&ncpOpenThread, &sThreadMutex); std::thread(UbusServerRun).detach(); #endif SuccessOrExit(ret = Mainloop(instance, interfaceName)); @@ -312,3 +306,28 @@ int main(int argc, char *argv[]) exit: return ret; } + +void otPlatReset(otInstance *aInstance) +{ + gPlatResetReason = OT_PLAT_RESET_REASON_SOFTWARE; + + otInstanceFinalize(aInstance); + otSysDeinit(); + + longjmp(sResetJump, 1); + assert(false); +} + +int main(int argc, char *argv[]) +{ + if (setjmp(sResetJump)) + { + alarm(0); +#if OPENTHREAD_ENABLE_COVERAGE + __gcov_flush(); +#endif + execvp(argv[0], argv); + } + + return realmain(argc, argv); +} diff --git a/src/agent/ncp.hpp b/src/agent/ncp.hpp deleted file mode 100644 index ddb8bda1c56..00000000000 --- a/src/agent/ncp.hpp +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright (c) 2017, The OpenThread Authors. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -/** - * @file - * This file includes definitions for NCP service. - */ - -#ifndef OTBR_AGENT_NCP_HPP_ -#define OTBR_AGENT_NCP_HPP_ - -#include "openthread-br/config.h" - -#include -#include - -#include "common/mainloop.h" -#include "common/types.hpp" -#include "utils/event_emitter.hpp" - -namespace otbr { - -namespace Ncp { - -/** - * @addtogroup border-router-ncp - * - * @brief - * This module includes definition for NCP service. - * - * @{ - */ - -/** - * NCP Events definition according to spinel protocol. - * - */ -enum -{ - kEventExtPanId, ///< Extended PAN ID arrived. - kEventNetworkName, ///< Network name arrived. - kEventPSKc, ///< PSKc arrived. - kEventThreadState, ///< Thread State. - kEventThreadVersion, ///< Thread Version. - kEventUdpForwardStream, ///< UDP forward stream arrived. -}; - -/** - * This interface defines NCP Controller functionality. - * - */ -class Controller : public EventEmitter -{ -public: - /** - * This method initalize the NCP controller. - * - * @retval OTBR_ERROR_NONE Successfully initialized NCP controller. - * @retval OTBR_ERROR_DBUS Failed due to dbus error. - * - */ - virtual otbrError Init(void) = 0; - - /** - * This method updates the fd_set to poll. - * - * @param[inout] aMainloop A reference to OpenThread mainloop context. - * - */ - virtual void UpdateFdSet(otSysMainloopContext &aMainloop) = 0; - - /** - * This method performs the Thread processing. - * - * @param[in] aMainloop A reference to OpenThread mainloop context. - * - */ - virtual void Process(const otSysMainloopContext &aMainloop) = 0; - - /** - * This method reest the Ncp Controller. - * - */ - virtual void Reset(void) = 0; - - /** - * This method return whether reset is requested. - * - * @retval TRUE reset is requested. - * @retval FALSE reset isn't requested. - * - */ - virtual bool IsResetRequested(void) = 0; - - /** - * This method request the event. - * - * @param[in] aEvent The event id to request. - * - * @retval OTBR_ERROR_NONE Successfully requested the event. - * @retval OTBR_ERROR_ERRNO Failed to request the event. - * - */ - virtual otbrError RequestEvent(int aEvent) = 0; - - /** - * This method creates a NCP Controller. - * - * @param[in] aInterfaceName A string of the NCP interface name. - * @param[in] aRadioUrl The URL describes the radio chip. - * @param[in] aBackboneInterfaceName The Backbone network interface name. - * - */ - static Controller *Create(const char *aInterfaceName, - const char *aRadioUrl, - const char *aBackboneInterfaceName = nullptr); - - /** - * This method destroys a NCP Controller. - * - * @param[in] aController A pointer to the NCP contorller. - * - */ - static void Destroy(Controller *aController) { delete aController; } - - virtual ~Controller(void) {} -}; - -} // namespace Ncp - -} // namespace otbr - -#endif // OTBR_AGENT_NCP_HPP_ diff --git a/src/agent/ncp_openthread.cpp b/src/agent/ncp_openthread.cpp index 446cd0c6d9a..26c7f8d5eb2 100644 --- a/src/agent/ncp_openthread.cpp +++ b/src/agent/ncp_openthread.cpp @@ -26,20 +26,24 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#define OTBR_LOG_TAG "AGENT" + #include "agent/ncp_openthread.hpp" #include #include #include -#include +#include #include #include +#include #include #include #include #include #include +#include #include #include "common/code_utils.hpp" @@ -50,26 +54,28 @@ #include #endif -static bool sReset; -using std::chrono::duration_cast; -using std::chrono::microseconds; -using std::chrono::seconds; -using std::chrono::steady_clock; - namespace otbr { namespace Ncp { -ControllerOpenThread::ControllerOpenThread(const char *aInterfaceName, - const char *aRadioUrl, - const char *aBackboneInterfaceName) - : mTriedAttach(false) +static const uint16_t kThreadVersion11 = 2; ///< Thread Version 1.1 +static const uint16_t kThreadVersion12 = 3; ///< Thread Version 1.2 + +ControllerOpenThread::ControllerOpenThread(const char * aInterfaceName, + const std::vector &aRadioUrls, + const char * aBackboneInterfaceName) + : mInstance(nullptr) { + VerifyOrDie(aRadioUrls.size() <= OT_PLATFORM_CONFIG_MAX_RADIO_URLS, "Too many Radio URLs!"); + memset(&mConfig, 0, sizeof(mConfig)); mConfig.mInterfaceName = aInterfaceName; mConfig.mBackboneInterfaceName = aBackboneInterfaceName; - mConfig.mRadioUrl = aRadioUrl; - mConfig.mSpeedUpFactor = 1; + for (const char *url : aRadioUrls) + { + mConfig.mRadioUrls[mConfig.mRadioUrlNum++] = url; + } + mConfig.mSpeedUpFactor = 1; } ControllerOpenThread::~ControllerOpenThread(void) @@ -110,7 +116,6 @@ otbrError ControllerOpenThread::Init(void) VerifyOrExit(otLoggingSetLevel(level) == OT_ERROR_NONE, error = OTBR_ERROR_OPENTHREAD); mInstance = otSysInit(&mConfig); - otCliUartInit(mInstance); #if OTBR_ENABLE_LEGACY otLegacyInit(); #endif @@ -122,6 +127,10 @@ otbrError ControllerOpenThread::Init(void) VerifyOrExit(result == OT_ERROR_NONE, error = OTBR_ERROR_OPENTHREAD); } +#if OTBR_ENABLE_SRP_ADVERTISING_PROXY + otSrpServerSetEnabled(mInstance, /* aEnabled */ true); +#endif + mThreadHelper = std::unique_ptr(new otbr::agent::ThreadHelper(mInstance, this)); exit: @@ -130,20 +139,8 @@ otbrError ControllerOpenThread::Init(void) void ControllerOpenThread::HandleStateChanged(otChangedFlags aFlags) { - if (aFlags & OT_CHANGED_THREAD_NETWORK_NAME) - { - EventEmitter::Emit(kEventNetworkName, otThreadGetNetworkName(mInstance)); - } - - if (aFlags & OT_CHANGED_THREAD_EXT_PANID) - { - EventEmitter::Emit(kEventExtPanId, otThreadGetExtendedPanId(mInstance)); - } - if (aFlags & OT_CHANGED_THREAD_ROLE) { - bool attached = false; - switch (otThreadGetDeviceRole(mInstance)) { case OT_DEVICE_ROLE_DISABLED: @@ -157,77 +154,65 @@ void ControllerOpenThread::HandleStateChanged(otChangedFlags aFlags) #if OTBR_ENABLE_LEGACY otLegacyStart(); #endif - attached = true; break; default: break; } + } - EventEmitter::Emit(kEventThreadState, attached); + for (auto &stateCallback : mThreadStateChangedCallbacks) + { + stateCallback(aFlags); } mThreadHelper->StateChangedCallback(aFlags); } -static struct timeval ToTimeVal(const microseconds &aTime) +void ControllerOpenThread::Update(MainloopContext &aMainloop) { - constexpr int kUsPerSecond = 1000000; - struct timeval val; - - val.tv_sec = aTime.count() / kUsPerSecond; - val.tv_usec = aTime.count() % kUsPerSecond; - - return val; -}; - -void ControllerOpenThread::UpdateFdSet(otSysMainloopContext &aMainloop) -{ - microseconds timeout = microseconds(aMainloop.mTimeout.tv_usec) + seconds(aMainloop.mTimeout.tv_sec); - auto now = steady_clock::now(); + mTaskRunner.Update(aMainloop); if (otTaskletsArePending(mInstance)) { - timeout = microseconds::zero(); + aMainloop.mTimeout = ToTimeval(Microseconds::zero()); } - else if (!mTimers.empty()) - { - if (mTimers.begin()->first < now) - { - timeout = microseconds::zero(); - } - else - { - timeout = std::min(timeout, duration_cast(mTimers.begin()->first - now)); - } - } - - aMainloop.mTimeout = ToTimeVal(timeout); otSysMainloopUpdate(mInstance, &aMainloop); } -void ControllerOpenThread::Process(const otSysMainloopContext &aMainloop) +void ControllerOpenThread::Process(const MainloopContext &aMainloop) { - auto now = steady_clock::now(); - otTaskletsProcess(mInstance); otSysMainloopProcess(mInstance, &aMainloop); - while (!mTimers.empty() && mTimers.begin()->first <= now) - { - mTimers.begin()->second(); - mTimers.erase(mTimers.begin()); - } + mTaskRunner.Process(aMainloop); - if (!mTriedAttach && mThreadHelper->TryResumeNetwork() == OT_ERROR_NONE) + if (getenv("OTBR_NO_AUTO_ATTACH") == nullptr && mThreadHelper->TryResumeNetwork() == OT_ERROR_NONE) { - mTriedAttach = true; + setenv("OTBR_NO_AUTO_ATTACH", "1", 0); } } +void ControllerOpenThread::PostTimerTask(Milliseconds aDelay, TaskRunner::Task aTask) +{ + mTaskRunner.Post(std::move(aDelay), std::move(aTask)); +} + +void ControllerOpenThread::RegisterResetHandler(std::function aHandler) +{ + mResetHandlers.emplace_back(std::move(aHandler)); +} + +void ControllerOpenThread::AddThreadStateChangedCallback(ThreadStateChangedCallback aCallback) +{ + mThreadStateChangedCallbacks.emplace_back(std::move(aCallback)); +} + void ControllerOpenThread::Reset(void) { + gPlatResetReason = OT_PLAT_RESET_REASON_SOFTWARE; + otInstanceFinalize(mInstance); otSysDeinit(); Init(); @@ -235,80 +220,26 @@ void ControllerOpenThread::Reset(void) { handler(); } - sReset = false; + unsetenv("OTBR_NO_AUTO_ATTACH"); } -bool ControllerOpenThread::IsResetRequested(void) +const char *ControllerOpenThread::GetThreadVersion(void) { - return sReset; -} - -otbrError ControllerOpenThread::RequestEvent(int aEvent) -{ - otbrError ret = OTBR_ERROR_NONE; + const char *version; - switch (aEvent) + switch (otThreadGetVersion()) { - case kEventExtPanId: - { - EventEmitter::Emit(kEventExtPanId, otThreadGetExtendedPanId(mInstance)); - break; - } - case kEventThreadState: - { - bool attached = false; - - switch (otThreadGetDeviceRole(mInstance)) - { - case OT_DEVICE_ROLE_CHILD: - case OT_DEVICE_ROLE_ROUTER: - case OT_DEVICE_ROLE_LEADER: - attached = true; - break; - default: - break; - } - - EventEmitter::Emit(kEventThreadState, attached); + case kThreadVersion11: + version = "1.1.1"; break; - } - case kEventNetworkName: - { - EventEmitter::Emit(kEventNetworkName, otThreadGetNetworkName(mInstance)); + case kThreadVersion12: + version = "1.2.0"; break; - } - case kEventPSKc: - { - EventEmitter::Emit(kEventPSKc, otThreadGetPskc(mInstance)); - break; - } - case kEventThreadVersion: - { - EventEmitter::Emit(kEventThreadVersion, otThreadGetVersion()); - break; - } default: - assert(false); - break; + otbrLogEmerg("Unexpected thread version %hu", otThreadGetVersion()); + exit(-1); } - - return ret; -} - -void ControllerOpenThread::PostTimerTask(std::chrono::steady_clock::time_point aTimePoint, - const std::function & aTask) -{ - mTimers.insert({aTimePoint, aTask}); -} - -void ControllerOpenThread::RegisterResetHandler(std::function aHandler) -{ - mResetHandlers.emplace_back(std::move(aHandler)); -} - -Controller *Controller::Create(const char *aInterfaceName, const char *aRadioUrl, const char *aBackboneInterfaceName) -{ - return new ControllerOpenThread(aInterfaceName, aRadioUrl, aBackboneInterfaceName); + return version; } /* @@ -318,7 +249,7 @@ extern "C" void otPlatLog(otLogLevel aLogLevel, otLogRegion aLogRegion, const ch { OT_UNUSED_VARIABLE(aLogRegion); - int otbrLogLevel; + otbrLogLevel otbrLogLevel; switch (aLogLevel) { @@ -353,9 +284,3 @@ extern "C" void otPlatLog(otLogLevel aLogLevel, otLogRegion aLogRegion, const ch } // namespace Ncp } // namespace otbr - -void otPlatReset(otInstance *aInstance) -{ - OT_UNUSED_VARIABLE(aInstance); - sReset = true; -} diff --git a/src/agent/ncp_openthread.hpp b/src/agent/ncp_openthread.hpp index f72e06089dd..11aa835882c 100644 --- a/src/agent/ncp_openthread.hpp +++ b/src/agent/ncp_openthread.hpp @@ -37,11 +37,15 @@ #include #include +#include +#include #include #include -#include "ncp.hpp" #include "agent/thread_helper.hpp" +#include "common/mainloop.hpp" +#include "common/task_runner.hpp" +#include "common/types.hpp" namespace otbr { namespace Ncp { @@ -50,18 +54,22 @@ namespace Ncp { * This interface defines NCP Controller functionality. * */ -class ControllerOpenThread : public Controller +class ControllerOpenThread : public MainloopProcessor { public: + using ThreadStateChangedCallback = std::function; + /** * This constructor initializes this object. * * @param[in] aInterfaceName A string of the NCP interface name. - * @param[in] aRadioUrl The URL describes the radio chip. + * @param[in] aRadioUrls The radio URLs (can be IEEE802.15.4 or TREL radio). * @param[in] aBackboneInterfaceName The Backbone network interface name. * */ - ControllerOpenThread(const char *aInterfaceName, const char *aRadioUrl, const char *aBackboneInterfaceName); + ControllerOpenThread(const char * aInterfaceName, + const std::vector &aRadioUrls, + const char * aBackboneInterfaceName); /** * This method initalize the NCP controller. @@ -69,7 +77,7 @@ class ControllerOpenThread : public Controller * @retval OTBR_ERROR_NONE Successfully initialized NCP controller. * */ - otbrError Init(void) override; + otbrError Init(void); /** * This method get mInstance pointer. @@ -88,63 +96,59 @@ class ControllerOpenThread : public Controller otbr::agent::ThreadHelper *GetThreadHelper(void) { return mThreadHelper.get(); } /** - * This method updates the fd_set to poll. + * This method updates the mainloop context. * - * @param[inout] aMainloop A reference to OpenThread mainloop context. + * @param[inout] aMainloop A reference to the mainloop to be updated. * */ - void UpdateFdSet(otSysMainloopContext &aMainloop) override; + void Update(MainloopContext &aMainloop) override; /** - * This method performs the Thread processing. + * This method processes mainloop events. * - * @param[in] aMainloop A reference to OpenThread mainloop context. + * @param[in] aMainloop A reference to the mainloop context. * */ - void Process(const otSysMainloopContext &aMainloop) override; + void Process(const MainloopContext &aMainloop) override; /** - * This method reset the NCP controller. + * This method posts a task to the timer + * + * @param[in] aDelay The delay in milliseconds before executing the task. + * @param[in] aTask The task function. * */ - void Reset(void) override; + void PostTimerTask(Milliseconds aDelay, TaskRunner::Task aTask); /** - * This method return whether reset is requested. + * This method registers a reset handler. * - * @retval TRUE reset is requested. - * @retval FALSE reset isn't requested. + * @param[in] aHandler The handler function. * */ - bool IsResetRequested(void) override; + void RegisterResetHandler(std::function aHandler); /** - * This method request the event. - * - * @param[in] aEvent The event id to request. + * This method adds a event listener for Thread state changes. * - * @retval OTBR_ERROR_NONE Successfully requested the event. - * @retval OTBR_ERROR_ERRNO Failed to request the event. + * @param[in] aCallback The callback to receive Thread state changed events. * */ - otbrError RequestEvent(int aEvent) override; + void AddThreadStateChangedCallback(ThreadStateChangedCallback aCallback); /** - * This method posts a task to the timer - * - * @param[in] aTimePoint The timepoint to trigger the task. - * @param[in] aTask The task function. + * This method resets the OpenThread instance. * */ - void PostTimerTask(std::chrono::steady_clock::time_point aTimePoint, const std::function &aTask); + void Reset(void); /** - * This method registers a reset handler. + * This method returns the Thread protocol version as a string. * - * @param[in] aHandler The handler function. + * @returns A pointer to the Thread version string. * */ - void RegisterResetHandler(std::function aHandler); + static const char *GetThreadVersion(void); ~ControllerOpenThread(void) override; @@ -155,13 +159,26 @@ class ControllerOpenThread : public Controller } void HandleStateChanged(otChangedFlags aFlags); + static void HandleBackboneRouterDomainPrefixEvent(void * aContext, + otBackboneRouterDomainPrefixEvent aEvent, + const otIp6Prefix * aDomainPrefix); + void HandleBackboneRouterDomainPrefixEvent(otBackboneRouterDomainPrefixEvent aEvent, + const otIp6Prefix * aDomainPrefix); + +#if OTBR_ENABLE_DUA_ROUTING + static void HandleBackboneRouterNdProxyEvent(void * aContext, + otBackboneRouterNdProxyEvent aEvent, + const otIp6Address * aAddress); + void HandleBackboneRouterNdProxyEvent(otBackboneRouterNdProxyEvent aEvent, const otIp6Address *aAddress); +#endif + otInstance *mInstance; - otPlatformConfig mConfig; - std::unique_ptr mThreadHelper; - std::multimap> mTimers; - bool mTriedAttach; - std::vector> mResetHandlers; + otPlatformConfig mConfig; + std::unique_ptr mThreadHelper; + std::vector> mResetHandlers; + TaskRunner mTaskRunner; + std::vector mThreadStateChangedCallbacks; }; } // namespace Ncp diff --git a/src/agent/otbr-agent.default b/src/agent/otbr-agent.default deleted file mode 100644 index b5c8bfbb4fa..00000000000 --- a/src/agent/otbr-agent.default +++ /dev/null @@ -1,4 +0,0 @@ -# Default settings for otbr-agent. This file is sourced by systemd - -# Options to pass to otbr-agent -OTBR_AGENT_OPTS="-I wpan0 spinel+hdlc+uart:///dev/ttyACM0" diff --git a/src/agent/otbr-agent.default.in b/src/agent/otbr-agent.default.in new file mode 100644 index 00000000000..a353d84d14a --- /dev/null +++ b/src/agent/otbr-agent.default.in @@ -0,0 +1,4 @@ +# Default settings for otbr-agent. This file is sourced by systemd + +# Options to pass to otbr-agent +OTBR_AGENT_OPTS="-I wpan0 -B @OTBR_INFRA_IF_NAME@ spinel+hdlc+uart:///dev/ttyACM0 trel://@OTBR_INFRA_IF_NAME@" diff --git a/src/agent/thread_helper.cpp b/src/agent/thread_helper.cpp index 6cf3802bc34..1d24133f80e 100644 --- a/src/agent/thread_helper.cpp +++ b/src/agent/thread_helper.cpp @@ -26,6 +26,8 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#define OTBR_LOG_TAG "AGENT" + #include "agent/thread_helper.hpp" #include @@ -207,7 +209,9 @@ void ThreadHelper::Attach(const std::string & aNetworkName, } else { - while (aExtPanId != UINT64_MAX) + *reinterpret_cast(&extPanId) = UINT64_MAX; + + while (*reinterpret_cast(&extPanId) == UINT64_MAX) { RandomFill(extPanId.m8, sizeof(extPanId.m8)); } @@ -247,7 +251,7 @@ void ThreadHelper::Attach(const std::string & aNetworkName, { channelMask = otLinkGetSupportedChannelMask(mInstance) & aChannelMask; } - VerifyOrExit(channelMask != 0, otbrLog(OTBR_LOG_WARNING, "Invalid channel mask"), error = OT_ERROR_INVALID_ARGS); + VerifyOrExit(channelMask != 0, otbrLogWarning("Invalid channel mask"), error = OT_ERROR_INVALID_ARGS); channel = RandomChannelFromChannelMask(channelMask); SuccessOrExit(otLinkSetChannel(mInstance, channel)); @@ -266,6 +270,41 @@ void ThreadHelper::Attach(const std::string & aNetworkName, } } +void ThreadHelper::Attach(ResultHandler aHandler) +{ + otError error = OT_ERROR_NONE; + + VerifyOrExit(mAttachHandler == nullptr && mJoinerHandler == nullptr, error = OT_ERROR_INVALID_STATE); + mAttachHandler = aHandler; + + if (!otIp6IsEnabled(mInstance)) + { + SuccessOrExit(error = otIp6SetEnabled(mInstance, true)); + } + SuccessOrExit(error = otThreadSetEnabled(mInstance, true)); + +exit: + if (error != OT_ERROR_NONE) + { + if (aHandler) + { + aHandler(error); + } + mAttachHandler = nullptr; + } +} + +otError ThreadHelper::Detach(void) +{ + otError error = OT_ERROR_NONE; + + SuccessOrExit(error = otThreadSetEnabled(mInstance, false)); + SuccessOrExit(error = otIp6SetEnabled(mInstance, false)); + +exit: + return error; +} + otError ThreadHelper::Reset(void) { mDeviceRoleHandlers.clear(); @@ -316,7 +355,7 @@ void ThreadHelper::JoinerCallback(otError aError) { if (aError != OT_ERROR_NONE) { - otbrLog(OTBR_LOG_WARNING, "Failed to join Thread network: %s", otThreadErrorToString(aError)); + otbrLogWarning("Failed to join Thread network: %s", otThreadErrorToString(aError)); mJoinerHandler(aError); mJoinerHandler = nullptr; } @@ -348,6 +387,18 @@ otError ThreadHelper::TryResumeNetwork(void) return error; } +void ThreadHelper::LogOpenThreadResult(const char *aAction, otError aError) +{ + if (aError == OT_ERROR_NONE) + { + otbrLogInfo("%s: %s", aAction, otThreadErrorToString(aError)); + } + else + { + otbrLogWarning("%s: %s", aAction, otThreadErrorToString(aError)); + } +} + #if OTBR_ENABLE_UNSECURE_JOIN otError ThreadHelper::PermitUnsecureJoin(uint16_t aPort, uint32_t aSeconds) { @@ -361,25 +412,23 @@ otError ThreadHelper::PermitUnsecureJoin(uint16_t aPort, uint32_t aSeconds) if (aSeconds > 0) { - auto triggerTime = std::chrono::steady_clock::now() + std::chrono::seconds(aSeconds); + auto delay = Milliseconds(aSeconds * 1000); - if (mUnsecurePortCloseTime.find(aPort) == mUnsecurePortCloseTime.end() || - mUnsecurePortCloseTime[aPort] < triggerTime) - { - mUnsecurePortCloseTime[aPort] = triggerTime; - } + ++mUnsecurePortRefCounter[aPort]; - mNcp->PostTimerTask(triggerTime, [this, aPort]() { - auto now = std::chrono::steady_clock::now(); - otExtAddress noneAddress; + mNcp->PostTimerTask(delay, [this, aPort]() { + assert(mUnsecurePortRefCounter.find(aPort) != mUnsecurePortRefCounter.end()); + assert(mUnsecurePortRefCounter[aPort] > 0); - // 0 to clean steering data - memset(&noneAddress.m8, 0, sizeof(noneAddress.m8)); - if (now >= mUnsecurePortCloseTime[aPort]) + if (--mUnsecurePortRefCounter[aPort] == 0) { + otExtAddress noneAddress; + + // 0 to clean steering data + memset(&noneAddress.m8, 0, sizeof(noneAddress.m8)); (void)otIp6RemoveUnsecurePort(mInstance, aPort); otThreadSetSteeringData(mInstance, &noneAddress); - mUnsecurePortCloseTime.erase(aPort); + mUnsecurePortRefCounter.erase(aPort); } }); } diff --git a/src/agent/thread_helper.hpp b/src/agent/thread_helper.hpp index e3bf9565bdb..9d853a8c8eb 100644 --- a/src/agent/thread_helper.hpp +++ b/src/agent/thread_helper.hpp @@ -26,6 +26,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definitions for Thread helper. + */ + #ifndef OTBR_THREAD_HELPER_HPP_ #define OTBR_THREAD_HELPER_HPP_ @@ -43,8 +48,6 @@ #include #include -#include "common/logging.hpp" - namespace otbr { namespace Ncp { class ControllerOpenThread; @@ -54,6 +57,9 @@ class ControllerOpenThread; namespace otbr { namespace agent { +/** + * This class implements Thread helper. + */ class ThreadHelper { public: @@ -119,6 +125,25 @@ class ThreadHelper uint32_t aChannelMask, ResultHandler aHandler); + /** + * This method detaches the device from the Thread network. + * + * @returns The error value of underlying OpenThread API calls. + * + */ + otError Detach(void); + + /** + * This method attaches the device to the Thread network. + * + * @note The joiner start and the attach proccesses are exclusive, and the + * network parameter will be set through the active dataset. + * + * @param[in] aHandler The attach result handler. + * + */ + void Attach(ResultHandler aHandler); + /** * This method resets the OpenThread stack. * @@ -180,11 +205,7 @@ class ThreadHelper * @param[in] aError The action result. * */ - static void LogOpenThreadResult(const char *aAction, otError aError) - { - otbrLog((aError == OT_ERROR_NONE ? OTBR_LOG_INFO : OTBR_LOG_WARNING), "%s: %s", aAction, - otThreadErrorToString(aError)); - } + static void LogOpenThreadResult(const char *aAction, otError aError); private: static void sActiveScanHandler(otActiveScanResult *aResult, void *aThreadHelper); @@ -205,7 +226,7 @@ class ThreadHelper std::vector mDeviceRoleHandlers; - std::map mUnsecurePortCloseTime; + std::map mUnsecurePortRefCounter; ResultHandler mAttachHandler; ResultHandler mJoinerHandler; diff --git a/third_party/d3js/CMakeLists.txt b/src/backbone_router/CMakeLists.txt similarity index 88% rename from third_party/d3js/CMakeLists.txt rename to src/backbone_router/CMakeLists.txt index 28d1701602c..e4c7361f45c 100644 --- a/third_party/d3js/CMakeLists.txt +++ b/src/backbone_router/CMakeLists.txt @@ -26,7 +26,14 @@ # POSSIBILITY OF SUCH DAMAGE. # -install(FILES - ${CMAKE_CURRENT_SOURCE_DIR}/repo/d3.js - ${CMAKE_CURRENT_SOURCE_DIR}/repo/d3.min.js - DESTINATION ${OTBR_WEB_DATADIR}/frontend/res/js) +add_library(otbr-backbone-router + backbone_agent.cpp + dua_routing_manager.cpp + nd_proxy.cpp +) + +target_link_libraries(otbr-backbone-router PRIVATE + otbr-common + otbr-utils + netfilter_queue +) diff --git a/src/backbone_router/backbone_agent.cpp b/src/backbone_router/backbone_agent.cpp new file mode 100644 index 00000000000..e1867482486 --- /dev/null +++ b/src/backbone_router/backbone_agent.cpp @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * The file implements the Thread Backbone agent. + */ + +#define OTBR_LOG_TAG "BBA" + +#include "backbone_router/backbone_agent.hpp" + +#include +#include + +#include + +#include "common/code_utils.hpp" + +namespace otbr { +namespace BackboneRouter { + +BackboneAgent::BackboneAgent(otbr::Ncp::ControllerOpenThread &aNcp) + : mNcp(aNcp) + , mBackboneRouterState(OT_BACKBONE_ROUTER_STATE_DISABLED) +#if OTBR_ENABLE_DUA_ROUTING + , mNdProxyManager(aNcp) +#endif +{ +} + +void BackboneAgent::Init(void) +{ + mNcp.AddThreadStateChangedCallback([this](otChangedFlags aFlags) { HandleThreadStateChanged(aFlags); }); + otBackboneRouterSetDomainPrefixCallback(mNcp.GetInstance(), &BackboneAgent::HandleBackboneRouterDomainPrefixEvent, + this); +#if OTBR_ENABLE_DUA_ROUTING + otBackboneRouterSetNdProxyCallback(mNcp.GetInstance(), &BackboneAgent::HandleBackboneRouterNdProxyEvent, this); + mNdProxyManager.Init(); +#endif + + otBackboneRouterSetEnabled(mNcp.GetInstance(), /* aEnabled */ true); +} + +void BackboneAgent::HandleThreadStateChanged(otChangedFlags aFlags) +{ + if (aFlags & OT_CHANGED_THREAD_BACKBONE_ROUTER_STATE) + { + HandleBackboneRouterState(); + } +} + +void BackboneAgent::HandleBackboneRouterState(void) +{ + otBackboneRouterState state = otBackboneRouterGetState(mNcp.GetInstance()); + bool wasPrimary = (mBackboneRouterState == OT_BACKBONE_ROUTER_STATE_PRIMARY); + + otbrLogDebug("BackboneAgent: HandleBackboneRouterState: state=%d, mBackboneRouterState=%d", state, + mBackboneRouterState); + VerifyOrExit(mBackboneRouterState != state); + + mBackboneRouterState = state; + + if (IsPrimary()) + { + OnBecomePrimary(); + } + else if (wasPrimary) + { + OnResignPrimary(); + } + +exit: + return; +} + +void BackboneAgent::OnBecomePrimary(void) +{ + otbrLogNotice("BackboneAgent: Backbone Router becomes Primary!"); + +#if OTBR_ENABLE_DUA_ROUTING + if (mDomainPrefix.IsValid()) + { + mDuaRoutingManager.Enable(mDomainPrefix); + mNdProxyManager.Enable(mDomainPrefix); + } +#endif +} + +void BackboneAgent::OnResignPrimary(void) +{ + otbrLogNotice("BackboneAgent: Backbone Router resigns Primary to %s!", StateToString(mBackboneRouterState)); + +#if OTBR_ENABLE_DUA_ROUTING + mDuaRoutingManager.Disable(); + mNdProxyManager.Disable(); +#endif +} + +const char *BackboneAgent::StateToString(otBackboneRouterState aState) +{ + const char *ret = "Unknown"; + + switch (aState) + { + case OT_BACKBONE_ROUTER_STATE_DISABLED: + ret = "Disabled"; + break; + case OT_BACKBONE_ROUTER_STATE_SECONDARY: + ret = "Secondary"; + break; + case OT_BACKBONE_ROUTER_STATE_PRIMARY: + ret = "Primary"; + break; + } + + return ret; +} + +void BackboneAgent::Update(MainloopContext &aMainloop) +{ + OTBR_UNUSED_VARIABLE(aMainloop); + +#if OTBR_ENABLE_DUA_ROUTING + mNdProxyManager.Update(aMainloop); +#endif +} + +void BackboneAgent::Process(const MainloopContext &aMainloop) +{ + OTBR_UNUSED_VARIABLE(aMainloop); + +#if OTBR_ENABLE_DUA_ROUTING + mNdProxyManager.Process(aMainloop); +#endif +} + +void BackboneAgent::HandleBackboneRouterDomainPrefixEvent(void * aContext, + otBackboneRouterDomainPrefixEvent aEvent, + const otIp6Prefix * aDomainPrefix) +{ + static_cast(aContext)->HandleBackboneRouterDomainPrefixEvent(aEvent, aDomainPrefix); +} + +void BackboneAgent::HandleBackboneRouterDomainPrefixEvent(otBackboneRouterDomainPrefixEvent aEvent, + const otIp6Prefix * aDomainPrefix) +{ + if (aEvent == OT_BACKBONE_ROUTER_DOMAIN_PREFIX_REMOVED) + { + mDomainPrefix.Clear(); + } + else + { + assert(aDomainPrefix != nullptr); + mDomainPrefix.Set(*aDomainPrefix); + assert(mDomainPrefix.IsValid()); + } + + VerifyOrExit(IsPrimary() && aEvent != OT_BACKBONE_ROUTER_DOMAIN_PREFIX_REMOVED); + +#if OTBR_ENABLE_DUA_ROUTING + mDuaRoutingManager.Disable(); + mNdProxyManager.Disable(); + + mDuaRoutingManager.Enable(mDomainPrefix); + mNdProxyManager.Enable(mDomainPrefix); +#endif + +exit: + return; +} + +#if OTBR_ENABLE_DUA_ROUTING +void BackboneAgent::HandleBackboneRouterNdProxyEvent(void * aContext, + otBackboneRouterNdProxyEvent aEvent, + const otIp6Address * aAddress) +{ + static_cast(aContext)->HandleBackboneRouterNdProxyEvent(aEvent, aAddress); +} + +void BackboneAgent::HandleBackboneRouterNdProxyEvent(otBackboneRouterNdProxyEvent aEvent, const otIp6Address *aDua) +{ + mNdProxyManager.HandleBackboneRouterNdProxyEvent(aEvent, aDua); +} +#endif + +} // namespace BackboneRouter +} // namespace otbr diff --git a/src/backbone_router/backbone_agent.hpp b/src/backbone_router/backbone_agent.hpp new file mode 100644 index 00000000000..2c1cdbcdfee --- /dev/null +++ b/src/backbone_router/backbone_agent.hpp @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definition for Thread Backbone agent. + */ + +#ifndef BACKBONE_ROUTER_BACKBONE_AGENT_HPP_ +#define BACKBONE_ROUTER_BACKBONE_AGENT_HPP_ + +#include + +#include "agent/instance_params.hpp" +#include "agent/ncp_openthread.hpp" +#include "backbone_router/dua_routing_manager.hpp" +#include "backbone_router/nd_proxy.hpp" +#include "common/mainloop.hpp" + +namespace otbr { +namespace BackboneRouter { + +/** + * @addtogroup border-router-backbone + * + * @brief + * This module includes definition for Thread Backbone agent. + * + * @{ + */ + +/** + * This class implements Thread Backbone agent functionality. + * + */ +class BackboneAgent : public MainloopProcessor +{ +public: + static constexpr uint16_t kBackboneUdpPort = 61631; ///< The BBR port. + + /** + * This constructor intiializes the `BackboneAgent` instance. + * + * @param[in] aNcp The Thread instance. + * + */ + BackboneAgent(otbr::Ncp::ControllerOpenThread &aNcp); + + /** + * This method initializes the Backbone agent. + * + */ + void Init(void); + + /** + * This method updates the mainloop context. + * + * @param[inout] aMainloop A reference to the mainloop to be updated. + * + */ + void Update(MainloopContext &aMainloop) override; + + /** + * This method processes mainloop events. + * + * @param[in] aMainloop A reference to the mainloop context. + * + */ + void Process(const MainloopContext &aMainloop) override; + +private: + void OnBecomePrimary(void); + void OnResignPrimary(void); + bool IsPrimary(void) const { return mBackboneRouterState == OT_BACKBONE_ROUTER_STATE_PRIMARY; } + void HandleThreadStateChanged(otChangedFlags aFlags); + void HandleBackboneRouterState(void); + static void HandleBackboneRouterDomainPrefixEvent(void * aContext, + otBackboneRouterDomainPrefixEvent aEvent, + const otIp6Prefix * aDomainPrefix); + void HandleBackboneRouterDomainPrefixEvent(otBackboneRouterDomainPrefixEvent aEvent, + const otIp6Prefix * aDomainPrefix); +#if OTBR_ENABLE_DUA_ROUTING + static void HandleBackboneRouterNdProxyEvent(void * aContext, + otBackboneRouterNdProxyEvent aEvent, + const otIp6Address * aAddress); + void HandleBackboneRouterNdProxyEvent(otBackboneRouterNdProxyEvent aEvent, const otIp6Address *aAddress); +#endif + + static const char *StateToString(otBackboneRouterState aState); + + otbr::Ncp::ControllerOpenThread &mNcp; + otBackboneRouterState mBackboneRouterState; + Ip6Prefix mDomainPrefix; +#if OTBR_ENABLE_DUA_ROUTING + NdProxyManager mNdProxyManager; + DuaRoutingManager mDuaRoutingManager; +#endif +}; + +/** + * @} + */ + +} // namespace BackboneRouter +} // namespace otbr + +#endif // BACKBONE_ROUTER_BACKBONE_AGENT_HPP_ diff --git a/src/backbone_router/constants.hpp b/src/backbone_router/constants.hpp new file mode 100644 index 00000000000..c853e12f138 --- /dev/null +++ b/src/backbone_router/constants.hpp @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definition for Thread Backbone constants. + */ + +#ifndef BACKBONE_CONSTANTS_HPP_ +#define BACKBONE_CONSTANTS_HPP_ + +namespace otbr { +namespace BackboneRouter { + +/** + * @addtogroup border-router-bbr + * + * @brief + * This module includes definition for Thread Backbone constants. + * + * @{ + */ + +/** + * Backbone configurations. + * + */ +enum +{ + kDuaRecentTime = 20, ///< Time period (in seconds) during which a DUA registration is considered 'recent' at a BBR. +}; + +/** + * @} + */ + +} // namespace BackboneRouter +} // namespace otbr + +#endif // BACKBONE_CONSTANTS_HPP_ diff --git a/src/backbone_router/dua_routing_manager.cpp b/src/backbone_router/dua_routing_manager.cpp new file mode 100644 index 00000000000..3b024fe2a9c --- /dev/null +++ b/src/backbone_router/dua_routing_manager.cpp @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * The file implements DUA routing functionalities. + */ + +#include "backbone_router/dua_routing_manager.hpp" + +#if OTBR_ENABLE_DUA_ROUTING + +#include "common/code_utils.hpp" + +namespace otbr { + +namespace BackboneRouter { + +void DuaRoutingManager::Enable(const Ip6Prefix &aDomainPrefix) +{ + VerifyOrExit(!mEnabled); + mEnabled = true; + + mDomainPrefix = aDomainPrefix; + + AddDefaultRouteToThread(); + AddPolicyRouteToBackbone(); + +exit: + otbrLogResult(OTBR_ERROR_NONE, "DuaRoutingManager: %s", __FUNCTION__); +} + +void DuaRoutingManager::Disable(void) +{ + VerifyOrExit(mEnabled); + mEnabled = false; + + DelDefaultRouteToThread(); + DelPolicyRouteToBackbone(); + +exit: + otbrLogResult(OTBR_ERROR_NONE, "DuaRoutingManager: %s", __FUNCTION__); +} + +void DuaRoutingManager::AddDefaultRouteToThread(void) +{ + SystemUtils::ExecuteCommand("ip -6 route add %s dev %s proto static metric 1", mDomainPrefix.ToString().c_str(), + InstanceParams::Get().GetThreadIfName()); +} + +void DuaRoutingManager::DelDefaultRouteToThread(void) +{ + SystemUtils::ExecuteCommand("ip -6 route del %s dev %s proto static metric 1", mDomainPrefix.ToString().c_str(), + InstanceParams::Get().GetThreadIfName()); +} + +void DuaRoutingManager::AddPolicyRouteToBackbone(void) +{ + // Packets from Thread interface use route table "openthread" + SystemUtils::ExecuteCommand("ip -6 rule add iif %s table openthread", InstanceParams::Get().GetThreadIfName()); + SystemUtils::ExecuteCommand("ip -6 route add %s dev %s proto static table openthread", + mDomainPrefix.ToString().c_str(), InstanceParams::Get().GetBackboneIfName()); +} + +void DuaRoutingManager::DelPolicyRouteToBackbone(void) +{ + SystemUtils::ExecuteCommand("ip -6 rule del iif %s table openthread", InstanceParams::Get().GetThreadIfName()); + SystemUtils::ExecuteCommand("ip -6 route del %s dev %s proto static table openthread", + mDomainPrefix.ToString().c_str(), InstanceParams::Get().GetBackboneIfName()); +} + +} // namespace BackboneRouter +} // namespace otbr + +#endif // OTBR_ENABLE_DUA_ROUTING diff --git a/src/backbone_router/dua_routing_manager.hpp b/src/backbone_router/dua_routing_manager.hpp new file mode 100644 index 00000000000..6af9decfb39 --- /dev/null +++ b/src/backbone_router/dua_routing_manager.hpp @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definition for DUA routing functionalities. + */ + +#ifndef BACKBONE_ROUTER_DUA_ROUTING_MANAGER +#define BACKBONE_ROUTER_DUA_ROUTING_MANAGER + +#if OTBR_ENABLE_DUA_ROUTING + +#include +#include + +#include "agent/instance_params.hpp" +#include "agent/ncp_openthread.hpp" +#include "utils/system_utils.hpp" + +namespace otbr { +namespace BackboneRouter { + +/** + * @addtogroup border-router-backbone + * + * @brief + * This module includes definition for DUA routing functionalities. + * + * @{ + */ + +/** + * This class implements the DUA routing manager. + * + */ +class DuaRoutingManager +{ +public: + /** + * This constructor initializes a DUA routing manager instance. + * + */ + explicit DuaRoutingManager() + : mEnabled(false) + { + } + + /** + * This method enables the DUA routing manager. + * + */ + void Enable(const Ip6Prefix &aDomainPrefix); + + /** + * This method disables the DUA routing manager. + * + */ + void Disable(void); + +private: + void AddDefaultRouteToThread(void); + void DelDefaultRouteToThread(void); + void AddPolicyRouteToBackbone(void); + void DelPolicyRouteToBackbone(void); + + Ip6Prefix mDomainPrefix; + bool mEnabled : 1; +}; + +/** + * @} + */ + +} // namespace BackboneRouter +} // namespace otbr + +#endif // OTBR_ENABLE_DUA_ROUTING + +#endif // BACKBONE_ROUTER_DUA_ROUTING_MANAGER diff --git a/src/backbone_router/nd_proxy.cpp b/src/backbone_router/nd_proxy.cpp new file mode 100644 index 00000000000..5d3e98f0cad --- /dev/null +++ b/src/backbone_router/nd_proxy.cpp @@ -0,0 +1,570 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * The file implements the ND Proxy management. + */ + +#define OTBR_LOG_TAG "NDPROXY" + +#include "backbone_router/nd_proxy.hpp" + +#if OTBR_ENABLE_DUA_ROUTING + +#include + +#include +#include +#include +#include +#include +#include + +#if __linux__ +#include +#else +#error "Platform not supported" +#endif + +#include "agent/instance_params.hpp" +#include "backbone_router/constants.hpp" +#include "common/code_utils.hpp" +#include "common/logging.hpp" +#include "common/types.hpp" +#include "utils/system_utils.hpp" + +namespace otbr { +namespace BackboneRouter { + +void NdProxyManager::Enable(const Ip6Prefix &aDomainPrefix) +{ + otbrError error = OTBR_ERROR_NONE; + + VerifyOrExit(!IsEnabled()); + + assert(aDomainPrefix.IsValid()); + mDomainPrefix = aDomainPrefix; + + SuccessOrExit(error = InitIcmp6RawSocket()); + SuccessOrExit(error = UpdateMacAddress()); + SuccessOrExit(error = InitNetfilterQueue()); + + // Add ip6tables rule for unicast ICMPv6 messages + VerifyOrExit(SystemUtils::ExecuteCommand( + "ip6tables -t raw -A PREROUTING -6 -d %s -p icmpv6 --icmpv6-type neighbor-solicitation -i %s -j " + "NFQUEUE --queue-num 88", + mDomainPrefix.ToString().c_str(), InstanceParams::Get().GetBackboneIfName()) == 0, + error = OTBR_ERROR_ERRNO); + +exit: + if (error != OTBR_ERROR_NONE) + { + FiniNetfilterQueue(); + FiniIcmp6RawSocket(); + } + + otbrLogResult(error, "NdProxyManager: %s", __FUNCTION__); +} + +void NdProxyManager::Disable(void) +{ + otbrError error = OTBR_ERROR_NONE; + + VerifyOrExit(IsEnabled()); + + FiniNetfilterQueue(); + FiniIcmp6RawSocket(); + + // Remove ip6tables rule for unicast ICMPv6 messages + VerifyOrExit(SystemUtils::ExecuteCommand( + "ip6tables -t raw -D PREROUTING -6 -d %s -p icmpv6 --icmpv6-type neighbor-solicitation -i %s -j " + "NFQUEUE --queue-num 88", + mDomainPrefix.ToString().c_str(), InstanceParams::Get().GetBackboneIfName()) == 0, + error = OTBR_ERROR_ERRNO); + +exit: + otbrLogResult(error, "NdProxyManager: %s", __FUNCTION__); +} + +void NdProxyManager::Init(void) +{ + mBackboneIfIndex = if_nametoindex(InstanceParams::Get().GetBackboneIfName()); + VerifyOrDie(mBackboneIfIndex > 0, "if_nametoindex failed"); +} + +void NdProxyManager::Update(MainloopContext &aMainloop) +{ + if (mIcmp6RawSock >= 0) + { + FD_SET(mIcmp6RawSock, &aMainloop.mReadFdSet); + aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, mIcmp6RawSock); + } + + if (mUnicastNsQueueSock >= 0) + { + FD_SET(mUnicastNsQueueSock, &aMainloop.mReadFdSet); + aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, mUnicastNsQueueSock); + } +} + +void NdProxyManager::Process(const MainloopContext &aMainloop) +{ + VerifyOrExit(IsEnabled()); + + if (FD_ISSET(mIcmp6RawSock, &aMainloop.mReadFdSet)) + { + ProcessMulticastNeighborSolicition(); + } + + if (FD_ISSET(mUnicastNsQueueSock, &aMainloop.mReadFdSet)) + { + ProcessUnicastNeighborSolicition(); + } +exit: + return; +} + +void NdProxyManager::ProcessMulticastNeighborSolicition() +{ + struct msghdr msghdr; + sockaddr_in6 sin6; + struct iovec iovec; + ssize_t len; + struct icmp6_hdr *icmp6header; + struct cmsghdr * cmsghdr; + unsigned char cbuf[2 * CMSG_SPACE(sizeof(struct in6_pktinfo))]; + uint8_t packet[kMaxICMP6PacketSize]; + otbrError error = OTBR_ERROR_NONE; + bool found = false; + + iovec.iov_len = kMaxICMP6PacketSize; + iovec.iov_base = packet; + + msghdr.msg_name = &sin6; + msghdr.msg_namelen = sizeof(sin6); + msghdr.msg_iov = &iovec; + msghdr.msg_iovlen = 1; + msghdr.msg_control = cbuf; + msghdr.msg_controllen = sizeof(cbuf); + + len = recvmsg(mIcmp6RawSock, &msghdr, 0); + + VerifyOrExit(len >= static_cast(sizeof(struct icmp6_hdr)), error = OTBR_ERROR_ERRNO); + + { + Ip6Address &src = *reinterpret_cast(&sin6.sin6_addr); + + icmp6header = reinterpret_cast(packet); + + // only process neighbor solicit + VerifyOrExit(icmp6header->icmp6_type == ND_NEIGHBOR_SOLICIT, error = OTBR_ERROR_PARSE); + + otbrLogDebug("NdProxyManager: Received ND-NS from %s", src.ToString().c_str()); + + for (cmsghdr = CMSG_FIRSTHDR(&msghdr); cmsghdr; cmsghdr = CMSG_NXTHDR(&msghdr, cmsghdr)) + { + if (cmsghdr->cmsg_level != IPPROTO_IPV6) + { + continue; + } + + switch (cmsghdr->cmsg_type) + { + case IPV6_PKTINFO: + if (cmsghdr->cmsg_len == CMSG_LEN(sizeof(struct in6_pktinfo))) + { + struct in6_pktinfo *pktinfo = (struct in6_pktinfo *)CMSG_DATA(cmsghdr); + Ip6Address & dst = *reinterpret_cast(&pktinfo->ipi6_addr); + uint32_t ifindex = pktinfo->ipi6_ifindex; + + for (const Ip6Address &ipaddr : mNdProxySet) + { + if (ipaddr.ToSolicitedNodeMulticastAddress() == dst) + { + found = true; + break; + } + } + + otbrLogDebug("NdProxyManager: dst=%s, ifindex=%d, proxying=%s", dst.ToString().c_str(), ifindex, + found ? "Y" : "N"); + } + break; + + case IPV6_HOPLIMIT: + if (cmsghdr->cmsg_len == CMSG_LEN(sizeof(int))) + { + int hops = *(int *)CMSG_DATA(cmsghdr); + + otbrLogDebug("NdProxyManager: hops=%d (%s)", hops, hops == 255 ? "Good" : "Bad"); + + VerifyOrExit(hops == 255); + } + break; + } + } + + VerifyOrExit(found, error = OTBR_ERROR_NOT_FOUND); + + { + struct nd_neighbor_solicit *ns = reinterpret_cast(packet); + Ip6Address & target = *reinterpret_cast(&ns->nd_ns_target); + + otbrLogInfo("NdProxyManager: send solicited NA for multicast NS: src=%s, target=%s", src.ToString().c_str(), + target.ToString().c_str()); + + SendNeighborAdvertisement(target, src); + } + } + +exit: + otbrLogResult(error, "NdProxyManager: %s", __FUNCTION__); +} + +void NdProxyManager::ProcessUnicastNeighborSolicition(void) +{ + otbrError error = OTBR_ERROR_NONE; + char packet[kMaxICMP6PacketSize]; + ssize_t len; + + VerifyOrExit((len = recv(mUnicastNsQueueSock, packet, sizeof(packet), 0)) >= 0, error = OTBR_ERROR_ERRNO); + VerifyOrExit(nfq_handle_packet(mNfqHandler, packet, len) == 0, error = OTBR_ERROR_ERRNO); + + error = OTBR_ERROR_NONE; + +exit: + otbrLogResult(error, "NdProxyManager: %s", __FUNCTION__); +} + +void NdProxyManager::HandleBackboneRouterNdProxyEvent(otBackboneRouterNdProxyEvent aEvent, const otIp6Address *aDua) +{ + Ip6Address target; + + if (aEvent != OT_BACKBONE_ROUTER_NDPROXY_CLEARED) + { + assert(aDua != nullptr); + target = Ip6Address(aDua->mFields.m8); + } + + switch (aEvent) + { + case OT_BACKBONE_ROUTER_NDPROXY_ADDED: + case OT_BACKBONE_ROUTER_NDPROXY_RENEWED: + { + bool isNewInsert = mNdProxySet.insert(target).second; + + if (isNewInsert) + { + JoinSolicitedNodeMulticastGroup(target); + } + + SendNeighborAdvertisement(target, Ip6Address::GetLinkLocalAllNodesMulticastAddress()); + break; + } + case OT_BACKBONE_ROUTER_NDPROXY_REMOVED: + mNdProxySet.erase(target); + LeaveSolicitedNodeMulticastGroup(target); + break; + case OT_BACKBONE_ROUTER_NDPROXY_CLEARED: + for (const Ip6Address &proxingTarget : mNdProxySet) + { + LeaveSolicitedNodeMulticastGroup(proxingTarget); + } + mNdProxySet.clear(); + break; + } +} + +void NdProxyManager::SendNeighborAdvertisement(const Ip6Address &aTarget, const Ip6Address &aDst) +{ + uint8_t packet[kMaxICMP6PacketSize]; + uint16_t len = 0; + struct nd_neighbor_advert &na = *reinterpret_cast(packet); + struct nd_opt_hdr & opt = *reinterpret_cast(packet + sizeof(struct nd_neighbor_advert)); + bool isSolicited = !aDst.IsMulticast(); + sockaddr_in6 dst; + otbrError error = OTBR_ERROR_NONE; + otBackboneRouterNdProxyInfo aNdProxyInfo; + + VerifyOrExit(otBackboneRouterGetNdProxyInfo(mNcp.GetInstance(), reinterpret_cast(&aTarget), + &aNdProxyInfo) == OT_ERROR_NONE, + error = OTBR_ERROR_OPENTHREAD); + + memset(packet, 0, sizeof(packet)); + + na.nd_na_type = ND_NEIGHBOR_ADVERT; + na.nd_na_code = 0; + // set Solicited + na.nd_na_flags_reserved = isSolicited ? ND_NA_FLAG_SOLICITED : 0; + // set Router + na.nd_na_flags_reserved |= ND_NA_FLAG_ROUTER; + // set Override + na.nd_na_flags_reserved |= aNdProxyInfo.mTimeSinceLastTransaction <= kDuaRecentTime ? ND_NA_FLAG_OVERRIDE : 0; + + memcpy(&na.nd_na_target, aTarget.m8, sizeof(Ip6Address)); + len += sizeof(struct nd_neighbor_advert); + + opt.nd_opt_type = ND_OPT_TARGET_LINKADDR; + opt.nd_opt_len = 1; + + memcpy(reinterpret_cast(&opt) + 2, mMacAddress.m8, sizeof(mMacAddress)); + + len += (opt.nd_opt_len * 8); + + aDst.CopyTo(dst); + + VerifyOrExit(sendto(mIcmp6RawSock, packet, len, 0, reinterpret_cast(&dst), sizeof(dst)) == len, + error = OTBR_ERROR_ERRNO); + +exit: + otbrLogResult(error, "NdProxyManager: %s", __FUNCTION__); +} + +otbrError NdProxyManager::UpdateMacAddress(void) +{ + otbrError error = OTBR_ERROR_NONE; + +#if !__APPLE__ + struct ifreq ifr; + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, InstanceParams::Get().GetBackboneIfName(), sizeof(ifr.ifr_name) - 1); + + VerifyOrExit(ioctl(mIcmp6RawSock, SIOCGIFHWADDR, &ifr) != -1, error = OTBR_ERROR_ERRNO); + memcpy(mMacAddress.m8, ifr.ifr_hwaddr.sa_data, sizeof(mMacAddress)); +#else + ExitNow(error = OTBR_ERROR_NOT_IMPLEMENTED); +#endif +exit: + otbrLogResult(error, "NdProxyManager: UpdateMacAddress to %s", mMacAddress.ToString().c_str()); + return error; +} + +otbrError NdProxyManager::InitIcmp6RawSocket(void) +{ + otbrError error = OTBR_ERROR_NONE; + int on = 1; + int hops = 255; + struct icmp6_filter filter; + + mIcmp6RawSock = socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6); + VerifyOrExit(mIcmp6RawSock >= 0, error = OTBR_ERROR_ERRNO); + +#if __linux__ + VerifyOrExit(setsockopt(mIcmp6RawSock, SOL_SOCKET, SO_BINDTODEVICE, InstanceParams::Get().GetBackboneIfName(), + strlen(InstanceParams::Get().GetBackboneIfName())) == 0, + error = OTBR_ERROR_ERRNO); +#else // __NetBSD__ || __FreeBSD__ || __APPLE__ + VerifyOrExit(setsockopt(mIcmp6RawSock, IPPROTO_IP, IP_BOUND_IF, mBackboneIfName.c_str(), mBackboneIfName.size()), + error = OTBR_ERROR_ERRNO); +#endif // __linux__ + + VerifyOrExit(setsockopt(mIcmp6RawSock, IPPROTO_IPV6, IPV6_RECVPKTINFO, &on, sizeof(on)) == 0, + error = OTBR_ERROR_ERRNO); + VerifyOrExit(setsockopt(mIcmp6RawSock, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &on, sizeof(on)) == 0, + error = OTBR_ERROR_ERRNO); + VerifyOrExit(setsockopt(mIcmp6RawSock, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &hops, sizeof(hops)) == 0, + error = OTBR_ERROR_ERRNO); + VerifyOrExit(setsockopt(mIcmp6RawSock, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &hops, sizeof(hops)) == 0, + error = OTBR_ERROR_ERRNO); + + ICMP6_FILTER_SETBLOCKALL(&filter); + ICMP6_FILTER_SETPASS(ND_NEIGHBOR_SOLICIT, &filter); + + VerifyOrExit(setsockopt(mIcmp6RawSock, IPPROTO_ICMPV6, ICMP6_FILTER, &filter, sizeof(filter)) == 0, + error = OTBR_ERROR_ERRNO); +exit: + if (error != OTBR_ERROR_NONE) + { + FiniIcmp6RawSocket(); + } + + return error; +} + +void NdProxyManager::FiniIcmp6RawSocket(void) +{ + if (mIcmp6RawSock != -1) + { + close(mIcmp6RawSock); + mIcmp6RawSock = -1; + } +} + +otbrError NdProxyManager::InitNetfilterQueue(void) +{ + otbrError error = OTBR_ERROR_ERRNO; + + VerifyOrExit((mNfqHandler = nfq_open()) != nullptr); + VerifyOrExit(nfq_unbind_pf(mNfqHandler, AF_INET6) >= 0); + VerifyOrExit(nfq_bind_pf(mNfqHandler, AF_INET6) >= 0); + + VerifyOrExit((mNfqQueueHandler = nfq_create_queue(mNfqHandler, 88, HandleNetfilterQueue, this)) != nullptr); + VerifyOrExit(nfq_set_mode(mNfqQueueHandler, NFQNL_COPY_PACKET, 0xffff) >= 0); + VerifyOrExit((mUnicastNsQueueSock = nfq_fd(mNfqHandler)) >= 0); + + error = OTBR_ERROR_NONE; + +exit: + otbrLogResult(error, "NdProxyManager: %s", __FUNCTION__); + + if (error != OTBR_ERROR_NONE) + { + FiniNetfilterQueue(); + } + + return error; +} + +void NdProxyManager::FiniNetfilterQueue(void) +{ + if (mUnicastNsQueueSock != -1) + { + close(mUnicastNsQueueSock); + mUnicastNsQueueSock = -1; + } + + if (mNfqQueueHandler != nullptr) + { + nfq_destroy_queue(mNfqQueueHandler); + mNfqQueueHandler = nullptr; + } + + if (mNfqHandler != nullptr) + { + nfq_close(mNfqHandler); + mNfqHandler = nullptr; + } +} + +int NdProxyManager::HandleNetfilterQueue(struct nfq_q_handle *aNfQueueHandler, + struct nfgenmsg * aNfMsg, + struct nfq_data * aNfData, + void * aContext) +{ + return static_cast(aContext)->HandleNetfilterQueue(aNfQueueHandler, aNfMsg, aNfData); +} + +int NdProxyManager::HandleNetfilterQueue(struct nfq_q_handle *aNfQueueHandler, + struct nfgenmsg * aNfMsg, + struct nfq_data * aNfData) +{ + OTBR_UNUSED_VARIABLE(aNfMsg); + + struct nfqnl_msg_packet_hdr *ph; + unsigned char * data; + uint32_t id = 0; + int ret = 0; + int len = 0; + int verdict = NF_ACCEPT; + + Ip6Address dst; + Ip6Address src; + struct icmp6_hdr *icmp6header = nullptr; + struct ip6_hdr * ip6header = nullptr; + otbrError error = OTBR_ERROR_NONE; + + if ((ph = nfq_get_msg_packet_hdr(aNfData)) != nullptr) + { + id = ntohl(ph->packet_id); + otbrLogDebug("NdProxyManager: %s: id %d", __FUNCTION__, id); + } + + VerifyOrExit((len = nfq_get_payload(aNfData, &data)) > 0, error = OTBR_ERROR_PARSE); + + ip6header = reinterpret_cast(data); + src = *reinterpret_cast(&ip6header->ip6_src); + dst = *reinterpret_cast(&ip6header->ip6_dst); + + VerifyOrExit(ip6header->ip6_nxt == IPPROTO_ICMPV6); + + otbrLogDebug("NdProxyManager: Handle Neighbor Solicitation: from %s to %s", src.ToString().c_str(), + dst.ToString().c_str()); + + icmp6header = reinterpret_cast(data + sizeof(struct ip6_hdr)); + VerifyOrExit(icmp6header->icmp6_type == ND_NEIGHBOR_SOLICIT); + + VerifyOrExit(mNdProxySet.find(dst) != mNdProxySet.end(), error = OTBR_ERROR_NOT_FOUND); + + { + struct nd_neighbor_solicit &ns = *reinterpret_cast(data + sizeof(struct ip6_hdr)); + Ip6Address & target = *reinterpret_cast(&ns.nd_ns_target); + + otbrLogDebug("NdProxyManager: %s: target: %s, hoplimit %d", __FUNCTION__, target.ToString().c_str(), + ip6header->ip6_hlim); + VerifyOrExit(ip6header->ip6_hlim == 255, error = OTBR_ERROR_PARSE); + SendNeighborAdvertisement(target, src); + verdict = NF_DROP; + } + +exit: + ret = nfq_set_verdict(aNfQueueHandler, id, verdict, len, data); + + otbrLogResult(error, "NdProxyManager: %s (nfq_set_verdict id %d, ret %d verdict %d)", __FUNCTION__, id, ret, + verdict); + + return ret; +} + +void NdProxyManager::JoinSolicitedNodeMulticastGroup(const Ip6Address &aTarget) const +{ + ipv6_mreq mreq; + otbrError error = OTBR_ERROR_NONE; + Ip6Address solicitedMulticastAddress = aTarget.ToSolicitedNodeMulticastAddress(); + + mreq.ipv6mr_interface = mBackboneIfIndex; + solicitedMulticastAddress.CopyTo(mreq.ipv6mr_multiaddr); + + VerifyOrExit(setsockopt(mIcmp6RawSock, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq, sizeof(mreq)) == 0, + error = OTBR_ERROR_ERRNO); +exit: + otbrLogResult(error, "NdProxyManager: JoinSolicitedNodeMulticastGroup of %s: %s", aTarget.ToString().c_str(), + solicitedMulticastAddress.ToString().c_str()); +} + +void NdProxyManager::LeaveSolicitedNodeMulticastGroup(const Ip6Address &aTarget) const +{ + ipv6_mreq mreq; + otbrError error = OTBR_ERROR_NONE; + Ip6Address solicitedMulticastAddress = aTarget.ToSolicitedNodeMulticastAddress(); + + mreq.ipv6mr_interface = mBackboneIfIndex; + solicitedMulticastAddress.CopyTo(mreq.ipv6mr_multiaddr); + + VerifyOrExit(setsockopt(mIcmp6RawSock, IPPROTO_IPV6, IPV6_LEAVE_GROUP, &mreq, sizeof(mreq)) == 0, + error = OTBR_ERROR_ERRNO); +exit: + otbrLogResult(error, "NdProxyManager: LeaveSolicitedNodeMulticastGroup of %s: %s", aTarget.ToString().c_str(), + solicitedMulticastAddress.ToString().c_str()); +} + +} // namespace BackboneRouter +} // namespace otbr + +#endif // OTBR_ENABLE_DUA_ROUTING diff --git a/src/backbone_router/nd_proxy.hpp b/src/backbone_router/nd_proxy.hpp new file mode 100644 index 00000000000..e43a520e81f --- /dev/null +++ b/src/backbone_router/nd_proxy.hpp @@ -0,0 +1,183 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definition for ICMPv6 Neighbor Advertisement (ND) proxy management. + */ + +#ifndef ND_PROXY_HPP_ +#define ND_PROXY_HPP_ + +#if OTBR_ENABLE_DUA_ROUTING + +#ifdef __APPLE__ +#define __APPLE_USE_RFC_3542 +#endif + +#include +#include +#include +#include +#include +#include + +#include + +#include "agent/ncp_openthread.hpp" +#include "common/mainloop.hpp" +#include "common/types.hpp" + +namespace otbr { +namespace BackboneRouter { + +/** + * @addtogroup border-router-bbr + * + * @brief + * This module includes definition for ND Proxy manager. + * + * @{ + */ + +/** + * This class implements ND Proxy manager. + * + */ +class NdProxyManager : public MainloopProcessor +{ +public: + /** + * This constructor initializes a NdProxyManager instance. + * + */ + explicit NdProxyManager(otbr::Ncp::ControllerOpenThread &aNcp) + : mNcp(aNcp) + , mIcmp6RawSock(-1) + , mUnicastNsQueueSock(-1) + , mNfqHandler(nullptr) + , mNfqQueueHandler(nullptr) + { + } + + /** + * This method initializes a ND Proxy manager instance. + * + */ + void Init(void); + + /** + * This method enables the ND Proxy manager. + * + * @param[in] aDomainPrefix The Domain Prefix. + * + */ + void Enable(const Ip6Prefix &aDomainPrefix); + + /** + * This method disables the ND Proxy manager. + * + */ + void Disable(void); + + /** + * This method updates the mainloop context. + * + * @param[inout] aMainloop A reference to the mainloop to be updated. + * + */ + void Update(MainloopContext &aMainloop) override; + + /** + * This method processes mainloop events. + * + * @param[in] aMainloop A reference to the mainloop context. + * + */ + void Process(const MainloopContext &aMainloop) override; + + /** + * This method handles a Backbone Router ND Proxy event. + * + * @param[in] aEvent The Backbone Router ND Proxy event type. + * @param[in] aDua The Domain Unicast Address of the ND Proxy, or `nullptr` if @p `aEvent` is + * `OT_BACKBONE_ROUTER_NDPROXY_CLEARED`. + * + */ + void HandleBackboneRouterNdProxyEvent(otBackboneRouterNdProxyEvent aEvent, const otIp6Address *aDua); + + /** + * This method returns if the ND Proxy manager is enabled. + * + * @returns If the ND Proxy manager is enabled; + * + */ + bool IsEnabled(void) const { return mIcmp6RawSock >= 0; } + +private: + enum + { + kMaxICMP6PacketSize = 1500, ///< Max size of an ICMP6 packet in bytes. + }; + + void SendNeighborAdvertisement(const Ip6Address &aTarget, const Ip6Address &aDst); + otbrError UpdateMacAddress(void); + otbrError InitIcmp6RawSocket(void); + void FiniIcmp6RawSocket(void); + otbrError InitNetfilterQueue(void); + void FiniNetfilterQueue(void); + void ProcessMulticastNeighborSolicition(void); + void ProcessUnicastNeighborSolicition(void); + void JoinSolicitedNodeMulticastGroup(const Ip6Address &aTarget) const; + void LeaveSolicitedNodeMulticastGroup(const Ip6Address &aTarget) const; + static int HandleNetfilterQueue(struct nfq_q_handle *aNfQueueHandler, + struct nfgenmsg * aNfMsg, + struct nfq_data * aNfData, + void * aContext); + int HandleNetfilterQueue(struct nfq_q_handle *aNfQueueHandler, struct nfgenmsg *aNfMsg, struct nfq_data *aNfData); + + otbr::Ncp::ControllerOpenThread &mNcp; + std::set mNdProxySet; + uint32_t mBackboneIfIndex; + int mIcmp6RawSock; + int mUnicastNsQueueSock; + struct nfq_handle * mNfqHandler; ///< A pointer to an NFQUEUE handler. + struct nfq_q_handle * mNfqQueueHandler; ///< A pointer to a newly created queue. + MacAddress mMacAddress; + Ip6Prefix mDomainPrefix; +}; + +/** + * @} + */ + +} // namespace BackboneRouter +} // namespace otbr + +#endif // OTBR_ENABLE_DUA_ROUTING +#endif // ND_PROXY_HPP_ diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt index 3408c147efa..fc6ac31afdb 100644 --- a/src/common/CMakeLists.txt +++ b/src/common/CMakeLists.txt @@ -27,9 +27,22 @@ # add_library(otbr-common + byteswap.hpp + code_utils.hpp + dns_utils.cpp logging.cpp + logging.hpp + mainloop.hpp + task_runner.cpp + task_runner.hpp + time.hpp + tlv.hpp + types.cpp + types.hpp ) target_link_libraries(otbr-common PUBLIC otbr-config + openthread-ftd + openthread-posix ) diff --git a/src/common/byteswap.hpp b/src/common/byteswap.hpp index cec3de7fbb0..7c3485c2709 100644 --- a/src/common/byteswap.hpp +++ b/src/common/byteswap.hpp @@ -26,6 +26,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file defines bswap_* on macOS. + */ + #ifndef OTBR_COMMON_BYTESWAP_HPP_ #define OTBR_COMMON_BYTESWAP_HPP_ @@ -34,8 +39,29 @@ #define bswap_16 OSSwapInt16 #define bswap_32 OSSwapInt32 #define bswap_64 OSSwapInt64 + +#define htobe16(x) OSSwapHostToBigInt16(x) +#define htole16(x) OSSwapHostToLittleInt16(x) +#define be16toh(x) OSSwapBigToHostInt16(x) +#define le16toh(x) OSSwapLittleToHostInt16(x) + +#define htobe32(x) OSSwapHostToBigInt32(x) +#define htole32(x) OSSwapHostToLittleInt32(x) +#define be32toh(x) OSSwapBigToHostInt32(x) +#define le32toh(x) OSSwapLittleToHostInt32(x) + +#define htobe64(x) OSSwapHostToBigInt64(x) +#define htole64(x) OSSwapHostToLittleInt64(x) +#define be64toh(x) OSSwapBigToHostInt64(x) +#define le64toh(x) OSSwapLittleToHostInt64(x) #else #include + +#if defined(__linux__) +#include +#elif defined(__FreeBSD__) || defined(__NetBSD__) +#include +#endif #endif #endif // OTBR_COMMON_BYTESWAP_HPP_ diff --git a/src/common/code_utils.hpp b/src/common/code_utils.hpp index e374d55a668..682ab4843cf 100644 --- a/src/common/code_utils.hpp +++ b/src/common/code_utils.hpp @@ -26,9 +26,21 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes utility macros for coding. + */ #ifndef OTBR_COMMON_CODE_UTILS_HPP_ #define OTBR_COMMON_CODE_UTILS_HPP_ +#ifndef OTBR_LOG_TAG +#define OTBR_LOG_TAG "UTILS" +#endif + +#include + +#include "common/logging.hpp" + /** * This aligns the pointer to @p aAlignType. * @@ -65,6 +77,24 @@ } \ } while (false) +/** + * This macro verifies a given error status to be successful (compared against value zero (0)), otherwise, it emits a + * given error messages and exits the program. + * + * @param[in] aStatus A scalar error status to be evaluated against zero (0). + * @param[in] aMessage A message (text string) to print on failure. + * + */ +#define SuccessOrDie(aStatus, aMessage) \ + do \ + { \ + if ((aStatus) != 0) \ + { \ + otbrLogEmerg("FAILED %s:%d - %s", __FUNCTION__, __LINE__, aMessage); \ + exit(-1); \ + } \ + } while (false) + /** * This checks for the specified condition, which is expected to * commonly be true, and both executes @a ... and branches to the @@ -85,6 +115,24 @@ } \ } while (false) +/** + * This macro checks for the specified condition, which is expected to commonly be true, + * and both prints the message and terminates the program if the condition is false. + * + * @param[in] aCondition The condition to verify + * @param[in] aMessage A message (text string) to print on failure. + * + */ +#define VerifyOrDie(aCondition, aMessage) \ + do \ + { \ + if (!(aCondition)) \ + { \ + otbrLogEmerg("FAILED %s:%d - %s", __FUNCTION__, __LINE__, aMessage); \ + exit(-1); \ + } \ + } while (false) + /** * This unconditionally executes @a ... and branches to the local * label 'exit'. @@ -105,5 +153,6 @@ } while (false) #define OTBR_NOOP +#define OTBR_UNUSED_VARIABLE(variable) ((void)(variable)) #endif // OTBR_COMMON_CODE_UTILS_HPP_ diff --git a/src/common/dns_utils.cpp b/src/common/dns_utils.cpp new file mode 100644 index 00000000000..8a13169b2ec --- /dev/null +++ b/src/common/dns_utils.cpp @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "common/dns_utils.hpp" + +#include + +#include "common/code_utils.hpp" + +static bool NameEndsWithDot(const std::string &aName) +{ + return !aName.empty() && aName.back() == '.'; +} + +DnsNameInfo SplitFullDnsName(const std::string &aName) +{ + size_t transportPos; + DnsNameInfo nameInfo; + std::string fullName = aName; + + if (!NameEndsWithDot(fullName)) + { + fullName += '.'; + } + + transportPos = fullName.rfind("._udp."); + + if (transportPos == std::string::npos) + { + transportPos = fullName.rfind("._tcp."); + } + + if (transportPos == std::string::npos) + { + // host.domain or domain + size_t dotPos = fullName.find_first_of('.'); + + assert(dotPos != std::string::npos); + + // host.domain + nameInfo.mHostName = fullName.substr(0, dotPos); + nameInfo.mDomain = fullName.substr(dotPos + 1, fullName.length() - dotPos - 1); + } + else + { + // service or service instance + size_t dotPos = transportPos > 0 ? fullName.find_last_of('.', transportPos - 1) : std::string::npos; + + nameInfo.mDomain = fullName.substr(transportPos + 6); // 6 is the length of "._tcp." or "._udp." + + if (dotPos == std::string::npos) + { + // service.domain + nameInfo.mServiceName = fullName.substr(0, transportPos + 5); + } + else + { + // instance.service.domain + nameInfo.mInstanceName = fullName.substr(0, dotPos); + nameInfo.mServiceName = fullName.substr(dotPos + 1, transportPos + 4 - dotPos); + } + } + + if (!NameEndsWithDot(nameInfo.mDomain)) + { + nameInfo.mDomain += '.'; + } + + return nameInfo; +} + +otbrError SplitFullServiceInstanceName(const std::string &aFullName, + std::string & aInstanceName, + std::string & aType, + std::string & aDomain) +{ + otbrError error = OTBR_ERROR_NONE; + DnsNameInfo nameInfo = SplitFullDnsName(aFullName); + + VerifyOrExit(nameInfo.IsServiceInstance(), error = OTBR_ERROR_INVALID_ARGS); + + aInstanceName = std::move(nameInfo.mInstanceName); + aType = std::move(nameInfo.mServiceName); + aDomain = std::move(nameInfo.mDomain); + +exit: + return error; +} + +otbrError SplitFullServiceName(const std::string &aFullName, std::string &aType, std::string &aDomain) +{ + otbrError error = OTBR_ERROR_NONE; + DnsNameInfo nameInfo = SplitFullDnsName(aFullName); + + VerifyOrExit(nameInfo.IsService(), error = OTBR_ERROR_INVALID_ARGS); + + aType = std::move(nameInfo.mServiceName); + aDomain = std::move(nameInfo.mDomain); + +exit: + return error; +} + +otbrError SplitFullHostName(const std::string &aFullName, std::string &aHostName, std::string &aDomain) +{ + otbrError error = OTBR_ERROR_NONE; + DnsNameInfo nameInfo = SplitFullDnsName(aFullName); + + VerifyOrExit(nameInfo.IsHost(), error = OTBR_ERROR_INVALID_ARGS); + + aHostName = std::move(nameInfo.mHostName); + aDomain = std::move(nameInfo.mDomain); + +exit: + return error; +} diff --git a/src/common/dns_utils.hpp b/src/common/dns_utils.hpp new file mode 100644 index 00000000000..477294ad614 --- /dev/null +++ b/src/common/dns_utils.hpp @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes DNS utilities. + * + */ +#ifndef OTBR_COMMON_DNS_UTILS_HPP_ +#define OTBR_COMMON_DNS_UTILS_HPP_ + +#include "common/types.hpp" + +/** + * This structure represents DNS Name information. + * + * @sa SplitFullDnsName + * + */ +struct DnsNameInfo +{ + std::string mInstanceName; ///< Instance name, or empty if the DNS name is not a service instance. + std::string mServiceName; ///< Service name, or empty if the DNS name is not a service or service instance. + std::string mHostName; ///< Host name, or empty if the DNS name is not a host name. + std::string mDomain; ///< Domain name. + + /** + * This method returns if the DNS name is a service instance. + * + * @returns Whether the DNS name is a service instance. + * + */ + bool IsServiceInstance(void) const { return !mInstanceName.empty(); }; + + /** + * This method returns if the DNS name is a service. + * + * @returns Whether the DNS name is a service. + * + */ + bool IsService(void) const { return !mServiceName.empty() && mInstanceName.empty(); } + + /** + * This method returns if the DNS name is a host. + * + * @returns Whether the DNS name is a host. + * + */ + bool IsHost(void) const { return mServiceName.empty(); } +}; + +/** + * This method splits a full DNS name into name components. + * + * @param[in] aName The full DNS name to dissect. + * + * @returns A `DnsNameInfo` structure containing DNS name information. + * + * @sa DnsNameInfo + * + */ +DnsNameInfo SplitFullDnsName(const std::string &aName); + +/** + * This function splits a full service name into components. + * + * @param[in] aFullName The full service name to split. + * @param[out] aType A reference to a string to receive the service type. + * @param[out] aDomain A reference to a string to receive the domain. + * + * @retval OTBR_ERROR_NONE Successfully split the full service name. + * @retval OTBR_ERROR_INVALID_ARGS If the full service name is not valid. + * + */ +otbrError SplitFullServiceName(const std::string &aFullName, std::string &aType, std::string &aDomain); + +/** + * This function splits a full service instance name into components. + * + * @param[in] aFullName The full service instance name to split. + * @param[out] aInstanceName A reference to a string to receive the instance name. + * @param[out] aType A reference to a string to receive the service type. + * @param[out] aDomain A reference to a string to receive the domain. + * + * @retval OTBR_ERROR_NONE Successfully split the full service instance name. + * @retval OTBR_ERROR_INVALID_ARGS If the full service instance name is not valid. + * + */ +otbrError SplitFullServiceInstanceName(const std::string &aFullName, + std::string & aInstanceName, + std::string & aType, + std::string & aDomain); + +/** + * This function splits a full host name into components. + * + * @param[in] aFullName The full host name to split. + * @param[out] aHostName A reference to a string to receive the host name. + * @param[out] aDomain A reference to a string to receive the domain. + * + * @retval OTBR_ERROR_NONE Successfully split the full host name. + * @retval OTBR_ERROR_INVALID_ARGS If the full host name is not valid. + * + */ +otbrError SplitFullHostName(const std::string &aFullName, std::string &aHostName, std::string &aDomain); + +#endif // OTBR_COMMON_DNS_UTILS_HPP_ diff --git a/src/common/logging.cpp b/src/common/logging.cpp index bf1e8a1a1de..78787826d31 100644 --- a/src/common/logging.cpp +++ b/src/common/logging.cpp @@ -26,6 +26,8 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#define OTBR_LOG_TAG "LOG" + #include "common/logging.hpp" #include @@ -38,49 +40,89 @@ #include #include +#include + +#include "common/code_utils.hpp" #include "common/time.hpp" -static int sLevel = LOG_INFO; +static otbrLogLevel sLevel = OTBR_LOG_INFO; +static const char sLevelString[][8] = { + "[EMERG]", "[ALERT]", "[CRIT]", "[ERR ]", "[WARN]", "[NOTE]", "[INFO]", "[DEBG]", +}; /** Get the current debug log level */ -int otbrLogGetLevel(void) +otbrLogLevel otbrLogGetLevel(void) { return sLevel; } /** Initialize logging */ -void otbrLogInit(const char *aIdent, int aLevel, bool aPrintStderr) +void otbrLogInit(const char *aIdent, otbrLogLevel aLevel, bool aPrintStderr) { assert(aIdent); - assert(aLevel >= LOG_EMERG && aLevel <= LOG_DEBUG); + assert(aLevel >= OTBR_LOG_EMERG && aLevel <= OTBR_LOG_DEBUG); openlog(aIdent, (LOG_CONS | LOG_PID) | (aPrintStderr ? LOG_PERROR : 0), LOG_USER); sLevel = aLevel; } +static const char *GetPrefix(const char *aLogTag) +{ + // Log prefix format : -xxx----- + const uint8_t kMaxTagSize = 7; + const uint8_t kBufferSize = kMaxTagSize + 3; + static char prefix[kBufferSize]; + uint8_t tagLength = strlen(aLogTag) > kMaxTagSize ? kMaxTagSize : strlen(aLogTag); + int index = 0; + + if (strlen(aLogTag) > 0) + { + prefix[0] = '-'; + memcpy(&prefix[1], aLogTag, tagLength); + + index = tagLength + 1; + + memset(&prefix[index], '-', kMaxTagSize - tagLength + 1); + index += kMaxTagSize - tagLength + 1; + } + + prefix[index++] = '\0'; + + return prefix; +} + /** log to the syslog or log file */ -void otbrLog(int aLevel, const char *aFormat, ...) +void otbrLog(otbrLogLevel aLevel, const char *aLogTag, const char *aFormat, ...) { - va_list ap; + const uint16_t kBufferSize = 1024; + va_list ap; + char buffer[kBufferSize]; va_start(ap, aFormat); - otbrLogv(aLevel, aFormat, ap); + + if ((aLevel <= sLevel) && (vsnprintf(buffer, sizeof(buffer), aFormat, ap) > 0)) + { + syslog(static_cast(aLevel), "%s%s: %s", sLevelString[aLevel], GetPrefix(aLogTag), buffer); + } + va_end(ap); + + return; } /** log to the syslog or log file */ -void otbrLogv(int aLevel, const char *aFormat, va_list ap) +void otbrLogv(otbrLogLevel aLevel, const char *aFormat, va_list ap) { assert(aFormat); if (aLevel <= sLevel) { - vsyslog(aLevel, aFormat, ap); + vsyslog(static_cast(aLevel), aFormat, ap); } } /** Hex dump data to the log */ -void otbrDump(int aLevel, const char *aPrefix, const void *aMemory, size_t aSize) +void otbrDump(otbrLogLevel aLevel, const char *aPrefix, const void *aMemory, size_t aSize) { static const char kHexChars[] = "0123456789abcdef"; assert(aPrefix && (aMemory || aSize == 0)); @@ -126,13 +168,13 @@ void otbrDump(int aLevel, const char *aPrefix, const void *aMemory, size_t aSize } *ch = 0; - syslog(aLevel, "%s: %04x: %s", aPrefix, addr, hex); + syslog(static_cast(aLevel), "%s: %04x: %s", aPrefix, addr, hex); } } const char *otbrErrorString(otbrError aError) { - const char *error = nullptr; + const char *error; switch (aError) { @@ -156,18 +198,29 @@ const char *otbrErrorString(otbrError aError) error = "OpenThread error"; break; + case OTBR_ERROR_NOT_FOUND: + error = "Not found"; + break; + + case OTBR_ERROR_PARSE: + error = "Parse error"; + break; + + case OTBR_ERROR_NOT_IMPLEMENTED: + error = "Not implemented"; + break; + + case OTBR_ERROR_INVALID_ARGS: + error = "Invalid arguments"; + break; + default: - assert(false); + error = "Unknown"; } return error; } -void otbrLogResult(const char *aAction, otbrError aError) -{ - otbrLog((aError == OTBR_ERROR_NONE ? OTBR_LOG_INFO : OTBR_LOG_WARNING), "%s: %s", aAction, otbrErrorString(aError)); -} - void otbrLogDeinit(void) { closelog(); diff --git a/src/common/logging.hpp b/src/common/logging.hpp index d65887b2429..115f25de5e9 100644 --- a/src/common/logging.hpp +++ b/src/common/logging.hpp @@ -26,6 +26,10 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file define logging interface. + */ #ifndef OTBR_COMMON_LOGGING_HPP_ #define OTBR_COMMON_LOGGING_HPP_ @@ -34,13 +38,17 @@ #include #include +#ifndef OTBR_LOG_TAG +#error "OTBR_LOG_TAG is not defined" +#endif + #include "common/types.hpp" /** - * Logging level, which is identical to syslog + * Logging level. * */ -enum +typedef enum { OTBR_LOG_EMERG, /* system is unusable */ OTBR_LOG_ALERT, /* action must be taken immediately */ @@ -50,12 +58,12 @@ enum OTBR_LOG_NOTICE, /* normal but significant condition */ OTBR_LOG_INFO, /* informational */ OTBR_LOG_DEBUG, /* debug-level messages */ -}; +} otbrLogLevel; /** * Get current log level */ -int otbrLogGetLevel(void); +otbrLogLevel otbrLogGetLevel(void); /** * Control log to syslog @@ -73,28 +81,17 @@ void otbrLogEnableSyslog(bool aEnabled); * @param[in] aPrintStderr Whether to log to stderr. * */ -void otbrLogInit(const char *aIdent, int aLevel, bool aPrintStderr); +void otbrLogInit(const char *aIdent, otbrLogLevel aLevel, bool aPrintStderr); /** * This function log at level @p aLevel. * - * @param[in] aLevel Log level of the logger. - * @param[in] aFormat Format string as in printf. - * - */ -void otbrLog(int aLevel, const char *aFormat, ...); - -/** - * This function log a action result according to @p aError. - * - * If @p aError is OTBR_ERROR_NONE, the log level will be OTBR_LOG_INFO, - * otherwise OTBR_LOG_WARNING. - * - * @param[in] aAction The action description. - * @param[in] aError The action result. + * @param[in] aLevel Log level of the logger. + * @param[in] aLogTag Log tag. + * @param[in] aFormat Format string as in printf. * */ -void otbrLogResult(const char *aAction, otbrError aError); +void otbrLog(otbrLogLevel aLevel, const char *aLogTag, const char *aFormat, ...); /** * This function log at level @p aLevel. @@ -103,7 +100,7 @@ void otbrLogResult(const char *aAction, otbrError aError); * @param[in] aFormat Format string as in printf. * */ -void otbrLogv(int aLevel, const char *aFormat, va_list); +void otbrLogv(otbrLogLevel aLevel, const char *aFormat, va_list); /** * This function dump memory as hex string at level @p aLevel. @@ -114,7 +111,7 @@ void otbrLogv(int aLevel, const char *aFormat, va_list); * @param[in] aSize The size of memory in bytes to be dumped. * */ -void otbrDump(int aLevel, const char *aPrefix, const void *aMemory, size_t aSize); +void otbrDump(otbrLogLevel aLevel, const char *aPrefix, const void *aMemory, size_t aSize); /** * This function converts error code to string. @@ -132,4 +129,103 @@ const char *otbrErrorString(otbrError aError); */ void otbrLogDeinit(void); +/** + * This macro log an action result according to @p aError. + * + * If @p aError is OTBR_ERROR_NONE, the log level will be OTBR_LOG_INFO, + * otherwise OTBR_LOG_WARNING. + * + * @param[in] aError The action result. + * @param[in] aFormat Format string as in printf. + * @param[in] ... Arguments for the format specification. + * + */ +#define otbrLogResult(aError, aFormat, ...) \ + do \ + { \ + otbrError _err = (aError); \ + otbrLog(_err == OTBR_ERROR_NONE ? OTBR_LOG_INFO : OTBR_LOG_WARNING, OTBR_LOG_TAG, aFormat ": %s", \ + ##__VA_ARGS__, otbrErrorString(_err)); \ + } while (0) + +/** + * @def otbrLogEmerg + * + * Log at level emergency. + * + * @param[in] ... Arguments for the format specification. + * + */ + +/** + * @def otbrLogAlert + * + * Log at level alert. + * + * @param[in] ... Arguments for the format specification. + * + */ + +/** + * @def otbrLogCrit + * + * Log at level critical. + * + * @param[in] ... Arguments for the format specification. + * + */ + +/** + * @def otbrLogErr + * + * Log at level error. + * + * @param[in] ... Arguments for the format specification. + * + */ + +/** + * @def otbrLogWarning + * + * Log at level warning. + * + * @param[in] ... Arguments for the format specification. + * + */ + +/** + * @def otbrLogNotice + * + * Log at level notice. + * + * @param[in] ... Arguments for the format specification. + * + */ + +/** + * @def otbrLogInfo + * + * Log at level information. + * + * @param[in] ... Arguments for the format specification. + * + */ + +/** + * @def otbrLogDebug + * + * Log at level debug. + * + * @param[in] ... Arguments for the format specification. + * + */ +#define otbrLogEmerg(...) otbrLog(OTBR_LOG_EMERG, OTBR_LOG_TAG, __VA_ARGS__) +#define otbrLogAlert(...) otbrLog(OTBR_LOG_ALERT, OTBR_LOG_TAG, __VA_ARGS__) +#define otbrLogCrit(...) otbrLog(OTBR_LOG_CRIT, OTBR_LOG_TAG, __VA_ARGS__) +#define otbrLogErr(...) otbrLog(OTBR_LOG_ERR, OTBR_LOG_TAG, __VA_ARGS__) +#define otbrLogWarning(...) otbrLog(OTBR_LOG_WARNING, OTBR_LOG_TAG, __VA_ARGS__) +#define otbrLogNotice(...) otbrLog(OTBR_LOG_NOTICE, OTBR_LOG_TAG, __VA_ARGS__) +#define otbrLogInfo(...) otbrLog(OTBR_LOG_INFO, OTBR_LOG_TAG, __VA_ARGS__) +#define otbrLogDebug(...) otbrLog(OTBR_LOG_DEBUG, OTBR_LOG_TAG, __VA_ARGS__) + #endif // OTBR_COMMON_LOGGING_HPP_ diff --git a/src/common/mainloop.h b/src/common/mainloop.hpp similarity index 68% rename from src/common/mainloop.h rename to src/common/mainloop.hpp index c9c0af47880..73b36d81d2c 100644 --- a/src/common/mainloop.h +++ b/src/common/mainloop.hpp @@ -34,32 +34,46 @@ #ifndef OTBR_COMMON_OTBR_MAINLOOP_HPP_ #define OTBR_COMMON_OTBR_MAINLOOP_HPP_ -#include "openthread-br/config.h" +#include #include -#else -#include +namespace otbr { -#ifdef __cplusplus -extern "C" { -#endif +/** + * This type defines the mainloop context that contains context data + * for running a mainloop. + * + */ +using MainloopContext = otSysMainloopContext; /** - * This structure represents a context for a select() based mainloop. + * This abstract class defines the interface of a mainloop processor + * which add fds to the mainloop context and handle fd events. * */ -typedef struct otSysMainloopContext +class MainloopProcessor { - fd_set mReadFdSet; ///< The read file descriptors. - fd_set mWriteFdSet; ///< The write file descriptors. - fd_set mErrorFdSet; ///< The error file descriptors. - int mMaxFd; ///< The max file descriptor. - struct timeval mTimeout; ///< The timeout. -} otSysMainloopContext; +public: + virtual ~MainloopProcessor(void) = default; + + /** + * This method updates the mainloop context. + * + * @param[inout] aMainloop A reference to the mainloop to be updated. + * + */ + virtual void Update(MainloopContext &aMainloop) = 0; + + /** + * This method processes mainloop events. + * + * @param[in] aMainloop A reference to the mainloop context. + * + */ + virtual void Process(const MainloopContext &aMainloop) = 0; +}; -#ifdef __cplusplus -} // end of extern "C" -#endif +} // namespace otbr #endif // OTBR_COMMON_OTBR_MAINLOOP_HPP_ diff --git a/src/common/task_runner.cpp b/src/common/task_runner.cpp new file mode 100644 index 00000000000..91924512ab8 --- /dev/null +++ b/src/common/task_runner.cpp @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file implements the Task Runner that executes tasks on the mainloop. + */ + +#include "common/task_runner.hpp" + +#include + +#include +#include + +#include "common/code_utils.hpp" + +namespace otbr { + +TaskRunner::TaskRunner(void) + : mTaskQueue(DelayedTask::Comparator{}) +{ + int flags; + + // We do not handle failures when creating a pipe, simply die. + VerifyOrDie(pipe(mEventFd) != -1, strerror(errno)); + + flags = fcntl(mEventFd[kRead], F_GETFL, 0); + VerifyOrDie(fcntl(mEventFd[kRead], F_SETFL, flags | O_NONBLOCK) != -1, strerror(errno)); + flags = fcntl(mEventFd[kWrite], F_GETFL, 0); + VerifyOrDie(fcntl(mEventFd[kWrite], F_SETFL, flags | O_NONBLOCK) != -1, strerror(errno)); +} + +TaskRunner::~TaskRunner(void) +{ + if (mEventFd[kRead] != -1) + { + close(mEventFd[kRead]); + mEventFd[kRead] = -1; + } + if (mEventFd[kWrite] != -1) + { + close(mEventFd[kWrite]); + mEventFd[kWrite] = -1; + } +} + +void TaskRunner::Post(const Task aTask) +{ + Post(Milliseconds::zero(), std::move(aTask)); +} + +void TaskRunner::Post(Milliseconds aDelay, const Task aTask) +{ + PushTask(aDelay, std::move(aTask)); +} + +void TaskRunner::Update(MainloopContext &aMainloop) +{ + FD_SET(mEventFd[kRead], &aMainloop.mReadFdSet); + aMainloop.mMaxFd = std::max(mEventFd[kRead], aMainloop.mMaxFd); + + if (!mTaskQueue.empty()) + { + auto now = Clock::now(); + auto &task = mTaskQueue.top(); + auto delay = std::chrono::duration_cast(task.GetTimeExecute() - now); + auto timeout = FromTimeval(aMainloop.mTimeout); + + if (task.GetTimeExecute() < now) + { + delay = Microseconds::zero(); + } + + if (delay <= timeout) + { + aMainloop.mTimeout.tv_sec = delay.count() / 1000000; + aMainloop.mTimeout.tv_usec = delay.count() % 1000000; + } + } +} + +void TaskRunner::Process(const MainloopContext &aMainloop) +{ + OTBR_UNUSED_VARIABLE(aMainloop); + + ssize_t rval; + + // Read any data in the pipe. + do + { + uint8_t n; + + rval = read(mEventFd[kRead], &n, sizeof(n)); + } while (rval > 0 || (rval == -1 && errno == EINTR)); + + // Critical error happens, simply die. + VerifyOrDie(errno == EAGAIN || errno == EWOULDBLOCK, strerror(errno)); + + PopTasks(); +} + +void TaskRunner::PushTask(Milliseconds aDelay, const Task aTask) +{ + ssize_t rval; + const uint8_t kOne = 1; + std::lock_guard _(mTaskQueueMutex); + + mTaskQueue.emplace(aDelay, std::move(aTask)); + do + { + rval = write(mEventFd[kWrite], &kOne, sizeof(kOne)); + } while (rval == -1 && errno == EINTR); + + VerifyOrExit(rval == -1); + + // Critical error happens, simply die. + VerifyOrDie(errno == EAGAIN || errno == EWOULDBLOCK, strerror(errno)); + + // We are blocked because there are already data (written by other concurrent callers in + // different threads) in the pipe, and the mEventFd[kRead] should be readable now. + otbrLogWarning("Failed to write fd %d: %s", mEventFd[kWrite], strerror(errno)); + +exit: + return; +} + +void TaskRunner::PopTasks(void) +{ + while (true) + { + Task task; + + // The braces here are necessary for auto-releasing of the mutex. + { + std::lock_guard _(mTaskQueueMutex); + + if (!mTaskQueue.empty() && mTaskQueue.top().GetTimeExecute() <= Clock::now()) + { + task = std::move(mTaskQueue.top().mTask); + mTaskQueue.pop(); + } + else + { + break; + } + } + + task(); + } +} + +} // namespace otbr diff --git a/src/common/task_runner.hpp b/src/common/task_runner.hpp new file mode 100644 index 00000000000..3aad74a2796 --- /dev/null +++ b/src/common/task_runner.hpp @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file defines the Task Runner that executes tasks on the mainloop. + */ + +#ifndef OTBR_COMMON_TASK_RUNNER_HPP_ +#define OTBR_COMMON_TASK_RUNNER_HPP_ + +#include + +#include +#include +#include +#include +#include + +#include "common/mainloop.hpp" +#include "common/time.hpp" + +namespace otbr { + +/** + * This class implements the Task Runner that executes + * tasks on the mainloop. + * + */ +class TaskRunner : public MainloopProcessor +{ +public: + /** + * This type represents the generic executable task. + * + */ + template using Task = std::function; + + /** + * This constructor initializes the Task Runner instance. + * + */ + TaskRunner(void); + + /** + * This destructor destroys the Task Runner instance. + * + */ + ~TaskRunner(void) override; + + /** + * This method posts a task to the task runner and returns immediately. + * + * Tasks are executed sequentially and follow the First-Come-First-Serve rule. + * It is safe to call this method in different threads concurrently. + * + * @param[in] aTask The task to be executed. + * + */ + void Post(const Task aTask); + + /** + * This method posts a task to the task runner and returns immediately. + * + * The task will be executed on the mainloop after `aDelay` milliseconds from now. + * + * @param[in] aDelay The delay before executing the task (in milliseconds). + * @param[in] aTask The task to be executed. + * + */ + void Post(Milliseconds aDelay, const Task aTask); + + /** + * This method posts a task and waits for the completion of the task. + * + * Tasks are executed sequentially and follow the First-Come-First-Serve rule. + * This method must be called in a thread other than the mainloop thread. Otherwise, + * the caller will be blocked forever. + * + * @returns The result returned by the task @p aTask. + * + */ + template T PostAndWait(const Task &aTask) + { + std::promise pro; + + Post([&]() { pro.set_value(aTask()); }); + + return pro.get_future().get(); + } + + /** + * This method updates the mainloop context. + * + * @param[inout] aMainloop A reference to the mainloop to be updated. + * + */ + void Update(MainloopContext &aMainloop) override; + + /** + * This method processes mainloop events. + * + * @param[in] aMainloop A reference to the mainloop context. + * + */ + void Process(const MainloopContext &aMainloop) override; + +private: + enum + { + kRead = 0, + kWrite = 1, + }; + + struct DelayedTask + { + friend class Comparator; + + struct Comparator + { + bool operator()(const DelayedTask &aLhs, const DelayedTask &aRhs) const { return aRhs < aLhs; } + }; + + DelayedTask(Milliseconds aDelay, Task aTask) + : mTimeCreated(Clock::now()) + , mDelay(aDelay) + , mTask(std::move(aTask)) + { + } + + bool operator<(const DelayedTask &aOther) const + { + return GetTimeExecute() <= aOther.GetTimeExecute() || + (GetTimeExecute() == aOther.GetTimeExecute() && mTimeCreated < aOther.mTimeCreated); + } + + Timepoint GetTimeExecute(void) const { return mTimeCreated + mDelay; } + + Timepoint mTimeCreated; + Milliseconds mDelay; + Task mTask; + }; + + void PushTask(Milliseconds aDelay, const Task aTask); + void PopTasks(void); + + // The event fds which are used to wakeup the mainloop + // when there are pending tasks in the task queue. + int mEventFd[2]; + + std::priority_queue, DelayedTask::Comparator> mTaskQueue; + + // The mutex which protects the `mTaskQueue` from being + // simultaneously accessed by multiple threads. + std::mutex mTaskQueueMutex; +}; + +} // namespace otbr + +#endif // OTBR_COMMON_TASK_RUNNER_HPP_ diff --git a/src/common/time.hpp b/src/common/time.hpp index d15b249cd52..1bd55a854d2 100644 --- a/src/common/time.hpp +++ b/src/common/time.hpp @@ -26,42 +26,46 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definitions for time functions. + */ + #ifndef OTBR_COMMON_TIME_HPP_ #define OTBR_COMMON_TIME_HPP_ #include "openthread-br/config.h" +#include + #include #include namespace otbr { -/** - * This method returns the timestamp in miniseconds of @aTime. - * - * @param[in] aTime The time to convert to timestamp. - * - * @returns timestamp in miniseconds. - * - */ -inline unsigned long GetTimestamp(const timeval &aTime) +using Seconds = std::chrono::seconds; +using Milliseconds = std::chrono::milliseconds; +using Microseconds = std::chrono::microseconds; +using Clock = std::chrono::steady_clock; +using Timepoint = Clock::time_point; + +template D FromTimeval(const timeval &aTime) { - return static_cast(aTime.tv_sec * 1000 + aTime.tv_usec / 1000); + return std::chrono::duration_cast(Microseconds{aTime.tv_usec}) + + std::chrono::duration_cast(Seconds{aTime.tv_sec}); } -/** - * This method returns the current timestamp in miniseconds. - * - * @returns Current timestamp in miniseconds. - * - */ -inline unsigned long GetNow(void) +template timeval ToTimeval(const D &aDuration) { - timeval now; + timeval ret; + const size_t kMicrosecondsPeriod = 1000000; + auto microseconds = std::chrono::duration_cast(aDuration).count(); + + ret.tv_sec = microseconds / kMicrosecondsPeriod; + ret.tv_usec = microseconds % kMicrosecondsPeriod; - gettimeofday(&now, nullptr); - return static_cast(now.tv_sec * 1000 + now.tv_usec / 1000); + return ret; } } // namespace otbr diff --git a/src/common/types.cpp b/src/common/types.cpp new file mode 100644 index 00000000000..a41bb4140a8 --- /dev/null +++ b/src/common/types.cpp @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include + +#include "common/code_utils.hpp" +#include "common/logging.hpp" +#include "common/types.hpp" + +namespace otbr { + +Ip6Address::Ip6Address(const uint8_t (&aAddress)[16]) +{ + memcpy(m8, aAddress, sizeof(m8)); +} + +std::string Ip6Address::ToString() const +{ + char strbuf[INET6_ADDRSTRLEN]; + + VerifyOrDie(inet_ntop(AF_INET6, this->m8, strbuf, sizeof(strbuf)) != nullptr, + "Failed to convert Ip6 address to string"); + + return std::string(strbuf); +} + +Ip6Address Ip6Address::ToSolicitedNodeMulticastAddress(void) const +{ + Ip6Address ma(Ip6Address::GetSolicitedMulticastAddressPrefix()); + + ma.m8[13] = m8[13]; + ma.m8[14] = m8[14]; + ma.m8[15] = m8[15]; + + return ma; +} + +void Ip6Address::CopyTo(struct sockaddr_in6 &aSockAddr) const +{ + memset(&aSockAddr, 0, sizeof(aSockAddr)); + CopyTo(aSockAddr.sin6_addr); + aSockAddr.sin6_family = AF_INET6; +} + +void Ip6Address::CopyFrom(const struct sockaddr_in6 &aSockAddr) +{ + CopyFrom(aSockAddr.sin6_addr); +} + +void Ip6Address::CopyTo(struct in6_addr &aIn6Addr) const +{ + static_assert(sizeof(m8) == sizeof(aIn6Addr.s6_addr), "invalid IPv6 address size"); + memcpy(aIn6Addr.s6_addr, m8, sizeof(aIn6Addr.s6_addr)); +} + +void Ip6Address::CopyFrom(const struct in6_addr &aIn6Addr) +{ + static_assert(sizeof(m8) == sizeof(aIn6Addr.s6_addr), "invalid IPv6 address size"); + memcpy(m8, aIn6Addr.s6_addr, sizeof(aIn6Addr.s6_addr)); +} + +otbrError Ip6Address::FromString(const char *aStr, Ip6Address &aAddr) +{ + int ret; + + ret = inet_pton(AF_INET6, aStr, &aAddr.m8); + + return ret == 1 ? OTBR_ERROR_NONE : OTBR_ERROR_INVALID_ARGS; +} + +Ip6Address Ip6Address::FromString(const char *aStr) +{ + Ip6Address addr; + + SuccessOrDie(FromString(aStr, addr), "inet_pton failed"); + + return addr; +} + +void Ip6Prefix::Set(const otIp6Prefix &aPrefix) +{ + memcpy(reinterpret_cast(this), &aPrefix, sizeof(*this)); +} + +std::string Ip6Prefix::ToString() const +{ + std::stringbuf strBuilder; + char strbuf[INET6_ADDRSTRLEN]; + + VerifyOrDie(inet_ntop(AF_INET6, mPrefix.m8, strbuf, sizeof(strbuf)) != nullptr, + "Failed to convert Ip6 prefix to string"); + + strBuilder.sputn(strbuf, strlen(strbuf)); + strBuilder.sputc('/'); + + sprintf(strbuf, "%d", mLength); + strBuilder.sputn(strbuf, strlen(strbuf)); + + return strBuilder.str(); +} + +std::string MacAddress::ToString(void) const +{ + char strbuf[sizeof(m8) * 3]; + + snprintf(strbuf, sizeof(strbuf), "%02x:%02x:%02x:%02x:%02x:%02x", m8[0], m8[1], m8[2], m8[3], m8[4], m8[5]); + + return std::string(strbuf); +} + +} // namespace otbr diff --git a/src/common/types.hpp b/src/common/types.hpp index 99987cad7bc..ddc5443e19e 100644 --- a/src/common/types.hpp +++ b/src/common/types.hpp @@ -36,10 +36,14 @@ #include "openthread-br/config.h" +#include #include +#include #include #include +#include "common/byteswap.hpp" + #ifndef IN6ADDR_ANY /** * Any IPv6 address literal. @@ -53,6 +57,12 @@ #define OTBR_MASTER_KEY_SIZE 16 #define OTBR_PSKC_SIZE 16 +/** + * Forward declaration for otIp6Prefix to avoid including + * + */ +struct otIp6Prefix; + /** * This enumeration represents error codes used throughout OpenThread Border Router. */ @@ -60,23 +70,33 @@ enum otbrError { OTBR_ERROR_NONE = 0, ///< No error. - OTBR_ERROR_ERRNO = -1, ///< Error defined by errno. - OTBR_ERROR_DBUS = -2, ///< DBus error. - OTBR_ERROR_MDNS = -3, ///< MDNS error. - OTBR_ERROR_OPENTHREAD = -4, ///< OpenThread error. - OTBR_ERROR_REST = -5 ///< Rest Server error. + OTBR_ERROR_ERRNO = -1, ///< Error defined by errno. + OTBR_ERROR_DBUS = -2, ///< DBus error. + OTBR_ERROR_MDNS = -3, ///< MDNS error. + OTBR_ERROR_OPENTHREAD = -4, ///< OpenThread error. + OTBR_ERROR_REST = -5, ///< Rest Server error. + OTBR_ERROR_SMCROUTE = -6, ///< SMCRoute error. + OTBR_ERROR_NOT_FOUND = -7, ///< Not found. + OTBR_ERROR_PARSE = -8, ///< Parse error. + OTBR_ERROR_NOT_IMPLEMENTED = -9, ///< Not implemented error. + OTBR_ERROR_INVALID_ARGS = -10, ///< Invalid arguments error. + OTBR_ERROR_DUPLICATED = -11, ///< Duplicated operation, resource or name. }; namespace otbr { enum { - kSizePSKc = 16, ///< Size of PSKc. - kSizeNetworkName = 16, ///< Max size of Network Name. - kSizeExtPanId = 8, ///< Size of Extended PAN ID. - kSizeEui64 = 8, ///< Size of Eui64. + kSizePSKc = 16, ///< Size of PSKc. + kSizeNetworkName = 16, ///< Max size of Network Name. + kSizeExtPanId = 8, ///< Size of Extended PAN ID. + kSizeEui64 = 8, ///< Size of Eui64. + kSizeExtAddr = kSizeEui64, ///< Size of Extended Address. }; +static constexpr char kSolicitedMulticastAddressPrefix[] = "ff02::01:ff00:0"; +static constexpr char kLinkLocalAllNodesMulticastAddress[] = "ff02::01"; + /** * This class implements the Ipv6 address functionality. * @@ -109,6 +129,34 @@ class Ip6Address m8[15] = aLocator & 0xff; } + /** + * Constructor with an Ip6 address. + * + * @param[in] aAddress The Ip6 address. + * + */ + Ip6Address(const uint8_t (&aAddress)[16]); + + /** + * This method overloads `<` operator and compares if the Ip6 address is smaller than the other address. + * + * @param[in] aOther The other Ip6 address to compare with. + * + * @returns Whether the Ip6 address is smaller than the other address. + * + */ + bool operator<(const Ip6Address &aOther) const { return memcmp(this, &aOther, sizeof(Ip6Address)) < 0; } + + /** + * This method overloads `==` operator and compares if the Ip6 address is equal to the other address. + * + * @param[in] aOther The other Ip6 address to compare with. + * + * @returns Whether the Ip6 address is equal to the other address. + * + */ + bool operator==(const Ip6Address &aOther) const { return m64[0] == aOther.m64[0] && m64[1] == aOther.m64[1]; } + /** * Retrieve the 16-bit Thread locator. * @@ -117,6 +165,126 @@ class Ip6Address */ uint16_t ToLocator(void) const { return static_cast(m8[14] << 8 | m8[15]); } + /** + * This method returns the solicited node multicast address. + * + * @returns The solicited node multicast address. + * + */ + Ip6Address ToSolicitedNodeMulticastAddress(void) const; + + /** + * This method returns the string representation for the Ip6 address. + * + * @returns The string representation of the Ip6 address. + * + */ + std::string ToString(void) const; + + /** + * This method indicates whether or not the Ip6 address is the Unspecified Address. + * + * @retval TRUE If the Ip6 address is the Unspecified Address. + * @retval FALSE If the Ip6 address is not the Unspecified Address. + * + */ + bool IsUnspecified(void) const { return m64[0] == 0 && m64[1] == 0; } + + /** + * This method returns if the Ip6 address is a multicast address. + * + * @returns Whether the Ip6 address is a multicast address. + * + */ + bool IsMulticast(void) const { return m8[0] == 0xff; } + + /** + * This method returns if the Ip6 address is a link-local address. + * + * @returns Whether the Ip6 address is a link-local address. + * + */ + bool IsLinkLocal(void) const { return (m16[0] & bswap_16(0xffc0)) == bswap_16(0xfe80); } + + /** + * This method returns whether or not the Ip6 address is the Loopback Address. + * + * @retval TRUE If the Ip6 address is the Loopback Address. + * @retval FALSE If the Ip6 address is not the Loopback Address. + * + */ + bool IsLoopback(void) const { return (m32[0] == 0 && m32[1] == 0 && m32[2] == 0 && m32[3] == htobe32(1)); } + + /** + * This function returns the wellknown Link Local All Nodes Multicast Address (ff02::1). + * + * @returns The Link Local All Nodes Multicast Address. + * + */ + static const Ip6Address &GetLinkLocalAllNodesMulticastAddress(void) + { + static Ip6Address sLinkLocalAllNodesMulticastAddress = FromString(kLinkLocalAllNodesMulticastAddress); + + return sLinkLocalAllNodesMulticastAddress; + } + + /** + * This function returns the wellknown Solicited Node Multicast Address Prefix (ff02::01:ff00:0). + * + * @returns The Solicited Node Multicast Address Prefix. + * + */ + static const Ip6Address &GetSolicitedMulticastAddressPrefix(void) + { + static Ip6Address sSolicitedMulticastAddressPrefix = FromString(kSolicitedMulticastAddressPrefix); + + return sSolicitedMulticastAddressPrefix; + } + + /** + * This function converts Ip6 addresses from text to `Ip6Address`. + * + * @param[in] aStr The Ip6 address text. + * @param[out] aAddr A reference to `Ip6Address` to output the Ip6 address. + * + * @retval OTBR_ERROR_NONE If the Ip6 address was successfully converted. + * @retval OTBR_ERROR_INVALID_ARGS If @p `aStr` is not a valid string representing of Ip6 address. + * + */ + static otbrError FromString(const char *aStr, Ip6Address &aAddr); + + /** + * This method copies the Ip6 address to a `sockaddr_in6` structure. + * + * @param[out] aSockAddr The `sockaddr_in6` structure to copy the Ip6 address to. + * + */ + void CopyTo(struct sockaddr_in6 &aSockAddr) const; + + /** + * This method copies the Ip6 address from a `sockaddr_in6` structure. + * + * @param[in] aSockAddr The `sockaddr_in6` structure to copy the Ip6 address from. + * + */ + void CopyFrom(const struct sockaddr_in6 &aSockAddr); + + /** + * This method copies the Ip6 address to a `in6_addr` structure. + * + * @param[out] aIn6Addr The `in6_addr` structure to copy the Ip6 address to. + * + */ + void CopyTo(struct in6_addr &aIn6Addr) const; + + /** + * This method copies the Ip6 address from a `in6_addr` structure. + * + * @param[in] aIn6Addr The `in6_addr` structure to copy the Ip6 address from. + * + */ + void CopyFrom(const struct in6_addr &aIn6Addr); + union { uint8_t m8[16]; @@ -124,6 +292,88 @@ class Ip6Address uint32_t m32[4]; uint64_t m64[2]; }; + +private: + static Ip6Address FromString(const char *aStr); +}; + +/** + * This class represents a Ipv6 prefix. + * + */ +class Ip6Prefix +{ +public: + /** + * Default constructor. + * + */ + Ip6Prefix(void) { Clear(); } + + /** + * This method sets the Ip6 prefix to an `otIp6Prefix` value. + * + * @param[in] aPrefix The `otIp6Prefix` value to set the Ip6 prefix. + * + */ + void Set(const otIp6Prefix &aPrefix); + + /** + * This method returns the string representation for the Ip6 prefix. + * + * @returns The string representation of the Ip6 prefix. + * + */ + std::string ToString(void) const; + + /** + * This method clears the Ip6 prefix to be unspecified. + * + */ + void Clear(void) { memset(reinterpret_cast(this), 0, sizeof(*this)); } + + /** + * This method returns if the Ip6 prefix is valid. + * + * @returns If the Ip6 prefix is valid. + * + */ + bool IsValid(void) const { return mLength > 0 && mLength <= 128; } + + Ip6Address mPrefix; ///< The IPv6 prefix. + uint8_t mLength; ///< The IPv6 prefix length (in bits). +}; + +/** + * This class represents an ethernet MAC address. + */ +class MacAddress +{ +public: + /** + * Default constructor. + * + */ + MacAddress(void) + { + m16[0] = 0; + m16[1] = 0; + m16[2] = 0; + } + + /** + * This method returns the string representation for the MAC address. + * + * @returns The string representation of the MAC address. + * + */ + std::string ToString(void) const; + + union + { + uint8_t m8[6]; + uint16_t m16[3]; + }; }; } // namespace otbr diff --git a/src/dbus/client/CMakeLists.txt b/src/dbus/client/CMakeLists.txt index 46c93de7106..ae0ae50441f 100644 --- a/src/dbus/client/CMakeLists.txt +++ b/src/dbus/client/CMakeLists.txt @@ -28,7 +28,9 @@ add_library(otbr-dbus-client client_error.cpp + client_error.hpp thread_api_dbus.cpp + thread_api_dbus.hpp ) target_link_libraries(otbr-dbus-client PUBLIC diff --git a/src/dbus/client/client_error.cpp b/src/dbus/client/client_error.cpp index 74d998980ca..f311eeba6ff 100644 --- a/src/dbus/client/client_error.cpp +++ b/src/dbus/client/client_error.cpp @@ -97,8 +97,15 @@ ClientError CheckErrorMessage(DBusMessage *aMessage) std::string errorMsg; auto args = std::tie(errorMsg); - VerifyOrExit(DBusMessageToTuple(*aMessage, args) == OTBR_ERROR_NONE, error = ClientError::ERROR_DBUS); - error = ConvertFromDBusErrorName(errorMsg); + if (dbus_message_get_type(aMessage) == DBUS_MESSAGE_TYPE_ERROR) + { + error = ConvertFromDBusErrorName(dbus_message_get_error_name(aMessage)); + } + else + { + VerifyOrExit(DBusMessageToTuple(*aMessage, args) == OTBR_ERROR_NONE, error = ClientError::ERROR_DBUS); + error = ConvertFromDBusErrorName(errorMsg); + } } exit: diff --git a/src/dbus/client/client_error.hpp b/src/dbus/client/client_error.hpp index bf689d13a08..ad033a2697a 100644 --- a/src/dbus/client/client_error.hpp +++ b/src/dbus/client/client_error.hpp @@ -26,6 +26,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes functions for handling d-bus client errors. + */ + #ifndef OTBR_DBUS_CLIENT_CLIENT_ERROR_HPP_ #define OTBR_DBUS_CLIENT_CLIENT_ERROR_HPP_ diff --git a/src/dbus/client/thread_api_dbus.cpp b/src/dbus/client/thread_api_dbus.cpp index 80f8c3df7b6..658bb2ced7e 100644 --- a/src/dbus/client/thread_api_dbus.cpp +++ b/src/dbus/client/thread_api_dbus.cpp @@ -123,7 +123,7 @@ DBusHandlerResult ThreadApiDBus::sDBusMessageFilter(DBusConnection *aConnection, DBusHandlerResult ThreadApiDBus::DBusMessageFilter(DBusConnection *aConnection, DBusMessage *aMessage) { - (void)aConnection; + OTBR_UNUSED_VARIABLE(aConnection); DBusMessageIter iter, subIter, dictEntryIter, valIter; std::string interfaceName, propertyName, val; @@ -228,6 +228,30 @@ ClientError ThreadApiDBus::Attach(const std::string & aNetworkName, return error; } +ClientError ThreadApiDBus::Attach(const OtResultHandler &aHandler) +{ + ClientError error = ClientError::ERROR_NONE; + + VerifyOrExit(mAttachHandler == nullptr && mJoinerHandler == nullptr, error = ClientError::OT_ERROR_INVALID_STATE); + mAttachHandler = aHandler; + + if (aHandler) + { + error = CallDBusMethodAsync(OTBR_DBUS_ATTACH_METHOD, + &ThreadApiDBus::sHandleDBusPendingCall<&ThreadApiDBus::AttachPendingCallHandler>); + } + else + { + error = CallDBusMethodSync(OTBR_DBUS_ATTACH_METHOD); + } + if (error != ClientError::ERROR_NONE) + { + mAttachHandler = nullptr; + } +exit: + return error; +} + void ThreadApiDBus::AttachPendingCallHandler(DBusPendingCall *aPending) { ClientError ret = ClientError::OT_ERROR_FAILED; @@ -369,11 +393,21 @@ ClientError ThreadApiDBus::SetLegacyUlaPrefix(const std::array &aDataset) +{ + return SetProperty(OTBR_DBUS_PROPERTY_ACTIVE_DATASET_TLVS, aDataset); +} + ClientError ThreadApiDBus::SetLinkMode(const LinkModeConfig &aConfig) { return SetProperty(OTBR_DBUS_PROPERTY_LINK_MODE, aConfig); } +ClientError ThreadApiDBus::SetRadioRegion(const std::string &aRadioRegion) +{ + return SetProperty(OTBR_DBUS_PROPERTY_RADIO_REGION, aRadioRegion); +} + ClientError ThreadApiDBus::GetLinkMode(LinkModeConfig &aConfig) { return GetProperty(OTBR_DBUS_PROPERTY_LINK_MODE, aConfig); @@ -510,6 +544,16 @@ ClientError ThreadApiDBus::GetExternalRoutes(std::vector &aExtern return GetProperty(OTBR_DBUS_PROPERTY_EXTERNAL_ROUTES, aExternalRoutes); } +ClientError ThreadApiDBus::GetActiveDatasetTlvs(std::vector &aDataset) +{ + return GetProperty(OTBR_DBUS_PROPERTY_ACTIVE_DATASET_TLVS, aDataset); +} + +ClientError ThreadApiDBus::GetRadioRegion(std::string &aRadioRegion) +{ + return GetProperty(OTBR_DBUS_PROPERTY_RADIO_REGION, aRadioRegion); +} + std::string ThreadApiDBus::GetInterfaceName(void) { return mInterfaceName; @@ -528,7 +572,8 @@ ClientError ThreadApiDBus::CallDBusMethodSync(const std::string &aMethodName) VerifyOrExit(message != nullptr, ret = ClientError::ERROR_DBUS); reply = UniqueDBusMessage( dbus_connection_send_with_reply_and_block(mConnection, message.get(), DBUS_TIMEOUT_USE_DEFAULT, &error)); - VerifyOrExit(!dbus_error_is_set(&error) && reply != nullptr, ret = ClientError::ERROR_DBUS); + VerifyOrExit(!dbus_error_is_set(&error), ret = DBus::ConvertFromDBusErrorName(error.message)); + VerifyOrExit(reply != nullptr, ret = ClientError::ERROR_DBUS); ret = DBus::CheckErrorMessage(reply.get()); exit: dbus_error_free(&error); @@ -569,7 +614,8 @@ ClientError ThreadApiDBus::CallDBusMethodSync(const std::string &aMethodName, co VerifyOrExit(otbr::DBus::TupleToDBusMessage(*message, aArgs) == OTBR_ERROR_NONE, ret = ClientError::ERROR_DBUS); reply = DBus::UniqueDBusMessage( dbus_connection_send_with_reply_and_block(mConnection, message.get(), DBUS_TIMEOUT_USE_DEFAULT, &error)); - VerifyOrExit(!dbus_error_is_set(&error) && reply != nullptr, ret = ClientError::ERROR_DBUS); + VerifyOrExit(!dbus_error_is_set(&error), ret = DBus::ConvertFromDBusErrorName(error.message)); + VerifyOrExit(reply != nullptr, ret = ClientError::ERROR_DBUS); ret = DBus::CheckErrorMessage(reply.get()); exit: dbus_error_free(&error); @@ -623,7 +669,8 @@ ClientError ThreadApiDBus::SetProperty(const std::string &aPropertyName, const V reply = DBus::UniqueDBusMessage( dbus_connection_send_with_reply_and_block(mConnection, message.get(), DBUS_TIMEOUT_USE_DEFAULT, &error)); - VerifyOrExit(!dbus_error_is_set(&error) && reply != nullptr, ret = ClientError::OT_ERROR_FAILED); + VerifyOrExit(!dbus_error_is_set(&error), ret = DBus::ConvertFromDBusErrorName(error.message)); + VerifyOrExit(reply != nullptr, ret = ClientError::ERROR_DBUS); ret = DBus::CheckErrorMessage(reply.get()); exit: dbus_error_free(&error); @@ -647,11 +694,11 @@ template ClientError ThreadApiDBus::GetProperty(const std::st reply = DBus::UniqueDBusMessage( dbus_connection_send_with_reply_and_block(mConnection, message.get(), DBUS_TIMEOUT_USE_DEFAULT, &error)); - VerifyOrExit(!dbus_error_is_set(&error) && reply != nullptr, ret = ClientError::OT_ERROR_FAILED); + VerifyOrExit(!dbus_error_is_set(&error), ret = DBus::ConvertFromDBusErrorName(error.message)); + VerifyOrExit(reply != nullptr, ret = ClientError::ERROR_DBUS); SuccessOrExit(DBus::CheckErrorMessage(reply.get())); - VerifyOrExit(dbus_message_iter_init(reply.get(), &iter), ret = ClientError::OT_ERROR_FAILED); - VerifyOrExit(DBus::DBusMessageExtractFromVariant(&iter, aValue) == OTBR_ERROR_NONE, - ret = ClientError::OT_ERROR_FAILED); + VerifyOrExit(dbus_message_iter_init(reply.get(), &iter), ret = ClientError::ERROR_DBUS); + VerifyOrExit(DBus::DBusMessageExtractFromVariant(&iter, aValue) == OTBR_ERROR_NONE, ret = ClientError::ERROR_DBUS); exit: dbus_error_free(&error); diff --git a/src/dbus/client/thread_api_dbus.hpp b/src/dbus/client/thread_api_dbus.hpp index 665d3b07cb7..257693fe64c 100644 --- a/src/dbus/client/thread_api_dbus.hpp +++ b/src/dbus/client/thread_api_dbus.hpp @@ -26,6 +26,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definitions for d-bus client API. + */ + #ifndef OTBR_THREAD_API_DBUS_HPP_ #define OTBR_THREAD_API_DBUS_HPP_ @@ -125,6 +130,21 @@ class ThreadApiDBus uint32_t aChannelMask, const OtResultHandler & aHandler); + /** + * This method attaches the device to the Thread network. + * + * The network parameters will be set with the active dataset under this + * circumstance. + * + * @param[in] aHandler The attach result handler. + * + * @retval ERROR_NONE successfully performed the dbus function call + * @retval ERROR_DBUS dbus encode/decode error + * @retval ... OpenThread defined error value otherwise + * + */ + ClientError Attach(const OtResultHandler &aHandler); + /** * This method performs a factory reset. * @@ -255,6 +275,18 @@ class ThreadApiDBus */ ClientError SetLegacyUlaPrefix(const std::array &aPrefix); + /** + * This method sets the active operational dataset. + * + * @param[out] aDataset The active operational dataset + * + * @retval ERROR_NONE successfully performed the dbus function call + * @retval ERROR_DBUS dbus encode/decode error + * @retval ... OpenThread defined error value otherwise + * + */ + ClientError SetActiveDatasetTlvs(const std::vector &aDataset); + /** * This method sets the link operating mode. * @@ -267,6 +299,18 @@ class ThreadApiDBus */ ClientError SetLinkMode(const LinkModeConfig &aConfig); + /** + * This method sets the radio region. + * + * @param[in] aRadioRegion The radio region. + * + * @retval ERROR_NONE successfully performed the dbus function call + * @retval ERROR_DBUS dbus encode/decode error + * @retval ... OpenThread defined error value otherwise + * + */ + ClientError SetRadioRegion(const std::string &aRadioRegion); + /** * This method gets the link operating mode. * @@ -580,6 +624,30 @@ class ThreadApiDBus */ ClientError GetExternalRoutes(std::vector &aExternalRoutes); + /** + * This method gets the active operational dataset + * + * @param[out] aDataset The active operational dataset + * + * @retval ERROR_NONE successfully performed the dbus function call + * @retval ERROR_DBUS dbus encode/decode error + * @retval ... OpenThread defined error value otherwise + * + */ + ClientError GetActiveDatasetTlvs(std::vector &aDataset); + + /** + * This method gets the radio region. + * + * @param[out] aRadioRegion The radio region. + * + * @retval ERROR_NONE successfully performed the dbus function call + * @retval ERROR_DBUS dbus encode/decode error + * @retval ... OpenThread defined error value otherwise + * + */ + ClientError GetRadioRegion(std::string &aRadioRegion); + /** * This method returns the network interface name the client is bound to. * @@ -616,7 +684,7 @@ class ThreadApiDBus static void sScanPendingCallHandler(DBusPendingCall *aPending, void *aThreadApiDBus); void ScanPendingCallHandler(DBusPendingCall *aPending); - static void EmptyFree(void *aData) { (void)aData; } + static void EmptyFree(void *) {} std::string mInterfaceName; diff --git a/src/dbus/common/constants.hpp b/src/dbus/common/constants.hpp index 8a254a94aba..158ff6f1ead 100644 --- a/src/dbus/common/constants.hpp +++ b/src/dbus/common/constants.hpp @@ -26,6 +26,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definitions for d-bus server constants. + */ + #ifndef OTBR_DBUS_CONSTANTS_HPP_ #define OTBR_DBUS_CONSTANTS_HPP_ @@ -41,6 +46,7 @@ #define OTBR_DBUS_SCAN_METHOD "Scan" #define OTBR_DBUS_ATTACH_METHOD "Attach" +#define OTBR_DBUS_DETACH_METHOD "Detach" #define OTBR_DBUS_FACTORY_RESET_METHOD "FactoryReset" #define OTBR_DBUS_RESET_METHOD "Reset" #define OTBR_DBUS_ADD_ON_MESH_PREFIX_METHOD "AddOnMeshPrefix" @@ -79,6 +85,8 @@ #define OTBR_DBUS_PROPERTY_INSTANT_RSSI "InstantRssi" #define OTBR_DBUS_PROPERTY_RADIO_TX_POWER "RadioTxPower" #define OTBR_DBUS_PROPERTY_EXTERNAL_ROUTES "ExternalRoutes" +#define OTBR_DBUS_PROPERTY_ACTIVE_DATASET_TLVS "ActiveDatasetTlvs" +#define OTBR_DBUS_PROPERTY_RADIO_REGION "RadioRegion" #define OTBR_ROLE_NAME_DISABLED "disabled" #define OTBR_ROLE_NAME_DETACHED "detached" diff --git a/src/dbus/common/dbus_message_dump.cpp b/src/dbus/common/dbus_message_dump.cpp index 98fb122b552..9e48aafe2a7 100644 --- a/src/dbus/common/dbus_message_dump.cpp +++ b/src/dbus/common/dbus_message_dump.cpp @@ -26,6 +26,8 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#define OTBR_LOG_TAG "DBUS" + #include "dbus_message_dump.hpp" #include @@ -170,12 +172,11 @@ void DumpDBusMessage(DBusMessage &aMessage) DBusMessageIter iter; std::ostringstream sout; - VerifyOrExit(dbus_message_iter_init(&aMessage, &iter), - otbrLog(OTBR_LOG_DEBUG, "Failed to iterate dbus message during dump")); + VerifyOrExit(dbus_message_iter_init(&aMessage, &iter), otbrLogDebug("Failed to iterate dbus message during dump")); sout << "{ "; DumpDBusMessage(sout, &iter); sout << "}"; - otbrLog(OTBR_LOG_DEBUG, sout.str().c_str()); + otbrLogDebug(sout.str().c_str()); exit: return; } diff --git a/src/dbus/common/dbus_message_dump.hpp b/src/dbus/common/dbus_message_dump.hpp index 5899ad10da0..d4b462f6687 100644 --- a/src/dbus/common/dbus_message_dump.hpp +++ b/src/dbus/common/dbus_message_dump.hpp @@ -26,6 +26,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definition for the API to dump d-bus message. + */ + #ifndef OTBR_AGENT_DBUS_MESSAGE_DUMP_HPP_ #define OTBR_AGENT_DBUS_MESSAGE_DUMP_HPP_ diff --git a/src/dbus/common/dbus_message_helper.cpp b/src/dbus/common/dbus_message_helper.cpp index 7474f23ed40..dbf7a0cd54e 100644 --- a/src/dbus/common/dbus_message_helper.cpp +++ b/src/dbus/common/dbus_message_helper.cpp @@ -177,5 +177,16 @@ otbrError DBusMessageEncode(DBusMessageIter *aIter, const std::vector & return DBusMessageEncodePrimitive(aIter, aValue); } +bool IsDBusMessageEmpty(DBusMessage &aMessage) +{ + DBusMessageIter iter; + + if (!dbus_message_iter_init(&aMessage, &iter)) + { + return true; + } + return dbus_message_iter_get_arg_type(&iter) == DBUS_TYPE_INVALID; +} + } // namespace DBus } // namespace otbr diff --git a/src/dbus/common/dbus_message_helper.hpp b/src/dbus/common/dbus_message_helper.hpp index c19f660ed4f..e7240f6b3bd 100644 --- a/src/dbus/common/dbus_message_helper.hpp +++ b/src/dbus/common/dbus_message_helper.hpp @@ -26,6 +26,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes utilities for manipulate d-bus message. + */ + #ifndef DBUS_MESSAGE_HELPER_HPP_ #define DBUS_MESSAGE_HELPER_HPP_ @@ -583,6 +588,8 @@ otbrError DBusMessageToTuple(UniqueDBusMessage const &aMessage, std::tuple namespace otbr { + +/** + * @namespace otbr::DBus + * + * @brief This namespace contains OpenThread Border Router DBus API. + * + */ namespace DBus { enum class ClientError diff --git a/src/dbus/common/types.hpp b/src/dbus/common/types.hpp index 96995acd08a..08811c818bc 100644 --- a/src/dbus/common/types.hpp +++ b/src/dbus/common/types.hpp @@ -26,6 +26,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definitions for types used by d-bus API. + */ + #ifndef OTBR_DBUS_COMMON_TYPES_HPP_ #define OTBR_DBUS_COMMON_TYPES_HPP_ diff --git a/src/dbus/server/CMakeLists.txt b/src/dbus/server/CMakeLists.txt index a5b10c38bc0..641887175b9 100644 --- a/src/dbus/server/CMakeLists.txt +++ b/src/dbus/server/CMakeLists.txt @@ -26,10 +26,13 @@ # POSSIBILITY OF SUCH DAMAGE. # -file(READ introspect.xml OTBR_DBUS_INTROSPECT) -file(WRITE introspect.hpp "R\"INTROSPECT(") -file(APPEND introspect.hpp ${OTBR_DBUS_INTROSPECT}) -file(APPEND introspect.hpp ")INTROSPECT\"") +add_custom_target(otbr-dbus-introspect-header ALL + COMMAND echo "R\"INTROSPECT(" > introspect.hpp + COMMAND cat ${CMAKE_CURRENT_SOURCE_DIR}/introspect.xml >> introspect.hpp + COMMAND echo ")INTROSPECT\"" >> introspect.hpp + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + VERBATIM +) option(OTBR_ENABLE_LEGACY "enable legacy support") @@ -40,6 +43,12 @@ add_library(otbr-dbus-server STATIC error_helper.cpp ) +target_include_directories(otbr-dbus-server PRIVATE + ${PROJECT_BINARY_DIR}/src +) + +add_dependencies(otbr-dbus-server otbr-dbus-introspect-header) + if(OTBR_ENABLE_LEGACY) target_compile_definitions(otbr-dbus-server PRIVATE "OTBR_ENABLE_LEGACY=1" @@ -52,3 +61,12 @@ endif() target_link_libraries(otbr-dbus-server PUBLIC otbr-dbus-common ) + +if(OTBR_DOC) +add_custom_target(otbr-dbus-server-doc ALL + COMMAND gdbus-codegen --generate-docbook generated-docs ${CMAKE_CURRENT_SOURCE_DIR}/introspect.xml + COMMAND xmlto html generated-docs-io.openthread.BorderRouter.xml + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + VERBATIM +) +endif() diff --git a/src/dbus/server/dbus_agent.cpp b/src/dbus/server/dbus_agent.cpp index 17b57fb13e7..e07143f73fd 100644 --- a/src/dbus/server/dbus_agent.cpp +++ b/src/dbus/server/dbus_agent.cpp @@ -26,7 +26,10 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#define OTBR_LOG_TAG "DBUS" + #include "dbus/server/dbus_agent.hpp" + #include "common/logging.hpp" #include "dbus/common/constants.hpp" @@ -59,22 +62,22 @@ otbrError DBusAgent::Init(void) VerifyOrExit(requestReply == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER || requestReply == DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER, error = OTBR_ERROR_DBUS); - VerifyOrExit(dbus_connection_set_watch_functions(mConnection.get(), AddDBusWatch, RemoveDBusWatch, ToggleDBusWatch, - this, nullptr)); + VerifyOrExit( + dbus_connection_set_watch_functions(mConnection.get(), AddDBusWatch, RemoveDBusWatch, nullptr, this, nullptr)); mThreadObject = std::unique_ptr(new DBusThreadObject(mConnection.get(), mInterfaceName, mNcp)); error = mThreadObject->Init(); exit: - dbus_error_free(&dbusError); if (error != OTBR_ERROR_NONE) { - otbrLog(OTBR_LOG_ERR, "dbus error %s: %s", dbusError.name, dbusError.message); + otbrLogErr("Dbus error %s: %s", dbusError.name, dbusError.message); } + dbus_error_free(&dbusError); return error; } dbus_bool_t DBusAgent::AddDBusWatch(struct DBusWatch *aWatch, void *aContext) { - static_cast(aContext)->mWatches[aWatch] = true; + static_cast(aContext)->mWatches.insert(aWatch); return TRUE; } @@ -83,34 +86,23 @@ void DBusAgent::RemoveDBusWatch(struct DBusWatch *aWatch, void *aContext) static_cast(aContext)->mWatches.erase(aWatch); } -void DBusAgent::ToggleDBusWatch(struct DBusWatch *aWatch, void *aContext) +void DBusAgent::Update(MainloopContext &aMainloop) { - static_cast(aContext)->mWatches[aWatch] = (dbus_watch_get_enabled(aWatch) ? true : false); -} - -void DBusAgent::UpdateFdSet(fd_set & aReadFdSet, - fd_set & aWriteFdSet, - fd_set & aErrorFdSet, - int & aMaxFd, - struct timeval &aTimeOut) -{ - DBusWatch * watch = nullptr; unsigned int flags; int fd; if (dbus_connection_get_dispatch_status(mConnection.get()) == DBUS_DISPATCH_DATA_REMAINS) { - aTimeOut = {0, 0}; + aMainloop.mTimeout = {0, 0}; } - for (const auto &p : mWatches) + for (const auto &watch : mWatches) { - if (!p.second) + if (!dbus_watch_get_enabled(watch)) { continue; } - watch = p.first; flags = dbus_watch_get_flags(watch); fd = dbus_watch_get_unix_fd(watch); @@ -121,37 +113,32 @@ void DBusAgent::UpdateFdSet(fd_set & aReadFdSet, if (flags & DBUS_WATCH_READABLE) { - FD_SET(fd, &aReadFdSet); + FD_SET(fd, &aMainloop.mReadFdSet); } - if ((flags & DBUS_WATCH_WRITABLE) && dbus_connection_has_messages_to_send(mConnection.get())) + if ((flags & DBUS_WATCH_WRITABLE)) { - FD_SET(fd, &aWriteFdSet); + FD_SET(fd, &aMainloop.mWriteFdSet); } - FD_SET(fd, &aErrorFdSet); + FD_SET(fd, &aMainloop.mErrorFdSet); - if (fd > aMaxFd) - { - aMaxFd = fd; - } + aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, fd); } } -void DBusAgent::Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const fd_set &aErrorFdSet) +void DBusAgent::Process(const MainloopContext &aMainloop) { - DBusWatch * watch = nullptr; unsigned int flags; int fd; - for (const auto &p : mWatches) + for (const auto &watch : mWatches) { - if (!p.second) + if (!dbus_watch_get_enabled(watch)) { continue; } - watch = p.first; flags = dbus_watch_get_flags(watch); fd = dbus_watch_get_unix_fd(watch); @@ -160,17 +147,17 @@ void DBusAgent::Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, con continue; } - if ((flags & DBUS_WATCH_READABLE) && !FD_ISSET(fd, &aReadFdSet)) + if ((flags & DBUS_WATCH_READABLE) && !FD_ISSET(fd, &aMainloop.mReadFdSet)) { flags &= static_cast(~DBUS_WATCH_READABLE); } - if ((flags & DBUS_WATCH_WRITABLE) && !FD_ISSET(fd, &aWriteFdSet)) + if ((flags & DBUS_WATCH_WRITABLE) && !FD_ISSET(fd, &aMainloop.mWriteFdSet)) { flags &= static_cast(~DBUS_WATCH_WRITABLE); } - if (FD_ISSET(fd, &aErrorFdSet)) + if (FD_ISSET(fd, &aMainloop.mErrorFdSet)) { flags |= DBUS_WATCH_ERROR; } @@ -178,8 +165,7 @@ void DBusAgent::Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, con dbus_watch_handle(watch, flags); } - while (DBUS_DISPATCH_DATA_REMAINS == dbus_connection_get_dispatch_status(mConnection.get()) && - dbus_connection_read_write_dispatch(mConnection.get(), 0)) + while (DBUS_DISPATCH_DATA_REMAINS == dbus_connection_dispatch(mConnection.get())) ; } diff --git a/src/dbus/server/dbus_agent.hpp b/src/dbus/server/dbus_agent.hpp index e0ef48b236b..b67f24249c7 100644 --- a/src/dbus/server/dbus_agent.hpp +++ b/src/dbus/server/dbus_agent.hpp @@ -26,13 +26,20 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definitions for d-bus agent. + */ + #ifndef OTBR_DBUS_AGENT_HPP_ #define OTBR_DBUS_AGENT_HPP_ #include +#include #include #include +#include "common/mainloop.hpp" #include "dbus/common/dbus_message_helper.hpp" #include "dbus/common/dbus_resources.hpp" #include "dbus/server/dbus_object.hpp" @@ -43,7 +50,7 @@ namespace otbr { namespace DBus { -class DBusAgent +class DBusAgent : public MainloopProcessor { public: /** @@ -64,35 +71,24 @@ class DBusAgent otbrError Init(void); /** - * This method performs the dbus select update. + * This method updates the mainloop context. * - * @param[out] aReadFdSet The read file descriptors. - * @param[out] aWriteFdSet The write file descriptors. - * @param[out] aErorFdSet The error file descriptors. - * @param[inout] aMaxFd The max file descriptor. - * @param[inout] aTimeOut The select timeout. + * @param[inout] aMainloop A reference to the mainloop to be updated. * */ - void UpdateFdSet(fd_set & aReadFdSet, - fd_set & aWriteFdSet, - fd_set & aErrorFdSet, - int & aMaxFd, - struct timeval &aTimeOut); + void Update(MainloopContext &aMainloop) override; /** - * This method processes the dbus I/O. + * This method processes mainloop events. * - * @param[in] aReadFdSet The read file descriptors. - * @param[in] aWriteFdSet The write file descriptors. - * @param[in] aErorFdSet The error file descriptors. + * @param[in] aMainloop A reference to the mainloop context. * */ - void Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const fd_set &aErrorFdSet); + void Process(const MainloopContext &aMainloop) override; private: static dbus_bool_t AddDBusWatch(struct DBusWatch *aWatch, void *aContext); static void RemoveDBusWatch(struct DBusWatch *aWatch, void *aContext); - static void ToggleDBusWatch(struct DBusWatch *aWatch, void *aContext); static const struct timeval kPollTimeout; @@ -106,8 +102,7 @@ class DBusAgent * This map is used to track DBusWatch-es. * */ - using WatchMap = std::map; - WatchMap mWatches; + std::set mWatches; }; } // namespace DBus diff --git a/src/dbus/server/dbus_object.cpp b/src/dbus/server/dbus_object.cpp index d58cfe3016e..7965d4eb6e5 100644 --- a/src/dbus/server/dbus_object.cpp +++ b/src/dbus/server/dbus_object.cpp @@ -26,6 +26,8 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#define OTBR_LOG_TAG "DBUS" + #include #include #include @@ -113,7 +115,7 @@ DBusHandlerResult DBusObject::MessageHandler(DBusConnection *aConnection, DBusMe if (dbus_message_get_type(aMessage) == DBUS_MESSAGE_TYPE_METHOD_CALL && iter != mMethodHandlers.end()) { - otbrLog(OTBR_LOG_INFO, "Handling method %s", memberName.c_str()); + otbrLogInfo("Handling method %s", memberName.c_str()); if (otbrLogGetLevel() >= OTBR_LOG_DEBUG) { DumpDBusMessage(*aMessage); @@ -141,7 +143,7 @@ void DBusObject::GetPropertyMethodHandler(DBusRequest &aRequest) { auto propertyIter = mGetPropertyHandlers.find(interfaceName); - otbrLog(OTBR_LOG_INFO, "GetProperty %s.%s", interfaceName.c_str(), propertyName.c_str()); + otbrLogInfo("GetProperty %s.%s", interfaceName.c_str(), propertyName.c_str()); VerifyOrExit(propertyIter != mGetPropertyHandlers.end(), error = OT_ERROR_NOT_FOUND); { DBusMessageIter replyIter; @@ -158,7 +160,7 @@ void DBusObject::GetPropertyMethodHandler(DBusRequest &aRequest) { if (otbrLogGetLevel() >= OTBR_LOG_DEBUG) { - otbrLog(OTBR_LOG_DEBUG, "GetProperty %s.%s reply:", interfaceName.c_str(), propertyName.c_str()); + otbrLogDebug("GetProperty %s.%s reply:", interfaceName.c_str(), propertyName.c_str()); DumpDBusMessage(*reply); } @@ -166,8 +168,8 @@ void DBusObject::GetPropertyMethodHandler(DBusRequest &aRequest) } else { - otbrLog(OTBR_LOG_WARNING, "GetProperty %s.%s error:%s", interfaceName.c_str(), propertyName.c_str(), - ConvertToDBusErrorName(error)); + otbrLogWarning("GetProperty %s.%s error:%s", interfaceName.c_str(), propertyName.c_str(), + ConvertToDBusErrorName(error)); aRequest.ReplyOtResult(error); } } @@ -225,7 +227,7 @@ void DBusObject::SetPropertyMethodHandler(DBusRequest &aRequest) VerifyOrExit(DBusMessageExtract(&iter, propertyName) == OTBR_ERROR_NONE, error = OT_ERROR_PARSE); propertyFullPath = interfaceName + "." + propertyName; - otbrLog(OTBR_LOG_INFO, "SetProperty %s", propertyFullPath.c_str()); + otbrLogInfo("SetProperty %s", propertyFullPath.c_str()); { auto handlerIter = mSetPropertyHandlers.find(propertyFullPath); @@ -236,8 +238,8 @@ void DBusObject::SetPropertyMethodHandler(DBusRequest &aRequest) exit: if (error != OT_ERROR_NONE) { - otbrLog(OTBR_LOG_WARNING, "SetProperty %s.%s error:%s", interfaceName.c_str(), propertyName.c_str(), - ConvertToDBusErrorName(error)); + otbrLogWarning("SetProperty %s.%s error:%s", interfaceName.c_str(), propertyName.c_str(), + ConvertToDBusErrorName(error)); } aRequest.ReplyOtResult(error); return; diff --git a/src/dbus/server/dbus_object.hpp b/src/dbus/server/dbus_object.hpp index 3d790200ddf..5d601833895 100644 --- a/src/dbus/server/dbus_object.hpp +++ b/src/dbus/server/dbus_object.hpp @@ -26,9 +26,18 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definitions for a d-bus object. + */ + #ifndef OTBR_DBUS_DBUS_OBJECT_HPP_ #define OTBR_DBUS_DBUS_OBJECT_HPP_ +#ifndef OTBR_LOG_TAG +#define OTBR_LOG_TAG "DBUS" +#endif + #include #include #include @@ -189,7 +198,7 @@ class DBusObject if (otbrLogGetLevel() >= OTBR_LOG_DEBUG) { - otbrLog(OTBR_LOG_DEBUG, "Signal %s.%s", aInterfaceName.c_str(), aPropertyName.c_str()); + otbrLogDebug("Signal %s.%s", aInterfaceName.c_str(), aPropertyName.c_str()); DumpDBusMessage(*signalMsg); } diff --git a/src/dbus/server/dbus_request.hpp b/src/dbus/server/dbus_request.hpp index 4b0b1878e0b..7126cd5b219 100644 --- a/src/dbus/server/dbus_request.hpp +++ b/src/dbus/server/dbus_request.hpp @@ -26,6 +26,15 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definitions for a d-bus request. + */ + +#ifndef OTBR_LOG_TAG +#define OTBR_LOG_TAG "DBUS" +#endif + #include "common/logging.hpp" #include "dbus/common/dbus_message_dump.hpp" @@ -114,8 +123,7 @@ class DBusRequest if (otbrLogGetLevel() >= OTBR_LOG_DEBUG) { - otbrLog(OTBR_LOG_DEBUG, "Replied to %s.%s :", dbus_message_get_interface(mMessage), - dbus_message_get_member(mMessage)); + otbrLogDebug("Replied to %s.%s :", dbus_message_get_interface(mMessage), dbus_message_get_member(mMessage)); DumpDBusMessage(*reply); } dbus_connection_send(mConnection, reply.get(), nullptr); @@ -133,10 +141,18 @@ class DBusRequest void ReplyOtResult(otError aError) { UniqueDBusMessage reply{nullptr}; - auto logLevel = (aError == OT_ERROR_NONE) ? OTBR_LOG_INFO : OTBR_LOG_ERR; - otbrLog(logLevel, "Replied to %s.%s with result %s", dbus_message_get_interface(mMessage), - dbus_message_get_member(mMessage), ConvertToDBusErrorName(aError)); + if (aError == OT_ERROR_NONE) + { + otbrLogInfo("Replied to %s.%s with result %s", dbus_message_get_interface(mMessage), + dbus_message_get_member(mMessage), ConvertToDBusErrorName(aError)); + } + else + { + otbrLogErr("Replied to %s.%s with result %s", dbus_message_get_interface(mMessage), + dbus_message_get_member(mMessage), ConvertToDBusErrorName(aError)); + } + if (aError == OT_ERROR_NONE) { reply = UniqueDBusMessage(dbus_message_new_method_return(mMessage)); diff --git a/src/dbus/server/dbus_thread_object.cpp b/src/dbus/server/dbus_thread_object.cpp index 685b2548681..26ef0cec581 100644 --- a/src/dbus/server/dbus_thread_object.cpp +++ b/src/dbus/server/dbus_thread_object.cpp @@ -111,6 +111,8 @@ otbrError DBusThreadObject::Init(void) std::bind(&DBusThreadObject::ScanHandler, this, _1)); RegisterMethod(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_ATTACH_METHOD, std::bind(&DBusThreadObject::AttachHandler, this, _1)); + RegisterMethod(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_DETACH_METHOD, + std::bind(&DBusThreadObject::DetachHandler, this, _1)); RegisterMethod(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_FACTORY_RESET_METHOD, std::bind(&DBusThreadObject::FactoryResetHandler, this, _1)); RegisterMethod(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_RESET_METHOD, @@ -139,6 +141,11 @@ otbrError DBusThreadObject::Init(void) std::bind(&DBusThreadObject::SetLegacyUlaPrefixHandler, this, _1)); RegisterSetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_LINK_MODE, std::bind(&DBusThreadObject::SetLinkModeHandler, this, _1)); + RegisterSetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_ACTIVE_DATASET_TLVS, + std::bind(&DBusThreadObject::SetActiveDatasetTlvsHandler, this, _1)); + RegisterSetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_RADIO_REGION, + std::bind(&DBusThreadObject::SetRadioRegionHandler, this, _1)); + RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_LINK_MODE, std::bind(&DBusThreadObject::GetLinkModeHandler, this, _1)); RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_DEVICE_ROLE, @@ -192,6 +199,10 @@ otbrError DBusThreadObject::Init(void) std::bind(&DBusThreadObject::GetRadioTxPowerHandler, this, _1)); RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_EXTERNAL_ROUTES, std::bind(&DBusThreadObject::GetExternalRoutesHandler, this, _1)); + RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_ACTIVE_DATASET_TLVS, + std::bind(&DBusThreadObject::GetActiveDatasetTlvsHandler, this, _1)); + RegisterGetPropertyHandler(OTBR_DBUS_THREAD_INTERFACE, OTBR_DBUS_PROPERTY_RADIO_REGION, + std::bind(&DBusThreadObject::GetRadioRegionHandler, this, _1)); return error; } @@ -263,7 +274,11 @@ void DBusThreadObject::AttachHandler(DBusRequest &aRequest) auto args = std::tie(masterKey, panid, name, extPanId, pskc, channelMask); - if (DBusMessageToTuple(*aRequest.GetMessage(), args) != OTBR_ERROR_NONE) + if (IsDBusMessageEmpty(*aRequest.GetMessage())) + { + threadHelper->Attach([aRequest](otError aError) mutable { aRequest.ReplyOtResult(aError); }); + } + else if (DBusMessageToTuple(*aRequest.GetMessage(), args) != OTBR_ERROR_NONE) { aRequest.ReplyOtResult(OT_ERROR_INVALID_ARGS); } @@ -274,11 +289,21 @@ void DBusThreadObject::AttachHandler(DBusRequest &aRequest) } } +void DBusThreadObject::DetachHandler(DBusRequest &aRequest) +{ + aRequest.ReplyOtResult(mNcp->GetThreadHelper()->Detach()); +} + void DBusThreadObject::FactoryResetHandler(DBusRequest &aRequest) { - aRequest.ReplyOtResult(OT_ERROR_NONE); - otInstanceFactoryReset(mNcp->GetThreadHelper()->GetInstance()); + otError error = OT_ERROR_NONE; + + SuccessOrExit(error = mNcp->GetThreadHelper()->Detach()); + SuccessOrExit(otInstanceErasePersistentInfo(mNcp->GetThreadHelper()->GetInstance())); mNcp->Reset(); + +exit: + aRequest.ReplyOtResult(error); } void DBusThreadObject::ResetHandler(DBusRequest &aRequest) @@ -466,7 +491,7 @@ otError DBusThreadObject::SetLegacyUlaPrefixHandler(DBusMessageIter &aIter) exit: return error; #else - (void)aIter; + OTBR_UNUSED_VARIABLE(aIter); return OT_ERROR_NOT_IMPLEMENTED; #endif // OTBR_ENABLE_LEGACY @@ -511,8 +536,7 @@ otError DBusThreadObject::GetDeviceRoleHandler(DBusMessageIter &aIter) auto threadHelper = mNcp->GetThreadHelper(); otDeviceRole role = otThreadGetDeviceRole(threadHelper->GetInstance()); std::string roleName = GetDeviceRoleName(role); - ; - otError error = OT_ERROR_NONE; + otError error = OT_ERROR_NONE; VerifyOrExit(DBusMessageEncodeToVariant(&aIter, roleName) == OTBR_ERROR_NONE, error = OT_ERROR_INVALID_ARGS); @@ -789,7 +813,7 @@ otError DBusThreadObject::GetChannelMonitorSampleCountHandler(DBusMessageIter &a exit: return error; #else // OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE - (void)aIter; + OTBR_UNUSED_VARIABLE(aIter); return OT_ERROR_NOT_IMPLEMENTED; #endif // OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE } @@ -818,7 +842,7 @@ otError DBusThreadObject::GetChannelMonitorAllChannelQualities(DBusMessageIter & exit: return error; #else // OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE - (void)aIter; + OTBR_UNUSED_VARIABLE(aIter); return OT_ERROR_NOT_IMPLEMENTED; #endif // OPENTHREAD_CONFIG_CHANNEL_MONITOR_ENABLE } @@ -961,5 +985,73 @@ otError DBusThreadObject::GetExternalRoutesHandler(DBusMessageIter &aIter) return error; } +otError DBusThreadObject::SetActiveDatasetTlvsHandler(DBusMessageIter &aIter) +{ + auto threadHelper = mNcp->GetThreadHelper(); + std::vector data; + otOperationalDatasetTlvs datasetTlvs; + otError error = OT_ERROR_NONE; + + VerifyOrExit(DBusMessageExtractFromVariant(&aIter, data) == OTBR_ERROR_NONE, error = OT_ERROR_INVALID_ARGS); + VerifyOrExit(data.size() <= sizeof(datasetTlvs.mTlvs)); + std::copy(std::begin(data), std::end(data), std::begin(datasetTlvs.mTlvs)); + datasetTlvs.mLength = data.size(); + error = otDatasetSetActiveTlvs(threadHelper->GetInstance(), &datasetTlvs); + +exit: + return error; +} + +otError DBusThreadObject::GetActiveDatasetTlvsHandler(DBusMessageIter &aIter) +{ + auto threadHelper = mNcp->GetThreadHelper(); + otError error = OT_ERROR_NONE; + std::vector data; + otOperationalDatasetTlvs datasetTlvs; + + SuccessOrExit(error = otDatasetGetActiveTlvs(threadHelper->GetInstance(), &datasetTlvs)); + data = std::vector{std::begin(datasetTlvs.mTlvs), std::begin(datasetTlvs.mTlvs) + datasetTlvs.mLength}; + + VerifyOrExit(DBusMessageEncodeToVariant(&aIter, data) == OTBR_ERROR_NONE, error = OT_ERROR_INVALID_ARGS); + +exit: + return error; +} + +otError DBusThreadObject::SetRadioRegionHandler(DBusMessageIter &aIter) +{ + auto threadHelper = mNcp->GetThreadHelper(); + std::string radioRegion; + uint16_t regionCode; + otError error = OT_ERROR_NONE; + + VerifyOrExit(DBusMessageExtractFromVariant(&aIter, radioRegion) == OTBR_ERROR_NONE, error = OT_ERROR_INVALID_ARGS); + VerifyOrExit(radioRegion.size() == sizeof(uint16_t), error = OT_ERROR_INVALID_ARGS); + regionCode = radioRegion[0] << 8 | radioRegion[1]; + + error = otPlatRadioSetRegion(threadHelper->GetInstance(), regionCode); + +exit: + return error; +} + +otError DBusThreadObject::GetRadioRegionHandler(DBusMessageIter &aIter) +{ + auto threadHelper = mNcp->GetThreadHelper(); + otError error = OT_ERROR_NONE; + std::string radioRegion; + uint16_t regionCode; + + SuccessOrExit(error = otPlatRadioGetRegion(threadHelper->GetInstance(), ®ionCode)); + radioRegion.resize(sizeof(uint16_t), '\0'); + radioRegion[0] = static_cast((regionCode >> 8) & 0xff); + radioRegion[1] = static_cast(regionCode & 0xff); + + VerifyOrExit(DBusMessageEncodeToVariant(&aIter, radioRegion) == OTBR_ERROR_NONE, error = OT_ERROR_INVALID_ARGS); + +exit: + return error; +} + } // namespace DBus } // namespace otbr diff --git a/src/dbus/server/dbus_thread_object.hpp b/src/dbus/server/dbus_thread_object.hpp index 4893935b4da..cf56f09d803 100644 --- a/src/dbus/server/dbus_thread_object.hpp +++ b/src/dbus/server/dbus_thread_object.hpp @@ -26,6 +26,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definitions for the d-bus object of OpenThread service. + */ + #ifndef OTBR_DBUS_THREAD_OBJECT_HPP_ #define OTBR_DBUS_THREAD_OBJECT_HPP_ @@ -39,6 +44,17 @@ namespace otbr { namespace DBus { +/** + * @addtogroup border-router-dbus-server + * + * @brief + * This module includes the dbus server api. + * + * @{ + * @} + * + */ + class DBusAgent; class DBusThreadObject : public DBusObject @@ -70,6 +86,7 @@ class DBusThreadObject : public DBusObject void ScanHandler(DBusRequest &aRequest); void AttachHandler(DBusRequest &aRequest); + void DetachHandler(DBusRequest &aRequest); void LeaveHandler(DBusRequest &aRequest); void FactoryResetHandler(DBusRequest &aRequest); void ResetHandler(DBusRequest &aRequest); @@ -86,6 +103,8 @@ class DBusThreadObject : public DBusObject otError SetMeshLocalPrefixHandler(DBusMessageIter &aIter); otError SetLegacyUlaPrefixHandler(DBusMessageIter &aIter); otError SetLinkModeHandler(DBusMessageIter &aIter); + otError SetActiveDatasetTlvsHandler(DBusMessageIter &aIter); + otError SetRadioRegionHandler(DBusMessageIter &aIter); otError GetLinkModeHandler(DBusMessageIter &aIter); otError GetDeviceRoleHandler(DBusMessageIter &aIter); @@ -113,6 +132,8 @@ class DBusThreadObject : public DBusObject otError GetInstantRssiHandler(DBusMessageIter &aIter); otError GetRadioTxPowerHandler(DBusMessageIter &aIter); otError GetExternalRoutesHandler(DBusMessageIter &aIter); + otError GetActiveDatasetTlvsHandler(DBusMessageIter &aIter); + otError GetRadioRegionHandler(DBusMessageIter &aIter); void ReplyScanResult(DBusRequest &aRequest, otError aError, const std::vector &aResult); diff --git a/src/dbus/server/error_helper.hpp b/src/dbus/server/error_helper.hpp index e1bb67d1dd8..1016639f98b 100644 --- a/src/dbus/server/error_helper.hpp +++ b/src/dbus/server/error_helper.hpp @@ -26,6 +26,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definitions for helpers of handling errors. + */ + #ifndef OTBR_DBUS_SERVER_ERROR_HELPER_HPP_ #define OTBR_DBUS_SERVER_ERROR_HELPER_HPP_ diff --git a/src/dbus/server/introspect.xml b/src/dbus/server/introspect.xml index 0850c5f9270..c9aebf390b6 100644 --- a/src/dbus/server/introspect.xml +++ b/src/dbus/server/introspect.xml @@ -2,8 +2,11 @@ "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd"> - - + + --> + + @@ -31,11 +46,27 @@ + + + + + + @@ -45,295 +76,380 @@ + + + - - + + --> + - - + + --> + - - + + - + + + + + + - - + + + + - + + + + - - - + + + - + + + + + + + + + + diff --git a/src/mdns/CMakeLists.txt b/src/mdns/CMakeLists.txt index 851403f55b7..1b28dc3cf49 100644 --- a/src/mdns/CMakeLists.txt +++ b/src/mdns/CMakeLists.txt @@ -27,30 +27,36 @@ # if(OTBR_MDNS STREQUAL "avahi") -add_library(otbr-mdns - mdns_avahi.cpp -) -target_compile_definitions(otbr-mdns PUBLIC - OTBR_ENABLE_MDNS_AVAHI=1 -) -target_link_libraries(otbr-mdns PRIVATE - otbr-common - otbr-utils - avahi-client - avahi-common -) + add_library(otbr-mdns + mdns.cpp + mdns_avahi.cpp + ) + target_compile_definitions(otbr-mdns PUBLIC + OTBR_ENABLE_MDNS_AVAHI=1 + ) + target_link_libraries(otbr-mdns + PUBLIC + otbr-common + PRIVATE + otbr-utils + avahi-client + avahi-common + ) endif() if(OTBR_MDNS STREQUAL "mDNSResponder") -add_library(otbr-mdns - mdns_mdnssd.cpp -) -target_compile_definitions(otbr-mdns PUBLIC - OTBR_ENABLE_MDNS_MDNSSD=1 -) -target_link_libraries(otbr-mdns PRIVATE - otbr-common - otbr-utils - dns_sd -) + add_library(otbr-mdns + mdns.cpp + mdns_mdnssd.cpp + ) + target_compile_definitions(otbr-mdns PUBLIC + OTBR_ENABLE_MDNS_MDNSSD=1 + ) + target_link_libraries(otbr-mdns + PUBLIC + otbr-common + PRIVATE + otbr-utils + dns_sd + ) endif() diff --git a/src/mdns/mdns.cpp b/src/mdns/mdns.cpp new file mode 100644 index 00000000000..214c427acb0 --- /dev/null +++ b/src/mdns/mdns.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes implementation of mDNS publisher. + */ + +#include "mdns/mdns.hpp" + +#include "common/code_utils.hpp" + +namespace otbr { + +namespace Mdns { + +bool Publisher::IsServiceTypeEqual(const char *aFirstType, const char *aSecondType) +{ + size_t firstLength = strlen(aFirstType); + size_t secondLength = strlen(aSecondType); + + if (firstLength > 0 && aFirstType[firstLength - 1] == '.') + { + --firstLength; + } + if (secondLength > 0 && aSecondType[secondLength - 1] == '.') + { + --secondLength; + } + + return firstLength == secondLength && memcmp(aFirstType, aSecondType, firstLength) == 0; +} + +otbrError Publisher::EncodeTxtData(const TxtList &aTxtList, uint8_t *aTxtData, uint16_t &aTxtLength) +{ + otbrError error = OTBR_ERROR_NONE; + uint8_t * cur = aTxtData; + + for (const auto &txtEntry : aTxtList) + { + const char * name = txtEntry.mName.c_str(); + const size_t nameLength = txtEntry.mName.length(); + const uint8_t *value = txtEntry.mValue.data(); + const size_t valueLength = txtEntry.mValue.size(); + const size_t entryLength = nameLength + 1 + valueLength; + + VerifyOrExit(nameLength > 0 && nameLength <= kMaxTextEntrySize, error = OTBR_ERROR_INVALID_ARGS); + VerifyOrExit(cur + entryLength + 1 <= aTxtData + aTxtLength, error = OTBR_ERROR_INVALID_ARGS); + + cur[0] = static_cast(entryLength); + ++cur; + + memcpy(cur, name, nameLength); + cur += nameLength; + + cur[0] = '='; + ++cur; + + memcpy(cur, value, valueLength); + cur += valueLength; + } + + aTxtLength = cur - aTxtData; + +exit: + return error; +} + +} // namespace Mdns + +} // namespace otbr diff --git a/src/mdns/mdns.hpp b/src/mdns/mdns.hpp index ef54dcc4091..24c2d8eb268 100644 --- a/src/mdns/mdns.hpp +++ b/src/mdns/mdns.hpp @@ -28,39 +28,25 @@ /** * @file - * This file includes definition for MDNS service. + * This file includes definitions for mDNS publisher. */ #ifndef OTBR_AGENT_MDNS_HPP_ #define OTBR_AGENT_MDNS_HPP_ +#include +#include +#include + #include +#include "common/mainloop.hpp" #include "common/types.hpp" namespace otbr { namespace Mdns { -/** - * MDNS state values. - * - */ -enum State -{ - kStateIdle, ///< Unable to publishing service. - kStateReady, ///< Ready for publishing service. -}; - -/** - * This function pointer is called when MDNS service state changed. - * - * @param[in] aContext A pointer to application-specific context. - * @param[in] aState The new state. - * - */ -typedef void (*StateHandler)(void *aContext, State aState); - /** * @addtogroup border-router-mdns * @@ -74,9 +60,165 @@ typedef void (*StateHandler)(void *aContext, State aState); * This interface defines the functionality of MDNS service. * */ -class Publisher +class Publisher : public MainloopProcessor { public: + /** + * This structure represents a name/value pair of the TXT record. + * + */ + struct TxtEntry + { + std::string mName; ///< The name of the TXT entry. + std::vector mValue; ///< The value of the TXT entry. + + TxtEntry(const char *aName, const char *aValue) + : TxtEntry(aName, reinterpret_cast(aValue), strlen(aValue)) + { + } + + TxtEntry(const char *aName, const uint8_t *aValue, size_t aValueLength) + : TxtEntry(aName, strlen(aName), aValue, aValueLength) + { + } + + TxtEntry(const char *aName, size_t aNameLength, const uint8_t *aValue, size_t aValueLength) + : mName(aName, aNameLength) + , mValue(aValue, aValue + aValueLength) + { + } + }; + + typedef std::vector TxtList; + + /** + * This structure represents information of a discovered service instance. + * + */ + struct DiscoveredInstanceInfo + { + /** + * Constructor to initialize a `DiscoveredInstanceInfo` instance. + * + */ + DiscoveredInstanceInfo(void) + : mPort(0) + , mPriority(0) + , mWeight(0) + , mTtl(0) + { + } + + std::string mName; ///< Instance name. + std::string mHostName; ///< Full host name. + std::vector mAddresses; ///< IPv6 addresses. + uint16_t mPort; ///< Port. + uint16_t mPriority; ///< Service priority. + uint16_t mWeight; ///< Service weight. + std::vector mTxtData; ///< TXT RDATA bytes. + uint32_t mTtl; ///< Service TTL. + }; + + /** + * This structure represents information of a discovered host. + * + */ + struct DiscoveredHostInfo + { + /** + * Constructor to initialize a `DiscoveredHostInfo` instance. + * + */ + DiscoveredHostInfo(void) + : mTtl(0) + { + } + + std::string mHostName; ///< Full host name. + std::vector mAddresses; ///< IP6 addresses. + uint32_t mTtl; ///< Host TTL. + }; + + /** + * This function is called to notify a discovered service instance. + * + */ + using DiscoveredServiceInstanceCallback = + std::function; + + /** + * This function is called to notify a discovered host. + * + */ + using DiscoveredHostCallback = + std::function; + + /** + * MDNS state values. + * + */ + enum class State + { + kIdle, ///< Unable to publishing service. + kReady, ///< Ready for publishing service. + }; + + /** + * This function pointer is called when MDNS service state changed. + * + * @param[in] aContext A pointer to application-specific context. + * @param[in] aState The new state. + * + */ + typedef void (*StateHandler)(void *aContext, State aState); + + /** + * This method reports the result of a service publication. + * + * @param[in] aName The service instance name. + * @param[in] aType The service type. + * @param[in] aError An error that indicates whether the service publication succeeds. + * @param[in] aContext A user context. + * + */ + typedef void (*PublishServiceHandler)(const char *aName, const char *aType, otbrError aError, void *aContext); + + /** + * This method reports the result of a host publication. + * + * @param[in] aName The host name. + * @param[in] aError An OTBR error that indicates whether the host publication succeeds. + * @param[in] aContext A user context. + * + */ + typedef void (*PublishHostHandler)(const char *aName, otbrError aError, void *aContext); + + /** + * This method sets the handler for service publication. + * + * @param[in] aHandler A handler which will be called when a service publication is finished. + * @param[in] aContext A user context which is associated to @p aHandler. + * + */ + void SetPublishServiceHandler(PublishServiceHandler aHandler, void *aContext) + { + mServiceHandler = aHandler; + mServiceHandlerContext = aContext; + } + + /** + * This method sets the handler for host publication. + * + * @param[in] aHandler A handler which will be called when a host publication is finished. + * @param[in] aContext A user context which is associated to @p aHandler. + * + */ + void SetPublishHostHandler(PublishHostHandler aHandler, void *aContext) + { + mHostHandler = aHandler; + mHostHandlerContext = aContext; + } + /** * This method starts the MDNS service. * @@ -104,72 +246,200 @@ class Publisher /** * This method publishes or updates a service. * + * @param[in] aHostName The name of the host which this service resides on. If NULL is provided, + * this service resides on local host and it is the implementation to provide + * specific host name. Otherwise, the caller MUST publish the host with method + * PublishHost. * @param[in] aName The name of this service. * @param[in] aType The type of this service. * @param[in] aPort The port number of this service. - * @param[in] ... Pointers to null-terminated string of key and value for text record. - * The last argument must be nullptr. + * @param[in] aTxtList A list of TXT name/value pairs. * * @retval OTBR_ERROR_NONE Successfully published or updated the service. * @retval OTBR_ERROR_ERRNO Failed to publish or update the service. * */ - virtual otbrError PublishService(uint16_t aPort, const char *aName, const char *aType, ...) = 0; + virtual otbrError PublishService(const char * aHostName, + uint16_t aPort, + const char * aName, + const char * aType, + const TxtList &aTxtList) = 0; + + /** + * This method un-publishes a service. + * + * @param[in] aName The name of this service. + * @param[in] aType The type of this service. + * + * @retval OTBR_ERROR_NONE Successfully un-published the service. + * @retval OTBR_ERROR_ERRNO Failed to un-publish the service. + * + */ + virtual otbrError UnpublishService(const char *aName, const char *aType) = 0; + + /** + * This method publishes or updates a host. + * + * Publishing a host is advertising an A/AAAA RR for the host name. This method should be called + * before a service with non-null host name is published. + * + * @param[in] aName The name of the host. + * @param[in] aAddress The address of the host. + * @param[in] aAddressLength The length of @p aAddress. + * + * @retval OTBR_ERROR_NONE Successfully published or updated the host. + * @retval OTBR_ERROR_INVALID_ARGS The arguments are not valid. + * @retval OTBR_ERROR_ERRNO Failed to publish or update the host. + * + */ + virtual otbrError PublishHost(const char *aName, const uint8_t *aAddress, uint8_t aAddressLength) = 0; + + /** + * This method un-publishes a host. + * + * @param[in] aName A host name. + * + * @retval OTBR_ERROR_NONE Successfully un-published the host. + * @retval OTBR_ERROR_ERRNO Failed to un-publish the host. + * + */ + virtual otbrError UnpublishHost(const char *aName) = 0; + + /** + * This method subscribes a given service or service instance. + * + * If @p aInstanceName is not empty, this method subscribes the service instance. Otherwise, this method subscribes + * the service. mDNS implementations should use the `DiscoveredServiceInstanceCallback` function to notify + * discovered service instances. + * + * @note Discovery Proxy implementation guarantees no duplicate subscriptions for the same service or service + * instance. + * + * @param[in] aType The service type. + * @param[in] aInstanceName The service instance to subscribe, or empty to subscribe the service. + * + */ + virtual void SubscribeService(const std::string &aType, const std::string &aInstanceName) = 0; + + /** + * This method unsubscribes a given service or service instance. + * + * If @p aInstanceName is not empty, this method unsubscribes the service instance. Otherwise, this method + * unsubscribes the service. + * + * @note Discovery Proxy implementation guarantees no redundant unsubscription for a service or service instance. + * + * @param[in] aType The service type. + * @param[in] aInstanceName The service instance to unsubscribe, or empty to unsubscribe the service. + * + */ + virtual void UnsubscribeService(const std::string &aType, const std::string &aInstanceName) = 0; + + /** + * This method subscribes a given host. + * + * mDNS implementations should use the `DiscoveredHostCallback` function to notify discovered hosts. + * + * @note Discovery Proxy implementation guarantees no duplicate subscriptions for the same host. + * + * @param[in] aHostName The host name (without domain). + * + */ + virtual void SubscribeHost(const std::string &aHostName) = 0; /** - * This method performs the MDNS processing. + * This method unsubscribes a given host. + * + * @note Discovery Proxy implementation guarantees no redundant unsubscription for a host. * - * @param[in] aReadFdSet A reference to fd_set ready for reading. - * @param[in] aWriteFdSet A reference to fd_set ready for writing. - * @param[in] aErrorFdSet A reference to fd_set with error occurred. + * @param[in] aHostName The host name (without domain). * */ - virtual void Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const fd_set &aErrorFdSet) = 0; + virtual void UnsubscribeHost(const std::string &aHostName) = 0; /** - * This method updates the fd_set and timeout for mainloop. + * This method sets the callbacks for subscriptions. * - * @param[inout] aReadFdSet A reference to fd_set for polling read. - * @param[inout] aWriteFdSet A reference to fd_set for polling read. - * @param[inout] aErrorFdSet A reference to fd_set for polling error. - * @param[inout] aMaxFd A reference to the current max fd in @p aReadFdSet and @p aWriteFdSet. - * @param[inout] aTimeout A reference to the timeout. Update this value if the MDNS service has - * pending process in less than its current value. + * @param[in] aInstanceCallback The callback function to receive discovered service instances. + * @param[in] aHostCallback The callback function to receive discovered hosts. * */ - virtual void UpdateFdSet(fd_set & aReadFdSet, - fd_set & aWriteFdSet, - fd_set & aErrorFdSet, - int & aMaxFd, - timeval &aTimeout) = 0; + void SetSubscriptionCallbacks(DiscoveredServiceInstanceCallback aInstanceCallback, + DiscoveredHostCallback aHostCallback) + { + mDiscoveredServiceInstanceCallback = std::move(aInstanceCallback); + mDiscoveredHostCallback = std::move(aHostCallback); + } - virtual ~Publisher(void) {} + virtual ~Publisher(void) = default; /** * This function creates a MDNS publisher. * * @param[in] aProtocol Protocol to use for publishing. AF_INET6, AF_INET or AF_UNSPEC. - * @param[in] aHost The host where these services is residing on. - * @param[in] aDomain The domain to register in. + * @param[in] aDomain The domain to register in. Set nullptr to use default mDNS domain ("local."). * @param[in] aHandler The function to be called when this service state changed. * @param[in] aContext A pointer to application-specific context. * * @returns A pointer to the newly created MDNS publisher. * */ - static Publisher *Create(int aProtocol, - const char * aHost, - const char * aDomain, - StateHandler aHandler, - void * aContext); + static Publisher *Create(int aProtocol, const char *aDomain, StateHandler aHandler, void *aContext); /** - * This function destroies the MDNS publisher. + * This function destroys the MDNS publisher. * * @param[in] aPublisher A pointer to the publisher. * */ static void Destroy(Publisher *aPublisher); + + /** + * This function decides if two service types (names) are equal. + * + * Different implementations may or not append a dot ('.') to the service type (name) + * and we can not compare if two service type are equal with `strcmp`. This function + * ignores the trailing dot when comparing two service types. + * + * @param[in] aFirstType The first service type. + * @param[in] aSecondType The second service type. + * + * returns A boolean that indicates whether the two service types are equal. + * + */ + static bool IsServiceTypeEqual(const char *aFirstType, const char *aSecondType); + + /** + * This function writes the TXT entry list to a TXT data buffer. + * + * The output data is in standard DNS-SD TXT data format. + * See RFC 6763 for details: https://tools.ietf.org/html/rfc6763#section-6. + * + * @param[in] aTxtList A TXT entry list. + * @param[out] aTxtData A TXT data buffer. + * @param[inout] aTxtLength As input, it is the length of the TXT data buffer; + * As output, it is the length of the TXT data written. + * + * @retval OTBR_ERROR_NONE Successfully write the TXT entry list. + * @retval OTBR_ERROR_INVALID_ARGS The input TXT data buffer cannot hold the TXT data. + * + */ + static otbrError EncodeTxtData(const TxtList &aTxtList, uint8_t *aTxtData, uint16_t &aTxtLength); + +protected: + enum : uint8_t + { + kMaxTextEntrySize = 255, + }; + + PublishServiceHandler mServiceHandler = nullptr; + void * mServiceHandlerContext = nullptr; + + PublishHostHandler mHostHandler = nullptr; + void * mHostHandlerContext = nullptr; + + DiscoveredServiceInstanceCallback mDiscoveredServiceInstanceCallback = nullptr; + DiscoveredHostCallback mDiscoveredHostCallback = nullptr; }; /** diff --git a/src/mdns/mdns_avahi.cpp b/src/mdns/mdns_avahi.cpp index 0db1b24170d..ee3543dcfea 100644 --- a/src/mdns/mdns_avahi.cpp +++ b/src/mdns/mdns_avahi.cpp @@ -31,8 +31,12 @@ * This file implements MDNS service based on avahi. */ +#define OTBR_LOG_TAG "MDNS" + #include "mdns/mdns_avahi.hpp" +#include + #include #include #include @@ -58,11 +62,11 @@ AvahiTimeout::AvahiTimeout(const struct timeval *aTimeout, { if (aTimeout) { - mTimeout = otbr::GetNow() + otbr::GetTimestamp(*aTimeout); + mTimeout = otbr::Clock::now() + otbr::FromTimeval(*aTimeout); } else { - mTimeout = 0; + mTimeout = otbr::Timepoint::min(); } } @@ -148,11 +152,11 @@ void Poller::TimeoutUpdate(AvahiTimeout *aTimer, const struct timeval *aTimeout) { if (aTimeout == nullptr) { - aTimer->mTimeout = 0; + aTimer->mTimeout = Timepoint::min(); } else { - aTimer->mTimeout = otbr::GetNow() + otbr::GetTimestamp(*aTimeout); + aTimer->mTimeout = Clock::now() + FromTimeval(*aTimeout); } } @@ -174,8 +178,10 @@ void Poller::TimeoutFree(AvahiTimeout &aTimer) } } -void Poller::UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, fd_set &aErrorFdSet, int &aMaxFd, timeval &aTimeout) +void Poller::Update(MainloopContext &aMainloop) { + Timepoint now = Clock::now(); + for (Watches::iterator it = mWatches.begin(); it != mWatches.end(); ++it) { int fd = (*it)->mFd; @@ -183,17 +189,17 @@ void Poller::UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, fd_set &aError if (AVAHI_WATCH_IN & events) { - FD_SET(fd, &aReadFdSet); + FD_SET(fd, &aMainloop.mReadFdSet); } if (AVAHI_WATCH_OUT & events) { - FD_SET(fd, &aWriteFdSet); + FD_SET(fd, &aMainloop.mWriteFdSet); } if (AVAHI_WATCH_ERR & events) { - FD_SET(fd, &aErrorFdSet); + FD_SET(fd, &aMainloop.mErrorFdSet); } if (AVAHI_WATCH_HUP & events) @@ -201,58 +207,41 @@ void Poller::UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, fd_set &aError // TODO what do with this event type? } - if (aMaxFd < fd) - { - aMaxFd = fd; - } + aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, fd); (*it)->mHappened = 0; } - unsigned long now = GetNow(); - for (Timers::iterator it = mTimers.begin(); it != mTimers.end(); ++it) { - unsigned long timeout = (*it)->mTimeout; + Timepoint timeout = (*it)->mTimeout; - if (timeout == 0) + if (timeout == Timepoint::min()) { continue; } - if (static_cast(timeout - now) <= 0) + if (timeout <= now) { - aTimeout.tv_usec = 0; - aTimeout.tv_sec = 0; + aMainloop.mTimeout = ToTimeval(Microseconds::zero()); break; } else { - time_t sec; - suseconds_t usec; + auto delay = std::chrono::duration_cast(timeout - now); - timeout -= now; - sec = static_cast(timeout / 1000); - usec = static_cast((timeout % 1000) * 1000); - - if (sec < aTimeout.tv_sec) - { - aTimeout.tv_sec = sec; - } - else if (sec == aTimeout.tv_sec) + if (delay < FromTimeval(aMainloop.mTimeout)) { - if (usec < aTimeout.tv_usec) - { - aTimeout.tv_usec = usec; - } + aMainloop.mTimeout = ToTimeval(delay); } } } } -void Poller::Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const fd_set &aErrorFdSet) +void Poller::Process(const MainloopContext &aMainloop) { - unsigned long now = GetNow(); + Timepoint now = Clock::now(); + std::vector expired; for (Watches::iterator it = mWatches.begin(); it != mWatches.end(); ++it) { @@ -261,17 +250,17 @@ void Poller::Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const (*it)->mHappened = 0; - if ((AVAHI_WATCH_IN & events) && FD_ISSET(fd, &aReadFdSet)) + if ((AVAHI_WATCH_IN & events) && FD_ISSET(fd, &aMainloop.mReadFdSet)) { (*it)->mHappened |= AVAHI_WATCH_IN; } - if ((AVAHI_WATCH_OUT & events) && FD_ISSET(fd, &aWriteFdSet)) + if ((AVAHI_WATCH_OUT & events) && FD_ISSET(fd, &aMainloop.mWriteFdSet)) { (*it)->mHappened |= AVAHI_WATCH_OUT; } - if ((AVAHI_WATCH_ERR & events) && FD_ISSET(fd, &aErrorFdSet)) + if ((AVAHI_WATCH_ERR & events) && FD_ISSET(fd, &aMainloop.mErrorFdSet)) { (*it)->mHappened |= AVAHI_WATCH_ERR; } @@ -283,16 +272,14 @@ void Poller::Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const } } - std::vector expired; - for (Timers::iterator it = mTimers.begin(); it != mTimers.end(); ++it) { - if ((*it)->mTimeout == 0) + if ((*it)->mTimeout == Timepoint::min()) { continue; } - if (static_cast((*it)->mTimeout - now) <= 0) + if ((*it)->mTimeout <= now) { expired.push_back(*it); } @@ -301,22 +288,17 @@ void Poller::Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const for (std::vector::iterator it = expired.begin(); it != expired.end(); ++it) { AvahiTimeout *avahiTimeout = *it; + avahiTimeout->mCallback(avahiTimeout, avahiTimeout->mContext); } } -PublisherAvahi::PublisherAvahi(int aProtocol, - const char * aHost, - const char * aDomain, - StateHandler aHandler, - void * aContext) +PublisherAvahi::PublisherAvahi(int aProtocol, const char *aDomain, StateHandler aHandler, void *aContext) : mClient(nullptr) - , mGroup(nullptr) , mProtocol(aProtocol == AF_INET6 ? AVAHI_PROTO_INET6 : aProtocol == AF_INET ? AVAHI_PROTO_INET : AVAHI_PROTO_UNSPEC) - , mHost(aHost) , mDomain(aDomain) - , mState(kStateIdle) + , mState(State::kIdle) , mStateHandler(aHandler) , mContext(aContext) { @@ -328,18 +310,18 @@ PublisherAvahi::~PublisherAvahi(void) otbrError PublisherAvahi::Start(void) { - otbrError ret = OTBR_ERROR_NONE; - int error = 0; + otbrError error = OTBR_ERROR_NONE; + int avahiError = 0; - mClient = avahi_client_new(mPoller.GetAvahiPoll(), AVAHI_CLIENT_NO_FAIL, HandleClientState, this, &error); + mClient = avahi_client_new(mPoller.GetAvahiPoll(), AVAHI_CLIENT_NO_FAIL, HandleClientState, this, &avahiError); - if (error) + if (avahiError) { - otbrLog(OTBR_LOG_ERR, "Failed to create avahi client: %s!", avahi_strerror(error)); - ret = OTBR_ERROR_MDNS; + otbrLogErr("Failed to create avahi client: %s!", avahi_strerror(avahiError)); + error = OTBR_ERROR_MDNS; } - return ret; + return error; } bool PublisherAvahi::IsStarted(void) const @@ -349,24 +331,13 @@ bool PublisherAvahi::IsStarted(void) const void PublisherAvahi::Stop(void) { - mServices.clear(); - - if (mGroup) - { - int error = avahi_entry_group_reset(mGroup); - - if (error) - { - otbrLog(OTBR_LOG_ERR, "Failed to reset entry group: %s!", avahi_strerror(error)); - } - } + FreeAllGroups(); if (mClient) { avahi_client_free(mClient); mClient = nullptr; - mGroup = nullptr; - mState = kStateIdle; + mState = State::kIdle; mStateHandler(mContext, mState); } } @@ -383,32 +354,31 @@ void PublisherAvahi::HandleGroupState(AvahiEntryGroup *aGroup, AvahiEntryGroupSt void PublisherAvahi::HandleGroupState(AvahiEntryGroup *aGroup, AvahiEntryGroupState aState) { - assert(mGroup == aGroup || mGroup == nullptr); - - otbrLog(OTBR_LOG_INFO, "Avahi group change to state %d.", aState); - mGroup = aGroup; + otbrLogInfo("Avahi group change to state %d.", aState); /* Called whenever the entry group state changes */ switch (aState) { case AVAHI_ENTRY_GROUP_ESTABLISHED: /* The entry group has been established successfully */ - otbrLog(OTBR_LOG_INFO, "Group established."); + otbrLogInfo("Group established."); + CallHostOrServiceCallback(aGroup, OTBR_ERROR_NONE); break; case AVAHI_ENTRY_GROUP_COLLISION: - otbrLog(OTBR_LOG_ERR, "Name collision!"); + otbrLogErr("Name collision!"); + CallHostOrServiceCallback(aGroup, OTBR_ERROR_DUPLICATED); break; case AVAHI_ENTRY_GROUP_FAILURE: - otbrLog(OTBR_LOG_ERR, "Group failed: %s!", - avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(aGroup)))); /* Some kind of failure happened while we were registering our services */ + otbrLogErr("Group failed: %s!", avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(aGroup)))); + CallHostOrServiceCallback(aGroup, OTBR_ERROR_MDNS); break; case AVAHI_ENTRY_GROUP_UNCOMMITED: case AVAHI_ENTRY_GROUP_REGISTERING: - otbrLog(OTBR_LOG_ERR, "Group ready."); + otbrLogErr("Group ready."); break; default: @@ -417,40 +387,172 @@ void PublisherAvahi::HandleGroupState(AvahiEntryGroup *aGroup, AvahiEntryGroupSt } } -void PublisherAvahi::CreateGroup(AvahiClient *aClient) +void PublisherAvahi::CallHostOrServiceCallback(AvahiEntryGroup *aGroup, otbrError aError) const { - VerifyOrExit(mGroup == nullptr); + if (mHostHandler != nullptr) + { + const auto hostIt = + std::find_if(mHosts.begin(), mHosts.end(), [aGroup](const Host &aHost) { return aHost.mGroup == aGroup; }); + + if (hostIt != mHosts.end()) + { + mHostHandler(hostIt->mHostName.c_str(), aError, mHostHandlerContext); + } + } - VerifyOrExit((mGroup = avahi_entry_group_new(aClient, HandleGroupState, this)) != nullptr); + if (mServiceHandler != nullptr) + { + const auto serviceIt = std::find_if(mServices.begin(), mServices.end(), + [aGroup](const Service &aService) { return aService.mGroup == aGroup; }); + + if (serviceIt != mServices.end()) + { + mServiceHandler(serviceIt->mName.c_str(), serviceIt->mType.c_str(), aError, mServiceHandlerContext); + } + } +} + +PublisherAvahi::Hosts::iterator PublisherAvahi::FindHost(const char *aHostName) +{ + assert(aHostName != nullptr); + + return std::find_if(mHosts.begin(), mHosts.end(), + [aHostName](const Host &aHost) { return aHost.mHostName == aHostName; }); +} + +otbrError PublisherAvahi::CreateHost(AvahiClient &aClient, const char *aHostName, Hosts::iterator &aOutHostIt) +{ + assert(aHostName != nullptr); + + otbrError error = OTBR_ERROR_NONE; + Host newHost; + + newHost.mHostName = aHostName; + SuccessOrExit(error = CreateGroup(aClient, newHost.mGroup)); + + mHosts.push_back(newHost); + aOutHostIt = mHosts.end() - 1; exit: - if (mGroup == nullptr) + return error; +} + +PublisherAvahi::Services::iterator PublisherAvahi::FindService(const char *aName, const char *aType) +{ + assert(aName != nullptr); + assert(aType != nullptr); + + return std::find_if(mServices.begin(), mServices.end(), [aName, aType](const Service &aService) { + return aService.mName == aName && aService.mType == aType; + }); +} + +otbrError PublisherAvahi::CreateService(AvahiClient & aClient, + const char * aName, + const char * aType, + Services::iterator &aOutServiceIt) +{ + assert(aName != nullptr); + assert(aType != nullptr); + + otbrError error = OTBR_ERROR_NONE; + Service newService; + + newService.mName = aName; + newService.mType = aType; + SuccessOrExit(error = CreateGroup(aClient, newService.mGroup)); + + mServices.push_back(newService); + aOutServiceIt = mServices.end() - 1; + +exit: + return error; +} + +otbrError PublisherAvahi::CreateGroup(AvahiClient &aClient, AvahiEntryGroup *&aOutGroup) +{ + otbrError error = OTBR_ERROR_NONE; + + assert(aOutGroup == nullptr); + + aOutGroup = avahi_entry_group_new(&aClient, HandleGroupState, this); + VerifyOrExit(aOutGroup != nullptr, error = OTBR_ERROR_MDNS); + +exit: + if (error == OTBR_ERROR_MDNS) { - otbrLog(OTBR_LOG_ERR, "avahi_entry_group_new() failed: %s", avahi_strerror(avahi_client_errno(aClient))); + otbrLogErr("Failed to create entry group for avahi error: %s", avahi_strerror(avahi_client_errno(&aClient))); } + + return error; +} + +otbrError PublisherAvahi::ResetGroup(AvahiEntryGroup *aGroup) +{ + assert(aGroup != nullptr); + + otbrError error = OTBR_ERROR_NONE; + int avahiError = avahi_entry_group_reset(aGroup); + + if (avahiError) + { + error = OTBR_ERROR_MDNS; + otbrLogErr("Failed to reset entry group for avahi error: %s", avahi_strerror(avahiError)); + } + + return error; +} + +otbrError PublisherAvahi::FreeGroup(AvahiEntryGroup *aGroup) +{ + assert(aGroup != nullptr); + + otbrError error = OTBR_ERROR_NONE; + int avahiError = avahi_entry_group_free(aGroup); + + if (avahiError) + { + error = OTBR_ERROR_MDNS; + otbrLogErr("Failed to free entry group for avahi error: %s", avahi_strerror(avahiError)); + } + + return error; +} + +void PublisherAvahi::FreeAllGroups(void) +{ + for (Service &service : mServices) + { + FreeGroup(service.mGroup); + } + + mServices.clear(); + + for (Host &host : mHosts) + { + FreeGroup(host.mGroup); + } + + mHosts.clear(); } void PublisherAvahi::HandleClientState(AvahiClient *aClient, AvahiClientState aState) { - otbrLog(OTBR_LOG_INFO, "Avahi client state changed to %d.", aState); + otbrLogInfo("Avahi client state changed to %d.", aState); switch (aState) { case AVAHI_CLIENT_S_RUNNING: /* The server has startup successfully and registered its host * name on the network, so it's time to create our services */ - otbrLog(OTBR_LOG_INFO, "Avahi client ready."); - mState = kStateReady; - CreateGroup(aClient); + otbrLogInfo("Avahi client ready."); + mState = State::kReady; + mClient = aClient; mStateHandler(mContext, mState); - if (mGroup) - { - avahi_entry_group_commit(mGroup); - } break; case AVAHI_CLIENT_FAILURE: - otbrLog(OTBR_LOG_ERR, "Client failure: %s", avahi_strerror(avahi_client_errno(aClient))); - mState = kStateIdle; + otbrLogErr("Client failure: %s", avahi_strerror(avahi_client_errno(aClient))); + mState = State::kIdle; mStateHandler(mContext, mState); break; @@ -458,7 +560,7 @@ void PublisherAvahi::HandleClientState(AvahiClient *aClient, AvahiClientState aS /* Let's drop our registered services. When the server is back * in AVAHI_SERVER_RUNNING state we will register them * again with the new host name. */ - otbrLog(OTBR_LOG_ERR, "Client collision: %s", avahi_strerror(avahi_client_errno(aClient))); + otbrLogErr("Client collision: %s", avahi_strerror(avahi_client_errno(aClient))); // fall through @@ -467,14 +569,11 @@ void PublisherAvahi::HandleClientState(AvahiClient *aClient, AvahiClientState aS * might be caused by a host name change. We need to wait * for our own records to register until the host name is * properly esatblished. */ - if (mGroup) - { - avahi_entry_group_reset(mGroup); - } + FreeAllGroups(); break; case AVAHI_CLIENT_CONNECTING: - otbrLog(OTBR_LOG_DEBUG, "Connecting to avahi server"); + otbrLogDebug("Connecting to avahi server"); break; default: @@ -483,51 +582,61 @@ void PublisherAvahi::HandleClientState(AvahiClient *aClient, AvahiClientState aS } } -void PublisherAvahi::UpdateFdSet(fd_set & aReadFdSet, - fd_set & aWriteFdSet, - fd_set & aErrorFdSet, - int & aMaxFd, - timeval &aTimeout) +void PublisherAvahi::Update(MainloopContext &aMainloop) { - mPoller.UpdateFdSet(aReadFdSet, aWriteFdSet, aErrorFdSet, aMaxFd, aTimeout); + mPoller.Update(aMainloop); } -void PublisherAvahi::Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const fd_set &aErrorFdSet) +void PublisherAvahi::Process(const MainloopContext &aMainloop) { - mPoller.Process(aReadFdSet, aWriteFdSet, aErrorFdSet); + mPoller.Process(aMainloop); } -otbrError PublisherAvahi::PublishService(uint16_t aPort, const char *aName, const char *aType, ...) +otbrError PublisherAvahi::PublishService(const char * aHostName, + uint16_t aPort, + const char * aName, + const char * aType, + const TxtList &aTxtList) { - otbrError ret = OTBR_ERROR_ERRNO; - int error = 0; + otbrError error = OTBR_ERROR_NONE; + int avahiError = 0; + Services::iterator serviceIt = mServices.end(); + const char * safeHostName = (aHostName != nullptr) ? aHostName : ""; + const char * logHostName = (aHostName != nullptr) ? aHostName : "localhost"; + std::string fullHostName; // aligned with AvahiStringList - AvahiStringList buffer[kMaxSizeOfTxtRecord / sizeof(AvahiStringList)]; + AvahiStringList buffer[(kMaxSizeOfTxtRecord - 1) / sizeof(AvahiStringList) + 1]; AvahiStringList *last = nullptr; AvahiStringList *curr = buffer; - va_list args; size_t used = 0; - va_start(args, aType); + VerifyOrExit(mState == State::kReady, errno = EAGAIN, error = OTBR_ERROR_ERRNO); + VerifyOrExit(mClient != nullptr, errno = EAGAIN, error = OTBR_ERROR_ERRNO); + VerifyOrExit(aName != nullptr, error = OTBR_ERROR_INVALID_ARGS); + VerifyOrExit(aType != nullptr, error = OTBR_ERROR_INVALID_ARGS); - VerifyOrExit(mState == kStateReady, errno = EAGAIN); - VerifyOrExit(mGroup != nullptr, ret = OTBR_ERROR_MDNS); + if (aHostName != nullptr) + { + fullHostName = MakeFullName(aHostName); + aHostName = fullHostName.c_str(); + } - for (const char *name = va_arg(args, const char *); name; name = va_arg(args, const char *)) + for (const auto &txtEntry : aTxtList) { - int rval; - const char *value = va_arg(args, const char *); - size_t valueLength = va_arg(args, size_t); + const char * name = txtEntry.mName.c_str(); + size_t nameLength = txtEntry.mName.length(); + const uint8_t *value = txtEntry.mValue.data(); + size_t valueLength = txtEntry.mValue.size(); // +1 for the size of "=", avahi doesn't need '\0' at the end of the entry - size_t needed = sizeof(AvahiStringList) - sizeof(AvahiStringList::text) + strlen(name) + valueLength + 1; + size_t needed = sizeof(AvahiStringList) - sizeof(AvahiStringList::text) + nameLength + valueLength + 1; - VerifyOrExit(used + needed <= sizeof(buffer), errno = EMSGSIZE); + VerifyOrExit(used + needed <= sizeof(buffer), errno = EMSGSIZE, error = OTBR_ERROR_ERRNO); curr->next = last; last = curr; - rval = sprintf(reinterpret_cast(curr->text), "%s=", name); - assert(rval > 0); - memcpy(curr->text + rval, value, valueLength); - curr->size = valueLength + static_cast(rval); + memcpy(curr->text, name, nameLength); + curr->text[nameLength] = '='; + memcpy(curr->text + nameLength + 1, value, valueLength); + curr->size = nameLength + valueLength + 1; { const uint8_t *next = curr->text + curr->size; curr = OTBR_ALIGNED(next, AvahiStringList *); @@ -535,55 +644,215 @@ otbrError PublisherAvahi::PublishService(uint16_t aPort, const char *aName, cons used = static_cast(reinterpret_cast(curr) - reinterpret_cast(buffer)); } - for (Services::iterator it = mServices.begin(); it != mServices.end(); ++it) + serviceIt = FindService(aName, aType); + + if (serviceIt == mServices.end()) + { + SuccessOrExit(error = CreateService(*mClient, aName, aType, serviceIt)); + } + else if (serviceIt->mHostName != safeHostName || serviceIt->mPort != aPort) + { + SuccessOrExit(error = ResetGroup(serviceIt->mGroup)); + } + else { - if (!strncmp(it->mName, aName, sizeof(it->mName)) && !strncmp(it->mType, aType, sizeof(it->mType)) && - it->mPort == aPort) + otbrLogInfo("Update service %s.%s for host %s", aName, aType, logHostName); + avahiError = avahi_entry_group_update_service_txt_strlst(serviceIt->mGroup, AVAHI_IF_UNSPEC, mProtocol, + AvahiPublishFlags{}, aName, aType, mDomain, last); + if (avahiError == 0 && mServiceHandler != nullptr) { - otbrLog(OTBR_LOG_INFO, "MDNS update service %s", aName); - error = avahi_entry_group_update_service_txt_strlst( - mGroup, AVAHI_IF_UNSPEC, mProtocol, static_cast(0), aName, aType, mDomain, last); - SuccessOrExit(error); - ret = OTBR_ERROR_NONE; - ExitNow(); + // The handler should be called even if the request can be processed synchronously + mServiceHandler(aName, aType, OTBR_ERROR_NONE, mServiceHandlerContext); } + ExitNow(); } - otbrLog(OTBR_LOG_INFO, "MDNS create service %s", aName); - error = avahi_entry_group_add_service_strlst(mGroup, AVAHI_IF_UNSPEC, mProtocol, static_cast(0), - aName, aType, mDomain, mHost, aPort, last); - SuccessOrExit(error); + otbrLogInfo("Create service %s.%s for host %s", aName, aType, logHostName); + avahiError = + avahi_entry_group_add_service_strlst(serviceIt->mGroup, AVAHI_IF_UNSPEC, mProtocol, AvahiPublishFlags{}, aName, + aType, mDomain, aHostName, aPort, last); + SuccessOrExit(avahiError); + + otbrLogInfo("Commit service %s.%s", aName, aType); + avahiError = avahi_entry_group_commit(serviceIt->mGroup); + SuccessOrExit(avahiError); + + serviceIt->mHostName = safeHostName; + serviceIt->mPort = aPort; + +exit: + if (avahiError) + { + error = OTBR_ERROR_MDNS; + otbrLogErr("Failed to publish service for avahi error: %s!", avahi_strerror(avahiError)); + } + else if (error != OTBR_ERROR_NONE) + { + otbrLogErr("Failed to publish service: %s!", otbrErrorString(error)); + } + + if (error != OTBR_ERROR_NONE && serviceIt != mServices.end()) + { + FreeGroup(serviceIt->mGroup); + mServices.erase(serviceIt); + } + + return error; +} + +otbrError PublisherAvahi::UnpublishService(const char *aName, const char *aType) +{ + otbrError error = OTBR_ERROR_NONE; + Services::iterator serviceIt; + + VerifyOrExit(aName != nullptr, error = OTBR_ERROR_INVALID_ARGS); + VerifyOrExit(aType != nullptr, error = OTBR_ERROR_INVALID_ARGS); + + serviceIt = FindService(aName, aType); + VerifyOrExit(serviceIt != mServices.end()); + + otbrLogInfo("Unpublish service %s.%s", aName, aType); + error = FreeGroup(serviceIt->mGroup); + mServices.erase(serviceIt); + +exit: + return error; +} + +otbrError PublisherAvahi::PublishHost(const char *aName, const uint8_t *aAddress, uint8_t aAddressLength) +{ + otbrError error = OTBR_ERROR_NONE; + int avahiError = 0; + Hosts::iterator hostIt = mHosts.end(); + std::string fullHostName; + AvahiAddress address; + + VerifyOrExit(mState == State::kReady, errno = EAGAIN, error = OTBR_ERROR_ERRNO); + VerifyOrExit(mClient != nullptr, errno = EAGAIN, error = OTBR_ERROR_ERRNO); + VerifyOrExit(aName != nullptr, error = OTBR_ERROR_INVALID_ARGS); + VerifyOrExit(aAddress != nullptr, error = OTBR_ERROR_INVALID_ARGS); + VerifyOrExit(aAddressLength == sizeof(address.data.ipv6.address), error = OTBR_ERROR_INVALID_ARGS); + + fullHostName = MakeFullName(aName); + hostIt = FindHost(aName); + + if (hostIt == mHosts.end()) + { + SuccessOrExit(error = CreateHost(*mClient, aName, hostIt)); + } + else if (memcmp(hostIt->mAddress.data.ipv6.address, aAddress, aAddressLength)) + { + SuccessOrExit(error = ResetGroup(hostIt->mGroup)); + } + else { - Service service; - strcpy_safe(service.mName, sizeof(service.mName), aName); - strcpy_safe(service.mType, sizeof(service.mType), aType); - service.mPort = aPort; - mServices.push_back(service); + if (mHostHandler != nullptr) + { + // The handler should be called even if the request can be processed synchronously + mHostHandler(aName, OTBR_ERROR_NONE, mHostHandlerContext); + } + ExitNow(); } - ret = OTBR_ERROR_NONE; + address.proto = AVAHI_PROTO_INET6; + memcpy(&address.data.ipv6.address[0], aAddress, aAddressLength); + + otbrLogInfo("Create host %s", aName); + avahiError = avahi_entry_group_add_address(hostIt->mGroup, AVAHI_IF_UNSPEC, AVAHI_PROTO_INET6, + AVAHI_PUBLISH_NO_REVERSE, fullHostName.c_str(), &address); + SuccessOrExit(avahiError); + + otbrLogInfo("Commit host %s", aName); + avahiError = avahi_entry_group_commit(hostIt->mGroup); + SuccessOrExit(avahiError); + + hostIt->mAddress = address; exit: - va_end(args); - if (error) + if (avahiError) + { + error = OTBR_ERROR_MDNS; + otbrLogErr("Failed to publish host for avahi error: %s!", avahi_strerror(avahiError)); + } + else if (error != OTBR_ERROR_NONE) { - ret = OTBR_ERROR_MDNS; - otbrLog(OTBR_LOG_ERR, "Failed to publish service for avahi error: %s!", avahi_strerror(error)); + otbrLogErr("Failed to publish host: %s!", otbrErrorString(error)); } - if (ret == OTBR_ERROR_ERRNO) + if (error != OTBR_ERROR_NONE && hostIt != mHosts.end()) { - otbrLog(OTBR_LOG_ERR, "Failed to publish service: %s!", strerror(errno)); + FreeGroup(hostIt->mGroup); + mHosts.erase(hostIt); } - return ret; + return error; +} + +otbrError PublisherAvahi::UnpublishHost(const char *aName) +{ + otbrError error = OTBR_ERROR_NONE; + Hosts::iterator hostIt; + + VerifyOrExit(aName != nullptr, error = OTBR_ERROR_INVALID_ARGS); + + hostIt = FindHost(aName); + VerifyOrExit(hostIt != mHosts.end()); + + otbrLogInfo("Delete host %s", aName); + error = FreeGroup(hostIt->mGroup); + mHosts.erase(hostIt); + +exit: + return error; +} + +std::string PublisherAvahi::MakeFullName(const char *aName) +{ + assert(aName != nullptr); + + std::string fullHostName(aName); + + fullHostName += '.'; + fullHostName += (mDomain == nullptr ? "local." : mDomain); + + return fullHostName; +} + +void PublisherAvahi::SubscribeService(const std::string &aType, const std::string &aInstanceName) +{ + OTBR_UNUSED_VARIABLE(aType); + OTBR_UNUSED_VARIABLE(aInstanceName); + + VerifyOrDie(false, "SubscribeService is not implemented with avahi"); +} + +void PublisherAvahi::UnsubscribeService(const std::string &aType, const std::string &aInstanceName) +{ + OTBR_UNUSED_VARIABLE(aType); + OTBR_UNUSED_VARIABLE(aInstanceName); + + VerifyOrDie(false, "UnsubscribeService is not implemented with avahi"); +} + +void PublisherAvahi::SubscribeHost(const std::string &aHostName) +{ + OTBR_UNUSED_VARIABLE(aHostName); + + VerifyOrDie(false, "SubscribeHost is not implemented with avahi"); +} + +void PublisherAvahi::UnsubscribeHost(const std::string &aHostName) +{ + OTBR_UNUSED_VARIABLE(aHostName); + + VerifyOrDie(false, "UnsubscribeHost is not implemented with avahi"); } -Publisher *Publisher::Create(int aFamily, const char *aHost, const char *aDomain, StateHandler aHandler, void *aContext) +Publisher *Publisher::Create(int aFamily, const char *aDomain, StateHandler aHandler, void *aContext) { - return new PublisherAvahi(aFamily, aHost, aDomain, aHandler, aContext); + return new PublisherAvahi(aFamily, aDomain, aHandler, aContext); } void Publisher::Destroy(Publisher *aPublisher) diff --git a/src/mdns/mdns_avahi.hpp b/src/mdns/mdns_avahi.hpp index 19b53c35bb9..11f2f4c6213 100644 --- a/src/mdns/mdns_avahi.hpp +++ b/src/mdns/mdns_avahi.hpp @@ -42,6 +42,8 @@ #include #include "mdns.hpp" +#include "common/mainloop.hpp" +#include "common/time.hpp" /** * @addtogroup border-router-mdns @@ -91,7 +93,7 @@ struct AvahiWatch */ struct AvahiTimeout { - unsigned long mTimeout; ///< Absolute time when this timer timeout. + otbr::Timepoint mTimeout; ///< Absolute time when this timer timeout. AvahiTimeoutCallback mCallback; ///< The function to be called when timeout. void * mContext; ///< The pointer to application-specific context. void * mPoller; ///< The poller created this timer. @@ -116,7 +118,7 @@ namespace Mdns { * This class implements the AvahiPoll. * */ -class Poller +class Poller : public MainloopProcessor { public: /** @@ -126,26 +128,20 @@ class Poller Poller(void); /** - * This method updates the fd_set and timeout for mainloop. + * This method updates the mainloop context. * - * @param[inout] aReadFdSet A reference to fd_set for polling read. - * @param[inout] aWriteFdSet A reference to fd_set for polling write. - * @param[inout] aErrorFdSet A reference to fd_set for polling error. - * @param[inout] aMaxFd A reference to the max file descriptor. - * @param[inout] aTimeout A reference to the timeout. + * @param[inout] aMainloop A reference to the mainloop to be updated. * */ - void UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, fd_set &aErrorFdSet, int &aMaxFd, timeval &aTimeout); + void Update(MainloopContext &aMainloop) override; /** - * This method performs avahi poll processing. + * This method processes mainloop events. * - * @param[in] aReadFdSet A reference to read file descriptors. - * @param[in] aWriteFdSet A reference to write file descriptors. - * @param[in] aErrorFdSet A reference to error file descriptors. + * @param[in] aMainloop A reference to the mainloop context. * */ - void Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const fd_set &aErrorFdSet); + void Process(const MainloopContext &aMainloop) override; /** * This method returns the AvahiPoll. @@ -194,33 +190,127 @@ class PublisherAvahi : public Publisher * The constructor to initialize a Publisher. * * @param[in] aProtocol The protocol used for publishing. IPv4, IPv6 or both. - * @param[in] aHost The name of host residing the services to be published. - nullptr to use default. * @param[in] aDomain The domain of the host. nullptr to use default. * @param[in] aHandler The function to be called when state changes. * @param[in] aContext A pointer to application-specific context. * */ - PublisherAvahi(int aProtocol, const char *aHost, const char *aDomain, StateHandler aHandler, void *aContext); + PublisherAvahi(int aProtocol, const char *aDomain, StateHandler aHandler, void *aContext); - ~PublisherAvahi(void); + ~PublisherAvahi(void) override; /** * This method publishes or updates a service. * - * @note only text record can be updated. - * + * @param[in] aHostName The name of the host which this service resides on. If NULL is provided, + * this service resides on local host and it is the implementation to provide + * specific host name. Otherwise, the caller MUST publish the host with method + * PublishHost. * @param[in] aName The name of this service. * @param[in] aType The type of this service. * @param[in] aPort The port number of this service. - * @param[in] ... Pointers to null-terminated string of key and value for text record. - * The last argument must be nullptr. + * @param[in] aTxtList A list of TXT name/value pairs. * * @retval OTBR_ERROR_NONE Successfully published or updated the service. * @retval OTBR_ERROR_ERRNO Failed to publish or update the service. * */ - otbrError PublishService(uint16_t aPort, const char *aName, const char *aType, ...); + otbrError PublishService(const char * aHostName, + uint16_t aPort, + const char * aName, + const char * aType, + const TxtList &aTxtList) override; + + /** + * This method un-publishes a service. + * + * @param[in] aName The name of this service. + * @param[in] aType The type of this service. + * + * @retval OTBR_ERROR_NONE Successfully un-published the service. + * @retval OTBR_ERROR_ERRNO Failed to un-publish the service. + * + */ + otbrError UnpublishService(const char *aName, const char *aType) override; + + /** + * This method publishes or updates a host. + * + * Publishing a host is advertising an AAAA RR for the host name. This method should be called + * before a service with non-null host name is published. + * + * @param[in] aName The name of the host. + * @param[in] aAddress The address of the host. + * @param[in] aAddressLength The length of @p aAddress. + * + * @retval OTBR_ERROR_NONE Successfully published or updated the host. + * @retval OTBR_ERROR_ERRNO Failed to publish or update the host. + * + */ + otbrError PublishHost(const char *aName, const uint8_t *aAddress, uint8_t aAddressLength) override; + + /** + * This method un-publishes a host. + * + * @param[in] aName A host name. + * + * @retval OTBR_ERROR_NONE Successfully un-published the host. + * @retval OTBR_ERROR_ERRNO Failed to un-publish the host. + * + * @note All services reside on this host should be un-published by UnpublishService. + * + */ + otbrError UnpublishHost(const char *aName) override; + + /** + * This method subscribes a given service or service instance. If @p aInstanceName is not empty, this method + * subscribes the service instance. Otherwise, this method subscribes the service. + * + * mDNS implementations should use the `DiscoveredServiceInstanceCallback` function to notify discovered service + * instances. + * + * @note Discovery Proxy implementation guarantees no duplicate subscriptions for the same service or service + * instance. + * + * @param[in] aType The service type. + * @param[in] aInstanceName The service instance to subscribe, or empty to subscribe the service. + * + */ + void SubscribeService(const std::string &aType, const std::string &aInstanceName) override; + + /** + * This method unsubscribes a given service or service instance. If @p aInstanceName is not empty, this method + * unsubscribes the service instance. Otherwise, this method unsubscribes the service. + * + * @note Discovery Proxy implementation guarantees no redundant unsubscription for a service or service instance. + * + * @param[in] aType The service type. + * @param[in] aInstanceName The service instance to unsubscribe, or empty to unsubscribe the service. + * + */ + void UnsubscribeService(const std::string &aType, const std::string &aInstanceName) override; + + /** + * This method subscribes a given host. + * + * mDNS implementations should use the `DiscoveredHostCallback` function to notify discovered hosts. + * + * @note Discovery Proxy implementation guarantees no duplicate subscriptions for the same host. + * + * @param[in] aHostName The host name (without domain). + * + */ + void SubscribeHost(const std::string &aHostName) override; + + /** + * This method unsubscribes a given host. + * + * @note Discovery Proxy implementation guarantees no redundant unsubscription for a host. + * + * @param[in] aHostName The host name (without domain). + * + */ + void UnsubscribeHost(const std::string &aHostName) override; /** * This method starts the MDNS service. @@ -229,7 +319,7 @@ class PublisherAvahi : public Publisher * @retval OTBR_ERROR_MDNS Failed to start MDNS service. * */ - otbrError Start(void); + otbrError Start(void) override; /** * This method checks if publisher has been started. @@ -238,40 +328,34 @@ class PublisherAvahi : public Publisher * @retval false Not started. * */ - bool IsStarted(void) const; + bool IsStarted(void) const override; /** * This method stops the MDNS service. * */ - void Stop(void); + void Stop(void) override; /** - * This method performs avahi poll processing. + * This method updates the mainloop context. * - * @param[in] aReadFdSet A reference to read file descriptors. - * @param[in] aWriteFdSet A reference to write file descriptors. - * @param[in] aErrorFdSet A reference to error file descriptors. + * @param[inout] aMainloop A reference to the mainloop to be updated. * */ - void Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const fd_set &aErrorFdSet); + void Update(MainloopContext &aMainloop) override; /** - * This method updates the fd_set and timeout for mainloop. + * This method processes mainloop events. * - * @param[inout] aReadFdSet A reference to fd_set for polling read. - * @param[inout] aWriteFdSet A reference to fd_set for polling write. - * @param[inout] aErrorFdSet A reference to fd_set for polling error. - * @param[inout] aMaxFd A reference to the max file descriptor. - * @param[inout] aTimeout A reference to the timeout. + * @param[in] aMainloop A reference to the mainloop context. * */ - void UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, fd_set &aErrorFdSet, int &aMaxFd, timeval &aTimeout); + void Process(const MainloopContext &aMainloop) override; private: enum { - kMaxSizeOfTxtRecord = 128, + kMaxSizeOfTxtRecord = 256, kMaxSizeOfServiceName = AVAHI_LABEL_MAX, kMaxSizeOfHost = AVAHI_LABEL_MAX, kMaxSizeOfDomain = AVAHI_LABEL_MAX, @@ -280,30 +364,55 @@ class PublisherAvahi : public Publisher struct Service { - char mName[kMaxSizeOfServiceName]; - char mType[kMaxSizeOfServiceType]; - uint16_t mPort; + std::string mName; + std::string mType; + std::string mHostName; + uint16_t mPort = 0; + AvahiEntryGroup *mGroup = nullptr; }; typedef std::vector Services; + struct Host + { + std::string mHostName; + AvahiAddress mAddress = {}; + AvahiEntryGroup *mGroup = nullptr; + }; + + typedef std::vector Hosts; + static void HandleClientState(AvahiClient *aClient, AvahiClientState aState, void *aContext); void HandleClientState(AvahiClient *aClient, AvahiClientState aState); - void CreateGroup(AvahiClient *aClient); - static void HandleGroupState(AvahiEntryGroup *aGroup, AvahiEntryGroupState aState, void *aContext); - void HandleGroupState(AvahiEntryGroup *aGroup, AvahiEntryGroupState aState); - - Services mServices; - AvahiClient * mClient; - AvahiEntryGroup *mGroup; - Poller mPoller; - int mProtocol; - const char * mHost; - const char * mDomain; - State mState; - StateHandler mStateHandler; - void * mContext; + Hosts::iterator FindHost(const char *aHostName); + otbrError CreateHost(AvahiClient &aClient, const char *aHostName, Hosts::iterator &aOutHostIt); + + Services::iterator FindService(const char *aName, const char *aType); + otbrError CreateService(AvahiClient & aClient, + const char * aName, + const char * aType, + Services::iterator &aOutServiceIt); + + otbrError CreateGroup(AvahiClient &aClient, AvahiEntryGroup *&aOutGroup); + static otbrError ResetGroup(AvahiEntryGroup *aGroup); + static otbrError FreeGroup(AvahiEntryGroup *aGroup); + void FreeAllGroups(void); + static void HandleGroupState(AvahiEntryGroup *aGroup, AvahiEntryGroupState aState, void *aContext); + void HandleGroupState(AvahiEntryGroup *aGroup, AvahiEntryGroupState aState); + void CallHostOrServiceCallback(AvahiEntryGroup *aGroup, otbrError aError) const; + + std::string MakeFullName(const char *aName); + + AvahiClient *mClient; + Hosts mHosts; + Services mServices; + Poller mPoller; + int mProtocol; + const char * mDomain; + State mState; + StateHandler mStateHandler; + void * mContext; }; } // namespace Mdns diff --git a/src/mdns/mdns_mdnssd.cpp b/src/mdns/mdns_mdnssd.cpp index f2d1af6136e..e6b8a02c88d 100644 --- a/src/mdns/mdns_mdnssd.cpp +++ b/src/mdns/mdns_mdnssd.cpp @@ -28,19 +28,25 @@ /** * @file - * This file implements MDNS service based on avahi. + * This file implements mDNS service based on mDNSResponder. */ +#define OTBR_LOG_TAG "MDNS" + #include "mdns/mdns_mdnssd.hpp" +#include + #include #include #include +#include #include #include #include #include "common/code_utils.hpp" +#include "common/dns_utils.hpp" #include "common/logging.hpp" #include "common/time.hpp" #include "utils/strcpy_utils.hpp" @@ -49,6 +55,46 @@ namespace otbr { namespace Mdns { +static otbrError DNSErrorToOtbrError(DNSServiceErrorType aError) +{ + otbrError error; + + switch (aError) + { + case kDNSServiceErr_NoError: + error = OTBR_ERROR_NONE; + break; + + case kDNSServiceErr_NoSuchKey: + case kDNSServiceErr_NoSuchName: + case kDNSServiceErr_NoSuchRecord: + error = OTBR_ERROR_NOT_FOUND; + break; + + case kDNSServiceErr_Invalid: + case kDNSServiceErr_BadParam: + case kDNSServiceErr_BadFlags: + case kDNSServiceErr_BadInterfaceIndex: + error = OTBR_ERROR_INVALID_ARGS; + break; + + case kDNSServiceErr_AlreadyRegistered: + case kDNSServiceErr_NameConflict: + error = OTBR_ERROR_DUPLICATED; + break; + + case kDNSServiceErr_Unsupported: + error = OTBR_ERROR_NOT_IMPLEMENTED; + break; + + default: + error = OTBR_ERROR_MDNS; + break; + } + + return error; +} + static const char *DNSErrorToString(DNSServiceErrorType aError) { switch (aError) @@ -162,18 +208,14 @@ static const char *DNSErrorToString(DNSServiceErrorType aError) } } -PublisherMDnsSd::PublisherMDnsSd(int aProtocol, - const char * aHost, - const char * aDomain, - StateHandler aHandler, - void * aContext) - : mHost(aHost) +PublisherMDnsSd::PublisherMDnsSd(int aProtocol, const char *aDomain, StateHandler aHandler, void *aContext) + : mHostsRef(nullptr) , mDomain(aDomain) - , mState(kStateIdle) + , mState(State::kIdle) , mStateHandler(aHandler) , mContext(aContext) { - (void)aProtocol; + OTBR_UNUSED_VARIABLE(aProtocol); } PublisherMDnsSd::~PublisherMDnsSd(void) @@ -183,81 +225,150 @@ PublisherMDnsSd::~PublisherMDnsSd(void) otbrError PublisherMDnsSd::Start(void) { - mState = kStateReady; - mStateHandler(mContext, kStateReady); + mState = State::kReady; + mStateHandler(mContext, State::kReady); return OTBR_ERROR_NONE; } bool PublisherMDnsSd::IsStarted(void) const { - return mState == kStateReady; + return mState == State::kReady; } void PublisherMDnsSd::Stop(void) { - VerifyOrExit(mState == kStateReady); + VerifyOrExit(mState == State::kReady); - for (Services::iterator it = mServices.begin(); it != mServices.end(); ++it) + for (Service &service : mServices) { - otbrLog(OTBR_LOG_INFO, "MDNS remove service %s", it->mName); - DNSServiceRefDeallocate(it->mService); + otbrLogInfo("Remove service %s.%s", service.mName, service.mType); + DNSServiceRefDeallocate(service.mService); } - mServices.clear(); + otbrLogInfo("Remove all hosts"); + if (mHostsRef != nullptr) + { + DNSServiceRefDeallocate(mHostsRef); + mHostsRef = nullptr; + } + + mHosts.clear(); + exit: return; } -void PublisherMDnsSd::UpdateFdSet(fd_set & aReadFdSet, - fd_set & aWriteFdSet, - fd_set & aErrorFdSet, - int & aMaxFd, - timeval &aTimeout) +void PublisherMDnsSd::Update(MainloopContext &aMainloop) { - (void)aWriteFdSet; - (void)aErrorFdSet; - (void)aTimeout; + for (Service &service : mServices) + { + assert(service.mService != nullptr); - for (Services::iterator it = mServices.begin(); it != mServices.end(); ++it) + int fd = DNSServiceRefSockFD(service.mService); + + assert(fd != -1); + + FD_SET(fd, &aMainloop.mReadFdSet); + + aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, fd); + } + + if (mHostsRef != nullptr) { - int fd = DNSServiceRefSockFD(it->mService); + int fd = DNSServiceRefSockFD(mHostsRef); assert(fd != -1); - FD_SET(fd, &aReadFdSet); + FD_SET(fd, &aMainloop.mReadFdSet); + + aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, fd); + } - if (fd > aMaxFd) + for (Subscription &subscription : mSubscribedServices) + { + if (subscription.mServiceRef != nullptr) { - aMaxFd = fd; + int fd = DNSServiceRefSockFD(subscription.mServiceRef); + assert(fd != -1); + + FD_SET(fd, &aMainloop.mReadFdSet); + aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, fd); + } + } + + for (Subscription &subscription : mSubscribedHosts) + { + if (subscription.mServiceRef != nullptr) + { + int fd = DNSServiceRefSockFD(subscription.mServiceRef); + assert(fd != -1); + + FD_SET(fd, &aMainloop.mReadFdSet); + aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, fd); } } } -void PublisherMDnsSd::Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const fd_set &aErrorFdSet) +void PublisherMDnsSd::Process(const MainloopContext &aMainloop) { std::vector readyServices; - (void)aWriteFdSet; - (void)aErrorFdSet; + for (Service &service : mServices) + { + int fd = DNSServiceRefSockFD(service.mService); + + if (FD_ISSET(fd, &aMainloop.mReadFdSet)) + { + readyServices.push_back(service.mService); + } + } - for (Services::iterator it = mServices.begin(); it != mServices.end(); ++it) + if (mHostsRef != nullptr) { - int fd = DNSServiceRefSockFD(it->mService); + int fd = DNSServiceRefSockFD(mHostsRef); + + if (FD_ISSET(fd, &aMainloop.mReadFdSet)) + { + readyServices.push_back(mHostsRef); + } + } + + for (Subscription &subscription : mSubscribedServices) + { + if (subscription.mServiceRef != nullptr) + { + int fd = DNSServiceRefSockFD(subscription.mServiceRef); + assert(fd != -1); + + if (FD_ISSET(fd, &aMainloop.mReadFdSet)) + { + readyServices.push_back(subscription.mServiceRef); + } + } + } - if (FD_ISSET(fd, &aReadFdSet)) + for (Subscription &service : mSubscribedHosts) + { + if (service.mServiceRef != nullptr) { - readyServices.push_back(it->mService); + int fd = DNSServiceRefSockFD(service.mServiceRef); + assert(fd != -1); + + if (FD_ISSET(fd, &aMainloop.mReadFdSet)) + { + readyServices.push_back(service.mServiceRef); + } } } - for (std::vector::iterator it = readyServices.begin(); it != readyServices.end(); ++it) + for (DNSServiceRef serviceRef : readyServices) { - DNSServiceErrorType error = DNSServiceProcessResult(*it); + DNSServiceErrorType error = DNSServiceProcessResult(serviceRef); if (error != kDNSServiceErr_NoError) { - otbrLog(OTBR_LOG_WARNING, "DNSServiceProcessResult failed: %s", DNSErrorToString(error)); + otbrLogWarning("DNSServiceProcessResult failed: %s", DNSErrorToString(error)); } } } @@ -281,143 +392,462 @@ void PublisherMDnsSd::HandleServiceRegisterResult(DNSServiceRef aService const char * aType, const char * aDomain) { - otbrLog(OTBR_LOG_INFO, "Got a reply for service %s.%s%s", aName, aType, aDomain); + OTBR_UNUSED_VARIABLE(aDomain); + + otbrError error = DNSErrorToOtbrError(aError); + std::string originalInstanceName; + ServiceIterator service = FindPublishedService(aServiceRef); + + VerifyOrExit(service != mServices.end()); + + // mDNSResponder could auto-rename the service instance name when name conflict + // is detected. In this case, `aName` may not match `service->mName` and we + // should use the original `service->mName` to find associated SRP service. + originalInstanceName = service->mName; + + otbrLogInfo("Received reply for service %s.%s", originalInstanceName.c_str(), aType); + + if (originalInstanceName != aName) + { + otbrLogInfo("Service %s.%s renamed to %s.%s", originalInstanceName.c_str(), aType, aName, aType); + } if (aError == kDNSServiceErr_NoError) { + otbrLogInfo("Successfully registered service %s.%s", originalInstanceName.c_str(), aType); if (aFlags & kDNSServiceFlagsAdd) { - otbrLog(OTBR_LOG_INFO, "MDNS added service %s", aName); - RecordService(aName, aType, aServiceRef); + RecordService(originalInstanceName.c_str(), aType, aServiceRef); } else { - otbrLog(OTBR_LOG_INFO, "MDNS remove service %s", aName); - DiscardService(aName, aType, aServiceRef); + DiscardService(originalInstanceName.c_str(), aType, aServiceRef); } } else { - otbrLog(OTBR_LOG_ERR, "Failed to register service %s: %s", aName, DNSErrorToString(aError)); - DiscardService(aName, aType, aServiceRef); + otbrLogErr("Failed to register service %s.%s: %s", originalInstanceName.c_str(), aType, + DNSErrorToString(aError)); + DiscardService(originalInstanceName.c_str(), aType, aServiceRef); } + + if (mServiceHandler != nullptr) + { + // TODO: pass the renewed service instance name back to SRP server handler. + mServiceHandler(originalInstanceName.c_str(), aType, error, mServiceHandlerContext); + } + +exit: + return; } void PublisherMDnsSd::DiscardService(const char *aName, const char *aType, DNSServiceRef aServiceRef) { - for (Services::iterator it = mServices.begin(); it != mServices.end(); ++it) + ServiceIterator service = FindPublishedService(aName, aType); + + if (service != mServices.end()) { - if (!strncmp(it->mName, aName, sizeof(it->mName)) && !strncmp(it->mType, aType, sizeof(it->mType))) - { - assert(aServiceRef == it->mService); - mServices.erase(it); - DNSServiceRefDeallocate(aServiceRef); - aServiceRef = nullptr; - break; - } - } + assert(aServiceRef == nullptr || aServiceRef == service->mService); + OTBR_UNUSED_VARIABLE(aServiceRef); - assert(aServiceRef == nullptr); + otbrLogInfo("Remove service ref %p", service->mService); + + DNSServiceRefDeallocate(service->mService); + mServices.erase(service); + } } void PublisherMDnsSd::RecordService(const char *aName, const char *aType, DNSServiceRef aServiceRef) { - for (Services::iterator it = mServices.begin(); it != mServices.end(); ++it) + ServiceIterator service = FindPublishedService(aName, aType); + + if (service == mServices.end()) + { + Service newService; + + otbrLogInfo("Add service: %s.%s (ref: %p)", aName, aType, aServiceRef); + + strcpy(newService.mName, aName); + strcpy(newService.mType, aType); + newService.mService = aServiceRef; + mServices.push_back(newService); + } + else + { + assert(service->mService == aServiceRef); + } +} + +otbrError PublisherMDnsSd::PublishService(const char * aHostName, + uint16_t aPort, + const char * aName, + const char * aType, + const TxtList &aTxtList) +{ + otbrError ret = OTBR_ERROR_NONE; + int error = 0; + uint8_t txt[kMaxSizeOfTxtRecord]; + uint16_t txtLength = sizeof(txt); + ServiceIterator service = FindPublishedService(aName, aType); + DNSServiceRef serviceRef = nullptr; + char fullHostName[kMaxSizeOfDomain]; + + if (aHostName != nullptr) { - if (!strncmp(it->mName, aName, sizeof(it->mName)) && !strncmp(it->mType, aType, sizeof(it->mType))) + HostIterator host = FindPublishedHost(aHostName); + + // Make sure that the host has been published. + VerifyOrExit(host != mHosts.end(), ret = OTBR_ERROR_INVALID_ARGS); + SuccessOrExit(error = MakeFullName(fullHostName, sizeof(fullHostName), aHostName)); + } + + SuccessOrExit(ret = EncodeTxtData(aTxtList, txt, txtLength)); + + if (service != mServices.end()) + { + otbrLogInfo("Update service %s.%s", aName, aType); + + // Setting TTL to 0 to use default value. + SuccessOrExit(error = DNSServiceUpdateRecord(service->mService, nullptr, 0, txtLength, txt, /* ttl */ 0)); + + if (mServiceHandler != nullptr) { - assert(aServiceRef == it->mService); - ExitNow(); + mServiceHandler(aName, aType, DNSErrorToOtbrError(error), mServiceHandlerContext); } } + else + { + SuccessOrExit(error = DNSServiceRegister(&serviceRef, /* flags */ 0, kDNSServiceInterfaceIndexAny, aName, aType, + mDomain, (aHostName != nullptr) ? fullHostName : nullptr, htons(aPort), + txtLength, txt, HandleServiceRegisterResult, this)); + RecordService(aName, aType, serviceRef); + } +exit: + if (error != kDNSServiceErr_NoError) { - Service service; + ret = OTBR_ERROR_MDNS; + otbrLogErr("Failed to publish service for mdnssd error: %s!", DNSErrorToString(error)); + } + return ret; +} + +otbrError PublisherMDnsSd::UnpublishService(const char *aName, const char *aType) +{ + DiscardService(aName, aType); + return OTBR_ERROR_NONE; +} - strcpy_safe(service.mName, sizeof(service.mName), aName); - strcpy_safe(service.mType, sizeof(service.mType), aType); - service.mService = aServiceRef; - mServices.push_back(service); +otbrError PublisherMDnsSd::DiscardHost(const char *aName, bool aSendGoodbye) +{ + otbrError ret = OTBR_ERROR_NONE; + int error = 0; + HostIterator host = FindPublishedHost(aName); + + VerifyOrExit(mHostsRef != nullptr && host != mHosts.end()); + + otbrLogInfo("Remove host: %s (record ref: %p)", host->mName, host->mRecord); + + if (aSendGoodbye) + { + // The Bonjour mDNSResponder somehow doesn't send goodbye message for the AAAA record when it is + // removed by `DNSServiceRemoveRecord`. Per RFC 6762, a goodbye message of a record sets its TTL + // to zero but the receiver should record the TTL of 1 and flushes the cache 1 second later. Here + // we remove the AAAA record after updating its TTL to 1 second. This has the same effect as + // sending a goodbye message. + // TODO: resolve the goodbye issue with Bonjour mDNSResponder. + error = DNSServiceUpdateRecord(mHostsRef, host->mRecord, kDNSServiceFlagsUnique, host->mAddress.size(), + &host->mAddress.front(), /* ttl */ 1); + // Do not SuccessOrExit so that we always erase the host entry. + + DNSServiceRemoveRecord(mHostsRef, host->mRecord, /* flags */ 0); } + mHosts.erase(host); exit: - return; + if (error != kDNSServiceErr_NoError) + { + ret = OTBR_ERROR_MDNS; + otbrLogErr("Failed to remove host %s for mdnssd error: %s!", aName, DNSErrorToString(error)); + } + return ret; } -otbrError PublisherMDnsSd::PublishService(uint16_t aPort, const char *aName, const char *aType, ...) +void PublisherMDnsSd::RecordHost(const char * aName, + const uint8_t *aAddress, + uint8_t aAddressLength, + DNSRecordRef aRecordRef) { - otbrError ret = OTBR_ERROR_NONE; - int error = 0; - va_list args; - uint8_t txt[kMaxSizeOfTxtRecord]; - uint8_t * cur = txt; - DNSServiceRef serviceRef = nullptr; - - va_start(args, aType); + HostIterator host = FindPublishedHost(aName); - for (const char *name = va_arg(args, const char *); name; name = va_arg(args, const char *)) + if (host == mHosts.end()) { - const char * value = va_arg(args, const char *); - const size_t nameLength = strlen(name); - const size_t valueLength = va_arg(args, size_t); - size_t recordLength = nameLength + 1 + valueLength; + Host newHost; - assert(nameLength > 0 && valueLength > 0 && recordLength < kMaxTextRecordSize); + otbrLogInfo("Add new host %s", aName); - if (cur + recordLength >= txt + sizeof(txt)) - { - otbrLog(OTBR_LOG_WARNING, "Skip text record too much long: %s=%s", name, value); - continue; - } + strcpy(newHost.mName, aName); + std::copy(aAddress, aAddress + aAddressLength, newHost.mAddress.begin()); + newHost.mRecord = aRecordRef; + mHosts.push_back(newHost); + } + else + { + otbrLogInfo("Update existing host %s", host->mName); - assert(recordLength <= 255); - cur[0] = static_cast(recordLength); - cur += 1; + // The address of the host may be updated. + std::copy(aAddress, aAddress + aAddressLength, host->mAddress.begin()); + assert(host->mRecord == aRecordRef); + } +} + +otbrError PublisherMDnsSd::PublishHost(const char *aName, const uint8_t *aAddress, uint8_t aAddressLength) +{ + otbrError ret = OTBR_ERROR_NONE; + int error = 0; + char fullName[kMaxSizeOfDomain]; + HostIterator host = FindPublishedHost(aName); - memcpy(cur, name, nameLength); - cur += nameLength; + // Supports only IPv6 for now, may support IPv4 in the future. + VerifyOrExit(aAddressLength == OTBR_IP6_ADDRESS_SIZE, error = OTBR_ERROR_INVALID_ARGS); - cur[0] = '='; - cur += 1; + SuccessOrExit(ret = MakeFullName(fullName, sizeof(fullName), aName)); - memcpy(cur, value, valueLength); - cur += valueLength; + if (mHostsRef == nullptr) + { + SuccessOrExit(error = DNSServiceCreateConnection(&mHostsRef)); } - va_end(args); + if (host != mHosts.end()) + { + otbrLogInfo("Update existing host %s", aName); + SuccessOrExit(error = DNSServiceUpdateRecord(mHostsRef, host->mRecord, kDNSServiceFlagsUnique, aAddressLength, + aAddress, /* ttl */ 0)); + + RecordHost(aName, aAddress, aAddressLength, host->mRecord); + if (mHostHandler != nullptr) + { + mHostHandler(aName, DNSErrorToOtbrError(error), mHostHandlerContext); + } + } + else + { + DNSRecordRef record; + + otbrLogInfo("Publish new host %s", aName); + SuccessOrExit(error = DNSServiceRegisterRecord(mHostsRef, &record, kDNSServiceFlagsUnique, + kDNSServiceInterfaceIndexAny, fullName, kDNSServiceType_AAAA, + kDNSServiceClass_IN, aAddressLength, aAddress, /* ttl */ 0, + HandleRegisterHostResult, this)); + RecordHost(aName, aAddress, aAddressLength, record); + } - for (Services::iterator it = mServices.begin(); it != mServices.end(); ++it) +exit: + if (error != kDNSServiceErr_NoError) { - if (!strncmp(it->mName, aName, sizeof(it->mName)) && !strncmp(it->mType, aType, sizeof(it->mType))) + if (mHostsRef != nullptr) { - otbrLog(OTBR_LOG_INFO, "MDNS remove current service %s", aName); - DNSServiceUpdateRecord(it->mService, nullptr, 0, static_cast(cur - txt), txt, 0); - ExitNow(); + DNSServiceRefDeallocate(mHostsRef); + mHostsRef = nullptr; } + + ret = OTBR_ERROR_MDNS; + otbrLogErr("Failed to publish/update host %s for mdnssd error: %s!", aName, DNSErrorToString(error)); } + return ret; +} + +otbrError PublisherMDnsSd::UnpublishHost(const char *aName) +{ + return DiscardHost(aName); +} + +void PublisherMDnsSd::HandleRegisterHostResult(DNSServiceRef aHostsConnection, + DNSRecordRef aHostRecord, + DNSServiceFlags aFlags, + DNSServiceErrorType aErrorCode, + void * aContext) +{ + static_cast(aContext)->HandleRegisterHostResult(aHostsConnection, aHostRecord, aFlags, + aErrorCode); +} - SuccessOrExit(error = DNSServiceRegister(&serviceRef, 0, kDNSServiceInterfaceIndexAny, aName, aType, mDomain, mHost, - htons(aPort), static_cast(cur - txt), txt, - HandleServiceRegisterResult, this)); - if (serviceRef != nullptr) +void PublisherMDnsSd::HandleRegisterHostResult(DNSServiceRef aHostsConnection, + DNSRecordRef aHostRecord, + DNSServiceFlags aFlags, + DNSServiceErrorType aErrorCode) +{ + OTBR_UNUSED_VARIABLE(aHostsConnection); + OTBR_UNUSED_VARIABLE(aFlags); + + HostIterator host = FindPublishedHost(aHostRecord); + std::string hostName; + + VerifyOrExit(host != mHosts.end()); + hostName = host->mName; + + otbrLogInfo("Received reply for host %s", hostName.c_str()); + + if (aErrorCode == kDNSServiceErr_NoError) { - RecordService(aName, aType, serviceRef); + otbrLogInfo("Successfully registered host %s", hostName.c_str()); + } + else + { + otbrLogWarning("failed to register host %s for mdnssd error: %s", hostName.c_str(), + DNSErrorToString(aErrorCode)); + + DiscardHost(hostName.c_str(), /* aSendGoodbye */ false); + } + + if (mHostHandler != nullptr) + { + mHostHandler(hostName.c_str(), DNSErrorToOtbrError(aErrorCode), mHostHandlerContext); } exit: + return; +} - if (error != kDNSServiceErr_NoError) +otbrError PublisherMDnsSd::MakeFullName(char *aFullName, size_t aFullNameLength, const char *aName) +{ + otbrError error = OTBR_ERROR_NONE; + size_t nameLength = strlen(aName); + const char *domain = (mDomain == nullptr) ? "local." : mDomain; + + VerifyOrExit(nameLength <= kMaxSizeOfHost, error = OTBR_ERROR_INVALID_ARGS); + + assert(aFullNameLength >= nameLength + sizeof(".") + strlen(domain)); + OTBR_UNUSED_VARIABLE(aFullNameLength); + + strcpy(aFullName, aName); + strcpy(aFullName + nameLength, "."); + strcpy(aFullName + nameLength + 1, domain); + +exit: + return error; +} + +PublisherMDnsSd::ServiceIterator PublisherMDnsSd::FindPublishedService(const char *aName, const char *aType) +{ + return std::find_if(mServices.begin(), mServices.end(), [&aName, aType](const Service &service) { + return strcmp(aName, service.mName) == 0 && IsServiceTypeEqual(aType, service.mType); + }); +} + +PublisherMDnsSd::ServiceIterator PublisherMDnsSd::FindPublishedService(const DNSServiceRef &aServiceRef) +{ + return std::find_if(mServices.begin(), mServices.end(), + [&aServiceRef](const Service &service) { return service.mService == aServiceRef; }); +} + +PublisherMDnsSd::HostIterator PublisherMDnsSd::FindPublishedHost(const DNSRecordRef &aRecordRef) +{ + return std::find_if(mHosts.begin(), mHosts.end(), + [&aRecordRef](const Host &host) { return host.mRecord == aRecordRef; }); +} + +PublisherMDnsSd::HostIterator PublisherMDnsSd::FindPublishedHost(const char *aHostName) +{ + return std::find_if(mHosts.begin(), mHosts.end(), + [&aHostName](const Host &host) { return strcmp(host.mName, aHostName) == 0; }); +} + +void PublisherMDnsSd::SubscribeService(const std::string &aType, const std::string &aInstanceName) +{ + mSubscribedServices.emplace_back(*this, aType, aInstanceName); + + otbrLogInfo("subscribe service %s.%s (total %zu)", aInstanceName.c_str(), aType.c_str(), + mSubscribedServices.size()); + + if (aInstanceName.empty()) { - ret = OTBR_ERROR_MDNS; - otbrLog(OTBR_LOG_ERR, "Failed to publish service for mdnssd error: %s!", DNSErrorToString(error)); + mSubscribedServices.back().Browse(); + } + else + { + mSubscribedServices.back().Resolve(kDNSServiceInterfaceIndexAny, aInstanceName.c_str(), aType.c_str(), + "local."); } +} - return ret; +void PublisherMDnsSd::UnsubscribeService(const std::string &aType, const std::string &aInstanceName) +{ + ServiceSubscriptionList::iterator it = + std::find_if(mSubscribedServices.begin(), mSubscribedServices.end(), + [&aType, &aInstanceName](const ServiceSubscription &aService) { + return aService.mType == aType && aService.mInstanceName == aInstanceName; + }); + + assert(it != mSubscribedServices.end()); + + it->Release(); + mSubscribedServices.erase(it); + + otbrLogInfo("unsubscribe service %s.%s (left %zu)", aInstanceName.c_str(), aType.c_str(), + mSubscribedServices.size()); } -Publisher *Publisher::Create(int aFamily, const char *aHost, const char *aDomain, StateHandler aHandler, void *aContext) +void PublisherMDnsSd::OnServiceResolved(PublisherMDnsSd::ServiceSubscription &aService) { - return new PublisherMDnsSd(aFamily, aHost, aDomain, aHandler, aContext); + otbrLogInfo("Service %s is resolved successfully: %s host %s addresses %zu", aService.mType.c_str(), + aService.mInstanceInfo.mName.c_str(), aService.mInstanceInfo.mHostName.c_str(), + aService.mInstanceInfo.mAddresses.size()); + + if (mDiscoveredServiceInstanceCallback != nullptr) + { + mDiscoveredServiceInstanceCallback(aService.mType, aService.mInstanceInfo); + } +} + +void PublisherMDnsSd::OnServiceResolveFailed(const ServiceSubscription &aService, DNSServiceErrorType aErrorCode) +{ + otbrLogWarning("Service %s resolving failed: code=%d", aService.mType.c_str(), aErrorCode); +} + +void PublisherMDnsSd::OnHostResolved(PublisherMDnsSd::HostSubscription &aHost) +{ + otbrLogInfo("Host %s is resolved successfully: host %s addresses %zu ttl %u", aHost.mHostName.c_str(), + aHost.mHostInfo.mHostName.c_str(), aHost.mHostInfo.mAddresses.size(), aHost.mHostInfo.mTtl); + + if (mDiscoveredHostCallback != nullptr) + { + mDiscoveredHostCallback(aHost.mHostName, aHost.mHostInfo); + } +} + +void PublisherMDnsSd::OnHostResolveFailed(const PublisherMDnsSd::HostSubscription &aHost, + DNSServiceErrorType aErrorCode) +{ + otbrLogWarning("Host %s resolving failed: code=%d", aHost.mHostName.c_str(), aErrorCode); +} + +void PublisherMDnsSd::SubscribeHost(const std::string &aHostName) +{ + mSubscribedHosts.emplace_back(*this, aHostName); + + otbrLogInfo("subscribe host %s (total %zu)", aHostName.c_str(), mSubscribedHosts.size()); + + mSubscribedHosts.back().Resolve(); +} + +void PublisherMDnsSd::UnsubscribeHost(const std::string &aHostName) +{ + HostSubscriptionList ::iterator it = + std::find_if(mSubscribedHosts.begin(), mSubscribedHosts.end(), + [&aHostName](const HostSubscription &aHost) { return aHost.mHostName == aHostName; }); + + assert(it != mSubscribedHosts.end()); + + it->Release(); + mSubscribedHosts.erase(it); + + otbrLogInfo("unsubscribe host %s (remaining %d)", aHostName.c_str(), mSubscribedHosts.size()); +} + +Publisher *Publisher::Create(int aFamily, const char *aDomain, StateHandler aHandler, void *aContext) +{ + return new PublisherMDnsSd(aFamily, aDomain, aHandler, aContext); } void Publisher::Destroy(Publisher *aPublisher) @@ -425,6 +855,285 @@ void Publisher::Destroy(Publisher *aPublisher) delete static_cast(aPublisher); } +void PublisherMDnsSd::Subscription::Release(void) +{ + DeallocateServiceRef(); +} + +void PublisherMDnsSd::Subscription::DeallocateServiceRef(void) +{ + if (mServiceRef != nullptr) + { + DNSServiceRefDeallocate(mServiceRef); + mServiceRef = nullptr; + } +} + +void PublisherMDnsSd::ServiceSubscription::Browse(void) +{ + assert(mServiceRef == nullptr); + + otbrLogInfo("DNSServiceBrowse %s", mType.c_str()); + DNSServiceBrowse(&mServiceRef, /* flags */ kDNSServiceFlagsTimeout, kDNSServiceInterfaceIndexAny, mType.c_str(), + /* domain */ nullptr, HandleBrowseResult, this); +} + +void PublisherMDnsSd::ServiceSubscription::HandleBrowseResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aInstanceName, + const char * aType, + const char * aDomain, + void * aContext) +{ + static_cast(aContext)->HandleBrowseResult(aServiceRef, aFlags, aInterfaceIndex, aErrorCode, + aInstanceName, aType, aDomain); +} + +void PublisherMDnsSd::ServiceSubscription::HandleBrowseResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aInstanceName, + const char * aType, + const char * aDomain) +{ + OTBR_UNUSED_VARIABLE(aServiceRef); + + otbrLogInfo("DNSServiceBrowse reply: %s.%s%s inf %u, flags=%u, error=%d", aInstanceName, aType, aDomain, + aInterfaceIndex, aFlags, aErrorCode); + + VerifyOrExit(aErrorCode == kDNSServiceErr_NoError); + VerifyOrExit(aFlags & kDNSServiceFlagsAdd); + + DeallocateServiceRef(); + Resolve(aInterfaceIndex, aInstanceName, aType, aDomain); + +exit: + if (aErrorCode != kDNSServiceErr_NoError) + { + mMDnsSd->OnServiceResolveFailed(*this, aErrorCode); + } + else if (!(aFlags & (kDNSServiceFlagsAdd | kDNSServiceFlagsMoreComing))) + { + mMDnsSd->OnServiceResolveFailed(*this, kDNSServiceErr_NoSuchName); + } +} + +void PublisherMDnsSd::ServiceSubscription::Resolve(uint32_t aInterfaceIndex, + const char *aInstanceName, + const char *aType, + const char *aDomain) +{ + assert(mServiceRef == nullptr); + + otbrLogInfo("DNSServiceResolve %s %s %s inf %d", aInstanceName, aType, aDomain, aInterfaceIndex); + DNSServiceResolve(&mServiceRef, /* flags */ 0, aInterfaceIndex, aInstanceName, aType, aDomain, HandleResolveResult, + this); +} + +void PublisherMDnsSd::ServiceSubscription::HandleResolveResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aFullName, + const char * aHostTarget, + uint16_t aPort, + uint16_t aTxtLen, + const unsigned char *aTxtRecord, + void * aContext) +{ + static_cast(aContext)->HandleResolveResult( + aServiceRef, aFlags, aInterfaceIndex, aErrorCode, aFullName, aHostTarget, aPort, aTxtLen, aTxtRecord); +} + +void PublisherMDnsSd::ServiceSubscription::HandleResolveResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aFullName, + const char * aHostTarget, + uint16_t aPort, + uint16_t aTxtLen, + const unsigned char *aTxtRecord) +{ + OTBR_UNUSED_VARIABLE(aServiceRef); + + std::string instanceName, type, domain; + otbrError error = OTBR_ERROR_NONE; + + otbrLogInfo("DNSServiceResolve reply: %s host %s:%d, TXT=%dB inf %u, flags=%u", aFullName, aHostTarget, aPort, + aTxtLen, aInterfaceIndex, aFlags); + + VerifyOrExit(aErrorCode == kDNSServiceErr_NoError); + + SuccessOrExit(error = SplitFullServiceInstanceName(aFullName, instanceName, type, domain)); + + mInstanceInfo.mName = instanceName; + mInstanceInfo.mHostName = aHostTarget; + mInstanceInfo.mPort = ntohs(aPort); + mInstanceInfo.mTxtData.assign(aTxtRecord, aTxtRecord + aTxtLen); + // priority and weight are not given in the reply + mInstanceInfo.mPriority = 0; + mInstanceInfo.mWeight = 0; + + DeallocateServiceRef(); + GetAddrInfo(aInterfaceIndex); + +exit: + if (aErrorCode != kDNSServiceErr_NoError || error != OTBR_ERROR_NONE) + { + mMDnsSd->OnServiceResolveFailed(*this, aErrorCode); + } + + if (error != OTBR_ERROR_NONE) + { + otbrLogWarning("failed to resolve service instance %s", aFullName); + } +} + +void PublisherMDnsSd::ServiceSubscription::GetAddrInfo(uint32_t aInterfaceIndex) +{ + assert(mServiceRef == nullptr); + + otbrLogInfo("DNSServiceGetAddrInfo %s inf %d", mInstanceInfo.mHostName.c_str(), aInterfaceIndex); + + DNSServiceGetAddrInfo(&mServiceRef, /* flags */ 0, aInterfaceIndex, + kDNSServiceProtocol_IPv6 | kDNSServiceProtocol_IPv4, mInstanceInfo.mHostName.c_str(), + HandleGetAddrInfoResult, this); +} + +void PublisherMDnsSd::ServiceSubscription::HandleGetAddrInfoResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aHostName, + const struct sockaddr *aAddress, + uint32_t aTtl, + void * aContext) +{ + static_cast(aContext)->HandleGetAddrInfoResult(aServiceRef, aFlags, aInterfaceIndex, + aErrorCode, aHostName, aAddress, aTtl); +} + +void PublisherMDnsSd::ServiceSubscription::HandleGetAddrInfoResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aHostName, + const struct sockaddr *aAddress, + uint32_t aTtl) +{ + OTBR_UNUSED_VARIABLE(aServiceRef); + OTBR_UNUSED_VARIABLE(aInterfaceIndex); + + Ip6Address address; + + otbrLogDebug("DNSServiceGetAddrInfo reply: %d, flags=%u, host=%s, sa_family=%d", aErrorCode, aFlags, aHostName, + aAddress->sa_family); + + VerifyOrExit(aErrorCode == kDNSServiceErr_NoError); + VerifyOrExit((aFlags & kDNSServiceFlagsAdd) && aAddress->sa_family == AF_INET6); + + address.CopyFrom(*reinterpret_cast(aAddress)); + VerifyOrExit(!address.IsUnspecified() && !address.IsLinkLocal() && !address.IsMulticast() && !address.IsLoopback(), + otbrLogDebug("DNSServiceGetAddrInfo ignores address %s", address.ToString().c_str())); + + mInstanceInfo.mAddresses.push_back(address); + mInstanceInfo.mTtl = aTtl; + + otbrLogDebug("DNSServiceGetAddrInfo reply: address=%s, ttl=%u", address.ToString().c_str(), aTtl); + + mMDnsSd->OnServiceResolved(*this); + +exit: + if (aErrorCode != kDNSServiceErr_NoError) + { + otbrLogWarning("DNSServiceGetAddrInfo failed: %d", aErrorCode); + + mMDnsSd->OnServiceResolveFailed(*this, aErrorCode); + } + else if (mInstanceInfo.mAddresses.empty() && (aFlags & kDNSServiceFlagsMoreComing) == 0) + { + otbrLogDebug("DNSServiceGetAddrInfo reply: no IPv6 address found"); + mInstanceInfo.mTtl = aTtl; + mMDnsSd->OnServiceResolved(*this); + } +} + +void PublisherMDnsSd::HostSubscription::Resolve(void) +{ + std::string fullHostName = mHostName + ".local."; + + assert(mServiceRef == nullptr); + + otbrLogDebug("DNSServiceGetAddrInfo %s inf %d", fullHostName.c_str(), kDNSServiceInterfaceIndexAny); + + DNSServiceGetAddrInfo(&mServiceRef, /* flags */ 0, kDNSServiceInterfaceIndexAny, + kDNSServiceProtocol_IPv6 | kDNSServiceProtocol_IPv4, fullHostName.c_str(), + HandleResolveResult, this); +} + +void PublisherMDnsSd::HostSubscription::HandleResolveResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aHostName, + const struct sockaddr *aAddress, + uint32_t aTtl, + void * aContext) +{ + static_cast(aContext)->HandleResolveResult(aServiceRef, aFlags, aInterfaceIndex, aErrorCode, + aHostName, aAddress, aTtl); +} + +void PublisherMDnsSd::HostSubscription::HandleResolveResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aHostName, + const struct sockaddr *aAddress, + uint32_t aTtl) +{ + OTBR_UNUSED_VARIABLE(aServiceRef); + OTBR_UNUSED_VARIABLE(aInterfaceIndex); + + Ip6Address address; + + otbrLogDebug("DNSServiceGetAddrInfo reply: %d, flags=%u, host=%s, sa_family=%d", aErrorCode, aFlags, aHostName, + aAddress->sa_family); + + VerifyOrExit(aErrorCode == kDNSServiceErr_NoError); + VerifyOrExit((aFlags & kDNSServiceFlagsAdd) && aAddress->sa_family == AF_INET6); + + address.CopyFrom(*reinterpret_cast(aAddress)); + VerifyOrExit(!address.IsLinkLocal(), + otbrLogDebug("DNSServiceGetAddrInfo ignore link-local address %s", address.ToString().c_str())); + + mHostInfo.mHostName = aHostName; + mHostInfo.mAddresses.push_back(address); + mHostInfo.mTtl = aTtl; + + otbrLogDebug("DNSServiceGetAddrInfo reply: address=%s, ttl=%u", address.ToString().c_str(), aTtl); + + mMDnsSd->OnHostResolved(*this); + +exit: + if (aErrorCode != kDNSServiceErr_NoError) + { + otbrLogWarning("DNSServiceGetAddrInfo failed: %d", aErrorCode); + + mMDnsSd->OnHostResolveFailed(*this, aErrorCode); + } + else if (mHostInfo.mAddresses.empty() && (aFlags & kDNSServiceFlagsMoreComing) == 0) + { + otbrLogDebug("DNSServiceGetAddrInfo reply: no IPv6 address found"); + mHostInfo.mTtl = aTtl; + mMDnsSd->OnHostResolved(*this); + } +} + } // namespace Mdns } // namespace otbr diff --git a/src/mdns/mdns_mdnssd.hpp b/src/mdns/mdns_mdnssd.hpp index aba49073999..b11aa22b0da 100644 --- a/src/mdns/mdns_mdnssd.hpp +++ b/src/mdns/mdns_mdnssd.hpp @@ -34,10 +34,15 @@ #ifndef OTBR_AGENT_MDNS_MDNSSD_HPP_ #define OTBR_AGENT_MDNS_MDNSSD_HPP_ +#include +#include +#include #include +#include #include +#include "common/code_utils.hpp" #include "common/types.hpp" #include "mdns/mdns.hpp" @@ -46,7 +51,7 @@ namespace otbr { namespace Mdns { /** - * This class implements MDNS service with avahi. + * This class implements MDNS service with mDNSResponder. * */ class PublisherMDnsSd : public Publisher @@ -56,33 +61,127 @@ class PublisherMDnsSd : public Publisher * The constructor to initialize a Publisher. * * @param[in] aProtocol The protocol used for publishing. IPv4, IPv6 or both. - * @param[in] aHost The name of host residing the services to be published. - nullptr to use default. * @param[in] aDomain The domain of the host. nullptr to use default. * @param[in] aHandler The function to be called when state changes. * @param[in] aContext A pointer to application-specific context. * */ - PublisherMDnsSd(int aProtocol, const char *aHost, const char *aDomain, StateHandler aHandler, void *aContext); + PublisherMDnsSd(int aProtocol, const char *aDomain, StateHandler aHandler, void *aContext); - ~PublisherMDnsSd(void); + ~PublisherMDnsSd(void) override; /** * This method publishes or updates a service. * - * @note only text record can be updated. - * + * @param[in] aHostName The name of the host which this service resides on. If NULL is provided, + * this service resides on local host and it is the implementation to provide + * specific host name. Otherwise, the caller MUST publish the host with method + * PublishHost. * @param[in] aName The name of this service. * @param[in] aType The type of this service. * @param[in] aPort The port number of this service. - * @param[in] ... Pointers to null-terminated string of key and value for text record. - * The last argument must be nullptr. + * @param[in] aTxtList A list of TXT name/value pairs. * * @retval OTBR_ERROR_NONE Successfully published or updated the service. * @retval OTBR_ERROR_ERRNO Failed to publish or update the service. * */ - otbrError PublishService(uint16_t aPort, const char *aName, const char *aType, ...); + otbrError PublishService(const char * aHostName, + uint16_t aPort, + const char * aName, + const char * aType, + const TxtList &aTxtList) override; + + /** + * This method un-publishes a service. + * + * @param[in] aName The name of this service. + * @param[in] aType The type of this service. + * + * @retval OTBR_ERROR_NONE Successfully un-published the service. + * @retval OTBR_ERROR_ERRNO Failed to un-publish the service. + * + */ + otbrError UnpublishService(const char *aName, const char *aType) override; + + /** + * This method publishes or updates a host. + * + * Publishing a host is advertising an AAAA RR for the host name. This method should be called + * before a service with non-null host name is published. + * + * @param[in] aName The name of the host. + * @param[in] aAddress The address of the host. + * @param[in] aAddressLength The length of @p aAddress. + * + * @retval OTBR_ERROR_NONE Successfully published or updated the host. + * @retval OTBR_ERROR_ERRNO Failed to publish or update the host. + * + */ + otbrError PublishHost(const char *aName, const uint8_t *aAddress, uint8_t aAddressLength) override; + + /** + * This method un-publishes a host. + * + * @param[in] aName A host name. + * + * @retval OTBR_ERROR_NONE Successfully un-published the host. + * @retval OTBR_ERROR_ERRNO Failed to un-publish the host. + * + * @note All services reside on this host should be un-published by UnpublishService. + * + */ + otbrError UnpublishHost(const char *aName) override; + + /** + * This method subscribes a given service or service instance. If @p aInstanceName is not empty, this method + * subscribes the service instance. Otherwise, this method subscribes the service. + * + * mDNS implementations should use the `DiscoveredServiceInstanceCallback` function to notify discovered service + * instances. + * + * @note Discovery Proxy implementation guarantees no duplicate subscriptions for the same service or service + * instance. + * + * @param[in] aType The service type. + * @param[in] aInstanceName The service instance to subscribe, or empty to subscribe the service. + * + */ + void SubscribeService(const std::string &aType, const std::string &aInstanceName) override; + + /** + * This method unsubscribes a given service or service instance. If @p aInstanceName is not empty, this method + * unsubscribes the service instance. Otherwise, this method unsubscribes the service. + * + * @note Discovery Proxy implementation guarantees no redundant unsubscription for a service or service instance. + * + * @param[in] aType The service type. + * @param[in] aInstanceName The service instance to unsubscribe, or empty to unsubscribe the service. + * + */ + void UnsubscribeService(const std::string &aType, const std::string &aInstanceName) override; + + /** + * This method subscribes a given host. + * + * mDNS implementations should use the `DiscoveredHostCallback` function to notify discovered hosts. + * + * @note Discovery Proxy implementation guarantees no duplicate subscriptions for the same host. + * + * @param[in] aHostName The host name (without domain). + * + */ + void SubscribeHost(const std::string &aHostName) override; + + /** + * This method unsubscribes a given host. + * + * @note Discovery Proxy implementation guarantees no redundant unsubscription for a host. + * + * @param[in] aHostName The host name (without domain). + * + */ + void UnsubscribeHost(const std::string &aHostName) override; /** * This method starts the MDNS service. @@ -91,7 +190,7 @@ class PublisherMDnsSd : public Publisher * @retval OTBR_ERROR_MDNS Failed to start MDNS service. * */ - otbrError Start(void); + otbrError Start(void) override; /** * This method checks if publisher has been started. @@ -100,40 +199,179 @@ class PublisherMDnsSd : public Publisher * @retval false Not started. * */ - bool IsStarted(void) const; + bool IsStarted(void) const override; /** * This method stops the MDNS service. * */ - void Stop(void); + void Stop(void) override; /** - * This method performs avahi poll processing. + * This method updates the mainloop context. * - * @param[in] aReadFdSet A reference to read file descriptors. - * @param[in] aWriteFdSet A reference to write file descriptors. - * @param[in] aErrorFdSet A reference to error file descriptors. + * @param[inout] aMainloop A reference to the mainloop to be updated. * */ - void Process(const fd_set &aReadFdSet, const fd_set &aWriteFdSet, const fd_set &aErrorFdSet); + void Update(MainloopContext &aMainloop) override; /** - * This method updates the fd_set and timeout for mainloop. + * This method processes mainloop events. * - * @param[inout] aReadFdSet A reference to fd_set for polling read. - * @param[inout] aWriteFdSet A reference to fd_set for polling write. - * @param[inout] aErrorFdSet A reference to fd_set for polling error. - * @param[inout] aMaxFd A reference to the max file descriptor. - * @param[inout] aTimeout A reference to the timeout. + * @param[in] aMainloop A reference to the mainloop context. * */ - void UpdateFdSet(fd_set &aReadFdSet, fd_set &aWriteFdSet, fd_set &aErrorFdSet, int &aMaxFd, timeval &aTimeout); + void Process(const MainloopContext &aMainloop) override; private: - void DiscardService(const char *aName, const char *aType, DNSServiceRef aServiceRef); + enum + { + kMaxSizeOfTxtRecord = 256, + kMaxSizeOfServiceName = kDNSServiceMaxServiceName, + kMaxSizeOfHost = 128, + kMaxSizeOfDomain = kDNSServiceMaxDomainName, + kMaxSizeOfServiceType = 69, + }; + + struct Service + { + char mName[kMaxSizeOfServiceName]; + char mType[kMaxSizeOfServiceType]; + DNSServiceRef mService; + }; + + struct Host + { + char mName[kMaxSizeOfServiceName]; + std::array mAddress; + DNSRecordRef mRecord; + }; + + struct Subscription + { + PublisherMDnsSd *mMDnsSd; + DNSServiceRef mServiceRef; + + explicit Subscription(PublisherMDnsSd &aMDnsSd) + : mMDnsSd(&aMDnsSd) + , mServiceRef(nullptr) + { + } + + void Release(void); + void DeallocateServiceRef(void); + }; + + struct ServiceSubscription : public Subscription + { + explicit ServiceSubscription(PublisherMDnsSd &aMDnsSd, std::string aType, std::string aInstanceName) + : Subscription(aMDnsSd) + , mType(std::move(aType)) + , mInstanceName(std::move(aInstanceName)) + { + } + + void Browse(void); + void Resolve(uint32_t aInterfaceIndex, const char *aInstanceName, const char *aType, const char *aDomain); + void GetAddrInfo(uint32_t aInterfaceIndex); + + static void HandleBrowseResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aInstanceName, + const char * aType, + const char * aDomain, + void * aContext); + void HandleBrowseResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aInstanceName, + const char * aType, + const char * aDomain); + static void HandleResolveResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aFullName, + const char * aHostTarget, + uint16_t aPort, // In network byte order. + uint16_t aTxtLen, + const unsigned char *aTxtRecord, + void * aContext); + void HandleResolveResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aFullName, + const char * aHostTarget, + uint16_t aPort, // In network byte order. + uint16_t aTxtLen, + const unsigned char *aTxtRecord); + static void HandleGetAddrInfoResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aHostName, + const struct sockaddr *aAddress, + uint32_t aTtl, + void * aContext); + void HandleGetAddrInfoResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aHostName, + const struct sockaddr *aAddress, + uint32_t aTtl); + + std::string mType; + std::string mInstanceName; + DiscoveredInstanceInfo mInstanceInfo; + }; + + struct HostSubscription : public Subscription + { + explicit HostSubscription(PublisherMDnsSd &aMDnsSd, std::string aHostName) + : Subscription(aMDnsSd) + , mHostName(std::move(aHostName)) + { + } + + void Resolve(void); + static void HandleResolveResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aHostName, + const struct sockaddr *aAddress, + uint32_t aTtl, + void * aContext); + void HandleResolveResult(DNSServiceRef aServiceRef, + DNSServiceFlags aFlags, + uint32_t aInterfaceIndex, + DNSServiceErrorType aErrorCode, + const char * aHostName, + const struct sockaddr *aAddress, + uint32_t aTtl); + + std::string mHostName; + DiscoveredHostInfo mHostInfo; + }; + + typedef std::vector Services; + typedef std::vector Hosts; + typedef std::vector::iterator ServiceIterator; + typedef std::vector::iterator HostIterator; + typedef std::vector ServiceSubscriptionList; + typedef std::vector HostSubscriptionList; + + void DiscardService(const char *aName, const char *aType, DNSServiceRef aServiceRef = nullptr); void RecordService(const char *aName, const char *aType, DNSServiceRef aServiceRef); + otbrError DiscardHost(const char *aName, bool aSendGoodbye = true); + void RecordHost(const char *aName, const uint8_t *aAddress, uint8_t aAddressLength, DNSRecordRef aRecordRef); + static void HandleServiceRegisterResult(DNSServiceRef aService, const DNSServiceFlags aFlags, DNSServiceErrorType aError, @@ -147,32 +385,38 @@ class PublisherMDnsSd : public Publisher const char * aName, const char * aType, const char * aDomain); + static void HandleRegisterHostResult(DNSServiceRef aHostsConnection, + DNSRecordRef aHostRecord, + DNSServiceFlags aFlags, + DNSServiceErrorType aErrorCode, + void * aContext); + void HandleRegisterHostResult(DNSServiceRef aHostsConnection, + DNSRecordRef aHostRecord, + DNSServiceFlags aFlags, + DNSServiceErrorType aErrorCode); - enum - { - kMaxSizeOfTxtRecord = 128, - kMaxSizeOfServiceName = kDNSServiceMaxServiceName, - kMaxSizeOfHost = 128, - kMaxSizeOfDomain = kDNSServiceMaxDomainName, - kMaxSizeOfServiceType = 64, - kMaxTextRecordSize = 255, - }; + otbrError MakeFullName(char *aFullName, size_t aFullNameLength, const char *aName); - struct Service - { - char mName[kMaxSizeOfServiceName]; - char mType[kMaxSizeOfServiceType]; - DNSServiceRef mService; - }; + ServiceIterator FindPublishedService(const char *aName, const char *aType); + ServiceIterator FindPublishedService(const DNSServiceRef &aServiceRef); + HostIterator FindPublishedHost(const DNSRecordRef &aRecordRef); + HostIterator FindPublishedHost(const char *aHostName); + + void OnServiceResolved(ServiceSubscription &aService); + static void OnServiceResolveFailed(const ServiceSubscription &aService, DNSServiceErrorType aErrorCode); + void OnHostResolved(HostSubscription &aHost); + void OnHostResolveFailed(const HostSubscription &aHost, DNSServiceErrorType aErrorCode); - typedef std::vector Services; + Services mServices; + Hosts mHosts; + DNSServiceRef mHostsRef; + const char * mDomain; + State mState; + StateHandler mStateHandler; + void * mContext; - Services mServices; - const char * mHost; - const char * mDomain; - State mState; - StateHandler mStateHandler; - void * mContext; + ServiceSubscriptionList mSubscribedServices; + HostSubscriptionList mSubscribedHosts; }; /** diff --git a/src/openwrt/controller/thread.lua b/src/openwrt/controller/thread.lua index 869ab3455c5..245d1fc5577 100644 --- a/src/openwrt/controller/thread.lua +++ b/src/openwrt/controller/thread.lua @@ -59,7 +59,6 @@ function thread_handler_setting() local panid = luci.http.formvalue("panid") local extpanid = luci.http.formvalue("extpanid") local mode = luci.http.formvalue("mode") - local leaderpartitionid = luci.http.formvalue("leaderpartitionid") + 0 local masterkey = luci.http.formvalue("masterkey") local pskc = luci.http.formvalue("pskc") local macfilter = luci.http.formvalue("macfilterselect") @@ -112,7 +111,6 @@ function thread_handler_setting() conn:call("otbr", "setpanid", { panid = panid }) conn:call("otbr", "setextpanid", { extpanid = extpanid }) conn:call("otbr", "setmode", { mode = mode }) - conn:call("otbr", "setleaderpartitionid", { leaderpartitionid = leaderpartitionid }) conn:call("otbr", "setmasterkey", { masterkey = masterkey }) conn:call("otbr", "setpskc", { pskc = pskc }) conn:call("otbr", "macfiltersetstate", { state = macfilter }) diff --git a/src/openwrt/otbr-agent.init.in b/src/openwrt/otbr-agent.init.in index d42777b2ae2..1f326eed90b 100755 --- a/src/openwrt/otbr-agent.init.in +++ b/src/openwrt/otbr-agent.init.in @@ -1,3 +1,4 @@ +#!/bin/sh /etc/rc.common # # Copyright (c) 2020, The OpenThread Authors. # All rights reserved. @@ -26,7 +27,6 @@ # POSSIBILITY OF SUCH DAMAGE. # -#!/bin/sh /etc/rc.common START=90 diff --git a/src/openwrt/ubus/otubus.cpp b/src/openwrt/ubus/otubus.cpp index d50e344db6a..fbe824d353c 100644 --- a/src/openwrt/ubus/otubus.cpp +++ b/src/openwrt/ubus/otubus.cpp @@ -30,10 +30,13 @@ #define BYTE_ORDER_BIG_ENDIAN 1 #endif +#define OTBR_LOG_TAG "UBUS" + #include "openwrt/ubus/otubus.hpp" #include +#include #include #include @@ -78,8 +81,6 @@ UbusServer &UbusServer::GetInstance(void) void UbusServer::Initialize(Ncp::ControllerOpenThread *aController) { sUbusServerInstance = new UbusServer(aController); - otThreadSetReceiveDiagnosticGetCallback(aController->GetInstance(), &UbusServer::HandleDiagnosticGetResponse, - sUbusServerInstance); } enum @@ -134,10 +135,6 @@ static const struct blobmsg_policy setModePolicy[SET_NETWORK_MAX] = { [SETNETWORK] = {.name = "mode", .type = BLOBMSG_TYPE_STRING}, }; -static const struct blobmsg_policy setLeaderPartitionIdPolicy[SET_NETWORK_MAX] = { - [SETNETWORK] = {.name = "leaderpartitionid", .type = BLOBMSG_TYPE_INT32}, -}; - static const struct blobmsg_policy macfilterAddPolicy[SET_NETWORK_MAX] = { [SETNETWORK] = {.name = "addr", .type = BLOBMSG_TYPE_STRING}, }; @@ -191,9 +188,7 @@ static const struct ubus_method otbrMethods[] = { {"parent", &UbusServer::UbusParentHandler, 0, 0, nullptr, 0}, {"mode", &UbusServer::UbusModeHandler, 0, 0, nullptr, 0}, {"setmode", &UbusServer::UbusSetModeHandler, 0, 0, setModePolicy, ARRAY_SIZE(setModePolicy)}, - {"leaderpartitionid", &UbusServer::UbusLeaderPartitionIdHandler, 0, 0, nullptr, 0}, - {"setleaderpartitionid", &UbusServer::UbusSetLeaderPartitionIdHandler, 0, 0, setLeaderPartitionIdPolicy, - ARRAY_SIZE(setLeaderPartitionIdPolicy)}, + {"partitionid", &UbusServer::UbusPartitionIdHandler, 0, 0, nullptr, 0}, {"leave", &UbusServer::UbusLeaveHandler, 0, 0, nullptr, 0}, {"leaderdata", &UbusServer::UbusLeaderdataHandler, 0, 0, nullptr, 0}, {"networkdata", &UbusServer::UbusNetworkdataHandler, 0, 0, nullptr, 0}, @@ -535,22 +530,13 @@ int UbusServer::UbusSetModeHandler(struct ubus_context * aContext, return GetInstance().UbusSetInformation(aContext, aObj, aRequest, aMethod, aMsg, "mode"); } -int UbusServer::UbusLeaderPartitionIdHandler(struct ubus_context * aContext, - struct ubus_object * aObj, - struct ubus_request_data *aRequest, - const char * aMethod, - struct blob_attr * aMsg) -{ - return GetInstance().UbusGetInformation(aContext, aObj, aRequest, aMethod, aMsg, "leaderpartitionid"); -} - -int UbusServer::UbusSetLeaderPartitionIdHandler(struct ubus_context * aContext, - struct ubus_object * aObj, - struct ubus_request_data *aRequest, - const char * aMethod, - struct blob_attr * aMsg) +int UbusServer::UbusPartitionIdHandler(struct ubus_context * aContext, + struct ubus_object * aObj, + struct ubus_request_data *aRequest, + const char * aMethod, + struct blob_attr * aMsg) { - return GetInstance().UbusSetInformation(aContext, aObj, aRequest, aMethod, aMsg, "leaderpartitionid"); + return GetInstance().UbusGetInformation(aContext, aObj, aRequest, aMethod, aMsg, "partitionid"); } int UbusServer::UbusLeaveHandler(struct ubus_context * aContext, @@ -1030,13 +1016,13 @@ void UbusServer::HandleStateChanged(otCommissionerState aState) switch (aState) { case OT_COMMISSIONER_STATE_DISABLED: - otbrLog(OTBR_LOG_INFO, "commissioner state disabled"); + otbrLogInfo("Commissioner state disabled"); break; case OT_COMMISSIONER_STATE_ACTIVE: - otbrLog(OTBR_LOG_INFO, "commissioner state active"); + otbrLogInfo("Commissioner state active"); break; case OT_COMMISSIONER_STATE_PETITION: - otbrLog(OTBR_LOG_INFO, "commissioner state petition"); + otbrLogInfo("Commissioner state petition"); break; } } @@ -1059,19 +1045,19 @@ void UbusServer::HandleJoinerEvent(otCommissionerJoinerEvent aEvent, switch (aEvent) { case OT_COMMISSIONER_JOINER_START: - otbrLog(OTBR_LOG_INFO, "joiner start"); + otbrLogInfo("Joiner start"); break; case OT_COMMISSIONER_JOINER_CONNECTED: - otbrLog(OTBR_LOG_INFO, "joiner connected"); + otbrLogInfo("Joiner connected"); break; case OT_COMMISSIONER_JOINER_FINALIZE: - otbrLog(OTBR_LOG_INFO, "joiner finalize"); + otbrLogInfo("Joiner finalize"); break; case OT_COMMISSIONER_JOINER_END: - otbrLog(OTBR_LOG_INFO, "joiner end"); + otbrLogInfo("Joiner end"); break; case OT_COMMISSIONER_JOINER_REMOVED: - otbrLog(OTBR_LOG_INFO, "joiner remove"); + otbrLogInfo("Joiner remove"); break; } } @@ -1161,9 +1147,9 @@ int UbusServer::UbusGetInformation(struct ubus_context * aContext, } blobmsg_add_string(&mBuf, "Mode", mode); } - else if (!strcmp(aAction, "leaderpartitionid")) + else if (!strcmp(aAction, "partitionid")) { - blobmsg_add_u32(&mBuf, "Leaderpartitionid", otThreadGetLocalLeaderPartitionId(mController->GetInstance())); + blobmsg_add_u32(&mBuf, "Partitionid", otThreadGetPartitionId(mController->GetInstance())); } else if (!strcmp(aAction, "leaderdata")) { @@ -1190,19 +1176,17 @@ int UbusServer::UbusGetInformation(struct ubus_context * aContext, uint8_t tlvTypes[OT_NETWORK_DIAGNOSTIC_TYPELIST_MAX_ENTRIES]; uint8_t count = 0; char multicastAddr[10] = "ff03::2"; - long value; blob_buf_init(&mNetworkdataBuf, 0); SuccessOrExit(error = otIp6AddressFromString(multicastAddr, &address)); - value = 5; - tlvTypes[count++] = static_cast(value); - value = 16; - tlvTypes[count++] = static_cast(value); + tlvTypes[count++] = static_cast(OT_NETWORK_DIAGNOSTIC_TLV_ROUTE); + tlvTypes[count++] = static_cast(OT_NETWORK_DIAGNOSTIC_TLV_CHILD_TABLE); sBufNum = 0; - otThreadSendDiagnosticGet(mController->GetInstance(), &address, tlvTypes, count); + otThreadSendDiagnosticGet(mController->GetInstance(), &address, tlvTypes, count, + &UbusServer::HandleDiagnosticGetResponse, this); mSecond = time(nullptr); } goto exit; @@ -1304,10 +1288,12 @@ int UbusServer::UbusGetInformation(struct ubus_context * aContext, return 0; } -void UbusServer::HandleDiagnosticGetResponse(otMessage *aMessage, const otMessageInfo *aMessageInfo, void *aContext) +void UbusServer::HandleDiagnosticGetResponse(otError aError, + otMessage * aMessage, + const otMessageInfo *aMessageInfo, + void * aContext) { - static_cast(aContext)->HandleDiagnosticGetResponse(aMessage, - *static_cast(aMessageInfo)); + static_cast(aContext)->HandleDiagnosticGetResponse(aError, aMessage, aMessageInfo); } static bool IsRoutingLocator(const otIp6Address *aAddress) @@ -1322,7 +1308,7 @@ static bool IsRoutingLocator(const otIp6Address *aAddress) aAddress->mFields.m8[14] < kAloc16Mask && (aAddress->mFields.m8[14] & kRloc16ReservedBitMask) == 0); } -void UbusServer::HandleDiagnosticGetResponse(otMessage *aMessage, const otMessageInfo &aMessageInfo) +void UbusServer::HandleDiagnosticGetResponse(otError aError, otMessage *aMessage, const otMessageInfo *aMessageInfo) { uint16_t rloc16; uint16_t sockRloc16 = 0; @@ -1332,14 +1318,16 @@ void UbusServer::HandleDiagnosticGetResponse(otMessage *aMessage, const otMessag otNetworkDiagTlv diagTlv; otNetworkDiagIterator iterator = OT_NETWORK_DIAGNOSTIC_ITERATOR_INIT; + SuccessOrExit(aError); + char networkdata[20]; sprintf(networkdata, "networkdata%d", sBufNum); sJsonUri = blobmsg_open_table(&mNetworkdataBuf, networkdata); sBufNum++; - if (IsRoutingLocator(&aMessageInfo.mSockAddr)) + if (IsRoutingLocator(&aMessageInfo->mSockAddr)) { - sockRloc16 = aMessageInfo.mPeerAddr.mFields.m16[7]; + sockRloc16 = ntohs(aMessageInfo->mPeerAddr.mFields.m16[7]); sprintf(xrloc, "0x%04x", sockRloc16); blobmsg_add_string(&mNetworkdataBuf, "rloc", xrloc); } @@ -1409,6 +1397,12 @@ void UbusServer::HandleDiagnosticGetResponse(otMessage *aMessage, const otMessag } blobmsg_close_table(&mNetworkdataBuf, sJsonUri); + +exit: + if (aError != OT_ERROR_NONE) + { + otbrLogWarning("Failed to receive diagnostic response: %s", otThreadErrorToString(aError)); + } } int UbusServer::UbusSetInformation(struct ubus_context * aContext, @@ -1536,17 +1530,6 @@ int UbusServer::UbusSetInformation(struct ubus_context * aContext, SuccessOrExit(error = otThreadSetLinkMode(mController->GetInstance(), linkMode)); } } - else if (!strcmp(aAction, "leaderpartitionid")) - { - struct blob_attr *tb[SET_NETWORK_MAX]; - - blobmsg_parse(setLeaderPartitionIdPolicy, SET_NETWORK_MAX, tb, blob_data(aMsg), blob_len(aMsg)); - if (tb[SETNETWORK] != nullptr) - { - uint32_t input = blobmsg_get_u32(tb[SETNETWORK]); - otThreadSetLocalLeaderPartitionId(mController->GetInstance(), input); - } - } else if (!strcmp(aAction, "macfilteradd")) { struct blob_attr *tb[SET_NETWORK_MAX]; @@ -1698,11 +1681,11 @@ int UbusServer::DisplayUbusInit(const char *aPath) mContext = ubus_connect(aPath); if (!mContext) { - otbrLog(OTBR_LOG_ERR, "ubus connect failed"); + otbrLogErr("Ubus connect failed"); return -1; } - otbrLog(OTBR_LOG_INFO, "connected as %08x\n", mContext->local_id); + otbrLogInfo("Connected as %08x\n", mContext->local_id); mContext->connection_lost = UbusConnectionLost; /* file description */ @@ -1711,7 +1694,7 @@ int UbusServer::DisplayUbusInit(const char *aPath) /* Add a object */ if (ubus_add_object(mContext, &otbr) != 0) { - otbrLog(OTBR_LOG_ERR, "ubus add obj failed"); + otbrLogErr("Ubus add obj failed"); return -1; } @@ -1733,11 +1716,11 @@ void UbusServer::InstallUbusObject(void) if (-1 == DisplayUbusInit(path)) { - otbrLog(OTBR_LOG_ERR, "ubus connect failed"); + otbrLogErr("Ubus connect failed"); return; } - otbrLog(OTBR_LOG_INFO, "uloop run"); + otbrLogInfo("Uloop run"); uloop_run(); DisplayUbusDone(); diff --git a/src/openwrt/ubus/otubus.hpp b/src/openwrt/ubus/otubus.hpp index f5c0e3ad13f..ea1699cb037 100644 --- a/src/openwrt/ubus/otubus.hpp +++ b/src/openwrt/ubus/otubus.hpp @@ -26,6 +26,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definitions for ubus API. + */ + #ifndef OTBR_AGENT_OTUBUS_HPP_ #define OTBR_AGENT_OTUBUS_HPP_ @@ -413,7 +418,7 @@ class UbusServer struct blob_attr * aMsg); /** - * This method handle ubus get leaderpartitionid function request. + * This method handle ubus get partitionid function request. * * @param[in] aContext A pointer to the ubus context. * @param[in] aObj A pointer to the ubus object. @@ -424,29 +429,11 @@ class UbusServer * @retval 0 Successfully handler the request. * */ - static int UbusLeaderPartitionIdHandler(struct ubus_context * aContext, - struct ubus_object * aObj, - struct ubus_request_data *aRequest, - const char * aMethod, - struct blob_attr * aMsg); - - /** - * This method handle ubus set learderpartitionid function request. - * - * @param[in] aContext A pointer to the ubus context. - * @param[in] aObj A pointer to the ubus object. - * @param[in] aRequest A pointer to the ubus request. - * @param[in] aMethod A pointer to the ubus method. - * @param[in] aMsg A pointer to the ubus message. - * - * @retval 0 Successfully handler the request. - * - */ - static int UbusSetLeaderPartitionIdHandler(struct ubus_context * aContext, - struct ubus_object * aObj, - struct ubus_request_data *aRequest, - const char * aMethod, - struct blob_attr * aMsg); + static int UbusPartitionIdHandler(struct ubus_context * aContext, + struct ubus_object * aObj, + struct ubus_request_data *aRequest, + const char * aMethod, + struct blob_attr * aMsg); /** * This method handle ubus get leaderdata function request. @@ -757,21 +744,26 @@ class UbusServer /** * This method handle initial diagnostic get response. * + * @param[in] aError A error of receiving the diagnostic response. * @param[in] aMessage A pointer to the message. * @param[in] aMessageInfo A pointer to the message information. * @param[in] aContext A pointer to the context. * */ - static void HandleDiagnosticGetResponse(otMessage *aMessage, const otMessageInfo *aMessageInfo, void *aContext); + static void HandleDiagnosticGetResponse(otError aError, + otMessage * aMessage, + const otMessageInfo *aMessageInfo, + void * aContext); /** * This method handle diagnosticget response. * + * @param[in] aError A error of receiving the diagnostic response. * @param[in] aMessage A pointer to the message. * @param[in] aMessageInfo A pointer to the message information. * */ - void HandleDiagnosticGetResponse(otMessage *aMessage, const otMessageInfo &aMessageInfo); + void HandleDiagnosticGetResponse(otError aError, otMessage *aMessage, const otMessageInfo *aMessageInfo); private: bool mIfFinishScan; diff --git a/src/openwrt/view/admin_thread/thread_setting.htm b/src/openwrt/view/admin_thread/thread_setting.htm index cfc16776767..f291a0a20be 100644 --- a/src/openwrt/view/admin_thread/thread_setting.htm +++ b/src/openwrt/view/admin_thread/thread_setting.htm @@ -2,7 +2,6 @@ local ubus = require "ubus" local sys = require "luci.sys" local utl = require "luci.util" - local state = threadget("state").State function connect_ubus(methods) local result @@ -17,6 +16,16 @@ return result end + function threadget(action) + local result = connect_ubus(action) + + return result + end + + local state = threadget("state").State + + + function addrlist() local k, v local l = { } @@ -30,11 +39,7 @@ return l end - function threadget(action) - local result = connect_ubus(action) - return result - end -%> <%+header%> @@ -155,23 +160,13 @@

<%:Network Configuration%>

- <% if state == 'disabled' then %> - +
- " style="width:50%;"/> -
-
- "/><%:leaderpartitionid must be a number%> -
- <% else %> - -
- " readonly="readonly" style="width:50%;"/> + <%=threadget("partitionid").Partitionid%>
"/><%:can not change partitionid when thread network is started.%>
- <% end %>
@@ -388,18 +383,6 @@

<%:Interface Configuration%>

return false; } - var leaderpartitionid = document.forms["settingForm"]["leaderpartitionid"].value; - if (isNaN(leaderpartitionid)) { - alert("leaderpartitionid must be a number"); - return false; - } - - var leaderpartitionid = document.forms["settingForm"]["leaderpartitionid"].value; - if (isNaN(leaderpartitionid)) { - alert("leaderpartitionid must be a number"); - return false; - } - var masterkey = document.forms["settingForm"]["masterkey"].value; if (masterkey.length != 32) { alert("Masterkey length must be 32 bytes"); diff --git a/src/rest/CMakeLists.txt b/src/rest/CMakeLists.txt index 0f889be4424..63715c409e5 100644 --- a/src/rest/CMakeLists.txt +++ b/src/rest/CMakeLists.txt @@ -26,7 +26,7 @@ # POSSIBILITY OF SUCH DAMAGE. # -add_library( otbr-rest +add_library(otbr-rest rest_web_server.cpp connection.cpp resource.cpp @@ -36,7 +36,7 @@ add_library( otbr-rest response.cpp ) -target_link_libraries(otbr-rest +target_link_libraries(otbr-rest PUBLIC http_parser PRIVATE diff --git a/src/rest/connection.cpp b/src/rest/connection.cpp index 28f02fff38e..5ca257a0bb4 100644 --- a/src/rest/connection.cpp +++ b/src/rest/connection.cpp @@ -31,6 +31,8 @@ #include #include + +#include #include using std::chrono::duration_cast; @@ -62,6 +64,11 @@ Connection::Connection(steady_clock::time_point aStartTime, Resource *aResource, { } +Connection::~Connection(void) +{ + Disconnect(); +} + void Connection::Init(void) { mParser.Init(); @@ -126,7 +133,7 @@ void Connection::UpdateTimeout(timeval &aTimeout) const } } -void Connection::UpdateFdSet(otSysMainloopContext &aMainloop) const +void Connection::Update(MainloopContext &aMainloop) { UpdateTimeout(aMainloop.mTimeout); UpdateReadFdSet(aMainloop.mReadFdSet, aMainloop.mMaxFd); @@ -144,7 +151,7 @@ void Connection::Disconnect(void) } } -void Connection::Process(fd_set &aReadFdSet, fd_set &aWriteFdSet) +void Connection::Process(const MainloopContext &aMainloop) { otbrError error = OTBR_ERROR_NONE; @@ -153,14 +160,14 @@ void Connection::Process(fd_set &aReadFdSet, fd_set &aWriteFdSet) // Initial state, directly read for the first time. case ConnectionState::kInit: case ConnectionState::kReadWait: - ProcessWaitRead(aReadFdSet); + ProcessWaitRead(aMainloop.mReadFdSet); break; case ConnectionState::kCallbackWait: // Wait for Callback process. ProcessWaitCallback(); break; case ConnectionState::kWriteWait: - ProcessWaitWrite(aWriteFdSet); + ProcessWaitWrite(aMainloop.mWriteFdSet); break; default: assert(false); @@ -172,7 +179,7 @@ void Connection::Process(fd_set &aReadFdSet, fd_set &aWriteFdSet) } } -void Connection::ProcessWaitRead(fd_set &aReadFdSet) +void Connection::ProcessWaitRead(const fd_set &aReadFdSet) { otbrError error = OTBR_ERROR_NONE; int32_t received = 0, err; @@ -275,7 +282,7 @@ void Connection::ProcessWaitCallback(void) } } -void Connection::ProcessWaitWrite(fd_set &aWriteFdSet) +void Connection::ProcessWaitWrite(const fd_set &aWriteFdSet) { auto duration = duration_cast(steady_clock::now() - mTimeStamp).count(); diff --git a/src/rest/connection.hpp b/src/rest/connection.hpp index 249f9d52ce3..454202ab47b 100644 --- a/src/rest/connection.hpp +++ b/src/rest/connection.hpp @@ -37,6 +37,7 @@ #include #include +#include "common/mainloop.hpp" #include "rest/parser.hpp" #include "rest/resource.hpp" @@ -49,7 +50,7 @@ namespace rest { * This class implements a Connection class of each socket connection. * */ -class Connection +class Connection : public MainloopProcessor { public: /** @@ -63,6 +64,12 @@ class Connection */ Connection(steady_clock::time_point aStartTime, Resource *aResource, int aFd); + /** + * The desctructor destroys the connection instance. + * + */ + ~Connection(void) override; + /** * This method initializes the connection. * @@ -71,21 +78,20 @@ class Connection void Init(void); /** - * This method performs processing. + * This method updates the mainloop context. * - * @param[in] aReadFdSet The read file descriptors. - * @param[in] aWriteFdSet The write file descriptors. + * @param[inout] aMainloop A reference to the mainloop to be updated. * */ - void Process(fd_set &aReadFdSet, fd_set &aWriteFdSet); + void Update(MainloopContext &aMainloop) override; /** - * This method updates the file descriptor sets and timeout for mainloop. + * This method processes mainloop events. * - * @param[inout] aMainloop A reference to OpenThread mainloop context. + * @param[in] aMainloop A reference to the mainloop context. * */ - void UpdateFdSet(otSysMainloopContext &aMainloop) const; + void Process(const MainloopContext &aMainloop) override; /** * This method indicates whether this connection no longer need to be processed. @@ -100,9 +106,9 @@ class Connection void UpdateReadFdSet(fd_set &aReadFdSet, int &aMaxFd) const; void UpdateWriteFdSet(fd_set &aWriteFdSet, int &aMaxFd) const; void UpdateTimeout(timeval &aTimeout) const; - void ProcessWaitRead(fd_set &aReadFdSet); + void ProcessWaitRead(const fd_set &aReadFdSet); void ProcessWaitCallback(void); - void ProcessWaitWrite(fd_set &aWriteFdSet); + void ProcessWaitWrite(const fd_set &aWriteFdSet); void Write(void); void Handle(void); void Disconnect(void); diff --git a/src/rest/json.cpp b/src/rest/json.cpp index 2e4450a1e8e..f2c7c837ef7 100644 --- a/src/rest/json.cpp +++ b/src/rest/json.cpp @@ -29,6 +29,7 @@ #include "rest/json.hpp" #include "common/code_utils.hpp" +#include "common/types.hpp" extern "C" { #include @@ -108,20 +109,9 @@ static cJSON *Mode2Json(const otLinkModeConfig &aMode) static cJSON *IpAddr2Json(const otIp6Address &aAddress) { - std::string serilizedIpAddr; - char ipAddrField[5]; + Ip6Address addr(aAddress.mFields.m8); - for (size_t i = 0; i < 8; ++i) - { - sprintf(ipAddrField, "%x", aAddress.mFields.m16[i]); - if (i >= 1) - { - serilizedIpAddr += ":"; - } - serilizedIpAddr += std::string(ipAddrField); - } - - return cJSON_CreateString(serilizedIpAddr.c_str()); + return cJSON_CreateString(addr.ToString().c_str()); } static cJSON *ChildTableEntry2Json(const otNetworkDiagChildEntry &aChildEntry) diff --git a/src/rest/resource.cpp b/src/rest/resource.cpp index def6a00ab23..62452dae9f4 100644 --- a/src/rest/resource.cpp +++ b/src/rest/resource.cpp @@ -26,6 +26,8 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#define OTBR_LOG_TAG "REST" + #include "rest/resource.hpp" #include "string.h" @@ -65,7 +67,7 @@ namespace otbr { namespace rest { // MulticastAddr -static const char *kMulticastAddrAllRouters = "ff02::2"; +static const char *kMulticastAddrAllRouters = "ff03::2"; // Default TlvTypes for Diagnostic inforamtion static const uint8_t kAllTlvTypes[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 14, 15, 16, 17, 18, 19}; @@ -513,17 +515,21 @@ void Resource::UpdateDiag(std::string aKey, std::vector &aDiag void Resource::Diagnostic(const Request &aRequest, Response &aResponse) const { - otThreadSetReceiveDiagnosticGetCallback(mInstance, &Resource::DiagnosticResponseHandler, - const_cast(this)); + otbrError error = OTBR_ERROR_NONE; OT_UNUSED_VARIABLE(aRequest); struct otIp6Address rloc16address = *otThreadGetRloc(mInstance); struct otIp6Address multicastAddress; - VerifyOrExit(otThreadSendDiagnosticGet(mInstance, &rloc16address, kAllTlvTypes, sizeof(kAllTlvTypes)) == - OT_ERROR_NONE); - VerifyOrExit(otIp6AddressFromString(kMulticastAddrAllRouters, &multicastAddress) == OT_ERROR_NONE); - VerifyOrExit(otThreadSendDiagnosticGet(mInstance, &multicastAddress, kAllTlvTypes, sizeof(kAllTlvTypes)) == - OT_ERROR_NONE); + VerifyOrExit(otThreadSendDiagnosticGet(mInstance, &rloc16address, kAllTlvTypes, sizeof(kAllTlvTypes), + &Resource::DiagnosticResponseHandler, + const_cast(this)) == OT_ERROR_NONE, + error = OTBR_ERROR_REST); + VerifyOrExit(otIp6AddressFromString(kMulticastAddrAllRouters, &multicastAddress) == OT_ERROR_NONE, + error = OTBR_ERROR_REST); + VerifyOrExit(otThreadSendDiagnosticGet(mInstance, &multicastAddress, kAllTlvTypes, sizeof(kAllTlvTypes), + &Resource::DiagnosticResponseHandler, + const_cast(this)) == OT_ERROR_NONE, + error = OTBR_ERROR_REST); exit: @@ -531,13 +537,15 @@ void Resource::Diagnostic(const Request &aRequest, Response &aResponse) const aResponse.SetCallback(); } -void Resource::DiagnosticResponseHandler(otMessage *aMessage, const otMessageInfo *aMessageInfo, void *aContext) +void Resource::DiagnosticResponseHandler(otError aError, + otMessage * aMessage, + const otMessageInfo *aMessageInfo, + void * aContext) { - static_cast(aContext)->DiagnosticResponseHandler(aMessage, - *static_cast(aMessageInfo)); + static_cast(aContext)->DiagnosticResponseHandler(aError, aMessage, aMessageInfo); } -void Resource::DiagnosticResponseHandler(otMessage *aMessage, const otMessageInfo) +void Resource::DiagnosticResponseHandler(otError aError, const otMessage *aMessage, const otMessageInfo *aMessageInfo) { std::vector diagSet; otNetworkDiagTlv diagTlv; @@ -546,6 +554,10 @@ void Resource::DiagnosticResponseHandler(otMessage *aMessage, const otMessageInf char rloc[7]; std::string keyRloc = "0xffee"; + SuccessOrExit(aError); + + OTBR_UNUSED_VARIABLE(aMessageInfo); + while ((error = otThreadGetNextDiagnosticTlv(aMessage, &iterator, &diagTlv)) == OT_ERROR_NONE) { if (diagTlv.mType == OT_NETWORK_DIAGNOSTIC_TLV_SHORT_ADDRESS) @@ -556,6 +568,12 @@ void Resource::DiagnosticResponseHandler(otMessage *aMessage, const otMessageInf diagSet.push_back(diagTlv); } UpdateDiag(keyRloc, diagSet); + +exit: + if (aError != OT_ERROR_NONE) + { + otbrLogWarning("Failed to get diagnostic data: %s", otThreadErrorToString(aError)); + } } } // namespace rest diff --git a/src/rest/resource.hpp b/src/rest/resource.hpp index b818d7c913f..0928d265d2d 100644 --- a/src/rest/resource.hpp +++ b/src/rest/resource.hpp @@ -94,18 +94,6 @@ class Resource */ void ErrorHandler(Response &aResponse, HttpStatusCode aErrorCode) const; - /** - * This method is a pre-defined callback function used for call another private method when diagnostic information - * arrives. - * - * @param[in] aMessage A pointer to the message buffer containing the received Network Diagnostic - * Get response payload. - * @param[in] aMessageInfo A pointer to the message info for @p aMessage . - * @param[in] aContext A pointer to application-specific context. - * - */ - static void DiagnosticResponseHandler(otMessage *aMessage, const otMessageInfo *aMessageInfo, void *aContext); - private: typedef void (Resource::*ResourceHandler)(const Request &aRequest, Response &aResponse) const; typedef void (Resource::*ResourceCallbackHandler)(const Request &aRequest, Response &aResponse); @@ -133,7 +121,12 @@ class Resource void DeleteOutDatedDiagnostic(void); void UpdateDiag(std::string aKey, std::vector &aDiag); - void DiagnosticResponseHandler(otMessage *aMessage, const otMessageInfo); + + static void DiagnosticResponseHandler(otError aError, + otMessage * aMessage, + const otMessageInfo *aMessageInfo, + void * aContext); + void DiagnosticResponseHandler(otError aError, const otMessage *aMessage, const otMessageInfo *aMessageInfo); otInstance * mInstance; ControllerOpenThread *mNcp; diff --git a/src/rest/rest_web_server.cpp b/src/rest/rest_web_server.cpp index 120519016e2..246aeca1827 100644 --- a/src/rest/rest_web_server.cpp +++ b/src/rest/rest_web_server.cpp @@ -26,12 +26,16 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#define OTBR_LOG_TAG "REST" + #include "rest/rest_web_server.hpp" #include #include +#include "utils/socket_utils.hpp" + using std::chrono::duration_cast; using std::chrono::microseconds; using std::chrono::steady_clock; @@ -50,6 +54,14 @@ RestWebServer::RestWebServer(ControllerOpenThread *aNcp) { } +RestWebServer::~RestWebServer(void) +{ + if (mListenFd != -1) + { + close(mListenFd); + } +} + RestWebServer *RestWebServer::GetRestWebServer(ControllerOpenThread *aNcp) { static RestWebServer *sServer = new RestWebServer(aNcp); @@ -57,51 +69,39 @@ RestWebServer *RestWebServer::GetRestWebServer(ControllerOpenThread *aNcp) return sServer; } -otbrError RestWebServer::Init(void) +void RestWebServer::Init(void) { - otbrError error = OTBR_ERROR_NONE; - - error = InitializeListenFd(); - - return error; + InitializeListenFd(); } -void RestWebServer::UpdateFdSet(otSysMainloopContext &aMainloop) +void RestWebServer::Update(MainloopContext &aMainloop) { - if (mListenFd == -1) - { - VerifyOrExit(InitializeListenFd() == OTBR_ERROR_NONE); - } FD_SET(mListenFd, &aMainloop.mReadFdSet); aMainloop.mMaxFd = std::max(aMainloop.mMaxFd, mListenFd); for (auto it = mConnectionSet.begin(); it != mConnectionSet.end(); ++it) { Connection *connection = it->second.get(); - connection->UpdateFdSet(aMainloop); + connection->Update(aMainloop); } -exit: return; } -otbrError RestWebServer::Process(otSysMainloopContext &aMainloop) +void RestWebServer::Process(const MainloopContext &aMainloop) { - otbrError error = OTBR_ERROR_NONE; Connection *connection; - error = UpdateConnections(aMainloop.mReadFdSet); + UpdateConnections(aMainloop.mReadFdSet); for (auto it = mConnectionSet.begin(); it != mConnectionSet.end(); ++it) { connection = it->second.get(); - connection->Process(aMainloop.mReadFdSet, aMainloop.mWriteFdSet); + connection->Process(aMainloop); } - - return error; } -otbrError RestWebServer::UpdateConnections(fd_set &aReadFdSet) +void RestWebServer::UpdateConnections(const fd_set &aReadFdSet) { otbrError error = OTBR_ERROR_NONE; auto eraseIt = mConnectionSet.begin(); @@ -127,10 +127,13 @@ otbrError RestWebServer::UpdateConnections(fd_set &aReadFdSet) error = Accept(mListenFd); } - return error; + if (error != OTBR_ERROR_NONE) + { + otbrLogWarning("Failed to accept new connection: %s", otbrErrorString(error)); + } } -otbrError RestWebServer::InitializeListenFd(void) +void RestWebServer::InitializeListenFd(void) { otbrError error = OTBR_ERROR_NONE; std::string errorMessage; @@ -142,7 +145,7 @@ otbrError RestWebServer::InitializeListenFd(void) mAddress.sin_addr.s_addr = INADDR_ANY; mAddress.sin_port = htons(kPortNumber); - mListenFd = socket(AF_INET, SOCK_STREAM, 0); + mListenFd = SocketWithCloseExec(AF_INET, SOCK_STREAM, 0, kSocketNonBlock); VerifyOrExit(mListenFd != -1, err = errno, error = OTBR_ERROR_REST, errorMessage = "socket"); ret = setsockopt(mListenFd, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&optval), sizeof(optval)); @@ -154,21 +157,14 @@ otbrError RestWebServer::InitializeListenFd(void) ret = listen(mListenFd, 5); VerifyOrExit(ret >= 0, err = errno, error = OTBR_ERROR_REST, errorMessage = "listen"); - ret = SetFdNonblocking(mListenFd); - VerifyOrExit(ret, err = errno, error = OTBR_ERROR_REST, errorMessage = " set nonblock"); - exit: + if (error != OTBR_ERROR_NONE) { - if (mListenFd != -1) - { - close(mListenFd); - mListenFd = -1; - } - otbrLog(OTBR_LOG_ERR, "otbr rest server init error %s : %s", errorMessage.c_str(), strerror(err)); + otbrLogErr("InitializeListenFd error %s : %s", errorMessage.c_str(), strerror(err)); } - return error; + VerifyOrDie(error == OTBR_ERROR_NONE, "otbr rest server init error"); } otbrError RestWebServer::Accept(int aListenFd) @@ -197,7 +193,7 @@ otbrError RestWebServer::Accept(int aListenFd) close(fd); fd = -1; } - otbrLog(OTBR_LOG_ERR, "rest server accept error: %s %s", errorMessage.c_str(), strerror(err)); + otbrLogErr("Rest server accept error: %s %s", errorMessage.c_str(), strerror(err)); } return error; diff --git a/src/rest/rest_web_server.hpp b/src/rest/rest_web_server.hpp index 3b7319c0e70..32260288e4c 100644 --- a/src/rest/rest_web_server.hpp +++ b/src/rest/rest_web_server.hpp @@ -34,6 +34,11 @@ #ifndef OTBR_REST_REST_WEB_SERVER_HPP_ #define OTBR_REST_REST_WEB_SERVER_HPP_ +#include +#include +#include + +#include "common/mainloop.hpp" #include "rest/connection.hpp" using otbr::Ncp::ControllerOpenThread; @@ -46,7 +51,7 @@ namespace rest { * This class implements a REST server. * */ -class RestWebServer +class RestWebServer : public MainloopProcessor { public: /** @@ -60,36 +65,39 @@ class RestWebServer static RestWebServer *GetRestWebServer(ControllerOpenThread *aNcp); /** - * This method initializes the REST server. + * The destructor destroys the server instance. * - * @retval OTBR_ERROR_NONE REST server initialized successfully. - * @retval OTBR_ERROR_REST Failed due to rest error . + */ + ~RestWebServer(void) override; + + /** + * This method initializes the REST server. * */ - otbrError Init(void); + void Init(void); /** - * This method updates the file descriptor sets and timeout for mainloop. + * This method updates the mainloop context. * - * @param[inout] aMainloop A reference to OpenThread mainloop context. + * @param[inout] aMainloop A reference to the mainloop to be updated. * */ - void UpdateFdSet(otSysMainloopContext &aMainloop); + void Update(MainloopContext &aMainloop) override; /** - * This method performs processing. + * This method processes mainloop events. * - * @param[in] aMainloop A reference to OpenThread mainloop context. + * @param[in] aMainloop A reference to the mainloop context. * */ - otbrError Process(otSysMainloopContext &aMainloop); + void Process(const MainloopContext &aMainloop) override; private: RestWebServer(ControllerOpenThread *aNcp); - otbrError UpdateConnections(fd_set &aReadFdSet); + void UpdateConnections(const fd_set &aReadFdSet); void CreateNewConnection(int32_t &aFd); otbrError Accept(int32_t aListenFd); - otbrError InitializeListenFd(void); + void InitializeListenFd(void); bool SetFdNonblocking(int32_t fd); // Resource handler diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index fb47f28c9ab..7533b082607 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -28,11 +28,12 @@ add_library(otbr-utils crc16.cpp - event_emitter.cpp hex.cpp pskc.cpp + socket_utils.cpp steering_data.cpp strcpy_utils.cpp + system_utils.cpp ) target_link_libraries(otbr-utils PRIVATE otbr-common diff --git a/src/utils/pskc.cpp b/src/utils/pskc.cpp index 0da32ac298e..bcd37a69af7 100644 --- a/src/utils/pskc.cpp +++ b/src/utils/pskc.cpp @@ -61,7 +61,7 @@ void Pskc::SetSalt(const uint8_t *aExtPanId, const char *aNetworkName) exit: if (ret != kPskcStatus_Ok) { - otbrLog(OTBR_LOG_ERR, "ExtPanId or NetworkName is nullptr"); + otbrLogErr("ExtPanId or NetworkName is nullptr"); } return; } diff --git a/src/utils/socket_utils.cpp b/src/utils/socket_utils.cpp new file mode 100644 index 00000000000..54b806c3b22 --- /dev/null +++ b/src/utils/socket_utils.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "utils/socket_utils.hpp" + +#include "common/code_utils.hpp" + +int SocketWithCloseExec(int aDomain, int aType, int aProtocol, SocketBlockOption aBlockOption) +{ + int rval = 0; + int fd = -1; + +#ifdef __APPLE__ + VerifyOrExit((fd = socket(aDomain, aType, aProtocol)) != -1, perror("socket(SOCK_CLOEXEC)")); + + VerifyOrExit((rval = fcntl(fd, F_GETFD, 0)) != -1, perror("fcntl(F_GETFD)")); + rval |= aBlockOption == kSocketNonBlock ? O_NONBLOCK | FD_CLOEXEC : FD_CLOEXEC; + VerifyOrExit((rval = fcntl(fd, F_SETFD, rval)) != -1, perror("fcntl(F_SETFD)")); +#else + aType |= aBlockOption == kSocketNonBlock ? SOCK_CLOEXEC | SOCK_NONBLOCK : SOCK_CLOEXEC; + VerifyOrExit((fd = socket(aDomain, aType, aProtocol)) != -1, perror("socket(SOCK_CLOEXEC)")); +#endif + +exit: + if (rval == -1) + { + VerifyOrDie(close(fd) == 0, "close(fd) failed"); + fd = -1; + } + + return fd; +} diff --git a/src/utils/socket_utils.hpp b/src/utils/socket_utils.hpp new file mode 100644 index 00000000000..0f591000937 --- /dev/null +++ b/src/utils/socket_utils.hpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef OTBR_UTILS_SOCKET_UTILS_HPP_ +#define OTBR_UTILS_SOCKET_UTILS_HPP_ + +#include "openthread-br/config.h" + +#include "common/code_utils.hpp" + +#include +#include +#include +#include + +enum SocketBlockOption +{ + kSocketBlock, ///< The socket is blocking. + kSocketNonBlock, ///< The socket is non-blocking. +}; + +/** + * This function creates a socket with SOCK_CLOEXEC flag set. + * + * @param[in] aDomain The communication domain. + * @param[in] aType The semantics of communication. + * @param[in] aProtocol The protocol to use. + * @param[in] aBlockOption Whether to add nonblock flags. + * + * @returns The file descriptor of the created socket. + * + * @retval -1 Failed to create socket. + * + */ +int SocketWithCloseExec(int aDomain, int aType, int aProtocol, SocketBlockOption aBlockOption); + +#endif // OTBR_UTILS_SOCKET_UTILS_HPP_ diff --git a/src/utils/strcpy_utils.hpp b/src/utils/strcpy_utils.hpp index dcf79af99bf..1637bb320e3 100644 --- a/src/utils/strcpy_utils.hpp +++ b/src/utils/strcpy_utils.hpp @@ -26,6 +26,11 @@ * POSSIBILITY OF SUCH DAMAGE. */ +/** + * @file + * This file includes definition for strcpy_safe(). + */ + #ifndef OTBR_UTILS_STRCPY_UTILS_HPP_ #define OTBR_UTILS_STRCPY_UTILS_HPP_ diff --git a/src/utils/system_utils.cpp b/src/utils/system_utils.cpp new file mode 100644 index 00000000000..0e943f8b65a --- /dev/null +++ b/src/utils/system_utils.cpp @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * The file implements POSIX system utilities. + */ + +#define OTBR_LOG_TAG "UTILS" + +#include "system_utils.hpp" + +#include +#include +#include + +#include "common/logging.hpp" + +namespace otbr { +namespace SystemUtils { + +enum +{ + kSystemCommandMaxLength = 1024, ///< Max length of a system call command. +}; + +int ExecuteCommand(const char *aFormat, ...) +{ + char cmd[kSystemCommandMaxLength]; + int exitCode; + va_list args; + + va_start(args, aFormat); + vsnprintf(cmd, sizeof(cmd), aFormat, args); + va_end(args); + + exitCode = system(cmd); + + if (exitCode == 0) + { + otbrLogInfo("$?=%-3d: %s", exitCode, cmd); + } + else + { + otbrLogWarning("$?=%-3d: %s", exitCode, cmd); + } + + return exitCode; +} + +} // namespace SystemUtils +} // namespace otbr diff --git a/src/utils/system_utils.hpp b/src/utils/system_utils.hpp new file mode 100644 index 00000000000..4ae2dad1597 --- /dev/null +++ b/src/utils/system_utils.hpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2020, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file + * This file includes definitions for POSIX system utilities. + */ + +#ifndef OTBR_UTILS_SYSTEM_UTILS_HPP_ +#define OTBR_UTILS_SYSTEM_UTILS_HPP_ + +namespace otbr { +namespace SystemUtils { + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * This method formats a system command to execute. + * + * @param[in] aFormat A pointer to the format string. + * @param[in] ... Arguments for the format specification. + * + * @returns The command exit code. + * + */ +int ExecuteCommand(const char *aFormat, ...); + +#ifdef __cplusplus +} +#endif + +} // namespace SystemUtils +} // namespace otbr + +#endif // OTBR_UTILS_SYSTEM_UTILS_HPP_ diff --git a/src/web/CMakeLists.txt b/src/web/CMakeLists.txt index 0b755129aac..37ba68ffcfc 100644 --- a/src/web/CMakeLists.txt +++ b/src/web/CMakeLists.txt @@ -26,6 +26,13 @@ # POSSIBILITY OF SUCH DAMAGE. # +pkg_check_modules(JSONCPP jsoncpp REQUIRED) +set(Boost_USE_STATIC_LIBS ON) +set(Boost_USE_MULTITHREADED ON) +set(Boost_USE_STATIC_RUNTIME OFF) +find_package(Boost REQUIRED COMPONENTS filesystem system) +set(OTBR_WEB_DATADIR ${CMAKE_INSTALL_FULL_DATADIR}/otbr-web) + add_executable(otbr-web main.cpp web-service/ot_client.cpp @@ -61,14 +68,9 @@ install( DESTINATION sbin ) -add_subdirectory(${PROJECT_SOURCE_DIR}/third_party/angular ${CMAKE_CURRENT_BINARY_DIR}/angular) -add_subdirectory(${PROJECT_SOURCE_DIR}/third_party/angular-material ${CMAKE_CURRENT_BINARY_DIR}/angular-material) -add_subdirectory(${PROJECT_SOURCE_DIR}/third_party/mdl ${CMAKE_CURRENT_BINARY_DIR}/mdl) -add_subdirectory(${PROJECT_SOURCE_DIR}/third_party/d3js ${CMAKE_CURRENT_BINARY_DIR}/d3js) +add_subdirectory(web-service/frontend) -install(DIRECTORY web-service/frontend - DESTINATION ${OTBR_WEB_DATADIR} -) +add_dependencies(otbr-web otbr-web-frontend) if(OTBR_SYSTEMD_UNIT_DIR) configure_file(otbr-web.service.in otbr-web.service) diff --git a/src/web/main.cpp b/src/web/main.cpp index 895a54b4da5..e5f47f52c25 100644 --- a/src/web/main.cpp +++ b/src/web/main.cpp @@ -31,6 +31,7 @@ * This file is the entry of the program, it starts a Web service. */ #define OT_HTTP_PORT 80 +#define OTBR_LOG_TAG "WEB" #include "openthread-br/config.h" @@ -55,7 +56,7 @@ static void HandleSignal(int aSignal) { signal(aSignal, SIG_DFL); - otbrLog(OTBR_LOG_CRIT, "Stopping web server"); + otbrLogCrit("Stopping web server"); if (sServer != nullptr) { @@ -70,13 +71,13 @@ static void PrintVersion(void) int main(int argc, char **argv) { - const char *interfaceName = nullptr; - const char *httpListenAddr = nullptr; - const char *httpPort = nullptr; - int logLevel = OTBR_LOG_INFO; - int ret = 0; - int opt; - uint16_t port = OT_HTTP_PORT; + const char * interfaceName = nullptr; + const char * httpListenAddr = nullptr; + const char * httpPort = nullptr; + otbrLogLevel logLevel = OTBR_LOG_INFO; + int ret = 0; + int opt; + uint16_t port = OT_HTTP_PORT; while ((opt = getopt(argc, argv, "d:I:p:va:")) != -1) { @@ -86,7 +87,7 @@ int main(int argc, char **argv) httpListenAddr = optarg; break; case 'd': - logLevel = atoi(optarg); + logLevel = static_cast(atoi(optarg)); break; case 'I': interfaceName = optarg; @@ -112,7 +113,7 @@ int main(int argc, char **argv) } otbrLogInit(kSyslogIdent, logLevel, true); - otbrLog(OTBR_LOG_INFO, "Running %s", OTBR_PACKAGE_VERSION); + otbrLogInfo("Running %s", OTBR_PACKAGE_VERSION); if (interfaceName == nullptr) { @@ -131,7 +132,7 @@ int main(int argc, char **argv) printf("http port not specified, using default %d\n", port); } - otbrLog(OTBR_LOG_INFO, "border router web started on %s", interfaceName); + otbrLogInfo("Border router web started on %s", interfaceName); // allow quitting elegantly signal(SIGTERM, HandleSignal); diff --git a/src/web/web-service/frontend/CMakeLists.txt b/src/web/web-service/frontend/CMakeLists.txt new file mode 100644 index 00000000000..d61a41f3f9b --- /dev/null +++ b/src/web/web-service/frontend/CMakeLists.txt @@ -0,0 +1,57 @@ +# +# Copyright (c) 2021, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +set(NPM_CSS_DEPENDENCIES + ${CMAKE_CURRENT_BINARY_DIR}/node_modules/angular-material/angular-material.min.css + ${CMAKE_CURRENT_BINARY_DIR}/node_modules/material-design-lite/material.min.css +) +set(NPM_JS_DEPENDENCIES + ${CMAKE_CURRENT_BINARY_DIR}/node_modules/angular-animate/angular-animate.min.js + ${CMAKE_CURRENT_BINARY_DIR}/node_modules/angular-aria/angular-aria.min.js + ${CMAKE_CURRENT_BINARY_DIR}/node_modules/angular-material/angular-material.min.js + ${CMAKE_CURRENT_BINARY_DIR}/node_modules/angular-messages/angular-messages.min.js + ${CMAKE_CURRENT_BINARY_DIR}/node_modules/angular/angular.min.js + ${CMAKE_CURRENT_BINARY_DIR}/node_modules/d3/d3.min.js + ${CMAKE_CURRENT_BINARY_DIR}/node_modules/material-design-lite/material.min.js +) + +add_custom_target(otbr-web-frontend + COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/package.json . + COMMAND npm install) + +install(FILES index.html join.dialog.html + DESTINATION ${OTBR_WEB_DATADIR}/frontend) + +install(DIRECTORY res + DESTINATION ${OTBR_WEB_DATADIR}/frontend) + +install(FILES ${NPM_JS_DEPENDENCIES} + DESTINATION ${OTBR_WEB_DATADIR}/frontend/res/js) + +install(FILES ${NPM_CSS_DEPENDENCIES} + DESTINATION ${OTBR_WEB_DATADIR}/frontend/res/css) diff --git a/src/web/web-service/frontend/index.html b/src/web/web-service/frontend/index.html index 88291dbbdff..2e815182ab8 100644 --- a/src/web/web-service/frontend/index.html +++ b/src/web/web-service/frontend/index.html @@ -288,16 +288,6 @@

Settings

Commission

-
- - - -
-
This is required.
-
-
-
-
diff --git a/src/web/web-service/frontend/package-lock.json b/src/web/web-service/frontend/package-lock.json new file mode 100644 index 00000000000..cc530d45728 --- /dev/null +++ b/src/web/web-service/frontend/package-lock.json @@ -0,0 +1,43 @@ +{ + "name": "otbr-web", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "angular": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/angular/-/angular-1.8.0.tgz", + "integrity": "sha512-VdaMx+Qk0Skla7B5gw77a8hzlcOakwF8mjlW13DpIWIDlfqwAbSSLfd8N/qZnzEmQF4jC4iofInd3gE7vL8ZZg==" + }, + "angular-animate": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/angular-animate/-/angular-animate-1.6.4.tgz", + "integrity": "sha1-0+uQbTmDTy373Zgua416O02QAdI=" + }, + "angular-aria": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/angular-aria/-/angular-aria-1.6.4.tgz", + "integrity": "sha1-yGg2ZqzhlmaPaOciCBG9z8nhBuQ=" + }, + "angular-material": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/angular-material/-/angular-material-1.1.4.tgz", + "integrity": "sha1-J941ZG9UzNMgCArwxwjhtDivh/Y=" + }, + "angular-messages": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/angular-messages/-/angular-messages-1.6.4.tgz", + "integrity": "sha1-nsnBOTKTsU3lQWHS+El3ZInpgP4=" + }, + "d3": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", + "integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=" + }, + "material-design-lite": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/material-design-lite/-/material-design-lite-1.3.0.tgz", + "integrity": "sha1-0ATOP+6Zoe63Sni4oyUTSl8RcdM=" + } + } +} diff --git a/src/web/web-service/frontend/package.json b/src/web/web-service/frontend/package.json new file mode 100644 index 00000000000..c5fc774be7f --- /dev/null +++ b/src/web/web-service/frontend/package.json @@ -0,0 +1,20 @@ +{ + "name": "otbr-web", + "version": "1.0.0", + "description": "OpenThread Border Router Web GUI", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "BSD-3-Clause", + "dependencies": { + "angular": "^1.8.0", + "angular-animate": "^1.6.4", + "angular-aria": "^1.6.4", + "angular-material": "^1.1.4", + "angular-messages": "^1.6.4", + "d3": "^3.5.17", + "material-design-lite": "^1.3.0" + } +} diff --git a/src/web/web-service/ot_client.cpp b/src/web/web-service/ot_client.cpp index f151a01262f..7eb862d4920 100644 --- a/src/web/web-service/ot_client.cpp +++ b/src/web/web-service/ot_client.cpp @@ -26,6 +26,8 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#define OTBR_LOG_TAG "WEB" + #include "web/web-service/ot_client.hpp" #include @@ -46,15 +48,22 @@ #include "utils/strcpy_utils.hpp" // Temporary solution before posix platform header files are cleaned up. -#ifndef OPENTHREAD_POSIX_APP_SOCKET_NAME -#define OPENTHREAD_POSIX_APP_SOCKET_NAME "/tmp/openthread.sock" +#ifndef OPENTHREAD_POSIX_DAEMON_SOCKET_NAME +#ifdef __linux__ +#define OPENTHREAD_POSIX_CONFIG_DAEMON_SOCKET_BASENAME "/run/openthread-%s" +#else +#define OPENTHREAD_POSIX_CONFIG_DAEMON_SOCKET_BASENAME "/tmp/openthread-%s" +#endif +#define OPENTHREAD_POSIX_DAEMON_SOCKET_NAME OPENTHREAD_POSIX_CONFIG_DAEMON_SOCKET_BASENAME ".sock" #endif namespace otbr { namespace Web { -OpenThreadClient::OpenThreadClient(void) - : mTimeout(kDefaultTimeout) +OpenThreadClient::OpenThreadClient(const char *aNetifName) + + : mNetifName(aNetifName) + , mTimeout(kDefaultTimeout) , mSocket(-1) { } @@ -83,13 +92,18 @@ bool OpenThreadClient::Connect(void) memset(&sockname, 0, sizeof(struct sockaddr_un)); sockname.sun_family = AF_UNIX; - strcpy_safe(sockname.sun_path, sizeof(sockname.sun_path), OPENTHREAD_POSIX_APP_SOCKET_NAME); + ret = snprintf(sockname.sun_path, sizeof(sockname.sun_path), OPENTHREAD_POSIX_DAEMON_SOCKET_NAME, mNetifName); + + VerifyOrExit(ret >= 0 && static_cast(ret) < sizeof(sockname.sun_path), { + errno = EINVAL; + ret = -1; + }); ret = connect(mSocket, reinterpret_cast(&sockname), sizeof(struct sockaddr_un)); if (ret == -1) { - otbrLog(OTBR_LOG_ERR, "OpenThread daemon is not running."); + otbrLogErr("OpenThread daemon is not running."); } exit: @@ -110,7 +124,7 @@ char *OpenThreadClient::Execute(const char *aFormat, ...) if (ret < 0) { - otbrLog(OTBR_LOG_ERR, "Failed to generate command: %s", strerror(errno)); + otbrLogErr("Failed to generate command: %s", strerror(errno)); } mBuffer[0] = '\n'; @@ -118,7 +132,7 @@ char *OpenThreadClient::Execute(const char *aFormat, ...) if (ret == sizeof(mBuffer)) { - otbrLog(OTBR_LOG_ERR, "Command exceeds maximum limit: %d", kBufferSize); + otbrLogErr("Command exceeds maximum limit: %d", kBufferSize); } mBuffer[ret] = '\n'; @@ -129,7 +143,7 @@ char *OpenThreadClient::Execute(const char *aFormat, ...) if (count < ret) { mBuffer[ret] = '\0'; - otbrLog(OTBR_LOG_ERR, "Failed to send command: %s", mBuffer); + otbrLogErr("Failed to send command: %s", mBuffer); } for (int i = 0; i < mTimeout; ++i) @@ -158,7 +172,7 @@ char *OpenThreadClient::Execute(const char *aFormat, ...) if (done != nullptr) { // remove trailing \r\n - if (done - rval > 2) + if (done - mBuffer > 2) { done[-2] = '\0'; } diff --git a/src/web/web-service/ot_client.hpp b/src/web/web-service/ot_client.hpp index c70513092c7..193046f61d4 100644 --- a/src/web/web-service/ot_client.hpp +++ b/src/web/web-service/ot_client.hpp @@ -25,6 +25,12 @@ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ + +/** + * @file + * This file includes definitions for OpenThread daemon client. + */ + #ifndef OTBR_WEB_WEB_SERVICE_OT_CLIENT_HPP_ #define OTBR_WEB_WEB_SERVICE_OT_CLIENT_HPP_ @@ -64,8 +70,11 @@ class OpenThreadClient /** * This constructor creates an OpenThread client. * + * @param[in] aNetifName The Thread network interface name. + * */ - OpenThreadClient(void); + OpenThreadClient(const char *aNetifName); + /** * This destructor destories an OpenThread client. * @@ -118,9 +127,10 @@ class OpenThreadClient kDefaultTimeout = 800, ///< Default timeout(ms) waiting for a command finish. }; - char mBuffer[kBufferSize]; - int mTimeout; /// Timeout in milliseconds - int mSocket; + const char *mNetifName; + char mBuffer[kBufferSize]; + int mTimeout; /// Timeout in milliseconds + int mSocket; }; } // namespace Web diff --git a/src/web/web-service/web_server.cpp b/src/web/web-service/web_server.cpp index d99cc01c823..3d2627cdb67 100644 --- a/src/web/web-service/web_server.cpp +++ b/src/web/web-service/web_server.cpp @@ -31,6 +31,8 @@ * This file implements the web server of border router */ +#define OTBR_LOG_TAG "WEB" + #include "web/web-service/web_server.hpp" #define BOOST_NO_CXX11_SCOPED_ENUMS @@ -39,6 +41,9 @@ #include +#include "common/code_utils.hpp" +#include "common/logging.hpp" + #define OT_ADD_PREFIX_PATH "^/add_prefix" #define OT_AVAILABLE_NETWORK_PATH "^/available_network$" #define OT_DELETE_PREFIX_PATH "^/delete_prefix" @@ -130,12 +135,26 @@ void WebServer::StartWebServer(const char *aIfName, const char *aListenAddr, uin ResponseGetAvailableNetwork(); ResponseCommission(); DefaultHttpResponse(); - mServer->start(); + + try + { + mServer->start(); + } catch (const std::exception &e) + { + otbrLogCrit("failed to start web server: %s", e.what()); + abort(); + } } void WebServer::StopWebServer(void) { - mServer->stop(); + try + { + mServer->stop(); + } catch (const std::exception &e) + { + otbrLogCrit("failed to stop web server: %s", e.what()); + } } void WebServer::HandleHttpRequest(const char *aUrl, const char *aMethod, HttpRequestCallback aCallback) @@ -355,13 +374,13 @@ std::string WebServer::HandleDeletePrefixRequest(const std::string &aDeletePrefi std::string WebServer::HandleGetStatusRequest(const std::string &aGetStatusRequest) { - (void)aGetStatusRequest; + OTBR_UNUSED_VARIABLE(aGetStatusRequest); return mWpanService.HandleStatusRequest(); } std::string WebServer::HandleGetAvailableNetworkResponse(const std::string &aGetAvailableNetworkRequest) { - (void)aGetAvailableNetworkRequest; + OTBR_UNUSED_VARIABLE(aGetAvailableNetworkRequest); return mWpanService.HandleAvailableNetworkRequest(); } diff --git a/src/web/web-service/wpan_service.cpp b/src/web/web-service/wpan_service.cpp index b272e6d9f74..b18e934c9dc 100644 --- a/src/web/web-service/wpan_service.cpp +++ b/src/web/web-service/wpan_service.cpp @@ -31,9 +31,15 @@ * This file implements the wpan controller service */ +#define OTBR_LOG_TAG "WEB" + #include "web/web-service/wpan_service.hpp" +#include + #include +#include +#include #include "common/byteswap.hpp" #include "common/code_utils.hpp" @@ -41,9 +47,6 @@ namespace otbr { namespace Web { -const char *WpanService::kBorderAgentHost = "127.0.0.1"; -const char *WpanService::kBorderAgentPort = "49191"; - #define WPAN_RESPONSE_SUCCESS "successful" #define WPAN_RESPONSE_FAILURE "failed" @@ -58,7 +61,7 @@ std::string WpanService::HandleJoinNetworkRequest(const std::string &aJoinReques std::string prefix; bool defaultRoute; int ret = kWpanStatus_Ok; - otbr::Web::OpenThreadClient client; + otbr::Web::OpenThreadClient client(mIfName); VerifyOrExit(client.Connect(), ret = kWpanStatus_SetFailed); @@ -74,16 +77,8 @@ std::string WpanService::HandleJoinNetworkRequest(const std::string &aJoinReques } VerifyOrExit(client.FactoryReset(), ret = kWpanStatus_LeaveFailed); - VerifyOrExit(client.Execute("dataset init new") != nullptr, ret = kWpanStatus_SetFailed); - VerifyOrExit(client.Execute("dataset masterkey %s", masterKey.c_str()) != nullptr, ret = kWpanStatus_SetFailed); - VerifyOrExit(client.Execute("dataset networkname %s", mNetworks[index].mNetworkName) != nullptr, - ret = kWpanStatus_SetFailed); - VerifyOrExit(client.Execute("dataset channel %u", mNetworks[index].mChannel) != nullptr, - ret = kWpanStatus_SetFailed); - VerifyOrExit(client.Execute("dataset extpanid %016" PRIx64, mNetworks[index].mExtPanId) != nullptr, - ret = kWpanStatus_SetFailed); - VerifyOrExit(client.Execute("dataset panid %u", mNetworks[index].mPanId) != nullptr, ret = kWpanStatus_SetFailed); - VerifyOrExit(client.Execute("dataset commit active") != nullptr, ret = kWpanStatus_SetFailed); + VerifyOrExit((ret = commitActiveDataset(client, masterKey, mNetworks[index].mNetworkName, mNetworks[index].mChannel, + mNetworks[index].mExtPanId, mNetworks[index].mPanId)) == kWpanStatus_Ok); VerifyOrExit(client.Execute("ifconfig up") != nullptr, ret = kWpanStatus_JoinFailed); VerifyOrExit(client.Execute("thread start") != nullptr, ret = kWpanStatus_JoinFailed); VerifyOrExit(client.Execute("prefix add %s paso%s", prefix.c_str(), (defaultRoute ? "r" : "")) != nullptr, @@ -96,7 +91,7 @@ std::string WpanService::HandleJoinNetworkRequest(const std::string &aJoinReques root["error"] = ret; if (ret != kWpanStatus_Ok) { - otbrLog(OTBR_LOG_ERR, "wpan service error: %d", ret); + otbrLogErr("Wpan service error: %d", ret); root["result"] = WPAN_RESPONSE_FAILURE; } response = jsonWriter.write(root); @@ -117,26 +112,27 @@ std::string WpanService::HandleFormNetworkRequest(const std::string &aFormReques uint16_t channel; std::string networkName; std::string passphrase; - std::string panId; - std::string extPanId; + uint16_t panId; + uint64_t extPanId; bool defaultRoute; int ret = kWpanStatus_Ok; - otbr::Web::OpenThreadClient client; + otbr::Web::OpenThreadClient client(mIfName); VerifyOrExit(client.Connect(), ret = kWpanStatus_SetFailed); pskcStr[OT_PSKC_MAX_LENGTH * 2] = '\0'; // for manipulating with strlen VerifyOrExit(reader.parse(aFormRequest.c_str(), root) == true, ret = kWpanStatus_ParseRequestFailed); - masterKey = root["masterKey"].asString(); - prefix = root["prefix"].asString(); - channel = root["channel"].asUInt(); - networkName = root["networkName"].asString(); - passphrase = root["passphrase"].asString(); - panId = root["panId"].asString(); - extPanId = root["extPanId"].asString(); + masterKey = root["masterKey"].asString(); + prefix = root["prefix"].asString(); + channel = root["channel"].asUInt(); + networkName = root["networkName"].asString(); + passphrase = root["passphrase"].asString(); + VerifyOrExit(sscanf(root["panId"].asString().c_str(), "%hx", &panId) == 1, ret = kWpanStatus_ParseRequestFailed); + VerifyOrExit(sscanf(root["extPanId"].asString().c_str(), "%" PRIx64, &extPanId) == 1, + ret = kWpanStatus_ParseRequestFailed); defaultRoute = root["defaultRoute"].asBool(); - otbr::Utils::Hex2Bytes(extPanId.c_str(), extPanIdBytes, OT_EXTENDED_PANID_LENGTH); + otbr::Utils::Hex2Bytes(root["extPanId"].asString().c_str(), extPanIdBytes, OT_EXTENDED_PANID_LENGTH); otbr::Utils::Bytes2Hex(psk.ComputePskc(extPanIdBytes, networkName.c_str(), passphrase.c_str()), OT_PSKC_MAX_LENGTH, pskcStr); @@ -146,11 +142,8 @@ std::string WpanService::HandleFormNetworkRequest(const std::string &aFormReques } VerifyOrExit(client.FactoryReset(), ret = kWpanStatus_LeaveFailed); - VerifyOrExit(client.Execute("masterkey %s", masterKey.c_str()) != nullptr, ret = kWpanStatus_SetFailed); - VerifyOrExit(client.Execute("networkname %s", networkName.c_str()) != nullptr, ret = kWpanStatus_SetFailed); - VerifyOrExit(client.Execute("channel %u", channel) != nullptr, ret = kWpanStatus_SetFailed); - VerifyOrExit(client.Execute("extpanid %s", extPanId.c_str()) != nullptr, ret = kWpanStatus_SetFailed); - VerifyOrExit(client.Execute("panid %s", panId.c_str()) != nullptr, ret = kWpanStatus_SetFailed); + VerifyOrExit((ret = commitActiveDataset(client, masterKey, networkName, channel, extPanId, panId)) == + kWpanStatus_Ok); VerifyOrExit(client.Execute("pskc %s", pskcStr) != nullptr, ret = kWpanStatus_SetFailed); VerifyOrExit(client.Execute("ifconfig up") != nullptr, ret = kWpanStatus_FormFailed); VerifyOrExit(client.Execute("thread start") != nullptr, ret = kWpanStatus_FormFailed); @@ -164,7 +157,7 @@ std::string WpanService::HandleFormNetworkRequest(const std::string &aFormReques root["error"] = ret; if (ret != kWpanStatus_Ok) { - otbrLog(OTBR_LOG_ERR, "wpan service error: %d", ret); + otbrLogErr("Wpan service error: %d", ret); root["result"] = WPAN_RESPONSE_FAILURE; } response = jsonWriter.write(root); @@ -180,7 +173,7 @@ std::string WpanService::HandleAddPrefixRequest(const std::string &aAddPrefixReq std::string prefix; bool defaultRoute; int ret = kWpanStatus_Ok; - otbr::Web::OpenThreadClient client; + otbr::Web::OpenThreadClient client(mIfName); VerifyOrExit(client.Connect(), ret = kWpanStatus_SetFailed); @@ -188,8 +181,14 @@ std::string WpanService::HandleAddPrefixRequest(const std::string &aAddPrefixReq prefix = root["prefix"].asString(); defaultRoute = root["defaultRoute"].asBool(); + if (prefix.find('/') == std::string::npos) + { + prefix += "/64"; + } + VerifyOrExit(client.Execute("prefix add %s paso%s", prefix.c_str(), (defaultRoute ? "r" : "")) != nullptr, ret = kWpanStatus_SetGatewayFailed); + VerifyOrExit(client.Execute("netdata register") != nullptr, ret = kWpanStatus_SetGatewayFailed); exit: root.clear(); @@ -198,7 +197,7 @@ std::string WpanService::HandleAddPrefixRequest(const std::string &aAddPrefixReq root["error"] = ret; if (ret != kWpanStatus_Ok) { - otbrLog(OTBR_LOG_ERR, "wpan service error: %d", ret); + otbrLogErr("Wpan service error: %d", ret); root["result"] = WPAN_RESPONSE_FAILURE; } response = jsonWriter.write(root); @@ -213,14 +212,20 @@ std::string WpanService::HandleDeletePrefixRequest(const std::string &aDeleteReq std::string response; std::string prefix; int ret = kWpanStatus_Ok; - otbr::Web::OpenThreadClient client; + otbr::Web::OpenThreadClient client(mIfName); VerifyOrExit(client.Connect(), ret = kWpanStatus_SetFailed); VerifyOrExit(reader.parse(aDeleteRequest.c_str(), root) == true, ret = kWpanStatus_ParseRequestFailed); prefix = root["prefix"].asString(); + if (prefix.find('/') == std::string::npos) + { + prefix += "/64"; + } + VerifyOrExit(client.Execute("prefix remove %s", prefix.c_str()) != nullptr, ret = kWpanStatus_SetGatewayFailed); + VerifyOrExit(client.Execute("netdata register") != nullptr, ret = kWpanStatus_SetGatewayFailed); exit: root.clear(); @@ -229,7 +234,7 @@ std::string WpanService::HandleDeletePrefixRequest(const std::string &aDeleteReq root["error"] = ret; if (ret != kWpanStatus_Ok) { - otbrLog(OTBR_LOG_ERR, "wpan service error: %d", ret); + otbrLogErr("Wpan service error: %d", ret); root["result"] = WPAN_RESPONSE_FAILURE; } response = jsonWriter.write(root); @@ -242,14 +247,14 @@ std::string WpanService::HandleStatusRequest() Json::FastWriter jsonWriter; std::string response, networkName, extPanId, propertyValue; int ret = kWpanStatus_Ok; - otbr::Web::OpenThreadClient client; + otbr::Web::OpenThreadClient client(mIfName); char * rval; networkInfo["WPAN service"] = "uninitialized"; VerifyOrExit(client.Connect(), ret = kWpanStatus_SetFailed); VerifyOrExit((rval = client.Execute("state")) != nullptr, ret = kWpanStatus_GetPropertyFailed); - networkInfo["NCP:State"] = rval; + networkInfo["RCP:State"] = rval; if (!strcmp(rval, "disabled")) { @@ -267,16 +272,22 @@ std::string WpanService::HandleStatusRequest() } VerifyOrExit((rval = client.Execute("version")) != nullptr, ret = kWpanStatus_GetPropertyFailed); - networkInfo["NCP:Version"] = rval; + networkInfo["OpenThread:Version"] = rval; + + VerifyOrExit((rval = client.Execute("version api")) != nullptr, ret = kWpanStatus_GetPropertyFailed); + networkInfo["OpenThread:Version API"] = rval; + + VerifyOrExit((rval = client.Execute("rcp version")) != nullptr, ret = kWpanStatus_GetPropertyFailed); + networkInfo["RCP:Version"] = rval; VerifyOrExit((rval = client.Execute("eui64")) != nullptr, ret = kWpanStatus_GetPropertyFailed); - networkInfo["NCP:HardwareAddress"] = rval; + networkInfo["RCP:EUI64"] = rval; VerifyOrExit((rval = client.Execute("channel")) != nullptr, ret = kWpanStatus_GetPropertyFailed); - networkInfo["NCP:Channel"] = rval; + networkInfo["RCP:Channel"] = rval; - VerifyOrExit((rval = client.Execute("state")) != nullptr, ret = kWpanStatus_GetPropertyFailed); - networkInfo["Network:NodeType"] = rval; + VerifyOrExit((rval = client.Execute("txpower")) != nullptr, ret = kWpanStatus_GetPropertyFailed); + networkInfo["RCP:TxPower"] = rval; VerifyOrExit((rval = client.Execute("networkname")) != nullptr, ret = kWpanStatus_GetPropertyFailed); networkInfo["Network:Name"] = rval; @@ -287,20 +298,27 @@ std::string WpanService::HandleStatusRequest() VerifyOrExit((rval = client.Execute("panid")) != nullptr, ret = kWpanStatus_GetPropertyFailed); networkInfo["Network:PANID"] = rval; + VerifyOrExit((rval = client.Execute("partitionid")) != nullptr, ret = kWpanStatus_GetPropertyFailed); + networkInfo["Network:PartitionID"] = rval; + { static const char kMeshLocalPrefixLocator[] = "Mesh Local Prefix: "; static const char kMeshLocalAddressTokenLocator[] = "0:ff:fe00:"; - std::string meshLocalPrefix; + static const char localAddressToken[] = "fd"; + static const char linkLocalAddressToken[] = "fe80"; + std::string meshLocalPrefix = ""; VerifyOrExit((rval = client.Execute("dataset active")) != nullptr, ret = kWpanStatus_GetPropertyFailed); rval = strstr(rval, kMeshLocalPrefixLocator); - rval += sizeof(kMeshLocalPrefixLocator) - 1; - *strstr(rval, "\r\n") = '\0'; - - networkInfo["IPv6:MeshLocalPrefix"] = rval; + if (rval != nullptr) + { + rval += sizeof(kMeshLocalPrefixLocator) - 1; + *strstr(rval, "\r\n") = '\0'; + networkInfo["IPv6:MeshLocalPrefix"] = rval; - meshLocalPrefix = rval; - meshLocalPrefix.resize(meshLocalPrefix.find('/')); + meshLocalPrefix = rval; + meshLocalPrefix.resize(meshLocalPrefix.find(":/")); + } VerifyOrExit((rval = client.Execute("ipaddr")) != nullptr, ret = kWpanStatus_GetPropertyFailed); @@ -308,8 +326,14 @@ std::string WpanService::HandleStatusRequest() { char *meshLocalAddressToken = nullptr; - if (strstr(rval, meshLocalPrefix.c_str()) != rval) + if (strstr(rval, "> ") != nullptr) { + rval += 2; + } + + if (strstr(rval, linkLocalAddressToken) == rval) + { + networkInfo["IPv6:LinkLocalAddress"] = rval; continue; } @@ -317,17 +341,34 @@ std::string WpanService::HandleStatusRequest() if (meshLocalAddressToken == nullptr) { - break; + if ((meshLocalPrefix.size() > 0) && (strstr(rval, meshLocalPrefix.c_str()) == rval)) + { + networkInfo["IPv6:MeshLocalAddress"] = rval; + continue; + } + + if (strstr(rval, localAddressToken) != rval) + { + networkInfo["IPv6:GlobalAddress"] = rval; + } + else + { + networkInfo["IPv6:LocalAddress"] = rval; + } } - - // In case this address is not ends with 0:ff:fe00:xxxx - if (strchr(meshLocalAddressToken + sizeof(kMeshLocalAddressTokenLocator) - 1, ':') != nullptr) + else { - break; + *meshLocalAddressToken = '\0'; + meshLocalPrefix = rval; + networkInfo["IPv6:MeshLocalPrefix"] = rval; + std::string la = networkInfo.get("IPv6:LocalAddress", "unknown").asString(); + if (strstr(rval, la.c_str()) != nullptr) + { + networkInfo["IPv6:MeshLocalAddress"] = networkInfo.get("IPv6:LocalAddress", "notfound").asString(); + networkInfo.removeMember("IPv6:LocalAddress"); + } } } - - networkInfo["IPv6:MeshLocalAddress"] = rval; } exit: @@ -336,7 +377,7 @@ std::string WpanService::HandleStatusRequest() if (ret != kWpanStatus_Ok) { root["result"] = WPAN_RESPONSE_FAILURE; - otbrLog(OTBR_LOG_ERR, "wpan service error: %d", ret); + otbrLogErr("Wpan service error: %d", ret); } root["error"] = ret; response = jsonWriter.write(root); @@ -349,7 +390,7 @@ std::string WpanService::HandleAvailableNetworkRequest() Json::FastWriter jsonWriter; std::string response; int ret = kWpanStatus_Ok; - otbr::Web::OpenThreadClient client; + otbr::Web::OpenThreadClient client(mIfName); VerifyOrExit(client.Connect(), ret = kWpanStatus_ScanFailed); VerifyOrExit((mNetworksCount = client.Scan(mNetworks, sizeof(mNetworks) / sizeof(mNetworks[0]))) > 0, @@ -375,7 +416,7 @@ std::string WpanService::HandleAvailableNetworkRequest() if (ret != kWpanStatus_Ok) { root["result"] = WPAN_RESPONSE_FAILURE; - otbrLog(OTBR_LOG_ERR, "Error is %d", ret); + otbrLogErr("Error is %d", ret); } root["error"] = ret; response = jsonWriter.write(root); @@ -385,7 +426,7 @@ std::string WpanService::HandleAvailableNetworkRequest() int WpanService::GetWpanServiceStatus(std::string &aNetworkName, std::string &aExtPanId) const { int status = kWpanStatus_Ok; - otbr::Web::OpenThreadClient client; + otbr::Web::OpenThreadClient client(mIfName); const char * rval; VerifyOrExit(client.Connect(), status = kWpanStatus_Uninitialized); @@ -417,33 +458,106 @@ int WpanService::GetWpanServiceStatus(std::string &aNetworkName, std::string &aE std::string WpanService::HandleCommission(const std::string &aCommissionRequest) { - Json::Value root; - Json::Reader reader; - int ret = kWpanStatus_Ok; - std::string pskd; - std::string response; + Json::Value root; + Json::Reader reader; + Json::FastWriter jsonWriter; + int ret = kWpanStatus_Ok; + std::string pskd; + std::string response; + const char * rval; VerifyOrExit(reader.parse(aCommissionRequest.c_str(), root) == true, ret = kWpanStatus_ParseRequestFailed); pskd = root["pskd"].asString(); + { - otbr::Web::OpenThreadClient client; - const char * rval; + otbr::Web::OpenThreadClient client(mIfName); VerifyOrExit(client.Connect(), ret = kWpanStatus_Uninitialized); - rval = client.Execute("commissioner start"); - VerifyOrExit(rval != nullptr, ret = kWpanStatus_Down); - rval = client.Execute("commissioner joiner add * %s", pskd.c_str()); - VerifyOrExit(rval != nullptr, ret = kWpanStatus_Down); - root["error"] = ret; + + for (int i = 0; i < 5; i++) + { + VerifyOrExit((rval = client.Execute("commissioner state")) != nullptr, ret = kWpanStatus_Down); + + if (strcmp(rval, "disabled") == 0) + { + VerifyOrExit((rval = client.Execute("commissioner start")) != nullptr, ret = kWpanStatus_Down); + } + else if (strcmp(rval, "active") == 0) + { + VerifyOrExit(client.Execute("commissioner joiner add * %s", pskd.c_str()) != nullptr, + ret = kWpanStatus_Down); + root["error"] = ret; + ExitNow(); + } + + sleep(1); + } + + client.Execute("commissioner stop"); } + + ret = kWpanStatus_SetFailed; + exit: + + root.clear(); + root["result"] = WPAN_RESPONSE_SUCCESS; + root["error"] = ret; + if (ret != kWpanStatus_Ok) { root["result"] = WPAN_RESPONSE_FAILURE; - otbrLog(OTBR_LOG_ERR, "error: %d", ret); + otbrLogErr("error: %d", ret); } + response = jsonWriter.write(root); + return response; } +int WpanService::commitActiveDataset(otbr::Web::OpenThreadClient &aClient, + const std::string & aMasterKey, + const std::string & aNetworkName, + uint16_t aChannel, + uint64_t aExtPanId, + uint16_t aPanId) +{ + int ret = kWpanStatus_Ok; + + VerifyOrExit(aClient.Execute("dataset init new") != nullptr, ret = kWpanStatus_SetFailed); + VerifyOrExit(aClient.Execute("dataset masterkey %s", aMasterKey.c_str()) != nullptr, ret = kWpanStatus_SetFailed); + VerifyOrExit(aClient.Execute("dataset networkname %s", escapeOtCliEscapable(aNetworkName).c_str()) != nullptr, + ret = kWpanStatus_SetFailed); + VerifyOrExit(aClient.Execute("dataset channel %u", aChannel) != nullptr, ret = kWpanStatus_SetFailed); + VerifyOrExit(aClient.Execute("dataset extpanid %016" PRIx64, aExtPanId) != nullptr, ret = kWpanStatus_SetFailed); + VerifyOrExit(aClient.Execute("dataset panid %u", aPanId) != nullptr, ret = kWpanStatus_SetFailed); + VerifyOrExit(aClient.Execute("dataset commit active") != nullptr, ret = kWpanStatus_SetFailed); + +exit: + return ret; +} + +std::string WpanService::escapeOtCliEscapable(const std::string &aArg) +{ + std::stringbuf strbuf; + + for (char c : aArg) + { + switch (c) + { + case ' ': + case '\t': + case '\r': + case '\n': + case '\\': + strbuf.sputc('\\'); + // Fallthrough + default: + strbuf.sputc(c); + } + } + + return strbuf.str(); +} + } // namespace Web } // namespace otbr diff --git a/src/web/web-service/wpan_service.hpp b/src/web/web-service/wpan_service.hpp index fdc65bbd19c..e4ed5695187 100644 --- a/src/web/web-service/wpan_service.hpp +++ b/src/web/web-service/wpan_service.hpp @@ -174,6 +174,14 @@ class WpanService std::string CommissionDevice(const char *aPskd, const char *aNetworkPassword); private: + int commitActiveDataset(otbr::Web::OpenThreadClient &aClient, + const std::string & aMasterKey, + const std::string & aNetworkName, + uint16_t aChannel, + uint64_t aExtPanId, + uint16_t aPanId); + static std::string escapeOtCliEscapable(const std::string &aArg); + WpanNetworkInfo mNetworks[OT_SCANNED_NET_BUFFER_SIZE]; int mNetworksCount; char mIfName[IFNAMSIZ]; @@ -203,9 +211,6 @@ class WpanService kPropertyType_String = 0, kPropertyType_Data, }; - - static const char *kBorderAgentHost; - static const char *kBorderAgentPort; }; } // namespace Web diff --git a/tests/dbus/test-client b/tests/dbus/test-client index 2026c049572..399d1522210 100755 --- a/tests/dbus/test-client +++ b/tests/dbus/test-client @@ -58,6 +58,7 @@ main() trap on_exit EXIT sleep 5 sudo "${CMAKE_BINARY_DIR}"/third_party/openthread/repo/src/posix/ot-ctl factoryreset + sleep 1 sudo dbus-send --system --dest=io.openthread.BorderRouter.wpan0 \ --type=method_call --print-reply /io/openthread/BorderRouter/wpan0 \ org.freedesktop.DBus.Introspectable.Introspect | grep JoinerStart @@ -87,14 +88,18 @@ send "commissioner start\r\n" expect "Commissioner: active" send "commissioner joiner add * ABCDEF\r\n" expect "Done" -wait +expect "Joiner end" +send "commissioner stop\r\n" +set timeout -1 +expect eof EOF - sleep 5 # The ot-cli-mtd node is used to test the child and neighbor table. expect < api; uint64_t extpanid = 0xdead00beaf00cafe; + std::string region; dbus_error_init(&error); connection = UniqueDBusConnection(dbus_bus_get(DBUS_BUS_SYSTEM, &error)); @@ -112,6 +113,10 @@ int main() api->AddDeviceRoleHandler( [](DeviceRole aRole) { printf("Device role changed to %d\n", static_cast(aRole)); }); + TEST_ASSERT(api->SetRadioRegion("US") == ClientError::ERROR_NONE); + TEST_ASSERT(api->GetRadioRegion(region) == ClientError::ERROR_NONE); + TEST_ASSERT(region == "US"); + api->Scan([&api, extpanid](const std::vector &aResult) { LinkModeConfig cfg = {true, false, true}; std::vector masterKey = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, @@ -134,7 +139,9 @@ int main() [&api, channel, extpanid](ClientError aError) { printf("Attach result %d\n", static_cast(aError)); sleep(10); - uint64_t extpanidCheck; + uint64_t extpanidCheck; + std::vector activeDataset; + if (aError == OTBR_ERROR_NONE) { std::string name; @@ -166,6 +173,7 @@ int main() TEST_ASSERT(api->GetPartitionId(partitionId) == OTBR_ERROR_NONE); TEST_ASSERT(api->GetInstantRssi(rssi) == OTBR_ERROR_NONE); TEST_ASSERT(api->GetRadioTxPower(txPower) == OTBR_ERROR_NONE); + TEST_ASSERT(api->GetActiveDatasetTlvs(activeDataset) == OTBR_ERROR_NONE); api->FactoryReset(nullptr); TEST_ASSERT(api->GetNetworkName(name) == OTBR_ERROR_NONE); TEST_ASSERT(rloc16 != 0xffff); @@ -178,10 +186,13 @@ int main() { exit(-1); } - api->Attach("Test", 0x5678, extpanid, {}, {}, UINT32_MAX, [&api](ClientError aErr) { + TEST_ASSERT(api->SetActiveDatasetTlvs(activeDataset) == OTBR_ERROR_NONE); + api->Attach([&api, channel, extpanid](ClientError aErr) { uint8_t routerId; otbr::DBus::LeaderData leaderData; uint8_t leaderWeight; + uint16_t channelResult; + uint64_t extpanidCheck; Ip6Prefix prefix; OnMeshPrefix onMeshPrefix = {}; @@ -192,6 +203,12 @@ int main() onMeshPrefix.mPreference = 0; onMeshPrefix.mStable = true; + TEST_ASSERT(aErr == ClientError::ERROR_NONE); + TEST_ASSERT(api->GetChannel(channelResult) == OTBR_ERROR_NONE); + TEST_ASSERT(channelResult == channel); + TEST_ASSERT(api->GetExtPanId(extpanidCheck) == OTBR_ERROR_NONE); + TEST_ASSERT(extpanidCheck == extpanid); + TEST_ASSERT(api->GetLocalLeaderWeight(leaderWeight) == OTBR_ERROR_NONE); TEST_ASSERT(api->GetLeaderData(leaderData) == OTBR_ERROR_NONE); TEST_ASSERT(api->GetRouterId(routerId) == OTBR_ERROR_NONE); @@ -201,7 +218,13 @@ int main() TEST_ASSERT(api->AddOnMeshPrefix(onMeshPrefix) == OTBR_ERROR_NONE); TEST_ASSERT(api->RemoveOnMeshPrefix(onMeshPrefix.mPrefix) == OTBR_ERROR_NONE); - exit(static_cast(aErr)); + api->FactoryReset(nullptr); + TEST_ASSERT(api->JoinerStart("ABCDEF", "", "", "", "", "", nullptr) == + ClientError::OT_ERROR_NOT_FOUND); + TEST_ASSERT(api->JoinerStart("ABCDEF", "", "", "", "", "", [](ClientError aJoinError) { + TEST_ASSERT(aJoinError == ClientError::OT_ERROR_NOT_FOUND); + exit(0); + }) == ClientError::ERROR_NONE); }); }); }); diff --git a/tests/mdns/CMakeLists.txt b/tests/mdns/CMakeLists.txt index 108014a0995..48c753741a2 100644 --- a/tests/mdns/CMakeLists.txt +++ b/tests/mdns/CMakeLists.txt @@ -55,6 +55,17 @@ add_test( COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test-stop ) -set_tests_properties(mdns-single mdns-multiple mdns-update mdns-stop PROPERTIES - ENVIRONMENT "OTBR_MDNS=${OTBR_MDNS};OTBR_TEST_MDNS=$" +add_test( + NAME mdns-single-custom-host + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test-single-custom-host +) + +add_test( + NAME mdns-multiple-custom-hosts + COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/test-multiple-custom-hosts +) + +set_tests_properties(mdns-single mdns-multiple mdns-update mdns-stop mdns-single-custom-host mdns-multiple-custom-hosts + PROPERTIES + ENVIRONMENT "OTBR_MDNS=${OTBR_MDNS};OTBR_TEST_MDNS=$" ) diff --git a/tests/mdns/main.cpp b/tests/mdns/main.cpp index 4239c73e121..757d4cdd88c 100644 --- a/tests/mdns/main.cpp +++ b/tests/mdns/main.cpp @@ -54,19 +54,17 @@ int Mainloop(Mdns::Publisher &aPublisher) while (true) { - fd_set readFdSet; - fd_set writeFdSet; - fd_set errorFdSet; - int maxFd = -1; - struct timeval timeout = {INT_MAX, INT_MAX}; + MainloopContext mainloop; - FD_ZERO(&readFdSet); - FD_ZERO(&writeFdSet); - FD_ZERO(&errorFdSet); + mainloop.mMaxFd = -1; + mainloop.mTimeout = {INT_MAX, INT_MAX}; + FD_ZERO(&mainloop.mReadFdSet); + FD_ZERO(&mainloop.mWriteFdSet); + FD_ZERO(&mainloop.mErrorFdSet); - aPublisher.UpdateFdSet(readFdSet, writeFdSet, errorFdSet, maxFd, timeout); - rval = - select(maxFd + 1, &readFdSet, &writeFdSet, &errorFdSet, (timeout.tv_sec == INT_MAX ? nullptr : &timeout)); + aPublisher.Update(mainloop); + rval = select(mainloop.mMaxFd + 1, &mainloop.mReadFdSet, &mainloop.mWriteFdSet, &mainloop.mErrorFdSet, + (mainloop.mTimeout.tv_sec == INT_MAX ? nullptr : &mainloop.mTimeout)); if (rval < 0) { @@ -74,77 +72,188 @@ int Mainloop(Mdns::Publisher &aPublisher) break; } - aPublisher.Process(readFdSet, writeFdSet, errorFdSet); + aPublisher.Process(mainloop); } return rval; } -void PublishSingleService(void *aContext, Mdns::State aState) +void PublishSingleServiceWithCustomHost(void *aContext, Mdns::Publisher::State aState) { - uint8_t xpanid[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48}; + uint8_t xpanid[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48}; + uint8_t extAddr[kSizeExtAddr] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48}; + uint8_t hostAddr[16] = {0}; + const char hostName[] = "custom-host"; - assert(aContext == &sContext); - if (aState == Mdns::kStateReady) + hostAddr[0] = 0x20; + hostAddr[1] = 0x02; + hostAddr[15] = 0x01; + + VerifyOrDie(aContext == &sContext, "unexpected context"); + if (aState == Mdns::Publisher::State::kReady) { - otbrError err = sContext.mPublisher->PublishService(12345, "SingleService", "_meshcop._udp.", "nn", "cool", - sizeof("cool") - 1, "xp", reinterpret_cast(&xpanid), - sizeof(xpanid), nullptr); + otbrError error; + Mdns::Publisher::TxtList txtList{ + {"nn", "cool"}, {"xp", xpanid, sizeof(xpanid)}, {"tv", "1.1.1"}, {"dd", extAddr, sizeof(extAddr)}}; - assert(err == OTBR_ERROR_NONE); + error = sContext.mPublisher->PublishHost(hostName, hostAddr, sizeof(hostAddr)); + SuccessOrDie(error, "cannot publish the host"); + + error = sContext.mPublisher->PublishService(hostName, 12345, "SingleService", "_meshcop._udp.", txtList); + SuccessOrDie(error, "cannot publish the service"); } } -void PublishMultipleServices(void *aContext, Mdns::State aState) +void PublishMultipleServicesWithCustomHost(void *aContext, Mdns::Publisher::State aState) +{ + uint8_t xpanid[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48}; + uint8_t extAddr[kSizeExtAddr] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48}; + uint8_t hostAddr[16] = {0}; + const char hostName1[] = "custom-host-1"; + const char hostName2[] = "custom-host-2"; + + hostAddr[0] = 0x20; + hostAddr[1] = 0x02; + hostAddr[15] = 0x01; + + VerifyOrDie(aContext == &sContext, "unexpected context"); + if (aState == Mdns::Publisher::State::kReady) + { + otbrError error; + Mdns::Publisher::TxtList txtList{ + {"nn", "cool"}, {"xp", xpanid, sizeof(xpanid)}, {"tv", "1.1.1"}, {"dd", extAddr, sizeof(extAddr)}}; + + error = sContext.mPublisher->PublishHost(hostName1, hostAddr, sizeof(hostAddr)); + SuccessOrDie(error, "cannot publish the first host"); + + error = sContext.mPublisher->PublishService(hostName1, 12345, "MultipleService11", "_meshcop._udp.", txtList); + SuccessOrDie(error, "cannot publish the first service"); + + error = sContext.mPublisher->PublishService(hostName1, 12345, "MultipleService12", "_meshcop._udp.", txtList); + SuccessOrDie(error, "cannot publish the second service"); + + error = sContext.mPublisher->PublishHost(hostName2, hostAddr, sizeof(hostAddr)); + SuccessOrDie(error, "cannot publish the second host"); + + error = sContext.mPublisher->PublishService(hostName2, 12345, "MultipleService21", "_meshcop._udp.", txtList); + SuccessOrDie(error, "cannot publish the first service"); + + error = sContext.mPublisher->PublishService(hostName2, 12345, "MultipleService22", "_meshcop._udp.", txtList); + SuccessOrDie(error, "cannot publish the second service"); + } +} + +void PublishSingleService(void *aContext, Mdns::Publisher::State aState) +{ + uint8_t xpanid[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48}; + uint8_t extAddr[kSizeExtAddr] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48}; + Mdns::Publisher::TxtList txtList{ + {"nn", "cool"}, {"xp", xpanid, sizeof(xpanid)}, {"tv", "1.1.1"}, {"dd", extAddr, sizeof(extAddr)}}; + + assert(aContext == &sContext); + if (aState == Mdns::Publisher::State::kReady) + { + otbrError error = + sContext.mPublisher->PublishService(nullptr, 12345, "SingleService", "_meshcop._udp.", txtList); + assert(error == OTBR_ERROR_NONE); + } +} + +void PublishMultipleServices(void *aContext, Mdns::Publisher::State aState) { uint8_t xpanid[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48}; + uint8_t extAddr[kSizeExtAddr] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48}; assert(aContext == &sContext); - if (aState == Mdns::kStateReady) + if (aState == Mdns::Publisher::State::kReady) + { + otbrError error; + Mdns::Publisher::TxtList txtList{ + {"nn", "cool1"}, {"xp", xpanid, sizeof(xpanid)}, {"tv", "1.1.1"}, {"dd", extAddr, sizeof(extAddr)}}; + + error = sContext.mPublisher->PublishService(nullptr, 12345, "MultipleService1", "_meshcop._udp.", txtList); + assert(error == OTBR_ERROR_NONE); + } + + if (aState == Mdns::Publisher::State::kReady) { - otbrError err = sContext.mPublisher->PublishService(12345, "MultipleService1", "_meshcop._udp.", "nn", "cool1", - sizeof("cool1") - 1, "xp", - reinterpret_cast(&xpanid), sizeof(xpanid), nullptr); - - assert(err == OTBR_ERROR_NONE); - err = sContext.mPublisher->PublishService(12345, "MultipleService2", "_meshcop._udp.", "nn", "cool2", - sizeof("cool1") - 1, "xp", reinterpret_cast(&xpanid), - sizeof(xpanid), nullptr); - assert(err == OTBR_ERROR_NONE); + otbrError error; + Mdns::Publisher::TxtList txtList{ + {"nn", "cool2"}, {"xp", xpanid, sizeof(xpanid)}, {"tv", "1.1.1"}, {"dd", extAddr, sizeof(extAddr)}}; + + error = sContext.mPublisher->PublishService(nullptr, 12345, "MultipleService2", "_meshcop._udp.", txtList); + assert(error == OTBR_ERROR_NONE); } } -void PublishUpdateServices(void *aContext, Mdns::State aState) +void PublishUpdateServices(void *aContext, Mdns::Publisher::State aState) { uint8_t xpanidOld[kSizeExtPanId] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48}; uint8_t xpanidNew[kSizeExtPanId] = {0x48, 0x47, 0x46, 0x45, 0x44, 0x43, 0x42, 0x41}; + uint8_t extAddr[kSizeExtAddr] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48}; assert(aContext == &sContext); - if (aState == Mdns::kStateReady) + if (aState == Mdns::Publisher::State::kReady) { - otbrError err; + otbrError error; if (!sContext.mUpdate) { - err = sContext.mPublisher->PublishService(12345, "UpdateService", "_meshcop._udp.", "nn", "cool", - sizeof("cool") - 1, "xp", reinterpret_cast(&xpanidOld), - sizeof(xpanidOld), nullptr); + Mdns::Publisher::TxtList txtList{{"nn", "cool"}, + {"xp", xpanidOld, sizeof(xpanidOld)}, + {"tv", "1.1.1"}, + {"dd", extAddr, sizeof(extAddr)}}; + + error = sContext.mPublisher->PublishService(nullptr, 12345, "UpdateService", "_meshcop._udp.", txtList); } else { - err = sContext.mPublisher->PublishService(12345, "UpdateService", "_meshcop._udp.", "nn", "coolcool", - sizeof("coolcool") - 1, "xp", - reinterpret_cast(&xpanidNew), sizeof(xpanidNew), nullptr); + Mdns::Publisher::TxtList txtList{{"nn", "coolcool"}, + {"xp", xpanidNew, sizeof(xpanidNew)}, + {"tv", "1.1.1"}, + {"dd", extAddr, sizeof(extAddr)}}; + + error = sContext.mPublisher->PublishService(nullptr, 12345, "UpdateService", "_meshcop._udp.", txtList); } - assert(err == OTBR_ERROR_NONE); + assert(error == OTBR_ERROR_NONE); } } +otbrError TestSingleServiceWithCustomHost(void) +{ + otbrError error = OTBR_ERROR_NONE; + + Mdns::Publisher *pub = + Mdns::Publisher::Create(AF_UNSPEC, /* aDomain */ nullptr, PublishSingleServiceWithCustomHost, &sContext); + sContext.mPublisher = pub; + SuccessOrExit(error = pub->Start()); + Mainloop(*pub); + +exit: + Mdns::Publisher::Destroy(pub); + return error; +} + +otbrError TestMultipleServicesWithCustomHost(void) +{ + otbrError error = OTBR_ERROR_NONE; + + Mdns::Publisher *pub = + Mdns::Publisher::Create(AF_UNSPEC, /* aDomain */ nullptr, PublishMultipleServicesWithCustomHost, &sContext); + sContext.mPublisher = pub; + SuccessOrExit(error = pub->Start()); + Mainloop(*pub); + +exit: + Mdns::Publisher::Destroy(pub); + return error; +} + otbrError TestSingleService(void) { otbrError ret = OTBR_ERROR_NONE; - Mdns::Publisher *pub = Mdns::Publisher::Create(AF_UNSPEC, nullptr, nullptr, PublishSingleService, &sContext); + Mdns::Publisher *pub = Mdns::Publisher::Create(AF_UNSPEC, /* aDomain */ nullptr, PublishSingleService, &sContext); sContext.mPublisher = pub; SuccessOrExit(ret = pub->Start()); Mainloop(*pub); @@ -158,8 +267,9 @@ otbrError TestMultipleServices(void) { otbrError ret = OTBR_ERROR_NONE; - Mdns::Publisher *pub = Mdns::Publisher::Create(AF_UNSPEC, nullptr, nullptr, PublishMultipleServices, &sContext); - sContext.mPublisher = pub; + Mdns::Publisher *pub = + Mdns::Publisher::Create(AF_UNSPEC, /* aDomain */ nullptr, PublishMultipleServices, &sContext); + sContext.mPublisher = pub; SuccessOrExit(ret = pub->Start()); Mainloop(*pub); @@ -172,12 +282,12 @@ otbrError TestUpdateService(void) { otbrError ret = OTBR_ERROR_NONE; - Mdns::Publisher *pub = Mdns::Publisher::Create(AF_UNSPEC, nullptr, nullptr, PublishUpdateServices, &sContext); + Mdns::Publisher *pub = Mdns::Publisher::Create(AF_UNSPEC, /* aDomain */ nullptr, PublishUpdateServices, &sContext); sContext.mPublisher = pub; sContext.mUpdate = false; SuccessOrExit(ret = pub->Start()); sContext.mUpdate = true; - PublishUpdateServices(&sContext, Mdns::kStateReady); + PublishUpdateServices(&sContext, Mdns::Publisher::State::kReady); Mainloop(*pub); exit: @@ -201,7 +311,7 @@ otbrError TestStopService(void) { otbrError ret = OTBR_ERROR_NONE; - Mdns::Publisher *pub = Mdns::Publisher::Create(AF_UNSPEC, nullptr, nullptr, PublishSingleService, &sContext); + Mdns::Publisher *pub = Mdns::Publisher::Create(AF_UNSPEC, /* aDomain */ nullptr, PublishSingleService, &sContext); sContext.mPublisher = pub; SuccessOrExit(ret = pub->Start()); signal(SIGUSR1, RecoverSignal); @@ -232,11 +342,11 @@ int main(int argc, char *argv[]) switch (argv[1][0]) { case 's': - ret = TestSingleService(); + ret = argv[1][1] == 'c' ? TestSingleServiceWithCustomHost() : TestSingleService(); break; case 'm': - ret = TestMultipleServices(); + ret = argv[1][1] == 'c' ? TestMultipleServicesWithCustomHost() : TestMultipleServices(); break; case 'u': diff --git a/tests/mdns/test-multiple b/tests/mdns/test-multiple index a7f1676f225..f023ed24212 100755 --- a/tests/mdns/test-multiple +++ b/tests/mdns/test-multiple @@ -40,11 +40,11 @@ main() if [[ ${OTBR_MDNS} == 'mDNSResponder' ]]; then # dns-sd will not exit - dns_sd_check MultipleService1 _meshcop._udp 'nn=cool1 xp=ABCDEFGH' - dns_sd_check MultipleService2 _meshcop._udp 'nn=cool2 xp=ABCDEFGH' + dns_sd_check MultipleService1 _meshcop._udp 'nn=cool1 xp=ABCDEFGH tv=1.1.1 dd=ABCDEFGH' + dns_sd_check MultipleService2 _meshcop._udp 'nn=cool2 xp=ABCDEFGH tv=1.1.1 dd=ABCDEFGH' else - avahi_check 'MultipleService1;_meshcop._udp;.\+"xp=ABCDEFGH.\+"nn=cool1"' - avahi_check 'MultipleService2;_meshcop._udp;.\+"xp=ABCDEFGH.\+"nn=cool2"' + avahi_check 'MultipleService1;_meshcop._udp;.\+"dd=ABCDEFGH.\+"tv=1\.1\.1.\+"xp=ABCDEFGH.\+"nn=cool1"' + avahi_check 'MultipleService2;_meshcop._udp;.\+"dd=ABCDEFGH.\+"tv=1\.1\.1.\+"xp=ABCDEFGH.\+"nn=cool2"' fi } diff --git a/tests/mdns/test-multiple-custom-hosts b/tests/mdns/test-multiple-custom-hosts new file mode 100755 index 00000000000..449cacd62e0 --- /dev/null +++ b/tests/mdns/test-multiple-custom-hosts @@ -0,0 +1,57 @@ +#!/bin/bash +# +# Copyright (c) 2020, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +# +# This script tests publishing single service with custom host name & address. +# + +# shellcheck source=tests/mdns/test_init +. "$(dirname "$0")/test_init" + +main() +{ + start_publisher mc + + if [[ ${OTBR_MDNS} == 'mDNSResponder' ]]; then + dns_sd_check MultipleService11 _meshcop._udp 'custom-host-1.local.' + dns_sd_check MultipleService12 _meshcop._udp 'custom-host-1.local.' + dns_sd_check_host 'custom-host-1.local.' '2002:0000:0000:0000:0000:0000:0000:0001' + + dns_sd_check MultipleService21 _meshcop._udp 'custom-host-2.local.' + dns_sd_check MultipleService22 _meshcop._udp 'custom-host-2.local.' + dns_sd_check_host 'custom-host-2.local.' '2002:0000:0000:0000:0000:0000:0000:0001' + else + avahi_check 'MultipleService11;_meshcop._udp;local;custom-host-1.local;2002::1;12345;.*"xp=ABCDEFGH.\+"nn=cool"' + avahi_check 'MultipleService12;_meshcop._udp;local;custom-host-1.local;2002::1;12345;.*"xp=ABCDEFGH.\+"nn=cool"' + avahi_check 'MultipleService21;_meshcop._udp;local;custom-host-2.local;2002::1;12345;.*"xp=ABCDEFGH.\+"nn=cool"' + avahi_check 'MultipleService22;_meshcop._udp;local;custom-host-2.local;2002::1;12345;.*"xp=ABCDEFGH.\+"nn=cool"' + fi +} + +main "$@" diff --git a/tests/mdns/test-single b/tests/mdns/test-single index 0212c8cd4fc..4e38ee20d62 100755 --- a/tests/mdns/test-single +++ b/tests/mdns/test-single @@ -39,9 +39,9 @@ main() start_publisher s if [[ ${OTBR_MDNS} == 'mDNSResponder' ]]; then - dns_sd_check SingleService _meshcop._udp 'nn=cool xp=ABCDEFGH' + dns_sd_check SingleService _meshcop._udp 'nn=cool xp=ABCDEFGH tv=1.1.1 dd=ABCDEFGH' else - avahi_check 'SingleService;_meshcop._udp;.\+"xp=ABCDEFGH.\+"nn=cool"' + avahi_check 'SingleService;_meshcop._udp;.\+"dd=ABCDEFGH.\+"tv=1\.1\.1.\+"xp=ABCDEFGH.\+"nn=cool"' fi } diff --git a/tests/mdns/test-single-custom-host b/tests/mdns/test-single-custom-host new file mode 100755 index 00000000000..8a6986133c5 --- /dev/null +++ b/tests/mdns/test-single-custom-host @@ -0,0 +1,49 @@ +#!/bin/bash +# +# Copyright (c) 2020, The OpenThread Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. Neither the name of the copyright holder nor the +# names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +# +# This script tests publishing single service with custom host name & address. +# + +# shellcheck source=tests/mdns/test_init +. "$(dirname "$0")/test_init" + +main() +{ + start_publisher sc + + if [[ ${OTBR_MDNS} == 'mDNSResponder' ]]; then + dns_sd_check SingleService _meshcop._udp 'custom-host.local.' + dns_sd_check_host 'custom-host.local.' '2002:0000:0000:0000:0000:0000:0000:0001' + else + avahi_check 'SingleService;_meshcop._udp;local;custom-host.local;2002::1;12345;.*"xp=ABCDEFGH.\+"nn=cool"' + fi +} + +main "$@" diff --git a/tests/mdns/test-stop b/tests/mdns/test-stop index 3dc5a6f081d..4c808145596 100755 --- a/tests/mdns/test-stop +++ b/tests/mdns/test-stop @@ -39,28 +39,28 @@ main() start_publisher k if [[ ${OTBR_MDNS} == 'mDNSResponder' ]]; then - dns_sd_check SingleService _meshcop._udp 'nn=cool xp=ABCDEFGH' + dns_sd_check SingleService _meshcop._udp 'nn=cool xp=ABCDEFGH tv=1.1.1 dd=ABCDEFGH' else - avahi_check 'SingleService;_meshcop._udp;.\+"xp=ABCDEFGH.\+"nn=cool"' + avahi_check 'SingleService;_meshcop._udp;.\+"dd=ABCDEFGH.\+"tv=1\.1\.1.\+"xp=ABCDEFGH.\+"nn=cool"' fi # stop service /bin/kill -USR1 $PID if [[ ${OTBR_MDNS} == 'mDNSResponder' ]]; then sleep 200 - (! dns_sd_check SingleService _meshcop._udp 'nn=cool xp=ABCDEFGH') || exit 1 + (! dns_sd_check SingleService _meshcop._udp 'nn=cool xp=ABCDEFGH tv=1.1.1 dd=ABCDEFGH') || exit 1 else sleep 1 - (! avahi_check 'SingleService;_meshcop._udp;.\+"xp=ABCDEFGH.\+"nn=cool"') || exit 1 + (! avahi_check 'SingleService;_meshcop._udp;.\+"dd=ABCDEFGH.\+"tv=1\.1\.1.\+"xp=ABCDEFGH.\+"nn=cool"') || exit 1 fi # start service /bin/kill -USR2 $PID sleep 1 if [[ ${OTBR_MDNS} == 'mDNSResponder' ]]; then - dns_sd_check SingleService _meshcop._udp 'nn=cool xp=ABCDEFGH' + dns_sd_check SingleService _meshcop._udp 'nn=cool xp=ABCDEFGH tv=1.1.1 dd=ABCDEFGH' else - avahi_check 'SingleService;_meshcop._udp;.\+"xp=ABCDEFGH.\+"nn=cool"' + avahi_check 'SingleService;_meshcop._udp;.\+"dd=ABCDEFGH.\+"tv=1\.1\.1.\+"xp=ABCDEFGH.\+"nn=cool"' fi } diff --git a/tests/mdns/test-update b/tests/mdns/test-update index 359414867fe..34407794e14 100755 --- a/tests/mdns/test-update +++ b/tests/mdns/test-update @@ -39,9 +39,9 @@ main() start_publisher u if [[ ${OTBR_MDNS} == 'mDNSResponder' ]]; then - dns_sd_check UpdateService _meshcop._udp 'nn=coolcool xp=HGFEDCBA' + dns_sd_check UpdateService _meshcop._udp 'nn=coolcool xp=HGFEDCBA tv=1.1.1 dd=ABCDEFGH' else - avahi_check 'UpdateService;_meshcop._udp;.\+"xp=HGFEDCBA.\+"nn=coolcool"' + avahi_check 'UpdateService;_meshcop._udp;.\+"dd=ABCDEFGH.\+"tv=1\.1\.1.\+"xp=HGFEDCBA.\+"nn=coolcool"' fi } diff --git a/tests/mdns/test_init b/tests/mdns/test_init index eb050e24324..47edff24f72 100644 --- a/tests/mdns/test_init +++ b/tests/mdns/test_init @@ -97,6 +97,29 @@ dns_sd_check() grep "$3" "${DNS_SD_RESULT}" } +####################################### +# Check if a host is regisered +# +# Arguments: +# $1 hostname +# $2 address +# +# Returns: +# 0 Registered +# otherwise Not registered +####################################### +dns_sd_check_host() +{ + # dns-sd will not exit + dns-sd -G v6 "$1" >"${DNS_SD_RESULT}" 2>&1 & + DNS_SD_PID=$! + sleep 1 + kill "${DNS_SD_PID}" + + cat "${DNS_SD_RESULT}" + grep "$2" "${DNS_SD_RESULT}" +} + ####################################### # Check if a service is regisered # diff --git a/tests/rest/test-rest-server b/tests/rest/test-rest-server index 1c30ca62013..2e268830d30 100755 --- a/tests/rest/test-rest-server +++ b/tests/rest/test-rest-server @@ -37,6 +37,7 @@ on_exit() sudo killall otbr-agent || true sudo killall expect || true + sudo killall ot-ctl || true sudo killall ot-cli-ftd || true sudo killall ot-cli-mtd || true @@ -46,6 +47,15 @@ on_exit() main() { sudo "${CMAKE_BINARY_DIR}"/src/agent/otbr-agent -d 7 -v -I wpan0 "spinel+hdlc+forkpty://$(command -v ot-rcp)?forkpty-arg=1" & + sleep 1 + sudo expect <>buildspec.mk < \ diff --git a/.travis/check-docker b/tests/scripts/check-docker similarity index 88% rename from .travis/check-docker rename to tests/scripts/check-docker index b2140b8faed..ac2b7f5f872 100755 --- a/.travis/check-docker +++ b/tests/scripts/check-docker @@ -45,16 +45,19 @@ on_exit() main() { - docker build -t otbr --build-arg OTBR_OPTIONS=-DOT_POSIX_CONFIG_RCP_BUS="${OT_POSIX_CONFIG_RCP_BUS}" -f etc/docker/Dockerfile . + docker build -t otbr \ + --build-arg OTBR_OPTIONS=-DOT_POSIX_CONFIG_RCP_BUS="${OT_POSIX_CONFIG_RCP_BUS}" \ + --build-arg BACKBONE_ROUTER=0 \ + -f etc/docker/Dockerfile . # SPI simulation is not available yet, so just verify the binary runs if [[ ${OT_POSIX_CONFIG_RCP_BUS} == SPI ]]; then - docker run --rm -it --entrypoint otbr-agent otbr -h | grep 'spi://' + docker run --rm -t --entrypoint otbr-agent otbr -h | grep 'spi://' return 0 fi local -r SOCAT_OUTPUT=/tmp/ot-socat - socat -d -d pty,raw,echo=0 pty,raw,echo=0 >/dev/null 2>$SOCAT_OUTPUT & + socat -d -d pty,raw,echo=0 pty,raw,echo=0 2>&1 | tee $SOCAT_OUTPUT & while true; do if test "$(head -n2 "$SOCAT_OUTPUT" | wc -l)" = 2; then local -r DEVICE_PTY=$(head -n1 $SOCAT_OUTPUT | grep -o '/dev/.\+') @@ -70,9 +73,9 @@ main() # shellcheck disable=SC2094 ot-rcp 1 >"$DEVICE_PTY" <"$DEVICE_PTY" & - readonly OTBR_DOCKER_PID=$(docker run --rm -dit \ + readonly OTBR_DOCKER_PID=$(docker run --rm -d \ --sysctl "net.ipv6.conf.all.disable_ipv6=0 net.ipv4.conf.all.forwarding=1 net.ipv6.conf.all.forwarding=1" \ - --privileged -p 8080:80 --dns=127.0.0.1 --volume "$DOCKER_PTY":/dev/ttyUSB0 otbr) + --privileged -p 8080:80 --dns=127.0.0.1 --volume "$DOCKER_PTY":/dev/ttyUSB0 otbr --backbone-interface eth0) sleep 10 sudo lsof -i :8080 diff --git a/tests/scripts/check-raspbian b/tests/scripts/check-raspbian index c4db478a567..b09868c00ce 100755 --- a/tests/scripts/check-raspbian +++ b/tests/scripts/check-raspbian @@ -49,16 +49,23 @@ set -ex export LC_ALL=C export DEBIAN_FRONTEND=noninteractive -export RELEASE=1 echo "127.0.0.1 \$(hostname)" >> /etc/hosts chown -R pi:pi /home/pi/repo cd /home/pi/repo echo 1 | sudo tee /proc/sys/net/ipv6/conf/all/disable_ipv6 apt-get update -apt-get install -y --no-install-recommends git -su -m -c 'script/bootstrap' pi -su -m -c 'NETWORK_MANAGER=0 script/setup' pi +apt-get install -y --no-install-recommends git python3-pip +su -c 'RELEASE=1 script/bootstrap' pi + +# Pin CMake version to 3.10.3 for issue https://github.com/openthread/ot-br-posix/issues/728. +# For more background, see https://gitlab.kitware.com/cmake/cmake/-/issues/20568. +apt-get purge -y cmake +pip3 install scikit-build +pip3 install cmake==3.10.3 +cmake --version + +su -c 'RELEASE=1 NETWORK_MANAGER=0 script/setup' pi EOF ( diff --git a/tests/scripts/check-scripts b/tests/scripts/check-scripts index 6ac1d957b51..33ad1583a22 100755 --- a/tests/scripts/check-scripts +++ b/tests/scripts/check-scripts @@ -43,7 +43,9 @@ main() { RELEASE=1 ./script/bootstrap ./script/bootstrap - NAT64=1 ./script/setup + INFRA_IF_NAME=eth0 BACKBONE_ROUTER=0 NAT64=1 ./script/setup + # re-run to ensure the script can run successfully multiple times + INFRA_IF_NAME=eth0 BACKBONE_ROUTER=0 NAT64=1 ./script/setup SOCAT_OUTPUT=/tmp/ot-socat socat -d -d pty,raw,echo=0 pty,raw,echo=0 >/dev/null 2>$SOCAT_OUTPUT & @@ -66,11 +68,11 @@ main() # shellcheck disable=SC2094 ot-rcp 1 >"${RCP_PTY}" <"${RCP_PTY}" & - RADIO_URL="spinel+hdlc+uart://${HOST_PTY}" ./script/console & + INFRA_IF_NAME=eth0 RADIO_URL="spinel+hdlc+uart://${HOST_PTY}" ./script/console & SERVICES_PID=$! sudo service tayga start echo 'Waiting for services to be ready...' - sleep 10 + sleep 30 check_otbr_agent netstat -an | grep 80 pidof tayga diff --git a/tests/scripts/meshcop b/tests/scripts/meshcop index f083f237aa6..a24141be057 100755 --- a/tests/scripts/meshcop +++ b/tests/scripts/meshcop @@ -29,10 +29,16 @@ # Test thread commissioning along with openthread. # # Usage: -# ./meshcop # test with latest openthread. -# NO_CLEAN=1 ./meshcop # test with existing binaries in ${TEST_BASE}. +# ./meshcop # test with latest openthread. +# NO_CLEAN=1 ./meshcop # test with existing binaries in ${TEST_BASE}. +# TEST_CASE=mdns_service ./meshcop # test the meshcop mDNS service. set -euxo pipefail +# The test case to run. available cases are: +# - commissioning +# - mdns_service +readonly TEST_CASE="${TEST_CASE:-commissioning}" + # Get our starting directory and remember it readonly ORIGIN_PWD="$(pwd)" readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -63,6 +69,7 @@ readonly OTBR_PSKC_PATH="${ABS_TOP_BUILDDIR}/tools/pskc" readonly OTBR_AGENT_PATH="${ABS_TOP_BUILDDIR}/src/agent/${OTBR_AGENT}" readonly OTBR_DBUS_CONF="${ABS_TOP_BUILDDIR}/src/agent/otbr-agent.conf" readonly OTBR_WEB_PATH="${ABS_TOP_BUILDDIR}/src/web/${OTBR_WEB}" +readonly OT_CTL="${ABS_TOP_BUILDDIR}/third_party/openthread/repo/src/posix/ot-ctl" # The node ids readonly LEADER_NODE_ID=1 @@ -73,6 +80,10 @@ readonly OTBR_WEB_HOST=127.0.0.1 readonly OTBR_WEB_PORT=8773 readonly OTBR_WEB_URL="http://${OTBR_WEB_HOST}:${OTBR_WEB_PORT}" +# External commissioner +readonly OT_COMMISSIONER_PATH=${BUILD_DIR}/ot-commissioner/build/src/app/cli/commissioner-cli +readonly OT_COMMISSIONER_CONFIG=${BUILD_DIR}/ot-commissioner/src/app/etc/commissioner/non-ccm-config.json + # # NOTE Joiner pass phrase: # Must be at least 6 bytes long @@ -194,7 +205,10 @@ build_dependencies() ot_cli=$(command -v "${OT_CLI}") ot_rcp=$(command -v "${OT_RCP}") - if [[ ${OTBR_USE_WEB_COMMISSIONER} != 1 ]]; then + if + [ "${TEST_CASE}" == "commissioning" ] \ + && [[ ${OTBR_USE_WEB_COMMISSIONER} != 1 ]] + then ot_commissioner_build fi @@ -259,6 +273,7 @@ ba_start() write_syslog "AGENT: kill old" sudo killall "${OTBR_AGENT}" || true + sleep 5 write_syslog "AGENT: starting" # we launch this in the background @@ -314,15 +329,12 @@ network_form() # verify mDNS is working as expected. local mdns_result="${TEST_BASE}"/mdns_result.log avahi-browse -aprt | tee "${mdns_result}" - grep -q "${OT_NETWORK_NAME}" "${mdns_result}" + OT_BORDER_AGENT_PORT=$(grep -a "${OT_NETWORK_NAME}" "${mdns_result}" | grep -ao ';[0-9]\{5\};' | head -n1 | tr -d ';') rm "${mdns_result}" } ot_commissioner_build() { - readonly OT_COMMISSIONER_PATH=${BUILD_DIR}/ot-commissioner/build/src/app/cli/commissioner-cli - readonly OT_COMMISSIONER_CONFIG=${BUILD_DIR}/ot-commissioner/src/app/etc/commissioner/non-ccm-config.json - if [[ -x ${OT_COMMISSIONER_PATH} ]]; then return 0 fi @@ -330,7 +342,7 @@ ot_commissioner_build() (mkdir -p "${BUILD_DIR}/ot-commissioner" \ && cd "${BUILD_DIR}/ot-commissioner" \ && (git --git-dir=.git rev-parse --is-inside-work-tree || git --git-dir=.git init .) \ - && git fetch --depth 1 https://github.com/openthread/ot-commissioner.git master \ + && git fetch --depth 1 https://github.com/openthread/ot-commissioner.git main \ && git checkout FETCH_HEAD \ && ./script/bootstrap.sh \ && mkdir build && cd build \ @@ -356,7 +368,7 @@ set timeout 1 expect_after { timeout { exit 1 } } -send "start :: 49191\n" +send "start :: $OT_BORDER_AGENT_PORT\n" expect "done" sleep 5 send "active\n" @@ -399,7 +411,126 @@ EOF exit_message="JOINER SUCCESS COMPLETE" } -main() +scan_meshcop_service() +{ + if command -v dns-sd; then + timeout 2 dns-sd -Z _meshcop._udp local. || true + else + avahi-browse -aprt || true + fi +} + +test_meshcop_service() +{ + local network_name="ot-test-net" + local xpanid="4142434445464748" + local xpanid_txt="ABCDEFGH" + local extaddr="4142434445464748" + local extaddr_txt="ABCDEFGH" + local passphrase="SECRET" + local service + + test_setup + ba_start + sudo "${OT_CTL}" factoryreset + sleep 1 + sudo "${OT_CTL}" dataset init new + sudo "${OT_CTL}" dataset networkname ${network_name} + sudo "${OT_CTL}" dataset extpanid ${xpanid} + sudo "${OT_CTL}" dataset pskc -p ${passphrase} + sudo "${OT_CTL}" dataset commit active + sudo "${OT_CTL}" ifconfig up + sudo "${OT_CTL}" extaddr ${extaddr} + sudo "${OT_CTL}" thread start + sleep 5 + + sudo "${OT_CTL}" state | grep "leader" + + service="$(scan_meshcop_service)" + grep "${network_name}._meshcop\._udp" <<<"${service}" + grep "rv=1" <<<"${service}" + grep "tv=1\.2\.0" <<<"${service}" + grep "nn=${network_name}" <<<"${service}" + grep "xp=${xpanid_txt}" <<<"${service}" + grep "dd=${extaddr_txt}" <<<"${service}" + + # TODO: enable the checks after enabling Thread 1.2 for tests. + #grep "dn=${domain_name}" <<< "${service}" + #grep "sq=" <<< "${service}" + #grep "bb=" <<< "${service}" + + # The binary values are not printable with dns-sd. + grep "sb=" <<<"${service}" + grep "at=" <<<"${service}" + grep "pt=" <<<"${service}" + + # Test if the meshcop service is unpublished when pskc is zeroed. + sudo "${OT_CTL}" dataset init active + sudo "${OT_CTL}" dataset pskc 00000000000000000000000000000000 + sudo "${OT_CTL}" dataset commit active + sleep 2 + service="$(scan_meshcop_service)" + if grep -q "${network_name}._meshcop\._udp" <<<"${service}"; then + die "unexpect meshcop service when PSKc is zeroed!" + fi + + # Test if the meshcop service is published again when a non-zero + # PSKc is set back. + sudo "${OT_CTL}" dataset init active + sudo "${OT_CTL}" dataset pskc 11223344556677889900aabbccddeeff + sudo "${OT_CTL}" dataset commit active + sleep 2 + service="$(scan_meshcop_service)" + grep "${network_name}._meshcop\._udp" <<<"${service}" + + # Test if the meshcop service is unpublished and a new service + # is published when the network name is changed. + local new_network_name="ot-test-net-new" + sudo "${OT_CTL}" dataset init active + sudo "${OT_CTL}" dataset networkname ${new_network_name} + sudo "${OT_CTL}" dataset commit active + sleep 2 + service="$(scan_meshcop_service)" + if grep -q "${network_name}._meshcop\._udp" <<<"${service}"; then + die "unexpect stale meshcop service!" + fi + grep "${new_network_name}._meshcop\._udp" <<<"${service}" + grep "nn=${new_network_name}" <<<"${service}" + + # Test if the discriminator is updated when extaddr is changed. + local new_extaddr="4847464544434241" + local new_extaddr_txt="HGFEDCBA" + sudo "${OT_CTL}" thread stop + sudo "${OT_CTL}" extaddr ${new_extaddr} + sudo "${OT_CTL}" thread start + sleep 5 + service="$(scan_meshcop_service)" + grep "${new_network_name}._meshcop\._udp" <<<"${service}" + grep "dd=${new_extaddr_txt}" <<<"${service}" + + # Test if the meshcop service is unpublished when Thread is stopped. + sudo "${OT_CTL}" thread stop + sleep 2 + service="$(scan_meshcop_service)" + if grep -q "${new_network_name}._meshcop\._udp" <<<"${service}"; then + die "unexpect meshcop service when Thread is stopped!" + fi + + sudo "${OT_CTL}" thread start + sleep 5 + service="$(scan_meshcop_service)" + grep "${new_network_name}._meshcop\._udp" <<<"${service}" + + # Test if the the meshcop service is unpublished when otbr-agent stops. + sudo killall "${OTBR_AGENT}" + sleep 2 + service="$(scan_meshcop_service)" + if grep -q "${new_network_name}._meshcop\._udp" <<<"${service}"; then + die "unexpect meshcop service when otbr-agent exits!" + fi +} + +test_commissioning() { test_setup ba_start @@ -413,4 +544,13 @@ main() joiner_start } +main() +{ + if [ "${TEST_CASE}" == "mdns_service" ]; then + test_meshcop_service + else + test_commissioning + fi +} + main "$@" diff --git a/tests/scripts/openwrt b/tests/scripts/openwrt index aa3517e748a..902a261ba12 100755 --- a/tests/scripts/openwrt +++ b/tests/scripts/openwrt @@ -45,20 +45,20 @@ do_prepare() echo 'src-link openthread /home/build/ot-br-posix/etc/openwrt' >feeds.conf docker start "${CONTAINER_NAME}" - docker exec -it "${CONTAINER_NAME}" scripts/feeds update base - docker exec -it "${CONTAINER_NAME}" scripts/feeds install libjson-c libubox libubus + docker exec "${CONTAINER_NAME}" scripts/feeds update base + docker exec "${CONTAINER_NAME}" scripts/feeds install libjson-c libubox libubus docker cp feeds.conf "${CONTAINER_NAME}":/home/build/openwrt/feeds.conf - docker exec -it "${CONTAINER_NAME}" sudo chown -R build:build /home/build/ot-br-posix /home/build/openwrt/feeds.conf /home/build/openwrt/bin - docker exec -it "${CONTAINER_NAME}" scripts/feeds update openthread - docker exec -it "${CONTAINER_NAME}" make defconfig - docker exec -it "${CONTAINER_NAME}" scripts/feeds install openthread-br + docker exec "${CONTAINER_NAME}" sudo chown -R build:build /home/build/ot-br-posix /home/build/openwrt/feeds.conf /home/build/openwrt/bin + docker exec "${CONTAINER_NAME}" scripts/feeds update openthread + docker exec "${CONTAINER_NAME}" make defconfig + docker exec "${CONTAINER_NAME}" scripts/feeds install openthread-br } do_build() { - docker exec -it "${CONTAINER_NAME}" make V=sc package/openthread-br/compile - docker exec -it "${CONTAINER_NAME}" find . -name "${PACKAGE_NAME}*.ipk" | grep "${PACKAGE_NAME}" + docker exec "${CONTAINER_NAME}" make V=sc package/openthread-br/compile + docker exec "${CONTAINER_NAME}" find . -name "${PACKAGE_NAME}*.ipk" | grep "${PACKAGE_NAME}" } do_clean() diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index d364b2ff97e..938d4dcdad2 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -30,9 +30,10 @@ add_executable(otbr-test-unit $<$:test_dbus_message.cpp> $<$:test_mdns_mdnssd.cpp> main.cpp - test_event_emitter.cpp + test_dns_utils.cpp test_logging.cpp test_pskc.cpp + test_task_runner.cpp ) target_include_directories(otbr-test-unit PRIVATE ${CPPUTEST_INCLUDE_DIRS} @@ -45,6 +46,7 @@ target_link_libraries(otbr-test-unit mbedtls otbr-common otbr-utils + pthread ) add_test( NAME unit diff --git a/tests/unit/test_dns_utils.cpp b/tests/unit/test_dns_utils.cpp new file mode 100644 index 00000000000..55d3389c60b --- /dev/null +++ b/tests/unit/test_dns_utils.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "common/dns_utils.hpp" + +#include + +#include + +TEST_GROUP(DnsUtils){}; + +static void CheckSplitFullDnsName(const std::string &aFullName, + bool aIsServiceInstance, + bool aIsService, + bool aIsHost, + const std::string &aInstanceName, + const std::string &aServiceName, + const std::string &aHostName, + const std::string &aDomain) +{ + DnsNameInfo info; + + assert(aFullName.empty() || aFullName.back() != '.'); + + info = SplitFullDnsName(aFullName); + + CHECK_EQUAL(aIsServiceInstance, info.IsServiceInstance()); + CHECK_EQUAL(aIsService, info.IsService()); + CHECK_EQUAL(aIsHost, info.IsHost()); + CHECK_EQUAL(aInstanceName, info.mInstanceName); + CHECK_EQUAL(aServiceName, info.mServiceName); + CHECK_EQUAL(aHostName, info.mHostName); + CHECK_EQUAL(aDomain, info.mDomain); + + info = SplitFullDnsName(aFullName + "."); + + CHECK_EQUAL(aIsServiceInstance, info.IsServiceInstance()); + CHECK_EQUAL(aIsService, info.IsService()); + CHECK_EQUAL(aIsHost, info.IsHost()); + CHECK_EQUAL(aInstanceName, info.mInstanceName); + CHECK_EQUAL(aServiceName, info.mServiceName); + CHECK_EQUAL(aHostName, info.mHostName); + CHECK_EQUAL(aDomain, info.mDomain); +} + +TEST(DnsUtils, TestSplitFullDnsName) +{ + // Check service instance names + CheckSplitFullDnsName("ins1._ipps._tcp.default.service.arpa", true, false, false, "ins1", "_ipps._tcp", "", + "default.service.arpa."); + CheckSplitFullDnsName("Instance Name._ipps._tcp.default.service.arpa", true, false, false, "Instance Name", + "_ipps._tcp", "", "default.service.arpa."); + CheckSplitFullDnsName("Instance.Name.With.Dots._ipps._tcp.default.service.arpa", true, false, false, + "Instance.Name.With.Dots", "_ipps._tcp", "", "default.service.arpa."); + + // Check service names + CheckSplitFullDnsName("_ipps._tcp.default.service.arpa", false, true, false, "", "_ipps._tcp", "", + "default.service.arpa."); + CheckSplitFullDnsName("_meshcop._udp.default.service.arpa", false, true, false, "", "_meshcop._udp", "", + "default.service.arpa."); + + // Check invalid service names + CheckSplitFullDnsName("_meshcop._abc.default.service.arpa", false, false, true, "", "", "_meshcop", + "_abc.default.service.arpa."); + CheckSplitFullDnsName("_tcp.default.service.arpa", false, false, true, "", "", "_tcp", "default.service.arpa."); + + // Check host names + CheckSplitFullDnsName("abc.example.com", false, false, true, "", "", "abc", "example.com."); + CheckSplitFullDnsName("example.com", false, false, true, "", "", "example", "com."); + CheckSplitFullDnsName("com", false, false, true, "", "", "com", "."); + CheckSplitFullDnsName("", false, false, true, "", "", "", "."); +} diff --git a/tests/unit/test_event_emitter.cpp b/tests/unit/test_event_emitter.cpp deleted file mode 100644 index 3055a1675c2..00000000000 --- a/tests/unit/test_event_emitter.cpp +++ /dev/null @@ -1,179 +0,0 @@ -/* - * Copyright (c) 2017, The OpenThread Authors. - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. Neither the name of the copyright holder nor the - * names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#include "utils/event_emitter.hpp" - -#include -#include - -static int sCounter = 0; -static int sEvent = 0; -static void *sContext = nullptr; - -static void HandleSingleEvent(void *aContext, int aEvent, va_list aArguments) -{ - sCounter++; - - CHECK_EQUAL(sContext, aContext); - CHECK_EQUAL(sEvent, aEvent); - (void)aArguments; -} - -static void HandleTestDifferentContextEvent(void *aContext, int aEvent, va_list aArguments) -{ - void *context1 = va_arg(aArguments, void *); - void *context2 = va_arg(aArguments, void *); - - CHECK_EQUAL(sEvent, aEvent); - - int id = *static_cast(aContext); - if (id == 1) - { - CHECK_EQUAL(context1, aContext); - } - else if (id == 2) - { - CHECK_EQUAL(context2, aContext); - } - else - { - FAIL("Unexpected id value!"); - } - - sCounter++; -} - -static void HandleTestCallSequenceEvent(void *aContext, int aEvent, va_list aArguments) -{ - CHECK_EQUAL(sEvent, aEvent); - - int id = *static_cast(aContext); - - ++sCounter; - - CHECK_EQUAL(sCounter, id); - - (void)aArguments; -} - -TEST_GROUP(EventEmitter){}; - -TEST(EventEmitter, TestSingleHandler) -{ - otbr::EventEmitter ee; - int event = 1; - ee.On(event, HandleSingleEvent, nullptr); - - sContext = nullptr; - sEvent = event; - sCounter = 0; - - ee.Emit(event); - - CHECK_EQUAL(1, sCounter); -} - -TEST(EventEmitter, TestDoubleHandler) -{ - otbr::EventEmitter ee; - int event = 1; - ee.On(event, HandleSingleEvent, nullptr); - ee.On(event, HandleSingleEvent, nullptr); - - sContext = nullptr; - sEvent = event; - sCounter = 0; - - ee.Emit(event); - - CHECK_EQUAL(2, sCounter); -} - -TEST(EventEmitter, TestDifferentContext) -{ - otbr::EventEmitter ee; - int event = 2; - - int context1 = 1; - int context2 = 2; - - ee.On(event, HandleTestDifferentContextEvent, &context1); - ee.On(event, HandleTestDifferentContextEvent, &context2); - - sContext = nullptr; - sEvent = event; - sCounter = 0; - - ee.Emit(event, &context1, &context2); - - CHECK_EQUAL(2, sCounter); -} - -TEST(EventEmitter, TestCallSequence) -{ - otbr::EventEmitter ee; - int event = 3; - - int context1 = 1; - int context2 = 2; - - ee.On(event, HandleTestCallSequenceEvent, &context1); - ee.On(event, HandleTestCallSequenceEvent, &context2); - - sContext = nullptr; - sEvent = event; - sCounter = 0; - - ee.Emit(event); - - CHECK_EQUAL(2, sCounter); -} - -TEST(EventEmitter, TestRemoveHandler) -{ - otbr::EventEmitter ee; - int event = 3; - - ee.On(event, HandleSingleEvent, nullptr); - ee.On(event, HandleSingleEvent, nullptr); - - sContext = nullptr; - sEvent = event; - sCounter = 0; - - ee.Emit(event); - CHECK_EQUAL(2, sCounter); - - ee.Off(event, HandleSingleEvent, nullptr); - ee.Emit(event); - CHECK_EQUAL(3, sCounter); - - ee.Off(event, HandleSingleEvent, nullptr); - ee.Emit(event); - CHECK_EQUAL(3, sCounter); -} diff --git a/tests/unit/test_logging.cpp b/tests/unit/test_logging.cpp index de5fb33392f..dccfc50abec 100644 --- a/tests/unit/test_logging.cpp +++ b/tests/unit/test_logging.cpp @@ -26,6 +26,8 @@ * POSSIBILITY OF SUCH DAMAGE. */ +#define OTBR_LOG_TAG "TEST" + #include #include @@ -42,7 +44,7 @@ TEST(Logging, TestLoggingHigherLevel) sprintf(ident, "otbr-test-%ld", clock()); otbrLogInit(ident, OTBR_LOG_INFO, true); - otbrLog(OTBR_LOG_DEBUG, "cool-higher"); + otbrLog(OTBR_LOG_DEBUG, OTBR_LOG_TAG, "cool-higher"); otbrLogDeinit(); sleep(0); @@ -57,7 +59,7 @@ TEST(Logging, TestLoggingEqualLevel) sprintf(ident, "otbr-test-%ld", clock()); otbrLogInit(ident, OTBR_LOG_INFO, true); - otbrLog(OTBR_LOG_INFO, "cool-equal"); + otbrLog(OTBR_LOG_INFO, OTBR_LOG_TAG, "cool-equal"); otbrLogDeinit(); sleep(0); @@ -74,7 +76,7 @@ TEST(Logging, TestLoggingLowerLevel) sprintf(ident, "otbr-test-%ld", clock()); otbrLogInit(ident, OTBR_LOG_INFO, true); - otbrLog(OTBR_LOG_WARNING, "cool-lower"); + otbrLog(OTBR_LOG_WARNING, OTBR_LOG_TAG, "cool-lower"); otbrLogDeinit(); sleep(0); diff --git a/tests/unit/test_task_runner.cpp b/tests/unit/test_task_runner.cpp new file mode 100644 index 00000000000..a918900ffb5 --- /dev/null +++ b/tests/unit/test_task_runner.cpp @@ -0,0 +1,295 @@ +/* + * Copyright (c) 2021, The OpenThread Authors. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the copyright holder nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "common/task_runner.hpp" + +#include +#include +#include + +#include + +TEST_GROUP(TaskRunner){}; + +TEST(TaskRunner, TestSingleThread) +{ + int rval; + int counter = 0; + otbr::MainloopContext mainloop; + otbr::TaskRunner taskRunner; + + mainloop.mMaxFd = -1; + mainloop.mTimeout = {10, 0}; + + FD_ZERO(&mainloop.mReadFdSet); + FD_ZERO(&mainloop.mWriteFdSet); + FD_ZERO(&mainloop.mErrorFdSet); + + // Increase the `counter` to 3. + taskRunner.Post([&]() { + ++counter; + taskRunner.Post([&]() { + ++counter; + taskRunner.Post([&]() { ++counter; }); + }); + }); + + taskRunner.Update(mainloop); + rval = select(mainloop.mMaxFd + 1, &mainloop.mReadFdSet, &mainloop.mWriteFdSet, &mainloop.mErrorFdSet, + &mainloop.mTimeout); + CHECK_EQUAL(1, rval); + + taskRunner.Process(mainloop); + CHECK_EQUAL(3, counter); +} + +TEST(TaskRunner, TestTasksOrder) +{ + std::string str; + otbr::TaskRunner taskRunner; + int rval; + otbr::MainloopContext mainloop; + + taskRunner.Post([&]() { str.push_back('a'); }); + taskRunner.Post([&]() { str.push_back('b'); }); + taskRunner.Post([&]() { str.push_back('c'); }); + + mainloop.mMaxFd = -1; + mainloop.mTimeout = {2, 0}; + + FD_ZERO(&mainloop.mReadFdSet); + FD_ZERO(&mainloop.mWriteFdSet); + FD_ZERO(&mainloop.mErrorFdSet); + + taskRunner.Update(mainloop); + rval = select(mainloop.mMaxFd + 1, &mainloop.mReadFdSet, &mainloop.mWriteFdSet, &mainloop.mErrorFdSet, + &mainloop.mTimeout); + CHECK_TRUE(rval == 1); + + taskRunner.Process(mainloop); + + // Make sure the tasks are executed in the order of posting. + STRCMP_EQUAL("abc", str.c_str()); +} + +TEST(TaskRunner, TestMultipleThreads) +{ + std::atomic counter{0}; + otbr::TaskRunner taskRunner; + std::vector threads; + + // Increase the `counter` to 10 in separate threads. + for (size_t i = 0; i < 10; ++i) + { + threads.emplace_back([&]() { taskRunner.Post([&]() { ++counter; }); }); + } + + while (counter.load() < 10) + { + int rval; + otbr::MainloopContext mainloop; + + mainloop.mMaxFd = -1; + mainloop.mTimeout = {10, 0}; + + FD_ZERO(&mainloop.mReadFdSet); + FD_ZERO(&mainloop.mWriteFdSet); + FD_ZERO(&mainloop.mErrorFdSet); + + taskRunner.Update(mainloop); + rval = select(mainloop.mMaxFd + 1, &mainloop.mReadFdSet, &mainloop.mWriteFdSet, &mainloop.mErrorFdSet, + &mainloop.mTimeout); + CHECK_EQUAL(1, rval); + + taskRunner.Process(mainloop); + } + + for (auto &th : threads) + { + th.join(); + } + + CHECK_EQUAL(10, counter.load()); +} + +TEST(TaskRunner, TestPostAndWait) +{ + std::atomic total{0}; + std::atomic counter{0}; + otbr::TaskRunner taskRunner; + std::vector threads; + + // Increase the `counter` to 10 in separate threads and accumulate the total value. + for (size_t i = 0; i < 10; ++i) + { + threads.emplace_back([&]() { total += taskRunner.PostAndWait([&]() { return ++counter; }); }); + } + + while (counter.load() < 10) + { + int rval; + otbr::MainloopContext mainloop; + + mainloop.mMaxFd = -1; + mainloop.mTimeout = {10, 0}; + + FD_ZERO(&mainloop.mReadFdSet); + FD_ZERO(&mainloop.mWriteFdSet); + FD_ZERO(&mainloop.mErrorFdSet); + + taskRunner.Update(mainloop); + rval = select(mainloop.mMaxFd + 1, &mainloop.mReadFdSet, &mainloop.mWriteFdSet, &mainloop.mErrorFdSet, + &mainloop.mTimeout); + CHECK_EQUAL(1, rval); + + taskRunner.Process(mainloop); + } + + for (auto &th : threads) + { + th.join(); + } + + CHECK_EQUAL(55, total); + CHECK_EQUAL(10, counter.load()); +} + +TEST(TaskRunner, TestDelayedTasks) +{ + std::atomic counter{0}; + otbr::TaskRunner taskRunner; + std::vector threads; + + // Increase the `counter` to 10 in separate threads. + for (size_t i = 0; i < 10; ++i) + { + threads.emplace_back([&]() { taskRunner.Post(std::chrono::milliseconds(10), [&]() { ++counter; }); }); + } + + while (counter.load() < 10) + { + int rval; + otbr::MainloopContext mainloop; + + mainloop.mMaxFd = -1; + mainloop.mTimeout = {2, 0}; + + FD_ZERO(&mainloop.mReadFdSet); + FD_ZERO(&mainloop.mWriteFdSet); + FD_ZERO(&mainloop.mErrorFdSet); + + taskRunner.Update(mainloop); + rval = select(mainloop.mMaxFd + 1, &mainloop.mReadFdSet, &mainloop.mWriteFdSet, &mainloop.mErrorFdSet, + &mainloop.mTimeout); + CHECK_TRUE(rval >= 0 || errno == EINTR); + + taskRunner.Process(mainloop); + } + + for (auto &th : threads) + { + th.join(); + } + + CHECK_EQUAL(10, counter.load()); +} + +TEST(TaskRunner, TestDelayedTasksOrder) +{ + std::string str; + otbr::TaskRunner taskRunner; + + taskRunner.Post(std::chrono::milliseconds(10), [&]() { str.push_back('a'); }); + taskRunner.Post(std::chrono::milliseconds(9), [&]() { str.push_back('b'); }); + taskRunner.Post(std::chrono::milliseconds(10), [&]() { str.push_back('c'); }); + + while (str.size() < 3) + { + int rval; + otbr::MainloopContext mainloop; + + mainloop.mMaxFd = -1; + mainloop.mTimeout = {2, 0}; + + FD_ZERO(&mainloop.mReadFdSet); + FD_ZERO(&mainloop.mWriteFdSet); + FD_ZERO(&mainloop.mErrorFdSet); + + taskRunner.Update(mainloop); + rval = select(mainloop.mMaxFd + 1, &mainloop.mReadFdSet, &mainloop.mWriteFdSet, &mainloop.mErrorFdSet, + &mainloop.mTimeout); + CHECK_TRUE(rval >= 0 || errno == EINTR); + + taskRunner.Process(mainloop); + } + + // Make sure that tasks with smaller delay are executed earlier. + STRCMP_EQUAL("bac", str.c_str()); +} + +TEST(TaskRunner, TestAllAPIs) +{ + std::atomic counter{0}; + otbr::TaskRunner taskRunner; + std::vector threads; + + // Increase the `counter` to 30 in separate threads. + for (size_t i = 0; i < 10; ++i) + { + threads.emplace_back([&]() { taskRunner.Post([&]() { ++counter; }); }); + threads.emplace_back([&]() { taskRunner.Post(std::chrono::milliseconds(10), [&]() { ++counter; }); }); + threads.emplace_back([&]() { taskRunner.PostAndWait([&]() { return ++counter; }); }); + } + + while (counter.load() < 30) + { + int rval; + otbr::MainloopContext mainloop; + + mainloop.mMaxFd = -1; + mainloop.mTimeout = {2, 0}; + + FD_ZERO(&mainloop.mReadFdSet); + FD_ZERO(&mainloop.mWriteFdSet); + FD_ZERO(&mainloop.mErrorFdSet); + + taskRunner.Update(mainloop); + rval = select(mainloop.mMaxFd + 1, &mainloop.mReadFdSet, &mainloop.mWriteFdSet, &mainloop.mErrorFdSet, + &mainloop.mTimeout); + CHECK_TRUE(rval >= 0 || errno == EINTR); + + taskRunner.Process(mainloop); + } + + for (auto &th : threads) + { + th.join(); + } + + CHECK_EQUAL(30, counter.load()); +} diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 829b11e4eb3..8e8aaf77a23 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -27,7 +27,7 @@ # add_subdirectory(openthread) -if(OTBR_WEB) +if(OTBR_REST) add_subdirectory(cJSON) add_subdirectory(http-parser) endif() diff --git a/third_party/angular-material/README.md b/third_party/angular-material/README.md deleted file mode 100644 index a1873c5bef3..00000000000 --- a/third_party/angular-material/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# Material Design for AngularJS - -## URL - -https://github.com/angular/bower-material/releases/tag/v1.1.4-master-e1345ae - -## Version - -1.1.4 - -## License - -MIT License - -## License File - -[LICENSE](repo/LICENSE) - -## Description -Material Design is a specification for a unified system of visual, motion, and interaction design that adapts across different devices. Our goal is to deliver a lean, lightweight set of AngularJS-native UI elements that implement the material design specification for use in AngularJS single-page applications (SPAs). - -Note: only the minimum set of files necessary to support the OpenThread project are included here. diff --git a/third_party/angular-material/repo/LICENSE b/third_party/angular-material/repo/LICENSE deleted file mode 100644 index 5bc59069aaf..00000000000 --- a/third_party/angular-material/repo/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License - -Copyright (c) 2014-2017 Google, Inc. http://angularjs.org - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/third_party/angular-material/repo/angular-material.min.css b/third_party/angular-material/repo/angular-material.min.css deleted file mode 100644 index 232a7fa9648..00000000000 --- a/third_party/angular-material/repo/angular-material.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/*! - * AngularJS Material Design - * https://github.com/angular/material - * @license MIT - * v1.1.4 - */body,html{height:100%;position:relative}body{margin:0;padding:0}[tabindex="-1"]:focus{outline:none}.inset{padding:10px}a.md-no-style,button.md-no-style{font-weight:400;background-color:inherit;text-align:left;border:none;padding:0;margin:0}button,input,select,textarea{vertical-align:baseline}button,html input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button}button[disabled],html input[type=button][disabled],input[type=reset][disabled],input[type=submit][disabled]{cursor:default}textarea{vertical-align:top;overflow:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box;-webkit-box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input:-webkit-autofill{text-shadow:none}.md-visually-hidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;text-transform:none;width:1px}.md-shadow{position:absolute;top:0;left:0;bottom:0;right:0;border-radius:inherit;pointer-events:none}.md-shadow-bottom-z-1{box-shadow:0 2px 5px 0 rgba(0,0,0,.26)}.md-shadow-bottom-z-2{box-shadow:0 4px 8px 0 rgba(0,0,0,.4)}.md-shadow-animated.md-shadow{-webkit-transition:box-shadow .28s cubic-bezier(.4,0,.2,1);transition:box-shadow .28s cubic-bezier(.4,0,.2,1)}.md-ripple-container{pointer-events:none;position:absolute;overflow:hidden;left:0;top:0;width:100%;height:100%;-webkit-transition:all .55s cubic-bezier(.25,.8,.25,1);transition:all .55s cubic-bezier(.25,.8,.25,1)}.md-ripple{position:absolute;-webkit-transform:translate(-50%,-50%) scale(0);transform:translate(-50%,-50%) scale(0);-webkit-transform-origin:50% 50%;transform-origin:50% 50%;opacity:0;border-radius:50%}.md-ripple.md-ripple-placed{-webkit-transition:margin .9s cubic-bezier(.25,.8,.25,1),border .9s cubic-bezier(.25,.8,.25,1),width .9s cubic-bezier(.25,.8,.25,1),height .9s cubic-bezier(.25,.8,.25,1),opacity .9s cubic-bezier(.25,.8,.25,1),-webkit-transform .9s cubic-bezier(.25,.8,.25,1);transition:margin .9s cubic-bezier(.25,.8,.25,1),border .9s cubic-bezier(.25,.8,.25,1),width .9s cubic-bezier(.25,.8,.25,1),height .9s cubic-bezier(.25,.8,.25,1),opacity .9s cubic-bezier(.25,.8,.25,1),-webkit-transform .9s cubic-bezier(.25,.8,.25,1);transition:margin .9s cubic-bezier(.25,.8,.25,1),border .9s cubic-bezier(.25,.8,.25,1),width .9s cubic-bezier(.25,.8,.25,1),height .9s cubic-bezier(.25,.8,.25,1),opacity .9s cubic-bezier(.25,.8,.25,1),transform .9s cubic-bezier(.25,.8,.25,1);transition:margin .9s cubic-bezier(.25,.8,.25,1),border .9s cubic-bezier(.25,.8,.25,1),width .9s cubic-bezier(.25,.8,.25,1),height .9s cubic-bezier(.25,.8,.25,1),opacity .9s cubic-bezier(.25,.8,.25,1),transform .9s cubic-bezier(.25,.8,.25,1),-webkit-transform .9s cubic-bezier(.25,.8,.25,1)}.md-ripple.md-ripple-scaled{-webkit-transform:translate(-50%,-50%) scale(1);transform:translate(-50%,-50%) scale(1)}.md-ripple.md-ripple-active,.md-ripple.md-ripple-full,.md-ripple.md-ripple-visible{opacity:.2}.md-ripple.md-ripple-remove{-webkit-animation:md-remove-ripple .9s cubic-bezier(.25,.8,.25,1);animation:md-remove-ripple .9s cubic-bezier(.25,.8,.25,1)}@-webkit-keyframes md-remove-ripple{0%{opacity:.15}to{opacity:0}}@keyframes md-remove-ripple{0%{opacity:.15}to{opacity:0}}.md-padding{padding:8px}.md-margin{margin:8px}.md-scroll-mask{position:absolute;background-color:transparent;top:0;right:0;bottom:0;left:0;z-index:50}.md-scroll-mask>.md-scroll-mask-bar{display:block;position:absolute;background-color:#fafafa;right:0;top:0;bottom:0;z-index:65;box-shadow:inset 0 0 1px rgba(0,0,0,.3)}.md-no-momentum{-webkit-overflow-scrolling:auto}.md-no-flicker{-webkit-filter:blur(0)}@media (min-width:960px){.md-padding{padding:16px}}body[dir=ltr],body[dir=rtl],html[dir=ltr],html[dir=rtl]{unicode-bidi:embed}bdo[dir=rtl]{direction:rtl}bdo[dir=ltr],bdo[dir=rtl]{unicode-bidi:bidi-override}bdo[dir=ltr]{direction:ltr}body,html{-webkit-tap-highlight-color:transparent;-webkit-touch-callout:none;min-height:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.md-display-4{font-size:112px;font-weight:300;letter-spacing:-.01em;line-height:112px}.md-display-3{font-size:56px;font-weight:400;letter-spacing:-.005em;line-height:56px}.md-display-2{font-size:45px;font-weight:400;line-height:64px}.md-display-1{font-size:34px;font-weight:400;line-height:40px}.md-headline{font-size:24px;font-weight:400;line-height:32px}.md-title{font-size:20px;font-weight:500;letter-spacing:.005em}.md-subhead{font-size:16px;line-height:24px}.md-body-1,.md-subhead{font-weight:400;letter-spacing:.01em}.md-body-1{font-size:14px;line-height:20px}.md-body-2{font-size:14px;font-weight:500;letter-spacing:.01em;line-height:24px}.md-caption{font-size:12px;letter-spacing:.02em}.md-button{letter-spacing:.01em}button,html,input,select,textarea{font-family:Roboto,Helvetica Neue,sans-serif}button,input,select,textarea{font-size:100%}.md-panel-outer-wrapper{height:100%;left:0;position:absolute;top:0;width:100%}._md-panel-hidden{display:none}._md-panel-offscreen{left:-9999px}._md-panel-fullscreen{border-radius:0;left:0;min-height:100%;min-width:100%;position:fixed;top:0}._md-panel-shown .md-panel{opacity:1;-webkit-transition:none;transition:none}.md-panel{opacity:0;position:fixed}.md-panel._md-panel-shown{opacity:1;-webkit-transition:none;transition:none}.md-panel._md-panel-animate-enter{opacity:1;-webkit-transition:all .3s cubic-bezier(0,0,.2,1);transition:all .3s cubic-bezier(0,0,.2,1)}.md-panel._md-panel-animate-leave{opacity:1;-webkit-transition:all .3s cubic-bezier(.4,0,1,1);transition:all .3s cubic-bezier(.4,0,1,1)}.md-panel._md-panel-animate-fade-out,.md-panel._md-panel-animate-scale-out{opacity:0}.md-panel._md-panel-backdrop{height:100%;position:absolute;width:100%}.md-panel._md-opaque-enter{opacity:.48;-webkit-transition:opacity .3s cubic-bezier(0,0,.2,1);transition:opacity .3s cubic-bezier(0,0,.2,1)}.md-panel._md-opaque-leave{-webkit-transition:opacity .3s cubic-bezier(.4,0,1,1);transition:opacity .3s cubic-bezier(.4,0,1,1)}md-autocomplete{border-radius:2px;display:block;height:40px;position:relative;overflow:visible;min-width:190px}md-autocomplete[disabled] input{cursor:default}md-autocomplete[md-floating-label]{border-radius:0;background:transparent;height:auto}md-autocomplete[md-floating-label] md-input-container{padding-bottom:0}md-autocomplete[md-floating-label] md-autocomplete-wrap{height:auto}md-autocomplete[md-floating-label] .md-show-clear-button button{display:block;position:absolute;right:0;top:20px;width:30px;height:30px}md-autocomplete[md-floating-label] .md-show-clear-button input{padding-right:30px}[dir=rtl] md-autocomplete[md-floating-label] .md-show-clear-button input{padding-right:0;padding-left:30px}md-autocomplete md-autocomplete-wrap{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;box-sizing:border-box;position:relative;overflow:visible;height:40px}md-autocomplete md-autocomplete-wrap.md-menu-showing{z-index:51}md-autocomplete md-autocomplete-wrap input,md-autocomplete md-autocomplete-wrap md-input-container{-webkit-box-flex:1;-webkit-flex:1 1 0%;flex:1 1 0%;box-sizing:border-box;min-width:0}md-autocomplete md-autocomplete-wrap md-progress-linear{position:absolute;bottom:-2px;left:0}md-autocomplete md-autocomplete-wrap md-progress-linear.md-inline{bottom:40px;right:2px;left:2px;width:auto}md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate{position:absolute;top:0;left:0;width:100%;height:3px;-webkit-transition:none;transition:none}md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate .md-container{-webkit-transition:none;transition:none;height:3px}md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate.ng-enter{-webkit-transition:opacity .15s linear;transition:opacity .15s linear}md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate.ng-enter.ng-enter-active{opacity:1}md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate.ng-leave{-webkit-transition:opacity .15s linear;transition:opacity .15s linear}md-autocomplete md-autocomplete-wrap md-progress-linear .md-mode-indeterminate.ng-leave.ng-leave-active{opacity:0}md-autocomplete input:not(.md-input){font-size:14px;box-sizing:border-box;border:none;box-shadow:none;outline:none;background:transparent;width:100%;padding:0 15px;line-height:40px;height:40px}md-autocomplete input:not(.md-input)::-ms-clear{display:none}md-autocomplete .md-show-clear-button button{position:relative;line-height:20px;text-align:center;width:30px;height:30px;cursor:pointer;border:none;border-radius:50%;padding:0;font-size:12px;background:transparent;margin:auto 5px}md-autocomplete .md-show-clear-button button:after{content:"";position:absolute;top:-6px;right:-6px;bottom:-6px;left:-6px;border-radius:50%;-webkit-transform:scale(0);transform:scale(0);opacity:0;-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1)}md-autocomplete .md-show-clear-button button:focus{outline:none}md-autocomplete .md-show-clear-button button:focus:after{-webkit-transform:scale(1);transform:scale(1);opacity:1}md-autocomplete .md-show-clear-button button md-icon{position:absolute;top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0) scale(.9);transform:translate3d(-50%,-50%,0) scale(.9)}md-autocomplete .md-show-clear-button button md-icon path{stroke-width:0}md-autocomplete .md-show-clear-button button.ng-enter{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:-webkit-transform .15s ease-out;transition:-webkit-transform .15s ease-out;transition:transform .15s ease-out;transition:transform .15s ease-out,-webkit-transform .15s ease-out}md-autocomplete .md-show-clear-button button.ng-enter.ng-enter-active{-webkit-transform:scale(1);transform:scale(1)}md-autocomplete .md-show-clear-button button.ng-leave{-webkit-transition:-webkit-transform .15s ease-out;transition:-webkit-transform .15s ease-out;transition:transform .15s ease-out;transition:transform .15s ease-out,-webkit-transform .15s ease-out}md-autocomplete .md-show-clear-button button.ng-leave.ng-leave-active{-webkit-transform:scale(0);transform:scale(0)}@media screen and (-ms-high-contrast:active){md-autocomplete input{border:1px solid #fff}md-autocomplete li:focus{color:#fff}}.md-virtual-repeat-container.md-autocomplete-suggestions-container{position:absolute;box-shadow:0 2px 5px rgba(0,0,0,.25);z-index:100;height:100%}.md-virtual-repeat-container.md-not-found{height:48px}.md-autocomplete-suggestions{margin:0;list-style:none;padding:0}.md-autocomplete-suggestions li{font-size:14px;overflow:hidden;padding:0 15px;line-height:48px;height:48px;-webkit-transition:background .15s linear;transition:background .15s linear;margin:0;white-space:nowrap;text-overflow:ellipsis}.md-autocomplete-suggestions li:focus{outline:none}.md-autocomplete-suggestions li:not(.md-not-found-wrapper){cursor:pointer}@media screen and (-ms-high-contrast:active){.md-autocomplete-suggestions,md-autocomplete{border:1px solid #fff}}md-backdrop{-webkit-transition:opacity .45s;transition:opacity .45s;position:absolute;top:0;bottom:0;left:0;right:0;z-index:50}md-backdrop.md-menu-backdrop{position:fixed!important;z-index:99}md-backdrop.md-select-backdrop{z-index:81;-webkit-transition-duration:0;transition-duration:0}md-backdrop.md-dialog-backdrop{z-index:79}md-backdrop.md-bottom-sheet-backdrop{z-index:69}md-backdrop.md-sidenav-backdrop{z-index:59}md-backdrop.md-click-catcher{position:absolute}md-backdrop.md-opaque{opacity:.48}md-backdrop.md-opaque.ng-enter{opacity:0}md-backdrop.md-opaque.ng-enter.md-opaque.ng-enter-active{opacity:.48}md-backdrop.md-opaque.ng-leave{opacity:.48;-webkit-transition:opacity .4s;transition:opacity .4s}md-backdrop.md-opaque.ng-leave.md-opaque.ng-leave-active{opacity:0}md-bottom-sheet{position:absolute;left:0;right:0;bottom:0;padding:8px 16px 88px;z-index:70;border-top-width:1px;border-top-style:solid;-webkit-transform:translate3d(0,80px,0);transform:translate3d(0,80px,0);-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1);-webkit-transition-property:-webkit-transform;transition-property:-webkit-transform;transition-property:transform;transition-property:transform,-webkit-transform}md-bottom-sheet.md-has-header{padding-top:0}md-bottom-sheet.ng-enter{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}md-bottom-sheet.ng-enter-active{opacity:1;display:block;-webkit-transform:translate3d(0,80px,0)!important;transform:translate3d(0,80px,0)!important}md-bottom-sheet.ng-leave-active{-webkit-transform:translate3d(0,100%,0)!important;transform:translate3d(0,100%,0)!important;-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2)}md-bottom-sheet .md-subheader{background-color:transparent;font-family:Roboto,Helvetica Neue,sans-serif;line-height:56px;padding:0;white-space:nowrap}md-bottom-sheet md-inline-icon{display:inline-block;height:24px;width:24px;fill:#444}md-bottom-sheet md-list-item{display:-webkit-box;display:-webkit-flex;display:flex;outline:none}md-bottom-sheet md-list-item:hover{cursor:pointer}md-bottom-sheet.md-list md-list-item{padding:0;-webkit-box-align:center;-webkit-align-items:center;align-items:center;height:48px}md-bottom-sheet.md-grid{padding-left:24px;padding-right:24px;padding-top:0}md-bottom-sheet.md-grid md-list{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;flex-wrap:wrap}md-bottom-sheet.md-grid md-list,md-bottom-sheet.md-grid md-list-item{-webkit-box-direction:normal;-webkit-transition:all .5s;transition:all .5s;-webkit-box-align:center;-webkit-align-items:center;align-items:center}md-bottom-sheet.md-grid md-list-item{-webkit-box-orient:vertical;-webkit-flex-direction:column;flex-direction:column;height:96px;margin-top:8px;margin-bottom:8px}@media (max-width:960px){md-bottom-sheet.md-grid md-list-item{-webkit-box-flex:1;-webkit-flex:1 1 33.33333%;flex:1 1 33.33333%;max-width:33.33333%}md-bottom-sheet.md-grid md-list-item:nth-of-type(3n+1){-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start}md-bottom-sheet.md-grid md-list-item:nth-of-type(3n){-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end}}@media (min-width:960px) and (max-width:1279px){md-bottom-sheet.md-grid md-list-item{-webkit-box-flex:1;-webkit-flex:1 1 25%;flex:1 1 25%;max-width:25%}}@media (min-width:1280px) and (max-width:1919px){md-bottom-sheet.md-grid md-list-item{-webkit-box-flex:1;-webkit-flex:1 1 16.66667%;flex:1 1 16.66667%;max-width:16.66667%}}@media (min-width:1920px){md-bottom-sheet.md-grid md-list-item{-webkit-box-flex:1;-webkit-flex:1 1 14.28571%;flex:1 1 14.28571%;max-width:14.28571%}}md-bottom-sheet.md-grid md-list-item:before{display:none}md-bottom-sheet.md-grid md-list-item .md-list-item-content{width:48px;padding-bottom:16px}md-bottom-sheet.md-grid md-list-item .md-grid-item-content,md-bottom-sheet.md-grid md-list-item .md-list-item-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-align:center;-webkit-align-items:center;align-items:center}md-bottom-sheet.md-grid md-list-item .md-grid-item-content{border:1px solid transparent;width:80px}md-bottom-sheet.md-grid md-list-item .md-grid-text{font-weight:400;line-height:16px;font-size:13px;margin:0;white-space:nowrap;width:64px;text-align:center;text-transform:none;padding-top:8px}@media screen and (-ms-high-contrast:active){md-bottom-sheet{border:1px solid #fff}}button.md-button::-moz-focus-inner{border:0}.md-button{display:inline-block;position:relative;cursor:pointer;min-height:36px;min-width:88px;line-height:36px;vertical-align:middle;-webkit-box-align:center;-webkit-align-items:center;align-items:center;text-align:center;border-radius:2px;box-sizing:border-box;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:none;border:0;padding:0 6px;margin:6px 8px;background:transparent;color:currentColor;white-space:nowrap;text-transform:uppercase;font-weight:500;font-size:14px;font-style:inherit;font-variant:inherit;font-family:inherit;text-decoration:none;overflow:hidden;-webkit-transition:box-shadow .4s cubic-bezier(.25,.8,.25,1),background-color .4s cubic-bezier(.25,.8,.25,1);transition:box-shadow .4s cubic-bezier(.25,.8,.25,1),background-color .4s cubic-bezier(.25,.8,.25,1)}.md-dense :not(.md-dense-disabled) .md-button:not(.md-dense-disabled),.md-dense>.md-button:not(.md-dense-disabled){min-height:32px;line-height:32px;font-size:13px}.md-button:focus{outline:none}.md-button:focus,.md-button:hover{text-decoration:none}.md-button.ng-hide,.md-button.ng-leave{-webkit-transition:none;transition:none}.md-button.md-cornered{border-radius:0}.md-button.md-icon{padding:0;background:none}.md-button.md-raised:not([disabled]){box-shadow:0 2px 5px 0 rgba(0,0,0,.26)}.md-button.md-icon-button{margin:0 6px;height:40px;min-width:0;line-height:24px;padding:8px;width:40px;border-radius:50%}.md-button.md-icon-button .md-ripple-container{border-radius:50%;background-clip:padding-box;overflow:hidden;-webkit-mask-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC")}.md-button.md-fab{z-index:20;line-height:56px;min-width:0;width:56px;height:56px;vertical-align:middle;box-shadow:0 2px 5px 0 rgba(0,0,0,.26);border-radius:50%;background-clip:padding-box;overflow:hidden;-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2);-webkit-transition-property:background-color,box-shadow,-webkit-transform;transition-property:background-color,box-shadow,-webkit-transform;transition-property:background-color,box-shadow,transform;transition-property:background-color,box-shadow,transform,-webkit-transform}.md-button.md-fab.md-fab-bottom-right{top:auto;right:20px;bottom:20px;left:auto;position:absolute}.md-button.md-fab.md-fab-bottom-left{top:auto;right:auto;bottom:20px;left:20px;position:absolute}.md-button.md-fab.md-fab-top-right{top:20px;right:20px;bottom:auto;left:auto;position:absolute}.md-button.md-fab.md-fab-top-left{top:20px;right:auto;bottom:auto;left:20px;position:absolute}.md-button.md-fab .md-ripple-container{border-radius:50%;background-clip:padding-box;overflow:hidden;-webkit-mask-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC")}.md-button.md-fab.md-mini{line-height:40px;width:40px;height:40px}.md-button.md-fab.ng-hide,.md-button.md-fab.ng-leave{-webkit-transition:none;transition:none}.md-button:not([disabled]).md-fab.md-focused,.md-button:not([disabled]).md-raised.md-focused{box-shadow:0 2px 5px 0 rgba(0,0,0,.26)}.md-button:not([disabled]).md-fab:active,.md-button:not([disabled]).md-raised:active{box-shadow:0 4px 8px 0 rgba(0,0,0,.4)}.md-button .md-ripple-container{border-radius:2px;background-clip:padding-box;overflow:hidden;-webkit-mask-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC")}.md-button.md-icon-button md-icon,button.md-button.md-fab md-icon{display:block}.md-toast-open-top .md-button.md-fab-top-left,.md-toast-open-top .md-button.md-fab-top-right{-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1);-webkit-transform:translate3d(0,42px,0);transform:translate3d(0,42px,0)}.md-toast-open-top .md-button.md-fab-top-left:not([disabled]).md-focused,.md-toast-open-top .md-button.md-fab-top-left:not([disabled]):hover,.md-toast-open-top .md-button.md-fab-top-right:not([disabled]).md-focused,.md-toast-open-top .md-button.md-fab-top-right:not([disabled]):hover{-webkit-transform:translate3d(0,41px,0);transform:translate3d(0,41px,0)}.md-toast-open-bottom .md-button.md-fab-bottom-left,.md-toast-open-bottom .md-button.md-fab-bottom-right{-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1);-webkit-transform:translate3d(0,-42px,0);transform:translate3d(0,-42px,0)}.md-toast-open-bottom .md-button.md-fab-bottom-left:not([disabled]).md-focused,.md-toast-open-bottom .md-button.md-fab-bottom-left:not([disabled]):hover,.md-toast-open-bottom .md-button.md-fab-bottom-right:not([disabled]).md-focused,.md-toast-open-bottom .md-button.md-fab-bottom-right:not([disabled]):hover{-webkit-transform:translate3d(0,-43px,0);transform:translate3d(0,-43px,0)}.md-button-group{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;width:100%}.md-button-group>.md-button{-webkit-box-flex:1;-webkit-flex:1;flex:1;display:block;overflow:hidden;width:0;border-width:1px 0 1px 1px;border-radius:0;text-align:center;text-overflow:ellipsis;white-space:nowrap}.md-button-group>.md-button:first-child{border-radius:2px 0 0 2px}.md-button-group>.md-button:last-child{border-right-width:1px;border-radius:0 2px 2px 0}@media screen and (-ms-high-contrast:active){.md-button.md-fab,.md-button.md-raised{border:1px solid #fff}}md-card{box-sizing:border-box;-webkit-box-orient:vertical;-webkit-flex-direction:column;flex-direction:column;margin:8px;box-shadow:0 1px 3px 0 rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 2px 1px -1px rgba(0,0,0,.12)}md-card,md-card md-card-header{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-direction:normal}md-card md-card-header{padding:16px;-webkit-box-orient:horizontal;-webkit-flex-direction:row;flex-direction:row}md-card md-card-header:first-child md-card-avatar{margin-right:12px}[dir=rtl] md-card md-card-header:first-child md-card-avatar{margin-right:auto;margin-left:12px}md-card md-card-header:last-child md-card-avatar{margin-left:12px}[dir=rtl] md-card md-card-header:last-child md-card-avatar{margin-left:auto;margin-right:12px}md-card md-card-header md-card-avatar{width:40px;height:40px}md-card md-card-header md-card-avatar .md-user-avatar,md-card md-card-header md-card-avatar md-icon{border-radius:50%}md-card md-card-header md-card-avatar md-icon{padding:8px}md-card md-card-header md-card-avatar md-icon>svg{height:inherit;width:inherit}md-card md-card-header md-card-avatar+md-card-header-text{max-height:40px}md-card md-card-header md-card-avatar+md-card-header-text .md-title{font-size:14px}md-card md-card-header md-card-header-text{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}md-card md-card-header md-card-header-text .md-subhead{font-size:14px}md-card>img,md-card>md-card-header img,md-card md-card-title-media img{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;width:100%;height:auto}md-card md-card-title{padding:24px 16px 16px;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}md-card md-card-title+md-card-content{padding-top:0}md-card md-card-title md-card-title-text{-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;display:-webkit-box;display:-webkit-flex;display:flex}md-card md-card-title md-card-title-text .md-subhead{padding-top:0;font-size:14px}md-card md-card-title md-card-title-text:only-child .md-subhead{padding-top:12px}md-card md-card-title md-card-title-media{margin-top:-8px}md-card md-card-title md-card-title-media .md-media-sm{height:80px;width:80px}md-card md-card-title md-card-title-media .md-media-md{height:112px;width:112px}md-card md-card-title md-card-title-media .md-media-lg{height:152px;width:152px}md-card md-card-content{display:block;padding:16px}md-card md-card-content>p:first-child{margin-top:0}md-card md-card-content>p:last-child{margin-bottom:0}md-card md-card-content .md-media-xl{height:240px;width:240px}md-card .md-actions,md-card md-card-actions{margin:8px}md-card .md-actions.layout-column .md-button:not(.md-icon-button),md-card md-card-actions.layout-column .md-button:not(.md-icon-button){margin:2px 0}md-card .md-actions.layout-column .md-button:not(.md-icon-button):first-of-type,md-card md-card-actions.layout-column .md-button:not(.md-icon-button):first-of-type{margin-top:0}md-card .md-actions.layout-column .md-button:not(.md-icon-button):last-of-type,md-card md-card-actions.layout-column .md-button:not(.md-icon-button):last-of-type{margin-bottom:0}md-card .md-actions.layout-column .md-button.md-icon-button,md-card md-card-actions.layout-column .md-button.md-icon-button{margin-top:6px;margin-bottom:6px}md-card .md-actions md-card-icon-actions,md-card md-card-actions md-card-icon-actions{-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button),md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button){margin:0 4px}md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button):first-of-type,md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button):first-of-type{margin-left:0}[dir=rtl] md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button):first-of-type,[dir=rtl] md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button):first-of-type{margin-left:auto;margin-right:0}md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button):last-of-type,md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button):last-of-type{margin-right:0}[dir=rtl] md-card .md-actions:not(.layout-column) .md-button:not(.md-icon-button):last-of-type,[dir=rtl] md-card md-card-actions:not(.layout-column) .md-button:not(.md-icon-button):last-of-type{margin-right:auto;margin-left:0}md-card .md-actions:not(.layout-column) .md-button.md-icon-button,md-card md-card-actions:not(.layout-column) .md-button.md-icon-button{margin-left:6px;margin-right:6px}md-card .md-actions:not(.layout-column) .md-button.md-icon-button:first-of-type,md-card md-card-actions:not(.layout-column) .md-button.md-icon-button:first-of-type{margin-left:12px}[dir=rtl] md-card .md-actions:not(.layout-column) .md-button.md-icon-button:first-of-type,[dir=rtl] md-card md-card-actions:not(.layout-column) .md-button.md-icon-button:first-of-type{margin-left:auto;margin-right:12px}md-card .md-actions:not(.layout-column) .md-button.md-icon-button:last-of-type,md-card md-card-actions:not(.layout-column) .md-button.md-icon-button:last-of-type{margin-right:12px}[dir=rtl] md-card .md-actions:not(.layout-column) .md-button.md-icon-button:last-of-type,[dir=rtl] md-card md-card-actions:not(.layout-column) .md-button.md-icon-button:last-of-type{margin-right:auto;margin-left:12px}md-card .md-actions:not(.layout-column) .md-button+md-card-icon-actions,md-card md-card-actions:not(.layout-column) .md-button+md-card-icon-actions{-webkit-box-flex:1;-webkit-flex:1;flex:1;-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}md-card md-card-footer{margin-top:auto;padding:16px}@media screen and (-ms-high-contrast:active){md-card{border:1px solid #fff}}.md-image-no-fill>img{width:auto;height:auto}.md-inline-form md-checkbox{margin:19px 0 18px}md-checkbox{box-sizing:border-box;display:inline-block;margin-bottom:16px;white-space:nowrap;cursor:pointer;outline:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:relative;min-width:20px;min-height:20px;margin-left:0;margin-right:16px}[dir=rtl] md-checkbox{margin-left:16px;margin-right:0}md-checkbox:last-of-type{margin-left:0;margin-right:0}md-checkbox.md-focused:not([disabled]) .md-container:before{left:-8px;top:-8px;right:-8px;bottom:-8px}md-checkbox.md-focused:not([disabled]):not(.md-checked) .md-container:before{background-color:rgba(0,0,0,.12)}md-checkbox.md-align-top-left>div.md-container{top:12px}md-checkbox .md-container{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);box-sizing:border-box;display:inline-block;width:20px;height:20px;left:0;right:auto}[dir=rtl] md-checkbox .md-container{left:auto;right:0}md-checkbox .md-container:before{box-sizing:border-box;background-color:transparent;border-radius:50%;content:"";position:absolute;display:block;height:auto;left:0;top:0;right:0;bottom:0;-webkit-transition:all .5s;transition:all .5s;width:auto}md-checkbox .md-container:after{box-sizing:border-box;content:"";position:absolute;top:-10px;right:-10px;bottom:-10px;left:-10px}md-checkbox .md-container .md-ripple-container{position:absolute;display:block;width:auto;height:auto;left:-15px;top:-15px;right:-15px;bottom:-15px}md-checkbox .md-icon{box-sizing:border-box;-webkit-transition:.24s;transition:.24s;position:absolute;top:0;left:0;width:20px;height:20px;border-width:2px;border-style:solid;border-radius:2px}md-checkbox.md-checked .md-icon{border-color:transparent}md-checkbox.md-checked .md-icon:after{box-sizing:border-box;-webkit-transform:rotate(45deg);transform:rotate(45deg);position:absolute;left:4.66667px;top:.22222px;display:table;width:6.66667px;height:13.33333px;border-width:2px;border-style:solid;border-top:0;border-left:0;content:""}md-checkbox[disabled]{cursor:default}md-checkbox.md-indeterminate .md-icon:after{box-sizing:border-box;position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);display:table;width:12px;height:2px;border-width:2px;border-style:solid;border-top:0;border-left:0;content:""}md-checkbox .md-label{box-sizing:border-box;position:relative;display:inline-block;vertical-align:middle;white-space:normal;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;margin-left:30px;margin-right:0}[dir=rtl] md-checkbox .md-label{margin-left:0;margin-right:30px}.md-contact-chips .md-chips md-chip{padding:0 25px 0 0}[dir=rtl] .md-contact-chips .md-chips md-chip{padding:0 0 0 25px}.md-contact-chips .md-chips md-chip .md-contact-avatar{float:left}[dir=rtl] .md-contact-chips .md-chips md-chip .md-contact-avatar{float:right}.md-contact-chips .md-chips md-chip .md-contact-avatar img{height:32px;border-radius:16px}.md-contact-chips .md-chips md-chip .md-contact-name{display:inline-block;height:32px;margin-left:8px}[dir=rtl] .md-contact-chips .md-chips md-chip .md-contact-name{margin-left:auto;margin-right:8px}.md-contact-suggestion{height:56px}.md-contact-suggestion img{height:40px;border-radius:20px;margin-top:8px}.md-contact-suggestion .md-contact-name{margin-left:8px;width:120px}[dir=rtl] .md-contact-suggestion .md-contact-name{margin-left:auto;margin-right:8px}.md-contact-suggestion .md-contact-email,.md-contact-suggestion .md-contact-name{display:inline-block;overflow:hidden;text-overflow:ellipsis}.md-contact-chips-suggestions li{height:100%}.md-chips{display:block;font-family:Roboto,Helvetica Neue,sans-serif;font-size:16px;padding:0 0 8px 3px;vertical-align:middle}.md-chips:after{content:"";display:table;clear:both}[dir=rtl] .md-chips{padding:0 3px 8px 0}.md-chips.md-readonly .md-chip-input-container{min-height:32px}.md-chips:not(.md-readonly){cursor:text}.md-chips.md-removable md-chip{padding-right:22px}[dir=rtl] .md-chips.md-removable md-chip{padding-right:0;padding-left:22px}.md-chips.md-removable md-chip .md-chip-content{padding-right:4px}[dir=rtl] .md-chips.md-removable md-chip .md-chip-content{padding-right:0;padding-left:4px}.md-chips md-chip{cursor:default;border-radius:16px;display:block;height:32px;line-height:32px;margin:8px 8px 0 0;padding:0 12px;float:left;box-sizing:border-box;max-width:100%;position:relative}[dir=rtl] .md-chips md-chip{margin:8px 0 0 8px;float:right}.md-chips md-chip .md-chip-content{display:block;float:left;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}[dir=rtl] .md-chips md-chip .md-chip-content{float:right}.md-chips md-chip .md-chip-content:focus{outline:none}.md-chips md-chip._md-chip-content-edit-is-enabled{-webkit-user-select:none;-moz-user-select:none;-khtml-user-select:none;-ms-user-select:none}.md-chips md-chip .md-chip-remove-container{position:absolute;right:0;line-height:22px}[dir=rtl] .md-chips md-chip .md-chip-remove-container{right:auto;left:0}.md-chips md-chip .md-chip-remove{text-align:center;width:32px;height:32px;min-width:0;padding:0;background:transparent;border:none;box-shadow:none;margin:0;position:relative}.md-chips md-chip .md-chip-remove md-icon{height:18px;width:18px;position:absolute;top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}.md-chips .md-chip-input-container{display:block;line-height:32px;margin:8px 8px 0 0;padding:0;float:left}[dir=rtl] .md-chips .md-chip-input-container{margin:8px 0 0 8px;float:right}.md-chips .md-chip-input-container input:not([type]),.md-chips .md-chip-input-container input[type=email],.md-chips .md-chip-input-container input[type=number],.md-chips .md-chip-input-container input[type=tel],.md-chips .md-chip-input-container input[type=text],.md-chips .md-chip-input-container input[type=url]{border:0;height:32px;line-height:32px;padding:0}.md-chips .md-chip-input-container input:not([type]):focus,.md-chips .md-chip-input-container input[type=email]:focus,.md-chips .md-chip-input-container input[type=number]:focus,.md-chips .md-chip-input-container input[type=tel]:focus,.md-chips .md-chip-input-container input[type=text]:focus,.md-chips .md-chip-input-container input[type=url]:focus{outline:none}.md-chips .md-chip-input-container md-autocomplete,.md-chips .md-chip-input-container md-autocomplete-wrap{background:transparent;height:32px}.md-chips .md-chip-input-container md-autocomplete md-autocomplete-wrap{box-shadow:none}.md-chips .md-chip-input-container input{border:0;height:32px;line-height:32px;padding:0}.md-chips .md-chip-input-container input:focus{outline:none}.md-chips .md-chip-input-container md-autocomplete,.md-chips .md-chip-input-container md-autocomplete-wrap{height:32px}.md-chips .md-chip-input-container md-autocomplete{box-shadow:none}.md-chips .md-chip-input-container md-autocomplete input{position:relative}.md-chips .md-chip-input-container:not(:first-child){margin:8px 8px 0 0}[dir=rtl] .md-chips .md-chip-input-container:not(:first-child){margin:8px 0 0 8px}.md-chips .md-chip-input-container input{background:transparent;border-width:0}.md-chips md-autocomplete button{display:none}@media screen and (-ms-high-contrast:active){.md-chip-input-container,md-chip{border:1px solid #fff}.md-chip-input-container md-autocomplete{border:none}}md-content{display:block;position:relative;overflow:auto;-webkit-overflow-scrolling:touch}md-content[md-scroll-y]{overflow-y:auto;overflow-x:hidden}md-content[md-scroll-x]{overflow-x:auto;overflow-y:hidden}@media print{md-content{overflow:visible!important}}md-calendar{font-size:13px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.md-calendar-scroll-mask{display:inline-block;overflow:hidden;height:308px}.md-calendar-scroll-mask .md-virtual-repeat-scroller{overflow-y:scroll;-webkit-overflow-scrolling:touch}.md-calendar-scroll-mask .md-virtual-repeat-scroller::-webkit-scrollbar{display:none}.md-calendar-scroll-mask .md-virtual-repeat-offsetter{width:100%}.md-calendar-scroll-container{box-shadow:inset -3px 3px 6px rgba(0,0,0,.2);display:inline-block;height:308px;width:346px}.md-calendar-date{height:44px;width:44px;text-align:center;padding:0;border:none;box-sizing:content-box}.md-calendar-date:first-child{padding-left:16px}[dir=rtl] .md-calendar-date:first-child{padding-left:0;padding-right:16px}.md-calendar-date:last-child{padding-right:16px}[dir=rtl] .md-calendar-date:last-child{padding-right:0;padding-left:16px}.md-calendar-date.md-calendar-date-disabled{cursor:default}.md-calendar-date-selection-indicator{-webkit-transition:background-color,color .4s cubic-bezier(.25,.8,.25,1);transition:background-color,color .4s cubic-bezier(.25,.8,.25,1);border-radius:50%;display:inline-block;width:40px;height:40px;line-height:40px}.md-calendar-date:not(.md-disabled) .md-calendar-date-selection-indicator{cursor:pointer}.md-calendar-month-label{height:44px;font-size:14px;font-weight:500;padding:0 0 0 24px}[dir=rtl] .md-calendar-month-label{padding:0 24px 0 0}md-calendar-month .md-calendar-month-label:not(.md-calendar-month-label-disabled){cursor:pointer}.md-calendar-month-label md-icon{-webkit-transform:rotate(180deg);transform:rotate(180deg)}[dir=rtl] .md-calendar-month-label md-icon{-webkit-transform:none;transform:none}.md-calendar-month-label span{vertical-align:middle}.md-calendar-day-header{table-layout:fixed;border-spacing:0;border-collapse:collapse}.md-calendar-day-header th{height:40px;width:44px;text-align:center;padding:0;border:none;box-sizing:content-box;font-weight:400}.md-calendar-day-header th:first-child{padding-left:16px}[dir=rtl] .md-calendar-day-header th:first-child{padding-left:0;padding-right:16px}.md-calendar-day-header th:last-child{padding-right:16px}[dir=rtl] .md-calendar-day-header th:last-child{padding-right:0;padding-left:16px}.md-calendar{table-layout:fixed;border-spacing:0;border-collapse:collapse}.md-calendar tr:last-child td{border-bottom-width:1px;border-bottom-style:solid}.md-calendar:first-child{border-top:1px solid transparent}.md-calendar tbody,.md-calendar td,.md-calendar tr{vertical-align:middle;box-sizing:content-box}md-datepicker{white-space:nowrap;overflow:hidden;vertical-align:middle}.md-inline-form md-datepicker{margin-top:12px}.md-datepicker-button{display:inline-block;box-sizing:border-box;background:none;vertical-align:middle;position:relative}.md-datepicker-button:before{top:0;left:0;bottom:0;right:0;position:absolute;content:"";speak:none}.md-datepicker-input{font-size:14px;box-sizing:border-box;border:none;box-shadow:none;outline:none;background:transparent;min-width:120px;max-width:328px;padding:0 0 5px}.md-datepicker-input::-ms-clear{display:none}._md-datepicker-floating-label>md-datepicker{overflow:visible}._md-datepicker-floating-label>md-datepicker .md-datepicker-input-container{border:none}._md-datepicker-floating-label>md-datepicker .md-datepicker-button{float:left;margin-top:-12px;top:9.5px}[dir=rtl] ._md-datepicker-floating-label>md-datepicker .md-datepicker-button{float:right}._md-datepicker-floating-label .md-input{float:none}._md-datepicker-floating-label._md-datepicker-has-calendar-icon>label:not(.md-no-float):not(.md-container-ignore){right:18px;left:auto;width:calc(100% - 84px)}[dir=rtl] ._md-datepicker-floating-label._md-datepicker-has-calendar-icon>label:not(.md-no-float):not(.md-container-ignore){right:auto;left:18px}._md-datepicker-floating-label._md-datepicker-has-calendar-icon .md-input-message-animation{margin-left:64px}[dir=rtl] ._md-datepicker-floating-label._md-datepicker-has-calendar-icon .md-input-message-animation{margin-left:auto;margin-right:64px}._md-datepicker-has-triangle-icon{padding-right:18px;margin-right:-18px}[dir=rtl] ._md-datepicker-has-triangle-icon{padding-right:0;padding-left:18px;margin-right:auto;margin-left:-18px}.md-datepicker-input-container{position:relative;border-bottom-width:1px;border-bottom-style:solid;display:inline-block;width:auto}.md-icon-button+.md-datepicker-input-container{margin-left:12px}[dir=rtl] .md-icon-button+.md-datepicker-input-container{margin-left:auto;margin-right:12px}.md-datepicker-input-container.md-datepicker-focused{border-bottom-width:2px}.md-datepicker-is-showing .md-scroll-mask{z-index:99}.md-datepicker-calendar-pane{position:absolute;top:0;left:-100%;z-index:100;border-width:1px;border-style:solid;background:transparent;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:0 0;transform-origin:0 0;-webkit-transition:-webkit-transform .2s cubic-bezier(.25,.8,.25,1);transition:-webkit-transform .2s cubic-bezier(.25,.8,.25,1);transition:transform .2s cubic-bezier(.25,.8,.25,1);transition:transform .2s cubic-bezier(.25,.8,.25,1),-webkit-transform .2s cubic-bezier(.25,.8,.25,1)}.md-datepicker-calendar-pane.md-pane-open{-webkit-transform:scale(1);transform:scale(1)}.md-datepicker-input-mask{height:40px;width:340px;position:relative;overflow:hidden;background:transparent;pointer-events:none;cursor:text}.md-datepicker-calendar{opacity:0;-webkit-transition:opacity .2s cubic-bezier(.5,0,.25,1);transition:opacity .2s cubic-bezier(.5,0,.25,1)}.md-pane-open .md-datepicker-calendar{opacity:1}.md-datepicker-calendar md-calendar:focus{outline:none}.md-datepicker-expand-triangle{position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid}.md-datepicker-triangle-button{position:absolute;right:0;bottom:-2.5px;-webkit-transform:translateX(45%);transform:translateX(45%)}[dir=rtl] .md-datepicker-triangle-button{right:auto;left:0;-webkit-transform:translateX(-45%);transform:translateX(-45%)}.md-datepicker-triangle-button.md-button.md-icon-button{height:36px;width:36px;position:absolute;padding:8px}md-datepicker[disabled] .md-datepicker-input-container{border-bottom-color:transparent}md-datepicker[disabled] .md-datepicker-triangle-button{display:none}.md-datepicker-open{overflow:hidden}.md-datepicker-open .md-datepicker-input-container,.md-datepicker-open input.md-input{border-bottom-color:transparent}.md-datepicker-open .md-datepicker-triangle-button,.md-datepicker-open.md-input-has-placeholder>label,.md-datepicker-open.md-input-has-value>label,.md-datepicker-pos-adjusted .md-datepicker-input-mask{display:none}.md-datepicker-calendar-pane .md-calendar{-webkit-transform:translateY(-85px);transform:translateY(-85px);-webkit-transition:-webkit-transform .65s cubic-bezier(.25,.8,.25,1);transition:-webkit-transform .65s cubic-bezier(.25,.8,.25,1);transition:transform .65s cubic-bezier(.25,.8,.25,1);transition:transform .65s cubic-bezier(.25,.8,.25,1),-webkit-transform .65s cubic-bezier(.25,.8,.25,1);-webkit-transition-delay:.125s;transition-delay:.125s}.md-datepicker-calendar-pane.md-pane-open .md-calendar{-webkit-transform:translateY(0);transform:translateY(0)}.md-dialog-is-showing{max-height:100%}.md-dialog-container{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;-webkit-box-align:center;-webkit-align-items:center;align-items:center;position:absolute;top:0;left:0;width:100%;height:100%;z-index:80;overflow:hidden}.md-dialog-container,md-dialog{display:-webkit-box;display:-webkit-flex;display:flex}md-dialog{opacity:0;min-width:240px;max-width:80%;max-height:80%;position:relative;overflow:auto;box-shadow:0 7px 8px -4px rgba(0,0,0,.2),0 13px 19px 2px rgba(0,0,0,.14),0 5px 24px 4px rgba(0,0,0,.12);-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}md-dialog.md-transition-in{opacity:1;-webkit-transform:translate(0,0) scale(1);transform:translate(0,0) scale(1)}md-dialog.md-transition-in,md-dialog.md-transition-out{-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1)}md-dialog.md-transition-out{opacity:0;-webkit-transform:translate(0,100%) scale(.2);transform:translate(0,100%) scale(.2)}md-dialog>form{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;overflow:auto}md-dialog .md-dialog-content{padding:24px}md-dialog md-dialog-content{-webkit-box-ordinal-group:2;-webkit-order:1;order:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;overflow:auto;-webkit-overflow-scrolling:touch}md-dialog md-dialog-content:not([layout=row])>:first-child:not(.md-subheader){margin-top:0}md-dialog md-dialog-content:focus{outline:none}md-dialog md-dialog-content .md-subheader{margin:0}md-dialog md-dialog-content .md-dialog-content-body{width:100%}md-dialog md-dialog-content .md-prompt-input-container{width:100%;box-sizing:border-box}md-dialog .md-actions,md-dialog md-dialog-actions{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-ordinal-group:3;-webkit-order:2;order:2;box-sizing:border-box;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end;margin-bottom:0;padding-right:8px;padding-left:16px;min-height:52px;overflow:hidden}[dir=rtl] md-dialog .md-actions,[dir=rtl] md-dialog md-dialog-actions{padding-right:16px;padding-left:8px}md-dialog .md-actions .md-button,md-dialog md-dialog-actions .md-button{margin:8px 0 8px 8px}[dir=rtl] md-dialog .md-actions .md-button,[dir=rtl] md-dialog md-dialog-actions .md-button{margin-left:0;margin-right:8px}md-dialog.md-content-overflow .md-actions,md-dialog.md-content-overflow md-dialog-actions{border-top-width:1px;border-top-style:solid}@media screen and (-ms-high-contrast:active){md-dialog{border:1px solid #fff}}@media (max-width:959px){md-dialog.md-dialog-fullscreen{min-height:100%;min-width:100%;border-radius:0}}md-divider{display:block;border-top-width:1px;border-top-style:solid;margin:0}md-divider[md-inset]{margin-left:80px}[dir=rtl] md-divider[md-inset]{margin-left:auto;margin-right:80px}.layout-gt-lg-row>md-divider,.layout-gt-md-row>md-divider,.layout-gt-sm-row>md-divider,.layout-gt-xs-row>md-divider,.layout-lg-row>md-divider,.layout-md-row>md-divider,.layout-row>md-divider,.layout-sm-row>md-divider,.layout-xl-row>md-divider,.layout-xs-row>md-divider{border-top-width:0;border-right-width:1px;border-right-style:solid}md-fab-speed-dial{position:relative;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;z-index:20}md-fab-speed-dial.md-fab-bottom-right{top:auto;right:20px;bottom:20px;left:auto;position:absolute}md-fab-speed-dial.md-fab-bottom-left{top:auto;right:auto;bottom:20px;left:20px;position:absolute}md-fab-speed-dial.md-fab-top-right{top:20px;right:20px;bottom:auto;left:auto;position:absolute}md-fab-speed-dial.md-fab-top-left{top:20px;right:auto;bottom:auto;left:20px;position:absolute}md-fab-speed-dial:not(.md-hover-full){pointer-events:none}md-fab-speed-dial:not(.md-hover-full) .md-fab-action-item,md-fab-speed-dial:not(.md-hover-full).md-is-open,md-fab-speed-dial:not(.md-hover-full) md-fab-trigger{pointer-events:auto}md-fab-speed-dial ._md-css-variables{z-index:20}md-fab-speed-dial.md-is-open .md-fab-action-item{-webkit-box-align:center;-webkit-align-items:center;align-items:center}md-fab-speed-dial md-fab-actions{display:-webkit-box;display:-webkit-flex;display:flex;height:auto}md-fab-speed-dial md-fab-actions .md-fab-action-item{-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2)}md-fab-speed-dial.md-down{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}md-fab-speed-dial.md-down md-fab-trigger{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}md-fab-speed-dial.md-down md-fab-actions{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}md-fab-speed-dial.md-up{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}md-fab-speed-dial.md-up md-fab-trigger{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}md-fab-speed-dial.md-up md-fab-actions{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-webkit-flex-direction:column-reverse;flex-direction:column-reverse;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}md-fab-speed-dial.md-left{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}md-fab-speed-dial.md-left md-fab-trigger{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}md-fab-speed-dial.md-left md-fab-actions{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-box-ordinal-group:2;-webkit-order:1;order:1}md-fab-speed-dial.md-left md-fab-actions .md-fab-action-item{-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2)}md-fab-speed-dial.md-right{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}md-fab-speed-dial.md-right md-fab-trigger{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}md-fab-speed-dial.md-right md-fab-actions{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}md-fab-speed-dial.md-right md-fab-actions .md-fab-action-item{-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2)}md-fab-speed-dial.md-fling-remove .md-fab-action-item>*,md-fab-speed-dial.md-scale-remove .md-fab-action-item>*{visibility:hidden}md-fab-speed-dial.md-fling .md-fab-action-item{opacity:1}md-fab-speed-dial.md-fling.md-animations-waiting .md-fab-action-item{opacity:0;-webkit-transition-duration:0s;transition-duration:0s}md-fab-speed-dial.md-scale .md-fab-action-item{-webkit-transform:scale(0);transform:scale(0);-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2);-webkit-transition-duration:.14286s;transition-duration:.14286s}md-fab-toolbar{display:block}md-fab-toolbar.md-fab-bottom-right{top:auto;right:20px;bottom:20px;left:auto;position:absolute}md-fab-toolbar.md-fab-bottom-left{top:auto;right:auto;bottom:20px;left:20px;position:absolute}md-fab-toolbar.md-fab-top-right{top:20px;right:20px;bottom:auto;left:auto;position:absolute}md-fab-toolbar.md-fab-top-left{top:20px;right:auto;bottom:auto;left:20px;position:absolute}md-fab-toolbar .md-fab-toolbar-wrapper{display:block;position:relative;overflow:hidden;height:68px}md-fab-toolbar md-fab-trigger{position:absolute;z-index:20}md-fab-toolbar md-fab-trigger button{overflow:visible!important}md-fab-toolbar md-fab-trigger .md-fab-toolbar-background{display:block;position:absolute;z-index:21;opacity:1;-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2)}md-fab-toolbar md-fab-trigger md-icon{position:relative;z-index:22;opacity:1;-webkit-transition:all .2s ease-in;transition:all .2s ease-in}md-fab-toolbar.md-left md-fab-trigger{right:0}[dir=rtl] md-fab-toolbar.md-left md-fab-trigger{right:auto;left:0}md-fab-toolbar.md-left .md-toolbar-tools{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-webkit-flex-direction:row-reverse;flex-direction:row-reverse}md-fab-toolbar.md-left .md-toolbar-tools>.md-button:first-child{margin-right:.6rem}[dir=rtl] md-fab-toolbar.md-left .md-toolbar-tools>.md-button:first-child{margin-right:auto;margin-left:.6rem}md-fab-toolbar.md-left .md-toolbar-tools>.md-button:first-child{margin-left:-.8rem}[dir=rtl] md-fab-toolbar.md-left .md-toolbar-tools>.md-button:first-child{margin-left:auto;margin-right:-.8rem}md-fab-toolbar.md-left .md-toolbar-tools>.md-button:last-child{margin-right:8px}[dir=rtl] md-fab-toolbar.md-left .md-toolbar-tools>.md-button:last-child{margin-right:auto;margin-left:8px}md-fab-toolbar.md-right md-fab-trigger{left:0}[dir=rtl] md-fab-toolbar.md-right md-fab-trigger{left:auto;right:0}md-fab-toolbar.md-right .md-toolbar-tools{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}md-fab-toolbar md-toolbar{background-color:transparent!important;pointer-events:none;z-index:23}md-fab-toolbar md-toolbar .md-toolbar-tools{padding:0 20px;margin-top:3px}md-fab-toolbar md-toolbar .md-fab-action-item{opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2);-webkit-transition-duration:.15s;transition-duration:.15s}md-fab-toolbar.md-is-open md-fab-trigger>button{box-shadow:none}md-fab-toolbar.md-is-open md-fab-trigger>button md-icon{opacity:0}md-fab-toolbar.md-is-open .md-fab-action-item{opacity:1;-webkit-transform:scale(1);transform:scale(1)}md-grid-list{display:block;position:relative}md-grid-list,md-grid-list md-grid-tile,md-grid-list md-grid-tile-footer,md-grid-list md-grid-tile-header,md-grid-list md-grid-tile>figure{box-sizing:border-box}md-grid-list md-grid-tile{display:block;position:absolute}md-grid-list md-grid-tile figure{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;height:100%;top:0;bottom:0;padding:0;margin:0}md-grid-list md-grid-tile figure,md-grid-list md-grid-tile md-grid-tile-footer,md-grid-list md-grid-tile md-grid-tile-header{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;position:absolute;right:0;left:0}md-grid-list md-grid-tile md-grid-tile-footer,md-grid-list md-grid-tile md-grid-tile-header{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;height:48px;color:#fff;background:rgba(0,0,0,.18);overflow:hidden}md-grid-list md-grid-tile md-grid-tile-footer h3,md-grid-list md-grid-tile md-grid-tile-footer h4,md-grid-list md-grid-tile md-grid-tile-header h3,md-grid-list md-grid-tile md-grid-tile-header h4{font-weight:400;margin:0 0 0 16px}md-grid-list md-grid-tile md-grid-tile-footer h3,md-grid-list md-grid-tile md-grid-tile-header h3{font-size:14px}md-grid-list md-grid-tile md-grid-tile-footer h4,md-grid-list md-grid-tile md-grid-tile-header h4{font-size:12px}md-grid-list md-grid-tile md-grid-tile-header{top:0}md-grid-list md-grid-tile md-grid-tile-footer{bottom:0}@media screen and (-ms-high-contrast:active){md-grid-tile{border:1px solid #fff}md-grid-tile-footer{border-top:1px solid #fff}}md-icon{margin:auto;background-repeat:no-repeat;display:inline-block;vertical-align:middle;fill:currentColor;height:24px;width:24px;min-height:24px;min-width:24px}md-icon svg{pointer-events:none;display:block}md-icon[md-font-icon]{line-height:24px;width:auto}md-input-container{display:inline-block;position:relative;padding:2px;margin:18px 0;vertical-align:middle}md-input-container:after{content:"";display:table;clear:both}md-input-container.md-block{display:block}md-input-container .md-errors-spacer{float:right;min-height:24px;min-width:1px}[dir=rtl] md-input-container .md-errors-spacer{float:left}md-input-container>md-icon{position:absolute;top:8px;left:2px;right:auto}[dir=rtl] md-input-container>md-icon{left:auto;right:2px}md-input-container input[type=color],md-input-container input[type=date],md-input-container input[type=datetime-local],md-input-container input[type=datetime],md-input-container input[type=email],md-input-container input[type=month],md-input-container input[type=number],md-input-container input[type=password],md-input-container input[type=search],md-input-container input[type=tel],md-input-container input[type=text],md-input-container input[type=time],md-input-container input[type=url],md-input-container input[type=week],md-input-container textarea{-moz-appearance:none;-webkit-appearance:none}md-input-container input[type=date],md-input-container input[type=datetime-local],md-input-container input[type=month],md-input-container input[type=time],md-input-container input[type=week]{min-height:26px}md-input-container textarea{resize:none;overflow:hidden}md-input-container textarea.md-input{min-height:26px;-ms-flex-preferred-size:auto}md-input-container textarea[md-no-autogrow]{height:auto;overflow:auto}md-input-container label:not(.md-container-ignore){position:absolute;bottom:100%;left:0;right:auto}[dir=rtl] md-input-container label:not(.md-container-ignore){left:auto;right:0}md-input-container label:not(.md-container-ignore).md-required:after{content:" *";font-size:13px;vertical-align:top}md-input-container .md-placeholder,md-input-container label:not(.md-no-float):not(.md-container-ignore){overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%;-webkit-box-ordinal-group:2;-webkit-order:1;order:1;pointer-events:none;-webkit-font-smoothing:antialiased;padding-left:3px;padding-right:0;z-index:1;-webkit-transform:translate3d(0,28px,0) scale(1);transform:translate3d(0,28px,0) scale(1);-webkit-transition:-webkit-transform .4s cubic-bezier(.25,.8,.25,1);transition:-webkit-transform .4s cubic-bezier(.25,.8,.25,1);transition:transform .4s cubic-bezier(.25,.8,.25,1);transition:transform .4s cubic-bezier(.25,.8,.25,1),-webkit-transform .4s cubic-bezier(.25,.8,.25,1);max-width:100%;-webkit-transform-origin:left top;transform-origin:left top}[dir=rtl] md-input-container .md-placeholder,[dir=rtl] md-input-container label:not(.md-no-float):not(.md-container-ignore){padding-left:0;padding-right:3px;-webkit-transform-origin:right top;transform-origin:right top}md-input-container .md-placeholder{position:absolute;top:0;opacity:0;-webkit-transition-property:opacity,-webkit-transform;transition-property:opacity,-webkit-transform;transition-property:opacity,transform;transition-property:opacity,transform,-webkit-transform;-webkit-transform:translate3d(0,30px,0);transform:translate3d(0,30px,0)}md-input-container.md-input-focused .md-placeholder{opacity:1;-webkit-transform:translate3d(0,24px,0);transform:translate3d(0,24px,0)}md-input-container.md-input-has-value .md-placeholder{-webkit-transition:none;transition:none;opacity:0}md-input-container:not(.md-input-has-value) input:not(:focus),md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-ampm-field,md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-day-field,md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-hour-field,md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-millisecond-field,md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-minute-field,md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-month-field,md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-second-field,md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-text,md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-week-field,md-input-container:not(.md-input-has-value) input:not(:focus)::-webkit-datetime-edit-year-field{color:transparent}md-input-container .md-input{-webkit-box-ordinal-group:3;-webkit-order:2;order:2;display:block;margin-top:0;background:none;padding:2px 2px 1px;border-width:0 0 1px;line-height:26px;height:30px;-ms-flex-preferred-size:26px;border-radius:0;border-style:solid;width:100%;box-sizing:border-box;float:left}[dir=rtl] md-input-container .md-input{float:right}md-input-container .md-input:focus{outline:none}md-input-container .md-input:invalid{outline:none;box-shadow:none}md-input-container .md-input.md-no-flex{-webkit-box-flex:0!important;-webkit-flex:none!important;flex:none!important}md-input-container .md-char-counter{text-align:right;padding-right:2px;padding-left:0}[dir=rtl] md-input-container .md-char-counter{text-align:left;padding-right:0;padding-left:2px}md-input-container .md-input-messages-animation{position:relative;-webkit-box-ordinal-group:5;-webkit-order:4;order:4;overflow:hidden;clear:left}[dir=rtl] md-input-container .md-input-messages-animation{clear:right}md-input-container .md-input-messages-animation.ng-enter .md-input-message-animation{opacity:0;margin-top:-100px}md-input-container .md-char-counter,md-input-container .md-input-message-animation{font-size:12px;line-height:14px;overflow:hidden;-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2);opacity:1;margin-top:0;padding-top:5px}md-input-container .md-char-counter:not(.md-char-counter),md-input-container .md-input-message-animation:not(.md-char-counter){padding-right:5px;padding-left:0}[dir=rtl] md-input-container .md-char-counter:not(.md-char-counter),[dir=rtl] md-input-container .md-input-message-animation:not(.md-char-counter){padding-right:0;padding-left:5px}md-input-container .md-input-message-animation.ng-enter,md-input-container .md-input-message-animation:not(.ng-animate),md-input-container:not(.md-input-invalid) .md-auto-hide .md-input-message-animation{opacity:0;margin-top:-100px}md-input-container.md-input-focused label:not(.md-no-float),md-input-container.md-input-has-placeholder label:not(.md-no-float),md-input-container.md-input-has-value label:not(.md-no-float){-webkit-transform:translate3d(0,6px,0) scale(.75);transform:translate3d(0,6px,0) scale(.75);-webkit-transition:width .4s cubic-bezier(.25,.8,.25,1),-webkit-transform .4s cubic-bezier(.25,.8,.25,1);transition:width .4s cubic-bezier(.25,.8,.25,1),-webkit-transform .4s cubic-bezier(.25,.8,.25,1);transition:transform .4s cubic-bezier(.25,.8,.25,1),width .4s cubic-bezier(.25,.8,.25,1);transition:transform .4s cubic-bezier(.25,.8,.25,1),width .4s cubic-bezier(.25,.8,.25,1),-webkit-transform .4s cubic-bezier(.25,.8,.25,1)}md-input-container.md-input-has-value label{-webkit-transition:none;transition:none}md-input-container.md-input-focused .md-input,md-input-container.md-input-resized .md-input,md-input-container .md-input.ng-invalid.ng-dirty{padding-bottom:0;border-width:0 0 2px}[disabled] md-input-container .md-input,md-input-container .md-input[disabled]{background-position:bottom -1px left 0;background-size:4px 1px;background-repeat:repeat-x}md-input-container.md-icon-float{-webkit-transition:margin-top .4s cubic-bezier(.25,.8,.25,1);transition:margin-top .4s cubic-bezier(.25,.8,.25,1)}md-input-container.md-icon-float>label{pointer-events:none;position:absolute}md-input-container.md-icon-float>md-icon{top:8px;left:2px;right:auto}[dir=rtl] md-input-container.md-icon-float>md-icon{left:auto;right:2px}md-input-container.md-icon-left>label .md-placeholder,md-input-container.md-icon-left>label:not(.md-no-float):not(.md-container-ignore),md-input-container.md-icon-right>label .md-placeholder,md-input-container.md-icon-right>label:not(.md-no-float):not(.md-container-ignore){width:calc(100% - 36px - 18px)}md-input-container.md-icon-left{padding-left:36px;padding-right:0}[dir=rtl] md-input-container.md-icon-left{padding-left:0;padding-right:36px}md-input-container.md-icon-left>label{left:36px;right:auto}[dir=rtl] md-input-container.md-icon-left>label{left:auto;right:36px}md-input-container.md-icon-right{padding-left:0;padding-right:36px}[dir=rtl] md-input-container.md-icon-right{padding-left:36px;padding-right:0}md-input-container.md-icon-right>md-icon:last-of-type{margin:0;right:2px;left:auto}[dir=rtl] md-input-container.md-icon-right>md-icon:last-of-type{right:auto;left:2px}md-input-container.md-icon-left.md-icon-right{padding-left:36px;padding-right:36px}md-input-container.md-icon-left.md-icon-right>label .md-placeholder,md-input-container.md-icon-left.md-icon-right>label:not(.md-no-float):not(.md-container-ignore){width:calc(100% - 72px)}.md-resize-wrapper{position:relative}.md-resize-wrapper:after{content:"";display:table;clear:both}.md-resize-handle{position:absolute;bottom:-5px;left:0;height:10px;background:transparent;width:100%;cursor:ns-resize}@media screen and (-ms-high-contrast:active){md-input-container.md-default-theme>md-icon{fill:#fff}}md-list{display:block;padding:8px 0}md-list .md-subheader{font-size:14px;font-weight:500;letter-spacing:.01em;line-height:1.2em}md-list.md-dense md-list-item,md-list.md-dense md-list-item .md-list-item-inner{min-height:48px}md-list.md-dense md-list-item .md-list-item-inner:before,md-list.md-dense md-list-item:before{content:"";min-height:48px;visibility:hidden;display:inline-block}md-list.md-dense md-list-item .md-list-item-inner md-icon:first-child,md-list.md-dense md-list-item md-icon:first-child{width:20px;height:20px}md-list.md-dense md-list-item .md-list-item-inner>md-icon:first-child:not(.md-avatar-icon),md-list.md-dense md-list-item>md-icon:first-child:not(.md-avatar-icon){margin-right:36px}[dir=rtl] md-list.md-dense md-list-item .md-list-item-inner>md-icon:first-child:not(.md-avatar-icon),[dir=rtl] md-list.md-dense md-list-item>md-icon:first-child:not(.md-avatar-icon){margin-right:auto;margin-left:36px}md-list.md-dense md-list-item .md-avatar,md-list.md-dense md-list-item .md-avatar-icon,md-list.md-dense md-list-item .md-list-item-inner .md-avatar,md-list.md-dense md-list-item .md-list-item-inner .md-avatar-icon{margin-right:20px}[dir=rtl] md-list.md-dense md-list-item .md-avatar,[dir=rtl] md-list.md-dense md-list-item .md-avatar-icon,[dir=rtl] md-list.md-dense md-list-item .md-list-item-inner .md-avatar,[dir=rtl] md-list.md-dense md-list-item .md-list-item-inner .md-avatar-icon{margin-right:auto;margin-left:20px}md-list.md-dense md-list-item .md-avatar,md-list.md-dense md-list-item .md-list-item-inner .md-avatar{-webkit-box-flex:0;-webkit-flex:none;flex:none;width:36px;height:36px}md-list.md-dense md-list-item.md-2-line .md-list-item-text.md-offset,md-list.md-dense md-list-item.md-2-line>.md-no-style .md-list-item-text.md-offset,md-list.md-dense md-list-item.md-3-line .md-list-item-text.md-offset,md-list.md-dense md-list-item.md-3-line>.md-no-style .md-list-item-text.md-offset{margin-left:56px}[dir=rtl] md-list.md-dense md-list-item.md-2-line .md-list-item-text.md-offset,[dir=rtl] md-list.md-dense md-list-item.md-2-line>.md-no-style .md-list-item-text.md-offset,[dir=rtl] md-list.md-dense md-list-item.md-3-line .md-list-item-text.md-offset,[dir=rtl] md-list.md-dense md-list-item.md-3-line>.md-no-style .md-list-item-text.md-offset{margin-left:auto;margin-right:56px}md-list.md-dense md-list-item.md-2-line .md-list-item-text h3,md-list.md-dense md-list-item.md-2-line .md-list-item-text h4,md-list.md-dense md-list-item.md-2-line .md-list-item-text p,md-list.md-dense md-list-item.md-2-line>.md-no-style .md-list-item-text h3,md-list.md-dense md-list-item.md-2-line>.md-no-style .md-list-item-text h4,md-list.md-dense md-list-item.md-2-line>.md-no-style .md-list-item-text p,md-list.md-dense md-list-item.md-3-line .md-list-item-text h3,md-list.md-dense md-list-item.md-3-line .md-list-item-text h4,md-list.md-dense md-list-item.md-3-line .md-list-item-text p,md-list.md-dense md-list-item.md-3-line>.md-no-style .md-list-item-text h3,md-list.md-dense md-list-item.md-3-line>.md-no-style .md-list-item-text h4,md-list.md-dense md-list-item.md-3-line>.md-no-style .md-list-item-text p{line-height:1.05;font-size:12px}md-list.md-dense md-list-item.md-2-line .md-list-item-text h3,md-list.md-dense md-list-item.md-2-line>.md-no-style .md-list-item-text h3,md-list.md-dense md-list-item.md-3-line .md-list-item-text h3,md-list.md-dense md-list-item.md-3-line>.md-no-style .md-list-item-text h3{font-size:13px}md-list.md-dense md-list-item.md-2-line,md-list.md-dense md-list-item.md-2-line>.md-no-style{min-height:60px}md-list.md-dense md-list-item.md-2-line:before,md-list.md-dense md-list-item.md-2-line>.md-no-style:before{content:"";min-height:60px;visibility:hidden;display:inline-block}md-list.md-dense md-list-item.md-2-line .md-avatar-icon,md-list.md-dense md-list-item.md-2-line>.md-avatar,md-list.md-dense md-list-item.md-2-line>.md-no-style .md-avatar-icon,md-list.md-dense md-list-item.md-2-line>.md-no-style>.md-avatar{margin-top:12px}md-list.md-dense md-list-item.md-3-line,md-list.md-dense md-list-item.md-3-line>.md-no-style{min-height:76px}md-list.md-dense md-list-item.md-3-line:before,md-list.md-dense md-list-item.md-3-line>.md-no-style:before{content:"";min-height:76px;visibility:hidden;display:inline-block}md-list.md-dense md-list-item.md-3-line>.md-avatar,md-list.md-dense md-list-item.md-3-line>.md-no-style>.md-avatar,md-list.md-dense md-list-item.md-3-line>.md-no-style>md-icon:first-child,md-list.md-dense md-list-item.md-3-line>md-icon:first-child{margin-top:16px}md-list-item{position:relative}md-list-item.md-proxy-focus.md-focused .md-no-style{-webkit-transition:background-color .15s linear;transition:background-color .15s linear}md-list-item._md-button-wrap{position:relative}md-list-item._md-button-wrap>div.md-button:first-child{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start;padding:0 16px;margin:0;font-weight:400;text-align:left;border:medium none}[dir=rtl] md-list-item._md-button-wrap>div.md-button:first-child{text-align:right}md-list-item._md-button-wrap>div.md-button:first-child>.md-button:first-child{position:absolute;top:0;left:0;height:100%;margin:0;padding:0}md-list-item._md-button-wrap>div.md-button:first-child .md-list-item-inner{width:100%;min-height:inherit}md-list-item.md-no-proxy,md-list-item .md-no-style{position:relative;padding:0 16px;-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto}md-list-item.md-no-proxy.md-button,md-list-item .md-no-style.md-button{font-size:inherit;height:inherit;text-align:left;text-transform:none;width:100%;white-space:normal;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:inherit;flex-direction:inherit;-webkit-box-align:inherit;-webkit-align-items:inherit;align-items:inherit;border-radius:0;margin:0}[dir=rtl] md-list-item.md-no-proxy.md-button,[dir=rtl] md-list-item .md-no-style.md-button{text-align:right}md-list-item.md-no-proxy.md-button>.md-ripple-container,md-list-item .md-no-style.md-button>.md-ripple-container{border-radius:0}md-list-item.md-no-proxy:focus,md-list-item .md-no-style:focus{outline:none}md-list-item.md-clickable:hover{cursor:pointer}md-list-item md-divider{position:absolute;bottom:0;left:0;width:100%}[dir=rtl] md-list-item md-divider{left:auto;right:0}md-list-item md-divider[md-inset]{left:72px;width:calc(100% - 72px);margin:0!important}[dir=rtl] md-list-item md-divider[md-inset]{left:auto;right:72px}md-list-item,md-list-item .md-list-item-inner{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start;-webkit-box-align:center;-webkit-align-items:center;align-items:center;min-height:48px;height:auto}md-list-item .md-list-item-inner:before,md-list-item:before{content:"";min-height:48px;visibility:hidden;display:inline-block}md-list-item .md-list-item-inner>div.md-primary>md-icon:not(.md-avatar-icon),md-list-item .md-list-item-inner>div.md-secondary>md-icon:not(.md-avatar-icon),md-list-item .md-list-item-inner>md-icon.md-secondary:not(.md-avatar-icon),md-list-item .md-list-item-inner>md-icon:first-child:not(.md-avatar-icon),md-list-item>div.md-primary>md-icon:not(.md-avatar-icon),md-list-item>div.md-secondary>md-icon:not(.md-avatar-icon),md-list-item>md-icon.md-secondary:not(.md-avatar-icon),md-list-item>md-icon:first-child:not(.md-avatar-icon){width:24px;margin-top:16px;margin-bottom:12px;box-sizing:content-box}md-list-item .md-list-item-inner>div.md-primary>md-checkbox,md-list-item .md-list-item-inner>div.md-secondary>md-checkbox,md-list-item .md-list-item-inner>md-checkbox,md-list-item .md-list-item-inner md-checkbox.md-secondary,md-list-item>div.md-primary>md-checkbox,md-list-item>div.md-secondary>md-checkbox,md-list-item>md-checkbox,md-list-item md-checkbox.md-secondary{-webkit-align-self:center;-ms-grid-row-align:center;align-self:center}md-list-item .md-list-item-inner>div.md-primary>md-checkbox .md-label,md-list-item .md-list-item-inner>div.md-secondary>md-checkbox .md-label,md-list-item .md-list-item-inner>md-checkbox .md-label,md-list-item .md-list-item-inner md-checkbox.md-secondary .md-label,md-list-item>div.md-primary>md-checkbox .md-label,md-list-item>div.md-secondary>md-checkbox .md-label,md-list-item>md-checkbox .md-label,md-list-item md-checkbox.md-secondary .md-label{display:none}md-list-item .md-list-item-inner>md-icon:first-child:not(.md-avatar-icon),md-list-item>md-icon:first-child:not(.md-avatar-icon){margin-right:32px}[dir=rtl] md-list-item .md-list-item-inner>md-icon:first-child:not(.md-avatar-icon),[dir=rtl] md-list-item>md-icon:first-child:not(.md-avatar-icon){margin-right:auto;margin-left:32px}md-list-item .md-avatar,md-list-item .md-avatar-icon,md-list-item .md-list-item-inner .md-avatar,md-list-item .md-list-item-inner .md-avatar-icon{margin-top:8px;margin-bottom:8px;margin-right:16px;border-radius:50%;box-sizing:content-box}[dir=rtl] md-list-item .md-avatar,[dir=rtl] md-list-item .md-avatar-icon,[dir=rtl] md-list-item .md-list-item-inner .md-avatar,[dir=rtl] md-list-item .md-list-item-inner .md-avatar-icon{margin-right:auto;margin-left:16px}md-list-item .md-avatar,md-list-item .md-list-item-inner .md-avatar{-webkit-box-flex:0;-webkit-flex:none;flex:none;width:40px;height:40px}md-list-item .md-avatar-icon,md-list-item .md-list-item-inner .md-avatar-icon{padding:8px}md-list-item .md-avatar-icon svg,md-list-item .md-list-item-inner .md-avatar-icon svg{width:24px;height:24px}md-list-item .md-list-item-inner>md-checkbox,md-list-item>md-checkbox{width:24px;margin-left:3px;margin-right:29px;margin-top:16px}[dir=rtl] md-list-item .md-list-item-inner>md-checkbox,[dir=rtl] md-list-item>md-checkbox{margin-left:29px;margin-right:3px}md-list-item .md-list-item-inner .md-secondary-container,md-list-item .md-secondary-container{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-flex-shrink:0;flex-shrink:0;margin:auto;margin-right:0;margin-left:auto}[dir=rtl] md-list-item .md-list-item-inner .md-secondary-container,[dir=rtl] md-list-item .md-secondary-container{margin-right:auto;margin-left:0}md-list-item .md-list-item-inner .md-secondary-container .md-button:last-of-type,md-list-item .md-list-item-inner .md-secondary-container .md-icon-button:last-of-type,md-list-item .md-secondary-container .md-button:last-of-type,md-list-item .md-secondary-container .md-icon-button:last-of-type{margin-right:0}[dir=rtl] md-list-item .md-list-item-inner .md-secondary-container .md-button:last-of-type,[dir=rtl] md-list-item .md-list-item-inner .md-secondary-container .md-icon-button:last-of-type,[dir=rtl] md-list-item .md-secondary-container .md-button:last-of-type,[dir=rtl] md-list-item .md-secondary-container .md-icon-button:last-of-type{margin-right:auto;margin-left:0}md-list-item .md-list-item-inner .md-secondary-container md-checkbox,md-list-item .md-secondary-container md-checkbox{margin-top:0;margin-bottom:0}md-list-item .md-list-item-inner .md-secondary-container md-checkbox:last-child,md-list-item .md-secondary-container md-checkbox:last-child{width:24px;margin-right:0}[dir=rtl] md-list-item .md-list-item-inner .md-secondary-container md-checkbox:last-child,[dir=rtl] md-list-item .md-secondary-container md-checkbox:last-child{margin-right:auto;margin-left:0}md-list-item .md-list-item-inner .md-secondary-container md-switch,md-list-item .md-secondary-container md-switch{margin-top:0;margin-bottom:0;margin-right:-6px}[dir=rtl] md-list-item .md-list-item-inner .md-secondary-container md-switch,[dir=rtl] md-list-item .md-secondary-container md-switch{margin-right:auto;margin-left:-6px}md-list-item .md-list-item-inner>.md-list-item-inner>p,md-list-item .md-list-item-inner>p,md-list-item>.md-list-item-inner>p,md-list-item>p{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;margin:0}md-list-item.md-2-line,md-list-item.md-2-line>.md-no-style,md-list-item.md-3-line,md-list-item.md-3-line>.md-no-style{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}md-list-item.md-2-line.md-long-text,md-list-item.md-2-line>.md-no-style.md-long-text,md-list-item.md-3-line.md-long-text,md-list-item.md-3-line>.md-no-style.md-long-text{margin-top:8px;margin-bottom:8px}md-list-item.md-2-line .md-list-item-text,md-list-item.md-2-line>.md-no-style .md-list-item-text,md-list-item.md-3-line .md-list-item-text,md-list-item.md-3-line>.md-no-style .md-list-item-text{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;margin:auto;text-overflow:ellipsis;overflow:hidden}md-list-item.md-2-line .md-list-item-text.md-offset,md-list-item.md-2-line>.md-no-style .md-list-item-text.md-offset,md-list-item.md-3-line .md-list-item-text.md-offset,md-list-item.md-3-line>.md-no-style .md-list-item-text.md-offset{margin-left:56px}[dir=rtl] md-list-item.md-2-line .md-list-item-text.md-offset,[dir=rtl] md-list-item.md-2-line>.md-no-style .md-list-item-text.md-offset,[dir=rtl] md-list-item.md-3-line .md-list-item-text.md-offset,[dir=rtl] md-list-item.md-3-line>.md-no-style .md-list-item-text.md-offset{margin-left:auto;margin-right:56px}md-list-item.md-2-line .md-list-item-text h3,md-list-item.md-2-line>.md-no-style .md-list-item-text h3,md-list-item.md-3-line .md-list-item-text h3,md-list-item.md-3-line>.md-no-style .md-list-item-text h3{font-size:16px;font-weight:400;letter-spacing:.01em;margin:0;line-height:1.2em;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}md-list-item.md-2-line .md-list-item-text h4,md-list-item.md-2-line>.md-no-style .md-list-item-text h4,md-list-item.md-3-line .md-list-item-text h4,md-list-item.md-3-line>.md-no-style .md-list-item-text h4{font-size:14px;letter-spacing:.01em;margin:3px 0 1px;font-weight:400;line-height:1.2em;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}md-list-item.md-2-line .md-list-item-text p,md-list-item.md-2-line>.md-no-style .md-list-item-text p,md-list-item.md-3-line .md-list-item-text p,md-list-item.md-3-line>.md-no-style .md-list-item-text p{font-size:14px;font-weight:500;letter-spacing:.01em;margin:0;line-height:1.6em}md-list-item.md-2-line,md-list-item.md-2-line>.md-no-style{height:auto;min-height:72px}md-list-item.md-2-line:before,md-list-item.md-2-line>.md-no-style:before{content:"";min-height:72px;visibility:hidden;display:inline-block}md-list-item.md-2-line .md-avatar-icon,md-list-item.md-2-line>.md-avatar,md-list-item.md-2-line>.md-no-style .md-avatar-icon,md-list-item.md-2-line>.md-no-style>.md-avatar{margin-top:12px}md-list-item.md-2-line>.md-no-style>md-icon:first-child,md-list-item.md-2-line>md-icon:first-child{-webkit-align-self:flex-start;align-self:flex-start}md-list-item.md-2-line .md-list-item-text,md-list-item.md-2-line>.md-no-style .md-list-item-text{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto}md-list-item.md-3-line,md-list-item.md-3-line>.md-no-style{height:auto;min-height:88px}md-list-item.md-3-line:before,md-list-item.md-3-line>.md-no-style:before{content:"";min-height:88px;visibility:hidden;display:inline-block}md-list-item.md-3-line>.md-avatar,md-list-item.md-3-line>.md-no-style>.md-avatar,md-list-item.md-3-line>.md-no-style>md-icon:first-child,md-list-item.md-3-line>md-icon:first-child{margin-top:16px}.md-open-menu-container{position:fixed;left:0;top:0;z-index:100;opacity:0;border-radius:2px}.md-open-menu-container md-menu-divider{margin-top:4px;margin-bottom:4px;height:1px;min-height:1px;max-height:1px;width:100%}.md-open-menu-container md-menu-content>*{opacity:0}.md-open-menu-container:not(.md-clickable){pointer-events:none}.md-open-menu-container.md-active{opacity:1;-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1);-webkit-transition-duration:.2s;transition-duration:.2s}.md-open-menu-container.md-active>md-menu-content>*{opacity:1;-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2);-webkit-transition-duration:.2s;transition-duration:.2s;-webkit-transition-delay:.1s;transition-delay:.1s}.md-open-menu-container.md-leave{opacity:0;-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2);-webkit-transition-duration:.25s;transition-duration:.25s}md-menu-content{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;padding:8px 0;max-height:304px;overflow-y:auto}md-menu-content.md-dense{max-height:208px}md-menu-content.md-dense md-menu-item{height:32px;min-height:0}md-menu-item{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;min-height:48px;height:48px;-webkit-align-content:center;align-content:center;-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}md-menu-item>*{width:100%;margin:auto 0;padding-left:16px;padding-right:16px}md-menu-item>a.md-button{padding-top:5px}md-menu-item>.md-button{text-align:left;display:inline-block;border-radius:0;margin:auto 0;font-size:15px;text-transform:none;font-weight:400;height:100%;padding-left:16px;padding-right:16px;width:100%}md-menu-item>.md-button::-moz-focus-inner{padding:0;border:0}[dir=rtl] md-menu-item>.md-button{text-align:right}md-menu-item>.md-button md-icon{margin:auto 16px auto 0}[dir=rtl] md-menu-item>.md-button md-icon{margin:auto 0 auto 16px}md-menu-item>.md-button p{display:inline-block;margin:auto}md-menu-item>.md-button span{margin-top:auto;margin-bottom:auto}md-menu-item>.md-button .md-ripple-container{border-radius:inherit}md-toolbar .md-menu{height:auto;margin:auto;padding:0}@media (max-width:959px){md-menu-content{min-width:112px}md-menu-content[width="3"]{min-width:168px}md-menu-content[width="4"]{min-width:224px}md-menu-content[width="5"]{min-width:280px}md-menu-content[width="6"]{min-width:336px}md-menu-content[width="7"]{min-width:392px}}@media (min-width:960px){md-menu-content{min-width:96px}md-menu-content[width="3"]{min-width:192px}md-menu-content[width="4"]{min-width:256px}md-menu-content[width="5"]{min-width:320px}md-menu-content[width="6"]{min-width:384px}md-menu-content[width="7"]{min-width:448px}}md-toolbar.md-menu-toolbar h2.md-toolbar-tools{line-height:1rem;height:auto;padding:28px;padding-bottom:12px}md-toolbar.md-has-open-menu{position:relative;z-index:100}md-menu-bar{padding:0 20px;display:block;position:relative;z-index:2}md-menu-bar .md-menu{display:inline-block;padding:0;position:relative}md-menu-bar button{font-size:14px;padding:0 10px;margin:0;border:0;background-color:transparent;height:40px}md-menu-bar md-backdrop.md-menu-backdrop{z-index:-2}md-menu-content.md-menu-bar-menu.md-dense{max-height:none;padding:16px 0}md-menu-content.md-menu-bar-menu.md-dense md-menu-item.md-indent{position:relative}md-menu-content.md-menu-bar-menu.md-dense md-menu-item.md-indent>md-icon{position:absolute;padding:0;width:24px;top:6px;left:24px}[dir=rtl] md-menu-content.md-menu-bar-menu.md-dense md-menu-item.md-indent>md-icon{left:auto;right:24px}md-menu-content.md-menu-bar-menu.md-dense md-menu-item.md-indent .md-menu>.md-button,md-menu-content.md-menu-bar-menu.md-dense md-menu-item.md-indent>.md-button{padding:0 32px 0 64px}[dir=rtl] md-menu-content.md-menu-bar-menu.md-dense md-menu-item.md-indent .md-menu>.md-button,[dir=rtl] md-menu-content.md-menu-bar-menu.md-dense md-menu-item.md-indent>.md-button{padding:0 64px 0 32px}md-menu-content.md-menu-bar-menu.md-dense .md-button{min-height:0;height:32px}md-menu-content.md-menu-bar-menu.md-dense .md-button span{float:left}[dir=rtl] md-menu-content.md-menu-bar-menu.md-dense .md-button span{float:right}md-menu-content.md-menu-bar-menu.md-dense .md-button span.md-alt-text{float:right;margin:0 8px}[dir=rtl] md-menu-content.md-menu-bar-menu.md-dense .md-button span.md-alt-text{float:left}md-menu-content.md-menu-bar-menu.md-dense md-menu-divider{margin:8px 0}md-menu-content.md-menu-bar-menu.md-dense .md-menu>.md-button,md-menu-content.md-menu-bar-menu.md-dense md-menu-item>.md-button{text-align:left}[dir=rtl] md-menu-content.md-menu-bar-menu.md-dense .md-menu>.md-button,[dir=rtl] md-menu-content.md-menu-bar-menu.md-dense md-menu-item>.md-button{text-align:right}md-menu-content.md-menu-bar-menu.md-dense .md-menu{padding:0}md-menu-content.md-menu-bar-menu.md-dense .md-menu>.md-button{position:relative;margin:0;width:100%;text-transform:none;font-weight:400;border-radius:0;padding-left:16px}[dir=rtl] md-menu-content.md-menu-bar-menu.md-dense .md-menu>.md-button{padding-left:0;padding-right:16px}md-menu-content.md-menu-bar-menu.md-dense .md-menu>.md-button:after{display:block;content:"\25BC";position:absolute;top:0;speak:none;-webkit-transform:rotate(270deg) scaleY(.45) scaleX(.9);transform:rotate(270deg) scaleY(.45) scaleX(.9);right:28px}[dir=rtl] md-menu-content.md-menu-bar-menu.md-dense .md-menu>.md-button:after{-webkit-transform:rotate(90deg) scaleY(.45) scaleX(.9);transform:rotate(90deg) scaleY(.45) scaleX(.9);right:auto;left:28px}.md-nav-bar{border-style:solid;border-width:0 0 1px;height:48px;position:relative}._md-nav-bar-list{outline:none;list-style:none;margin:0;padding:0;box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.md-nav-item:first-of-type{margin-left:8px}.md-button._md-nav-button{line-height:24px;margin:0 4px;padding:12px 16px;-webkit-transition:background-color .35s cubic-bezier(.35,0,.25,1);transition:background-color .35s cubic-bezier(.35,0,.25,1)}.md-button._md-nav-button:focus{outline:none}.md-button._md-nav-button:hover{background-color:inherit}md-nav-ink-bar{bottom:0;height:2px;left:auto;position:absolute;right:auto;background-color:#000}md-nav-ink-bar._md-left{-webkit-transition:left .125s cubic-bezier(.35,0,.25,1),right .25s cubic-bezier(.35,0,.25,1);transition:left .125s cubic-bezier(.35,0,.25,1),right .25s cubic-bezier(.35,0,.25,1)}md-nav-ink-bar._md-right{-webkit-transition:left .25s cubic-bezier(.35,0,.25,1),right .125s cubic-bezier(.35,0,.25,1);transition:left .25s cubic-bezier(.35,0,.25,1),right .125s cubic-bezier(.35,0,.25,1)}md-nav-ink-bar.ng-animate{-webkit-transition:none;transition:none}md-nav-extra-content{min-height:48px;padding-right:12px}@-webkit-keyframes indeterminate-rotate{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes indeterminate-rotate{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}md-progress-circular{position:relative;display:block}md-progress-circular._md-progress-circular-disabled{visibility:hidden}md-progress-circular.md-mode-indeterminate svg{-webkit-animation:indeterminate-rotate 1568.63ms linear infinite;animation:indeterminate-rotate 1568.63ms linear infinite}md-progress-circular svg{position:absolute;overflow:visible;top:0;left:0}md-progress-linear{display:block;position:relative;width:100%;height:5px;padding-top:0!important;margin-bottom:0!important}md-progress-linear._md-progress-linear-disabled{visibility:hidden}md-progress-linear .md-container{display:block;position:relative;overflow:hidden;width:100%;height:5px;-webkit-transform:translate(0,0) scale(1,1);transform:translate(0,0) scale(1,1)}md-progress-linear .md-container .md-bar{position:absolute;left:0;top:0;bottom:0;width:100%;height:5px}md-progress-linear .md-container .md-dashed:before{content:"";display:none;position:absolute;margin-top:0;height:5px;width:100%;background-color:transparent;background-size:10px 10px!important;background-position:0 -23px}md-progress-linear .md-container .md-bar1,md-progress-linear .md-container .md-bar2{-webkit-transition:-webkit-transform .2s linear;transition:-webkit-transform .2s linear;transition:transform .2s linear;transition:transform .2s linear,-webkit-transform .2s linear}md-progress-linear .md-container.md-mode-query .md-bar1{display:none}md-progress-linear .md-container.md-mode-query .md-bar2{-webkit-transition:all .2s linear;transition:all .2s linear;-webkit-animation:query .8s infinite cubic-bezier(.39,.575,.565,1);animation:query .8s infinite cubic-bezier(.39,.575,.565,1)}md-progress-linear .md-container.md-mode-determinate .md-bar1{display:none}md-progress-linear .md-container.md-mode-indeterminate .md-bar1{-webkit-animation:md-progress-linear-indeterminate-scale-1 4s infinite,md-progress-linear-indeterminate-1 4s infinite;animation:md-progress-linear-indeterminate-scale-1 4s infinite,md-progress-linear-indeterminate-1 4s infinite}md-progress-linear .md-container.md-mode-indeterminate .md-bar2{-webkit-animation:md-progress-linear-indeterminate-scale-2 4s infinite,md-progress-linear-indeterminate-2 4s infinite;animation:md-progress-linear-indeterminate-scale-2 4s infinite,md-progress-linear-indeterminate-2 4s infinite}md-progress-linear .md-container.ng-hide ._md-progress-linear-disabled md-progress-linear .md-container{-webkit-animation:none;animation:none}md-progress-linear .md-container.ng-hide ._md-progress-linear-disabled md-progress-linear .md-container .md-bar1,md-progress-linear .md-container.ng-hide ._md-progress-linear-disabled md-progress-linear .md-container .md-bar2{-webkit-animation-name:none;animation-name:none}md-progress-linear .md-container.md-mode-buffer{background-color:transparent!important;-webkit-transition:all .2s linear;transition:all .2s linear}md-progress-linear .md-container.md-mode-buffer .md-dashed:before{display:block;-webkit-animation:buffer 3s infinite linear;animation:buffer 3s infinite linear}@-webkit-keyframes query{0%{opacity:1;-webkit-transform:translateX(35%) scale(.3,1);transform:translateX(35%) scale(.3,1)}to{opacity:0;-webkit-transform:translateX(-50%) scale(0,1);transform:translateX(-50%) scale(0,1)}}@keyframes query{0%{opacity:1;-webkit-transform:translateX(35%) scale(.3,1);transform:translateX(35%) scale(.3,1)}to{opacity:0;-webkit-transform:translateX(-50%) scale(0,1);transform:translateX(-50%) scale(0,1)}}@-webkit-keyframes buffer{0%{opacity:1;background-position:0 -23px}50%{opacity:0}to{opacity:1;background-position:-200px -23px}}@keyframes buffer{0%{opacity:1;background-position:0 -23px}50%{opacity:0}to{opacity:1;background-position:-200px -23px}}@-webkit-keyframes md-progress-linear-indeterminate-scale-1{0%{-webkit-transform:scaleX(.1);transform:scaleX(.1);-webkit-animation-timing-function:linear;animation-timing-function:linear}36.6%{-webkit-transform:scaleX(.1);transform:scaleX(.1);-webkit-animation-timing-function:cubic-bezier(.33473,.12482,.78584,1);animation-timing-function:cubic-bezier(.33473,.12482,.78584,1)}69.15%{-webkit-transform:scaleX(.83);transform:scaleX(.83);-webkit-animation-timing-function:cubic-bezier(.22573,0,.23365,1.37098);animation-timing-function:cubic-bezier(.22573,0,.23365,1.37098)}to{-webkit-transform:scaleX(.1);transform:scaleX(.1)}}@keyframes md-progress-linear-indeterminate-scale-1{0%{-webkit-transform:scaleX(.1);transform:scaleX(.1);-webkit-animation-timing-function:linear;animation-timing-function:linear}36.6%{-webkit-transform:scaleX(.1);transform:scaleX(.1);-webkit-animation-timing-function:cubic-bezier(.33473,.12482,.78584,1);animation-timing-function:cubic-bezier(.33473,.12482,.78584,1)}69.15%{-webkit-transform:scaleX(.83);transform:scaleX(.83);-webkit-animation-timing-function:cubic-bezier(.22573,0,.23365,1.37098);animation-timing-function:cubic-bezier(.22573,0,.23365,1.37098)}to{-webkit-transform:scaleX(.1);transform:scaleX(.1)}}@-webkit-keyframes md-progress-linear-indeterminate-1{0%{left:-105.16667%;-webkit-animation-timing-function:linear;animation-timing-function:linear}20%{left:-105.16667%;-webkit-animation-timing-function:cubic-bezier(.5,0,.70173,.49582);animation-timing-function:cubic-bezier(.5,0,.70173,.49582)}69.15%{left:21.5%;-webkit-animation-timing-function:cubic-bezier(.30244,.38135,.55,.95635);animation-timing-function:cubic-bezier(.30244,.38135,.55,.95635)}to{left:95.44444%}}@keyframes md-progress-linear-indeterminate-1{0%{left:-105.16667%;-webkit-animation-timing-function:linear;animation-timing-function:linear}20%{left:-105.16667%;-webkit-animation-timing-function:cubic-bezier(.5,0,.70173,.49582);animation-timing-function:cubic-bezier(.5,0,.70173,.49582)}69.15%{left:21.5%;-webkit-animation-timing-function:cubic-bezier(.30244,.38135,.55,.95635);animation-timing-function:cubic-bezier(.30244,.38135,.55,.95635)}to{left:95.44444%}}@-webkit-keyframes md-progress-linear-indeterminate-scale-2{0%{-webkit-transform:scaleX(.1);transform:scaleX(.1);-webkit-animation-timing-function:cubic-bezier(.20503,.05705,.57661,.45397);animation-timing-function:cubic-bezier(.20503,.05705,.57661,.45397)}19.15%{-webkit-transform:scaleX(.57);transform:scaleX(.57);-webkit-animation-timing-function:cubic-bezier(.15231,.19643,.64837,1.00432);animation-timing-function:cubic-bezier(.15231,.19643,.64837,1.00432)}44.15%{-webkit-transform:scaleX(.91);transform:scaleX(.91);-webkit-animation-timing-function:cubic-bezier(.25776,-.00316,.21176,1.38179);animation-timing-function:cubic-bezier(.25776,-.00316,.21176,1.38179)}to{-webkit-transform:scaleX(.1);transform:scaleX(.1)}}@keyframes md-progress-linear-indeterminate-scale-2{0%{-webkit-transform:scaleX(.1);transform:scaleX(.1);-webkit-animation-timing-function:cubic-bezier(.20503,.05705,.57661,.45397);animation-timing-function:cubic-bezier(.20503,.05705,.57661,.45397)}19.15%{-webkit-transform:scaleX(.57);transform:scaleX(.57);-webkit-animation-timing-function:cubic-bezier(.15231,.19643,.64837,1.00432);animation-timing-function:cubic-bezier(.15231,.19643,.64837,1.00432)}44.15%{-webkit-transform:scaleX(.91);transform:scaleX(.91);-webkit-animation-timing-function:cubic-bezier(.25776,-.00316,.21176,1.38179);animation-timing-function:cubic-bezier(.25776,-.00316,.21176,1.38179)}to{-webkit-transform:scaleX(.1);transform:scaleX(.1)}}@-webkit-keyframes md-progress-linear-indeterminate-2{0%{left:-54.88889%;-webkit-animation-timing-function:cubic-bezier(.15,0,.51506,.40968);animation-timing-function:cubic-bezier(.15,0,.51506,.40968)}25%{left:-17.25%;-webkit-animation-timing-function:cubic-bezier(.31033,.28406,.8,.73372);animation-timing-function:cubic-bezier(.31033,.28406,.8,.73372)}48.35%{left:29.5%;-webkit-animation-timing-function:cubic-bezier(.4,.62703,.6,.90203);animation-timing-function:cubic-bezier(.4,.62703,.6,.90203)}to{left:117.38889%}}@keyframes md-progress-linear-indeterminate-2{0%{left:-54.88889%;-webkit-animation-timing-function:cubic-bezier(.15,0,.51506,.40968);animation-timing-function:cubic-bezier(.15,0,.51506,.40968)}25%{left:-17.25%;-webkit-animation-timing-function:cubic-bezier(.31033,.28406,.8,.73372);animation-timing-function:cubic-bezier(.31033,.28406,.8,.73372)}48.35%{left:29.5%;-webkit-animation-timing-function:cubic-bezier(.4,.62703,.6,.90203);animation-timing-function:cubic-bezier(.4,.62703,.6,.90203)}to{left:117.38889%}}md-radio-button{box-sizing:border-box;display:block;margin-bottom:16px;white-space:nowrap;cursor:pointer;position:relative}md-radio-button[disabled],md-radio-button[disabled] .md-container{cursor:default}md-radio-button .md-container{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);box-sizing:border-box;display:inline-block;width:20px;height:20px;cursor:pointer;left:0;right:auto}[dir=rtl] md-radio-button .md-container{left:auto;right:0}md-radio-button .md-container .md-ripple-container{position:absolute;display:block;width:auto;height:auto;left:-15px;top:-15px;right:-15px;bottom:-15px}md-radio-button .md-container:before{box-sizing:border-box;background-color:transparent;border-radius:50%;content:"";position:absolute;display:block;height:auto;left:0;top:0;right:0;bottom:0;-webkit-transition:all .5s;transition:all .5s;width:auto}md-radio-button.md-align-top-left>div.md-container{top:12px}md-radio-button .md-off{border-style:solid;border-width:2px;-webkit-transition:border-color .28s ease;transition:border-color .28s ease}md-radio-button .md-off,md-radio-button .md-on{box-sizing:border-box;position:absolute;top:0;left:0;width:20px;height:20px;border-radius:50%}md-radio-button .md-on{-webkit-transition:-webkit-transform .28s ease;transition:-webkit-transform .28s ease;transition:transform .28s ease;transition:transform .28s ease,-webkit-transform .28s ease;-webkit-transform:scale(0);transform:scale(0)}md-radio-button.md-checked .md-on{-webkit-transform:scale(.5);transform:scale(.5)}md-radio-button .md-label{box-sizing:border-box;position:relative;display:inline-block;margin-left:30px;margin-right:0;vertical-align:middle;white-space:normal;pointer-events:none;width:auto}[dir=rtl] md-radio-button .md-label{margin-left:0;margin-right:30px}md-radio-group.layout-column md-radio-button,md-radio-group.layout-gt-lg-column md-radio-button,md-radio-group.layout-gt-md-column md-radio-button,md-radio-group.layout-gt-sm-column md-radio-button,md-radio-group.layout-gt-xs-column md-radio-button,md-radio-group.layout-lg-column md-radio-button,md-radio-group.layout-md-column md-radio-button,md-radio-group.layout-sm-column md-radio-button,md-radio-group.layout-xl-column md-radio-button,md-radio-group.layout-xs-column md-radio-button{margin-bottom:16px}md-radio-group.layout-gt-lg-row md-radio-button,md-radio-group.layout-gt-md-row md-radio-button,md-radio-group.layout-gt-sm-row md-radio-button,md-radio-group.layout-gt-xs-row md-radio-button,md-radio-group.layout-lg-row md-radio-button,md-radio-group.layout-md-row md-radio-button,md-radio-group.layout-row md-radio-button,md-radio-group.layout-sm-row md-radio-button,md-radio-group.layout-xl-row md-radio-button,md-radio-group.layout-xs-row md-radio-button{margin:0 16px 0 0}[dir=rtl] md-radio-group.layout-gt-lg-row md-radio-button,[dir=rtl] md-radio-group.layout-gt-md-row md-radio-button,[dir=rtl] md-radio-group.layout-gt-sm-row md-radio-button,[dir=rtl] md-radio-group.layout-gt-xs-row md-radio-button,[dir=rtl] md-radio-group.layout-lg-row md-radio-button,[dir=rtl] md-radio-group.layout-md-row md-radio-button,[dir=rtl] md-radio-group.layout-row md-radio-button,[dir=rtl] md-radio-group.layout-sm-row md-radio-button,[dir=rtl] md-radio-group.layout-xl-row md-radio-button,[dir=rtl] md-radio-group.layout-xs-row md-radio-button{margin-left:16px;margin-right:0}md-radio-group.layout-gt-lg-row md-radio-button:last-of-type,md-radio-group.layout-gt-md-row md-radio-button:last-of-type,md-radio-group.layout-gt-sm-row md-radio-button:last-of-type,md-radio-group.layout-gt-xs-row md-radio-button:last-of-type,md-radio-group.layout-lg-row md-radio-button:last-of-type,md-radio-group.layout-md-row md-radio-button:last-of-type,md-radio-group.layout-row md-radio-button:last-of-type,md-radio-group.layout-sm-row md-radio-button:last-of-type,md-radio-group.layout-xl-row md-radio-button:last-of-type,md-radio-group.layout-xs-row md-radio-button:last-of-type{margin-left:0;margin-right:0}md-radio-group:focus{outline:none}md-radio-group.md-focused .md-checked .md-container:before{left:-8px;top:-8px;right:-8px;bottom:-8px}md-radio-group[disabled] md-radio-button,md-radio-group[disabled] md-radio-button .md-container{cursor:default}.md-inline-form md-radio-group{margin:18px 0 19px}.md-inline-form md-radio-group md-radio-button{display:inline-block;height:30px;padding:2px;box-sizing:border-box;margin-top:0;margin-bottom:0}@media screen and (-ms-high-contrast:active){md-radio-button.md-default-theme .md-on{background-color:#fff}}md-sidenav{box-sizing:border-box;position:absolute;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;z-index:60;width:320px;max-width:320px;bottom:0;overflow:auto;-webkit-overflow-scrolling:touch}md-sidenav ul{list-style:none}md-sidenav.md-closed{display:none}md-sidenav.md-closed-add,md-sidenav.md-closed-remove{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-transition:all .2s ease-in;transition:all .2s ease-in}md-sidenav.md-closed-add.md-closed-add-active,md-sidenav.md-closed-remove.md-closed-remove-active{-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1)}md-sidenav.md-closed.md-locked-open-add,md-sidenav.md-locked-open,md-sidenav.md-locked-open-add,md-sidenav.md-locked-open-remove,md-sidenav.md-locked-open-remove.md-closed,md-sidenav.md-locked-open.md-closed,md-sidenav.md-locked-open.md-closed.md-sidenav-left,md-sidenav.md-locked-open.md-closed.md-sidenav-right{position:static;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}md-sidenav.md-closed.md-locked-open-add:not(.md-locked-open-add-active){width:0!important;min-width:0!important}md-sidenav.md-closed.md-locked-open-add-active,md-sidenav.md-closed.md-locked-open-add:not(.md-locked-open-add-active),md-sidenav.md-locked-open-remove-active{-webkit-transition:width .3s cubic-bezier(.55,0,.55,.2),min-width .3s cubic-bezier(.55,0,.55,.2);transition:width .3s cubic-bezier(.55,0,.55,.2),min-width .3s cubic-bezier(.55,0,.55,.2)}md-sidenav.md-locked-open-remove-active{width:0!important;min-width:0!important}.md-sidenav-backdrop.md-locked-open{display:none}.md-sidenav-left,md-sidenav{left:0;top:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.md-sidenav-left.md-closed,md-sidenav.md-closed{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}.md-sidenav-right{left:100%;top:0;-webkit-transform:translate(-100%,0);transform:translate(-100%,0)}.md-sidenav-right.md-closed{-webkit-transform:translate(0,0);transform:translate(0,0)}@media (min-width:600px){md-sidenav{max-width:400px}}@media (max-width:456px){md-sidenav{width:calc(100% - 56px);min-width:calc(100% - 56px);max-width:calc(100% - 56px)}}@media screen and (-ms-high-contrast:active){.md-sidenav-left,md-sidenav{border-right:1px solid #fff}.md-sidenav-right{border-left:1px solid #fff}}md-input-container:not([md-no-float]) .md-select-placeholder span:first-child{-webkit-transition:-webkit-transform .4s cubic-bezier(.25,.8,.25,1);transition:-webkit-transform .4s cubic-bezier(.25,.8,.25,1);transition:transform .4s cubic-bezier(.25,.8,.25,1);transition:transform .4s cubic-bezier(.25,.8,.25,1),-webkit-transform .4s cubic-bezier(.25,.8,.25,1);-webkit-transform-origin:left top;transform-origin:left top}[dir=rtl] md-input-container:not([md-no-float]) .md-select-placeholder span:first-child{-webkit-transform-origin:right top;transform-origin:right top}md-input-container.md-input-focused:not([md-no-float]) .md-select-placeholder span:first-child{-webkit-transform:translateY(-22px) translateX(-2px) scale(.75);transform:translateY(-22px) translateX(-2px) scale(.75)}.md-select-menu-container{position:fixed;left:0;top:0;z-index:90;opacity:0;display:none;-webkit-transform:translateY(-1px);transform:translateY(-1px)}.md-select-menu-container:not(.md-clickable){pointer-events:none}.md-select-menu-container md-progress-circular{display:table;margin:24px auto!important}.md-select-menu-container.md-active{display:block;opacity:1}.md-select-menu-container.md-active md-select-menu{-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1);-webkit-transition-duration:.15s;transition-duration:.15s}.md-select-menu-container.md-active md-select-menu>*{opacity:1;-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2);-webkit-transition-duration:.15s;transition-duration:.15s;-webkit-transition-delay:.1s;transition-delay:.1s}.md-select-menu-container.md-leave{opacity:0;-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2);-webkit-transition-duration:.25s;transition-duration:.25s}md-input-container>md-select{margin:0;-webkit-box-ordinal-group:3;-webkit-order:2;order:2}md-input-container:not(.md-input-has-value) md-select.ng-required:not(.md-no-asterisk) .md-select-value span:first-child:after,md-input-container:not(.md-input-has-value) md-select[required]:not(.md-no-asterisk) .md-select-value span:first-child:after{content:" *";font-size:13px;vertical-align:top}md-input-container.md-input-invalid md-select .md-select-value{border-bottom-style:solid;padding-bottom:1px}md-select{display:-webkit-box;display:-webkit-flex;display:flex;margin:20px 0 26px}md-select.ng-required.ng-invalid:not(.md-no-asterisk) .md-select-value span:first-child:after,md-select[required].ng-invalid:not(.md-no-asterisk) .md-select-value span:first-child:after{content:" *";font-size:13px;vertical-align:top}md-select[disabled] .md-select-value{background-position:0 bottom;background-size:4px 1px;background-repeat:repeat-x;margin-bottom:-1px}md-select:focus{outline:none}md-select[disabled]:hover{cursor:default}md-select:not([disabled]):hover{cursor:pointer}md-select:not([disabled]).ng-invalid.ng-touched .md-select-value{border-bottom-style:solid;padding-bottom:1px}md-select:not([disabled]):focus .md-select-value{border-bottom-width:2px;border-bottom-style:solid;padding-bottom:0}md-select:not([disabled]):focus.ng-invalid.ng-touched .md-select-value{padding-bottom:0}md-input-container.md-input-has-value .md-select-value>span:not(.md-select-icon){-webkit-transform:translate3d(0,1px,0);transform:translate3d(0,1px,0)}.md-select-value{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;padding:2px 2px 1px;border-bottom-width:1px;border-bottom-style:solid;background-color:transparent;position:relative;box-sizing:content-box;min-width:64px;min-height:26px;-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}.md-select-value>span:not(.md-select-icon){max-width:100%;-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.md-select-value>span:not(.md-select-icon) .md-text{display:inline}.md-select-value .md-select-icon{display:block;-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;text-align:end;width:24px;margin:0 4px;-webkit-transform:translate3d(0,-2px,0);transform:translate3d(0,-2px,0);font-size:1.2rem}.md-select-value .md-select-icon:after{display:block;content:"\25BC";position:relative;top:2px;speak:none;font-size:13px;-webkit-transform:scaleY(.5) scaleX(1);transform:scaleY(.5) scaleX(1)}.md-select-value.md-select-placeholder{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-ordinal-group:2;-webkit-order:1;order:1;pointer-events:none;-webkit-font-smoothing:antialiased;padding-left:2px;z-index:1}md-select-menu{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;box-shadow:0 1px 3px 0 rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 2px 1px -1px rgba(0,0,0,.12);max-height:256px;min-height:48px;overflow-y:hidden;-webkit-transform-origin:left top;transform-origin:left top;-webkit-transform:scale(1);transform:scale(1)}md-select-menu.md-reverse{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-webkit-flex-direction:column-reverse;flex-direction:column-reverse}md-select-menu:not(.md-overflow) md-content{padding-top:8px;padding-bottom:8px}[dir=rtl] md-select-menu{-webkit-transform-origin:right top;transform-origin:right top}md-select-menu md-content{min-width:136px;min-height:48px;max-height:256px;overflow-y:auto}md-select-menu>*{opacity:0}md-option{cursor:pointer;position:relative;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;width:auto;-webkit-transition:background .15s linear;transition:background .15s linear;padding:0 16px;height:48px}md-option[disabled]{cursor:default}md-option:focus{outline:none}md-option .md-text{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}md-optgroup{display:block}md-optgroup label{display:block;font-size:14px;text-transform:uppercase;padding:16px;font-weight:500}md-optgroup md-option{padding-left:32px;padding-right:32px}@media screen and (-ms-high-contrast:active){.md-select-backdrop{background-color:transparent}md-select-menu{border:1px solid #fff}}md-select-menu[multiple] md-option.md-checkbox-enabled{padding-left:40px;padding-right:16px}[dir=rtl] md-select-menu[multiple] md-option.md-checkbox-enabled{padding-left:16px;padding-right:40px}md-select-menu[multiple] md-option.md-checkbox-enabled .md-container{position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);box-sizing:border-box;display:inline-block;width:20px;height:20px;left:0;right:auto}[dir=rtl] md-select-menu[multiple] md-option.md-checkbox-enabled .md-container{left:auto;right:0}md-select-menu[multiple] md-option.md-checkbox-enabled .md-container:before{box-sizing:border-box;background-color:transparent;border-radius:50%;content:"";position:absolute;display:block;height:auto;left:0;top:0;right:0;bottom:0;-webkit-transition:all .5s;transition:all .5s;width:auto}md-select-menu[multiple] md-option.md-checkbox-enabled .md-container:after{box-sizing:border-box;content:"";position:absolute;top:-10px;right:-10px;bottom:-10px;left:-10px}md-select-menu[multiple] md-option.md-checkbox-enabled .md-container .md-ripple-container{position:absolute;display:block;width:auto;height:auto;left:-15px;top:-15px;right:-15px;bottom:-15px}md-select-menu[multiple] md-option.md-checkbox-enabled .md-icon{box-sizing:border-box;-webkit-transition:.24s;transition:.24s;position:absolute;top:0;left:0;width:20px;height:20px;border-width:2px;border-style:solid;border-radius:2px}md-select-menu[multiple] md-option.md-checkbox-enabled[selected] .md-icon{border-color:transparent}md-select-menu[multiple] md-option.md-checkbox-enabled[selected] .md-icon:after{box-sizing:border-box;-webkit-transform:rotate(45deg);transform:rotate(45deg);position:absolute;left:4.66667px;top:.22222px;display:table;width:6.66667px;height:13.33333px;border-width:2px;border-style:solid;border-top:0;border-left:0;content:""}md-select-menu[multiple] md-option.md-checkbox-enabled[disabled]{cursor:default}md-select-menu[multiple] md-option.md-checkbox-enabled.md-indeterminate .md-icon:after{box-sizing:border-box;position:absolute;top:50%;left:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);display:table;width:12px;height:2px;border-width:2px;border-style:solid;border-top:0;border-left:0;content:""}md-select-menu[multiple] md-option.md-checkbox-enabled .md-container{margin-left:10.66667px;margin-right:auto}[dir=rtl] md-select-menu[multiple] md-option.md-checkbox-enabled .md-container{margin-left:auto;margin-right:10.66667px}@-webkit-keyframes sliderFocusThumb{0%{-webkit-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1);transform:scale(1)}to{-webkit-transform:scale(.7);transform:scale(.7)}}@keyframes sliderFocusThumb{0%{-webkit-transform:scale(.7);transform:scale(.7)}30%{-webkit-transform:scale(1);transform:scale(1)}to{-webkit-transform:scale(.7);transform:scale(.7)}}@-webkit-keyframes sliderDiscreteFocusThumb{0%{-webkit-transform:scale(.7);transform:scale(.7)}50%{-webkit-transform:scale(.8);transform:scale(.8)}to{-webkit-transform:scale(0);transform:scale(0)}}@keyframes sliderDiscreteFocusThumb{0%{-webkit-transform:scale(.7);transform:scale(.7)}50%{-webkit-transform:scale(.8);transform:scale(.8)}to{-webkit-transform:scale(0);transform:scale(0)}}@-webkit-keyframes sliderDiscreteFocusRing{0%{-webkit-transform:scale(.7);transform:scale(.7);opacity:0}50%{-webkit-transform:scale(1);transform:scale(1);opacity:1}to{-webkit-transform:scale(0);transform:scale(0)}}@keyframes sliderDiscreteFocusRing{0%{-webkit-transform:scale(.7);transform:scale(.7);opacity:0}50%{-webkit-transform:scale(1);transform:scale(1);opacity:1}to{-webkit-transform:scale(0);transform:scale(0)}}md-slider{height:48px;min-width:128px;position:relative;margin-left:4px;margin-right:4px;padding:0;display:block;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}md-slider *,md-slider :after{box-sizing:border-box}md-slider .md-slider-wrapper{outline:none;width:100%;height:100%}md-slider .md-slider-content{position:relative}md-slider .md-track-container{width:100%;position:absolute;top:23px;height:2px}md-slider .md-track{position:absolute;left:0;right:0;height:100%}md-slider .md-track-fill{-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1);-webkit-transition-property:width,height;transition-property:width,height}md-slider .md-track-ticks{position:absolute;left:0;right:0;height:100%}md-slider .md-track-ticks canvas{width:100%;height:100%}md-slider .md-thumb-container{position:absolute;left:0;top:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0);-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1);-webkit-transition-property:left,right,bottom;transition-property:left,right,bottom}[dir=rtl] md-slider .md-thumb-container{left:auto;right:0}md-slider .md-thumb{z-index:1;position:absolute;left:-10px;top:14px;width:20px;height:20px;border-radius:20px;-webkit-transform:scale(.7);transform:scale(.7);-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1)}[dir=rtl] md-slider .md-thumb{left:auto;right:-10px}md-slider .md-thumb:after{content:"";position:absolute;width:20px;height:20px;border-radius:20px;border-width:3px;border-style:solid;-webkit-transition:inherit;transition:inherit}md-slider .md-sign{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center;position:absolute;left:-14px;top:-17px;width:28px;height:28px;border-radius:28px;-webkit-transform:scale(.4) translate3d(0,67.5px,0);transform:scale(.4) translate3d(0,67.5px,0);-webkit-transition:all .3s cubic-bezier(.35,0,.25,1);transition:all .3s cubic-bezier(.35,0,.25,1)}md-slider .md-sign:after{position:absolute;content:"";left:0;border-radius:16px;top:19px;border-left:14px solid transparent;border-right:14px solid transparent;border-top-width:16px;border-top-style:solid;opacity:0;-webkit-transform:translate3d(0,-8px,0);transform:translate3d(0,-8px,0);-webkit-transition:all .2s cubic-bezier(.35,0,.25,1);transition:all .2s cubic-bezier(.35,0,.25,1)}[dir=rtl] md-slider .md-sign:after{left:auto;right:0}md-slider .md-sign .md-thumb-text{z-index:1;font-size:12px;font-weight:700}md-slider .md-focus-ring{position:absolute;left:-17px;top:7px;width:34px;height:34px;border-radius:34px;-webkit-transform:scale(.7);transform:scale(.7);opacity:0;-webkit-transition:all .35s cubic-bezier(.35,0,.25,1);transition:all .35s cubic-bezier(.35,0,.25,1)}[dir=rtl] md-slider .md-focus-ring{left:auto;right:-17px}md-slider .md-disabled-thumb{position:absolute;left:-14px;top:10px;width:28px;height:28px;border-radius:28px;-webkit-transform:scale(.5);transform:scale(.5);border-width:4px;border-style:solid;display:none}[dir=rtl] md-slider .md-disabled-thumb{left:auto;right:-14px}md-slider.md-min .md-sign{opacity:0}md-slider:focus{outline:none}md-slider.md-dragging .md-thumb-container,md-slider.md-dragging .md-track-fill{-webkit-transition:none;transition:none}md-slider:not([md-discrete]) .md-sign,md-slider:not([md-discrete]) .md-track-ticks{display:none}md-slider:not([md-discrete]):not([disabled]) .md-slider-wrapper .md-thumb:hover{-webkit-transform:scale(.8);transform:scale(.8)}md-slider:not([md-discrete]):not([disabled]) .md-slider-wrapper.md-focused .md-focus-ring{-webkit-transform:scale(1);transform:scale(1);opacity:1}md-slider:not([md-discrete]):not([disabled]) .md-slider-wrapper.md-focused .md-thumb{-webkit-animation:sliderFocusThumb .7s cubic-bezier(.35,0,.25,1);animation:sliderFocusThumb .7s cubic-bezier(.35,0,.25,1)}md-slider:not([md-discrete]):not([disabled]).md-active .md-slider-wrapper .md-thumb{-webkit-transform:scale(1);transform:scale(1)}md-slider[md-discrete]:not([disabled]) .md-slider-wrapper.md-focused .md-focus-ring{-webkit-transform:scale(0);transform:scale(0);-webkit-animation:sliderDiscreteFocusRing .5s cubic-bezier(.35,0,.25,1);animation:sliderDiscreteFocusRing .5s cubic-bezier(.35,0,.25,1)}md-slider[md-discrete]:not([disabled]) .md-slider-wrapper.md-focused .md-thumb{-webkit-animation:sliderDiscreteFocusThumb .5s cubic-bezier(.35,0,.25,1);animation:sliderDiscreteFocusThumb .5s cubic-bezier(.35,0,.25,1)}md-slider[md-discrete]:not([disabled]).md-active .md-thumb,md-slider[md-discrete]:not([disabled]) .md-slider-wrapper.md-focused .md-thumb{-webkit-transform:scale(0);transform:scale(0)}md-slider[md-discrete]:not([disabled]).md-active .md-sign,md-slider[md-discrete]:not([disabled]).md-active .md-sign:after,md-slider[md-discrete]:not([disabled]) .md-slider-wrapper.md-focused .md-sign,md-slider[md-discrete]:not([disabled]) .md-slider-wrapper.md-focused .md-sign:after{opacity:1;-webkit-transform:translate3d(0,0,0) scale(1);transform:translate3d(0,0,0) scale(1)}md-slider[md-discrete][disabled][readonly] .md-thumb{-webkit-transform:scale(0);transform:scale(0)}md-slider[md-discrete][disabled][readonly] .md-sign,md-slider[md-discrete][disabled][readonly] .md-sign:after{opacity:1;-webkit-transform:translate3d(0,0,0) scale(1);transform:translate3d(0,0,0) scale(1)}md-slider[disabled] .md-track-fill{display:none}md-slider[disabled] .md-track-ticks,md-slider[disabled]:not([readonly]) .md-sign{opacity:0}md-slider[disabled] .md-thumb{-webkit-transform:scale(.5);transform:scale(.5)}md-slider[disabled] .md-disabled-thumb{display:block}md-slider[md-vertical]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;min-height:128px;min-width:0}md-slider[md-vertical] .md-slider-wrapper{-webkit-box-flex:1;-webkit-flex:1;flex:1;padding-top:12px;padding-bottom:12px;width:48px;-webkit-align-self:center;align-self:center;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}md-slider[md-vertical] .md-track-container{height:100%;width:2px;top:0;left:calc(50% - 1px)}md-slider[md-vertical] .md-thumb-container{top:auto;margin-bottom:23px;left:calc(50% - 1px);bottom:0}md-slider[md-vertical] .md-thumb-container .md-thumb:after{left:1px}md-slider[md-vertical] .md-thumb-container .md-focus-ring{left:-16px}md-slider[md-vertical] .md-track-fill{bottom:0}md-slider[md-vertical][md-discrete] .md-sign{left:-40px;top:9.5px;-webkit-transform:scale(.4) translate3d(67.5px,0,0);transform:scale(.4) translate3d(67.5px,0,0)}md-slider[md-vertical][md-discrete] .md-sign:after{top:9.5px;left:19px;border-top:14px solid transparent;border-right:0;border-bottom:14px solid transparent;border-left-width:16px;border-left-style:solid;opacity:0;-webkit-transform:translate3d(0,-8px,0);transform:translate3d(0,-8px,0);-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}md-slider[md-vertical][md-discrete] .md-sign .md-thumb-text{z-index:1;font-size:12px;font-weight:700}md-slider[md-vertical][md-discrete].md-active .md-sign:after,md-slider[md-vertical][md-discrete] .md-focused .md-sign:after,md-slider[md-vertical][md-discrete][disabled][readonly] .md-sign:after{top:0}md-slider[md-vertical][disabled][readonly] .md-thumb{-webkit-transform:scale(0);transform:scale(0)}md-slider[md-vertical][disabled][readonly] .md-sign,md-slider[md-vertical][disabled][readonly] .md-sign:after{opacity:1;-webkit-transform:translate3d(0,0,0) scale(1);transform:translate3d(0,0,0) scale(1)}md-slider[md-invert]:not([md-vertical]) .md-track-fill{left:auto;right:0}[dir=rtl] md-slider[md-invert]:not([md-vertical]) .md-track-fill{left:0;right:auto}md-slider[md-invert][md-vertical] .md-track-fill{bottom:auto;top:0}md-slider-container{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}md-slider-container>:first-child:not(md-slider),md-slider-container>:last-child:not(md-slider){min-width:25px;max-width:42px;height:25px;-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1);-webkit-transition-property:color,max-width;transition-property:color,max-width}md-slider-container>:first-child:not(md-slider){margin-right:16px}[dir=rtl] md-slider-container>:first-child:not(md-slider){margin-right:auto;margin-left:16px}md-slider-container>:last-child:not(md-slider){margin-left:16px}[dir=rtl] md-slider-container>:last-child:not(md-slider){margin-left:auto;margin-right:16px}md-slider-container[md-vertical]{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}md-slider-container[md-vertical]>:first-child:not(md-slider),md-slider-container[md-vertical]>:last-child:not(md-slider){margin-right:0;margin-left:0;text-align:center}md-slider-container md-input-container input[type=number]{text-align:center;padding-left:15px;height:50px;margin-top:-25px}[dir=rtl] md-slider-container md-input-container input[type=number]{padding-left:0;padding-right:15px}@media screen and (-ms-high-contrast:active){md-slider.md-default-theme .md-track{border-bottom:1px solid #fff}}.md-sticky-clone{z-index:2;top:0;left:0;right:0;position:absolute!important;-webkit-transform:translate3d(-9999px,-9999px,0);transform:translate3d(-9999px,-9999px,0)}.md-sticky-clone[sticky-state=active]{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.md-sticky-clone[sticky-state=active]:not(.md-sticky-no-effect) .md-subheader-inner{-webkit-animation:subheaderStickyHoverIn .3s ease-out both;animation:subheaderStickyHoverIn .3s ease-out both}@-webkit-keyframes subheaderStickyHoverIn{0%{box-shadow:0 0 0 0 transparent}to{box-shadow:0 2px 4px 0 rgba(0,0,0,.16)}}@keyframes subheaderStickyHoverIn{0%{box-shadow:0 0 0 0 transparent}to{box-shadow:0 2px 4px 0 rgba(0,0,0,.16)}}@-webkit-keyframes subheaderStickyHoverOut{0%{box-shadow:0 2px 4px 0 rgba(0,0,0,.16)}to{box-shadow:0 0 0 0 transparent}}@keyframes subheaderStickyHoverOut{0%{box-shadow:0 2px 4px 0 rgba(0,0,0,.16)}to{box-shadow:0 0 0 0 transparent}}.md-subheader-wrapper:not(.md-sticky-no-effect){-webkit-transition:margin .2s ease-out;transition:margin .2s ease-out}.md-subheader-wrapper:not(.md-sticky-no-effect) .md-subheader{margin:0}.md-subheader-wrapper:not(.md-sticky-no-effect).md-sticky-clone{z-index:2}.md-subheader-wrapper:not(.md-sticky-no-effect)[sticky-state=active]{margin-top:-2px}.md-subheader-wrapper:not(.md-sticky-no-effect):not(.md-sticky-clone)[sticky-prev-state=active] .md-subheader-inner:after{-webkit-animation:subheaderStickyHoverOut .3s ease-out both;animation:subheaderStickyHoverOut .3s ease-out both}.md-subheader{display:block;font-size:14px;font-weight:500;line-height:1em;margin:0;position:relative}.md-subheader .md-subheader-inner{display:block;padding:16px}.md-subheader .md-subheader-content{display:block;z-index:1;position:relative}.md-inline-form md-switch{margin-top:18px;margin-bottom:19px}md-switch{margin:16px 0;white-space:nowrap;cursor:pointer;outline:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;height:30px;line-height:28px;-webkit-box-align:center;-webkit-align-items:center;align-items:center;display:-webkit-box;display:-webkit-flex;display:flex;margin-left:inherit;margin-right:16px}[dir=rtl] md-switch{margin-left:16px;margin-right:inherit}md-switch:last-of-type{margin-left:inherit;margin-right:0}[dir=rtl] md-switch:last-of-type{margin-left:0;margin-right:inherit}md-switch[disabled],md-switch[disabled] .md-container{cursor:default}md-switch .md-container{cursor:-webkit-grab;cursor:grab;width:36px;height:24px;position:relative;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;margin-right:8px;float:left}[dir=rtl] md-switch .md-container,md-switch.md-inverted .md-container{margin-right:0;margin-left:8px}[dir=rtl] md-switch.md-inverted .md-container{margin-right:8px;margin-left:0}md-switch:not([disabled]) .md-dragging,md-switch:not([disabled]).md-dragging .md-container{cursor:-webkit-grabbing;cursor:grabbing}md-switch.md-focused:not([disabled]) .md-thumb:before{left:-8px;top:-8px;right:-8px;bottom:-8px}md-switch.md-focused:not([disabled]):not(.md-checked) .md-thumb:before{background-color:rgba(0,0,0,.12)}md-switch .md-label{border-color:transparent;border-width:0;float:left}md-switch .md-bar{left:1px;width:34px;top:5px;height:14px;border-radius:8px;position:absolute}md-switch .md-thumb-container{top:2px;left:0;width:16px;position:absolute;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);z-index:1}md-switch.md-checked .md-thumb-container{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}md-switch .md-thumb{margin:0;outline:none;height:20px;width:20px;box-shadow:0 1px 3px 0 rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 2px 1px -1px rgba(0,0,0,.12)}md-switch .md-thumb,md-switch .md-thumb:before{position:absolute;left:0;top:0;border-radius:50%}md-switch .md-thumb:before{background-color:transparent;content:"";display:block;height:auto;right:0;bottom:0;-webkit-transition:all .5s;transition:all .5s;width:auto}md-switch .md-thumb .md-ripple-container{position:absolute;display:block;width:auto;height:auto;left:-20px;top:-20px;right:-20px;bottom:-20px}md-switch:not(.md-dragging) .md-bar,md-switch:not(.md-dragging) .md-thumb,md-switch:not(.md-dragging) .md-thumb-container{-webkit-transition:all .08s linear;transition:all .08s linear;-webkit-transition-property:background-color,-webkit-transform;transition-property:background-color,-webkit-transform;transition-property:transform,background-color;transition-property:transform,background-color,-webkit-transform}md-switch:not(.md-dragging) .md-bar,md-switch:not(.md-dragging) .md-thumb{-webkit-transition-delay:.05s;transition-delay:.05s}@media screen and (-ms-high-contrast:active){md-switch.md-default-theme .md-bar{background-color:#666}md-switch.md-default-theme.md-checked .md-bar{background-color:#9e9e9e}md-switch.md-default-theme .md-thumb{background-color:#fff}}@-webkit-keyframes md-tab-content-hide{0%{opacity:1}50%{opacity:1}to{opacity:0}}@keyframes md-tab-content-hide{0%{opacity:1}50%{opacity:1}to{opacity:0}}md-tab-data{position:absolute;top:0;left:0;right:0;bottom:0;z-index:-1;opacity:0}md-tabs{display:block;margin:0;border-radius:2px;overflow:hidden;position:relative;-webkit-flex-shrink:0;flex-shrink:0}md-tabs:not(.md-no-tab-content):not(.md-dynamic-height){min-height:248px}md-tabs[md-align-tabs=bottom]{padding-bottom:48px}md-tabs[md-align-tabs=bottom] md-tabs-wrapper{position:absolute;bottom:0;left:0;right:0;height:48px;z-index:2}md-tabs[md-align-tabs=bottom] md-tabs-content-wrapper{top:0;bottom:48px}md-tabs.md-dynamic-height md-tabs-content-wrapper{min-height:0;position:relative;top:auto;left:auto;right:auto;bottom:auto;overflow:visible}md-tabs.md-dynamic-height md-tab-content.md-active{position:relative}md-tabs[md-border-bottom] md-tabs-wrapper{border-width:0 0 1px;border-style:solid}md-tabs[md-border-bottom]:not(.md-dynamic-height) md-tabs-content-wrapper{top:49px}md-tabs-wrapper{display:block;position:relative;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}md-tabs-wrapper md-next-button,md-tabs-wrapper md-prev-button{height:100%;width:32px;position:absolute;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%);line-height:1em;z-index:2;cursor:pointer;font-size:16px;background:transparent no-repeat 50%;-webkit-transition:all .5s cubic-bezier(.35,0,.25,1);transition:all .5s cubic-bezier(.35,0,.25,1)}md-tabs-wrapper md-next-button:focus,md-tabs-wrapper md-prev-button:focus{outline:none}md-tabs-wrapper md-next-button.md-disabled,md-tabs-wrapper md-prev-button.md-disabled{opacity:.25;cursor:default}md-tabs-wrapper md-next-button.ng-leave,md-tabs-wrapper md-prev-button.ng-leave{-webkit-transition:none;transition:none}md-tabs-wrapper md-next-button md-icon,md-tabs-wrapper md-prev-button md-icon{position:absolute;top:50%;left:50%;-webkit-transform:translate3d(-50%,-50%,0);transform:translate3d(-50%,-50%,0)}md-tabs-wrapper md-prev-button{left:0;background-image:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE3LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPiA8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPiA8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjQgMjQiIHhtbDpzcGFjZT0icHJlc2VydmUiPiA8ZyBpZD0iSGVhZGVyIj4gPGc+IDxyZWN0IHg9Ii02MTgiIHk9Ii0xMjA4IiBmaWxsPSJub25lIiB3aWR0aD0iMTQwMCIgaGVpZ2h0PSIzNjAwIi8+IDwvZz4gPC9nPiA8ZyBpZD0iTGFiZWwiPiA8L2c+IDxnIGlkPSJJY29uIj4gPGc+IDxwb2x5Z29uIHBvaW50cz0iMTUuNCw3LjQgMTQsNiA4LDEyIDE0LDE4IDE1LjQsMTYuNiAxMC44LDEyIAkJIiBzdHlsZT0iZmlsbDp3aGl0ZTsiLz4gPHJlY3QgZmlsbD0ibm9uZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0Ii8+IDwvZz4gPC9nPiA8ZyBpZD0iR3JpZCIgZGlzcGxheT0ibm9uZSI+IDxnIGRpc3BsYXk9ImlubGluZSI+IDwvZz4gPC9nPiA8L3N2Zz4NCg==")}[dir=rtl] md-tabs-wrapper md-prev-button{left:auto;right:0}md-tabs-wrapper md-next-button{right:0;background-image:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE3LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPiA8IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPiA8c3ZnIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyNHB4IiBoZWlnaHQ9IjI0cHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgMjQgMjQiIHhtbDpzcGFjZT0icHJlc2VydmUiPiA8ZyBpZD0iSGVhZGVyIj4gPGc+IDxyZWN0IHg9Ii02MTgiIHk9Ii0xMzM2IiBmaWxsPSJub25lIiB3aWR0aD0iMTQwMCIgaGVpZ2h0PSIzNjAwIi8+IDwvZz4gPC9nPiA8ZyBpZD0iTGFiZWwiPiA8L2c+IDxnIGlkPSJJY29uIj4gPGc+IDxwb2x5Z29uIHBvaW50cz0iMTAsNiA4LjYsNy40IDEzLjIsMTIgOC42LDE2LjYgMTAsMTggMTYsMTIgCQkiIHN0eWxlPSJmaWxsOndoaXRlOyIvPiA8cmVjdCBmaWxsPSJub25lIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiLz4gPC9nPiA8L2c+IDxnIGlkPSJHcmlkIiBkaXNwbGF5PSJub25lIj4gPGcgZGlzcGxheT0iaW5saW5lIj4gPC9nPiA8L2c+IDwvc3ZnPg0K")}[dir=rtl] md-tabs-wrapper md-next-button{right:auto;left:0}md-tabs-wrapper md-next-button md-icon{-webkit-transform:translate3d(-50%,-50%,0) rotate(180deg);transform:translate3d(-50%,-50%,0) rotate(180deg)}md-tabs-wrapper.md-stretch-tabs md-pagination-wrapper{width:100%;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}md-tabs-wrapper.md-stretch-tabs md-pagination-wrapper md-tab-item{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1}md-tabs-canvas{position:relative;overflow:hidden;display:block;height:48px}md-tabs-canvas:after{content:"";display:table;clear:both}md-tabs-canvas .md-dummy-wrapper{position:absolute;top:0;left:0}[dir=rtl] md-tabs-canvas .md-dummy-wrapper{left:auto;right:0}md-tabs-canvas.md-paginated{margin:0 32px}md-tabs-canvas.md-center-tabs{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;text-align:center}md-tabs-canvas.md-center-tabs .md-tab{float:none;display:inline-block}md-pagination-wrapper{height:48px;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-transition:-webkit-transform .5s cubic-bezier(.35,0,.25,1);transition:-webkit-transform .5s cubic-bezier(.35,0,.25,1);transition:transform .5s cubic-bezier(.35,0,.25,1);transition:transform .5s cubic-bezier(.35,0,.25,1),-webkit-transform .5s cubic-bezier(.35,0,.25,1);position:absolute;left:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}md-pagination-wrapper:after{content:"";display:table;clear:both}[dir=rtl] md-pagination-wrapper{left:auto;right:0}md-pagination-wrapper.md-center-tabs{position:relative;-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}md-tabs-content-wrapper{display:block;top:48px;overflow:hidden}md-tab-content,md-tabs-content-wrapper{position:absolute;left:0;right:0;bottom:0}md-tab-content{display:-webkit-box;display:-webkit-flex;display:flex;top:0;-webkit-transition:-webkit-transform .5s cubic-bezier(.35,0,.25,1);transition:-webkit-transform .5s cubic-bezier(.35,0,.25,1);transition:transform .5s cubic-bezier(.35,0,.25,1);transition:transform .5s cubic-bezier(.35,0,.25,1),-webkit-transform .5s cubic-bezier(.35,0,.25,1);overflow:auto;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}md-tab-content.md-no-scroll{bottom:auto;overflow:hidden}md-tab-content.md-no-transition,md-tab-content.ng-leave{-webkit-transition:none;transition:none}md-tab-content.md-left:not(.md-active){-webkit-transform:translateX(-100%);transform:translateX(-100%);-webkit-animation:1s md-tab-content-hide;animation:1s md-tab-content-hide;opacity:0}[dir=rtl] md-tab-content.md-left:not(.md-active){-webkit-transform:translateX(100%);transform:translateX(100%)}md-tab-content.md-left:not(.md-active) *{-webkit-transition:visibility 0s linear;transition:visibility 0s linear;-webkit-transition-delay:.5s;transition-delay:.5s;visibility:hidden}md-tab-content.md-right:not(.md-active){-webkit-transform:translateX(100%);transform:translateX(100%);-webkit-animation:1s md-tab-content-hide;animation:1s md-tab-content-hide;opacity:0}[dir=rtl] md-tab-content.md-right:not(.md-active){-webkit-transform:translateX(-100%);transform:translateX(-100%)}md-tab-content.md-right:not(.md-active) *{-webkit-transition:visibility 0s linear;transition:visibility 0s linear;-webkit-transition-delay:.5s;transition-delay:.5s;visibility:hidden}md-tab-content>div{-webkit-box-flex:1;-webkit-flex:1 0 100%;flex:1 0 100%;min-width:0}md-tab-content>div.ng-leave{-webkit-animation:1s md-tab-content-hide;animation:1s md-tab-content-hide}md-ink-bar{position:absolute;left:auto;right:auto;bottom:0;height:2px}md-ink-bar.md-left{-webkit-transition:left .125s cubic-bezier(.35,0,.25,1),right .25s cubic-bezier(.35,0,.25,1);transition:left .125s cubic-bezier(.35,0,.25,1),right .25s cubic-bezier(.35,0,.25,1)}md-ink-bar.md-right{-webkit-transition:left .25s cubic-bezier(.35,0,.25,1),right .125s cubic-bezier(.35,0,.25,1);transition:left .25s cubic-bezier(.35,0,.25,1),right .125s cubic-bezier(.35,0,.25,1)}md-tab{position:absolute;z-index:-1;left:-9999px}.md-tab{font-size:14px;text-align:center;line-height:24px;padding:12px 24px;-webkit-transition:background-color .35s cubic-bezier(.35,0,.25,1);transition:background-color .35s cubic-bezier(.35,0,.25,1);cursor:pointer;white-space:nowrap;position:relative;text-transform:uppercase;float:left;font-weight:500;box-sizing:border-box;overflow:hidden;text-overflow:ellipsis}[dir=rtl] .md-tab{float:right}.md-tab.md-focused{box-shadow:none;outline:none}.md-tab.md-active{cursor:default}.md-tab.md-disabled{pointer-events:none;touch-action:pan-y;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-user-drag:none;opacity:.5;cursor:default}.md-tab.ng-leave{-webkit-transition:none;transition:none}md-toolbar+md-tabs{border-top-left-radius:0;border-top-right-radius:0}.md-toast-text{padding:0 6px}md-toast{position:absolute;z-index:105;box-sizing:border-box;cursor:default;padding:8px;opacity:1}md-toast,md-toast .md-toast-content{overflow:hidden;-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1)}md-toast .md-toast-content{display:-webkit-box;display:-webkit-flex;display:flex;direction:row;-webkit-box-align:center;-webkit-align-items:center;align-items:center;max-height:168px;max-width:100%;min-height:48px;padding:0 18px;box-shadow:0 2px 5px 0 rgba(0,0,0,.26);border-radius:2px;font-size:14px;-webkit-transform:translate3d(0,0,0) rotateZ(0deg);transform:translate3d(0,0,0) rotateZ(0deg);-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}md-toast .md-toast-content:before{content:"";min-height:48px;visibility:hidden;display:inline-block}[dir=rtl] md-toast .md-toast-content{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}md-toast .md-toast-content span{-webkit-box-flex:1;-webkit-flex:1 1 0%;flex:1 1 0%;box-sizing:border-box;min-width:0}md-toast.md-capsule,md-toast.md-capsule .md-toast-content{border-radius:24px}md-toast.ng-leave-active .md-toast-content{-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2)}md-toast.md-swipedown .md-toast-content,md-toast.md-swipeleft .md-toast-content,md-toast.md-swiperight .md-toast-content,md-toast.md-swipeup .md-toast-content{-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1)}md-toast.ng-enter{opacity:0}md-toast.ng-enter .md-toast-content{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}md-toast.ng-enter.md-top .md-toast-content{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}md-toast.ng-enter.ng-enter-active{opacity:1}md-toast.ng-enter.ng-enter-active .md-toast-content{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}md-toast.ng-leave.ng-leave-active .md-toast-content{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}md-toast.ng-leave.ng-leave-active.md-swipeup .md-toast-content{-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0)}md-toast.ng-leave.ng-leave-active.md-swipedown .md-toast-content{-webkit-transform:translate3d(0,50%,0);transform:translate3d(0,50%,0)}md-toast.ng-leave.ng-leave-active.md-top .md-toast-content{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}md-toast .md-action{line-height:19px;margin-left:24px;margin-right:0;cursor:pointer;text-transform:uppercase;float:right}md-toast .md-button{min-width:0;margin-right:0;margin-left:12px}[dir=rtl] md-toast .md-button{margin-right:12px;margin-left:0}@media (max-width:959px){md-toast{left:0;right:0;width:100%;max-width:100%;min-width:0;border-radius:0;bottom:0;padding:0}md-toast.ng-leave.ng-leave-active.md-swipeup .md-toast-content{-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0)}md-toast.ng-leave.ng-leave-active.md-swipedown .md-toast-content{-webkit-transform:translate3d(0,50%,0);transform:translate3d(0,50%,0)}}@media (min-width:960px){md-toast{min-width:304px}md-toast.md-bottom{bottom:0}md-toast.md-left{left:0}md-toast.md-right{right:0}md-toast.md-top{top:0}md-toast._md-start{left:0}[dir=rtl] md-toast._md-start{left:auto;right:0}md-toast._md-end{right:0}[dir=rtl] md-toast._md-end{right:auto;left:0}md-toast.ng-leave.ng-leave-active.md-swipeleft .md-toast-content{-webkit-transform:translate3d(-50%,0,0);transform:translate3d(-50%,0,0)}md-toast.ng-leave.ng-leave-active.md-swiperight .md-toast-content{-webkit-transform:translate3d(50%,0,0);transform:translate3d(50%,0,0)}}@media (min-width:1920px){md-toast .md-toast-content{max-width:568px}}@media screen and (-ms-high-contrast:active){md-toast{border:1px solid #fff}}.md-toast-animating{overflow:hidden!important}md-toolbar{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;position:relative;z-index:2;font-size:20px;min-height:64px;width:100%}md-toolbar._md-toolbar-transitions{-webkit-transition-duration:.5s;transition-duration:.5s;-webkit-transition-timing-function:cubic-bezier(.35,0,.25,1);transition-timing-function:cubic-bezier(.35,0,.25,1);-webkit-transition-property:background-color,fill,color;transition-property:background-color,fill,color}md-toolbar.md-whiteframe-z1-add,md-toolbar.md-whiteframe-z1-remove{-webkit-transition:box-shadow .5s linear;transition:box-shadow .5s linear}md-toolbar md-toolbar-filler{width:72px}md-toolbar *,md-toolbar :after,md-toolbar :before{box-sizing:border-box}md-toolbar.ng-animate{-webkit-transition:none;transition:none}md-toolbar.md-tall{height:128px;min-height:128px;max-height:128px}md-toolbar.md-medium-tall{height:88px;min-height:88px;max-height:88px}md-toolbar.md-medium-tall .md-toolbar-tools{height:48px;min-height:48px;max-height:48px}md-toolbar>.md-indent{margin-left:64px}[dir=rtl] md-toolbar>.md-indent{margin-left:auto;margin-right:64px}md-toolbar~md-content>md-list{padding:0}md-toolbar~md-content>md-list md-list-item:last-child md-divider{display:none}.md-toolbar-tools{font-size:20px;letter-spacing:.005em;box-sizing:border-box;font-weight:400;display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row;width:100%;height:64px;max-height:64px;padding:0 16px;margin:0}.md-toolbar-tools h1,.md-toolbar-tools h2,.md-toolbar-tools h3{font-size:inherit;font-weight:inherit;margin:inherit}.md-toolbar-tools a{color:inherit;text-decoration:none}.md-toolbar-tools .fill-height{display:-webkit-box;display:-webkit-flex;display:flex;-webkit-box-align:center;-webkit-align-items:center;align-items:center}.md-toolbar-tools md-checkbox{margin:inherit}.md-toolbar-tools .md-button{margin-top:0;margin-bottom:0}.md-toolbar-tools .md-button,.md-toolbar-tools .md-button.md-icon-button md-icon{-webkit-transition-duration:.5s;transition-duration:.5s;-webkit-transition-timing-function:cubic-bezier(.35,0,.25,1);transition-timing-function:cubic-bezier(.35,0,.25,1);-webkit-transition-property:background-color,fill,color;transition-property:background-color,fill,color}.md-toolbar-tools .md-button.md-icon-button md-icon.ng-animate,.md-toolbar-tools .md-button.ng-animate{-webkit-transition:none;transition:none}.md-toolbar-tools>.md-button:first-child{margin-left:-8px}[dir=rtl] .md-toolbar-tools>.md-button:first-child{margin-left:auto;margin-right:-8px}.md-toolbar-tools>.md-button:last-child{margin-right:-8px}[dir=rtl] .md-toolbar-tools>.md-button:last-child{margin-right:auto;margin-left:-8px}.md-toolbar-tools>md-menu:last-child{margin-right:-8px}[dir=rtl] .md-toolbar-tools>md-menu:last-child{margin-right:auto;margin-left:-8px}.md-toolbar-tools>md-menu:last-child>.md-button{margin-right:0}[dir=rtl] .md-toolbar-tools>md-menu:last-child>.md-button{margin-right:auto;margin-left:0}@media screen and (-ms-high-contrast:active){.md-toolbar-tools{border-bottom:1px solid #fff}}@media (min-width:0) and (max-width:959px) and (orientation:portrait){md-toolbar{min-height:56px}.md-toolbar-tools{height:56px;max-height:56px}}@media (min-width:0) and (max-width:959px) and (orientation:landscape){md-toolbar{min-height:48px}.md-toolbar-tools{height:48px;max-height:48px}}.md-tooltip{pointer-events:none;border-radius:4px;overflow:hidden;opacity:0;font-weight:500;font-size:14px;white-space:nowrap;text-overflow:ellipsis;height:32px;line-height:32px;padding-right:16px;padding-left:16px}.md-tooltip.md-origin-top{-webkit-transform-origin:center bottom;transform-origin:center bottom;margin-top:-24px}.md-tooltip.md-origin-right{-webkit-transform-origin:left center;transform-origin:left center;margin-left:24px}.md-tooltip.md-origin-bottom{-webkit-transform-origin:center top;transform-origin:center top;margin-top:24px}.md-tooltip.md-origin-left{-webkit-transform-origin:right center;transform-origin:right center;margin-left:-24px}@media (min-width:960px){.md-tooltip{font-size:10px;height:22px;line-height:22px;padding-right:8px;padding-left:8px}.md-tooltip.md-origin-top{margin-top:-14px}.md-tooltip.md-origin-right{margin-left:14px}.md-tooltip.md-origin-bottom{margin-top:14px}.md-tooltip.md-origin-left{margin-left:-14px}}.md-tooltip.md-show-add{-webkit-transform:scale(0);transform:scale(0)}.md-tooltip.md-show{-webkit-transition:all .4s cubic-bezier(.25,.8,.25,1);transition:all .4s cubic-bezier(.25,.8,.25,1);-webkit-transition-duration:.15s;transition-duration:.15s;-webkit-transform:scale(1);transform:scale(1);opacity:.9}.md-tooltip.md-hide{-webkit-transition:all .3s cubic-bezier(.55,0,.55,.2);transition:all .3s cubic-bezier(.55,0,.55,.2);-webkit-transition-duration:.15s;transition-duration:.15s;-webkit-transform:scale(0);transform:scale(0);opacity:0}.md-truncate{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.md-truncate.md-clip{text-overflow:clip}.md-truncate.flex{width:0}.md-virtual-repeat-container{box-sizing:border-box;display:block;margin:0;overflow:hidden;padding:0;position:relative}.md-virtual-repeat-container .md-virtual-repeat-scroller{bottom:0;box-sizing:border-box;left:0;margin:0;overflow-x:hidden;padding:0;position:absolute;right:0;top:0;-webkit-overflow-scrolling:touch}.md-virtual-repeat-container .md-virtual-repeat-sizer{box-sizing:border-box;height:1px;display:block;margin:0;padding:0;width:1px}.md-virtual-repeat-container .md-virtual-repeat-offsetter{box-sizing:border-box;left:0;margin:0;padding:0;position:absolute;right:0;top:0}.md-virtual-repeat-container.md-orient-horizontal .md-virtual-repeat-scroller{overflow-x:auto;overflow-y:hidden}.md-virtual-repeat-container.md-orient-horizontal .md-virtual-repeat-offsetter{bottom:16px;right:auto;white-space:nowrap}[dir=rtl] .md-virtual-repeat-container.md-orient-horizontal .md-virtual-repeat-offsetter{right:auto;left:auto}.md-whiteframe-1dp,.md-whiteframe-z1{box-shadow:0 1px 3px 0 rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 2px 1px -1px rgba(0,0,0,.12)}.md-whiteframe-2dp{box-shadow:0 1px 5px 0 rgba(0,0,0,.2),0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.12)}.md-whiteframe-3dp{box-shadow:0 1px 8px 0 rgba(0,0,0,.2),0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.12)}.md-whiteframe-4dp,.md-whiteframe-z2{box-shadow:0 2px 4px -1px rgba(0,0,0,.2),0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12)}.md-whiteframe-5dp{box-shadow:0 3px 5px -1px rgba(0,0,0,.2),0 5px 8px 0 rgba(0,0,0,.14),0 1px 14px 0 rgba(0,0,0,.12)}.md-whiteframe-6dp{box-shadow:0 3px 5px -1px rgba(0,0,0,.2),0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12)}.md-whiteframe-7dp,.md-whiteframe-z3{box-shadow:0 4px 5px -2px rgba(0,0,0,.2),0 7px 10px 1px rgba(0,0,0,.14),0 2px 16px 1px rgba(0,0,0,.12)}.md-whiteframe-8dp{box-shadow:0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12)}.md-whiteframe-9dp{box-shadow:0 5px 6px -3px rgba(0,0,0,.2),0 9px 12px 1px rgba(0,0,0,.14),0 3px 16px 2px rgba(0,0,0,.12)}.md-whiteframe-10dp,.md-whiteframe-z4{box-shadow:0 6px 6px -3px rgba(0,0,0,.2),0 10px 14px 1px rgba(0,0,0,.14),0 4px 18px 3px rgba(0,0,0,.12)}.md-whiteframe-11dp{box-shadow:0 6px 7px -4px rgba(0,0,0,.2),0 11px 15px 1px rgba(0,0,0,.14),0 4px 20px 3px rgba(0,0,0,.12)}.md-whiteframe-12dp{box-shadow:0 7px 8px -4px rgba(0,0,0,.2),0 12px 17px 2px rgba(0,0,0,.14),0 5px 22px 4px rgba(0,0,0,.12)}.md-whiteframe-13dp,.md-whiteframe-z5{box-shadow:0 7px 8px -4px rgba(0,0,0,.2),0 13px 19px 2px rgba(0,0,0,.14),0 5px 24px 4px rgba(0,0,0,.12)}.md-whiteframe-14dp{box-shadow:0 7px 9px -4px rgba(0,0,0,.2),0 14px 21px 2px rgba(0,0,0,.14),0 5px 26px 4px rgba(0,0,0,.12)}.md-whiteframe-15dp{box-shadow:0 8px 9px -5px rgba(0,0,0,.2),0 15px 22px 2px rgba(0,0,0,.14),0 6px 28px 5px rgba(0,0,0,.12)}.md-whiteframe-16dp{box-shadow:0 8px 10px -5px rgba(0,0,0,.2),0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12)}.md-whiteframe-17dp{box-shadow:0 8px 11px -5px rgba(0,0,0,.2),0 17px 26px 2px rgba(0,0,0,.14),0 6px 32px 5px rgba(0,0,0,.12)}.md-whiteframe-18dp{box-shadow:0 9px 11px -5px rgba(0,0,0,.2),0 18px 28px 2px rgba(0,0,0,.14),0 7px 34px 6px rgba(0,0,0,.12)}.md-whiteframe-19dp{box-shadow:0 9px 12px -6px rgba(0,0,0,.2),0 19px 29px 2px rgba(0,0,0,.14),0 7px 36px 6px rgba(0,0,0,.12)}.md-whiteframe-20dp{box-shadow:0 10px 13px -6px rgba(0,0,0,.2),0 20px 31px 3px rgba(0,0,0,.14),0 8px 38px 7px rgba(0,0,0,.12)}.md-whiteframe-21dp{box-shadow:0 10px 13px -6px rgba(0,0,0,.2),0 21px 33px 3px rgba(0,0,0,.14),0 8px 40px 7px rgba(0,0,0,.12)}.md-whiteframe-22dp{box-shadow:0 10px 14px -6px rgba(0,0,0,.2),0 22px 35px 3px rgba(0,0,0,.14),0 8px 42px 7px rgba(0,0,0,.12)}.md-whiteframe-23dp{box-shadow:0 11px 14px -7px rgba(0,0,0,.2),0 23px 36px 3px rgba(0,0,0,.14),0 9px 44px 8px rgba(0,0,0,.12)}.md-whiteframe-24dp{box-shadow:0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14),0 9px 46px 8px rgba(0,0,0,.12)}@media screen and (-ms-high-contrast:active){md-whiteframe{border:1px solid #fff}}@media print{[md-whiteframe],md-whiteframe{background-color:#fff}}.ng-cloak,.x-ng-cloak,[data-ng-cloak],[ng-cloak],[ng\:cloak],[x-ng-cloak]{display:none!important}@-moz-document url-prefix(){.layout-fill{margin:0;width:100%;min-height:100%;height:100%}}.flex-order{-webkit-box-ordinal-group:1;-webkit-order:0;order:0}.flex-order--20{-webkit-box-ordinal-group:-19;-webkit-order:-20;order:-20}.flex-order--19{-webkit-box-ordinal-group:-18;-webkit-order:-19;order:-19}.flex-order--18{-webkit-box-ordinal-group:-17;-webkit-order:-18;order:-18}.flex-order--17{-webkit-box-ordinal-group:-16;-webkit-order:-17;order:-17}.flex-order--16{-webkit-box-ordinal-group:-15;-webkit-order:-16;order:-16}.flex-order--15{-webkit-box-ordinal-group:-14;-webkit-order:-15;order:-15}.flex-order--14{-webkit-box-ordinal-group:-13;-webkit-order:-14;order:-14}.flex-order--13{-webkit-box-ordinal-group:-12;-webkit-order:-13;order:-13}.flex-order--12{-webkit-box-ordinal-group:-11;-webkit-order:-12;order:-12}.flex-order--11{-webkit-box-ordinal-group:-10;-webkit-order:-11;order:-11}.flex-order--10{-webkit-box-ordinal-group:-9;-webkit-order:-10;order:-10}.flex-order--9{-webkit-box-ordinal-group:-8;-webkit-order:-9;order:-9}.flex-order--8{-webkit-box-ordinal-group:-7;-webkit-order:-8;order:-8}.flex-order--7{-webkit-box-ordinal-group:-6;-webkit-order:-7;order:-7}.flex-order--6{-webkit-box-ordinal-group:-5;-webkit-order:-6;order:-6}.flex-order--5{-webkit-box-ordinal-group:-4;-webkit-order:-5;order:-5}.flex-order--4{-webkit-box-ordinal-group:-3;-webkit-order:-4;order:-4}.flex-order--3{-webkit-box-ordinal-group:-2;-webkit-order:-3;order:-3}.flex-order--2{-webkit-box-ordinal-group:-1;-webkit-order:-2;order:-2}.flex-order--1{-webkit-box-ordinal-group:0;-webkit-order:-1;order:-1}.flex-order-0{-webkit-box-ordinal-group:1;-webkit-order:0;order:0}.flex-order-1{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.flex-order-2{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.flex-order-3{-webkit-box-ordinal-group:4;-webkit-order:3;order:3}.flex-order-4{-webkit-box-ordinal-group:5;-webkit-order:4;order:4}.flex-order-5{-webkit-box-ordinal-group:6;-webkit-order:5;order:5}.flex-order-6{-webkit-box-ordinal-group:7;-webkit-order:6;order:6}.flex-order-7{-webkit-box-ordinal-group:8;-webkit-order:7;order:7}.flex-order-8{-webkit-box-ordinal-group:9;-webkit-order:8;order:8}.flex-order-9{-webkit-box-ordinal-group:10;-webkit-order:9;order:9}.flex-order-10{-webkit-box-ordinal-group:11;-webkit-order:10;order:10}.flex-order-11{-webkit-box-ordinal-group:12;-webkit-order:11;order:11}.flex-order-12{-webkit-box-ordinal-group:13;-webkit-order:12;order:12}.flex-order-13{-webkit-box-ordinal-group:14;-webkit-order:13;order:13}.flex-order-14{-webkit-box-ordinal-group:15;-webkit-order:14;order:14}.flex-order-15{-webkit-box-ordinal-group:16;-webkit-order:15;order:15}.flex-order-16{-webkit-box-ordinal-group:17;-webkit-order:16;order:16}.flex-order-17{-webkit-box-ordinal-group:18;-webkit-order:17;order:17}.flex-order-18{-webkit-box-ordinal-group:19;-webkit-order:18;order:18}.flex-order-19{-webkit-box-ordinal-group:20;-webkit-order:19;order:19}.flex-order-20{-webkit-box-ordinal-group:21;-webkit-order:20;order:20}.flex-offset-0,.offset-0{margin-left:0}[dir=rtl] .flex-offset-0,[dir=rtl] .offset-0{margin-left:auto;margin-right:0}.flex-offset-5,.offset-5{margin-left:5%}[dir=rtl] .flex-offset-5,[dir=rtl] .offset-5{margin-left:auto;margin-right:5%}.flex-offset-10,.offset-10{margin-left:10%}[dir=rtl] .flex-offset-10,[dir=rtl] .offset-10{margin-left:auto;margin-right:10%}.flex-offset-15,.offset-15{margin-left:15%}[dir=rtl] .flex-offset-15,[dir=rtl] .offset-15{margin-left:auto;margin-right:15%}.flex-offset-20,.offset-20{margin-left:20%}[dir=rtl] .flex-offset-20,[dir=rtl] .offset-20{margin-left:auto;margin-right:20%}.flex-offset-25,.offset-25{margin-left:25%}[dir=rtl] .flex-offset-25,[dir=rtl] .offset-25{margin-left:auto;margin-right:25%}.flex-offset-30,.offset-30{margin-left:30%}[dir=rtl] .flex-offset-30,[dir=rtl] .offset-30{margin-left:auto;margin-right:30%}.flex-offset-35,.offset-35{margin-left:35%}[dir=rtl] .flex-offset-35,[dir=rtl] .offset-35{margin-left:auto;margin-right:35%}.flex-offset-40,.offset-40{margin-left:40%}[dir=rtl] .flex-offset-40,[dir=rtl] .offset-40{margin-left:auto;margin-right:40%}.flex-offset-45,.offset-45{margin-left:45%}[dir=rtl] .flex-offset-45,[dir=rtl] .offset-45{margin-left:auto;margin-right:45%}.flex-offset-50,.offset-50{margin-left:50%}[dir=rtl] .flex-offset-50,[dir=rtl] .offset-50{margin-left:auto;margin-right:50%}.flex-offset-55,.offset-55{margin-left:55%}[dir=rtl] .flex-offset-55,[dir=rtl] .offset-55{margin-left:auto;margin-right:55%}.flex-offset-60,.offset-60{margin-left:60%}[dir=rtl] .flex-offset-60,[dir=rtl] .offset-60{margin-left:auto;margin-right:60%}.flex-offset-65,.offset-65{margin-left:65%}[dir=rtl] .flex-offset-65,[dir=rtl] .offset-65{margin-left:auto;margin-right:65%}.flex-offset-70,.offset-70{margin-left:70%}[dir=rtl] .flex-offset-70,[dir=rtl] .offset-70{margin-left:auto;margin-right:70%}.flex-offset-75,.offset-75{margin-left:75%}[dir=rtl] .flex-offset-75,[dir=rtl] .offset-75{margin-left:auto;margin-right:75%}.flex-offset-80,.offset-80{margin-left:80%}[dir=rtl] .flex-offset-80,[dir=rtl] .offset-80{margin-left:auto;margin-right:80%}.flex-offset-85,.offset-85{margin-left:85%}[dir=rtl] .flex-offset-85,[dir=rtl] .offset-85{margin-left:auto;margin-right:85%}.flex-offset-90,.offset-90{margin-left:90%}[dir=rtl] .flex-offset-90,[dir=rtl] .offset-90{margin-left:auto;margin-right:90%}.flex-offset-95,.offset-95{margin-left:95%}[dir=rtl] .flex-offset-95,[dir=rtl] .offset-95{margin-left:auto;margin-right:95%}.flex-offset-33,.offset-33{margin-left:33.33333%}.flex-offset-66,.offset-66{margin-left:66.66667%}[dir=rtl] .flex-offset-66,[dir=rtl] .offset-66{margin-left:auto;margin-right:66.66667%}.layout-align,.layout-align-start-stretch{-webkit-align-content:stretch;align-content:stretch;-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch}.layout-align,.layout-align-start,.layout-align-start-center,.layout-align-start-end,.layout-align-start-start,.layout-align-start-stretch{-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.layout-align-center,.layout-align-center-center,.layout-align-center-end,.layout-align-center-start,.layout-align-center-stretch{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.layout-align-end,.layout-align-end-center,.layout-align-end-end,.layout-align-end-start,.layout-align-end-stretch{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.layout-align-space-around,.layout-align-space-around-center,.layout-align-space-around-end,.layout-align-space-around-start,.layout-align-space-around-stretch{-webkit-justify-content:space-around;justify-content:space-around}.layout-align-space-between,.layout-align-space-between-center,.layout-align-space-between-end,.layout-align-space-between-start,.layout-align-space-between-stretch{-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.layout-align-center-start,.layout-align-end-start,.layout-align-space-around-start,.layout-align-space-between-start,.layout-align-start-start{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.layout-align-center-center,.layout-align-end-center,.layout-align-space-around-center,.layout-align-space-between-center,.layout-align-start-center{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-content:center;align-content:center;max-width:100%}.layout-align-center-center>*,.layout-align-end-center>*,.layout-align-space-around-center>*,.layout-align-space-between-center>*,.layout-align-start-center>*{max-width:100%;box-sizing:border-box}.layout-align-center-end,.layout-align-end-end,.layout-align-space-around-end,.layout-align-space-between-end,.layout-align-start-end{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;-webkit-align-content:flex-end;align-content:flex-end}.layout-align-center-stretch,.layout-align-end-stretch,.layout-align-space-around-stretch,.layout-align-space-between-stretch,.layout-align-start-stretch{-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch;-webkit-align-content:stretch;align-content:stretch}.flex{-webkit-flex:1;flex:1}.flex,.flex-grow{-webkit-box-flex:1;box-sizing:border-box}.flex-grow{-webkit-flex:1 1 100%;flex:1 1 100%}.flex-initial{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-auto{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;box-sizing:border-box}.flex-none{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;box-sizing:border-box}.flex-noshrink{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;box-sizing:border-box}.flex-nogrow{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-0{max-width:0;max-height:100%}.flex-0,.layout-column>.flex-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-0{max-width:100%;max-height:0%}.layout-row>.flex-0{max-width:0;max-height:100%;min-width:0}.layout-column>.flex-0,.layout-row>.flex-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-0{max-width:100%;max-height:0%;min-height:0}.flex-5,.layout-row>.flex-5{max-width:5%;max-height:100%}.flex-5,.layout-column>.flex-5,.layout-row>.flex-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-5{max-width:100%;max-height:5%}.flex-10,.layout-row>.flex-10{max-width:10%;max-height:100%}.flex-10,.layout-column>.flex-10,.layout-row>.flex-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-10{max-width:100%;max-height:10%}.flex-15,.layout-row>.flex-15{max-width:15%;max-height:100%}.flex-15,.layout-column>.flex-15,.layout-row>.flex-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-15{max-width:100%;max-height:15%}.flex-20,.layout-row>.flex-20{max-width:20%;max-height:100%}.flex-20,.layout-column>.flex-20,.layout-row>.flex-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-20{max-width:100%;max-height:20%}.flex-25,.layout-row>.flex-25{max-width:25%;max-height:100%}.flex-25,.layout-column>.flex-25,.layout-row>.flex-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-25{max-width:100%;max-height:25%}.flex-30,.layout-row>.flex-30{max-width:30%;max-height:100%}.flex-30,.layout-column>.flex-30,.layout-row>.flex-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-30{max-width:100%;max-height:30%}.flex-35,.layout-row>.flex-35{max-width:35%;max-height:100%}.flex-35,.layout-column>.flex-35,.layout-row>.flex-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-35{max-width:100%;max-height:35%}.flex-40,.layout-row>.flex-40{max-width:40%;max-height:100%}.flex-40,.layout-column>.flex-40,.layout-row>.flex-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-40{max-width:100%;max-height:40%}.flex-45,.layout-row>.flex-45{max-width:45%;max-height:100%}.flex-45,.layout-column>.flex-45,.layout-row>.flex-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-45{max-width:100%;max-height:45%}.flex-50,.layout-row>.flex-50{max-width:50%;max-height:100%}.flex-50,.layout-column>.flex-50,.layout-row>.flex-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-50{max-width:100%;max-height:50%}.flex-55,.layout-row>.flex-55{max-width:55%;max-height:100%}.flex-55,.layout-column>.flex-55,.layout-row>.flex-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-55{max-width:100%;max-height:55%}.flex-60,.layout-row>.flex-60{max-width:60%;max-height:100%}.flex-60,.layout-column>.flex-60,.layout-row>.flex-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-60{max-width:100%;max-height:60%}.flex-65,.layout-row>.flex-65{max-width:65%;max-height:100%}.flex-65,.layout-column>.flex-65,.layout-row>.flex-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-65{max-width:100%;max-height:65%}.flex-70,.layout-row>.flex-70{max-width:70%;max-height:100%}.flex-70,.layout-column>.flex-70,.layout-row>.flex-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-70{max-width:100%;max-height:70%}.flex-75,.layout-row>.flex-75{max-width:75%;max-height:100%}.flex-75,.layout-column>.flex-75,.layout-row>.flex-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-75{max-width:100%;max-height:75%}.flex-80,.layout-row>.flex-80{max-width:80%;max-height:100%}.flex-80,.layout-column>.flex-80,.layout-row>.flex-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-80{max-width:100%;max-height:80%}.flex-85,.layout-row>.flex-85{max-width:85%;max-height:100%}.flex-85,.layout-column>.flex-85,.layout-row>.flex-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-85{max-width:100%;max-height:85%}.flex-90,.layout-row>.flex-90{max-width:90%;max-height:100%}.flex-90,.layout-column>.flex-90,.layout-row>.flex-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-90{max-width:100%;max-height:90%}.flex-95,.layout-row>.flex-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:95%;max-height:100%;box-sizing:border-box}.layout-column>.flex-95{max-height:95%}.flex-100,.layout-column>.flex-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.flex-100{max-height:100%}.layout-column>.flex-100,.layout-row>.flex-100{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:100%;box-sizing:border-box}.layout-row>.flex-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:33.33%}.layout-row>.flex-33,.layout-row>.flex-66{-webkit-box-flex:1;max-height:100%;box-sizing:border-box}.layout-row>.flex-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:66.66%}.layout-column>.flex-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-height:33.33%}.layout-column>.flex-33,.layout-column>.flex-66{-webkit-box-flex:1;max-width:100%;box-sizing:border-box}.layout-column>.flex-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-height:66.66%}.layout-row>.flex-33{max-width:33.33%}.layout-row>.flex-33,.layout-row>.flex-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-height:100%;box-sizing:border-box}.layout-row>.flex-66{max-width:66.66%}.layout-row>.flex{min-width:0}.layout-column>.flex-33{max-height:33.33%}.layout-column>.flex-33,.layout-column>.flex-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.layout-column>.flex-66{max-height:66.66%}.layout-column>.flex{min-height:0}.layout,.layout-column,.layout-row{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex}.layout-column{-webkit-box-orient:vertical;-webkit-flex-direction:column;flex-direction:column}.layout-column,.layout-row{-webkit-box-direction:normal}.layout-row{-webkit-box-orient:horizontal;-webkit-flex-direction:row;flex-direction:row}.layout-padding-sm>*,.layout-padding>.flex-sm{padding:4px}.layout-padding,.layout-padding-gt-sm,.layout-padding-gt-sm>*,.layout-padding-md,.layout-padding-md>*,.layout-padding>*,.layout-padding>.flex,.layout-padding>.flex-gt-sm,.layout-padding>.flex-md{padding:8px}.layout-padding-gt-lg>*,.layout-padding-gt-md>*,.layout-padding-lg>*,.layout-padding>.flex-gt-lg,.layout-padding>.flex-gt-md,.layout-padding>.flex-lg{padding:16px}.layout-margin-sm>*,.layout-margin>.flex-sm{margin:4px}.layout-margin,.layout-margin-gt-sm,.layout-margin-gt-sm>*,.layout-margin-md,.layout-margin-md>*,.layout-margin>*,.layout-margin>.flex,.layout-margin>.flex-gt-sm,.layout-margin>.flex-md{margin:8px}.layout-margin-gt-lg>*,.layout-margin-gt-md>*,.layout-margin-lg>*,.layout-margin>.flex-gt-lg,.layout-margin>.flex-gt-md,.layout-margin>.flex-lg{margin:16px}.layout-wrap{-webkit-flex-wrap:wrap;flex-wrap:wrap}.layout-nowrap{-webkit-flex-wrap:nowrap;flex-wrap:nowrap}.layout-fill{margin:0;width:100%;min-height:100%;height:100%}@media (max-width:599px){.hide-xs:not(.show-xs):not(.show),.hide:not(.show-xs):not(.show){display:none}.flex-order-xs--20{-webkit-box-ordinal-group:-19;-webkit-order:-20;order:-20}.flex-order-xs--19{-webkit-box-ordinal-group:-18;-webkit-order:-19;order:-19}.flex-order-xs--18{-webkit-box-ordinal-group:-17;-webkit-order:-18;order:-18}.flex-order-xs--17{-webkit-box-ordinal-group:-16;-webkit-order:-17;order:-17}.flex-order-xs--16{-webkit-box-ordinal-group:-15;-webkit-order:-16;order:-16}.flex-order-xs--15{-webkit-box-ordinal-group:-14;-webkit-order:-15;order:-15}.flex-order-xs--14{-webkit-box-ordinal-group:-13;-webkit-order:-14;order:-14}.flex-order-xs--13{-webkit-box-ordinal-group:-12;-webkit-order:-13;order:-13}.flex-order-xs--12{-webkit-box-ordinal-group:-11;-webkit-order:-12;order:-12}.flex-order-xs--11{-webkit-box-ordinal-group:-10;-webkit-order:-11;order:-11}.flex-order-xs--10{-webkit-box-ordinal-group:-9;-webkit-order:-10;order:-10}.flex-order-xs--9{-webkit-box-ordinal-group:-8;-webkit-order:-9;order:-9}.flex-order-xs--8{-webkit-box-ordinal-group:-7;-webkit-order:-8;order:-8}.flex-order-xs--7{-webkit-box-ordinal-group:-6;-webkit-order:-7;order:-7}.flex-order-xs--6{-webkit-box-ordinal-group:-5;-webkit-order:-6;order:-6}.flex-order-xs--5{-webkit-box-ordinal-group:-4;-webkit-order:-5;order:-5}.flex-order-xs--4{-webkit-box-ordinal-group:-3;-webkit-order:-4;order:-4}.flex-order-xs--3{-webkit-box-ordinal-group:-2;-webkit-order:-3;order:-3}.flex-order-xs--2{-webkit-box-ordinal-group:-1;-webkit-order:-2;order:-2}.flex-order-xs--1{-webkit-box-ordinal-group:0;-webkit-order:-1;order:-1}.flex-order-xs-0{-webkit-box-ordinal-group:1;-webkit-order:0;order:0}.flex-order-xs-1{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.flex-order-xs-2{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.flex-order-xs-3{-webkit-box-ordinal-group:4;-webkit-order:3;order:3}.flex-order-xs-4{-webkit-box-ordinal-group:5;-webkit-order:4;order:4}.flex-order-xs-5{-webkit-box-ordinal-group:6;-webkit-order:5;order:5}.flex-order-xs-6{-webkit-box-ordinal-group:7;-webkit-order:6;order:6}.flex-order-xs-7{-webkit-box-ordinal-group:8;-webkit-order:7;order:7}.flex-order-xs-8{-webkit-box-ordinal-group:9;-webkit-order:8;order:8}.flex-order-xs-9{-webkit-box-ordinal-group:10;-webkit-order:9;order:9}.flex-order-xs-10{-webkit-box-ordinal-group:11;-webkit-order:10;order:10}.flex-order-xs-11{-webkit-box-ordinal-group:12;-webkit-order:11;order:11}.flex-order-xs-12{-webkit-box-ordinal-group:13;-webkit-order:12;order:12}.flex-order-xs-13{-webkit-box-ordinal-group:14;-webkit-order:13;order:13}.flex-order-xs-14{-webkit-box-ordinal-group:15;-webkit-order:14;order:14}.flex-order-xs-15{-webkit-box-ordinal-group:16;-webkit-order:15;order:15}.flex-order-xs-16{-webkit-box-ordinal-group:17;-webkit-order:16;order:16}.flex-order-xs-17{-webkit-box-ordinal-group:18;-webkit-order:17;order:17}.flex-order-xs-18{-webkit-box-ordinal-group:19;-webkit-order:18;order:18}.flex-order-xs-19{-webkit-box-ordinal-group:20;-webkit-order:19;order:19}.flex-order-xs-20{-webkit-box-ordinal-group:21;-webkit-order:20;order:20}.flex-offset-xs-0,.offset-xs-0{margin-left:0}[dir=rtl] .flex-offset-xs-0,[dir=rtl] .offset-xs-0{margin-left:auto;margin-right:0}.flex-offset-xs-5,.offset-xs-5{margin-left:5%}[dir=rtl] .flex-offset-xs-5,[dir=rtl] .offset-xs-5{margin-left:auto;margin-right:5%}.flex-offset-xs-10,.offset-xs-10{margin-left:10%}[dir=rtl] .flex-offset-xs-10,[dir=rtl] .offset-xs-10{margin-left:auto;margin-right:10%}.flex-offset-xs-15,.offset-xs-15{margin-left:15%}[dir=rtl] .flex-offset-xs-15,[dir=rtl] .offset-xs-15{margin-left:auto;margin-right:15%}.flex-offset-xs-20,.offset-xs-20{margin-left:20%}[dir=rtl] .flex-offset-xs-20,[dir=rtl] .offset-xs-20{margin-left:auto;margin-right:20%}.flex-offset-xs-25,.offset-xs-25{margin-left:25%}[dir=rtl] .flex-offset-xs-25,[dir=rtl] .offset-xs-25{margin-left:auto;margin-right:25%}.flex-offset-xs-30,.offset-xs-30{margin-left:30%}[dir=rtl] .flex-offset-xs-30,[dir=rtl] .offset-xs-30{margin-left:auto;margin-right:30%}.flex-offset-xs-35,.offset-xs-35{margin-left:35%}[dir=rtl] .flex-offset-xs-35,[dir=rtl] .offset-xs-35{margin-left:auto;margin-right:35%}.flex-offset-xs-40,.offset-xs-40{margin-left:40%}[dir=rtl] .flex-offset-xs-40,[dir=rtl] .offset-xs-40{margin-left:auto;margin-right:40%}.flex-offset-xs-45,.offset-xs-45{margin-left:45%}[dir=rtl] .flex-offset-xs-45,[dir=rtl] .offset-xs-45{margin-left:auto;margin-right:45%}.flex-offset-xs-50,.offset-xs-50{margin-left:50%}[dir=rtl] .flex-offset-xs-50,[dir=rtl] .offset-xs-50{margin-left:auto;margin-right:50%}.flex-offset-xs-55,.offset-xs-55{margin-left:55%}[dir=rtl] .flex-offset-xs-55,[dir=rtl] .offset-xs-55{margin-left:auto;margin-right:55%}.flex-offset-xs-60,.offset-xs-60{margin-left:60%}[dir=rtl] .flex-offset-xs-60,[dir=rtl] .offset-xs-60{margin-left:auto;margin-right:60%}.flex-offset-xs-65,.offset-xs-65{margin-left:65%}[dir=rtl] .flex-offset-xs-65,[dir=rtl] .offset-xs-65{margin-left:auto;margin-right:65%}.flex-offset-xs-70,.offset-xs-70{margin-left:70%}[dir=rtl] .flex-offset-xs-70,[dir=rtl] .offset-xs-70{margin-left:auto;margin-right:70%}.flex-offset-xs-75,.offset-xs-75{margin-left:75%}[dir=rtl] .flex-offset-xs-75,[dir=rtl] .offset-xs-75{margin-left:auto;margin-right:75%}.flex-offset-xs-80,.offset-xs-80{margin-left:80%}[dir=rtl] .flex-offset-xs-80,[dir=rtl] .offset-xs-80{margin-left:auto;margin-right:80%}.flex-offset-xs-85,.offset-xs-85{margin-left:85%}[dir=rtl] .flex-offset-xs-85,[dir=rtl] .offset-xs-85{margin-left:auto;margin-right:85%}.flex-offset-xs-90,.offset-xs-90{margin-left:90%}[dir=rtl] .flex-offset-xs-90,[dir=rtl] .offset-xs-90{margin-left:auto;margin-right:90%}.flex-offset-xs-95,.offset-xs-95{margin-left:95%}[dir=rtl] .flex-offset-xs-95,[dir=rtl] .offset-xs-95{margin-left:auto;margin-right:95%}.flex-offset-xs-33,.offset-xs-33{margin-left:33.33333%}.flex-offset-xs-66,.offset-xs-66{margin-left:66.66667%}[dir=rtl] .flex-offset-xs-66,[dir=rtl] .offset-xs-66{margin-left:auto;margin-right:66.66667%}.layout-align-xs,.layout-align-xs-start-stretch{-webkit-align-content:stretch;align-content:stretch;-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch}.layout-align-xs,.layout-align-xs-start,.layout-align-xs-start-center,.layout-align-xs-start-end,.layout-align-xs-start-start,.layout-align-xs-start-stretch{-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.layout-align-xs-center,.layout-align-xs-center-center,.layout-align-xs-center-end,.layout-align-xs-center-start,.layout-align-xs-center-stretch{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.layout-align-xs-end,.layout-align-xs-end-center,.layout-align-xs-end-end,.layout-align-xs-end-start,.layout-align-xs-end-stretch{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.layout-align-xs-space-around,.layout-align-xs-space-around-center,.layout-align-xs-space-around-end,.layout-align-xs-space-around-start,.layout-align-xs-space-around-stretch{-webkit-justify-content:space-around;justify-content:space-around}.layout-align-xs-space-between,.layout-align-xs-space-between-center,.layout-align-xs-space-between-end,.layout-align-xs-space-between-start,.layout-align-xs-space-between-stretch{-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.layout-align-xs-center-start,.layout-align-xs-end-start,.layout-align-xs-space-around-start,.layout-align-xs-space-between-start,.layout-align-xs-start-start{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.layout-align-xs-center-center,.layout-align-xs-end-center,.layout-align-xs-space-around-center,.layout-align-xs-space-between-center,.layout-align-xs-start-center{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-content:center;align-content:center;max-width:100%}.layout-align-xs-center-center>*,.layout-align-xs-end-center>*,.layout-align-xs-space-around-center>*,.layout-align-xs-space-between-center>*,.layout-align-xs-start-center>*{max-width:100%;box-sizing:border-box}.layout-align-xs-center-end,.layout-align-xs-end-end,.layout-align-xs-space-around-end,.layout-align-xs-space-between-end,.layout-align-xs-start-end{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;-webkit-align-content:flex-end;align-content:flex-end}.layout-align-xs-center-stretch,.layout-align-xs-end-stretch,.layout-align-xs-space-around-stretch,.layout-align-xs-space-between-stretch,.layout-align-xs-start-stretch{-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch;-webkit-align-content:stretch;align-content:stretch}.flex-xs{-webkit-flex:1;flex:1}.flex-xs,.flex-xs-grow{-webkit-box-flex:1;box-sizing:border-box}.flex-xs-grow{-webkit-flex:1 1 100%;flex:1 1 100%}.flex-xs-initial{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-xs-auto{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;box-sizing:border-box}.flex-xs-none{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;box-sizing:border-box}.flex-xs-noshrink{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;box-sizing:border-box}.flex-xs-nogrow{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-xs-0,.layout-row>.flex-xs-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:0;max-height:100%;box-sizing:border-box}.layout-row>.flex-xs-0{min-width:0}.layout-column>.flex-xs-0{max-width:100%;max-height:0%}.layout-column>.flex-xs-0,.layout-xs-row>.flex-xs-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-row>.flex-xs-0{max-width:0;max-height:100%;min-width:0}.layout-xs-column>.flex-xs-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:0%;box-sizing:border-box;min-height:0}.flex-xs-5,.layout-row>.flex-xs-5{max-width:5%;max-height:100%}.flex-xs-5,.layout-column>.flex-xs-5,.layout-row>.flex-xs-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-5{max-width:100%;max-height:5%}.layout-xs-row>.flex-xs-5{max-width:5%;max-height:100%}.layout-xs-column>.flex-xs-5,.layout-xs-row>.flex-xs-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-5{max-width:100%;max-height:5%}.flex-xs-10,.layout-row>.flex-xs-10{max-width:10%;max-height:100%}.flex-xs-10,.layout-column>.flex-xs-10,.layout-row>.flex-xs-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-10{max-width:100%;max-height:10%}.layout-xs-row>.flex-xs-10{max-width:10%;max-height:100%}.layout-xs-column>.flex-xs-10,.layout-xs-row>.flex-xs-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-10{max-width:100%;max-height:10%}.flex-xs-15,.layout-row>.flex-xs-15{max-width:15%;max-height:100%}.flex-xs-15,.layout-column>.flex-xs-15,.layout-row>.flex-xs-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-15{max-width:100%;max-height:15%}.layout-xs-row>.flex-xs-15{max-width:15%;max-height:100%}.layout-xs-column>.flex-xs-15,.layout-xs-row>.flex-xs-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-15{max-width:100%;max-height:15%}.flex-xs-20,.layout-row>.flex-xs-20{max-width:20%;max-height:100%}.flex-xs-20,.layout-column>.flex-xs-20,.layout-row>.flex-xs-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-20{max-width:100%;max-height:20%}.layout-xs-row>.flex-xs-20{max-width:20%;max-height:100%}.layout-xs-column>.flex-xs-20,.layout-xs-row>.flex-xs-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-20{max-width:100%;max-height:20%}.flex-xs-25,.layout-row>.flex-xs-25{max-width:25%;max-height:100%}.flex-xs-25,.layout-column>.flex-xs-25,.layout-row>.flex-xs-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-25{max-width:100%;max-height:25%}.layout-xs-row>.flex-xs-25{max-width:25%;max-height:100%}.layout-xs-column>.flex-xs-25,.layout-xs-row>.flex-xs-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-25{max-width:100%;max-height:25%}.flex-xs-30,.layout-row>.flex-xs-30{max-width:30%;max-height:100%}.flex-xs-30,.layout-column>.flex-xs-30,.layout-row>.flex-xs-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-30{max-width:100%;max-height:30%}.layout-xs-row>.flex-xs-30{max-width:30%;max-height:100%}.layout-xs-column>.flex-xs-30,.layout-xs-row>.flex-xs-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-30{max-width:100%;max-height:30%}.flex-xs-35,.layout-row>.flex-xs-35{max-width:35%;max-height:100%}.flex-xs-35,.layout-column>.flex-xs-35,.layout-row>.flex-xs-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-35{max-width:100%;max-height:35%}.layout-xs-row>.flex-xs-35{max-width:35%;max-height:100%}.layout-xs-column>.flex-xs-35,.layout-xs-row>.flex-xs-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-35{max-width:100%;max-height:35%}.flex-xs-40,.layout-row>.flex-xs-40{max-width:40%;max-height:100%}.flex-xs-40,.layout-column>.flex-xs-40,.layout-row>.flex-xs-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-40{max-width:100%;max-height:40%}.layout-xs-row>.flex-xs-40{max-width:40%;max-height:100%}.layout-xs-column>.flex-xs-40,.layout-xs-row>.flex-xs-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-40{max-width:100%;max-height:40%}.flex-xs-45,.layout-row>.flex-xs-45{max-width:45%;max-height:100%}.flex-xs-45,.layout-column>.flex-xs-45,.layout-row>.flex-xs-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-45{max-width:100%;max-height:45%}.layout-xs-row>.flex-xs-45{max-width:45%;max-height:100%}.layout-xs-column>.flex-xs-45,.layout-xs-row>.flex-xs-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-45{max-width:100%;max-height:45%}.flex-xs-50,.layout-row>.flex-xs-50{max-width:50%;max-height:100%}.flex-xs-50,.layout-column>.flex-xs-50,.layout-row>.flex-xs-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-50{max-width:100%;max-height:50%}.layout-xs-row>.flex-xs-50{max-width:50%;max-height:100%}.layout-xs-column>.flex-xs-50,.layout-xs-row>.flex-xs-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-50{max-width:100%;max-height:50%}.flex-xs-55,.layout-row>.flex-xs-55{max-width:55%;max-height:100%}.flex-xs-55,.layout-column>.flex-xs-55,.layout-row>.flex-xs-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-55{max-width:100%;max-height:55%}.layout-xs-row>.flex-xs-55{max-width:55%;max-height:100%}.layout-xs-column>.flex-xs-55,.layout-xs-row>.flex-xs-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-55{max-width:100%;max-height:55%}.flex-xs-60,.layout-row>.flex-xs-60{max-width:60%;max-height:100%}.flex-xs-60,.layout-column>.flex-xs-60,.layout-row>.flex-xs-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-60{max-width:100%;max-height:60%}.layout-xs-row>.flex-xs-60{max-width:60%;max-height:100%}.layout-xs-column>.flex-xs-60,.layout-xs-row>.flex-xs-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-60{max-width:100%;max-height:60%}.flex-xs-65,.layout-row>.flex-xs-65{max-width:65%;max-height:100%}.flex-xs-65,.layout-column>.flex-xs-65,.layout-row>.flex-xs-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-65{max-width:100%;max-height:65%}.layout-xs-row>.flex-xs-65{max-width:65%;max-height:100%}.layout-xs-column>.flex-xs-65,.layout-xs-row>.flex-xs-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-65{max-width:100%;max-height:65%}.flex-xs-70,.layout-row>.flex-xs-70{max-width:70%;max-height:100%}.flex-xs-70,.layout-column>.flex-xs-70,.layout-row>.flex-xs-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-70{max-width:100%;max-height:70%}.layout-xs-row>.flex-xs-70{max-width:70%;max-height:100%}.layout-xs-column>.flex-xs-70,.layout-xs-row>.flex-xs-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-70{max-width:100%;max-height:70%}.flex-xs-75,.layout-row>.flex-xs-75{max-width:75%;max-height:100%}.flex-xs-75,.layout-column>.flex-xs-75,.layout-row>.flex-xs-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-75{max-width:100%;max-height:75%}.layout-xs-row>.flex-xs-75{max-width:75%;max-height:100%}.layout-xs-column>.flex-xs-75,.layout-xs-row>.flex-xs-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-75{max-width:100%;max-height:75%}.flex-xs-80,.layout-row>.flex-xs-80{max-width:80%;max-height:100%}.flex-xs-80,.layout-column>.flex-xs-80,.layout-row>.flex-xs-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-80{max-width:100%;max-height:80%}.layout-xs-row>.flex-xs-80{max-width:80%;max-height:100%}.layout-xs-column>.flex-xs-80,.layout-xs-row>.flex-xs-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-80{max-width:100%;max-height:80%}.flex-xs-85,.layout-row>.flex-xs-85{max-width:85%;max-height:100%}.flex-xs-85,.layout-column>.flex-xs-85,.layout-row>.flex-xs-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-85{max-width:100%;max-height:85%}.layout-xs-row>.flex-xs-85{max-width:85%;max-height:100%}.layout-xs-column>.flex-xs-85,.layout-xs-row>.flex-xs-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-85{max-width:100%;max-height:85%}.flex-xs-90,.layout-row>.flex-xs-90{max-width:90%;max-height:100%}.flex-xs-90,.layout-column>.flex-xs-90,.layout-row>.flex-xs-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-90{max-width:100%;max-height:90%}.layout-xs-row>.flex-xs-90{max-width:90%;max-height:100%}.layout-xs-column>.flex-xs-90,.layout-xs-row>.flex-xs-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xs-column>.flex-xs-90{max-width:100%;max-height:90%}.flex-xs-95,.layout-row>.flex-xs-95{max-width:95%;max-height:100%}.flex-xs-95,.layout-column>.flex-xs-95,.layout-row>.flex-xs-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xs-95{max-width:100%;max-height:95%}.layout-xs-row>.flex-xs-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:95%;max-height:100%;box-sizing:border-box}.layout-xs-column>.flex-xs-95{max-height:95%}.flex-xs-100,.layout-xs-column>.flex-xs-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.flex-xs-100{max-height:100%}.layout-column>.flex-xs-100,.layout-row>.flex-xs-100,.layout-xs-column>.flex-xs-100,.layout-xs-row>.flex-xs-100{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:100%;box-sizing:border-box}.layout-row>.flex-xs-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:33.33%}.layout-row>.flex-xs-33,.layout-row>.flex-xs-66{-webkit-box-flex:1;max-height:100%;box-sizing:border-box}.layout-row>.flex-xs-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:66.66%}.layout-column>.flex-xs-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-height:33.33%}.layout-column>.flex-xs-33,.layout-column>.flex-xs-66{-webkit-box-flex:1;max-width:100%;box-sizing:border-box}.layout-column>.flex-xs-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-height:66.66%}.layout-xs-row>.flex-xs-33{max-width:33.33%}.layout-xs-row>.flex-xs-33,.layout-xs-row>.flex-xs-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-height:100%;box-sizing:border-box}.layout-xs-row>.flex-xs-66{max-width:66.66%}.layout-xs-row>.flex{min-width:0}.layout-xs-column>.flex-xs-33{max-height:33.33%}.layout-xs-column>.flex-xs-33,.layout-xs-column>.flex-xs-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.layout-xs-column>.flex-xs-66{max-height:66.66%}.layout-xs-column>.flex{min-height:0}.layout-xs,.layout-xs-column,.layout-xs-row{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex}.layout-xs-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.layout-xs-row{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}}@media (min-width:600px){.flex-order-gt-xs--20{-webkit-box-ordinal-group:-19;-webkit-order:-20;order:-20}.flex-order-gt-xs--19{-webkit-box-ordinal-group:-18;-webkit-order:-19;order:-19}.flex-order-gt-xs--18{-webkit-box-ordinal-group:-17;-webkit-order:-18;order:-18}.flex-order-gt-xs--17{-webkit-box-ordinal-group:-16;-webkit-order:-17;order:-17}.flex-order-gt-xs--16{-webkit-box-ordinal-group:-15;-webkit-order:-16;order:-16}.flex-order-gt-xs--15{-webkit-box-ordinal-group:-14;-webkit-order:-15;order:-15}.flex-order-gt-xs--14{-webkit-box-ordinal-group:-13;-webkit-order:-14;order:-14}.flex-order-gt-xs--13{-webkit-box-ordinal-group:-12;-webkit-order:-13;order:-13}.flex-order-gt-xs--12{-webkit-box-ordinal-group:-11;-webkit-order:-12;order:-12}.flex-order-gt-xs--11{-webkit-box-ordinal-group:-10;-webkit-order:-11;order:-11}.flex-order-gt-xs--10{-webkit-box-ordinal-group:-9;-webkit-order:-10;order:-10}.flex-order-gt-xs--9{-webkit-box-ordinal-group:-8;-webkit-order:-9;order:-9}.flex-order-gt-xs--8{-webkit-box-ordinal-group:-7;-webkit-order:-8;order:-8}.flex-order-gt-xs--7{-webkit-box-ordinal-group:-6;-webkit-order:-7;order:-7}.flex-order-gt-xs--6{-webkit-box-ordinal-group:-5;-webkit-order:-6;order:-6}.flex-order-gt-xs--5{-webkit-box-ordinal-group:-4;-webkit-order:-5;order:-5}.flex-order-gt-xs--4{-webkit-box-ordinal-group:-3;-webkit-order:-4;order:-4}.flex-order-gt-xs--3{-webkit-box-ordinal-group:-2;-webkit-order:-3;order:-3}.flex-order-gt-xs--2{-webkit-box-ordinal-group:-1;-webkit-order:-2;order:-2}.flex-order-gt-xs--1{-webkit-box-ordinal-group:0;-webkit-order:-1;order:-1}.flex-order-gt-xs-0{-webkit-box-ordinal-group:1;-webkit-order:0;order:0}.flex-order-gt-xs-1{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.flex-order-gt-xs-2{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.flex-order-gt-xs-3{-webkit-box-ordinal-group:4;-webkit-order:3;order:3}.flex-order-gt-xs-4{-webkit-box-ordinal-group:5;-webkit-order:4;order:4}.flex-order-gt-xs-5{-webkit-box-ordinal-group:6;-webkit-order:5;order:5}.flex-order-gt-xs-6{-webkit-box-ordinal-group:7;-webkit-order:6;order:6}.flex-order-gt-xs-7{-webkit-box-ordinal-group:8;-webkit-order:7;order:7}.flex-order-gt-xs-8{-webkit-box-ordinal-group:9;-webkit-order:8;order:8}.flex-order-gt-xs-9{-webkit-box-ordinal-group:10;-webkit-order:9;order:9}.flex-order-gt-xs-10{-webkit-box-ordinal-group:11;-webkit-order:10;order:10}.flex-order-gt-xs-11{-webkit-box-ordinal-group:12;-webkit-order:11;order:11}.flex-order-gt-xs-12{-webkit-box-ordinal-group:13;-webkit-order:12;order:12}.flex-order-gt-xs-13{-webkit-box-ordinal-group:14;-webkit-order:13;order:13}.flex-order-gt-xs-14{-webkit-box-ordinal-group:15;-webkit-order:14;order:14}.flex-order-gt-xs-15{-webkit-box-ordinal-group:16;-webkit-order:15;order:15}.flex-order-gt-xs-16{-webkit-box-ordinal-group:17;-webkit-order:16;order:16}.flex-order-gt-xs-17{-webkit-box-ordinal-group:18;-webkit-order:17;order:17}.flex-order-gt-xs-18{-webkit-box-ordinal-group:19;-webkit-order:18;order:18}.flex-order-gt-xs-19{-webkit-box-ordinal-group:20;-webkit-order:19;order:19}.flex-order-gt-xs-20{-webkit-box-ordinal-group:21;-webkit-order:20;order:20}.flex-offset-gt-xs-0,.offset-gt-xs-0{margin-left:0}[dir=rtl] .flex-offset-gt-xs-0,[dir=rtl] .offset-gt-xs-0{margin-left:auto;margin-right:0}.flex-offset-gt-xs-5,.offset-gt-xs-5{margin-left:5%}[dir=rtl] .flex-offset-gt-xs-5,[dir=rtl] .offset-gt-xs-5{margin-left:auto;margin-right:5%}.flex-offset-gt-xs-10,.offset-gt-xs-10{margin-left:10%}[dir=rtl] .flex-offset-gt-xs-10,[dir=rtl] .offset-gt-xs-10{margin-left:auto;margin-right:10%}.flex-offset-gt-xs-15,.offset-gt-xs-15{margin-left:15%}[dir=rtl] .flex-offset-gt-xs-15,[dir=rtl] .offset-gt-xs-15{margin-left:auto;margin-right:15%}.flex-offset-gt-xs-20,.offset-gt-xs-20{margin-left:20%}[dir=rtl] .flex-offset-gt-xs-20,[dir=rtl] .offset-gt-xs-20{margin-left:auto;margin-right:20%}.flex-offset-gt-xs-25,.offset-gt-xs-25{margin-left:25%}[dir=rtl] .flex-offset-gt-xs-25,[dir=rtl] .offset-gt-xs-25{margin-left:auto;margin-right:25%}.flex-offset-gt-xs-30,.offset-gt-xs-30{margin-left:30%}[dir=rtl] .flex-offset-gt-xs-30,[dir=rtl] .offset-gt-xs-30{margin-left:auto;margin-right:30%}.flex-offset-gt-xs-35,.offset-gt-xs-35{margin-left:35%}[dir=rtl] .flex-offset-gt-xs-35,[dir=rtl] .offset-gt-xs-35{margin-left:auto;margin-right:35%}.flex-offset-gt-xs-40,.offset-gt-xs-40{margin-left:40%}[dir=rtl] .flex-offset-gt-xs-40,[dir=rtl] .offset-gt-xs-40{margin-left:auto;margin-right:40%}.flex-offset-gt-xs-45,.offset-gt-xs-45{margin-left:45%}[dir=rtl] .flex-offset-gt-xs-45,[dir=rtl] .offset-gt-xs-45{margin-left:auto;margin-right:45%}.flex-offset-gt-xs-50,.offset-gt-xs-50{margin-left:50%}[dir=rtl] .flex-offset-gt-xs-50,[dir=rtl] .offset-gt-xs-50{margin-left:auto;margin-right:50%}.flex-offset-gt-xs-55,.offset-gt-xs-55{margin-left:55%}[dir=rtl] .flex-offset-gt-xs-55,[dir=rtl] .offset-gt-xs-55{margin-left:auto;margin-right:55%}.flex-offset-gt-xs-60,.offset-gt-xs-60{margin-left:60%}[dir=rtl] .flex-offset-gt-xs-60,[dir=rtl] .offset-gt-xs-60{margin-left:auto;margin-right:60%}.flex-offset-gt-xs-65,.offset-gt-xs-65{margin-left:65%}[dir=rtl] .flex-offset-gt-xs-65,[dir=rtl] .offset-gt-xs-65{margin-left:auto;margin-right:65%}.flex-offset-gt-xs-70,.offset-gt-xs-70{margin-left:70%}[dir=rtl] .flex-offset-gt-xs-70,[dir=rtl] .offset-gt-xs-70{margin-left:auto;margin-right:70%}.flex-offset-gt-xs-75,.offset-gt-xs-75{margin-left:75%}[dir=rtl] .flex-offset-gt-xs-75,[dir=rtl] .offset-gt-xs-75{margin-left:auto;margin-right:75%}.flex-offset-gt-xs-80,.offset-gt-xs-80{margin-left:80%}[dir=rtl] .flex-offset-gt-xs-80,[dir=rtl] .offset-gt-xs-80{margin-left:auto;margin-right:80%}.flex-offset-gt-xs-85,.offset-gt-xs-85{margin-left:85%}[dir=rtl] .flex-offset-gt-xs-85,[dir=rtl] .offset-gt-xs-85{margin-left:auto;margin-right:85%}.flex-offset-gt-xs-90,.offset-gt-xs-90{margin-left:90%}[dir=rtl] .flex-offset-gt-xs-90,[dir=rtl] .offset-gt-xs-90{margin-left:auto;margin-right:90%}.flex-offset-gt-xs-95,.offset-gt-xs-95{margin-left:95%}[dir=rtl] .flex-offset-gt-xs-95,[dir=rtl] .offset-gt-xs-95{margin-left:auto;margin-right:95%}.flex-offset-gt-xs-33,.offset-gt-xs-33{margin-left:33.33333%}.flex-offset-gt-xs-66,.offset-gt-xs-66{margin-left:66.66667%}[dir=rtl] .flex-offset-gt-xs-66,[dir=rtl] .offset-gt-xs-66{margin-left:auto;margin-right:66.66667%}.layout-align-gt-xs,.layout-align-gt-xs-start-stretch{-webkit-align-content:stretch;align-content:stretch;-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch}.layout-align-gt-xs,.layout-align-gt-xs-start,.layout-align-gt-xs-start-center,.layout-align-gt-xs-start-end,.layout-align-gt-xs-start-start,.layout-align-gt-xs-start-stretch{-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.layout-align-gt-xs-center,.layout-align-gt-xs-center-center,.layout-align-gt-xs-center-end,.layout-align-gt-xs-center-start,.layout-align-gt-xs-center-stretch{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.layout-align-gt-xs-end,.layout-align-gt-xs-end-center,.layout-align-gt-xs-end-end,.layout-align-gt-xs-end-start,.layout-align-gt-xs-end-stretch{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.layout-align-gt-xs-space-around,.layout-align-gt-xs-space-around-center,.layout-align-gt-xs-space-around-end,.layout-align-gt-xs-space-around-start,.layout-align-gt-xs-space-around-stretch{-webkit-justify-content:space-around;justify-content:space-around}.layout-align-gt-xs-space-between,.layout-align-gt-xs-space-between-center,.layout-align-gt-xs-space-between-end,.layout-align-gt-xs-space-between-start,.layout-align-gt-xs-space-between-stretch{-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.layout-align-gt-xs-center-start,.layout-align-gt-xs-end-start,.layout-align-gt-xs-space-around-start,.layout-align-gt-xs-space-between-start,.layout-align-gt-xs-start-start{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.layout-align-gt-xs-center-center,.layout-align-gt-xs-end-center,.layout-align-gt-xs-space-around-center,.layout-align-gt-xs-space-between-center,.layout-align-gt-xs-start-center{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-content:center;align-content:center;max-width:100%}.layout-align-gt-xs-center-center>*,.layout-align-gt-xs-end-center>*,.layout-align-gt-xs-space-around-center>*,.layout-align-gt-xs-space-between-center>*,.layout-align-gt-xs-start-center>*{max-width:100%;box-sizing:border-box}.layout-align-gt-xs-center-end,.layout-align-gt-xs-end-end,.layout-align-gt-xs-space-around-end,.layout-align-gt-xs-space-between-end,.layout-align-gt-xs-start-end{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;-webkit-align-content:flex-end;align-content:flex-end}.layout-align-gt-xs-center-stretch,.layout-align-gt-xs-end-stretch,.layout-align-gt-xs-space-around-stretch,.layout-align-gt-xs-space-between-stretch,.layout-align-gt-xs-start-stretch{-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch;-webkit-align-content:stretch;align-content:stretch}.flex-gt-xs{-webkit-flex:1;flex:1}.flex-gt-xs,.flex-gt-xs-grow{-webkit-box-flex:1;box-sizing:border-box}.flex-gt-xs-grow{-webkit-flex:1 1 100%;flex:1 1 100%}.flex-gt-xs-initial{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-gt-xs-auto{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;box-sizing:border-box}.flex-gt-xs-none{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;box-sizing:border-box}.flex-gt-xs-noshrink{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;box-sizing:border-box}.flex-gt-xs-nogrow{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-gt-xs-0,.layout-row>.flex-gt-xs-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:0;max-height:100%;box-sizing:border-box}.layout-row>.flex-gt-xs-0{min-width:0}.layout-column>.flex-gt-xs-0{max-width:100%;max-height:0%}.layout-column>.flex-gt-xs-0,.layout-gt-xs-row>.flex-gt-xs-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-row>.flex-gt-xs-0{max-width:0;max-height:100%;min-width:0}.layout-gt-xs-column>.flex-gt-xs-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:0%;box-sizing:border-box;min-height:0}.flex-gt-xs-5,.layout-row>.flex-gt-xs-5{max-width:5%;max-height:100%}.flex-gt-xs-5,.layout-column>.flex-gt-xs-5,.layout-row>.flex-gt-xs-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-5{max-width:100%;max-height:5%}.layout-gt-xs-row>.flex-gt-xs-5{max-width:5%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-5,.layout-gt-xs-row>.flex-gt-xs-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-5{max-width:100%;max-height:5%}.flex-gt-xs-10,.layout-row>.flex-gt-xs-10{max-width:10%;max-height:100%}.flex-gt-xs-10,.layout-column>.flex-gt-xs-10,.layout-row>.flex-gt-xs-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-10{max-width:100%;max-height:10%}.layout-gt-xs-row>.flex-gt-xs-10{max-width:10%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-10,.layout-gt-xs-row>.flex-gt-xs-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-10{max-width:100%;max-height:10%}.flex-gt-xs-15,.layout-row>.flex-gt-xs-15{max-width:15%;max-height:100%}.flex-gt-xs-15,.layout-column>.flex-gt-xs-15,.layout-row>.flex-gt-xs-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-15{max-width:100%;max-height:15%}.layout-gt-xs-row>.flex-gt-xs-15{max-width:15%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-15,.layout-gt-xs-row>.flex-gt-xs-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-15{max-width:100%;max-height:15%}.flex-gt-xs-20,.layout-row>.flex-gt-xs-20{max-width:20%;max-height:100%}.flex-gt-xs-20,.layout-column>.flex-gt-xs-20,.layout-row>.flex-gt-xs-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-20{max-width:100%;max-height:20%}.layout-gt-xs-row>.flex-gt-xs-20{max-width:20%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-20,.layout-gt-xs-row>.flex-gt-xs-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-20{max-width:100%;max-height:20%}.flex-gt-xs-25,.layout-row>.flex-gt-xs-25{max-width:25%;max-height:100%}.flex-gt-xs-25,.layout-column>.flex-gt-xs-25,.layout-row>.flex-gt-xs-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-25{max-width:100%;max-height:25%}.layout-gt-xs-row>.flex-gt-xs-25{max-width:25%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-25,.layout-gt-xs-row>.flex-gt-xs-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-25{max-width:100%;max-height:25%}.flex-gt-xs-30,.layout-row>.flex-gt-xs-30{max-width:30%;max-height:100%}.flex-gt-xs-30,.layout-column>.flex-gt-xs-30,.layout-row>.flex-gt-xs-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-30{max-width:100%;max-height:30%}.layout-gt-xs-row>.flex-gt-xs-30{max-width:30%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-30,.layout-gt-xs-row>.flex-gt-xs-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-30{max-width:100%;max-height:30%}.flex-gt-xs-35,.layout-row>.flex-gt-xs-35{max-width:35%;max-height:100%}.flex-gt-xs-35,.layout-column>.flex-gt-xs-35,.layout-row>.flex-gt-xs-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-35{max-width:100%;max-height:35%}.layout-gt-xs-row>.flex-gt-xs-35{max-width:35%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-35,.layout-gt-xs-row>.flex-gt-xs-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-35{max-width:100%;max-height:35%}.flex-gt-xs-40,.layout-row>.flex-gt-xs-40{max-width:40%;max-height:100%}.flex-gt-xs-40,.layout-column>.flex-gt-xs-40,.layout-row>.flex-gt-xs-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-40{max-width:100%;max-height:40%}.layout-gt-xs-row>.flex-gt-xs-40{max-width:40%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-40,.layout-gt-xs-row>.flex-gt-xs-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-40{max-width:100%;max-height:40%}.flex-gt-xs-45,.layout-row>.flex-gt-xs-45{max-width:45%;max-height:100%}.flex-gt-xs-45,.layout-column>.flex-gt-xs-45,.layout-row>.flex-gt-xs-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-45{max-width:100%;max-height:45%}.layout-gt-xs-row>.flex-gt-xs-45{max-width:45%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-45,.layout-gt-xs-row>.flex-gt-xs-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-45{max-width:100%;max-height:45%}.flex-gt-xs-50,.layout-row>.flex-gt-xs-50{max-width:50%;max-height:100%}.flex-gt-xs-50,.layout-column>.flex-gt-xs-50,.layout-row>.flex-gt-xs-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-50{max-width:100%;max-height:50%}.layout-gt-xs-row>.flex-gt-xs-50{max-width:50%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-50,.layout-gt-xs-row>.flex-gt-xs-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-50{max-width:100%;max-height:50%}.flex-gt-xs-55,.layout-row>.flex-gt-xs-55{max-width:55%;max-height:100%}.flex-gt-xs-55,.layout-column>.flex-gt-xs-55,.layout-row>.flex-gt-xs-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-55{max-width:100%;max-height:55%}.layout-gt-xs-row>.flex-gt-xs-55{max-width:55%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-55,.layout-gt-xs-row>.flex-gt-xs-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-55{max-width:100%;max-height:55%}.flex-gt-xs-60,.layout-row>.flex-gt-xs-60{max-width:60%;max-height:100%}.flex-gt-xs-60,.layout-column>.flex-gt-xs-60,.layout-row>.flex-gt-xs-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-60{max-width:100%;max-height:60%}.layout-gt-xs-row>.flex-gt-xs-60{max-width:60%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-60,.layout-gt-xs-row>.flex-gt-xs-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-60{max-width:100%;max-height:60%}.flex-gt-xs-65,.layout-row>.flex-gt-xs-65{max-width:65%;max-height:100%}.flex-gt-xs-65,.layout-column>.flex-gt-xs-65,.layout-row>.flex-gt-xs-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-65{max-width:100%;max-height:65%}.layout-gt-xs-row>.flex-gt-xs-65{max-width:65%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-65,.layout-gt-xs-row>.flex-gt-xs-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-65{max-width:100%;max-height:65%}.flex-gt-xs-70,.layout-row>.flex-gt-xs-70{max-width:70%;max-height:100%}.flex-gt-xs-70,.layout-column>.flex-gt-xs-70,.layout-row>.flex-gt-xs-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-70{max-width:100%;max-height:70%}.layout-gt-xs-row>.flex-gt-xs-70{max-width:70%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-70,.layout-gt-xs-row>.flex-gt-xs-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-70{max-width:100%;max-height:70%}.flex-gt-xs-75,.layout-row>.flex-gt-xs-75{max-width:75%;max-height:100%}.flex-gt-xs-75,.layout-column>.flex-gt-xs-75,.layout-row>.flex-gt-xs-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-75{max-width:100%;max-height:75%}.layout-gt-xs-row>.flex-gt-xs-75{max-width:75%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-75,.layout-gt-xs-row>.flex-gt-xs-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-75{max-width:100%;max-height:75%}.flex-gt-xs-80,.layout-row>.flex-gt-xs-80{max-width:80%;max-height:100%}.flex-gt-xs-80,.layout-column>.flex-gt-xs-80,.layout-row>.flex-gt-xs-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-80{max-width:100%;max-height:80%}.layout-gt-xs-row>.flex-gt-xs-80{max-width:80%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-80,.layout-gt-xs-row>.flex-gt-xs-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-80{max-width:100%;max-height:80%}.flex-gt-xs-85,.layout-row>.flex-gt-xs-85{max-width:85%;max-height:100%}.flex-gt-xs-85,.layout-column>.flex-gt-xs-85,.layout-row>.flex-gt-xs-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-85{max-width:100%;max-height:85%}.layout-gt-xs-row>.flex-gt-xs-85{max-width:85%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-85,.layout-gt-xs-row>.flex-gt-xs-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-85{max-width:100%;max-height:85%}.flex-gt-xs-90,.layout-row>.flex-gt-xs-90{max-width:90%;max-height:100%}.flex-gt-xs-90,.layout-column>.flex-gt-xs-90,.layout-row>.flex-gt-xs-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-90{max-width:100%;max-height:90%}.layout-gt-xs-row>.flex-gt-xs-90{max-width:90%;max-height:100%}.layout-gt-xs-column>.flex-gt-xs-90,.layout-gt-xs-row>.flex-gt-xs-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-90{max-width:100%;max-height:90%}.flex-gt-xs-95,.layout-row>.flex-gt-xs-95{max-width:95%;max-height:100%}.flex-gt-xs-95,.layout-column>.flex-gt-xs-95,.layout-row>.flex-gt-xs-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-xs-95{max-width:100%;max-height:95%}.layout-gt-xs-row>.flex-gt-xs-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:95%;max-height:100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-95{max-height:95%}.flex-gt-xs-100,.layout-gt-xs-column>.flex-gt-xs-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.flex-gt-xs-100{max-height:100%}.layout-column>.flex-gt-xs-100,.layout-gt-xs-column>.flex-gt-xs-100,.layout-gt-xs-row>.flex-gt-xs-100,.layout-row>.flex-gt-xs-100{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:100%;box-sizing:border-box}.layout-row>.flex-gt-xs-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:33.33%}.layout-row>.flex-gt-xs-33,.layout-row>.flex-gt-xs-66{-webkit-box-flex:1;max-height:100%;box-sizing:border-box}.layout-row>.flex-gt-xs-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:66.66%}.layout-column>.flex-gt-xs-33{-webkit-box-flex:1;-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:100%;max-height:33.33%;box-sizing:border-box}.layout-column>.flex-gt-xs-66{-webkit-box-flex:1;-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:100%;max-height:66.66%;box-sizing:border-box}.layout-gt-xs-row>.flex-gt-xs-33{max-width:33.33%}.layout-gt-xs-row>.flex-gt-xs-33,.layout-gt-xs-row>.flex-gt-xs-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-height:100%;box-sizing:border-box}.layout-gt-xs-row>.flex-gt-xs-66{max-width:66.66%}.layout-gt-xs-row>.flex{min-width:0}.layout-gt-xs-column>.flex-gt-xs-33{max-height:33.33%}.layout-gt-xs-column>.flex-gt-xs-33,.layout-gt-xs-column>.flex-gt-xs-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.layout-gt-xs-column>.flex-gt-xs-66{max-height:66.66%}.layout-gt-xs-column>.flex{min-height:0}.layout-gt-xs,.layout-gt-xs-column,.layout-gt-xs-row{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex}.layout-gt-xs-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.layout-gt-xs-row{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}}@media (min-width:600px) and (max-width:959px){.hide-gt-xs:not(.show-gt-xs):not(.show-sm):not(.show),.hide-sm:not(.show-gt-xs):not(.show-sm):not(.show),.hide:not(.show-gt-xs):not(.show-sm):not(.show){display:none}.flex-order-sm--20{-webkit-box-ordinal-group:-19;-webkit-order:-20;order:-20}.flex-order-sm--19{-webkit-box-ordinal-group:-18;-webkit-order:-19;order:-19}.flex-order-sm--18{-webkit-box-ordinal-group:-17;-webkit-order:-18;order:-18}.flex-order-sm--17{-webkit-box-ordinal-group:-16;-webkit-order:-17;order:-17}.flex-order-sm--16{-webkit-box-ordinal-group:-15;-webkit-order:-16;order:-16}.flex-order-sm--15{-webkit-box-ordinal-group:-14;-webkit-order:-15;order:-15}.flex-order-sm--14{-webkit-box-ordinal-group:-13;-webkit-order:-14;order:-14}.flex-order-sm--13{-webkit-box-ordinal-group:-12;-webkit-order:-13;order:-13}.flex-order-sm--12{-webkit-box-ordinal-group:-11;-webkit-order:-12;order:-12}.flex-order-sm--11{-webkit-box-ordinal-group:-10;-webkit-order:-11;order:-11}.flex-order-sm--10{-webkit-box-ordinal-group:-9;-webkit-order:-10;order:-10}.flex-order-sm--9{-webkit-box-ordinal-group:-8;-webkit-order:-9;order:-9}.flex-order-sm--8{-webkit-box-ordinal-group:-7;-webkit-order:-8;order:-8}.flex-order-sm--7{-webkit-box-ordinal-group:-6;-webkit-order:-7;order:-7}.flex-order-sm--6{-webkit-box-ordinal-group:-5;-webkit-order:-6;order:-6}.flex-order-sm--5{-webkit-box-ordinal-group:-4;-webkit-order:-5;order:-5}.flex-order-sm--4{-webkit-box-ordinal-group:-3;-webkit-order:-4;order:-4}.flex-order-sm--3{-webkit-box-ordinal-group:-2;-webkit-order:-3;order:-3}.flex-order-sm--2{-webkit-box-ordinal-group:-1;-webkit-order:-2;order:-2}.flex-order-sm--1{-webkit-box-ordinal-group:0;-webkit-order:-1;order:-1}.flex-order-sm-0{-webkit-box-ordinal-group:1;-webkit-order:0;order:0}.flex-order-sm-1{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.flex-order-sm-2{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.flex-order-sm-3{-webkit-box-ordinal-group:4;-webkit-order:3;order:3}.flex-order-sm-4{-webkit-box-ordinal-group:5;-webkit-order:4;order:4}.flex-order-sm-5{-webkit-box-ordinal-group:6;-webkit-order:5;order:5}.flex-order-sm-6{-webkit-box-ordinal-group:7;-webkit-order:6;order:6}.flex-order-sm-7{-webkit-box-ordinal-group:8;-webkit-order:7;order:7}.flex-order-sm-8{-webkit-box-ordinal-group:9;-webkit-order:8;order:8}.flex-order-sm-9{-webkit-box-ordinal-group:10;-webkit-order:9;order:9}.flex-order-sm-10{-webkit-box-ordinal-group:11;-webkit-order:10;order:10}.flex-order-sm-11{-webkit-box-ordinal-group:12;-webkit-order:11;order:11}.flex-order-sm-12{-webkit-box-ordinal-group:13;-webkit-order:12;order:12}.flex-order-sm-13{-webkit-box-ordinal-group:14;-webkit-order:13;order:13}.flex-order-sm-14{-webkit-box-ordinal-group:15;-webkit-order:14;order:14}.flex-order-sm-15{-webkit-box-ordinal-group:16;-webkit-order:15;order:15}.flex-order-sm-16{-webkit-box-ordinal-group:17;-webkit-order:16;order:16}.flex-order-sm-17{-webkit-box-ordinal-group:18;-webkit-order:17;order:17}.flex-order-sm-18{-webkit-box-ordinal-group:19;-webkit-order:18;order:18}.flex-order-sm-19{-webkit-box-ordinal-group:20;-webkit-order:19;order:19}.flex-order-sm-20{-webkit-box-ordinal-group:21;-webkit-order:20;order:20}.flex-offset-sm-0,.offset-sm-0{margin-left:0}[dir=rtl] .flex-offset-sm-0,[dir=rtl] .offset-sm-0{margin-left:auto;margin-right:0}.flex-offset-sm-5,.offset-sm-5{margin-left:5%}[dir=rtl] .flex-offset-sm-5,[dir=rtl] .offset-sm-5{margin-left:auto;margin-right:5%}.flex-offset-sm-10,.offset-sm-10{margin-left:10%}[dir=rtl] .flex-offset-sm-10,[dir=rtl] .offset-sm-10{margin-left:auto;margin-right:10%}.flex-offset-sm-15,.offset-sm-15{margin-left:15%}[dir=rtl] .flex-offset-sm-15,[dir=rtl] .offset-sm-15{margin-left:auto;margin-right:15%}.flex-offset-sm-20,.offset-sm-20{margin-left:20%}[dir=rtl] .flex-offset-sm-20,[dir=rtl] .offset-sm-20{margin-left:auto;margin-right:20%}.flex-offset-sm-25,.offset-sm-25{margin-left:25%}[dir=rtl] .flex-offset-sm-25,[dir=rtl] .offset-sm-25{margin-left:auto;margin-right:25%}.flex-offset-sm-30,.offset-sm-30{margin-left:30%}[dir=rtl] .flex-offset-sm-30,[dir=rtl] .offset-sm-30{margin-left:auto;margin-right:30%}.flex-offset-sm-35,.offset-sm-35{margin-left:35%}[dir=rtl] .flex-offset-sm-35,[dir=rtl] .offset-sm-35{margin-left:auto;margin-right:35%}.flex-offset-sm-40,.offset-sm-40{margin-left:40%}[dir=rtl] .flex-offset-sm-40,[dir=rtl] .offset-sm-40{margin-left:auto;margin-right:40%}.flex-offset-sm-45,.offset-sm-45{margin-left:45%}[dir=rtl] .flex-offset-sm-45,[dir=rtl] .offset-sm-45{margin-left:auto;margin-right:45%}.flex-offset-sm-50,.offset-sm-50{margin-left:50%}[dir=rtl] .flex-offset-sm-50,[dir=rtl] .offset-sm-50{margin-left:auto;margin-right:50%}.flex-offset-sm-55,.offset-sm-55{margin-left:55%}[dir=rtl] .flex-offset-sm-55,[dir=rtl] .offset-sm-55{margin-left:auto;margin-right:55%}.flex-offset-sm-60,.offset-sm-60{margin-left:60%}[dir=rtl] .flex-offset-sm-60,[dir=rtl] .offset-sm-60{margin-left:auto;margin-right:60%}.flex-offset-sm-65,.offset-sm-65{margin-left:65%}[dir=rtl] .flex-offset-sm-65,[dir=rtl] .offset-sm-65{margin-left:auto;margin-right:65%}.flex-offset-sm-70,.offset-sm-70{margin-left:70%}[dir=rtl] .flex-offset-sm-70,[dir=rtl] .offset-sm-70{margin-left:auto;margin-right:70%}.flex-offset-sm-75,.offset-sm-75{margin-left:75%}[dir=rtl] .flex-offset-sm-75,[dir=rtl] .offset-sm-75{margin-left:auto;margin-right:75%}.flex-offset-sm-80,.offset-sm-80{margin-left:80%}[dir=rtl] .flex-offset-sm-80,[dir=rtl] .offset-sm-80{margin-left:auto;margin-right:80%}.flex-offset-sm-85,.offset-sm-85{margin-left:85%}[dir=rtl] .flex-offset-sm-85,[dir=rtl] .offset-sm-85{margin-left:auto;margin-right:85%}.flex-offset-sm-90,.offset-sm-90{margin-left:90%}[dir=rtl] .flex-offset-sm-90,[dir=rtl] .offset-sm-90{margin-left:auto;margin-right:90%}.flex-offset-sm-95,.offset-sm-95{margin-left:95%}[dir=rtl] .flex-offset-sm-95,[dir=rtl] .offset-sm-95{margin-left:auto;margin-right:95%}.flex-offset-sm-33,.offset-sm-33{margin-left:33.33333%}.flex-offset-sm-66,.offset-sm-66{margin-left:66.66667%}[dir=rtl] .flex-offset-sm-66,[dir=rtl] .offset-sm-66{margin-left:auto;margin-right:66.66667%}.layout-align-sm,.layout-align-sm-start-stretch{-webkit-align-content:stretch;align-content:stretch;-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch}.layout-align-sm,.layout-align-sm-start,.layout-align-sm-start-center,.layout-align-sm-start-end,.layout-align-sm-start-start,.layout-align-sm-start-stretch{-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.layout-align-sm-center,.layout-align-sm-center-center,.layout-align-sm-center-end,.layout-align-sm-center-start,.layout-align-sm-center-stretch{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.layout-align-sm-end,.layout-align-sm-end-center,.layout-align-sm-end-end,.layout-align-sm-end-start,.layout-align-sm-end-stretch{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.layout-align-sm-space-around,.layout-align-sm-space-around-center,.layout-align-sm-space-around-end,.layout-align-sm-space-around-start,.layout-align-sm-space-around-stretch{-webkit-justify-content:space-around;justify-content:space-around}.layout-align-sm-space-between,.layout-align-sm-space-between-center,.layout-align-sm-space-between-end,.layout-align-sm-space-between-start,.layout-align-sm-space-between-stretch{-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.layout-align-sm-center-start,.layout-align-sm-end-start,.layout-align-sm-space-around-start,.layout-align-sm-space-between-start,.layout-align-sm-start-start{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.layout-align-sm-center-center,.layout-align-sm-end-center,.layout-align-sm-space-around-center,.layout-align-sm-space-between-center,.layout-align-sm-start-center{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-content:center;align-content:center;max-width:100%}.layout-align-sm-center-center>*,.layout-align-sm-end-center>*,.layout-align-sm-space-around-center>*,.layout-align-sm-space-between-center>*,.layout-align-sm-start-center>*{max-width:100%;box-sizing:border-box}.layout-align-sm-center-end,.layout-align-sm-end-end,.layout-align-sm-space-around-end,.layout-align-sm-space-between-end,.layout-align-sm-start-end{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;-webkit-align-content:flex-end;align-content:flex-end}.layout-align-sm-center-stretch,.layout-align-sm-end-stretch,.layout-align-sm-space-around-stretch,.layout-align-sm-space-between-stretch,.layout-align-sm-start-stretch{-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch;-webkit-align-content:stretch;align-content:stretch}.flex-sm{-webkit-flex:1;flex:1}.flex-sm,.flex-sm-grow{-webkit-box-flex:1;box-sizing:border-box}.flex-sm-grow{-webkit-flex:1 1 100%;flex:1 1 100%}.flex-sm-initial{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-sm-auto{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;box-sizing:border-box}.flex-sm-none{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;box-sizing:border-box}.flex-sm-noshrink{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;box-sizing:border-box}.flex-sm-nogrow{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-sm-0,.layout-row>.flex-sm-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:0;max-height:100%;box-sizing:border-box}.layout-row>.flex-sm-0{min-width:0}.layout-column>.flex-sm-0{max-width:100%;max-height:0%}.layout-column>.flex-sm-0,.layout-sm-row>.flex-sm-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-row>.flex-sm-0{max-width:0;max-height:100%;min-width:0}.layout-sm-column>.flex-sm-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:0%;box-sizing:border-box;min-height:0}.flex-sm-5,.layout-row>.flex-sm-5{max-width:5%;max-height:100%}.flex-sm-5,.layout-column>.flex-sm-5,.layout-row>.flex-sm-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-5{max-width:100%;max-height:5%}.layout-sm-row>.flex-sm-5{max-width:5%;max-height:100%}.layout-sm-column>.flex-sm-5,.layout-sm-row>.flex-sm-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-5{max-width:100%;max-height:5%}.flex-sm-10,.layout-row>.flex-sm-10{max-width:10%;max-height:100%}.flex-sm-10,.layout-column>.flex-sm-10,.layout-row>.flex-sm-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-10{max-width:100%;max-height:10%}.layout-sm-row>.flex-sm-10{max-width:10%;max-height:100%}.layout-sm-column>.flex-sm-10,.layout-sm-row>.flex-sm-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-10{max-width:100%;max-height:10%}.flex-sm-15,.layout-row>.flex-sm-15{max-width:15%;max-height:100%}.flex-sm-15,.layout-column>.flex-sm-15,.layout-row>.flex-sm-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-15{max-width:100%;max-height:15%}.layout-sm-row>.flex-sm-15{max-width:15%;max-height:100%}.layout-sm-column>.flex-sm-15,.layout-sm-row>.flex-sm-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-15{max-width:100%;max-height:15%}.flex-sm-20,.layout-row>.flex-sm-20{max-width:20%;max-height:100%}.flex-sm-20,.layout-column>.flex-sm-20,.layout-row>.flex-sm-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-20{max-width:100%;max-height:20%}.layout-sm-row>.flex-sm-20{max-width:20%;max-height:100%}.layout-sm-column>.flex-sm-20,.layout-sm-row>.flex-sm-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-20{max-width:100%;max-height:20%}.flex-sm-25,.layout-row>.flex-sm-25{max-width:25%;max-height:100%}.flex-sm-25,.layout-column>.flex-sm-25,.layout-row>.flex-sm-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-25{max-width:100%;max-height:25%}.layout-sm-row>.flex-sm-25{max-width:25%;max-height:100%}.layout-sm-column>.flex-sm-25,.layout-sm-row>.flex-sm-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-25{max-width:100%;max-height:25%}.flex-sm-30,.layout-row>.flex-sm-30{max-width:30%;max-height:100%}.flex-sm-30,.layout-column>.flex-sm-30,.layout-row>.flex-sm-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-30{max-width:100%;max-height:30%}.layout-sm-row>.flex-sm-30{max-width:30%;max-height:100%}.layout-sm-column>.flex-sm-30,.layout-sm-row>.flex-sm-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-30{max-width:100%;max-height:30%}.flex-sm-35,.layout-row>.flex-sm-35{max-width:35%;max-height:100%}.flex-sm-35,.layout-column>.flex-sm-35,.layout-row>.flex-sm-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-35{max-width:100%;max-height:35%}.layout-sm-row>.flex-sm-35{max-width:35%;max-height:100%}.layout-sm-column>.flex-sm-35,.layout-sm-row>.flex-sm-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-35{max-width:100%;max-height:35%}.flex-sm-40,.layout-row>.flex-sm-40{max-width:40%;max-height:100%}.flex-sm-40,.layout-column>.flex-sm-40,.layout-row>.flex-sm-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-40{max-width:100%;max-height:40%}.layout-sm-row>.flex-sm-40{max-width:40%;max-height:100%}.layout-sm-column>.flex-sm-40,.layout-sm-row>.flex-sm-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-40{max-width:100%;max-height:40%}.flex-sm-45,.layout-row>.flex-sm-45{max-width:45%;max-height:100%}.flex-sm-45,.layout-column>.flex-sm-45,.layout-row>.flex-sm-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-45{max-width:100%;max-height:45%}.layout-sm-row>.flex-sm-45{max-width:45%;max-height:100%}.layout-sm-column>.flex-sm-45,.layout-sm-row>.flex-sm-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-45{max-width:100%;max-height:45%}.flex-sm-50,.layout-row>.flex-sm-50{max-width:50%;max-height:100%}.flex-sm-50,.layout-column>.flex-sm-50,.layout-row>.flex-sm-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-50{max-width:100%;max-height:50%}.layout-sm-row>.flex-sm-50{max-width:50%;max-height:100%}.layout-sm-column>.flex-sm-50,.layout-sm-row>.flex-sm-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-50{max-width:100%;max-height:50%}.flex-sm-55,.layout-row>.flex-sm-55{max-width:55%;max-height:100%}.flex-sm-55,.layout-column>.flex-sm-55,.layout-row>.flex-sm-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-55{max-width:100%;max-height:55%}.layout-sm-row>.flex-sm-55{max-width:55%;max-height:100%}.layout-sm-column>.flex-sm-55,.layout-sm-row>.flex-sm-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-55{max-width:100%;max-height:55%}.flex-sm-60,.layout-row>.flex-sm-60{max-width:60%;max-height:100%}.flex-sm-60,.layout-column>.flex-sm-60,.layout-row>.flex-sm-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-60{max-width:100%;max-height:60%}.layout-sm-row>.flex-sm-60{max-width:60%;max-height:100%}.layout-sm-column>.flex-sm-60,.layout-sm-row>.flex-sm-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-60{max-width:100%;max-height:60%}.flex-sm-65,.layout-row>.flex-sm-65{max-width:65%;max-height:100%}.flex-sm-65,.layout-column>.flex-sm-65,.layout-row>.flex-sm-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-65{max-width:100%;max-height:65%}.layout-sm-row>.flex-sm-65{max-width:65%;max-height:100%}.layout-sm-column>.flex-sm-65,.layout-sm-row>.flex-sm-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-65{max-width:100%;max-height:65%}.flex-sm-70,.layout-row>.flex-sm-70{max-width:70%;max-height:100%}.flex-sm-70,.layout-column>.flex-sm-70,.layout-row>.flex-sm-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-70{max-width:100%;max-height:70%}.layout-sm-row>.flex-sm-70{max-width:70%;max-height:100%}.layout-sm-column>.flex-sm-70,.layout-sm-row>.flex-sm-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-70{max-width:100%;max-height:70%}.flex-sm-75,.layout-row>.flex-sm-75{max-width:75%;max-height:100%}.flex-sm-75,.layout-column>.flex-sm-75,.layout-row>.flex-sm-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-75{max-width:100%;max-height:75%}.layout-sm-row>.flex-sm-75{max-width:75%;max-height:100%}.layout-sm-column>.flex-sm-75,.layout-sm-row>.flex-sm-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-75{max-width:100%;max-height:75%}.flex-sm-80,.layout-row>.flex-sm-80{max-width:80%;max-height:100%}.flex-sm-80,.layout-column>.flex-sm-80,.layout-row>.flex-sm-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-80{max-width:100%;max-height:80%}.layout-sm-row>.flex-sm-80{max-width:80%;max-height:100%}.layout-sm-column>.flex-sm-80,.layout-sm-row>.flex-sm-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-80{max-width:100%;max-height:80%}.flex-sm-85,.layout-row>.flex-sm-85{max-width:85%;max-height:100%}.flex-sm-85,.layout-column>.flex-sm-85,.layout-row>.flex-sm-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-85{max-width:100%;max-height:85%}.layout-sm-row>.flex-sm-85{max-width:85%;max-height:100%}.layout-sm-column>.flex-sm-85,.layout-sm-row>.flex-sm-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-85{max-width:100%;max-height:85%}.flex-sm-90,.layout-row>.flex-sm-90{max-width:90%;max-height:100%}.flex-sm-90,.layout-column>.flex-sm-90,.layout-row>.flex-sm-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-90{max-width:100%;max-height:90%}.layout-sm-row>.flex-sm-90{max-width:90%;max-height:100%}.layout-sm-column>.flex-sm-90,.layout-sm-row>.flex-sm-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-sm-column>.flex-sm-90{max-width:100%;max-height:90%}.flex-sm-95,.layout-row>.flex-sm-95{max-width:95%;max-height:100%}.flex-sm-95,.layout-column>.flex-sm-95,.layout-row>.flex-sm-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-sm-95{max-width:100%;max-height:95%}.layout-sm-row>.flex-sm-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:95%;max-height:100%;box-sizing:border-box}.layout-sm-column>.flex-sm-95{max-height:95%}.flex-sm-100,.layout-sm-column>.flex-sm-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.flex-sm-100{max-height:100%}.layout-column>.flex-sm-100,.layout-row>.flex-sm-100,.layout-sm-column>.flex-sm-100,.layout-sm-row>.flex-sm-100{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:100%;box-sizing:border-box}.layout-row>.flex-sm-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:33.33%}.layout-row>.flex-sm-33,.layout-row>.flex-sm-66{-webkit-box-flex:1;max-height:100%;box-sizing:border-box}.layout-row>.flex-sm-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:66.66%}.layout-column>.flex-sm-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-height:33.33%}.layout-column>.flex-sm-33,.layout-column>.flex-sm-66{-webkit-box-flex:1;max-width:100%;box-sizing:border-box}.layout-column>.flex-sm-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-height:66.66%}.layout-sm-row>.flex-sm-33{max-width:33.33%}.layout-sm-row>.flex-sm-33,.layout-sm-row>.flex-sm-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-height:100%;box-sizing:border-box}.layout-sm-row>.flex-sm-66{max-width:66.66%}.layout-sm-row>.flex{min-width:0}.layout-sm-column>.flex-sm-33{max-height:33.33%}.layout-sm-column>.flex-sm-33,.layout-sm-column>.flex-sm-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.layout-sm-column>.flex-sm-66{max-height:66.66%}.layout-sm-column>.flex{min-height:0}.layout-sm,.layout-sm-column,.layout-sm-row{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex}.layout-sm-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.layout-sm-row{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}}@media (min-width:960px){.flex-order-gt-sm--20{-webkit-box-ordinal-group:-19;-webkit-order:-20;order:-20}.flex-order-gt-sm--19{-webkit-box-ordinal-group:-18;-webkit-order:-19;order:-19}.flex-order-gt-sm--18{-webkit-box-ordinal-group:-17;-webkit-order:-18;order:-18}.flex-order-gt-sm--17{-webkit-box-ordinal-group:-16;-webkit-order:-17;order:-17}.flex-order-gt-sm--16{-webkit-box-ordinal-group:-15;-webkit-order:-16;order:-16}.flex-order-gt-sm--15{-webkit-box-ordinal-group:-14;-webkit-order:-15;order:-15}.flex-order-gt-sm--14{-webkit-box-ordinal-group:-13;-webkit-order:-14;order:-14}.flex-order-gt-sm--13{-webkit-box-ordinal-group:-12;-webkit-order:-13;order:-13}.flex-order-gt-sm--12{-webkit-box-ordinal-group:-11;-webkit-order:-12;order:-12}.flex-order-gt-sm--11{-webkit-box-ordinal-group:-10;-webkit-order:-11;order:-11}.flex-order-gt-sm--10{-webkit-box-ordinal-group:-9;-webkit-order:-10;order:-10}.flex-order-gt-sm--9{-webkit-box-ordinal-group:-8;-webkit-order:-9;order:-9}.flex-order-gt-sm--8{-webkit-box-ordinal-group:-7;-webkit-order:-8;order:-8}.flex-order-gt-sm--7{-webkit-box-ordinal-group:-6;-webkit-order:-7;order:-7}.flex-order-gt-sm--6{-webkit-box-ordinal-group:-5;-webkit-order:-6;order:-6}.flex-order-gt-sm--5{-webkit-box-ordinal-group:-4;-webkit-order:-5;order:-5}.flex-order-gt-sm--4{-webkit-box-ordinal-group:-3;-webkit-order:-4;order:-4}.flex-order-gt-sm--3{-webkit-box-ordinal-group:-2;-webkit-order:-3;order:-3}.flex-order-gt-sm--2{-webkit-box-ordinal-group:-1;-webkit-order:-2;order:-2}.flex-order-gt-sm--1{-webkit-box-ordinal-group:0;-webkit-order:-1;order:-1}.flex-order-gt-sm-0{-webkit-box-ordinal-group:1;-webkit-order:0;order:0}.flex-order-gt-sm-1{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.flex-order-gt-sm-2{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.flex-order-gt-sm-3{-webkit-box-ordinal-group:4;-webkit-order:3;order:3}.flex-order-gt-sm-4{-webkit-box-ordinal-group:5;-webkit-order:4;order:4}.flex-order-gt-sm-5{-webkit-box-ordinal-group:6;-webkit-order:5;order:5}.flex-order-gt-sm-6{-webkit-box-ordinal-group:7;-webkit-order:6;order:6}.flex-order-gt-sm-7{-webkit-box-ordinal-group:8;-webkit-order:7;order:7}.flex-order-gt-sm-8{-webkit-box-ordinal-group:9;-webkit-order:8;order:8}.flex-order-gt-sm-9{-webkit-box-ordinal-group:10;-webkit-order:9;order:9}.flex-order-gt-sm-10{-webkit-box-ordinal-group:11;-webkit-order:10;order:10}.flex-order-gt-sm-11{-webkit-box-ordinal-group:12;-webkit-order:11;order:11}.flex-order-gt-sm-12{-webkit-box-ordinal-group:13;-webkit-order:12;order:12}.flex-order-gt-sm-13{-webkit-box-ordinal-group:14;-webkit-order:13;order:13}.flex-order-gt-sm-14{-webkit-box-ordinal-group:15;-webkit-order:14;order:14}.flex-order-gt-sm-15{-webkit-box-ordinal-group:16;-webkit-order:15;order:15}.flex-order-gt-sm-16{-webkit-box-ordinal-group:17;-webkit-order:16;order:16}.flex-order-gt-sm-17{-webkit-box-ordinal-group:18;-webkit-order:17;order:17}.flex-order-gt-sm-18{-webkit-box-ordinal-group:19;-webkit-order:18;order:18}.flex-order-gt-sm-19{-webkit-box-ordinal-group:20;-webkit-order:19;order:19}.flex-order-gt-sm-20{-webkit-box-ordinal-group:21;-webkit-order:20;order:20}.flex-offset-gt-sm-0,.offset-gt-sm-0{margin-left:0}[dir=rtl] .flex-offset-gt-sm-0,[dir=rtl] .offset-gt-sm-0{margin-left:auto;margin-right:0}.flex-offset-gt-sm-5,.offset-gt-sm-5{margin-left:5%}[dir=rtl] .flex-offset-gt-sm-5,[dir=rtl] .offset-gt-sm-5{margin-left:auto;margin-right:5%}.flex-offset-gt-sm-10,.offset-gt-sm-10{margin-left:10%}[dir=rtl] .flex-offset-gt-sm-10,[dir=rtl] .offset-gt-sm-10{margin-left:auto;margin-right:10%}.flex-offset-gt-sm-15,.offset-gt-sm-15{margin-left:15%}[dir=rtl] .flex-offset-gt-sm-15,[dir=rtl] .offset-gt-sm-15{margin-left:auto;margin-right:15%}.flex-offset-gt-sm-20,.offset-gt-sm-20{margin-left:20%}[dir=rtl] .flex-offset-gt-sm-20,[dir=rtl] .offset-gt-sm-20{margin-left:auto;margin-right:20%}.flex-offset-gt-sm-25,.offset-gt-sm-25{margin-left:25%}[dir=rtl] .flex-offset-gt-sm-25,[dir=rtl] .offset-gt-sm-25{margin-left:auto;margin-right:25%}.flex-offset-gt-sm-30,.offset-gt-sm-30{margin-left:30%}[dir=rtl] .flex-offset-gt-sm-30,[dir=rtl] .offset-gt-sm-30{margin-left:auto;margin-right:30%}.flex-offset-gt-sm-35,.offset-gt-sm-35{margin-left:35%}[dir=rtl] .flex-offset-gt-sm-35,[dir=rtl] .offset-gt-sm-35{margin-left:auto;margin-right:35%}.flex-offset-gt-sm-40,.offset-gt-sm-40{margin-left:40%}[dir=rtl] .flex-offset-gt-sm-40,[dir=rtl] .offset-gt-sm-40{margin-left:auto;margin-right:40%}.flex-offset-gt-sm-45,.offset-gt-sm-45{margin-left:45%}[dir=rtl] .flex-offset-gt-sm-45,[dir=rtl] .offset-gt-sm-45{margin-left:auto;margin-right:45%}.flex-offset-gt-sm-50,.offset-gt-sm-50{margin-left:50%}[dir=rtl] .flex-offset-gt-sm-50,[dir=rtl] .offset-gt-sm-50{margin-left:auto;margin-right:50%}.flex-offset-gt-sm-55,.offset-gt-sm-55{margin-left:55%}[dir=rtl] .flex-offset-gt-sm-55,[dir=rtl] .offset-gt-sm-55{margin-left:auto;margin-right:55%}.flex-offset-gt-sm-60,.offset-gt-sm-60{margin-left:60%}[dir=rtl] .flex-offset-gt-sm-60,[dir=rtl] .offset-gt-sm-60{margin-left:auto;margin-right:60%}.flex-offset-gt-sm-65,.offset-gt-sm-65{margin-left:65%}[dir=rtl] .flex-offset-gt-sm-65,[dir=rtl] .offset-gt-sm-65{margin-left:auto;margin-right:65%}.flex-offset-gt-sm-70,.offset-gt-sm-70{margin-left:70%}[dir=rtl] .flex-offset-gt-sm-70,[dir=rtl] .offset-gt-sm-70{margin-left:auto;margin-right:70%}.flex-offset-gt-sm-75,.offset-gt-sm-75{margin-left:75%}[dir=rtl] .flex-offset-gt-sm-75,[dir=rtl] .offset-gt-sm-75{margin-left:auto;margin-right:75%}.flex-offset-gt-sm-80,.offset-gt-sm-80{margin-left:80%}[dir=rtl] .flex-offset-gt-sm-80,[dir=rtl] .offset-gt-sm-80{margin-left:auto;margin-right:80%}.flex-offset-gt-sm-85,.offset-gt-sm-85{margin-left:85%}[dir=rtl] .flex-offset-gt-sm-85,[dir=rtl] .offset-gt-sm-85{margin-left:auto;margin-right:85%}.flex-offset-gt-sm-90,.offset-gt-sm-90{margin-left:90%}[dir=rtl] .flex-offset-gt-sm-90,[dir=rtl] .offset-gt-sm-90{margin-left:auto;margin-right:90%}.flex-offset-gt-sm-95,.offset-gt-sm-95{margin-left:95%}[dir=rtl] .flex-offset-gt-sm-95,[dir=rtl] .offset-gt-sm-95{margin-left:auto;margin-right:95%}.flex-offset-gt-sm-33,.offset-gt-sm-33{margin-left:33.33333%}.flex-offset-gt-sm-66,.offset-gt-sm-66{margin-left:66.66667%}[dir=rtl] .flex-offset-gt-sm-66,[dir=rtl] .offset-gt-sm-66{margin-left:auto;margin-right:66.66667%}.layout-align-gt-sm,.layout-align-gt-sm-start-stretch{-webkit-align-content:stretch;align-content:stretch;-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch}.layout-align-gt-sm,.layout-align-gt-sm-start,.layout-align-gt-sm-start-center,.layout-align-gt-sm-start-end,.layout-align-gt-sm-start-start,.layout-align-gt-sm-start-stretch{-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.layout-align-gt-sm-center,.layout-align-gt-sm-center-center,.layout-align-gt-sm-center-end,.layout-align-gt-sm-center-start,.layout-align-gt-sm-center-stretch{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.layout-align-gt-sm-end,.layout-align-gt-sm-end-center,.layout-align-gt-sm-end-end,.layout-align-gt-sm-end-start,.layout-align-gt-sm-end-stretch{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.layout-align-gt-sm-space-around,.layout-align-gt-sm-space-around-center,.layout-align-gt-sm-space-around-end,.layout-align-gt-sm-space-around-start,.layout-align-gt-sm-space-around-stretch{-webkit-justify-content:space-around;justify-content:space-around}.layout-align-gt-sm-space-between,.layout-align-gt-sm-space-between-center,.layout-align-gt-sm-space-between-end,.layout-align-gt-sm-space-between-start,.layout-align-gt-sm-space-between-stretch{-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.layout-align-gt-sm-center-start,.layout-align-gt-sm-end-start,.layout-align-gt-sm-space-around-start,.layout-align-gt-sm-space-between-start,.layout-align-gt-sm-start-start{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.layout-align-gt-sm-center-center,.layout-align-gt-sm-end-center,.layout-align-gt-sm-space-around-center,.layout-align-gt-sm-space-between-center,.layout-align-gt-sm-start-center{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-content:center;align-content:center;max-width:100%}.layout-align-gt-sm-center-center>*,.layout-align-gt-sm-end-center>*,.layout-align-gt-sm-space-around-center>*,.layout-align-gt-sm-space-between-center>*,.layout-align-gt-sm-start-center>*{max-width:100%;box-sizing:border-box}.layout-align-gt-sm-center-end,.layout-align-gt-sm-end-end,.layout-align-gt-sm-space-around-end,.layout-align-gt-sm-space-between-end,.layout-align-gt-sm-start-end{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;-webkit-align-content:flex-end;align-content:flex-end}.layout-align-gt-sm-center-stretch,.layout-align-gt-sm-end-stretch,.layout-align-gt-sm-space-around-stretch,.layout-align-gt-sm-space-between-stretch,.layout-align-gt-sm-start-stretch{-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch;-webkit-align-content:stretch;align-content:stretch}.flex-gt-sm{-webkit-flex:1;flex:1}.flex-gt-sm,.flex-gt-sm-grow{-webkit-box-flex:1;box-sizing:border-box}.flex-gt-sm-grow{-webkit-flex:1 1 100%;flex:1 1 100%}.flex-gt-sm-initial{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-gt-sm-auto{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;box-sizing:border-box}.flex-gt-sm-none{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;box-sizing:border-box}.flex-gt-sm-noshrink{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;box-sizing:border-box}.flex-gt-sm-nogrow{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-gt-sm-0,.layout-row>.flex-gt-sm-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:0;max-height:100%;box-sizing:border-box}.layout-row>.flex-gt-sm-0{min-width:0}.layout-column>.flex-gt-sm-0{max-width:100%;max-height:0%}.layout-column>.flex-gt-sm-0,.layout-gt-sm-row>.flex-gt-sm-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-row>.flex-gt-sm-0{max-width:0;max-height:100%;min-width:0}.layout-gt-sm-column>.flex-gt-sm-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:0%;box-sizing:border-box;min-height:0}.flex-gt-sm-5,.layout-row>.flex-gt-sm-5{max-width:5%;max-height:100%}.flex-gt-sm-5,.layout-column>.flex-gt-sm-5,.layout-row>.flex-gt-sm-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-5{max-width:100%;max-height:5%}.layout-gt-sm-row>.flex-gt-sm-5{max-width:5%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-5,.layout-gt-sm-row>.flex-gt-sm-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-5{max-width:100%;max-height:5%}.flex-gt-sm-10,.layout-row>.flex-gt-sm-10{max-width:10%;max-height:100%}.flex-gt-sm-10,.layout-column>.flex-gt-sm-10,.layout-row>.flex-gt-sm-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-10{max-width:100%;max-height:10%}.layout-gt-sm-row>.flex-gt-sm-10{max-width:10%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-10,.layout-gt-sm-row>.flex-gt-sm-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-10{max-width:100%;max-height:10%}.flex-gt-sm-15,.layout-row>.flex-gt-sm-15{max-width:15%;max-height:100%}.flex-gt-sm-15,.layout-column>.flex-gt-sm-15,.layout-row>.flex-gt-sm-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-15{max-width:100%;max-height:15%}.layout-gt-sm-row>.flex-gt-sm-15{max-width:15%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-15,.layout-gt-sm-row>.flex-gt-sm-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-15{max-width:100%;max-height:15%}.flex-gt-sm-20,.layout-row>.flex-gt-sm-20{max-width:20%;max-height:100%}.flex-gt-sm-20,.layout-column>.flex-gt-sm-20,.layout-row>.flex-gt-sm-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-20{max-width:100%;max-height:20%}.layout-gt-sm-row>.flex-gt-sm-20{max-width:20%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-20,.layout-gt-sm-row>.flex-gt-sm-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-20{max-width:100%;max-height:20%}.flex-gt-sm-25,.layout-row>.flex-gt-sm-25{max-width:25%;max-height:100%}.flex-gt-sm-25,.layout-column>.flex-gt-sm-25,.layout-row>.flex-gt-sm-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-25{max-width:100%;max-height:25%}.layout-gt-sm-row>.flex-gt-sm-25{max-width:25%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-25,.layout-gt-sm-row>.flex-gt-sm-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-25{max-width:100%;max-height:25%}.flex-gt-sm-30,.layout-row>.flex-gt-sm-30{max-width:30%;max-height:100%}.flex-gt-sm-30,.layout-column>.flex-gt-sm-30,.layout-row>.flex-gt-sm-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-30{max-width:100%;max-height:30%}.layout-gt-sm-row>.flex-gt-sm-30{max-width:30%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-30,.layout-gt-sm-row>.flex-gt-sm-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-30{max-width:100%;max-height:30%}.flex-gt-sm-35,.layout-row>.flex-gt-sm-35{max-width:35%;max-height:100%}.flex-gt-sm-35,.layout-column>.flex-gt-sm-35,.layout-row>.flex-gt-sm-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-35{max-width:100%;max-height:35%}.layout-gt-sm-row>.flex-gt-sm-35{max-width:35%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-35,.layout-gt-sm-row>.flex-gt-sm-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-35{max-width:100%;max-height:35%}.flex-gt-sm-40,.layout-row>.flex-gt-sm-40{max-width:40%;max-height:100%}.flex-gt-sm-40,.layout-column>.flex-gt-sm-40,.layout-row>.flex-gt-sm-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-40{max-width:100%;max-height:40%}.layout-gt-sm-row>.flex-gt-sm-40{max-width:40%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-40,.layout-gt-sm-row>.flex-gt-sm-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-40{max-width:100%;max-height:40%}.flex-gt-sm-45,.layout-row>.flex-gt-sm-45{max-width:45%;max-height:100%}.flex-gt-sm-45,.layout-column>.flex-gt-sm-45,.layout-row>.flex-gt-sm-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-45{max-width:100%;max-height:45%}.layout-gt-sm-row>.flex-gt-sm-45{max-width:45%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-45,.layout-gt-sm-row>.flex-gt-sm-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-45{max-width:100%;max-height:45%}.flex-gt-sm-50,.layout-row>.flex-gt-sm-50{max-width:50%;max-height:100%}.flex-gt-sm-50,.layout-column>.flex-gt-sm-50,.layout-row>.flex-gt-sm-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-50{max-width:100%;max-height:50%}.layout-gt-sm-row>.flex-gt-sm-50{max-width:50%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-50,.layout-gt-sm-row>.flex-gt-sm-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-50{max-width:100%;max-height:50%}.flex-gt-sm-55,.layout-row>.flex-gt-sm-55{max-width:55%;max-height:100%}.flex-gt-sm-55,.layout-column>.flex-gt-sm-55,.layout-row>.flex-gt-sm-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-55{max-width:100%;max-height:55%}.layout-gt-sm-row>.flex-gt-sm-55{max-width:55%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-55,.layout-gt-sm-row>.flex-gt-sm-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-55{max-width:100%;max-height:55%}.flex-gt-sm-60,.layout-row>.flex-gt-sm-60{max-width:60%;max-height:100%}.flex-gt-sm-60,.layout-column>.flex-gt-sm-60,.layout-row>.flex-gt-sm-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-60{max-width:100%;max-height:60%}.layout-gt-sm-row>.flex-gt-sm-60{max-width:60%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-60,.layout-gt-sm-row>.flex-gt-sm-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-60{max-width:100%;max-height:60%}.flex-gt-sm-65,.layout-row>.flex-gt-sm-65{max-width:65%;max-height:100%}.flex-gt-sm-65,.layout-column>.flex-gt-sm-65,.layout-row>.flex-gt-sm-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-65{max-width:100%;max-height:65%}.layout-gt-sm-row>.flex-gt-sm-65{max-width:65%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-65,.layout-gt-sm-row>.flex-gt-sm-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-65{max-width:100%;max-height:65%}.flex-gt-sm-70,.layout-row>.flex-gt-sm-70{max-width:70%;max-height:100%}.flex-gt-sm-70,.layout-column>.flex-gt-sm-70,.layout-row>.flex-gt-sm-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-70{max-width:100%;max-height:70%}.layout-gt-sm-row>.flex-gt-sm-70{max-width:70%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-70,.layout-gt-sm-row>.flex-gt-sm-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-70{max-width:100%;max-height:70%}.flex-gt-sm-75,.layout-row>.flex-gt-sm-75{max-width:75%;max-height:100%}.flex-gt-sm-75,.layout-column>.flex-gt-sm-75,.layout-row>.flex-gt-sm-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-75{max-width:100%;max-height:75%}.layout-gt-sm-row>.flex-gt-sm-75{max-width:75%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-75,.layout-gt-sm-row>.flex-gt-sm-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-75{max-width:100%;max-height:75%}.flex-gt-sm-80,.layout-row>.flex-gt-sm-80{max-width:80%;max-height:100%}.flex-gt-sm-80,.layout-column>.flex-gt-sm-80,.layout-row>.flex-gt-sm-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-80{max-width:100%;max-height:80%}.layout-gt-sm-row>.flex-gt-sm-80{max-width:80%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-80,.layout-gt-sm-row>.flex-gt-sm-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-80{max-width:100%;max-height:80%}.flex-gt-sm-85,.layout-row>.flex-gt-sm-85{max-width:85%;max-height:100%}.flex-gt-sm-85,.layout-column>.flex-gt-sm-85,.layout-row>.flex-gt-sm-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-85{max-width:100%;max-height:85%}.layout-gt-sm-row>.flex-gt-sm-85{max-width:85%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-85,.layout-gt-sm-row>.flex-gt-sm-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-85{max-width:100%;max-height:85%}.flex-gt-sm-90,.layout-row>.flex-gt-sm-90{max-width:90%;max-height:100%}.flex-gt-sm-90,.layout-column>.flex-gt-sm-90,.layout-row>.flex-gt-sm-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-90{max-width:100%;max-height:90%}.layout-gt-sm-row>.flex-gt-sm-90{max-width:90%;max-height:100%}.layout-gt-sm-column>.flex-gt-sm-90,.layout-gt-sm-row>.flex-gt-sm-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-90{max-width:100%;max-height:90%}.flex-gt-sm-95,.layout-row>.flex-gt-sm-95{max-width:95%;max-height:100%}.flex-gt-sm-95,.layout-column>.flex-gt-sm-95,.layout-row>.flex-gt-sm-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-sm-95{max-width:100%;max-height:95%}.layout-gt-sm-row>.flex-gt-sm-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:95%;max-height:100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-95{max-height:95%}.flex-gt-sm-100,.layout-gt-sm-column>.flex-gt-sm-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.flex-gt-sm-100{max-height:100%}.layout-column>.flex-gt-sm-100,.layout-gt-sm-column>.flex-gt-sm-100,.layout-gt-sm-row>.flex-gt-sm-100,.layout-row>.flex-gt-sm-100{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:100%;box-sizing:border-box}.layout-row>.flex-gt-sm-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:33.33%}.layout-row>.flex-gt-sm-33,.layout-row>.flex-gt-sm-66{-webkit-box-flex:1;max-height:100%;box-sizing:border-box}.layout-row>.flex-gt-sm-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:66.66%}.layout-column>.flex-gt-sm-33{-webkit-box-flex:1;-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:100%;max-height:33.33%;box-sizing:border-box}.layout-column>.flex-gt-sm-66{-webkit-box-flex:1;-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:100%;max-height:66.66%;box-sizing:border-box}.layout-gt-sm-row>.flex-gt-sm-33{max-width:33.33%}.layout-gt-sm-row>.flex-gt-sm-33,.layout-gt-sm-row>.flex-gt-sm-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-height:100%;box-sizing:border-box}.layout-gt-sm-row>.flex-gt-sm-66{max-width:66.66%}.layout-gt-sm-row>.flex{min-width:0}.layout-gt-sm-column>.flex-gt-sm-33{max-height:33.33%}.layout-gt-sm-column>.flex-gt-sm-33,.layout-gt-sm-column>.flex-gt-sm-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.layout-gt-sm-column>.flex-gt-sm-66{max-height:66.66%}.layout-gt-sm-column>.flex{min-height:0}.layout-gt-sm,.layout-gt-sm-column,.layout-gt-sm-row{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex}.layout-gt-sm-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.layout-gt-sm-row{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}}@media (min-width:960px) and (max-width:1279px){.hide-gt-sm:not(.show-gt-xs):not(.show-gt-sm):not(.show-md):not(.show),.hide-gt-xs:not(.show-gt-xs):not(.show-gt-sm):not(.show-md):not(.show),.hide-md:not(.show-md):not(.show-gt-sm):not(.show-gt-xs):not(.show),.hide:not(.show-gt-xs):not(.show-gt-sm):not(.show-md):not(.show){display:none}.flex-order-md--20{-webkit-box-ordinal-group:-19;-webkit-order:-20;order:-20}.flex-order-md--19{-webkit-box-ordinal-group:-18;-webkit-order:-19;order:-19}.flex-order-md--18{-webkit-box-ordinal-group:-17;-webkit-order:-18;order:-18}.flex-order-md--17{-webkit-box-ordinal-group:-16;-webkit-order:-17;order:-17}.flex-order-md--16{-webkit-box-ordinal-group:-15;-webkit-order:-16;order:-16}.flex-order-md--15{-webkit-box-ordinal-group:-14;-webkit-order:-15;order:-15}.flex-order-md--14{-webkit-box-ordinal-group:-13;-webkit-order:-14;order:-14}.flex-order-md--13{-webkit-box-ordinal-group:-12;-webkit-order:-13;order:-13}.flex-order-md--12{-webkit-box-ordinal-group:-11;-webkit-order:-12;order:-12}.flex-order-md--11{-webkit-box-ordinal-group:-10;-webkit-order:-11;order:-11}.flex-order-md--10{-webkit-box-ordinal-group:-9;-webkit-order:-10;order:-10}.flex-order-md--9{-webkit-box-ordinal-group:-8;-webkit-order:-9;order:-9}.flex-order-md--8{-webkit-box-ordinal-group:-7;-webkit-order:-8;order:-8}.flex-order-md--7{-webkit-box-ordinal-group:-6;-webkit-order:-7;order:-7}.flex-order-md--6{-webkit-box-ordinal-group:-5;-webkit-order:-6;order:-6}.flex-order-md--5{-webkit-box-ordinal-group:-4;-webkit-order:-5;order:-5}.flex-order-md--4{-webkit-box-ordinal-group:-3;-webkit-order:-4;order:-4}.flex-order-md--3{-webkit-box-ordinal-group:-2;-webkit-order:-3;order:-3}.flex-order-md--2{-webkit-box-ordinal-group:-1;-webkit-order:-2;order:-2}.flex-order-md--1{-webkit-box-ordinal-group:0;-webkit-order:-1;order:-1}.flex-order-md-0{-webkit-box-ordinal-group:1;-webkit-order:0;order:0}.flex-order-md-1{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.flex-order-md-2{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.flex-order-md-3{-webkit-box-ordinal-group:4;-webkit-order:3;order:3}.flex-order-md-4{-webkit-box-ordinal-group:5;-webkit-order:4;order:4}.flex-order-md-5{-webkit-box-ordinal-group:6;-webkit-order:5;order:5}.flex-order-md-6{-webkit-box-ordinal-group:7;-webkit-order:6;order:6}.flex-order-md-7{-webkit-box-ordinal-group:8;-webkit-order:7;order:7}.flex-order-md-8{-webkit-box-ordinal-group:9;-webkit-order:8;order:8}.flex-order-md-9{-webkit-box-ordinal-group:10;-webkit-order:9;order:9}.flex-order-md-10{-webkit-box-ordinal-group:11;-webkit-order:10;order:10}.flex-order-md-11{-webkit-box-ordinal-group:12;-webkit-order:11;order:11}.flex-order-md-12{-webkit-box-ordinal-group:13;-webkit-order:12;order:12}.flex-order-md-13{-webkit-box-ordinal-group:14;-webkit-order:13;order:13}.flex-order-md-14{-webkit-box-ordinal-group:15;-webkit-order:14;order:14}.flex-order-md-15{-webkit-box-ordinal-group:16;-webkit-order:15;order:15}.flex-order-md-16{-webkit-box-ordinal-group:17;-webkit-order:16;order:16}.flex-order-md-17{-webkit-box-ordinal-group:18;-webkit-order:17;order:17}.flex-order-md-18{-webkit-box-ordinal-group:19;-webkit-order:18;order:18}.flex-order-md-19{-webkit-box-ordinal-group:20;-webkit-order:19;order:19}.flex-order-md-20{-webkit-box-ordinal-group:21;-webkit-order:20;order:20}.flex-offset-md-0,.offset-md-0{margin-left:0}[dir=rtl] .flex-offset-md-0,[dir=rtl] .offset-md-0{margin-left:auto;margin-right:0}.flex-offset-md-5,.offset-md-5{margin-left:5%}[dir=rtl] .flex-offset-md-5,[dir=rtl] .offset-md-5{margin-left:auto;margin-right:5%}.flex-offset-md-10,.offset-md-10{margin-left:10%}[dir=rtl] .flex-offset-md-10,[dir=rtl] .offset-md-10{margin-left:auto;margin-right:10%}.flex-offset-md-15,.offset-md-15{margin-left:15%}[dir=rtl] .flex-offset-md-15,[dir=rtl] .offset-md-15{margin-left:auto;margin-right:15%}.flex-offset-md-20,.offset-md-20{margin-left:20%}[dir=rtl] .flex-offset-md-20,[dir=rtl] .offset-md-20{margin-left:auto;margin-right:20%}.flex-offset-md-25,.offset-md-25{margin-left:25%}[dir=rtl] .flex-offset-md-25,[dir=rtl] .offset-md-25{margin-left:auto;margin-right:25%}.flex-offset-md-30,.offset-md-30{margin-left:30%}[dir=rtl] .flex-offset-md-30,[dir=rtl] .offset-md-30{margin-left:auto;margin-right:30%}.flex-offset-md-35,.offset-md-35{margin-left:35%}[dir=rtl] .flex-offset-md-35,[dir=rtl] .offset-md-35{margin-left:auto;margin-right:35%}.flex-offset-md-40,.offset-md-40{margin-left:40%}[dir=rtl] .flex-offset-md-40,[dir=rtl] .offset-md-40{margin-left:auto;margin-right:40%}.flex-offset-md-45,.offset-md-45{margin-left:45%}[dir=rtl] .flex-offset-md-45,[dir=rtl] .offset-md-45{margin-left:auto;margin-right:45%}.flex-offset-md-50,.offset-md-50{margin-left:50%}[dir=rtl] .flex-offset-md-50,[dir=rtl] .offset-md-50{margin-left:auto;margin-right:50%}.flex-offset-md-55,.offset-md-55{margin-left:55%}[dir=rtl] .flex-offset-md-55,[dir=rtl] .offset-md-55{margin-left:auto;margin-right:55%}.flex-offset-md-60,.offset-md-60{margin-left:60%}[dir=rtl] .flex-offset-md-60,[dir=rtl] .offset-md-60{margin-left:auto;margin-right:60%}.flex-offset-md-65,.offset-md-65{margin-left:65%}[dir=rtl] .flex-offset-md-65,[dir=rtl] .offset-md-65{margin-left:auto;margin-right:65%}.flex-offset-md-70,.offset-md-70{margin-left:70%}[dir=rtl] .flex-offset-md-70,[dir=rtl] .offset-md-70{margin-left:auto;margin-right:70%}.flex-offset-md-75,.offset-md-75{margin-left:75%}[dir=rtl] .flex-offset-md-75,[dir=rtl] .offset-md-75{margin-left:auto;margin-right:75%}.flex-offset-md-80,.offset-md-80{margin-left:80%}[dir=rtl] .flex-offset-md-80,[dir=rtl] .offset-md-80{margin-left:auto;margin-right:80%}.flex-offset-md-85,.offset-md-85{margin-left:85%}[dir=rtl] .flex-offset-md-85,[dir=rtl] .offset-md-85{margin-left:auto;margin-right:85%}.flex-offset-md-90,.offset-md-90{margin-left:90%}[dir=rtl] .flex-offset-md-90,[dir=rtl] .offset-md-90{margin-left:auto;margin-right:90%}.flex-offset-md-95,.offset-md-95{margin-left:95%}[dir=rtl] .flex-offset-md-95,[dir=rtl] .offset-md-95{margin-left:auto;margin-right:95%}.flex-offset-md-33,.offset-md-33{margin-left:33.33333%}.flex-offset-md-66,.offset-md-66{margin-left:66.66667%}[dir=rtl] .flex-offset-md-66,[dir=rtl] .offset-md-66{margin-left:auto;margin-right:66.66667%}.layout-align-md,.layout-align-md-start-stretch{-webkit-align-content:stretch;align-content:stretch;-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch}.layout-align-md,.layout-align-md-start,.layout-align-md-start-center,.layout-align-md-start-end,.layout-align-md-start-start,.layout-align-md-start-stretch{-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.layout-align-md-center,.layout-align-md-center-center,.layout-align-md-center-end,.layout-align-md-center-start,.layout-align-md-center-stretch{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.layout-align-md-end,.layout-align-md-end-center,.layout-align-md-end-end,.layout-align-md-end-start,.layout-align-md-end-stretch{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.layout-align-md-space-around,.layout-align-md-space-around-center,.layout-align-md-space-around-end,.layout-align-md-space-around-start,.layout-align-md-space-around-stretch{-webkit-justify-content:space-around;justify-content:space-around}.layout-align-md-space-between,.layout-align-md-space-between-center,.layout-align-md-space-between-end,.layout-align-md-space-between-start,.layout-align-md-space-between-stretch{-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.layout-align-md-center-start,.layout-align-md-end-start,.layout-align-md-space-around-start,.layout-align-md-space-between-start,.layout-align-md-start-start{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.layout-align-md-center-center,.layout-align-md-end-center,.layout-align-md-space-around-center,.layout-align-md-space-between-center,.layout-align-md-start-center{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-content:center;align-content:center;max-width:100%}.layout-align-md-center-center>*,.layout-align-md-end-center>*,.layout-align-md-space-around-center>*,.layout-align-md-space-between-center>*,.layout-align-md-start-center>*{max-width:100%;box-sizing:border-box}.layout-align-md-center-end,.layout-align-md-end-end,.layout-align-md-space-around-end,.layout-align-md-space-between-end,.layout-align-md-start-end{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;-webkit-align-content:flex-end;align-content:flex-end}.layout-align-md-center-stretch,.layout-align-md-end-stretch,.layout-align-md-space-around-stretch,.layout-align-md-space-between-stretch,.layout-align-md-start-stretch{-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch;-webkit-align-content:stretch;align-content:stretch}.flex-md{-webkit-flex:1;flex:1}.flex-md,.flex-md-grow{-webkit-box-flex:1;box-sizing:border-box}.flex-md-grow{-webkit-flex:1 1 100%;flex:1 1 100%}.flex-md-initial{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-md-auto{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;box-sizing:border-box}.flex-md-none{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;box-sizing:border-box}.flex-md-noshrink{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;box-sizing:border-box}.flex-md-nogrow{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-md-0,.layout-row>.flex-md-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:0;max-height:100%;box-sizing:border-box}.layout-row>.flex-md-0{min-width:0}.layout-column>.flex-md-0{max-width:100%;max-height:0%}.layout-column>.flex-md-0,.layout-md-row>.flex-md-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-row>.flex-md-0{max-width:0;max-height:100%;min-width:0}.layout-md-column>.flex-md-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:0%;box-sizing:border-box;min-height:0}.flex-md-5,.layout-row>.flex-md-5{max-width:5%;max-height:100%}.flex-md-5,.layout-column>.flex-md-5,.layout-row>.flex-md-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-5{max-width:100%;max-height:5%}.layout-md-row>.flex-md-5{max-width:5%;max-height:100%}.layout-md-column>.flex-md-5,.layout-md-row>.flex-md-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-5{max-width:100%;max-height:5%}.flex-md-10,.layout-row>.flex-md-10{max-width:10%;max-height:100%}.flex-md-10,.layout-column>.flex-md-10,.layout-row>.flex-md-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-10{max-width:100%;max-height:10%}.layout-md-row>.flex-md-10{max-width:10%;max-height:100%}.layout-md-column>.flex-md-10,.layout-md-row>.flex-md-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-10{max-width:100%;max-height:10%}.flex-md-15,.layout-row>.flex-md-15{max-width:15%;max-height:100%}.flex-md-15,.layout-column>.flex-md-15,.layout-row>.flex-md-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-15{max-width:100%;max-height:15%}.layout-md-row>.flex-md-15{max-width:15%;max-height:100%}.layout-md-column>.flex-md-15,.layout-md-row>.flex-md-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-15{max-width:100%;max-height:15%}.flex-md-20,.layout-row>.flex-md-20{max-width:20%;max-height:100%}.flex-md-20,.layout-column>.flex-md-20,.layout-row>.flex-md-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-20{max-width:100%;max-height:20%}.layout-md-row>.flex-md-20{max-width:20%;max-height:100%}.layout-md-column>.flex-md-20,.layout-md-row>.flex-md-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-20{max-width:100%;max-height:20%}.flex-md-25,.layout-row>.flex-md-25{max-width:25%;max-height:100%}.flex-md-25,.layout-column>.flex-md-25,.layout-row>.flex-md-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-25{max-width:100%;max-height:25%}.layout-md-row>.flex-md-25{max-width:25%;max-height:100%}.layout-md-column>.flex-md-25,.layout-md-row>.flex-md-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-25{max-width:100%;max-height:25%}.flex-md-30,.layout-row>.flex-md-30{max-width:30%;max-height:100%}.flex-md-30,.layout-column>.flex-md-30,.layout-row>.flex-md-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-30{max-width:100%;max-height:30%}.layout-md-row>.flex-md-30{max-width:30%;max-height:100%}.layout-md-column>.flex-md-30,.layout-md-row>.flex-md-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-30{max-width:100%;max-height:30%}.flex-md-35,.layout-row>.flex-md-35{max-width:35%;max-height:100%}.flex-md-35,.layout-column>.flex-md-35,.layout-row>.flex-md-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-35{max-width:100%;max-height:35%}.layout-md-row>.flex-md-35{max-width:35%;max-height:100%}.layout-md-column>.flex-md-35,.layout-md-row>.flex-md-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-35{max-width:100%;max-height:35%}.flex-md-40,.layout-row>.flex-md-40{max-width:40%;max-height:100%}.flex-md-40,.layout-column>.flex-md-40,.layout-row>.flex-md-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-40{max-width:100%;max-height:40%}.layout-md-row>.flex-md-40{max-width:40%;max-height:100%}.layout-md-column>.flex-md-40,.layout-md-row>.flex-md-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-40{max-width:100%;max-height:40%}.flex-md-45,.layout-row>.flex-md-45{max-width:45%;max-height:100%}.flex-md-45,.layout-column>.flex-md-45,.layout-row>.flex-md-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-45{max-width:100%;max-height:45%}.layout-md-row>.flex-md-45{max-width:45%;max-height:100%}.layout-md-column>.flex-md-45,.layout-md-row>.flex-md-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-45{max-width:100%;max-height:45%}.flex-md-50,.layout-row>.flex-md-50{max-width:50%;max-height:100%}.flex-md-50,.layout-column>.flex-md-50,.layout-row>.flex-md-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-50{max-width:100%;max-height:50%}.layout-md-row>.flex-md-50{max-width:50%;max-height:100%}.layout-md-column>.flex-md-50,.layout-md-row>.flex-md-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-50{max-width:100%;max-height:50%}.flex-md-55,.layout-row>.flex-md-55{max-width:55%;max-height:100%}.flex-md-55,.layout-column>.flex-md-55,.layout-row>.flex-md-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-55{max-width:100%;max-height:55%}.layout-md-row>.flex-md-55{max-width:55%;max-height:100%}.layout-md-column>.flex-md-55,.layout-md-row>.flex-md-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-55{max-width:100%;max-height:55%}.flex-md-60,.layout-row>.flex-md-60{max-width:60%;max-height:100%}.flex-md-60,.layout-column>.flex-md-60,.layout-row>.flex-md-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-60{max-width:100%;max-height:60%}.layout-md-row>.flex-md-60{max-width:60%;max-height:100%}.layout-md-column>.flex-md-60,.layout-md-row>.flex-md-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-60{max-width:100%;max-height:60%}.flex-md-65,.layout-row>.flex-md-65{max-width:65%;max-height:100%}.flex-md-65,.layout-column>.flex-md-65,.layout-row>.flex-md-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-65{max-width:100%;max-height:65%}.layout-md-row>.flex-md-65{max-width:65%;max-height:100%}.layout-md-column>.flex-md-65,.layout-md-row>.flex-md-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-65{max-width:100%;max-height:65%}.flex-md-70,.layout-row>.flex-md-70{max-width:70%;max-height:100%}.flex-md-70,.layout-column>.flex-md-70,.layout-row>.flex-md-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-70{max-width:100%;max-height:70%}.layout-md-row>.flex-md-70{max-width:70%;max-height:100%}.layout-md-column>.flex-md-70,.layout-md-row>.flex-md-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-70{max-width:100%;max-height:70%}.flex-md-75,.layout-row>.flex-md-75{max-width:75%;max-height:100%}.flex-md-75,.layout-column>.flex-md-75,.layout-row>.flex-md-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-75{max-width:100%;max-height:75%}.layout-md-row>.flex-md-75{max-width:75%;max-height:100%}.layout-md-column>.flex-md-75,.layout-md-row>.flex-md-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-75{max-width:100%;max-height:75%}.flex-md-80,.layout-row>.flex-md-80{max-width:80%;max-height:100%}.flex-md-80,.layout-column>.flex-md-80,.layout-row>.flex-md-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-80{max-width:100%;max-height:80%}.layout-md-row>.flex-md-80{max-width:80%;max-height:100%}.layout-md-column>.flex-md-80,.layout-md-row>.flex-md-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-80{max-width:100%;max-height:80%}.flex-md-85,.layout-row>.flex-md-85{max-width:85%;max-height:100%}.flex-md-85,.layout-column>.flex-md-85,.layout-row>.flex-md-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-85{max-width:100%;max-height:85%}.layout-md-row>.flex-md-85{max-width:85%;max-height:100%}.layout-md-column>.flex-md-85,.layout-md-row>.flex-md-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-85{max-width:100%;max-height:85%}.flex-md-90,.layout-row>.flex-md-90{max-width:90%;max-height:100%}.flex-md-90,.layout-column>.flex-md-90,.layout-row>.flex-md-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-90{max-width:100%;max-height:90%}.layout-md-row>.flex-md-90{max-width:90%;max-height:100%}.layout-md-column>.flex-md-90,.layout-md-row>.flex-md-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-md-column>.flex-md-90{max-width:100%;max-height:90%}.flex-md-95,.layout-row>.flex-md-95{max-width:95%;max-height:100%}.flex-md-95,.layout-column>.flex-md-95,.layout-row>.flex-md-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-md-95{max-width:100%;max-height:95%}.layout-md-row>.flex-md-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:95%;max-height:100%;box-sizing:border-box}.layout-md-column>.flex-md-95{max-height:95%}.flex-md-100,.layout-md-column>.flex-md-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.flex-md-100{max-height:100%}.layout-column>.flex-md-100,.layout-md-column>.flex-md-100,.layout-md-row>.flex-md-100,.layout-row>.flex-md-100{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:100%;box-sizing:border-box}.layout-row>.flex-md-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:33.33%}.layout-row>.flex-md-33,.layout-row>.flex-md-66{-webkit-box-flex:1;max-height:100%;box-sizing:border-box}.layout-row>.flex-md-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:66.66%}.layout-column>.flex-md-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-height:33.33%}.layout-column>.flex-md-33,.layout-column>.flex-md-66{-webkit-box-flex:1;max-width:100%;box-sizing:border-box}.layout-column>.flex-md-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-height:66.66%}.layout-md-row>.flex-md-33{max-width:33.33%}.layout-md-row>.flex-md-33,.layout-md-row>.flex-md-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-height:100%;box-sizing:border-box}.layout-md-row>.flex-md-66{max-width:66.66%}.layout-md-row>.flex{min-width:0}.layout-md-column>.flex-md-33{max-height:33.33%}.layout-md-column>.flex-md-33,.layout-md-column>.flex-md-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.layout-md-column>.flex-md-66{max-height:66.66%}.layout-md-column>.flex{min-height:0}.layout-md,.layout-md-column,.layout-md-row{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex}.layout-md-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.layout-md-row{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}}@media (min-width:1280px){.flex-order-gt-md--20{-webkit-box-ordinal-group:-19;-webkit-order:-20;order:-20}.flex-order-gt-md--19{-webkit-box-ordinal-group:-18;-webkit-order:-19;order:-19}.flex-order-gt-md--18{-webkit-box-ordinal-group:-17;-webkit-order:-18;order:-18}.flex-order-gt-md--17{-webkit-box-ordinal-group:-16;-webkit-order:-17;order:-17}.flex-order-gt-md--16{-webkit-box-ordinal-group:-15;-webkit-order:-16;order:-16}.flex-order-gt-md--15{-webkit-box-ordinal-group:-14;-webkit-order:-15;order:-15}.flex-order-gt-md--14{-webkit-box-ordinal-group:-13;-webkit-order:-14;order:-14}.flex-order-gt-md--13{-webkit-box-ordinal-group:-12;-webkit-order:-13;order:-13}.flex-order-gt-md--12{-webkit-box-ordinal-group:-11;-webkit-order:-12;order:-12}.flex-order-gt-md--11{-webkit-box-ordinal-group:-10;-webkit-order:-11;order:-11}.flex-order-gt-md--10{-webkit-box-ordinal-group:-9;-webkit-order:-10;order:-10}.flex-order-gt-md--9{-webkit-box-ordinal-group:-8;-webkit-order:-9;order:-9}.flex-order-gt-md--8{-webkit-box-ordinal-group:-7;-webkit-order:-8;order:-8}.flex-order-gt-md--7{-webkit-box-ordinal-group:-6;-webkit-order:-7;order:-7}.flex-order-gt-md--6{-webkit-box-ordinal-group:-5;-webkit-order:-6;order:-6}.flex-order-gt-md--5{-webkit-box-ordinal-group:-4;-webkit-order:-5;order:-5}.flex-order-gt-md--4{-webkit-box-ordinal-group:-3;-webkit-order:-4;order:-4}.flex-order-gt-md--3{-webkit-box-ordinal-group:-2;-webkit-order:-3;order:-3}.flex-order-gt-md--2{-webkit-box-ordinal-group:-1;-webkit-order:-2;order:-2}.flex-order-gt-md--1{-webkit-box-ordinal-group:0;-webkit-order:-1;order:-1}.flex-order-gt-md-0{-webkit-box-ordinal-group:1;-webkit-order:0;order:0}.flex-order-gt-md-1{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.flex-order-gt-md-2{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.flex-order-gt-md-3{-webkit-box-ordinal-group:4;-webkit-order:3;order:3}.flex-order-gt-md-4{-webkit-box-ordinal-group:5;-webkit-order:4;order:4}.flex-order-gt-md-5{-webkit-box-ordinal-group:6;-webkit-order:5;order:5}.flex-order-gt-md-6{-webkit-box-ordinal-group:7;-webkit-order:6;order:6}.flex-order-gt-md-7{-webkit-box-ordinal-group:8;-webkit-order:7;order:7}.flex-order-gt-md-8{-webkit-box-ordinal-group:9;-webkit-order:8;order:8}.flex-order-gt-md-9{-webkit-box-ordinal-group:10;-webkit-order:9;order:9}.flex-order-gt-md-10{-webkit-box-ordinal-group:11;-webkit-order:10;order:10}.flex-order-gt-md-11{-webkit-box-ordinal-group:12;-webkit-order:11;order:11}.flex-order-gt-md-12{-webkit-box-ordinal-group:13;-webkit-order:12;order:12}.flex-order-gt-md-13{-webkit-box-ordinal-group:14;-webkit-order:13;order:13}.flex-order-gt-md-14{-webkit-box-ordinal-group:15;-webkit-order:14;order:14}.flex-order-gt-md-15{-webkit-box-ordinal-group:16;-webkit-order:15;order:15}.flex-order-gt-md-16{-webkit-box-ordinal-group:17;-webkit-order:16;order:16}.flex-order-gt-md-17{-webkit-box-ordinal-group:18;-webkit-order:17;order:17}.flex-order-gt-md-18{-webkit-box-ordinal-group:19;-webkit-order:18;order:18}.flex-order-gt-md-19{-webkit-box-ordinal-group:20;-webkit-order:19;order:19}.flex-order-gt-md-20{-webkit-box-ordinal-group:21;-webkit-order:20;order:20}.flex-offset-gt-md-0,.offset-gt-md-0{margin-left:0}[dir=rtl] .flex-offset-gt-md-0,[dir=rtl] .offset-gt-md-0{margin-left:auto;margin-right:0}.flex-offset-gt-md-5,.offset-gt-md-5{margin-left:5%}[dir=rtl] .flex-offset-gt-md-5,[dir=rtl] .offset-gt-md-5{margin-left:auto;margin-right:5%}.flex-offset-gt-md-10,.offset-gt-md-10{margin-left:10%}[dir=rtl] .flex-offset-gt-md-10,[dir=rtl] .offset-gt-md-10{margin-left:auto;margin-right:10%}.flex-offset-gt-md-15,.offset-gt-md-15{margin-left:15%}[dir=rtl] .flex-offset-gt-md-15,[dir=rtl] .offset-gt-md-15{margin-left:auto;margin-right:15%}.flex-offset-gt-md-20,.offset-gt-md-20{margin-left:20%}[dir=rtl] .flex-offset-gt-md-20,[dir=rtl] .offset-gt-md-20{margin-left:auto;margin-right:20%}.flex-offset-gt-md-25,.offset-gt-md-25{margin-left:25%}[dir=rtl] .flex-offset-gt-md-25,[dir=rtl] .offset-gt-md-25{margin-left:auto;margin-right:25%}.flex-offset-gt-md-30,.offset-gt-md-30{margin-left:30%}[dir=rtl] .flex-offset-gt-md-30,[dir=rtl] .offset-gt-md-30{margin-left:auto;margin-right:30%}.flex-offset-gt-md-35,.offset-gt-md-35{margin-left:35%}[dir=rtl] .flex-offset-gt-md-35,[dir=rtl] .offset-gt-md-35{margin-left:auto;margin-right:35%}.flex-offset-gt-md-40,.offset-gt-md-40{margin-left:40%}[dir=rtl] .flex-offset-gt-md-40,[dir=rtl] .offset-gt-md-40{margin-left:auto;margin-right:40%}.flex-offset-gt-md-45,.offset-gt-md-45{margin-left:45%}[dir=rtl] .flex-offset-gt-md-45,[dir=rtl] .offset-gt-md-45{margin-left:auto;margin-right:45%}.flex-offset-gt-md-50,.offset-gt-md-50{margin-left:50%}[dir=rtl] .flex-offset-gt-md-50,[dir=rtl] .offset-gt-md-50{margin-left:auto;margin-right:50%}.flex-offset-gt-md-55,.offset-gt-md-55{margin-left:55%}[dir=rtl] .flex-offset-gt-md-55,[dir=rtl] .offset-gt-md-55{margin-left:auto;margin-right:55%}.flex-offset-gt-md-60,.offset-gt-md-60{margin-left:60%}[dir=rtl] .flex-offset-gt-md-60,[dir=rtl] .offset-gt-md-60{margin-left:auto;margin-right:60%}.flex-offset-gt-md-65,.offset-gt-md-65{margin-left:65%}[dir=rtl] .flex-offset-gt-md-65,[dir=rtl] .offset-gt-md-65{margin-left:auto;margin-right:65%}.flex-offset-gt-md-70,.offset-gt-md-70{margin-left:70%}[dir=rtl] .flex-offset-gt-md-70,[dir=rtl] .offset-gt-md-70{margin-left:auto;margin-right:70%}.flex-offset-gt-md-75,.offset-gt-md-75{margin-left:75%}[dir=rtl] .flex-offset-gt-md-75,[dir=rtl] .offset-gt-md-75{margin-left:auto;margin-right:75%}.flex-offset-gt-md-80,.offset-gt-md-80{margin-left:80%}[dir=rtl] .flex-offset-gt-md-80,[dir=rtl] .offset-gt-md-80{margin-left:auto;margin-right:80%}.flex-offset-gt-md-85,.offset-gt-md-85{margin-left:85%}[dir=rtl] .flex-offset-gt-md-85,[dir=rtl] .offset-gt-md-85{margin-left:auto;margin-right:85%}.flex-offset-gt-md-90,.offset-gt-md-90{margin-left:90%}[dir=rtl] .flex-offset-gt-md-90,[dir=rtl] .offset-gt-md-90{margin-left:auto;margin-right:90%}.flex-offset-gt-md-95,.offset-gt-md-95{margin-left:95%}[dir=rtl] .flex-offset-gt-md-95,[dir=rtl] .offset-gt-md-95{margin-left:auto;margin-right:95%}.flex-offset-gt-md-33,.offset-gt-md-33{margin-left:33.33333%}.flex-offset-gt-md-66,.offset-gt-md-66{margin-left:66.66667%}[dir=rtl] .flex-offset-gt-md-66,[dir=rtl] .offset-gt-md-66{margin-left:auto;margin-right:66.66667%}.layout-align-gt-md,.layout-align-gt-md-start-stretch{-webkit-align-content:stretch;align-content:stretch;-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch}.layout-align-gt-md,.layout-align-gt-md-start,.layout-align-gt-md-start-center,.layout-align-gt-md-start-end,.layout-align-gt-md-start-start,.layout-align-gt-md-start-stretch{-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.layout-align-gt-md-center,.layout-align-gt-md-center-center,.layout-align-gt-md-center-end,.layout-align-gt-md-center-start,.layout-align-gt-md-center-stretch{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.layout-align-gt-md-end,.layout-align-gt-md-end-center,.layout-align-gt-md-end-end,.layout-align-gt-md-end-start,.layout-align-gt-md-end-stretch{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.layout-align-gt-md-space-around,.layout-align-gt-md-space-around-center,.layout-align-gt-md-space-around-end,.layout-align-gt-md-space-around-start,.layout-align-gt-md-space-around-stretch{-webkit-justify-content:space-around;justify-content:space-around}.layout-align-gt-md-space-between,.layout-align-gt-md-space-between-center,.layout-align-gt-md-space-between-end,.layout-align-gt-md-space-between-start,.layout-align-gt-md-space-between-stretch{-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.layout-align-gt-md-center-start,.layout-align-gt-md-end-start,.layout-align-gt-md-space-around-start,.layout-align-gt-md-space-between-start,.layout-align-gt-md-start-start{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.layout-align-gt-md-center-center,.layout-align-gt-md-end-center,.layout-align-gt-md-space-around-center,.layout-align-gt-md-space-between-center,.layout-align-gt-md-start-center{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-content:center;align-content:center;max-width:100%}.layout-align-gt-md-center-center>*,.layout-align-gt-md-end-center>*,.layout-align-gt-md-space-around-center>*,.layout-align-gt-md-space-between-center>*,.layout-align-gt-md-start-center>*{max-width:100%;box-sizing:border-box}.layout-align-gt-md-center-end,.layout-align-gt-md-end-end,.layout-align-gt-md-space-around-end,.layout-align-gt-md-space-between-end,.layout-align-gt-md-start-end{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;-webkit-align-content:flex-end;align-content:flex-end}.layout-align-gt-md-center-stretch,.layout-align-gt-md-end-stretch,.layout-align-gt-md-space-around-stretch,.layout-align-gt-md-space-between-stretch,.layout-align-gt-md-start-stretch{-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch;-webkit-align-content:stretch;align-content:stretch}.flex-gt-md{-webkit-flex:1;flex:1}.flex-gt-md,.flex-gt-md-grow{-webkit-box-flex:1;box-sizing:border-box}.flex-gt-md-grow{-webkit-flex:1 1 100%;flex:1 1 100%}.flex-gt-md-initial{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-gt-md-auto{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;box-sizing:border-box}.flex-gt-md-none{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;box-sizing:border-box}.flex-gt-md-noshrink{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;box-sizing:border-box}.flex-gt-md-nogrow{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-gt-md-0,.layout-row>.flex-gt-md-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:0;max-height:100%;box-sizing:border-box}.layout-row>.flex-gt-md-0{min-width:0}.layout-column>.flex-gt-md-0{max-width:100%;max-height:0%}.layout-column>.flex-gt-md-0,.layout-gt-md-row>.flex-gt-md-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-row>.flex-gt-md-0{max-width:0;max-height:100%;min-width:0}.layout-gt-md-column>.flex-gt-md-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:0%;box-sizing:border-box;min-height:0}.flex-gt-md-5,.layout-row>.flex-gt-md-5{max-width:5%;max-height:100%}.flex-gt-md-5,.layout-column>.flex-gt-md-5,.layout-row>.flex-gt-md-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-5{max-width:100%;max-height:5%}.layout-gt-md-row>.flex-gt-md-5{max-width:5%;max-height:100%}.layout-gt-md-column>.flex-gt-md-5,.layout-gt-md-row>.flex-gt-md-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-5{max-width:100%;max-height:5%}.flex-gt-md-10,.layout-row>.flex-gt-md-10{max-width:10%;max-height:100%}.flex-gt-md-10,.layout-column>.flex-gt-md-10,.layout-row>.flex-gt-md-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-10{max-width:100%;max-height:10%}.layout-gt-md-row>.flex-gt-md-10{max-width:10%;max-height:100%}.layout-gt-md-column>.flex-gt-md-10,.layout-gt-md-row>.flex-gt-md-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-10{max-width:100%;max-height:10%}.flex-gt-md-15,.layout-row>.flex-gt-md-15{max-width:15%;max-height:100%}.flex-gt-md-15,.layout-column>.flex-gt-md-15,.layout-row>.flex-gt-md-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-15{max-width:100%;max-height:15%}.layout-gt-md-row>.flex-gt-md-15{max-width:15%;max-height:100%}.layout-gt-md-column>.flex-gt-md-15,.layout-gt-md-row>.flex-gt-md-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-15{max-width:100%;max-height:15%}.flex-gt-md-20,.layout-row>.flex-gt-md-20{max-width:20%;max-height:100%}.flex-gt-md-20,.layout-column>.flex-gt-md-20,.layout-row>.flex-gt-md-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-20{max-width:100%;max-height:20%}.layout-gt-md-row>.flex-gt-md-20{max-width:20%;max-height:100%}.layout-gt-md-column>.flex-gt-md-20,.layout-gt-md-row>.flex-gt-md-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-20{max-width:100%;max-height:20%}.flex-gt-md-25,.layout-row>.flex-gt-md-25{max-width:25%;max-height:100%}.flex-gt-md-25,.layout-column>.flex-gt-md-25,.layout-row>.flex-gt-md-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-25{max-width:100%;max-height:25%}.layout-gt-md-row>.flex-gt-md-25{max-width:25%;max-height:100%}.layout-gt-md-column>.flex-gt-md-25,.layout-gt-md-row>.flex-gt-md-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-25{max-width:100%;max-height:25%}.flex-gt-md-30,.layout-row>.flex-gt-md-30{max-width:30%;max-height:100%}.flex-gt-md-30,.layout-column>.flex-gt-md-30,.layout-row>.flex-gt-md-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-30{max-width:100%;max-height:30%}.layout-gt-md-row>.flex-gt-md-30{max-width:30%;max-height:100%}.layout-gt-md-column>.flex-gt-md-30,.layout-gt-md-row>.flex-gt-md-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-30{max-width:100%;max-height:30%}.flex-gt-md-35,.layout-row>.flex-gt-md-35{max-width:35%;max-height:100%}.flex-gt-md-35,.layout-column>.flex-gt-md-35,.layout-row>.flex-gt-md-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-35{max-width:100%;max-height:35%}.layout-gt-md-row>.flex-gt-md-35{max-width:35%;max-height:100%}.layout-gt-md-column>.flex-gt-md-35,.layout-gt-md-row>.flex-gt-md-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-35{max-width:100%;max-height:35%}.flex-gt-md-40,.layout-row>.flex-gt-md-40{max-width:40%;max-height:100%}.flex-gt-md-40,.layout-column>.flex-gt-md-40,.layout-row>.flex-gt-md-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-40{max-width:100%;max-height:40%}.layout-gt-md-row>.flex-gt-md-40{max-width:40%;max-height:100%}.layout-gt-md-column>.flex-gt-md-40,.layout-gt-md-row>.flex-gt-md-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-40{max-width:100%;max-height:40%}.flex-gt-md-45,.layout-row>.flex-gt-md-45{max-width:45%;max-height:100%}.flex-gt-md-45,.layout-column>.flex-gt-md-45,.layout-row>.flex-gt-md-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-45{max-width:100%;max-height:45%}.layout-gt-md-row>.flex-gt-md-45{max-width:45%;max-height:100%}.layout-gt-md-column>.flex-gt-md-45,.layout-gt-md-row>.flex-gt-md-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-45{max-width:100%;max-height:45%}.flex-gt-md-50,.layout-row>.flex-gt-md-50{max-width:50%;max-height:100%}.flex-gt-md-50,.layout-column>.flex-gt-md-50,.layout-row>.flex-gt-md-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-50{max-width:100%;max-height:50%}.layout-gt-md-row>.flex-gt-md-50{max-width:50%;max-height:100%}.layout-gt-md-column>.flex-gt-md-50,.layout-gt-md-row>.flex-gt-md-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-50{max-width:100%;max-height:50%}.flex-gt-md-55,.layout-row>.flex-gt-md-55{max-width:55%;max-height:100%}.flex-gt-md-55,.layout-column>.flex-gt-md-55,.layout-row>.flex-gt-md-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-55{max-width:100%;max-height:55%}.layout-gt-md-row>.flex-gt-md-55{max-width:55%;max-height:100%}.layout-gt-md-column>.flex-gt-md-55,.layout-gt-md-row>.flex-gt-md-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-55{max-width:100%;max-height:55%}.flex-gt-md-60,.layout-row>.flex-gt-md-60{max-width:60%;max-height:100%}.flex-gt-md-60,.layout-column>.flex-gt-md-60,.layout-row>.flex-gt-md-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-60{max-width:100%;max-height:60%}.layout-gt-md-row>.flex-gt-md-60{max-width:60%;max-height:100%}.layout-gt-md-column>.flex-gt-md-60,.layout-gt-md-row>.flex-gt-md-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-60{max-width:100%;max-height:60%}.flex-gt-md-65,.layout-row>.flex-gt-md-65{max-width:65%;max-height:100%}.flex-gt-md-65,.layout-column>.flex-gt-md-65,.layout-row>.flex-gt-md-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-65{max-width:100%;max-height:65%}.layout-gt-md-row>.flex-gt-md-65{max-width:65%;max-height:100%}.layout-gt-md-column>.flex-gt-md-65,.layout-gt-md-row>.flex-gt-md-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-65{max-width:100%;max-height:65%}.flex-gt-md-70,.layout-row>.flex-gt-md-70{max-width:70%;max-height:100%}.flex-gt-md-70,.layout-column>.flex-gt-md-70,.layout-row>.flex-gt-md-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-70{max-width:100%;max-height:70%}.layout-gt-md-row>.flex-gt-md-70{max-width:70%;max-height:100%}.layout-gt-md-column>.flex-gt-md-70,.layout-gt-md-row>.flex-gt-md-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-70{max-width:100%;max-height:70%}.flex-gt-md-75,.layout-row>.flex-gt-md-75{max-width:75%;max-height:100%}.flex-gt-md-75,.layout-column>.flex-gt-md-75,.layout-row>.flex-gt-md-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-75{max-width:100%;max-height:75%}.layout-gt-md-row>.flex-gt-md-75{max-width:75%;max-height:100%}.layout-gt-md-column>.flex-gt-md-75,.layout-gt-md-row>.flex-gt-md-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-75{max-width:100%;max-height:75%}.flex-gt-md-80,.layout-row>.flex-gt-md-80{max-width:80%;max-height:100%}.flex-gt-md-80,.layout-column>.flex-gt-md-80,.layout-row>.flex-gt-md-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-80{max-width:100%;max-height:80%}.layout-gt-md-row>.flex-gt-md-80{max-width:80%;max-height:100%}.layout-gt-md-column>.flex-gt-md-80,.layout-gt-md-row>.flex-gt-md-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-80{max-width:100%;max-height:80%}.flex-gt-md-85,.layout-row>.flex-gt-md-85{max-width:85%;max-height:100%}.flex-gt-md-85,.layout-column>.flex-gt-md-85,.layout-row>.flex-gt-md-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-85{max-width:100%;max-height:85%}.layout-gt-md-row>.flex-gt-md-85{max-width:85%;max-height:100%}.layout-gt-md-column>.flex-gt-md-85,.layout-gt-md-row>.flex-gt-md-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-85{max-width:100%;max-height:85%}.flex-gt-md-90,.layout-row>.flex-gt-md-90{max-width:90%;max-height:100%}.flex-gt-md-90,.layout-column>.flex-gt-md-90,.layout-row>.flex-gt-md-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-90{max-width:100%;max-height:90%}.layout-gt-md-row>.flex-gt-md-90{max-width:90%;max-height:100%}.layout-gt-md-column>.flex-gt-md-90,.layout-gt-md-row>.flex-gt-md-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-90{max-width:100%;max-height:90%}.flex-gt-md-95,.layout-row>.flex-gt-md-95{max-width:95%;max-height:100%}.flex-gt-md-95,.layout-column>.flex-gt-md-95,.layout-row>.flex-gt-md-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-md-95{max-width:100%;max-height:95%}.layout-gt-md-row>.flex-gt-md-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:95%;max-height:100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-95{max-height:95%}.flex-gt-md-100,.layout-gt-md-column>.flex-gt-md-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.flex-gt-md-100{max-height:100%}.layout-column>.flex-gt-md-100,.layout-gt-md-column>.flex-gt-md-100,.layout-gt-md-row>.flex-gt-md-100,.layout-row>.flex-gt-md-100{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:100%;box-sizing:border-box}.layout-row>.flex-gt-md-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:33.33%}.layout-row>.flex-gt-md-33,.layout-row>.flex-gt-md-66{-webkit-box-flex:1;max-height:100%;box-sizing:border-box}.layout-row>.flex-gt-md-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:66.66%}.layout-column>.flex-gt-md-33{-webkit-box-flex:1;-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:100%;max-height:33.33%;box-sizing:border-box}.layout-column>.flex-gt-md-66{-webkit-box-flex:1;-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:100%;max-height:66.66%;box-sizing:border-box}.layout-gt-md-row>.flex-gt-md-33{max-width:33.33%}.layout-gt-md-row>.flex-gt-md-33,.layout-gt-md-row>.flex-gt-md-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-height:100%;box-sizing:border-box}.layout-gt-md-row>.flex-gt-md-66{max-width:66.66%}.layout-gt-md-row>.flex{min-width:0}.layout-gt-md-column>.flex-gt-md-33{max-height:33.33%}.layout-gt-md-column>.flex-gt-md-33,.layout-gt-md-column>.flex-gt-md-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.layout-gt-md-column>.flex-gt-md-66{max-height:66.66%}.layout-gt-md-column>.flex{min-height:0}.layout-gt-md,.layout-gt-md-column,.layout-gt-md-row{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex}.layout-gt-md-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.layout-gt-md-row{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}}@media (min-width:1280px) and (max-width:1919px){.hide-gt-md:not(.show-gt-xs):not(.show-gt-sm):not(.show-gt-md):not(.show-lg):not(.show),.hide-gt-sm:not(.show-gt-xs):not(.show-gt-sm):not(.show-gt-md):not(.show-lg):not(.show),.hide-gt-xs:not(.show-gt-xs):not(.show-gt-sm):not(.show-gt-md):not(.show-lg):not(.show),.hide-lg:not(.show-lg):not(.show-gt-md):not(.show-gt-sm):not(.show-gt-xs):not(.show),.hide:not(.show-gt-xs):not(.show-gt-sm):not(.show-gt-md):not(.show-lg):not(.show){display:none}.flex-order-lg--20{-webkit-box-ordinal-group:-19;-webkit-order:-20;order:-20}.flex-order-lg--19{-webkit-box-ordinal-group:-18;-webkit-order:-19;order:-19}.flex-order-lg--18{-webkit-box-ordinal-group:-17;-webkit-order:-18;order:-18}.flex-order-lg--17{-webkit-box-ordinal-group:-16;-webkit-order:-17;order:-17}.flex-order-lg--16{-webkit-box-ordinal-group:-15;-webkit-order:-16;order:-16}.flex-order-lg--15{-webkit-box-ordinal-group:-14;-webkit-order:-15;order:-15}.flex-order-lg--14{-webkit-box-ordinal-group:-13;-webkit-order:-14;order:-14}.flex-order-lg--13{-webkit-box-ordinal-group:-12;-webkit-order:-13;order:-13}.flex-order-lg--12{-webkit-box-ordinal-group:-11;-webkit-order:-12;order:-12}.flex-order-lg--11{-webkit-box-ordinal-group:-10;-webkit-order:-11;order:-11}.flex-order-lg--10{-webkit-box-ordinal-group:-9;-webkit-order:-10;order:-10}.flex-order-lg--9{-webkit-box-ordinal-group:-8;-webkit-order:-9;order:-9}.flex-order-lg--8{-webkit-box-ordinal-group:-7;-webkit-order:-8;order:-8}.flex-order-lg--7{-webkit-box-ordinal-group:-6;-webkit-order:-7;order:-7}.flex-order-lg--6{-webkit-box-ordinal-group:-5;-webkit-order:-6;order:-6}.flex-order-lg--5{-webkit-box-ordinal-group:-4;-webkit-order:-5;order:-5}.flex-order-lg--4{-webkit-box-ordinal-group:-3;-webkit-order:-4;order:-4}.flex-order-lg--3{-webkit-box-ordinal-group:-2;-webkit-order:-3;order:-3}.flex-order-lg--2{-webkit-box-ordinal-group:-1;-webkit-order:-2;order:-2}.flex-order-lg--1{-webkit-box-ordinal-group:0;-webkit-order:-1;order:-1}.flex-order-lg-0{-webkit-box-ordinal-group:1;-webkit-order:0;order:0}.flex-order-lg-1{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.flex-order-lg-2{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.flex-order-lg-3{-webkit-box-ordinal-group:4;-webkit-order:3;order:3}.flex-order-lg-4{-webkit-box-ordinal-group:5;-webkit-order:4;order:4}.flex-order-lg-5{-webkit-box-ordinal-group:6;-webkit-order:5;order:5}.flex-order-lg-6{-webkit-box-ordinal-group:7;-webkit-order:6;order:6}.flex-order-lg-7{-webkit-box-ordinal-group:8;-webkit-order:7;order:7}.flex-order-lg-8{-webkit-box-ordinal-group:9;-webkit-order:8;order:8}.flex-order-lg-9{-webkit-box-ordinal-group:10;-webkit-order:9;order:9}.flex-order-lg-10{-webkit-box-ordinal-group:11;-webkit-order:10;order:10}.flex-order-lg-11{-webkit-box-ordinal-group:12;-webkit-order:11;order:11}.flex-order-lg-12{-webkit-box-ordinal-group:13;-webkit-order:12;order:12}.flex-order-lg-13{-webkit-box-ordinal-group:14;-webkit-order:13;order:13}.flex-order-lg-14{-webkit-box-ordinal-group:15;-webkit-order:14;order:14}.flex-order-lg-15{-webkit-box-ordinal-group:16;-webkit-order:15;order:15}.flex-order-lg-16{-webkit-box-ordinal-group:17;-webkit-order:16;order:16}.flex-order-lg-17{-webkit-box-ordinal-group:18;-webkit-order:17;order:17}.flex-order-lg-18{-webkit-box-ordinal-group:19;-webkit-order:18;order:18}.flex-order-lg-19{-webkit-box-ordinal-group:20;-webkit-order:19;order:19}.flex-order-lg-20{-webkit-box-ordinal-group:21;-webkit-order:20;order:20}.flex-offset-lg-0,.offset-lg-0{margin-left:0}[dir=rtl] .flex-offset-lg-0,[dir=rtl] .offset-lg-0{margin-left:auto;margin-right:0}.flex-offset-lg-5,.offset-lg-5{margin-left:5%}[dir=rtl] .flex-offset-lg-5,[dir=rtl] .offset-lg-5{margin-left:auto;margin-right:5%}.flex-offset-lg-10,.offset-lg-10{margin-left:10%}[dir=rtl] .flex-offset-lg-10,[dir=rtl] .offset-lg-10{margin-left:auto;margin-right:10%}.flex-offset-lg-15,.offset-lg-15{margin-left:15%}[dir=rtl] .flex-offset-lg-15,[dir=rtl] .offset-lg-15{margin-left:auto;margin-right:15%}.flex-offset-lg-20,.offset-lg-20{margin-left:20%}[dir=rtl] .flex-offset-lg-20,[dir=rtl] .offset-lg-20{margin-left:auto;margin-right:20%}.flex-offset-lg-25,.offset-lg-25{margin-left:25%}[dir=rtl] .flex-offset-lg-25,[dir=rtl] .offset-lg-25{margin-left:auto;margin-right:25%}.flex-offset-lg-30,.offset-lg-30{margin-left:30%}[dir=rtl] .flex-offset-lg-30,[dir=rtl] .offset-lg-30{margin-left:auto;margin-right:30%}.flex-offset-lg-35,.offset-lg-35{margin-left:35%}[dir=rtl] .flex-offset-lg-35,[dir=rtl] .offset-lg-35{margin-left:auto;margin-right:35%}.flex-offset-lg-40,.offset-lg-40{margin-left:40%}[dir=rtl] .flex-offset-lg-40,[dir=rtl] .offset-lg-40{margin-left:auto;margin-right:40%}.flex-offset-lg-45,.offset-lg-45{margin-left:45%}[dir=rtl] .flex-offset-lg-45,[dir=rtl] .offset-lg-45{margin-left:auto;margin-right:45%}.flex-offset-lg-50,.offset-lg-50{margin-left:50%}[dir=rtl] .flex-offset-lg-50,[dir=rtl] .offset-lg-50{margin-left:auto;margin-right:50%}.flex-offset-lg-55,.offset-lg-55{margin-left:55%}[dir=rtl] .flex-offset-lg-55,[dir=rtl] .offset-lg-55{margin-left:auto;margin-right:55%}.flex-offset-lg-60,.offset-lg-60{margin-left:60%}[dir=rtl] .flex-offset-lg-60,[dir=rtl] .offset-lg-60{margin-left:auto;margin-right:60%}.flex-offset-lg-65,.offset-lg-65{margin-left:65%}[dir=rtl] .flex-offset-lg-65,[dir=rtl] .offset-lg-65{margin-left:auto;margin-right:65%}.flex-offset-lg-70,.offset-lg-70{margin-left:70%}[dir=rtl] .flex-offset-lg-70,[dir=rtl] .offset-lg-70{margin-left:auto;margin-right:70%}.flex-offset-lg-75,.offset-lg-75{margin-left:75%}[dir=rtl] .flex-offset-lg-75,[dir=rtl] .offset-lg-75{margin-left:auto;margin-right:75%}.flex-offset-lg-80,.offset-lg-80{margin-left:80%}[dir=rtl] .flex-offset-lg-80,[dir=rtl] .offset-lg-80{margin-left:auto;margin-right:80%}.flex-offset-lg-85,.offset-lg-85{margin-left:85%}[dir=rtl] .flex-offset-lg-85,[dir=rtl] .offset-lg-85{margin-left:auto;margin-right:85%}.flex-offset-lg-90,.offset-lg-90{margin-left:90%}[dir=rtl] .flex-offset-lg-90,[dir=rtl] .offset-lg-90{margin-left:auto;margin-right:90%}.flex-offset-lg-95,.offset-lg-95{margin-left:95%}[dir=rtl] .flex-offset-lg-95,[dir=rtl] .offset-lg-95{margin-left:auto;margin-right:95%}.flex-offset-lg-33,.offset-lg-33{margin-left:33.33333%}.flex-offset-lg-66,.offset-lg-66{margin-left:66.66667%}[dir=rtl] .flex-offset-lg-66,[dir=rtl] .offset-lg-66{margin-left:auto;margin-right:66.66667%}.layout-align-lg,.layout-align-lg-start-stretch{-webkit-align-content:stretch;align-content:stretch;-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch}.layout-align-lg,.layout-align-lg-start,.layout-align-lg-start-center,.layout-align-lg-start-end,.layout-align-lg-start-start,.layout-align-lg-start-stretch{-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.layout-align-lg-center,.layout-align-lg-center-center,.layout-align-lg-center-end,.layout-align-lg-center-start,.layout-align-lg-center-stretch{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.layout-align-lg-end,.layout-align-lg-end-center,.layout-align-lg-end-end,.layout-align-lg-end-start,.layout-align-lg-end-stretch{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.layout-align-lg-space-around,.layout-align-lg-space-around-center,.layout-align-lg-space-around-end,.layout-align-lg-space-around-start,.layout-align-lg-space-around-stretch{-webkit-justify-content:space-around;justify-content:space-around}.layout-align-lg-space-between,.layout-align-lg-space-between-center,.layout-align-lg-space-between-end,.layout-align-lg-space-between-start,.layout-align-lg-space-between-stretch{-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.layout-align-lg-center-start,.layout-align-lg-end-start,.layout-align-lg-space-around-start,.layout-align-lg-space-between-start,.layout-align-lg-start-start{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.layout-align-lg-center-center,.layout-align-lg-end-center,.layout-align-lg-space-around-center,.layout-align-lg-space-between-center,.layout-align-lg-start-center{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-content:center;align-content:center;max-width:100%}.layout-align-lg-center-center>*,.layout-align-lg-end-center>*,.layout-align-lg-space-around-center>*,.layout-align-lg-space-between-center>*,.layout-align-lg-start-center>*{max-width:100%;box-sizing:border-box}.layout-align-lg-center-end,.layout-align-lg-end-end,.layout-align-lg-space-around-end,.layout-align-lg-space-between-end,.layout-align-lg-start-end{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;-webkit-align-content:flex-end;align-content:flex-end}.layout-align-lg-center-stretch,.layout-align-lg-end-stretch,.layout-align-lg-space-around-stretch,.layout-align-lg-space-between-stretch,.layout-align-lg-start-stretch{-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch;-webkit-align-content:stretch;align-content:stretch}.flex-lg{-webkit-flex:1;flex:1}.flex-lg,.flex-lg-grow{-webkit-box-flex:1;box-sizing:border-box}.flex-lg-grow{-webkit-flex:1 1 100%;flex:1 1 100%}.flex-lg-initial{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-lg-auto{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;box-sizing:border-box}.flex-lg-none{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;box-sizing:border-box}.flex-lg-noshrink{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;box-sizing:border-box}.flex-lg-nogrow{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-lg-0,.layout-row>.flex-lg-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:0;max-height:100%;box-sizing:border-box}.layout-row>.flex-lg-0{min-width:0}.layout-column>.flex-lg-0{max-width:100%;max-height:0%}.layout-column>.flex-lg-0,.layout-lg-row>.flex-lg-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-row>.flex-lg-0{max-width:0;max-height:100%;min-width:0}.layout-lg-column>.flex-lg-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:0%;box-sizing:border-box;min-height:0}.flex-lg-5,.layout-row>.flex-lg-5{max-width:5%;max-height:100%}.flex-lg-5,.layout-column>.flex-lg-5,.layout-row>.flex-lg-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-5{max-width:100%;max-height:5%}.layout-lg-row>.flex-lg-5{max-width:5%;max-height:100%}.layout-lg-column>.flex-lg-5,.layout-lg-row>.flex-lg-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-5{max-width:100%;max-height:5%}.flex-lg-10,.layout-row>.flex-lg-10{max-width:10%;max-height:100%}.flex-lg-10,.layout-column>.flex-lg-10,.layout-row>.flex-lg-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-10{max-width:100%;max-height:10%}.layout-lg-row>.flex-lg-10{max-width:10%;max-height:100%}.layout-lg-column>.flex-lg-10,.layout-lg-row>.flex-lg-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-10{max-width:100%;max-height:10%}.flex-lg-15,.layout-row>.flex-lg-15{max-width:15%;max-height:100%}.flex-lg-15,.layout-column>.flex-lg-15,.layout-row>.flex-lg-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-15{max-width:100%;max-height:15%}.layout-lg-row>.flex-lg-15{max-width:15%;max-height:100%}.layout-lg-column>.flex-lg-15,.layout-lg-row>.flex-lg-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-15{max-width:100%;max-height:15%}.flex-lg-20,.layout-row>.flex-lg-20{max-width:20%;max-height:100%}.flex-lg-20,.layout-column>.flex-lg-20,.layout-row>.flex-lg-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-20{max-width:100%;max-height:20%}.layout-lg-row>.flex-lg-20{max-width:20%;max-height:100%}.layout-lg-column>.flex-lg-20,.layout-lg-row>.flex-lg-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-20{max-width:100%;max-height:20%}.flex-lg-25,.layout-row>.flex-lg-25{max-width:25%;max-height:100%}.flex-lg-25,.layout-column>.flex-lg-25,.layout-row>.flex-lg-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-25{max-width:100%;max-height:25%}.layout-lg-row>.flex-lg-25{max-width:25%;max-height:100%}.layout-lg-column>.flex-lg-25,.layout-lg-row>.flex-lg-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-25{max-width:100%;max-height:25%}.flex-lg-30,.layout-row>.flex-lg-30{max-width:30%;max-height:100%}.flex-lg-30,.layout-column>.flex-lg-30,.layout-row>.flex-lg-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-30{max-width:100%;max-height:30%}.layout-lg-row>.flex-lg-30{max-width:30%;max-height:100%}.layout-lg-column>.flex-lg-30,.layout-lg-row>.flex-lg-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-30{max-width:100%;max-height:30%}.flex-lg-35,.layout-row>.flex-lg-35{max-width:35%;max-height:100%}.flex-lg-35,.layout-column>.flex-lg-35,.layout-row>.flex-lg-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-35{max-width:100%;max-height:35%}.layout-lg-row>.flex-lg-35{max-width:35%;max-height:100%}.layout-lg-column>.flex-lg-35,.layout-lg-row>.flex-lg-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-35{max-width:100%;max-height:35%}.flex-lg-40,.layout-row>.flex-lg-40{max-width:40%;max-height:100%}.flex-lg-40,.layout-column>.flex-lg-40,.layout-row>.flex-lg-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-40{max-width:100%;max-height:40%}.layout-lg-row>.flex-lg-40{max-width:40%;max-height:100%}.layout-lg-column>.flex-lg-40,.layout-lg-row>.flex-lg-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-40{max-width:100%;max-height:40%}.flex-lg-45,.layout-row>.flex-lg-45{max-width:45%;max-height:100%}.flex-lg-45,.layout-column>.flex-lg-45,.layout-row>.flex-lg-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-45{max-width:100%;max-height:45%}.layout-lg-row>.flex-lg-45{max-width:45%;max-height:100%}.layout-lg-column>.flex-lg-45,.layout-lg-row>.flex-lg-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-45{max-width:100%;max-height:45%}.flex-lg-50,.layout-row>.flex-lg-50{max-width:50%;max-height:100%}.flex-lg-50,.layout-column>.flex-lg-50,.layout-row>.flex-lg-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-50{max-width:100%;max-height:50%}.layout-lg-row>.flex-lg-50{max-width:50%;max-height:100%}.layout-lg-column>.flex-lg-50,.layout-lg-row>.flex-lg-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-50{max-width:100%;max-height:50%}.flex-lg-55,.layout-row>.flex-lg-55{max-width:55%;max-height:100%}.flex-lg-55,.layout-column>.flex-lg-55,.layout-row>.flex-lg-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-55{max-width:100%;max-height:55%}.layout-lg-row>.flex-lg-55{max-width:55%;max-height:100%}.layout-lg-column>.flex-lg-55,.layout-lg-row>.flex-lg-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-55{max-width:100%;max-height:55%}.flex-lg-60,.layout-row>.flex-lg-60{max-width:60%;max-height:100%}.flex-lg-60,.layout-column>.flex-lg-60,.layout-row>.flex-lg-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-60{max-width:100%;max-height:60%}.layout-lg-row>.flex-lg-60{max-width:60%;max-height:100%}.layout-lg-column>.flex-lg-60,.layout-lg-row>.flex-lg-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-60{max-width:100%;max-height:60%}.flex-lg-65,.layout-row>.flex-lg-65{max-width:65%;max-height:100%}.flex-lg-65,.layout-column>.flex-lg-65,.layout-row>.flex-lg-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-65{max-width:100%;max-height:65%}.layout-lg-row>.flex-lg-65{max-width:65%;max-height:100%}.layout-lg-column>.flex-lg-65,.layout-lg-row>.flex-lg-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-65{max-width:100%;max-height:65%}.flex-lg-70,.layout-row>.flex-lg-70{max-width:70%;max-height:100%}.flex-lg-70,.layout-column>.flex-lg-70,.layout-row>.flex-lg-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-70{max-width:100%;max-height:70%}.layout-lg-row>.flex-lg-70{max-width:70%;max-height:100%}.layout-lg-column>.flex-lg-70,.layout-lg-row>.flex-lg-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-70{max-width:100%;max-height:70%}.flex-lg-75,.layout-row>.flex-lg-75{max-width:75%;max-height:100%}.flex-lg-75,.layout-column>.flex-lg-75,.layout-row>.flex-lg-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-75{max-width:100%;max-height:75%}.layout-lg-row>.flex-lg-75{max-width:75%;max-height:100%}.layout-lg-column>.flex-lg-75,.layout-lg-row>.flex-lg-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-75{max-width:100%;max-height:75%}.flex-lg-80,.layout-row>.flex-lg-80{max-width:80%;max-height:100%}.flex-lg-80,.layout-column>.flex-lg-80,.layout-row>.flex-lg-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-80{max-width:100%;max-height:80%}.layout-lg-row>.flex-lg-80{max-width:80%;max-height:100%}.layout-lg-column>.flex-lg-80,.layout-lg-row>.flex-lg-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-80{max-width:100%;max-height:80%}.flex-lg-85,.layout-row>.flex-lg-85{max-width:85%;max-height:100%}.flex-lg-85,.layout-column>.flex-lg-85,.layout-row>.flex-lg-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-85{max-width:100%;max-height:85%}.layout-lg-row>.flex-lg-85{max-width:85%;max-height:100%}.layout-lg-column>.flex-lg-85,.layout-lg-row>.flex-lg-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-85{max-width:100%;max-height:85%}.flex-lg-90,.layout-row>.flex-lg-90{max-width:90%;max-height:100%}.flex-lg-90,.layout-column>.flex-lg-90,.layout-row>.flex-lg-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-90{max-width:100%;max-height:90%}.layout-lg-row>.flex-lg-90{max-width:90%;max-height:100%}.layout-lg-column>.flex-lg-90,.layout-lg-row>.flex-lg-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-lg-column>.flex-lg-90{max-width:100%;max-height:90%}.flex-lg-95,.layout-row>.flex-lg-95{max-width:95%;max-height:100%}.flex-lg-95,.layout-column>.flex-lg-95,.layout-row>.flex-lg-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-lg-95{max-width:100%;max-height:95%}.layout-lg-row>.flex-lg-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:95%;max-height:100%;box-sizing:border-box}.layout-lg-column>.flex-lg-95{max-height:95%}.flex-lg-100,.layout-lg-column>.flex-lg-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.flex-lg-100{max-height:100%}.layout-column>.flex-lg-100,.layout-lg-column>.flex-lg-100,.layout-lg-row>.flex-lg-100,.layout-row>.flex-lg-100{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:100%;box-sizing:border-box}.layout-row>.flex-lg-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:33.33%}.layout-row>.flex-lg-33,.layout-row>.flex-lg-66{-webkit-box-flex:1;max-height:100%;box-sizing:border-box}.layout-row>.flex-lg-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:66.66%}.layout-column>.flex-lg-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-height:33.33%}.layout-column>.flex-lg-33,.layout-column>.flex-lg-66{-webkit-box-flex:1;max-width:100%;box-sizing:border-box}.layout-column>.flex-lg-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-height:66.66%}.layout-lg-row>.flex-lg-33{max-width:33.33%}.layout-lg-row>.flex-lg-33,.layout-lg-row>.flex-lg-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-height:100%;box-sizing:border-box}.layout-lg-row>.flex-lg-66{max-width:66.66%}.layout-lg-row>.flex{min-width:0}.layout-lg-column>.flex-lg-33{max-height:33.33%}.layout-lg-column>.flex-lg-33,.layout-lg-column>.flex-lg-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.layout-lg-column>.flex-lg-66{max-height:66.66%}.layout-lg-column>.flex{min-height:0}.layout-lg,.layout-lg-column,.layout-lg-row{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex}.layout-lg-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.layout-lg-row{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}}@media (min-width:1920px){.flex-order-gt-lg--20{-webkit-box-ordinal-group:-19;-webkit-order:-20;order:-20}.flex-order-gt-lg--19{-webkit-box-ordinal-group:-18;-webkit-order:-19;order:-19}.flex-order-gt-lg--18{-webkit-box-ordinal-group:-17;-webkit-order:-18;order:-18}.flex-order-gt-lg--17{-webkit-box-ordinal-group:-16;-webkit-order:-17;order:-17}.flex-order-gt-lg--16{-webkit-box-ordinal-group:-15;-webkit-order:-16;order:-16}.flex-order-gt-lg--15{-webkit-box-ordinal-group:-14;-webkit-order:-15;order:-15}.flex-order-gt-lg--14{-webkit-box-ordinal-group:-13;-webkit-order:-14;order:-14}.flex-order-gt-lg--13{-webkit-box-ordinal-group:-12;-webkit-order:-13;order:-13}.flex-order-gt-lg--12{-webkit-box-ordinal-group:-11;-webkit-order:-12;order:-12}.flex-order-gt-lg--11{-webkit-box-ordinal-group:-10;-webkit-order:-11;order:-11}.flex-order-gt-lg--10{-webkit-box-ordinal-group:-9;-webkit-order:-10;order:-10}.flex-order-gt-lg--9{-webkit-box-ordinal-group:-8;-webkit-order:-9;order:-9}.flex-order-gt-lg--8{-webkit-box-ordinal-group:-7;-webkit-order:-8;order:-8}.flex-order-gt-lg--7{-webkit-box-ordinal-group:-6;-webkit-order:-7;order:-7}.flex-order-gt-lg--6{-webkit-box-ordinal-group:-5;-webkit-order:-6;order:-6}.flex-order-gt-lg--5{-webkit-box-ordinal-group:-4;-webkit-order:-5;order:-5}.flex-order-gt-lg--4{-webkit-box-ordinal-group:-3;-webkit-order:-4;order:-4}.flex-order-gt-lg--3{-webkit-box-ordinal-group:-2;-webkit-order:-3;order:-3}.flex-order-gt-lg--2{-webkit-box-ordinal-group:-1;-webkit-order:-2;order:-2}.flex-order-gt-lg--1{-webkit-box-ordinal-group:0;-webkit-order:-1;order:-1}.flex-order-gt-lg-0{-webkit-box-ordinal-group:1;-webkit-order:0;order:0}.flex-order-gt-lg-1{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.flex-order-gt-lg-2{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.flex-order-gt-lg-3{-webkit-box-ordinal-group:4;-webkit-order:3;order:3}.flex-order-gt-lg-4{-webkit-box-ordinal-group:5;-webkit-order:4;order:4}.flex-order-gt-lg-5{-webkit-box-ordinal-group:6;-webkit-order:5;order:5}.flex-order-gt-lg-6{-webkit-box-ordinal-group:7;-webkit-order:6;order:6}.flex-order-gt-lg-7{-webkit-box-ordinal-group:8;-webkit-order:7;order:7}.flex-order-gt-lg-8{-webkit-box-ordinal-group:9;-webkit-order:8;order:8}.flex-order-gt-lg-9{-webkit-box-ordinal-group:10;-webkit-order:9;order:9}.flex-order-gt-lg-10{-webkit-box-ordinal-group:11;-webkit-order:10;order:10}.flex-order-gt-lg-11{-webkit-box-ordinal-group:12;-webkit-order:11;order:11}.flex-order-gt-lg-12{-webkit-box-ordinal-group:13;-webkit-order:12;order:12}.flex-order-gt-lg-13{-webkit-box-ordinal-group:14;-webkit-order:13;order:13}.flex-order-gt-lg-14{-webkit-box-ordinal-group:15;-webkit-order:14;order:14}.flex-order-gt-lg-15{-webkit-box-ordinal-group:16;-webkit-order:15;order:15}.flex-order-gt-lg-16{-webkit-box-ordinal-group:17;-webkit-order:16;order:16}.flex-order-gt-lg-17{-webkit-box-ordinal-group:18;-webkit-order:17;order:17}.flex-order-gt-lg-18{-webkit-box-ordinal-group:19;-webkit-order:18;order:18}.flex-order-gt-lg-19{-webkit-box-ordinal-group:20;-webkit-order:19;order:19}.flex-order-gt-lg-20{-webkit-box-ordinal-group:21;-webkit-order:20;order:20}.flex-offset-gt-lg-0,.offset-gt-lg-0{margin-left:0}[dir=rtl] .flex-offset-gt-lg-0,[dir=rtl] .offset-gt-lg-0{margin-left:auto;margin-right:0}.flex-offset-gt-lg-5,.offset-gt-lg-5{margin-left:5%}[dir=rtl] .flex-offset-gt-lg-5,[dir=rtl] .offset-gt-lg-5{margin-left:auto;margin-right:5%}.flex-offset-gt-lg-10,.offset-gt-lg-10{margin-left:10%}[dir=rtl] .flex-offset-gt-lg-10,[dir=rtl] .offset-gt-lg-10{margin-left:auto;margin-right:10%}.flex-offset-gt-lg-15,.offset-gt-lg-15{margin-left:15%}[dir=rtl] .flex-offset-gt-lg-15,[dir=rtl] .offset-gt-lg-15{margin-left:auto;margin-right:15%}.flex-offset-gt-lg-20,.offset-gt-lg-20{margin-left:20%}[dir=rtl] .flex-offset-gt-lg-20,[dir=rtl] .offset-gt-lg-20{margin-left:auto;margin-right:20%}.flex-offset-gt-lg-25,.offset-gt-lg-25{margin-left:25%}[dir=rtl] .flex-offset-gt-lg-25,[dir=rtl] .offset-gt-lg-25{margin-left:auto;margin-right:25%}.flex-offset-gt-lg-30,.offset-gt-lg-30{margin-left:30%}[dir=rtl] .flex-offset-gt-lg-30,[dir=rtl] .offset-gt-lg-30{margin-left:auto;margin-right:30%}.flex-offset-gt-lg-35,.offset-gt-lg-35{margin-left:35%}[dir=rtl] .flex-offset-gt-lg-35,[dir=rtl] .offset-gt-lg-35{margin-left:auto;margin-right:35%}.flex-offset-gt-lg-40,.offset-gt-lg-40{margin-left:40%}[dir=rtl] .flex-offset-gt-lg-40,[dir=rtl] .offset-gt-lg-40{margin-left:auto;margin-right:40%}.flex-offset-gt-lg-45,.offset-gt-lg-45{margin-left:45%}[dir=rtl] .flex-offset-gt-lg-45,[dir=rtl] .offset-gt-lg-45{margin-left:auto;margin-right:45%}.flex-offset-gt-lg-50,.offset-gt-lg-50{margin-left:50%}[dir=rtl] .flex-offset-gt-lg-50,[dir=rtl] .offset-gt-lg-50{margin-left:auto;margin-right:50%}.flex-offset-gt-lg-55,.offset-gt-lg-55{margin-left:55%}[dir=rtl] .flex-offset-gt-lg-55,[dir=rtl] .offset-gt-lg-55{margin-left:auto;margin-right:55%}.flex-offset-gt-lg-60,.offset-gt-lg-60{margin-left:60%}[dir=rtl] .flex-offset-gt-lg-60,[dir=rtl] .offset-gt-lg-60{margin-left:auto;margin-right:60%}.flex-offset-gt-lg-65,.offset-gt-lg-65{margin-left:65%}[dir=rtl] .flex-offset-gt-lg-65,[dir=rtl] .offset-gt-lg-65{margin-left:auto;margin-right:65%}.flex-offset-gt-lg-70,.offset-gt-lg-70{margin-left:70%}[dir=rtl] .flex-offset-gt-lg-70,[dir=rtl] .offset-gt-lg-70{margin-left:auto;margin-right:70%}.flex-offset-gt-lg-75,.offset-gt-lg-75{margin-left:75%}[dir=rtl] .flex-offset-gt-lg-75,[dir=rtl] .offset-gt-lg-75{margin-left:auto;margin-right:75%}.flex-offset-gt-lg-80,.offset-gt-lg-80{margin-left:80%}[dir=rtl] .flex-offset-gt-lg-80,[dir=rtl] .offset-gt-lg-80{margin-left:auto;margin-right:80%}.flex-offset-gt-lg-85,.offset-gt-lg-85{margin-left:85%}[dir=rtl] .flex-offset-gt-lg-85,[dir=rtl] .offset-gt-lg-85{margin-left:auto;margin-right:85%}.flex-offset-gt-lg-90,.offset-gt-lg-90{margin-left:90%}[dir=rtl] .flex-offset-gt-lg-90,[dir=rtl] .offset-gt-lg-90{margin-left:auto;margin-right:90%}.flex-offset-gt-lg-95,.offset-gt-lg-95{margin-left:95%}[dir=rtl] .flex-offset-gt-lg-95,[dir=rtl] .offset-gt-lg-95{margin-left:auto;margin-right:95%}.flex-offset-gt-lg-33,.offset-gt-lg-33{margin-left:33.33333%}.flex-offset-gt-lg-66,.offset-gt-lg-66{margin-left:66.66667%}[dir=rtl] .flex-offset-gt-lg-66,[dir=rtl] .offset-gt-lg-66{margin-left:auto;margin-right:66.66667%}.layout-align-gt-lg,.layout-align-gt-lg-start-stretch{-webkit-align-content:stretch;align-content:stretch;-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch}.layout-align-gt-lg,.layout-align-gt-lg-start,.layout-align-gt-lg-start-center,.layout-align-gt-lg-start-end,.layout-align-gt-lg-start-start,.layout-align-gt-lg-start-stretch{-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.layout-align-gt-lg-center,.layout-align-gt-lg-center-center,.layout-align-gt-lg-center-end,.layout-align-gt-lg-center-start,.layout-align-gt-lg-center-stretch{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.layout-align-gt-lg-end,.layout-align-gt-lg-end-center,.layout-align-gt-lg-end-end,.layout-align-gt-lg-end-start,.layout-align-gt-lg-end-stretch{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.layout-align-gt-lg-space-around,.layout-align-gt-lg-space-around-center,.layout-align-gt-lg-space-around-end,.layout-align-gt-lg-space-around-start,.layout-align-gt-lg-space-around-stretch{-webkit-justify-content:space-around;justify-content:space-around}.layout-align-gt-lg-space-between,.layout-align-gt-lg-space-between-center,.layout-align-gt-lg-space-between-end,.layout-align-gt-lg-space-between-start,.layout-align-gt-lg-space-between-stretch{-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.layout-align-gt-lg-center-start,.layout-align-gt-lg-end-start,.layout-align-gt-lg-space-around-start,.layout-align-gt-lg-space-between-start,.layout-align-gt-lg-start-start{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.layout-align-gt-lg-center-center,.layout-align-gt-lg-end-center,.layout-align-gt-lg-space-around-center,.layout-align-gt-lg-space-between-center,.layout-align-gt-lg-start-center{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-content:center;align-content:center;max-width:100%}.layout-align-gt-lg-center-center>*,.layout-align-gt-lg-end-center>*,.layout-align-gt-lg-space-around-center>*,.layout-align-gt-lg-space-between-center>*,.layout-align-gt-lg-start-center>*{max-width:100%;box-sizing:border-box}.layout-align-gt-lg-center-end,.layout-align-gt-lg-end-end,.layout-align-gt-lg-space-around-end,.layout-align-gt-lg-space-between-end,.layout-align-gt-lg-start-end{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;-webkit-align-content:flex-end;align-content:flex-end}.layout-align-gt-lg-center-stretch,.layout-align-gt-lg-end-stretch,.layout-align-gt-lg-space-around-stretch,.layout-align-gt-lg-space-between-stretch,.layout-align-gt-lg-start-stretch{-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch;-webkit-align-content:stretch;align-content:stretch}.flex-gt-lg{-webkit-flex:1;flex:1}.flex-gt-lg,.flex-gt-lg-grow{-webkit-box-flex:1;box-sizing:border-box}.flex-gt-lg-grow{-webkit-flex:1 1 100%;flex:1 1 100%}.flex-gt-lg-initial{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-gt-lg-auto{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;box-sizing:border-box}.flex-gt-lg-none{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;box-sizing:border-box}.flex-gt-lg-noshrink{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;box-sizing:border-box}.flex-gt-lg-nogrow{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-gt-lg-0,.layout-row>.flex-gt-lg-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:0;max-height:100%;box-sizing:border-box}.layout-row>.flex-gt-lg-0{min-width:0}.layout-column>.flex-gt-lg-0{max-width:100%;max-height:0%}.layout-column>.flex-gt-lg-0,.layout-gt-lg-row>.flex-gt-lg-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-row>.flex-gt-lg-0{max-width:0;max-height:100%;min-width:0}.layout-gt-lg-column>.flex-gt-lg-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:0%;box-sizing:border-box;min-height:0}.flex-gt-lg-5,.layout-row>.flex-gt-lg-5{max-width:5%;max-height:100%}.flex-gt-lg-5,.layout-column>.flex-gt-lg-5,.layout-row>.flex-gt-lg-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-5{max-width:100%;max-height:5%}.layout-gt-lg-row>.flex-gt-lg-5{max-width:5%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-5,.layout-gt-lg-row>.flex-gt-lg-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-5{max-width:100%;max-height:5%}.flex-gt-lg-10,.layout-row>.flex-gt-lg-10{max-width:10%;max-height:100%}.flex-gt-lg-10,.layout-column>.flex-gt-lg-10,.layout-row>.flex-gt-lg-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-10{max-width:100%;max-height:10%}.layout-gt-lg-row>.flex-gt-lg-10{max-width:10%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-10,.layout-gt-lg-row>.flex-gt-lg-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-10{max-width:100%;max-height:10%}.flex-gt-lg-15,.layout-row>.flex-gt-lg-15{max-width:15%;max-height:100%}.flex-gt-lg-15,.layout-column>.flex-gt-lg-15,.layout-row>.flex-gt-lg-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-15{max-width:100%;max-height:15%}.layout-gt-lg-row>.flex-gt-lg-15{max-width:15%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-15,.layout-gt-lg-row>.flex-gt-lg-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-15{max-width:100%;max-height:15%}.flex-gt-lg-20,.layout-row>.flex-gt-lg-20{max-width:20%;max-height:100%}.flex-gt-lg-20,.layout-column>.flex-gt-lg-20,.layout-row>.flex-gt-lg-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-20{max-width:100%;max-height:20%}.layout-gt-lg-row>.flex-gt-lg-20{max-width:20%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-20,.layout-gt-lg-row>.flex-gt-lg-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-20{max-width:100%;max-height:20%}.flex-gt-lg-25,.layout-row>.flex-gt-lg-25{max-width:25%;max-height:100%}.flex-gt-lg-25,.layout-column>.flex-gt-lg-25,.layout-row>.flex-gt-lg-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-25{max-width:100%;max-height:25%}.layout-gt-lg-row>.flex-gt-lg-25{max-width:25%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-25,.layout-gt-lg-row>.flex-gt-lg-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-25{max-width:100%;max-height:25%}.flex-gt-lg-30,.layout-row>.flex-gt-lg-30{max-width:30%;max-height:100%}.flex-gt-lg-30,.layout-column>.flex-gt-lg-30,.layout-row>.flex-gt-lg-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-30{max-width:100%;max-height:30%}.layout-gt-lg-row>.flex-gt-lg-30{max-width:30%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-30,.layout-gt-lg-row>.flex-gt-lg-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-30{max-width:100%;max-height:30%}.flex-gt-lg-35,.layout-row>.flex-gt-lg-35{max-width:35%;max-height:100%}.flex-gt-lg-35,.layout-column>.flex-gt-lg-35,.layout-row>.flex-gt-lg-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-35{max-width:100%;max-height:35%}.layout-gt-lg-row>.flex-gt-lg-35{max-width:35%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-35,.layout-gt-lg-row>.flex-gt-lg-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-35{max-width:100%;max-height:35%}.flex-gt-lg-40,.layout-row>.flex-gt-lg-40{max-width:40%;max-height:100%}.flex-gt-lg-40,.layout-column>.flex-gt-lg-40,.layout-row>.flex-gt-lg-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-40{max-width:100%;max-height:40%}.layout-gt-lg-row>.flex-gt-lg-40{max-width:40%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-40,.layout-gt-lg-row>.flex-gt-lg-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-40{max-width:100%;max-height:40%}.flex-gt-lg-45,.layout-row>.flex-gt-lg-45{max-width:45%;max-height:100%}.flex-gt-lg-45,.layout-column>.flex-gt-lg-45,.layout-row>.flex-gt-lg-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-45{max-width:100%;max-height:45%}.layout-gt-lg-row>.flex-gt-lg-45{max-width:45%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-45,.layout-gt-lg-row>.flex-gt-lg-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-45{max-width:100%;max-height:45%}.flex-gt-lg-50,.layout-row>.flex-gt-lg-50{max-width:50%;max-height:100%}.flex-gt-lg-50,.layout-column>.flex-gt-lg-50,.layout-row>.flex-gt-lg-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-50{max-width:100%;max-height:50%}.layout-gt-lg-row>.flex-gt-lg-50{max-width:50%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-50,.layout-gt-lg-row>.flex-gt-lg-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-50{max-width:100%;max-height:50%}.flex-gt-lg-55,.layout-row>.flex-gt-lg-55{max-width:55%;max-height:100%}.flex-gt-lg-55,.layout-column>.flex-gt-lg-55,.layout-row>.flex-gt-lg-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-55{max-width:100%;max-height:55%}.layout-gt-lg-row>.flex-gt-lg-55{max-width:55%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-55,.layout-gt-lg-row>.flex-gt-lg-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-55{max-width:100%;max-height:55%}.flex-gt-lg-60,.layout-row>.flex-gt-lg-60{max-width:60%;max-height:100%}.flex-gt-lg-60,.layout-column>.flex-gt-lg-60,.layout-row>.flex-gt-lg-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-60{max-width:100%;max-height:60%}.layout-gt-lg-row>.flex-gt-lg-60{max-width:60%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-60,.layout-gt-lg-row>.flex-gt-lg-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-60{max-width:100%;max-height:60%}.flex-gt-lg-65,.layout-row>.flex-gt-lg-65{max-width:65%;max-height:100%}.flex-gt-lg-65,.layout-column>.flex-gt-lg-65,.layout-row>.flex-gt-lg-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-65{max-width:100%;max-height:65%}.layout-gt-lg-row>.flex-gt-lg-65{max-width:65%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-65,.layout-gt-lg-row>.flex-gt-lg-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-65{max-width:100%;max-height:65%}.flex-gt-lg-70,.layout-row>.flex-gt-lg-70{max-width:70%;max-height:100%}.flex-gt-lg-70,.layout-column>.flex-gt-lg-70,.layout-row>.flex-gt-lg-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-70{max-width:100%;max-height:70%}.layout-gt-lg-row>.flex-gt-lg-70{max-width:70%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-70,.layout-gt-lg-row>.flex-gt-lg-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-70{max-width:100%;max-height:70%}.flex-gt-lg-75,.layout-row>.flex-gt-lg-75{max-width:75%;max-height:100%}.flex-gt-lg-75,.layout-column>.flex-gt-lg-75,.layout-row>.flex-gt-lg-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-75{max-width:100%;max-height:75%}.layout-gt-lg-row>.flex-gt-lg-75{max-width:75%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-75,.layout-gt-lg-row>.flex-gt-lg-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-75{max-width:100%;max-height:75%}.flex-gt-lg-80,.layout-row>.flex-gt-lg-80{max-width:80%;max-height:100%}.flex-gt-lg-80,.layout-column>.flex-gt-lg-80,.layout-row>.flex-gt-lg-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-80{max-width:100%;max-height:80%}.layout-gt-lg-row>.flex-gt-lg-80{max-width:80%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-80,.layout-gt-lg-row>.flex-gt-lg-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-80{max-width:100%;max-height:80%}.flex-gt-lg-85,.layout-row>.flex-gt-lg-85{max-width:85%;max-height:100%}.flex-gt-lg-85,.layout-column>.flex-gt-lg-85,.layout-row>.flex-gt-lg-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-85{max-width:100%;max-height:85%}.layout-gt-lg-row>.flex-gt-lg-85{max-width:85%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-85,.layout-gt-lg-row>.flex-gt-lg-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-85{max-width:100%;max-height:85%}.flex-gt-lg-90,.layout-row>.flex-gt-lg-90{max-width:90%;max-height:100%}.flex-gt-lg-90,.layout-column>.flex-gt-lg-90,.layout-row>.flex-gt-lg-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-90{max-width:100%;max-height:90%}.layout-gt-lg-row>.flex-gt-lg-90{max-width:90%;max-height:100%}.layout-gt-lg-column>.flex-gt-lg-90,.layout-gt-lg-row>.flex-gt-lg-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-90{max-width:100%;max-height:90%}.flex-gt-lg-95,.layout-row>.flex-gt-lg-95{max-width:95%;max-height:100%}.flex-gt-lg-95,.layout-column>.flex-gt-lg-95,.layout-row>.flex-gt-lg-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-gt-lg-95{max-width:100%;max-height:95%}.layout-gt-lg-row>.flex-gt-lg-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:95%;max-height:100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-95{max-height:95%}.flex-gt-lg-100,.layout-gt-lg-column>.flex-gt-lg-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.flex-gt-lg-100{max-height:100%}.layout-column>.flex-gt-lg-100,.layout-gt-lg-column>.flex-gt-lg-100,.layout-gt-lg-row>.flex-gt-lg-100,.layout-row>.flex-gt-lg-100{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:100%;box-sizing:border-box}.layout-row>.flex-gt-lg-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:33.33%}.layout-row>.flex-gt-lg-33,.layout-row>.flex-gt-lg-66{-webkit-box-flex:1;max-height:100%;box-sizing:border-box}.layout-row>.flex-gt-lg-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:66.66%}.layout-column>.flex-gt-lg-33{-webkit-box-flex:1;-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:100%;max-height:33.33%;box-sizing:border-box}.layout-column>.flex-gt-lg-66{-webkit-box-flex:1;-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:100%;max-height:66.66%;box-sizing:border-box}.layout-gt-lg-row>.flex-gt-lg-33{max-width:33.33%}.layout-gt-lg-row>.flex-gt-lg-33,.layout-gt-lg-row>.flex-gt-lg-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-height:100%;box-sizing:border-box}.layout-gt-lg-row>.flex-gt-lg-66{max-width:66.66%}.layout-gt-lg-row>.flex{min-width:0}.layout-gt-lg-column>.flex-gt-lg-33{max-height:33.33%}.layout-gt-lg-column>.flex-gt-lg-33,.layout-gt-lg-column>.flex-gt-lg-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.layout-gt-lg-column>.flex-gt-lg-66{max-height:66.66%}.layout-gt-lg-column>.flex{min-height:0}.layout-gt-lg,.layout-gt-lg-column,.layout-gt-lg-row{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex}.layout-gt-lg-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.layout-gt-lg-row{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.flex-order-xl--20{-webkit-box-ordinal-group:-19;-webkit-order:-20;order:-20}.flex-order-xl--19{-webkit-box-ordinal-group:-18;-webkit-order:-19;order:-19}.flex-order-xl--18{-webkit-box-ordinal-group:-17;-webkit-order:-18;order:-18}.flex-order-xl--17{-webkit-box-ordinal-group:-16;-webkit-order:-17;order:-17}.flex-order-xl--16{-webkit-box-ordinal-group:-15;-webkit-order:-16;order:-16}.flex-order-xl--15{-webkit-box-ordinal-group:-14;-webkit-order:-15;order:-15}.flex-order-xl--14{-webkit-box-ordinal-group:-13;-webkit-order:-14;order:-14}.flex-order-xl--13{-webkit-box-ordinal-group:-12;-webkit-order:-13;order:-13}.flex-order-xl--12{-webkit-box-ordinal-group:-11;-webkit-order:-12;order:-12}.flex-order-xl--11{-webkit-box-ordinal-group:-10;-webkit-order:-11;order:-11}.flex-order-xl--10{-webkit-box-ordinal-group:-9;-webkit-order:-10;order:-10}.flex-order-xl--9{-webkit-box-ordinal-group:-8;-webkit-order:-9;order:-9}.flex-order-xl--8{-webkit-box-ordinal-group:-7;-webkit-order:-8;order:-8}.flex-order-xl--7{-webkit-box-ordinal-group:-6;-webkit-order:-7;order:-7}.flex-order-xl--6{-webkit-box-ordinal-group:-5;-webkit-order:-6;order:-6}.flex-order-xl--5{-webkit-box-ordinal-group:-4;-webkit-order:-5;order:-5}.flex-order-xl--4{-webkit-box-ordinal-group:-3;-webkit-order:-4;order:-4}.flex-order-xl--3{-webkit-box-ordinal-group:-2;-webkit-order:-3;order:-3}.flex-order-xl--2{-webkit-box-ordinal-group:-1;-webkit-order:-2;order:-2}.flex-order-xl--1{-webkit-box-ordinal-group:0;-webkit-order:-1;order:-1}.flex-order-xl-0{-webkit-box-ordinal-group:1;-webkit-order:0;order:0}.flex-order-xl-1{-webkit-box-ordinal-group:2;-webkit-order:1;order:1}.flex-order-xl-2{-webkit-box-ordinal-group:3;-webkit-order:2;order:2}.flex-order-xl-3{-webkit-box-ordinal-group:4;-webkit-order:3;order:3}.flex-order-xl-4{-webkit-box-ordinal-group:5;-webkit-order:4;order:4}.flex-order-xl-5{-webkit-box-ordinal-group:6;-webkit-order:5;order:5}.flex-order-xl-6{-webkit-box-ordinal-group:7;-webkit-order:6;order:6}.flex-order-xl-7{-webkit-box-ordinal-group:8;-webkit-order:7;order:7}.flex-order-xl-8{-webkit-box-ordinal-group:9;-webkit-order:8;order:8}.flex-order-xl-9{-webkit-box-ordinal-group:10;-webkit-order:9;order:9}.flex-order-xl-10{-webkit-box-ordinal-group:11;-webkit-order:10;order:10}.flex-order-xl-11{-webkit-box-ordinal-group:12;-webkit-order:11;order:11}.flex-order-xl-12{-webkit-box-ordinal-group:13;-webkit-order:12;order:12}.flex-order-xl-13{-webkit-box-ordinal-group:14;-webkit-order:13;order:13}.flex-order-xl-14{-webkit-box-ordinal-group:15;-webkit-order:14;order:14}.flex-order-xl-15{-webkit-box-ordinal-group:16;-webkit-order:15;order:15}.flex-order-xl-16{-webkit-box-ordinal-group:17;-webkit-order:16;order:16}.flex-order-xl-17{-webkit-box-ordinal-group:18;-webkit-order:17;order:17}.flex-order-xl-18{-webkit-box-ordinal-group:19;-webkit-order:18;order:18}.flex-order-xl-19{-webkit-box-ordinal-group:20;-webkit-order:19;order:19}.flex-order-xl-20{-webkit-box-ordinal-group:21;-webkit-order:20;order:20}.flex-offset-xl-0,.offset-xl-0{margin-left:0}[dir=rtl] .flex-offset-xl-0,[dir=rtl] .offset-xl-0{margin-left:auto;margin-right:0}.flex-offset-xl-5,.offset-xl-5{margin-left:5%}[dir=rtl] .flex-offset-xl-5,[dir=rtl] .offset-xl-5{margin-left:auto;margin-right:5%}.flex-offset-xl-10,.offset-xl-10{margin-left:10%}[dir=rtl] .flex-offset-xl-10,[dir=rtl] .offset-xl-10{margin-left:auto;margin-right:10%}.flex-offset-xl-15,.offset-xl-15{margin-left:15%}[dir=rtl] .flex-offset-xl-15,[dir=rtl] .offset-xl-15{margin-left:auto;margin-right:15%}.flex-offset-xl-20,.offset-xl-20{margin-left:20%}[dir=rtl] .flex-offset-xl-20,[dir=rtl] .offset-xl-20{margin-left:auto;margin-right:20%}.flex-offset-xl-25,.offset-xl-25{margin-left:25%}[dir=rtl] .flex-offset-xl-25,[dir=rtl] .offset-xl-25{margin-left:auto;margin-right:25%}.flex-offset-xl-30,.offset-xl-30{margin-left:30%}[dir=rtl] .flex-offset-xl-30,[dir=rtl] .offset-xl-30{margin-left:auto;margin-right:30%}.flex-offset-xl-35,.offset-xl-35{margin-left:35%}[dir=rtl] .flex-offset-xl-35,[dir=rtl] .offset-xl-35{margin-left:auto;margin-right:35%}.flex-offset-xl-40,.offset-xl-40{margin-left:40%}[dir=rtl] .flex-offset-xl-40,[dir=rtl] .offset-xl-40{margin-left:auto;margin-right:40%}.flex-offset-xl-45,.offset-xl-45{margin-left:45%}[dir=rtl] .flex-offset-xl-45,[dir=rtl] .offset-xl-45{margin-left:auto;margin-right:45%}.flex-offset-xl-50,.offset-xl-50{margin-left:50%}[dir=rtl] .flex-offset-xl-50,[dir=rtl] .offset-xl-50{margin-left:auto;margin-right:50%}.flex-offset-xl-55,.offset-xl-55{margin-left:55%}[dir=rtl] .flex-offset-xl-55,[dir=rtl] .offset-xl-55{margin-left:auto;margin-right:55%}.flex-offset-xl-60,.offset-xl-60{margin-left:60%}[dir=rtl] .flex-offset-xl-60,[dir=rtl] .offset-xl-60{margin-left:auto;margin-right:60%}.flex-offset-xl-65,.offset-xl-65{margin-left:65%}[dir=rtl] .flex-offset-xl-65,[dir=rtl] .offset-xl-65{margin-left:auto;margin-right:65%}.flex-offset-xl-70,.offset-xl-70{margin-left:70%}[dir=rtl] .flex-offset-xl-70,[dir=rtl] .offset-xl-70{margin-left:auto;margin-right:70%}.flex-offset-xl-75,.offset-xl-75{margin-left:75%}[dir=rtl] .flex-offset-xl-75,[dir=rtl] .offset-xl-75{margin-left:auto;margin-right:75%}.flex-offset-xl-80,.offset-xl-80{margin-left:80%}[dir=rtl] .flex-offset-xl-80,[dir=rtl] .offset-xl-80{margin-left:auto;margin-right:80%}.flex-offset-xl-85,.offset-xl-85{margin-left:85%}[dir=rtl] .flex-offset-xl-85,[dir=rtl] .offset-xl-85{margin-left:auto;margin-right:85%}.flex-offset-xl-90,.offset-xl-90{margin-left:90%}[dir=rtl] .flex-offset-xl-90,[dir=rtl] .offset-xl-90{margin-left:auto;margin-right:90%}.flex-offset-xl-95,.offset-xl-95{margin-left:95%}[dir=rtl] .flex-offset-xl-95,[dir=rtl] .offset-xl-95{margin-left:auto;margin-right:95%}.flex-offset-xl-33,.offset-xl-33{margin-left:33.33333%}.flex-offset-xl-66,.offset-xl-66{margin-left:66.66667%}[dir=rtl] .flex-offset-xl-66,[dir=rtl] .offset-xl-66{margin-left:auto;margin-right:66.66667%}.layout-align-xl,.layout-align-xl-start-stretch{-webkit-align-content:stretch;align-content:stretch;-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch}.layout-align-xl,.layout-align-xl-start,.layout-align-xl-start-center,.layout-align-xl-start-end,.layout-align-xl-start-start,.layout-align-xl-start-stretch{-webkit-box-pack:start;-webkit-justify-content:flex-start;justify-content:flex-start}.layout-align-xl-center,.layout-align-xl-center-center,.layout-align-xl-center-end,.layout-align-xl-center-start,.layout-align-xl-center-stretch{-webkit-box-pack:center;-webkit-justify-content:center;justify-content:center}.layout-align-xl-end,.layout-align-xl-end-center,.layout-align-xl-end-end,.layout-align-xl-end-start,.layout-align-xl-end-stretch{-webkit-box-pack:end;-webkit-justify-content:flex-end;justify-content:flex-end}.layout-align-xl-space-around,.layout-align-xl-space-around-center,.layout-align-xl-space-around-end,.layout-align-xl-space-around-start,.layout-align-xl-space-around-stretch{-webkit-justify-content:space-around;justify-content:space-around}.layout-align-xl-space-between,.layout-align-xl-space-between-center,.layout-align-xl-space-between-end,.layout-align-xl-space-between-start,.layout-align-xl-space-between-stretch{-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between}.layout-align-xl-center-start,.layout-align-xl-end-start,.layout-align-xl-space-around-start,.layout-align-xl-space-between-start,.layout-align-xl-start-start{-webkit-box-align:start;-webkit-align-items:flex-start;align-items:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.layout-align-xl-center-center,.layout-align-xl-end-center,.layout-align-xl-space-around-center,.layout-align-xl-space-between-center,.layout-align-xl-start-center{-webkit-box-align:center;-webkit-align-items:center;align-items:center;-webkit-align-content:center;align-content:center;max-width:100%}.layout-align-xl-center-center>*,.layout-align-xl-end-center>*,.layout-align-xl-space-around-center>*,.layout-align-xl-space-between-center>*,.layout-align-xl-start-center>*{max-width:100%;box-sizing:border-box}.layout-align-xl-center-end,.layout-align-xl-end-end,.layout-align-xl-space-around-end,.layout-align-xl-space-between-end,.layout-align-xl-start-end{-webkit-box-align:end;-webkit-align-items:flex-end;align-items:flex-end;-webkit-align-content:flex-end;align-content:flex-end}.layout-align-xl-center-stretch,.layout-align-xl-end-stretch,.layout-align-xl-space-around-stretch,.layout-align-xl-space-between-stretch,.layout-align-xl-start-stretch{-webkit-box-align:stretch;-webkit-align-items:stretch;align-items:stretch;-webkit-align-content:stretch;align-content:stretch}.flex-xl{-webkit-flex:1;flex:1}.flex-xl,.flex-xl-grow{-webkit-box-flex:1;box-sizing:border-box}.flex-xl-grow{-webkit-flex:1 1 100%;flex:1 1 100%}.flex-xl-initial{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-xl-auto{-webkit-box-flex:1;-webkit-flex:1 1 auto;flex:1 1 auto;box-sizing:border-box}.flex-xl-none{-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;box-sizing:border-box}.flex-xl-noshrink{-webkit-box-flex:1;-webkit-flex:1 0 auto;flex:1 0 auto;box-sizing:border-box}.flex-xl-nogrow{-webkit-box-flex:0;-webkit-flex:0 1 auto;flex:0 1 auto;box-sizing:border-box}.flex-xl-0,.layout-row>.flex-xl-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:0;max-height:100%;box-sizing:border-box}.layout-row>.flex-xl-0{min-width:0}.layout-column>.flex-xl-0{max-width:100%;max-height:0%}.layout-column>.flex-xl-0,.layout-xl-row>.flex-xl-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-row>.flex-xl-0{max-width:0;max-height:100%;min-width:0}.layout-xl-column>.flex-xl-0{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:0%;box-sizing:border-box;min-height:0}.flex-xl-5,.layout-row>.flex-xl-5{max-width:5%;max-height:100%}.flex-xl-5,.layout-column>.flex-xl-5,.layout-row>.flex-xl-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-5{max-width:100%;max-height:5%}.layout-xl-row>.flex-xl-5{max-width:5%;max-height:100%}.layout-xl-column>.flex-xl-5,.layout-xl-row>.flex-xl-5{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-5{max-width:100%;max-height:5%}.flex-xl-10,.layout-row>.flex-xl-10{max-width:10%;max-height:100%}.flex-xl-10,.layout-column>.flex-xl-10,.layout-row>.flex-xl-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-10{max-width:100%;max-height:10%}.layout-xl-row>.flex-xl-10{max-width:10%;max-height:100%}.layout-xl-column>.flex-xl-10,.layout-xl-row>.flex-xl-10{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-10{max-width:100%;max-height:10%}.flex-xl-15,.layout-row>.flex-xl-15{max-width:15%;max-height:100%}.flex-xl-15,.layout-column>.flex-xl-15,.layout-row>.flex-xl-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-15{max-width:100%;max-height:15%}.layout-xl-row>.flex-xl-15{max-width:15%;max-height:100%}.layout-xl-column>.flex-xl-15,.layout-xl-row>.flex-xl-15{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-15{max-width:100%;max-height:15%}.flex-xl-20,.layout-row>.flex-xl-20{max-width:20%;max-height:100%}.flex-xl-20,.layout-column>.flex-xl-20,.layout-row>.flex-xl-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-20{max-width:100%;max-height:20%}.layout-xl-row>.flex-xl-20{max-width:20%;max-height:100%}.layout-xl-column>.flex-xl-20,.layout-xl-row>.flex-xl-20{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-20{max-width:100%;max-height:20%}.flex-xl-25,.layout-row>.flex-xl-25{max-width:25%;max-height:100%}.flex-xl-25,.layout-column>.flex-xl-25,.layout-row>.flex-xl-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-25{max-width:100%;max-height:25%}.layout-xl-row>.flex-xl-25{max-width:25%;max-height:100%}.layout-xl-column>.flex-xl-25,.layout-xl-row>.flex-xl-25{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-25{max-width:100%;max-height:25%}.flex-xl-30,.layout-row>.flex-xl-30{max-width:30%;max-height:100%}.flex-xl-30,.layout-column>.flex-xl-30,.layout-row>.flex-xl-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-30{max-width:100%;max-height:30%}.layout-xl-row>.flex-xl-30{max-width:30%;max-height:100%}.layout-xl-column>.flex-xl-30,.layout-xl-row>.flex-xl-30{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-30{max-width:100%;max-height:30%}.flex-xl-35,.layout-row>.flex-xl-35{max-width:35%;max-height:100%}.flex-xl-35,.layout-column>.flex-xl-35,.layout-row>.flex-xl-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-35{max-width:100%;max-height:35%}.layout-xl-row>.flex-xl-35{max-width:35%;max-height:100%}.layout-xl-column>.flex-xl-35,.layout-xl-row>.flex-xl-35{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-35{max-width:100%;max-height:35%}.flex-xl-40,.layout-row>.flex-xl-40{max-width:40%;max-height:100%}.flex-xl-40,.layout-column>.flex-xl-40,.layout-row>.flex-xl-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-40{max-width:100%;max-height:40%}.layout-xl-row>.flex-xl-40{max-width:40%;max-height:100%}.layout-xl-column>.flex-xl-40,.layout-xl-row>.flex-xl-40{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-40{max-width:100%;max-height:40%}.flex-xl-45,.layout-row>.flex-xl-45{max-width:45%;max-height:100%}.flex-xl-45,.layout-column>.flex-xl-45,.layout-row>.flex-xl-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-45{max-width:100%;max-height:45%}.layout-xl-row>.flex-xl-45{max-width:45%;max-height:100%}.layout-xl-column>.flex-xl-45,.layout-xl-row>.flex-xl-45{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-45{max-width:100%;max-height:45%}.flex-xl-50,.layout-row>.flex-xl-50{max-width:50%;max-height:100%}.flex-xl-50,.layout-column>.flex-xl-50,.layout-row>.flex-xl-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-50{max-width:100%;max-height:50%}.layout-xl-row>.flex-xl-50{max-width:50%;max-height:100%}.layout-xl-column>.flex-xl-50,.layout-xl-row>.flex-xl-50{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-50{max-width:100%;max-height:50%}.flex-xl-55,.layout-row>.flex-xl-55{max-width:55%;max-height:100%}.flex-xl-55,.layout-column>.flex-xl-55,.layout-row>.flex-xl-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-55{max-width:100%;max-height:55%}.layout-xl-row>.flex-xl-55{max-width:55%;max-height:100%}.layout-xl-column>.flex-xl-55,.layout-xl-row>.flex-xl-55{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-55{max-width:100%;max-height:55%}.flex-xl-60,.layout-row>.flex-xl-60{max-width:60%;max-height:100%}.flex-xl-60,.layout-column>.flex-xl-60,.layout-row>.flex-xl-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-60{max-width:100%;max-height:60%}.layout-xl-row>.flex-xl-60{max-width:60%;max-height:100%}.layout-xl-column>.flex-xl-60,.layout-xl-row>.flex-xl-60{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-60{max-width:100%;max-height:60%}.flex-xl-65,.layout-row>.flex-xl-65{max-width:65%;max-height:100%}.flex-xl-65,.layout-column>.flex-xl-65,.layout-row>.flex-xl-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-65{max-width:100%;max-height:65%}.layout-xl-row>.flex-xl-65{max-width:65%;max-height:100%}.layout-xl-column>.flex-xl-65,.layout-xl-row>.flex-xl-65{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-65{max-width:100%;max-height:65%}.flex-xl-70,.layout-row>.flex-xl-70{max-width:70%;max-height:100%}.flex-xl-70,.layout-column>.flex-xl-70,.layout-row>.flex-xl-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-70{max-width:100%;max-height:70%}.layout-xl-row>.flex-xl-70{max-width:70%;max-height:100%}.layout-xl-column>.flex-xl-70,.layout-xl-row>.flex-xl-70{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-70{max-width:100%;max-height:70%}.flex-xl-75,.layout-row>.flex-xl-75{max-width:75%;max-height:100%}.flex-xl-75,.layout-column>.flex-xl-75,.layout-row>.flex-xl-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-75{max-width:100%;max-height:75%}.layout-xl-row>.flex-xl-75{max-width:75%;max-height:100%}.layout-xl-column>.flex-xl-75,.layout-xl-row>.flex-xl-75{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-75{max-width:100%;max-height:75%}.flex-xl-80,.layout-row>.flex-xl-80{max-width:80%;max-height:100%}.flex-xl-80,.layout-column>.flex-xl-80,.layout-row>.flex-xl-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-80{max-width:100%;max-height:80%}.layout-xl-row>.flex-xl-80{max-width:80%;max-height:100%}.layout-xl-column>.flex-xl-80,.layout-xl-row>.flex-xl-80{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-80{max-width:100%;max-height:80%}.flex-xl-85,.layout-row>.flex-xl-85{max-width:85%;max-height:100%}.flex-xl-85,.layout-column>.flex-xl-85,.layout-row>.flex-xl-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-85{max-width:100%;max-height:85%}.layout-xl-row>.flex-xl-85{max-width:85%;max-height:100%}.layout-xl-column>.flex-xl-85,.layout-xl-row>.flex-xl-85{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-85{max-width:100%;max-height:85%}.flex-xl-90,.layout-row>.flex-xl-90{max-width:90%;max-height:100%}.flex-xl-90,.layout-column>.flex-xl-90,.layout-row>.flex-xl-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-90{max-width:100%;max-height:90%}.layout-xl-row>.flex-xl-90{max-width:90%;max-height:100%}.layout-xl-column>.flex-xl-90,.layout-xl-row>.flex-xl-90{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-xl-column>.flex-xl-90{max-width:100%;max-height:90%}.flex-xl-95,.layout-row>.flex-xl-95{max-width:95%;max-height:100%}.flex-xl-95,.layout-column>.flex-xl-95,.layout-row>.flex-xl-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;box-sizing:border-box}.layout-column>.flex-xl-95{max-width:100%;max-height:95%}.layout-xl-row>.flex-xl-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:95%;max-height:100%;box-sizing:border-box}.layout-xl-column>.flex-xl-95{max-height:95%}.flex-xl-100,.layout-xl-column>.flex-xl-95{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.flex-xl-100{max-height:100%}.layout-column>.flex-xl-100,.layout-row>.flex-xl-100,.layout-xl-column>.flex-xl-100,.layout-xl-row>.flex-xl-100{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;max-height:100%;box-sizing:border-box}.layout-row>.flex-xl-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-width:33.33%}.layout-row>.flex-xl-33,.layout-row>.flex-xl-66{-webkit-box-flex:1;max-height:100%;box-sizing:border-box}.layout-row>.flex-xl-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-width:66.66%}.layout-column>.flex-xl-33{-webkit-flex:1 1 33.33%;flex:1 1 33.33%;max-height:33.33%}.layout-column>.flex-xl-33,.layout-column>.flex-xl-66{-webkit-box-flex:1;max-width:100%;box-sizing:border-box}.layout-column>.flex-xl-66{-webkit-flex:1 1 66.66%;flex:1 1 66.66%;max-height:66.66%}.layout-xl-row>.flex-xl-33{max-width:33.33%}.layout-xl-row>.flex-xl-33,.layout-xl-row>.flex-xl-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-height:100%;box-sizing:border-box}.layout-xl-row>.flex-xl-66{max-width:66.66%}.layout-xl-row>.flex{min-width:0}.layout-xl-column>.flex-xl-33{max-height:33.33%}.layout-xl-column>.flex-xl-33,.layout-xl-column>.flex-xl-66{-webkit-box-flex:1;-webkit-flex:1 1 100%;flex:1 1 100%;max-width:100%;box-sizing:border-box}.layout-xl-column>.flex-xl-66{max-height:66.66%}.layout-xl-column>.flex{min-height:0}.layout-xl,.layout-xl-column,.layout-xl-row{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:flex}.layout-xl-column{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column}.layout-xl-row{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;flex-direction:row}.hide-gt-lg:not(.show-gt-xs):not(.show-gt-sm):not(.show-gt-md):not(.show-gt-lg):not(.show-xl):not(.show),.hide-gt-md:not(.show-gt-xs):not(.show-gt-sm):not(.show-gt-md):not(.show-gt-lg):not(.show-xl):not(.show),.hide-gt-sm:not(.show-gt-xs):not(.show-gt-sm):not(.show-gt-md):not(.show-gt-lg):not(.show-xl):not(.show),.hide-gt-xs:not(.show-gt-xs):not(.show-gt-sm):not(.show-gt-md):not(.show-gt-lg):not(.show-xl):not(.show),.hide-xl:not(.show-xl):not(.show-gt-lg):not(.show-gt-md):not(.show-gt-sm):not(.show-gt-xs):not(.show),.hide:not(.show-gt-xs):not(.show-gt-sm):not(.show-gt-md):not(.show-gt-lg):not(.show-xl):not(.show){display:none}}@media print{.hide-print:not(.show-print):not(.show){display:none!important}} \ No newline at end of file diff --git a/third_party/angular-material/repo/angular-material.min.js b/third_party/angular-material/repo/angular-material.min.js deleted file mode 100644 index f412349c6b8..00000000000 --- a/third_party/angular-material/repo/angular-material.min.js +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * AngularJS Material Design - * https://github.com/angular/material - * @license MIT - * v1.1.4 - */ -!function(e,t,n){"use strict";!function(){t.module("ngMaterial",["ng","ngAnimate","ngAria","material.core","material.core.interaction","material.core.gestures","material.core.layout","material.core.meta","material.core.theming.palette","material.core.theming","material.core.animate","material.components.autocomplete","material.components.backdrop","material.components.bottomSheet","material.components.button","material.components.card","material.components.checkbox","material.components.chips","material.components.colors","material.components.content","material.components.datepicker","material.components.dialog","material.components.divider","material.components.fabActions","material.components.fabShared","material.components.fabSpeedDial","material.components.fabToolbar","material.components.gridList","material.components.icon","material.components.input","material.components.list","material.components.menu","material.components.menuBar","material.components.navBar","material.components.progressCircular","material.components.panel","material.components.progressLinear","material.components.radioButton","material.components.showHide","material.components.sidenav","material.components.select","material.components.slider","material.components.sticky","material.components.subheader","material.components.switch","material.components.swipe","material.components.tabs","material.components.toast","material.components.toolbar","material.components.tooltip","material.components.truncate","material.components.virtualRepeat","material.components.whiteframe"])}(),function(){function e(e,t){if(t.has("$swipe")){var n="You are using the ngTouch module. \nAngularJS Material already has mobile click, tap, and swipe support... \nngTouch is not supported with AngularJS Material!";e.warn(n)}}function n(e,t){e.decorator("$$rAF",["$delegate",o]),e.decorator("$q",["$delegate",i]),t.theme("default").primaryPalette("indigo").accentPalette("pink").warnPalette("deep-orange").backgroundPalette("grey")}function o(e){return e.throttle=function(t){var n,o,i,r;return function(){n=arguments,r=this,i=t,o||(o=!0,e(function(){i.apply(r,Array.prototype.slice.call(n)),o=!1}))}},e}function i(e){return e.resolve||(e.resolve=e.when),e}e.$inject=["$log","$injector"],n.$inject=["$provide","$mdThemingProvider"],o.$inject=["$delegate"],i.$inject=["$delegate"],t.module("material.core",["ngAnimate","material.core.animate","material.core.layout","material.core.interaction","material.core.gestures","material.core.theming"]).config(n).run(e)}(),function(){function e(e){function n(n,o,i){function r(e){t.isUndefined(e)&&(e=!0),o.toggleClass("md-autofocus",!!e)}var a=i.mdAutoFocus||i.mdAutofocus||i.mdSidenavFocus;r(e(a)(n)),a&&n.$watch(a,r)}return{restrict:"A",link:{pre:n}}}e.$inject=["$parse"],t.module("material.core").directive("mdAutofocus",e).directive("mdAutoFocus",e).directive("mdSidenavFocus",e)}(),function(){function e(){function e(e){var t="#"===e[0]?e.substr(1):e,n=t.length/3,o=t.substr(0,n),i=t.substr(n,n),r=t.substr(2*n);return 1===n&&(o+=o,i+=i,r+=r),"rgba("+parseInt(o,16)+","+parseInt(i,16)+","+parseInt(r,16)+",0.1)"}function t(e){e=e.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);var t=e&&4===e.length?"#"+("0"+parseInt(e[1],10).toString(16)).slice(-2)+("0"+parseInt(e[2],10).toString(16)).slice(-2)+("0"+parseInt(e[3],10).toString(16)).slice(-2):"";return t.toUpperCase()}function n(e){return e.replace(")",", 0.1)").replace("(","a(")}function o(e){return e?e.replace("rgba","rgb").replace(/,[^\),]+\)/,")"):"rgb(0,0,0)"}return{rgbaToHex:t,hexToRgba:e,rgbToRgba:n,rgbaToRgb:o}}t.module("material.core").factory("$mdColorUtil",e)}(),function(){function e(){function e(e){var t=a+"-"+e,i=o(t),d=i.charAt(0).toLowerCase()+i.substring(1);return n(r,e)?e:n(r,i)?i:n(r,d)?d:e}function n(e,n){return t.isDefined(e.style[n])}function o(e){return e.replace(s,function(e,t,n,o){return o?n.toUpperCase():n})}function i(e){var t,n,o=/^(Moz|webkit|ms)(?=[A-Z])/;for(t in e.style)if(n=o.exec(t))return n[0]}var r=document.createElement("div"),a=i(r),d=/webkit/i.test(a),s=/([:\-_]+(.))/g,c={isInputKey:function(e){return e.keyCode>=31&&e.keyCode<=90},isNumPadKey:function(e){return 3===e.location&&e.keyCode>=97&&e.keyCode<=105},isMetaKey:function(e){return e.keyCode>=91&&e.keyCode<=93},isFnLockKey:function(e){return e.keyCode>=112&&e.keyCode<=145},isNavigationKey:function(e){var t=c.KEY_CODE,n=[t.SPACE,t.ENTER,t.UP_ARROW,t.DOWN_ARROW];return n.indexOf(e.keyCode)!=-1},hasModifierKey:function(e){return e.ctrlKey||e.metaKey||e.altKey},ELEMENT_MAX_PIXELS:1533917,BEFORE_NG_ARIA:210,KEY_CODE:{COMMA:188,SEMICOLON:186,ENTER:13,ESCAPE:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT_ARROW:37,UP_ARROW:38,RIGHT_ARROW:39,DOWN_ARROW:40,TAB:9,BACKSPACE:8,DELETE:46},CSS:{TRANSITIONEND:"transitionend"+(d?" webkitTransitionEnd":""),ANIMATIONEND:"animationend"+(d?" webkitAnimationEnd":""),TRANSFORM:e("transform"),TRANSFORM_ORIGIN:e("transformOrigin"),TRANSITION:e("transition"),TRANSITION_DURATION:e("transitionDuration"),ANIMATION_PLAY_STATE:e("animationPlayState"),ANIMATION_DURATION:e("animationDuration"),ANIMATION_NAME:e("animationName"),ANIMATION_TIMING:e("animationTimingFunction"),ANIMATION_DIRECTION:e("animationDirection")},MEDIA:{xs:"(max-width: 599px)","gt-xs":"(min-width: 600px)",sm:"(min-width: 600px) and (max-width: 959px)","gt-sm":"(min-width: 960px)",md:"(min-width: 960px) and (max-width: 1279px)","gt-md":"(min-width: 1280px)",lg:"(min-width: 1280px) and (max-width: 1919px)","gt-lg":"(min-width: 1920px)",xl:"(min-width: 1920px)",landscape:"(orientation: landscape)",portrait:"(orientation: portrait)",print:"print"},MEDIA_PRIORITY:["xl","gt-lg","lg","gt-md","md","gt-sm","sm","gt-xs","xs","landscape","portrait","print"]};return c}t.module("material.core").factory("$mdConstant",e)}(),function(){function e(e,n){function o(){return[].concat(v)}function i(){return v.length}function r(e){return v.length&&e>-1&&e-1}function h(){return v.length?v[0]:null}function f(){return v.length?v[v.length-1]:null}function g(e,o,i,a){i=i||b;for(var d=u(o);;){if(!r(d))return null;var s=d+(e?-1:1),c=null;if(r(s)?c=v[s]:n&&(c=e?f():h(),s=u(c)),null===c||s===a)return null;if(i(c))return c;t.isUndefined(a)&&(a=s),d=s}}var b=function(){return!0};e&&!t.isArray(e)&&(e=Array.prototype.slice.call(e)),n=!!n;var v=e||[];return{items:o,count:i,inRange:r,contains:p,indexOf:u,itemAt:s,findBy:c,add:l,remove:m,first:h,last:f,next:t.bind(null,g,!1),previous:t.bind(null,g,!0),hasPrevious:d,hasNext:a}}t.module("material.core").config(["$provide",function(t){t.decorator("$mdUtil",["$delegate",function(t){return t.iterator=e,t}])}])}(),function(){function e(e,n,o){function i(e){var n=u[e];t.isUndefined(n)&&(n=u[e]=r(e));var o=h[n];return t.isUndefined(o)&&(o=a(n)),o}function r(t){return e.MEDIA[t]||("("!==t.charAt(0)?"("+t+")":t)}function a(e){var t=p[e];return t||(t=p[e]=o.matchMedia(e)),t.addListener(d),h[t.media]=!!t.matches}function d(e){n.$evalAsync(function(){h[e.media]=!!e.matches})}function s(e){return p[e]}function c(t,n){for(var o=0;o-1}function g(e){return String(e).indexOf("%")>-1}function b(e){return e[0]||e}var v=c.startSymbol(),E=c.endSymbol(),$="{{"===v&&"}}"===E,C=function(e,n,o){var i=!1;if(e&&e.length){var r=u.getComputedStyle(e[0]);i=t.isDefined(r[n])&&(!o||r[n]==o)}return i},y={dom:{},now:e.performance&&e.performance.now?t.bind(e.performance,e.performance.now):Date.now||function(){return(new Date).getTime()},getModelOption:function(e,t){if(e.$options){var n=e.$options;return n.getOption?n.getOption(t):n[t]}},bidi:function(e,n,i,r){var a=!("rtl"==o[0].dir||"rtl"==o[0].body.dir);if(0==arguments.length)return a?"ltr":"rtl";var d=t.element(e);a&&t.isDefined(i)?d.css(n,h(i)):!a&&t.isDefined(r)&&d.css(n,h(r))},bidiProperty:function(e,n,i,r){var a=!("rtl"==o[0].dir||"rtl"==o[0].body.dir),d=t.element(e);a&&t.isDefined(n)?(d.css(n,h(r)),d.css(i,"")):!a&&t.isDefined(i)&&(d.css(i,h(r)),d.css(n,""))},clientRect:function(e,t,n){var o=b(e);t=b(t||o.offsetParent||document.body);var i=o.getBoundingClientRect(),r=n?t.getBoundingClientRect():{left:0,top:0,width:0,height:0};return{left:i.left-r.left,top:i.top-r.top,width:i.width,height:i.height}},offsetRect:function(e,t){return y.clientRect(e,t,!0)},nodesToArray:function(e){e=e||[];for(var t=[],n=0;n
'),e.append(o)),o.on("wheel",n),o.on("touchmove",n),function(){o.off("wheel"),o.off("touchmove"),i.disableScrollMask||o[0].parentNode.removeChild(o[0])}}function a(){var e=o[0].documentElement,n=e.style.cssText||"",i=d.style.cssText||"",r=y.getViewportTop(),a=d.clientWidth,s=d.scrollHeight>d.clientHeight+1;return s&&t.element(d).css({position:"fixed",width:"100%",top:-r+"px"}),d.clientWidth
").css({width:"100%","z-index":-1,position:"absolute",height:"35px","overflow-y":"scroll"});e.children().css("height","60px"),o[0].body.appendChild(e[0]),this.floatingScrollbars.cached=e[0].offsetWidth==e[0].childNodes[0].offsetWidth,e.remove()}return this.floatingScrollbars.cached},forceFocus:function(t){var n=t[0]||t;document.addEventListener("click",function i(e){e.target===n&&e.$focus&&(n.focus(),e.stopImmediatePropagation(),e.preventDefault(),n.removeEventListener("click",i))},!0);var o=document.createEvent("MouseEvents");o.initMouseEvent("click",!1,!0,e,{},0,0,0,0,!1,!1,!1,!1,0,null),o.$material=!0,o.$focus=!0,n.dispatchEvent(o)},createBackdrop:function(e,t){return a(y.supplant('',[t]))(e)},supplant:function(e,t,n){return n=n||/\{([^\{\}]*)\}/g,e.replace(n,function(e,n){var o=n.split("."),i=t;try{for(var r in o)o.hasOwnProperty(r)&&(i=i[o[r]])}catch(a){i=e}return"string"==typeof i||"number"==typeof i?i:e})},fakeNgModel:function(){return{$fake:!0,$setTouched:t.noop,$setViewValue:function(e){this.$viewValue=e,this.$render(e),this.$viewChangeListeners.forEach(function(e){e()})},$isEmpty:function(e){return 0===(""+e).length},$parsers:[],$formatters:[],$viewChangeListeners:[],$render:t.noop}},debounce:function(e,t,o,i){var a;return function(){var d=o,s=Array.prototype.slice.call(arguments);r.cancel(a),a=r(function(){a=n,e.apply(d,s)},t||10,i)}},throttle:function(e,t){var n;return function(){var o=this,i=arguments,r=y.now();(!n||r-n>t)&&(e.apply(o,i),n=r)}},time:function(e){var t=y.now();return e(),y.now()-t},valueOnUse:function(e,t,n){var o=null,i=Array.prototype.slice.call(arguments),r=i.length>3?i.slice(3):[];Object.defineProperty(e,t,{get:function(){return null===o&&(o=n.apply(e,r)),o}})},nextUid:function(){return""+i++},disconnectScope:function(e){if(e&&e.$root!==e&&!e.$$destroyed){var t=e.$parent;e.$$disconnected=!0,t.$$childHead===e&&(t.$$childHead=e.$$nextSibling),t.$$childTail===e&&(t.$$childTail=e.$$prevSibling),e.$$prevSibling&&(e.$$prevSibling.$$nextSibling=e.$$nextSibling),e.$$nextSibling&&(e.$$nextSibling.$$prevSibling=e.$$prevSibling),e.$$nextSibling=e.$$prevSibling=null}},reconnectScope:function(e){if(e&&e.$root!==e&&e.$$disconnected){var t=e,n=t.$parent;t.$$disconnected=!1,t.$$prevSibling=n.$$childTail,n.$$childHead?(n.$$childTail.$$nextSibling=t,n.$$childTail=t):n.$$childHead=n.$$childTail=t}},getClosest:function(e,n,o){if(t.isString(n)){var i=n.toUpperCase();n=function(e){return e.nodeName.toUpperCase()===i}}if(e instanceof t.element&&(e=e[0]),o&&(e=e.parentNode),!e)return null;do if(n(e))return e;while(e=e.parentNode);return null},elementContains:function(n,o){var i=e.Node&&e.Node.prototype&&Node.prototype.contains,r=i?t.bind(n,n.contains):t.bind(n,function(e){return n===o||!!(16&this.compareDocumentPosition(e))});return r(o)},extractElementByName:function(e,n,o,i){function r(e){return a(e)||(o?d(e):null)}function a(e){if(e)for(var t=0,o=e.length;t");o[0].body.appendChild(n[0]);for(var i=["sticky","-webkit-sticky"],r=0;rt)&&p(o)}function i(){var e=n||1e3,t=y.now()-c;return r(t,a,d,e)}function r(e,t,n,o){if(e>o)return t+n;var i=(e/=o)*e,r=i*e;return t+n*(-2*r+3*i)}var a=e.scrollTop,d=t-a,s=a").html(i.trim()).contents();return n._compileElement(o,r,e)})},e.prototype._compileElement=function(e,n,o){function i(i){if(e.$scope=i,o.controller){var s=t.extend(e,{$element:n}),c=r.$controller(o.controller,s,!0,o.controllerAs);o.bindToController&&t.extend(c.instance,e);var l=c();n.data("$ngControllerController",l),n.children().data("$ngControllerController",l),d.controller=l}return a(i)}var r=this,a=this.$compile(n),d={element:n,cleanup:n.remove.bind(n),locals:e,link:i};return d},e.prototype._fetchContentElement=function(e){function n(e){var t=e.parentNode,n=e.nextElementSibling;return function(){n?t.insertBefore(e,n):t.appendChild(e)}}var o=e.contentElement,i=null;return t.isString(o)?(o=document.querySelector(o),i=n(o)):(o=o[0]||o,i=document.contains(o)?n(o):function(){o.parentNode&&o.parentNode.removeChild(o)}),{element:t.element(o),restore:i}}}(),function(){function e(){function e(){t.showWarnings=!1}var t={showWarnings:!0};return{disableWarnings:e,$get:["$$rAF","$log","$window","$interpolate",function(e,o,i,r){return n.apply(t,arguments)}]}}function n(e,n,o,i){function r(e,o,i){var r=t.element(e)[0]||e;!r||r.hasAttribute(o)&&0!==r.getAttribute(o).length||l(r,o)||(i=t.isString(i)?i.trim():"",i.length?e.attr(o,i):p&&n.warn('ARIA: Attribute "',o,'", required for accessibility, is missing on node:',r))}function a(t,n,o){e(function(){r(t,n,o())})}function d(e,t){var n=c(e)||"",o=n.indexOf(i.startSymbol())>-1;o?a(e,t,function(){return c(e)}):r(e,t,n)}function s(e,t){var n=c(e),o=n.indexOf(i.startSymbol())>-1;o||n||r(e,t,n)}function c(e){function t(t){for(;t.parentNode&&(t=t.parentNode)!==e;)if(t.getAttribute&&"true"===t.getAttribute("aria-hidden"))return!0}e=e[0]||e;for(var n,o=document.createTreeWalker(e,NodeFilter.SHOW_TEXT,null,!1),i="";n=o.nextNode();)t(n)||(i+=n.textContent);return i.trim()||""}function l(e,t){function n(e){var t=e.currentStyle?e.currentStyle:o.getComputedStyle(e);return"none"===t.display}var i=e.hasChildNodes(),r=!1;if(i)for(var a=e.childNodes,d=0;d=this.$mdUtil.now()-n}}(),function(){function n(){}function o(n,o,i){function r(e){return function(t,n){n.distancethis.options.maxDistance&&this.cancel()},onEnd:function(){this.onCancel()}}).handler("drag",{options:{minDistance:6,horizontal:!0,cancelMultiplier:1.5},onSetup:function(e,t){g&&(this.oldTouchAction=e[0].style[g],e[0].style[g]=t.horizontal?"pan-y":"pan-x")},onCleanup:function(e){this.oldTouchAction&&(e[0].style[g]=this.oldTouchAction)},onStart:function(e){this.state.registeredParent||this.cancel()},onMove:function(e,t){var n,o;g||"touchmove"!==e.type||e.preventDefault(),this.state.dragPointer?this.dispatchDragMove(e):(this.state.options.horizontal?(n=Math.abs(t.distanceX)>this.state.options.minDistance,o=Math.abs(t.distanceY)>this.state.options.minDistance*this.state.options.cancelMultiplier):(n=Math.abs(t.distanceY)>this.state.options.minDistance,o=Math.abs(t.distanceX)>this.state.options.minDistance*this.state.options.cancelMultiplier),n?(this.state.dragPointer=d(e),l(e,this.state.dragPointer),this.dispatchEvent(e,"$md.dragstart",this.state.dragPointer)):o&&this.cancel())},dispatchDragMove:o.throttle(function(e){this.state.isRunning&&(l(e,this.state.dragPointer),this.dispatchEvent(e,"$md.drag",this.state.dragPointer))}),onEnd:function(e,t){this.state.dragPointer&&(l(e,this.state.dragPointer),this.dispatchEvent(e,"$md.dragend",this.state.dragPointer))}}).handler("swipe",{options:{minVelocity:.65,minDistance:10},onEnd:function(e,t){var n;Math.abs(t.velocityX)>this.state.options.minVelocity&&Math.abs(t.distanceX)>this.state.options.minDistance?(n="left"==t.directionX?"$md.swipeleft":"$md.swiperight",this.dispatchEvent(e,n)):Math.abs(t.velocityY)>this.state.options.minVelocity&&Math.abs(t.distanceY)>this.state.options.minDistance&&(n="up"==t.directionY?"$md.swipeup":"$md.swipedown",this.dispatchEvent(e,n))}})}function i(e){this.name=e,this.state={}}function r(){function n(e,n,o){o=o||u;var i=new t.element.Event(n);i.$material=!0,i.pointer=o,i.srcEvent=e,t.extend(i,{clientX:o.x,clientY:o.y,screenX:o.x,screenY:o.y,pageX:o.x,pageY:o.y,ctrlKey:e.ctrlKey,altKey:e.altKey,shiftKey:e.shiftKey,metaKey:e.metaKey}),t.element(o.target).trigger(i)}function o(t,n,o){o=o||u;var i;"click"===n||"mouseup"==n||"mousedown"==n?(i=document.createEvent("MouseEvents"),i.initMouseEvent(n,!0,!0,e,t.detail,o.x,o.y,o.x,o.y,t.ctrlKey,t.altKey,t.shiftKey,t.metaKey,t.button,t.relatedTarget||null)):(i=document.createEvent("CustomEvent"),i.initCustomEvent(n,!0,!0,{})),i.$material=!0,i.pointer=o,i.srcEvent=t,o.target.dispatchEvent(i)}var r="undefined"!=typeof e.jQuery&&t.element===e.jQuery;return i.prototype={options:{},dispatchEvent:r?n:o,onSetup:t.noop,onCleanup:t.noop,onStart:t.noop,onMove:t.noop,onEnd:t.noop,onCancel:t.noop,start:function(e,n){if(!this.state.isRunning){var o=this.getNearestParent(e.target),i=o&&o.$mdGesture[this.name]||{};this.state={isRunning:!0,options:t.extend({},this.options,i),registeredParent:o},this.onStart(e,n)}},move:function(e,t){this.state.isRunning&&this.onMove(e,t)},end:function(e,t){this.state.isRunning&&(this.onEnd(e,t),this.state.isRunning=!1)},cancel:function(e,t){this.onCancel(e,t),this.state={}},getNearestParent:function(e){for(var t=e;t;){if((t.$mdGesture||{})[this.name])return t;t=t.parentNode}return null},registerElement:function(e,t){function n(){delete e[0].$mdGesture[o.name],e.off("$destroy",n),o.onCleanup(e,t||{})}var o=this;return e[0].$mdGesture=e[0].$mdGesture||{},e[0].$mdGesture[this.name]=t||{},e.on("$destroy",n),o.onSetup(e,t||{}),n}},i}function a(e,n){function o(e){var t=!e.clientX&&!e.clientY;t||e.$material||e.isIonicTap||c(e)||(e.preventDefault(),e.stopPropagation())}function i(e){var t=0===e.clientX&&0===e.clientY,n=e.target&&"submit"===e.target.type;t||e.$material||e.isIonicTap||c(e)||n?(g=null,"label"==e.target.tagName.toLowerCase()&&(g={x:e.x,y:e.y})):(e.preventDefault(),e.stopPropagation(),g=null)}function r(e,t){var o;for(var i in h)o=h[i],o instanceof n&&("start"===e&&o.cancel(),o[e](t,u))}function a(e){if(!u){var t=+Date.now();p&&!s(e,p)&&t-p.endTime<1500||(u=d(e),r("start",e))}}function m(e){u&&s(e,u)&&(l(e,u),r("move",e))}function f(e){u&&s(e,u)&&(l(e,u),u.endTime=+Date.now(),r("end",e),p=u,u=null)}document.contains||(document.contains=function(e){return document.body.contains(e)}),!b&&e.isHijackingClicks&&(document.addEventListener("click",i,!0),document.addEventListener("mouseup",o,!0),document.addEventListener("mousedown",o,!0),document.addEventListener("focus",o,!0),b=!0);var v="mousedown touchstart pointerdown",E="mousemove touchmove pointermove",$="mouseup mouseleave touchend touchcancel pointerup pointercancel";t.element(document).on(v,a).on(E,m).on($,f).on("$$mdGestureReset",function(){p=u=null})}function d(e){var t=m(e),n={startTime:+Date.now(),target:e.target,type:e.type.charAt(0)};return n.startX=n.x=t.pageX,n.startY=n.y=t.pageY,n}function s(e,t){return e&&t&&e.type.charAt(0)===t.type}function c(e){return g&&g.x==e.x&&g.y==e.y}function l(e,t){var n=m(e),o=t.x=n.pageX,i=t.y=n.pageY;t.distanceX=o-t.startX,t.distanceY=i-t.startY,t.distance=Math.sqrt(t.distanceX*t.distanceX+t.distanceY*t.distanceY),t.directionX=t.distanceX>0?"right":t.distanceX<0?"left":"",t.directionY=t.distanceY>0?"down":t.distanceY<0?"up":"",t.duration=+Date.now()-t.startTime,t.velocityX=t.distanceX/t.duration,t.velocityY=t.distanceY/t.duration}function m(e){return e=e.originalEvent||e,e.touches&&e.touches[0]||e.changedTouches&&e.changedTouches[0]||e}o.$inject=["$$MdGestureHandler","$$rAF","$timeout"],a.$inject=["$mdGesture","$$MdGestureHandler"];var u,p,h={},f=!1,g=null,b=!1;t.module("material.core.gestures",[]).provider("$mdGesture",n).factory("$$MdGestureHandler",r).run(a),n.prototype={skipClickHijack:function(){return f=!0},$get:["$$MdGestureHandler","$$rAF","$timeout",function(e,t,n){return new o(e,t,n)}]}}(),function(){function e(){function e(e){function n(e){return s.optionsFactory=e.options,s.methods=(e.methods||[]).concat(a),c}function o(e,t){return d[e]=t,c}function i(t,n){if(n=n||{},n.methods=n.methods||[],n.options=n.options||function(){return{}},/^cancel|hide|show$/.test(t))throw new Error("Preset '"+t+"' in "+e+" is reserved!");if(n.methods.indexOf("_options")>-1)throw new Error("Method '_options' in "+e+" is reserved!");return s.presets[t]={methods:n.methods.concat(a), -optionsFactory:n.options,argOption:n.argOption},c}function r(n,o){function i(e){return e=e||{},e._options&&(e=e._options),m.show(t.extend({},l,e))}function r(e){return m.destroy(e)}function a(t,n){var i={};return i[e]=u,o.invoke(t||function(){return n},{},i)}var c,l,m=n(),u={hide:m.hide,cancel:m.cancel,show:i,destroy:r};return c=s.methods||[],l=a(s.optionsFactory,{}),t.forEach(d,function(e,t){u[t]=e}),t.forEach(s.presets,function(e,n){function o(e){this._options=t.extend({},i,e)}var i=a(e.optionsFactory,{}),r=(e.methods||[]).concat(c);if(t.extend(i,{$type:n}),t.forEach(r,function(e){o.prototype[e]=function(t){return this._options[e]=t,this}}),e.argOption){var d="show"+n.charAt(0).toUpperCase()+n.slice(1);u[d]=function(e){var t=u[n](e);return u.show(t)}}u[n]=function(n){return arguments.length&&e.argOption&&!t.isObject(n)&&!t.isArray(n)?(new o)[e.argOption](n):new o(n)}}),u}r.$inject=["$$interimElement","$injector"];var a=["onHide","onShow","onRemove"],d={},s={presets:{}},c={setDefaults:n,addPreset:i,addMethod:o,$get:r};return c.addPreset("build",{methods:["controller","controllerAs","resolve","multiple","template","templateUrl","themable","transformTemplate","parent","contentElement"]}),c}function o(e,o,i,r,a,d,s,c,l,m,u){return function(){function p(e){e=e||{};var t=new v(e||{}),n=e.multiple?o.resolve():o.all(C);e.multiple||(n=n.then(function(){var e=y.concat(M.map(E.cancel));return o.all(e)}));var i=n.then(function(){return t.show()["catch"](function(e){return e})["finally"](function(){C.splice(C.indexOf(i),1),M.push(t)})});return C.push(i),t.deferred.promise["catch"](function(e){return e instanceof Error&&u(e),e}),t.deferred.promise}function h(e,t){function i(n){var o=n.remove(e,!1,t||{})["catch"](function(e){return e})["finally"](function(){y.splice(y.indexOf(o),1)});return M.splice(M.indexOf(n),1),y.push(o),n.deferred.promise}return t=t||{},t.closeAll?o.all(M.slice().reverse().map(i)):t.closeTo!==n?o.all(M.slice(t.closeTo).map(i)):i(M[M.length-1])}function f(e,n){var i=M.pop();if(!i)return o.when(e);var r=i.remove(e,!0,n||{})["catch"](function(e){return e})["finally"](function(){y.splice(y.indexOf(r),1)});return y.push(r),i.deferred.promise["catch"](t.noop)}function g(e){return function(){var t=arguments;return M.length?e.apply(E,t):C.length?C[0]["finally"](function(){return e.apply(E,t)}):o.when("No interim elements currently showing up.")}}function b(e){var n=e?null:M.shift(),i=t.element(e).length&&t.element(e)[0].parentNode;if(i){var r=M.filter(function(e){return e.options.element[0]===i});r.length&&(n=r[0],M.splice(M.indexOf(n),1))}return n?n.remove($,!1,{$destroy:!0}):o.when($)}function v(m){function u(){return o(function(e,t){function n(e){y.deferred.reject(e),t(e)}m.onCompiling&&m.onCompiling(m),f(m).then(function(t){M=g(t,m),m.cleanupElement=t.cleanup,T=$(M,m,t.controller).then(e,n)})["catch"](n)})}function p(e,n,i){function r(e){y.deferred.resolve(e)}function a(e){y.deferred.reject(e)}return M?(m=t.extend(m||{},i||{}),m.cancelAutoHide&&m.cancelAutoHide(),m.element.triggerHandler("$mdInterimElementRemove"),m.$destroy===!0?C(m.element,m).then(function(){n&&a(e)||r(e)}):(o.when(T)["finally"](function(){C(m.element,m).then(function(){n?a(e):r(e)},a)}),y.deferred.promise)):o.when(!1)}function h(e){return e=e||{},e.template&&(e.template=s.processTemplate(e.template)),t.extend({preserveScope:!1,cancelAutoHide:t.noop,scope:e.scope||i.$new(e.isolateScope),onShow:function(e,t,n){return d.enter(t,n.parent)},onRemove:function(e,t){return t&&d.leave(t)||o.when()}},e)}function f(e){var t=e.skipCompile?null:c.compile(e);return t||o(function(t){t({locals:{},link:function(){return e.element}})})}function g(e,n){t.extend(e.locals,n);var o=e.link(n.scope);return n.element=o,n.parent=b(o,n),n.themable&&l(o),o}function b(n,o){var i=o.parent;if(i=t.isFunction(i)?i(o.scope,n,o):t.isString(i)?t.element(e[0].querySelector(i)):t.element(i),!(i||{}).length){var r;return a[0]&&a[0].querySelector&&(r=a[0].querySelector(":not(svg) > body")),r||(r=a[0]),"#comment"==r.nodeName&&(r=e[0].body),t.element(r)}return i}function v(){var e,o=t.noop;m.hideDelay&&(e=r(E.hide,m.hideDelay),o=function(){r.cancel(e)}),m.cancelAutoHide=function(){o(),m.cancelAutoHide=n}}function $(e,n,i){var r=n.onShowing||t.noop,a=n.onComplete||t.noop;try{r(n.scope,e,n,i)}catch(d){return o.reject(d)}return o(function(t,r){try{o.when(n.onShow(n.scope,e,n,i)).then(function(){a(n.scope,e,n),v(),t(e)},r)}catch(d){r(d.message)}})}function C(e,n){var i=n.onRemoving||t.noop;return o(function(t,r){try{var a=o.when(n.onRemove(n.scope,e,n)||!0);i(e,a),n.$destroy?(t(e),!n.preserveScope&&n.scope&&a.then(function(){n.scope.$destroy()})):a.then(function(){!n.preserveScope&&n.scope&&n.scope.$destroy(),t(e)},r)}catch(d){r(d.message)}})}var y,M,T=o.when(!0);return m=h(m),y={options:m,deferred:o.defer(),show:u,remove:p}}var E,$=!1,C=[],y=[],M=[];return E={show:p,hide:g(h),cancel:g(f),destroy:b,$injector_:m}}}return o.$inject=["$document","$q","$rootScope","$timeout","$rootElement","$animate","$mdUtil","$mdCompiler","$mdTheming","$injector","$exceptionHandler"],e.$get=o,e}t.module("material.core").provider("$$interimElement",e)}(),function(){!function(){function e(e){function d(e){return e.replace(m,"").replace(u,function(e,t,n,o){return o?n.toUpperCase():n})}var m=/^((?:x|data)[\:\-_])/i,u=/([\:\-\_]+(.))/g,p=["","xs","gt-xs","sm","gt-sm","md","gt-md","lg","gt-lg","xl","print"],h=["layout","flex","flex-order","flex-offset","layout-align"],f=["show","hide","layout-padding","layout-margin"];t.forEach(p,function(n){t.forEach(h,function(t){var o=n?t+"-"+n:t;e.directive(d(o),r(o))}),t.forEach(f,function(t){var o=n?t+"-"+n:t;e.directive(d(o),a(o))})}),e.provider("$$mdLayout",function(){return{$get:t.noop,validateAttributeValue:l,validateAttributeUsage:c,disableLayouts:function(e){A.enabled=e!==!0}}}).directive("mdLayoutCss",o).directive("ngCloak",i("ng-cloak")).directive("layoutWrap",a("layout-wrap")).directive("layoutNowrap",a("layout-nowrap")).directive("layoutNoWrap",a("layout-no-wrap")).directive("layoutFill",a("layout-fill")).directive("layoutLtMd",s("layout-lt-md",!0)).directive("layoutLtLg",s("layout-lt-lg",!0)).directive("flexLtMd",s("flex-lt-md",!0)).directive("flexLtLg",s("flex-lt-lg",!0)).directive("layoutAlignLtMd",s("layout-align-lt-md")).directive("layoutAlignLtLg",s("layout-align-lt-lg")).directive("flexOrderLtMd",s("flex-order-lt-md")).directive("flexOrderLtLg",s("flex-order-lt-lg")).directive("offsetLtMd",s("flex-offset-lt-md")).directive("offsetLtLg",s("flex-offset-lt-lg")).directive("hideLtMd",s("hide-lt-md")).directive("hideLtLg",s("hide-lt-lg")).directive("showLtMd",s("show-lt-md")).directive("showLtLg",s("show-lt-lg")).config(n)}function n(){var e=!!document.querySelector("[md-layouts-disabled]");A.enabled=!e}function o(){return A.enabled=!1,{restrict:"A",priority:"900"}}function i(e){return["$timeout",function(n){return{restrict:"A",priority:-10,compile:function(o){return A.enabled?(o.addClass(e),function(t,o){n(function(){o.removeClass(e)},10,!1)}):t.noop}}}]}function r(e){function n(t,n,o){var i=d(n,e,o),r=o.$observe(o.$normalize(e),i);i(p(e,o,"")),t.$on("$destroy",function(){r()})}return["$mdUtil","$interpolate","$log",function(o,i,r){return g=o,b=i,v=r,{restrict:"A",compile:function(o,i){var r;return A.enabled&&(c(e,i,o,v),l(e,p(e,i,""),m(o,e,i)),r=n),r||t.noop}}}]}function a(e){function n(t,n){n.addClass(e)}return["$mdUtil","$interpolate","$log",function(o,i,r){return g=o,b=i,v=r,{restrict:"A",compile:function(o,i){var r;return A.enabled&&(l(e,p(e,i,""),m(o,e,i)),n(null,o),r=n),r||t.noop}}}]}function d(e,n){var o;return function(i){var r=l(n,i||"");t.isDefined(r)&&(o&&e.removeClass(o),o=r?n+"-"+r.trim().replace($,"-"):n,e.addClass(o))}}function s(e){var n=e.split("-");return["$log",function(o){return o.warn(e+"has been deprecated. Please use a `"+n[0]+"-gt-` variant."),t.noop}]}function c(e,t,n,o){var i,r,a,d=n[0].nodeName.toLowerCase();switch(e.replace(E,"")){case"flex":"md-button"!=d&&"fieldset"!=d||(r="<"+d+" "+e+">",a="https://github.com/philipwalton/flexbugs#9-some-html-elements-cant-be-flex-containers",i="Markup '{0}' may not work as expected in IE Browsers. Consult '{1}' for details.",o.warn(g.supplant(i,[r,a])))}}function l(e,n,o){var i;if(!u(n)){switch(e.replace(E,"")){case"layout":h(n,y)||(n=y[0]);break;case"flex":h(n,C)||isNaN(n)&&(n="");break;case"flex-offset":case"flex-order":n&&!isNaN(+n)||(n="0");break;case"layout-align":var r=f(n);n=g.supplant("{main}-{cross}",r);break;case"layout-padding":case"layout-margin":case"layout-fill":case"layout-wrap":case"layout-nowrap":case"layout-nowrap":n=""}n!=i&&(o||t.noop)(n)}return n?n.trim():""}function m(e,t,n){return function(e){u(e)||(n[n.$normalize(t)]=e)}}function u(e){return(e||"").indexOf(b.startSymbol())>-1}function p(e,t,n){var o=t.$normalize(e);return t[o]?t[o].trim().replace($,"-"):n||null}function h(e,t,n){e=n&&e?e.replace($,n):e;var o=!1;return e&&t.forEach(function(t){t=n?t.replace($,n):t,o=o||t===e}),o}function f(e){var t,n={main:"start",cross:"stretch"};return e=e||"",0!==e.indexOf("-")&&0!==e.indexOf(" ")||(e="none"+e),t=e.toLowerCase().trim().replace($,"-").split("-"),t.length&&"space"===t[0]&&(t=[t[0]+"-"+t[1],t[2]]),t.length>0&&(n.main=t[0]||n.main),t.length>1&&(n.cross=t[1]||n.cross),M.indexOf(n.main)<0&&(n.main="start"),T.indexOf(n.cross)<0&&(n.cross="stretch"),n}var g,b,v,E=/(-gt)?-(sm|md|lg|print)/g,$=/\s+/g,C=["grow","initial","auto","none","noshrink","nogrow"],y=["row","column"],M=["","start","center","end","stretch","space-around","space-between"],T=["","start","center","end","stretch"],A={enabled:!0,breakpoints:[]};e(t.module("material.core.layout",["ng"]))}()}(),function(){function e(e){this._$timeout=e,this._liveElement=this._createLiveElement(),this._announceTimeout=100}e.$inject=["$timeout"],t.module("material.core").service("$mdLiveAnnouncer",e),e.prototype.announce=function(e,t){t||(t="polite");var n=this;n._liveElement.textContent="",n._liveElement.setAttribute("aria-live",t),n._$timeout(function(){n._liveElement.textContent=e},n._announceTimeout,!1)},e.prototype._createLiveElement=function(){var e=document.createElement("div");return e.classList.add("md-visually-hidden"),e.setAttribute("role","status"),e.setAttribute("aria-atomic","true"),e.setAttribute("aria-live","polite"),document.body.appendChild(e),e}}(),function(){t.module("material.core.meta",[]).provider("$$mdMeta",function(){function e(e){if(r[e])return!0;var n=document.getElementsByName(e)[0];return!!n&&(r[e]=t.element(n),!0)}function n(n,o){if(e(n),r[n])r[n].attr("content",o);else{var a=t.element('');i.append(a),r[n]=a}return function(){r[n].attr("content",""),r[n].remove(),delete r[n]}}function o(t){if(!e(t))throw Error("$$mdMeta: could not find a meta tag with the name '"+t+"'");return r[t].attr("content")}var i=t.element(document.head),r={},a={setMeta:n,getMeta:o};return t.extend({},a,{$get:function(){return a}})})}(),function(){function e(e,o){function i(e){return e&&""!==e}var r,a=[],d={};return r={notFoundError:function(t,n){e.error((n||"")+"No instance found for handle",t)},getInstances:function(){return a},get:function(e){if(!i(e))return null;var t,n,o;for(t=0,n=a.length;t');return this.$element.append(e),e},o.prototype.clearTimeout=function(){this.timeout&&(this.$timeout.cancel(this.timeout),this.timeout=null)},o.prototype.isRippleAllowed=function(){var e=this.$element[0];do{if(!e.tagName||"BODY"===e.tagName)break;if(e&&t.isFunction(e.hasAttribute)){if(e.hasAttribute("disabled"))return!1;if("false"===this.inkRipple()||"0"===this.inkRipple())return!1}}while(e=e.parentNode);return!0},o.prototype.inkRipple=function(){return this.$element.attr("md-ink-ripple")},o.prototype.createRipple=function(e,n){function o(e,t,n){return e?Math.max(t,n):Math.sqrt(Math.pow(t,2)+Math.pow(n,2))}if(this.isRippleAllowed()){var i=this,r=i.$mdColorUtil,d=t.element('
'),s=this.$element.prop("clientWidth"),c=this.$element.prop("clientHeight"),l=2*Math.max(Math.abs(s-e),e),m=2*Math.max(Math.abs(c-n),n),u=o(this.options.fitRipple,l,m),p=this.calculateColor();d.css({left:e+"px",top:n+"px",background:"black",width:u+"px",height:u+"px",backgroundColor:r.rgbaToRgb(p),borderColor:r.rgbaToRgb(p)}),this.lastRipple=d,this.clearTimeout(),this.timeout=this.$timeout(function(){i.clearTimeout(),i.mousedown||i.fadeInComplete(d)},.35*a,!1),this.options.dimBackground&&this.container.css({backgroundColor:p}),this.container.append(d),this.ripples.push(d),d.addClass("md-ripple-placed"),this.$mdUtil.nextTick(function(){d.addClass("md-ripple-scaled md-ripple-active"),i.$timeout(function(){i.clearRipples()},a,!1)},!1)}},o.prototype.fadeInComplete=function(e){this.lastRipple===e?this.timeout||this.mousedown||this.removeRipple(e):this.removeRipple(e)},o.prototype.removeRipple=function(e){var t=this,n=this.ripples.indexOf(e);n<0||(this.ripples.splice(this.ripples.indexOf(e),1),e.removeClass("md-ripple-active"),e.addClass("md-ripple-remove"),0===this.ripples.length&&this.container.css({backgroundColor:""}),this.$timeout(function(){t.fadeOutComplete(e)},a,!1))},o.prototype.fadeOutComplete=function(e){e.remove(),this.lastRipple=null}}(),function(){!function(){function e(e){function n(n,o,i){return e.attach(n,o,t.extend({center:!1,dimBackground:!0,outline:!1,rippleSize:"full"},i))}return{attach:n}}e.$inject=["$mdInkRipple"],t.module("material.core").factory("$mdTabInkRipple",e)}()}(),function(){t.module("material.core.theming.palette",[]).constant("$mdColorPalette",{red:{50:"#ffebee",100:"#ffcdd2",200:"#ef9a9a",300:"#e57373",400:"#ef5350",500:"#f44336",600:"#e53935",700:"#d32f2f",800:"#c62828",900:"#b71c1c",A100:"#ff8a80",A200:"#ff5252",A400:"#ff1744",A700:"#d50000",contrastDefaultColor:"light",contrastDarkColors:"50 100 200 300 A100",contrastStrongLightColors:"400 500 600 700 A200 A400 A700"},pink:{50:"#fce4ec",100:"#f8bbd0",200:"#f48fb1",300:"#f06292",400:"#ec407a",500:"#e91e63",600:"#d81b60",700:"#c2185b",800:"#ad1457",900:"#880e4f",A100:"#ff80ab",A200:"#ff4081",A400:"#f50057",A700:"#c51162",contrastDefaultColor:"light",contrastDarkColors:"50 100 200 A100",contrastStrongLightColors:"500 600 A200 A400 A700"},purple:{50:"#f3e5f5",100:"#e1bee7",200:"#ce93d8",300:"#ba68c8",400:"#ab47bc",500:"#9c27b0",600:"#8e24aa",700:"#7b1fa2",800:"#6a1b9a",900:"#4a148c",A100:"#ea80fc",A200:"#e040fb",A400:"#d500f9",A700:"#aa00ff",contrastDefaultColor:"light",contrastDarkColors:"50 100 200 A100",contrastStrongLightColors:"300 400 A200 A400 A700"},"deep-purple":{50:"#ede7f6",100:"#d1c4e9",200:"#b39ddb",300:"#9575cd",400:"#7e57c2",500:"#673ab7",600:"#5e35b1",700:"#512da8",800:"#4527a0",900:"#311b92",A100:"#b388ff",A200:"#7c4dff",A400:"#651fff",A700:"#6200ea",contrastDefaultColor:"light",contrastDarkColors:"50 100 200 A100",contrastStrongLightColors:"300 400 A200"},indigo:{50:"#e8eaf6",100:"#c5cae9",200:"#9fa8da",300:"#7986cb",400:"#5c6bc0",500:"#3f51b5",600:"#3949ab",700:"#303f9f",800:"#283593",900:"#1a237e",A100:"#8c9eff",A200:"#536dfe",A400:"#3d5afe",A700:"#304ffe",contrastDefaultColor:"light",contrastDarkColors:"50 100 200 A100",contrastStrongLightColors:"300 400 A200 A400"},blue:{50:"#e3f2fd",100:"#bbdefb",200:"#90caf9",300:"#64b5f6",400:"#42a5f5",500:"#2196f3",600:"#1e88e5",700:"#1976d2",800:"#1565c0",900:"#0d47a1",A100:"#82b1ff",A200:"#448aff",A400:"#2979ff",A700:"#2962ff",contrastDefaultColor:"light",contrastDarkColors:"50 100 200 300 400 A100",contrastStrongLightColors:"500 600 700 A200 A400 A700"},"light-blue":{50:"#e1f5fe",100:"#b3e5fc",200:"#81d4fa",300:"#4fc3f7",400:"#29b6f6",500:"#03a9f4",600:"#039be5",700:"#0288d1",800:"#0277bd",900:"#01579b",A100:"#80d8ff",A200:"#40c4ff",A400:"#00b0ff",A700:"#0091ea",contrastDefaultColor:"dark",contrastLightColors:"600 700 800 900 A700",contrastStrongLightColors:"600 700 800 A700"},cyan:{50:"#e0f7fa",100:"#b2ebf2",200:"#80deea",300:"#4dd0e1",400:"#26c6da",500:"#00bcd4",600:"#00acc1",700:"#0097a7",800:"#00838f",900:"#006064",A100:"#84ffff",A200:"#18ffff",A400:"#00e5ff",A700:"#00b8d4",contrastDefaultColor:"dark",contrastLightColors:"700 800 900",contrastStrongLightColors:"700 800 900"},teal:{50:"#e0f2f1",100:"#b2dfdb",200:"#80cbc4",300:"#4db6ac",400:"#26a69a",500:"#009688",600:"#00897b",700:"#00796b",800:"#00695c",900:"#004d40",A100:"#a7ffeb",A200:"#64ffda",A400:"#1de9b6",A700:"#00bfa5",contrastDefaultColor:"dark",contrastLightColors:"500 600 700 800 900",contrastStrongLightColors:"500 600 700"},green:{50:"#e8f5e9",100:"#c8e6c9",200:"#a5d6a7",300:"#81c784",400:"#66bb6a",500:"#4caf50",600:"#43a047",700:"#388e3c",800:"#2e7d32",900:"#1b5e20",A100:"#b9f6ca",A200:"#69f0ae",A400:"#00e676",A700:"#00c853",contrastDefaultColor:"dark",contrastLightColors:"500 600 700 800 900",contrastStrongLightColors:"500 600 700"},"light-green":{50:"#f1f8e9",100:"#dcedc8",200:"#c5e1a5",300:"#aed581",400:"#9ccc65",500:"#8bc34a",600:"#7cb342",700:"#689f38",800:"#558b2f",900:"#33691e",A100:"#ccff90",A200:"#b2ff59",A400:"#76ff03",A700:"#64dd17",contrastDefaultColor:"dark",contrastLightColors:"700 800 900",contrastStrongLightColors:"700 800 900"},lime:{50:"#f9fbe7",100:"#f0f4c3",200:"#e6ee9c",300:"#dce775",400:"#d4e157",500:"#cddc39",600:"#c0ca33",700:"#afb42b",800:"#9e9d24",900:"#827717",A100:"#f4ff81",A200:"#eeff41",A400:"#c6ff00",A700:"#aeea00",contrastDefaultColor:"dark",contrastLightColors:"900",contrastStrongLightColors:"900"},yellow:{50:"#fffde7",100:"#fff9c4",200:"#fff59d",300:"#fff176",400:"#ffee58",500:"#ffeb3b",600:"#fdd835",700:"#fbc02d",800:"#f9a825",900:"#f57f17",A100:"#ffff8d",A200:"#ffff00",A400:"#ffea00",A700:"#ffd600",contrastDefaultColor:"dark"},amber:{50:"#fff8e1",100:"#ffecb3",200:"#ffe082",300:"#ffd54f",400:"#ffca28",500:"#ffc107",600:"#ffb300",700:"#ffa000",800:"#ff8f00",900:"#ff6f00",A100:"#ffe57f",A200:"#ffd740",A400:"#ffc400",A700:"#ffab00",contrastDefaultColor:"dark"},orange:{50:"#fff3e0",100:"#ffe0b2",200:"#ffcc80",300:"#ffb74d",400:"#ffa726",500:"#ff9800",600:"#fb8c00",700:"#f57c00",800:"#ef6c00",900:"#e65100",A100:"#ffd180",A200:"#ffab40",A400:"#ff9100",A700:"#ff6d00",contrastDefaultColor:"dark",contrastLightColors:"800 900",contrastStrongLightColors:"800 900"},"deep-orange":{50:"#fbe9e7",100:"#ffccbc",200:"#ffab91",300:"#ff8a65",400:"#ff7043",500:"#ff5722",600:"#f4511e",700:"#e64a19",800:"#d84315",900:"#bf360c",A100:"#ff9e80",A200:"#ff6e40",A400:"#ff3d00",A700:"#dd2c00",contrastDefaultColor:"light",contrastDarkColors:"50 100 200 300 400 A100 A200",contrastStrongLightColors:"500 600 700 800 900 A400 A700"},brown:{50:"#efebe9",100:"#d7ccc8",200:"#bcaaa4",300:"#a1887f",400:"#8d6e63",500:"#795548",600:"#6d4c41",700:"#5d4037",800:"#4e342e",900:"#3e2723",A100:"#d7ccc8",A200:"#bcaaa4",A400:"#8d6e63",A700:"#5d4037",contrastDefaultColor:"light",contrastDarkColors:"50 100 200 A100 A200",contrastStrongLightColors:"300 400"},grey:{50:"#fafafa",100:"#f5f5f5",200:"#eeeeee",300:"#e0e0e0",400:"#bdbdbd",500:"#9e9e9e",600:"#757575",700:"#616161",800:"#424242",900:"#212121",A100:"#ffffff",A200:"#000000",A400:"#303030",A700:"#616161",contrastDefaultColor:"dark",contrastLightColors:"600 700 800 900 A200 A400 A700"},"blue-grey":{50:"#eceff1",100:"#cfd8dc",200:"#b0bec5",300:"#90a4ae",400:"#78909c",500:"#607d8b",600:"#546e7a",700:"#455a64",800:"#37474f",900:"#263238",A100:"#cfd8dc",A200:"#b0bec5",A400:"#78909c",A700:"#455a64",contrastDefaultColor:"light",contrastDarkColors:"50 100 200 300 A100 A200",contrastStrongLightColors:"400 500 700"}})}(),function(){!function(e){function t(e){var t=!!document.querySelector("[md-themes-disabled]");e.disableTheming(t)}function o(t,o){function i(e,t){return t=t||{},p[e]=a(e,t),h}function r(t,n){return a(t,e.extend({},p[t]||{},n))}function a(e,t){var n=w.filter(function(e){return!t[e]});if(n.length)throw new Error("Missing colors %1 in palette %2!".replace("%1",n.join(", ")).replace("%2",e));return t}function s(t,n){if(E[t])return E[t];n=n||"default";var o="string"==typeof n?E[n]:n,i=new l(t);return o&&e.forEach(o.colors,function(t,n){i.colors[n]={name:t.name,hues:e.extend({},t.hues)}}),E[t]=i,i}function l(t){function n(t){if(t=0===arguments.length||!!t,t!==o.isDark){o.isDark=t,o.foregroundPalette=o.isDark?g:f,o.foregroundShadow=o.isDark?b:v;var n=o.isDark?A:T,i=o.isDark?T:A;return e.forEach(n,function(e,t){var n=o.colors[t],r=i[t];if(n)for(var a in n.hues)n.hues[a]===r[a]&&(n.hues[a]=e[a])}),o}}var o=this;o.name=t,o.colors={},o.dark=n,n(!1),y.forEach(function(t){var n=(o.isDark?A:T)[t];o[t+"Palette"]=function(i,r){var a=o.colors[t]={name:i,hues:e.extend({},n,r)};return Object.keys(a.hues).forEach(function(e){if(!n[e])throw new Error("Invalid hue name '%1' in theme %2's %3 color %4. Available hue names: %4".replace("%1",e).replace("%2",o.name).replace("%3",i).replace("%4",Object.keys(n).join(", ")))}),Object.keys(a.hues).map(function(e){return a.hues[e]}).forEach(function(e){if(w.indexOf(e)==-1)throw new Error("Invalid hue value '%1' in theme %2's %3 color %4. Available hue values: %5".replace("%1",e).replace("%2",o.name).replace("%3",t).replace("%4",i).replace("%5",w.join(", ")))}),o},o[t+"Color"]=function(){var e=Array.prototype.slice.call(arguments);return console.warn("$mdThemingProviderTheme."+t+"Color() has been deprecated. Use $mdThemingProviderTheme."+t+"Palette() instead."),o[t+"Palette"].apply(o,e)}})}function m(t,o,i,r){function a(e){return e===n||""===e||l.THEMES[e]!==n}function d(e,t){function n(){return d&&d.$mdTheme||("default"==C?"":C)}function i(t){if(t){a(t)||r.warn("Attempted to use unregistered theme '"+t+"'. Register it with $mdThemingProvider.theme().");var n=e.data("$mdThemeName");n&&e.removeClass("md-"+n+"-theme"),e.addClass("md-"+t+"-theme"),e.data("$mdThemeName",t),d&&e.data("$mdThemeController",d)}}var d=t.controller("mdTheme")||e.data("$mdThemeController");if(i(n()),d)var s=$||d.$shouldWatch||o.parseAttributeBoolean(e.attr("md-theme-watch")),c=d.registerChanges(function(t){i(t),s?e.on("$destroy",c):c()})}var l=function(e,o){o===n&&(o=e,e=n),e===n&&(e=t),l.inherit(o,o)};return Object.defineProperty(l,"THEMES",{get:function(){return e.extend({},E)}}),Object.defineProperty(l,"PALETTES",{get:function(){return e.extend({},p)}}),Object.defineProperty(l,"ALWAYS_WATCH",{get:function(){return $}}),l.inherit=d,l.registered=a,l.defaultTheme=function(){return C},l.generateTheme=function(e){c(E[e],e,k.nonce)},l.defineTheme=function(e,t){t=t||{};var n=s(e);return t.primary&&n.primaryPalette(t.primary),t.accent&&n.accentPalette(t.accent),t.warn&&n.warnPalette(t.warn),t.background&&n.backgroundPalette(t.background),t.dark&&n.dark(),this.generateTheme(e),i.resolve(e)},l.setBrowserColor=_,l}m.$inject=["$rootScope","$mdUtil","$q","$log"],p={};var h,E={},$=!1,C="default";e.extend(p,t);var M=function(e){var t=o.setMeta("theme-color",e),n=o.setMeta("msapplication-navbutton-color",e);return function(){t(),n()}},_=function(t){t=e.isObject(t)?t:{};var n=t.theme||"default",o=t.hue||"800",i=p[t.palette]||p[E[n].colors[t.palette||"primary"].name],r=e.isObject(i[o])?i[o].hex:i[o];return M(r)};return h={definePalette:i,extendPalette:r,theme:s,configuration:function(){return e.extend({},k,{defaultTheme:C,alwaysWatchTheme:$,registeredStyles:[].concat(k.registeredStyles)})},disableTheming:function(t){k.disableTheming=e.isUndefined(t)||!!t},registerStyles:function(e){k.registeredStyles.push(e)},setNonce:function(e){k.nonce=e},generateThemesOnDemand:function(e){k.generateOnDemand=e},setDefaultTheme:function(e){C=e},alwaysWatchTheme:function(e){$=e},enableBrowserColor:_,$get:m,_LIGHT_DEFAULT_HUES:T,_DARK_DEFAULT_HUES:A,_PALETTES:p,_THEMES:E,_parseRules:d,_rgba:u}}function i(t,n,o,i,r,a){return{priority:101,link:{pre:function(d,s,c){var l=[],m=n.startSymbol(),u=n.endSymbol(),p=c.mdTheme.trim(),h=p.substr(0,m.length)===m&&p.lastIndexOf(u)===p.length-u.length,f="::",g=c.mdTheme.split(m).join("").split(u).join("").trim().substr(0,f.length)===f,b={registerChanges:function(t,n){return n&&(t=e.bind(n,t)),l.push(t),function(){var e=l.indexOf(t);e>-1&&l.splice(e,1)}},$setTheme:function(e){t.registered(e)||a.warn("attempted to use unregistered theme '"+e+"'"),b.$mdTheme=e;for(var n=l.length;n--;)l[n](e)},$shouldWatch:i.parseAttributeBoolean(s.attr("md-theme-watch"))||t.ALWAYS_WATCH||h&&!g};s.data("$mdThemeController",b);var v=function(){var e=n(c.mdTheme)(d);return o(e)(d)||e},E=function(t){return"string"==typeof t?b.$setTheme(t):void r.when(e.isFunction(t)?t():t).then(function(e){b.$setTheme(e)})};E(v());var $=d.$watch(v,function(e){e&&(E(e),b.$shouldWatch||$())})}}}}function r(){return k.disableTheming=!0,{restrict:"A",priority:"900"}}function a(e){return e}function d(t,n,o){l(t,n),o=o.replace(/THEME_NAME/g,t.name);var i=[],r=t.colors[n],a=new RegExp("\\.md-"+t.name+"-theme","g"),d=new RegExp("('|\")?{{\\s*("+n+")-(color|contrast)-?(\\d\\.?\\d*)?\\s*}}(\"|')?","g"),s=/'?"?\{\{\s*([a-zA-Z]+)-(A?\d+|hue\-[0-3]|shadow|default)-?(\d\.?\d*)?(contrast)?\s*\}\}'?"?/g,c=p[r.name];return o=o.replace(s,function(e,n,o,i,r){return"foreground"===n?"shadow"==o?t.foregroundShadow:t.foregroundPalette[o]||t.foregroundPalette[1]:(0!==o.indexOf("hue")&&"default"!==o||(o=t.colors[n].hues[o]),u((p[t.colors[n].name][o]||"")[r?"contrast":"value"],i))}),e.forEach(r.hues,function(e,n){var r=o.replace(d,function(t,n,o,i,r){return u(c[e]["color"===i?"value":"contrast"],r)});if("default"!==n&&(r=r.replace(a,".md-"+t.name+"-theme.md-"+n)),"default"==t.name){var s=/((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)\.md-default-theme((?:\s|>|\.|\w|-|:|\(|\)|\[|\]|"|'|=)*)/g;r=r.replace(s,function(e,t,n){return e+", "+t+n})}i.push(r)}),i}function s(t,n){function o(t,n){var o=t.contrastDefaultColor,i=t.contrastLightColors||[],r=t.contrastStrongLightColors||[],a=t.contrastDarkColors||[];"string"==typeof i&&(i=i.split(" ")),"string"==typeof r&&(r=r.split(" ")),"string"==typeof a&&(a=a.split(" ")),delete t.contrastDefaultColor,delete t.contrastLightColors,delete t.contrastStrongLightColors,delete t.contrastDarkColors,e.forEach(t,function(n,d){function s(){return"light"===o?a.indexOf(d)>-1?E:r.indexOf(d)>-1?C:$:i.indexOf(d)>-1?r.indexOf(d)>-1?C:$:E}if(!e.isObject(n)){var c=m(n);if(!c)throw new Error("Color %1, in palette %2's hue %3, is invalid. Hex or rgb(a) color expected.".replace("%1",n).replace("%2",t.name).replace("%3",d));t[d]={hex:t[d],value:c,contrast:s()}}})}var i=document.head,r=i?i.firstElementChild:null,a=!k.disableTheming&&t.has("$MD_THEME_CSS")?t.get("$MD_THEME_CSS"):"";if(a+=k.registeredStyles.join(""),r&&0!==a.length){e.forEach(p,o);var d=a.split(/\}(?!(\}|'|"|;))/).filter(function(e){return e&&e.trim().length}).map(function(e){return e.trim()+"}"}),s=new RegExp("md-("+y.join("|")+")","g");y.forEach(function(e){_[e]=""}),d.forEach(function(e){for(var t,n=(e.match(s),0);t=y[n];n++)if(e.indexOf(".md-"+t)>-1)return _[t]+=e;for(n=0;t=y[n];n++)if(e.indexOf(t)>-1)return _[t]+=e;return _[M]+=e}),k.generateOnDemand||e.forEach(n.THEMES,function(e){h[e.name]||"default"!==n.defaultTheme()&&"default"===e.name||c(e,e.name,k.nonce)})}}function c(e,t,n){var o=document.head,i=o?o.firstElementChild:null;h[t]||(y.forEach(function(t){for(var r=d(e,t,_[t]);r.length;){var a=r.shift();if(a){var s=document.createElement("style");s.setAttribute("md-theme-style",""),n&&s.setAttribute("nonce",n),s.appendChild(document.createTextNode(a)),o.insertBefore(s,i)}}}),h[e.name]=!0)}function l(e,t){if(!p[(e.colors[t]||{}).name])throw new Error("You supplied an invalid color palette for theme %1's %2 palette. Available palettes: %3".replace("%1",e.name).replace("%2",t).replace("%3",Object.keys(p).join(", ")))}function m(t){ -if(e.isArray(t)&&3==t.length)return t;if(/^rgb/.test(t))return t.replace(/(^\s*rgba?\(|\)\s*$)/g,"").split(",").map(function(e,t){return 3==t?parseFloat(e,10):parseInt(e,10)});if("#"==t.charAt(0)&&(t=t.substring(1)),/^([a-fA-F0-9]{3}){1,2}$/g.test(t)){var n=t.length/3,o=t.substr(0,n),i=t.substr(n,n),r=t.substr(2*n);return 1===n&&(o+=o,i+=i,r+=r),[parseInt(o,16),parseInt(i,16),parseInt(r,16)]}}function u(t,n){return t?(4==t.length&&(t=e.copy(t),n?t.pop():n=t.pop()),n&&("number"==typeof n||"string"==typeof n&&n.length)?"rgba("+t.join(",")+","+n+")":"rgb("+t.join(",")+")"):"rgb('0,0,0')"}t.$inject=["$mdThemingProvider"],i.$inject=["$mdTheming","$interpolate","$parse","$mdUtil","$q","$log"],a.$inject=["$mdTheming"],o.$inject=["$mdColorPalette","$$mdMetaProvider"],s.$inject=["$injector","$mdTheming"],e.module("material.core.theming",["material.core.theming.palette","material.core.meta"]).directive("mdTheme",i).directive("mdThemable",a).directive("mdThemesDisabled",r).provider("$mdTheming",o).config(t).run(s);var p,h={},f={name:"dark",1:"rgba(0,0,0,0.87)",2:"rgba(0,0,0,0.54)",3:"rgba(0,0,0,0.38)",4:"rgba(0,0,0,0.12)"},g={name:"light",1:"rgba(255,255,255,1.0)",2:"rgba(255,255,255,0.7)",3:"rgba(255,255,255,0.5)",4:"rgba(255,255,255,0.12)"},b="1px 1px 0px rgba(0,0,0,0.4), -1px -1px 0px rgba(0,0,0,0.4)",v="",E=m("rgba(0,0,0,0.87)"),$=m("rgba(255,255,255,0.87)"),C=m("rgb(255,255,255)"),y=["primary","accent","warn","background"],M="primary",T={accent:{"default":"A200","hue-1":"A100","hue-2":"A400","hue-3":"A700"},background:{"default":"50","hue-1":"A100","hue-2":"100","hue-3":"300"}},A={background:{"default":"A400","hue-1":"800","hue-2":"900","hue-3":"A200"}};y.forEach(function(e){var t={"default":"500","hue-1":"300","hue-2":"800","hue-3":"A100"};T[e]||(T[e]=t),A[e]||(A[e]=t)});var w=["50","100","200","300","400","500","600","700","800","900","A100","A200","A400","A700"],k={disableTheming:!1,generateOnDemand:!1,registeredStyles:[],nonce:null},_={}}(e.angular)}(),function(){function n(n,o,i,r,a){var d;return d={translate3d:function(e,t,n,o){function i(n){return a(e,{to:n||t,addClass:o.transitionOutClass,removeClass:o.transitionInClass,duration:o.duration}).start()}return a(e,{from:t,to:n,addClass:o.transitionInClass,removeClass:o.transitionOutClass,duration:o.duration}).start().then(function(){return i})},waitTransitionEnd:function(t,n){var a=3e3;return o(function(o,d){function s(e){e&&e.target!==t[0]||(e&&i.cancel(l),t.off(r.CSS.TRANSITIONEND,s),o())}function c(n){return n=n||e.getComputedStyle(t[0]),"0s"==n.transitionDuration||!n.transition&&!n.transitionProperty}n=n||{},c(n.cachedTransitionStyles)&&(a=0);var l=i(s,n.timeout||a);t.on(r.CSS.TRANSITIONEND,s)})},calculateTransformValues:function(e,t){function n(){var t=e?e.parent():null,n=t?t.parent():null;return n?d.clientRect(n):null}var o=t.element,i=t.bounds;if(o||i){var r=o?d.clientRect(o)||n():d.copyRect(i),a=d.copyRect(e[0].getBoundingClientRect()),s=d.centerPointFor(a),c=d.centerPointFor(r);return{centerX:c.x-s.x,centerY:c.y-s.y,scaleX:Math.round(100*Math.min(.5,r.width/a.width))/100,scaleY:Math.round(100*Math.min(.5,r.height/a.height))/100}}return{centerX:0,centerY:0,scaleX:.5,scaleY:.5}},calculateZoomToOrigin:function(e,o){var i="translate3d( {centerX}px, {centerY}px, 0 ) scale( {scaleX}, {scaleY} )",r=t.bind(null,n.supplant,i);return r(d.calculateTransformValues(e,o))},calculateSlideToOrigin:function(e,o){var i="translate3d( {centerX}px, {centerY}px, 0 )",r=t.bind(null,n.supplant,i);return r(d.calculateTransformValues(e,o))},toCss:function(e){function n(e,n,i){t.forEach(n.split(" "),function(e){o[e]=i})}var o={},i="left top right bottom width height x y min-width min-height max-width max-height";return t.forEach(e,function(e,a){if(!t.isUndefined(e))if(i.indexOf(a)>=0)o[a]=e+"px";else switch(a){case"transition":n(a,r.CSS.TRANSITION,e);break;case"transform":n(a,r.CSS.TRANSFORM,e);break;case"transformOrigin":n(a,r.CSS.TRANSFORM_ORIGIN,e);break;case"font-size":o["font-size"]=e}}),o},toTransformCss:function(e,n,o){var i={};return t.forEach(r.CSS.TRANSFORM.split(" "),function(t){i[t]=e}),n&&(o=o||"all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1) !important",i.transition=o),i},copyRect:function(e,n){return e?(n=n||{},t.forEach("left top right bottom width height".split(" "),function(t){n[t]=Math.round(e[t])}),n.width=n.width||n.right-n.left,n.height=n.height||n.bottom-n.top,n):null},clientRect:function(e){var n=t.element(e)[0].getBoundingClientRect(),o=function(e){return e&&e.width>0&&e.height>0};return o(n)?d.copyRect(n):null},centerPointFor:function(e){return e?{x:Math.round(e.left+e.width/2),y:Math.round(e.top+e.height/2)}:{x:0,y:0}}}}t.module("material.core").factory("$$mdAnimate",["$q","$timeout","$mdConstant","$animateCss",function(e,t,o,i){return function(r){return n(r,e,t,o,i)}}])}(),function(){t.version.minor>=4?t.module("material.core.animate",[]):!function(){function e(e){return e.replace(/-[a-z]/g,function(e){return e.charAt(1).toUpperCase()})}var n=t.forEach,o=t.isDefined(document.documentElement.style.WebkitAppearance),i=o?"-webkit-":"",r=(o?"webkitTransitionEnd ":"")+"transitionend",a=(o?"webkitAnimationEnd ":"")+"animationend",d=["$document",function(e){return function(){return e[0].body.clientWidth+1}}],s=["$$rAF",function(e){return function(){var t=!1;return e(function(){t=!0}),function(n){t?n():e(n)}}}],c=["$q","$$rAFMutex",function(e,o){function i(e){this.setHost(e),this._doneCallbacks=[],this._runInAnimationFrame=o(),this._state=0}var r=0,a=1,d=2;return i.prototype={setHost:function(e){this.host=e||{}},done:function(e){this._state===d?e():this._doneCallbacks.push(e)},progress:t.noop,getPromise:function(){if(!this.promise){var t=this;this.promise=e(function(e,n){t.done(function(t){t===!1?n():e()})})}return this.promise},then:function(e,t){return this.getPromise().then(e,t)},"catch":function(e){return this.getPromise()["catch"](e)},"finally":function(e){return this.getPromise()["finally"](e)},pause:function(){this.host.pause&&this.host.pause()},resume:function(){this.host.resume&&this.host.resume()},end:function(){this.host.end&&this.host.end(),this._resolve(!0)},cancel:function(){this.host.cancel&&this.host.cancel(),this._resolve(!1)},complete:function(e){var t=this;t._state===r&&(t._state=a,t._runInAnimationFrame(function(){t._resolve(e)}))},_resolve:function(e){this._state!==d&&(n(this._doneCallbacks,function(t){t(e)}),this._doneCallbacks.length=0,this._state=d)}},i.all=function(e,t){function o(n){r=r&&n,++i===e.length&&t(r)}var i=0,r=!0;n(e,function(e){e.done(o)})},i}];t.module("material.core.animate",[]).factory("$$forceReflow",d).factory("$$AnimateRunner",c).factory("$$rAFMutex",s).factory("$animateCss",["$window","$$rAF","$$AnimateRunner","$$forceReflow","$$jqLite","$timeout","$animate",function(t,d,s,c,l,m,u){function p(o,d){var c=[],l=C(o),p=l&&u.enabled(),g=!1,M=!1;p&&(d.transitionStyle&&c.push([i+"transition",d.transitionStyle]),d.keyframeStyle&&c.push([i+"animation",d.keyframeStyle]),d.delay&&c.push([i+"transition-delay",d.delay+"s"]),d.duration&&c.push([i+"transition-duration",d.duration+"s"]),g=d.keyframeStyle||d.to&&(d.duration>0||d.transitionStyle),M=!!d.addClass||!!d.removeClass,y(o,!0));var T=p&&(g||M);E(o,d);var A,w,k=!1;return{close:t.close,start:function(){function t(){if(!k)return k=!0,A&&w&&o.off(A,w),h(o,d),v(o,d),n(c,function(t){l.style[e(t[0])]=""}),u.complete(!0),u}var u=new s;return b(function(){if(y(o,!1),!T)return t();n(c,function(t){var n=t[0],o=t[1];l.style[e(n)]=o}),h(o,d);var s=f(o);if(0===s.duration)return t();var u=[];d.easing&&(s.transitionDuration&&u.push([i+"transition-timing-function",d.easing]),s.animationDuration&&u.push([i+"animation-timing-function",d.easing])),d.delay&&s.animationDelay&&u.push([i+"animation-delay",d.delay+"s"]),d.duration&&s.animationDuration&&u.push([i+"animation-duration",d.duration+"s"]),n(u,function(t){var n=t[0],o=t[1];l.style[e(n)]=o,c.push(t)});var p=s.delay,g=1e3*p,b=s.duration,v=1e3*b,E=Date.now();A=[],s.transitionDuration&&A.push(r),s.animationDuration&&A.push(a),A=A.join(" "),w=function(e){e.stopPropagation();var n=e.originalEvent||e,o=n.timeStamp||Date.now(),i=parseFloat(n.elapsedTime.toFixed(3));Math.max(o-E,0)>=g&&i>=b&&t()},o.on(A,w),$(o,d),m(t,g+1.5*v,!1)}),u}}}function h(e,t){t.addClass&&(l.addClass(e,t.addClass),t.addClass=null),t.removeClass&&(l.removeClass(e,t.removeClass),t.removeClass=null)}function f(e){function n(e){return o?"Webkit"+e.charAt(0).toUpperCase()+e.substr(1):e}var i=C(e),r=t.getComputedStyle(i),a=g(r[n("transitionDuration")]),d=g(r[n("animationDuration")]),s=g(r[n("transitionDelay")]),c=g(r[n("animationDelay")]);d*=parseInt(r[n("animationIterationCount")],10)||1;var l=Math.max(d,a),m=Math.max(c,s);return{duration:l,delay:m,animationDuration:d,transitionDuration:a,animationDelay:c,transitionDelay:s}}function g(e){var t=0,o=(e||"").split(/\s*,\s*/);return n(o,function(e){"s"==e.charAt(e.length-1)&&(e=e.substring(0,e.length-1)),e=parseFloat(e)||0,t=t?Math.max(e,t):e}),t}function b(e){M&&M(),T.push(e),M=d(function(){M=null;for(var e=c(),t=0;t0&&(t.pointer.distanceY>20||Math.abs(t.pointer.velocityY)>o)){var i=e.prop("offsetHeight")-t.pointer.distanceY,a=Math.min(i/t.pointer.velocityY*.75,500);e.css(n.CSS.TRANSITION_DURATION,a+"ms"),r.nextTick(d.cancel,!0)}else e.css(n.CSS.TRANSITION_DURATION,""),e.css(n.CSS.TRANSFORM,"")}var m=c.register(t,"drag",{horizontal:!1});return t.on("$md.dragstart",a).on("$md.drag",s).on("$md.dragend",l),{element:e,cleanup:function(){m(),t.off("$md.dragstart",a),t.off("$md.drag",s),t.off("$md.dragend",l)}}}var h;return{themable:!0,onShow:m,onRemove:u,disableBackdrop:!1,escapeToClose:!0,clickOutsideToClose:!0,disableParentScroll:!0}}n.$inject=["$animate","$mdConstant","$mdUtil","$mdTheming","$mdBottomSheet","$rootElement","$mdGesture","$log"];var o=.5,i=80;return e("$mdBottomSheet").setDefaults({methods:["disableParentScroll","escapeToClose","clickOutsideToClose"],options:n})}e.$inject=["$mdBottomSheet"],n.$inject=["$$interimElementProvider"],t.module("material.components.bottomSheet",["material.core","material.components.backdrop"]).directive("mdBottomSheet",e).provider("$mdBottomSheet",n)}(),function(){function e(e){return{restrict:"E",link:function(t,n){e(n)}}}function n(e,n,o,i){function r(e){return t.isDefined(e.href)||t.isDefined(e.ngHref)||t.isDefined(e.ngLink)||t.isDefined(e.uiSref)}function a(e,t){if(r(t))return'';var n="undefined"==typeof t.type?"button":t.type;return''}function d(a,d,s){n(d),e.attach(a,d),o.expectWithoutText(d,"aria-label"),r(s)&&t.isDefined(s.ngDisabled)&&a.$watch(s.ngDisabled,function(e){d.attr("tabindex",e?-1:0)}),d.on("click",function(e){s.disabled===!0&&(e.preventDefault(),e.stopImmediatePropagation())}),d.hasClass("md-no-focus")||(d.on("focus",function(){i.isUserInvoked()&&"keyboard"!==i.getLastInteractionType()||d.addClass("md-focused")}),d.on("blur",function(){d.removeClass("md-focused")}))}return{restrict:"EA",replace:!0,transclude:!0,template:a,link:d}}n.$inject=["$mdButtonInkRipple","$mdTheming","$mdAria","$mdInteraction"],e.$inject=["$mdTheming"],t.module("material.components.button",["material.core"]).directive("mdButton",n).directive("a",e)}(),function(){function e(e){return{restrict:"E",link:function(t,n,o){n.addClass("_md"),e(n)}}}e.$inject=["$mdTheming"],t.module("material.components.card",["material.core"]).directive("mdCard",e)}(),function(){function e(e,n,o,i,r,a){function d(d,s){function c(d,s,c,l){function m(e,t,n){c[e]&&d.$watch(c[e],function(e){n[e]&&s.attr(t,n[e])})}function u(e){var t=e.which||e.keyCode;t!==o.KEY_CODE.SPACE&&t!==o.KEY_CODE.ENTER||(e.preventDefault(),s.addClass("md-focused"),p(e))}function p(e){s[0].hasAttribute("disabled")||d.skipToggle||d.$apply(function(){var t=c.ngChecked&&c.ngClick?c.checked:!v.$viewValue;v.$setViewValue(t,e&&e.type),v.$render()})}function h(){s.toggleClass("md-checked",!!v.$viewValue&&!g)}function f(e){g=e!==!1,g&&s.attr("aria-checked","mixed"),s.toggleClass("md-indeterminate",g)}var g,b=l[0],v=l[1]||r.fakeNgModel(),E=l[2];if(b){var $=b.isErrorGetter||function(){return v.$invalid&&(v.$touched||E&&E.$submitted)};b.input=s,d.$watch($,b.setInvalid)}i(s),s.children().on("focus",function(){s.focus()}),r.parseAttributeBoolean(c.mdIndeterminate)&&(f(),d.$watch(c.mdIndeterminate,f)),c.ngChecked&&d.$watch(d.$eval.bind(d,c.ngChecked),function(e){v.$setViewValue(e),v.$render()}),m("ngDisabled","tabindex",{"true":"-1","false":c.tabindex}),n.expectWithText(s,"aria-label"),e.link.pre(d,{on:t.noop,0:{}},c,[v]),s.on("click",p).on("keypress",u).on("focus",function(){"keyboard"===a.getLastInteractionType()&&s.addClass("md-focused")}).on("blur",function(){s.removeClass("md-focused")}),v.$render=h}return s.$set("tabindex",s.tabindex||"0"),s.$set("type","checkbox"),s.$set("role",s.type),{pre:function(e,t){t.on("click",function(e){this.hasAttribute("disabled")&&e.stopImmediatePropagation()})},post:c}}return e=e[0],{restrict:"E",transclude:!0,require:["^?mdInputContainer","?ngModel","?^form"],priority:o.BEFORE_NG_ARIA,template:'
',compile:d}}e.$inject=["inputDirective","$mdAria","$mdConstant","$mdTheming","$mdUtil","$mdInteraction"],t.module("material.components.checkbox",["material.core"]).directive("mdCheckbox",e)}(),function(){t.module("material.components.chips",["material.core","material.components.autocomplete"])}(),function(){!function(){function e(e,n,o){function r(e,t){try{t&&e.css(s(t))}catch(n){o.error(n.message)}}function a(e){var t=l(e);return d(t)}function d(t,o){o=o||!1;var i=e.PALETTES[t.palette][t.hue];return i=o?i.contrast:i.value,n.supplant("rgba({0}, {1}, {2}, {3})",[i[0],i[1],i[2],i[3]||t.opacity])}function s(e){var n={},o=e.hasOwnProperty("color");return t.forEach(e,function(e,t){var i=l(e),r=t.indexOf("background")>-1;n[t]=d(i),r&&!o&&(n.color=d(i,!0))}),n}function c(n){return t.isDefined(e.THEMES[n.split("-")[0]])}function l(n){var o=n.split("-"),i=t.isDefined(e.THEMES[o[0]]),r=i?o.splice(0,1)[0]:e.defaultTheme();return{theme:r,palette:m(o,r),hue:u(o,r),opacity:o[2]||1}}function m(t,o){var r=t.length>1&&i.indexOf(t[1])!==-1,a=t[0].replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();if(r&&(a=t[0]+"-"+t.splice(1,1)),i.indexOf(a)===-1){var d=e.THEMES[o].colors[a];if(!d)throw new Error(n.supplant("mdColors: couldn't find '{palette}' in the palettes.",{palette:a}));a=d.name}return a}function u(t,o){var i=e.THEMES[o].colors;if("hue"===t[1]){var r=parseInt(t.splice(2,1)[0],10);if(r<1||r>3)throw new Error(n.supplant("mdColors: 'hue-{hueNumber}' is not a valid hue, can be only 'hue-1', 'hue-2' and 'hue-3'",{hueNumber:r}));if(t[1]="hue-"+r,!(t[0]in i))throw new Error(n.supplant("mdColors: 'hue-x' can only be used with [{availableThemes}], but was used with '{usedTheme}'",{availableThemes:Object.keys(i).join(", "),usedTheme:t[0]}));return i[t[0]].hues[t[1]]}return t[1]||i[t[0]in i?t[0]:"primary"].hues["default"]}return i=i||Object.keys(e.PALETTES),{applyThemeColors:r,getThemeColor:a,hasTheme:c}}function n(e,n,i,r){return{restrict:"A",require:["^?mdTheme"],compile:function(a,d){function s(){var e=d.mdColors,i=e.indexOf("::")>-1,r=!!i||o.test(d.mdColors);d.mdColors=e.replace("::","");var a=t.isDefined(d.mdColorsWatch);return!i&&!r&&(!a||n.parseAttributeBoolean(d.mdColorsWatch))}var c=s();return function(n,o,a,d){var s=d[0],l={},m=function(t){"string"!=typeof t&&(t=""),a.mdColors||(a.mdColors="{}");var o=r(a.mdColors)(n);return s&&Object.keys(o).forEach(function(n){var i=o[n];e.hasTheme(i)||(o[n]=(t||s.$mdTheme)+"-"+i)}),u(o),o},u=function(e){if(!t.equals(e,l)){var n=Object.keys(l);l.background&&!n.color&&n.push("color"),n.forEach(function(e){o.css(e,"")})}l=e},p=t.noop;s&&(p=s.registerChanges(function(t){e.applyThemeColors(o,m(t))})),n.$on("$destroy",function(){p()});try{c?n.$watch(m,t.bind(this,e.applyThemeColors,o),!0):e.applyThemeColors(o,m())}catch(h){i.error(h.message)}}}}}n.$inject=["$mdColors","$mdUtil","$log","$parse"],e.$inject=["$mdTheming","$mdUtil","$log"];var o=/^{((\s|,)*?["'a-zA-Z-]+?\s*?:\s*?('|")[a-zA-Z0-9-.]*('|"))+\s*}$/,i=null;t.module("material.components.colors",["material.core"]).directive("mdColors",n).service("$mdColors",e)}()}(),function(){function e(e){function t(e,t){this.$scope=e,this.$element=t}return{restrict:"E",controller:["$scope","$element",t],link:function(t,o){o.addClass("_md"),e(o),t.$broadcast("$mdContentLoaded",o),n(o[0])}}}function n(e){t.element(e).on("$md.pressdown",function(t){"t"===t.pointer.type&&(t.$materialScrollFixed||(t.$materialScrollFixed=!0,0===e.scrollTop?e.scrollTop=1:e.scrollHeight===e.scrollTop+e.offsetHeight&&(e.scrollTop-=1)))})}e.$inject=["$mdTheming"],t.module("material.components.content",["material.core"]).directive("mdContent",e)}(),function(){t.module("material.components.datepicker",["material.core","material.components.icon","material.components.virtualRepeat"])}(),function(){function e(e,n,o){return{restrict:"E",link:function(i,r){r.addClass("_md"),n(r),e(function(){function e(){r.toggleClass("md-content-overflow",a.scrollHeight>a.clientHeight)}var n,a=r[0].querySelector("md-dialog-content");a&&(n=a.getElementsByTagName("img"),e(),t.element(n).on("load",e)),i.$on("$destroy",function(){o.destroy(r)})})}}}function o(e){function o(e,t){return{template:['',' ','

{{ dialog.title }}

','
','
',"

{{::dialog.mdTextContent}}

","
",' ',' '," ","
"," ",' '," {{ dialog.cancel }}"," ",' '," {{ dialog.ok }}"," "," ","
"].join("").replace(/\s\s+/g,""),controller:function(){var n="prompt"==this.$type;n&&this.initialValue&&(this.result=this.initialValue),this.hide=function(){e.hide(!n||this.result)},this.abort=function(){e.cancel()},this.keypress=function(n){n.keyCode===t.KEY_CODE.ENTER&&e.hide(this.result)}},controllerAs:"dialog",bindToController:!0}}function i(e,o,i,d,s,c,l,m,u,p,h,f,g){function b(e){e.defaultTheme=h.defaultTheme(),C(e)}function v(e,t,n,o){if(o){var i=o.htmlContent||n.htmlContent||"",r=o.textContent||n.textContent||o.content||n.content||"";if(i&&!p.has("$sanitize"))throw Error("The ngSanitize module must be loaded in order to use htmlContent.");if(i&&r)throw Error("md-dialog cannot have both `htmlContent` and `textContent`");o.mdHtmlContent=i,o.mdTextContent=r}}function E(e,n,o,r){function a(){n[0].querySelector(".md-actions")&&u.warn("Using a class of md-actions is deprecated, please use .")}function d(){function e(){return n[0].querySelector(".dialog-close, md-dialog-actions button:last-child")}if(o.focusOnOpen){var t=i.findFocusTarget(n)||e()||s;t.focus()}}t.element(c[0].body).addClass("md-dialog-is-showing");var s=n.find("md-dialog");if(s.hasClass("ng-cloak")){var l="$mdDialog: using `` will affect the dialog opening animations.";u.warn(l,n[0])}return y(o),A(s,o),T(e,n,o),M(n,o),_(n,o).then(function(){w(n,o),a(),d()})}function $(e,n,o){function i(){return x(n,o)}function d(){t.element(c[0].body).removeClass("md-dialog-is-showing"),o.contentElement&&o.reverseContainerStretch(),o.cleanupElement(),o.$destroy||"keyboard"!==o.originInteraction||o.origin.focus()}return o.deactivateListeners(),o.unlockScreenReader(),o.hideBackdrop(o.$destroy),r&&r.parentNode&&r.parentNode.removeChild(r),a&&a.parentNode&&a.parentNode.removeChild(a),o.$destroy?d():i().then(d)}function C(e){var n;e.targetEvent&&e.targetEvent.target&&(n=t.element(e.targetEvent.target));var o=n&&n.controller("mdTheme");if(o){e.themeWatch=o.$shouldWatch;var i=e.theme||o.$mdTheme;i&&(e.scope.theme=i);var r=o.registerChanges(function(t){e.scope.theme=t,e.themeWatch||r()})}}function y(e){function o(e,o){var i=t.element(e||{});if(i&&i.length){var r={top:0,left:0,height:0,width:0},a=t.isFunction(i[0].getBoundingClientRect);return t.extend(o||{},{element:a?i:n,bounds:a?i[0].getBoundingClientRect():t.extend({},r,i[0]),focus:t.bind(i,i.focus)})}}function i(e,n){return t.isString(e)&&(e=c[0].querySelector(e)),t.element(e||n)}e.origin=t.extend({element:null,bounds:null,focus:t.noop},e.origin||{}),e.parent=i(e.parent,m),e.closeTo=o(i(e.closeTo)),e.openFrom=o(i(e.openFrom)),e.targetEvent&&(e.origin=o(e.targetEvent.target,e.origin),e.originInteraction=g.getLastInteractionType())}function M(n,o){var r=t.element(l),a=i.debounce(function(){k(n,o)},60),s=[],c=function(){var t="alert"==o.$type?e.hide:e.cancel;i.nextTick(t,!0)};if(o.escapeToClose){var m=o.parent,u=function(e){e.keyCode===d.KEY_CODE.ESCAPE&&(e.stopPropagation(),e.preventDefault(),c())};n.on("keydown",u),m.on("keydown",u),s.push(function(){n.off("keydown",u),m.off("keydown",u)})}if(r.on("resize",a),s.push(function(){r.off("resize",a)}),o.clickOutsideToClose){var p,h=n,f=function(e){p=e.target},g=function(e){p===h[0]&&e.target===h[0]&&(e.stopPropagation(),e.preventDefault(),c())};h.on("mousedown",f),h.on("mouseup",g),s.push(function(){h.off("mousedown",f),h.off("mouseup",g)})}o.deactivateListeners=function(){s.forEach(function(e){e()}),o.deactivateListeners=null}}function T(e,t,n){n.disableParentScroll&&(n.restoreScroll=i.disableScrollAround(t,n.parent)),n.hasBackdrop&&(n.backdrop=i.createBackdrop(e,"md-dialog-backdrop md-opaque"),s.enter(n.backdrop,n.parent)),n.hideBackdrop=function(e){n.backdrop&&(e?n.backdrop.remove():s.leave(n.backdrop)),n.disableParentScroll&&(n.restoreScroll&&n.restoreScroll(),delete n.restoreScroll),n.hideBackdrop=null}}function A(e,t){var n="alert"===t.$type?"alertdialog":"dialog",d=e.find("md-dialog-content"),s=e.attr("id"),c="dialogContent_"+(s||i.nextUid());e.attr({role:n,tabIndex:"-1"}),0===d.length&&(d=e,s&&(c=s)),d.attr("id",c),e.attr("aria-describedby",c),t.ariaLabel?o.expect(e,"aria-label",t.ariaLabel):o.expectAsync(e,"aria-label",function(){var e=d.text().split(/\s+/);return e.length>3&&(e=e.slice(0,3).concat("...")),e.join(" ")}),r=document.createElement("div"),r.classList.add("md-dialog-focus-trap"),r.tabIndex=0,a=r.cloneNode(!1);var l=function(){e.focus()};r.addEventListener("focus",l),a.addEventListener("focus",l),e[0].parentNode.insertBefore(r,e[0]),e.after(a)}function w(e,t){function n(e){for(;e.parentNode;){if(e===document.body)return;for(var t=e.parentNode.children,i=0;i/g.test(e)?""+(e||"")+"":e||""}var o=f.startSymbol(),i=f.endSymbol(),r=o+(t.themeWatch?"":"::")+"theme"+i;return'
'+n(e)+"
"}}}o.$inject=["$mdDialog","$mdConstant"],i.$inject=["$mdDialog","$mdAria","$mdUtil","$mdConstant","$animate","$document","$window","$rootElement","$log","$injector","$mdTheming","$interpolate","$mdInteraction"];var r,a;return e("$mdDialog").setDefaults({methods:["disableParentScroll","hasBackdrop","clickOutsideToClose","escapeToClose","targetEvent","closeTo","openFrom","parent","fullscreen","multiple"],options:i}).addPreset("alert",{methods:["title","htmlContent","textContent","content","ariaLabel","ok","theme","css"],options:o}).addPreset("confirm",{methods:["title","htmlContent","textContent","content","ariaLabel","ok","cancel","theme","css"],options:o}).addPreset("prompt",{methods:["title","htmlContent","textContent","initialValue","content","placeholder","ariaLabel","ok","cancel","theme","css"],options:o})}e.$inject=["$$rAF","$mdTheming","$mdDialog"],o.$inject=["$$interimElementProvider"],t.module("material.components.dialog",["material.core","material.components.backdrop"]).directive("mdDialog",e).provider("$mdDialog",o)}(),function(){function e(e){return{restrict:"E",link:e}}e.$inject=["$mdTheming"],t.module("material.components.divider",["material.core"]).directive("mdDivider",e)}(),function(){!function(){function e(e){return{restrict:"E",require:["^?mdFabSpeedDial","^?mdFabToolbar"],compile:function(t,n){var o=t.children(),i=e.prefixer().hasAttribute(o,"ng-repeat");i?o.addClass("md-fab-action-item"):o.wrap('
')}}}e.$inject=["$mdUtil"],t.module("material.components.fabActions",["material.core"]).directive("mdFabActions",e)}()}(),function(){!function(){function e(e,n,o,i,r,a){function d(){N.direction=N.direction||"down",N.isOpen=N.isOpen||!1,l(),n.addClass("md-animations-waiting")}function s(){var o=["click","focusin","focusout"];t.forEach(o,function(e){n.on(e,c)}),e.$on("$destroy",function(){t.forEach(o,function(e){n.off(e,c)}),h()})}function c(e){"click"==e.type&&k(e),"focusout"!=e.type||D||(D=a(function(){N.close()},100,!1)),"focusin"==e.type&&D&&(a.cancel(D),D=null)}function l(){N.currentActionIndex=-1}function m(){e.$watch("vm.direction",function(e,t){o.removeClass(n,"md-"+t),o.addClass(n,"md-"+e),l()});var t,i;e.$watch("vm.isOpen",function(e){l(),t&&i||(t=_(),i=x()),e?p():h();var r=e?"md-is-open":"",a=e?"":"md-is-open";t.attr("aria-haspopup",!0),t.attr("aria-expanded",e),i.attr("aria-hidden",!e),o.setClass(n,r,a)})}function u(){n[0].scrollHeight>0?o.addClass(n,"_md-animations-ready").then(function(){n.removeClass("md-animations-waiting")}):S<10&&(a(u,100),S+=1)}function p(){n.on("keydown",g),i.nextTick(function(){t.element(document).on("click touchend",f)})}function h(){n.off("keydown",g),t.element(document).off("click touchend",f)}function f(e){if(e.target){var t=i.getClosest(e.target,"md-fab-trigger"),n=i.getClosest(e.target,"md-fab-actions");t||n||N.close()}}function g(e){switch(e.which){case r.KEY_CODE.ESCAPE:return N.close(),e.preventDefault(),!1;case r.KEY_CODE.LEFT_ARROW:return C(e),!1;case r.KEY_CODE.UP_ARROW:return y(e),!1;case r.KEY_CODE.RIGHT_ARROW:return M(e),!1;case r.KEY_CODE.DOWN_ARROW:return T(e),!1}}function b(e){E(e,-1)}function v(e){E(e,1)}function E(e,n){var o=$();N.currentActionIndex=N.currentActionIndex+n,N.currentActionIndex=Math.min(o.length-1,N.currentActionIndex),N.currentActionIndex=Math.max(0,N.currentActionIndex);var i=t.element(o[N.currentActionIndex]).children()[0];t.element(i).attr("tabindex",0),i.focus(),e.preventDefault(),e.stopImmediatePropagation()}function $(){var e=x()[0].querySelectorAll(".md-fab-action-item");return t.forEach(e,function(e){t.element(t.element(e).children()[0]).attr("tabindex",-1)}),e}function C(e){"left"===N.direction?v(e):b(e); -}function y(e){"down"===N.direction?b(e):v(e)}function M(e){"left"===N.direction?b(e):v(e)}function T(e){"up"===N.direction?b(e):v(e)}function A(e){return i.getClosest(e,"md-fab-trigger")}function w(e){return i.getClosest(e,"md-fab-actions")}function k(e){A(e.target)&&N.toggle(),w(e.target)&&N.close()}function _(){return n.find("md-fab-trigger")}function x(){return n.find("md-fab-actions")}var N=this,S=0;N.open=function(){e.$evalAsync("vm.isOpen = true")},N.close=function(){e.$evalAsync("vm.isOpen = false"),n.find("md-fab-trigger")[0].focus()},N.toggle=function(){e.$evalAsync("vm.isOpen = !vm.isOpen")},N.$onInit=function(){d(),s(),m(),u()},1===t.version.major&&t.version.minor<=4&&this.$onInit();var D}e.$inject=["$scope","$element","$animate","$mdUtil","$mdConstant","$timeout"],t.module("material.components.fabShared",["material.core"]).controller("MdFabController",e)}()}(),function(){!function(){function n(){function e(e,t){t.prepend('
')}return{restrict:"E",scope:{direction:"@?mdDirection",isOpen:"=?mdOpen"},bindToController:!0,controller:"MdFabController",controllerAs:"vm",link:e}}function o(n){function o(e){n(e,r,!1)}function i(n){if(!n.hasClass("md-animations-waiting")||n.hasClass("_md-animations-ready")){var o=n[0],i=n.controller("mdFabSpeedDial"),r=o.querySelectorAll(".md-fab-action-item"),a=o.querySelector("md-fab-trigger"),d=o.querySelector("._md-css-variables"),s=parseInt(e.getComputedStyle(d).zIndex);t.forEach(r,function(e,t){var n=e.style;n.transform=n.webkitTransform="",n.transitionDelay="",n.opacity=1,n.zIndex=r.length-t+s}),a.style.zIndex=s+r.length+1,i.isOpen||t.forEach(r,function(e,t){var n,o,r=e.style,d=(a.clientHeight-e.clientHeight)/2,s=(a.clientWidth-e.clientWidth)/2;switch(i.direction){case"up":n=e.scrollHeight*(t+1)+d,o="Y";break;case"down":n=-(e.scrollHeight*(t+1)+d),o="Y";break;case"left":n=e.scrollWidth*(t+1)+s,o="X";break;case"right":n=-(e.scrollWidth*(t+1)+s),o="X"}var c="translate"+o+"("+n+"px)";r.transform=r.webkitTransform=c})}}return{addClass:function(e,t,n){e.hasClass("md-fling")?(i(e),o(n)):n()},removeClass:function(e,t,n){i(e),o(n)}}}function i(n){function o(e){n(e,r,!1)}function i(n){var o=n[0],i=n.controller("mdFabSpeedDial"),r=o.querySelectorAll(".md-fab-action-item"),d=o.querySelector("._md-css-variables"),s=parseInt(e.getComputedStyle(d).zIndex);t.forEach(r,function(e,t){var n=e.style,o=t*a;n.opacity=i.isOpen?1:0,n.transform=n.webkitTransform=i.isOpen?"scale(1)":"scale(0)",n.transitionDelay=(i.isOpen?o:r.length-o)+"ms",n.zIndex=r.length-t+s})}var a=65;return{addClass:function(e,t,n){i(e),o(n)},removeClass:function(e,t,n){i(e),o(n)}}}o.$inject=["$timeout"],i.$inject=["$timeout"];var r=300;t.module("material.components.fabSpeedDial",["material.core","material.components.fabShared","material.components.fabActions"]).directive("mdFabSpeedDial",n).animation(".md-fling",o).animation(".md-scale",i).service("mdFabSpeedDialFlingAnimation",o).service("mdFabSpeedDialScaleAnimation",i)}()}(),function(){!function(){function n(){function e(e,t,n){t.addClass("md-fab-toolbar"),t.find("md-fab-trigger").find("button").prepend('
')}return{restrict:"E",transclude:!0,template:'
',scope:{direction:"@?mdDirection",isOpen:"=?mdOpen"},bindToController:!0,controller:"MdFabController",controllerAs:"vm",link:e}}function o(){function n(n,o,i){if(o){var r=n[0],a=n.controller("mdFabToolbar"),d=r.querySelector(".md-fab-toolbar-background"),s=r.querySelector("md-fab-trigger button"),c=r.querySelector("md-toolbar"),l=r.querySelector("md-fab-trigger button md-icon"),m=n.find("md-fab-actions").children();if(s&&d){var u=e.getComputedStyle(s).getPropertyValue("background-color"),p=r.offsetWidth,h=(r.offsetHeight,2*(p/s.offsetWidth));d.style.backgroundColor=u,d.style.borderRadius=p+"px",a.isOpen?(c.style.pointerEvents="inherit",d.style.width=s.offsetWidth+"px",d.style.height=s.offsetHeight+"px",d.style.transform="scale("+h+")",d.style.transitionDelay="0ms",l&&(l.style.transitionDelay=".3s"),t.forEach(m,function(e,t){e.style.transitionDelay=25*(m.length-t)+"ms"})):(c.style.pointerEvents="none",d.style.transform="scale(1)",d.style.top="0",n.hasClass("md-right")&&(d.style.left="0",d.style.right=null),n.hasClass("md-left")&&(d.style.right="0",d.style.left=null),d.style.transitionDelay="200ms",l&&(l.style.transitionDelay="0ms"),t.forEach(m,function(e,t){e.style.transitionDelay=200+25*t+"ms"}))}}}return{addClass:function(e,t,o){n(e,t,o),o()},removeClass:function(e,t,o){n(e,t,o),o()}}}t.module("material.components.fabToolbar",["material.core","material.components.fabShared","material.components.fabActions"]).directive("mdFabToolbar",n).animation(".md-fab-toolbar",o).service("mdFabToolbarAnimation",o)}()}(),function(){function e(e,o,i,r){function a(n,a,d,s){function c(){for(var e in o.MEDIA)r(e),r.getQuery(o.MEDIA[e]).addListener(M);return r.watchResponsiveAttributes(["md-cols","md-row-height","md-gutter"],d,m)}function l(){s.layoutDelegate=t.noop,T();for(var e in o.MEDIA)r.getQuery(o.MEDIA[e]).removeListener(M)}function m(e){null==e?s.invalidateLayout():r(e)&&s.invalidateLayout()}function u(e){var o=g(),r={tileSpans:b(o),colCount:v(),rowMode:C(),rowHeight:$(),gutter:E()};if(e||!t.equals(r,A)){var d=i(r.colCount,r.tileSpans,o).map(function(e,n){return{grid:{element:a,style:f(r.colCount,n,r.gutter,r.rowMode,r.rowHeight)},tiles:e.map(function(e,i){return{element:t.element(o[i]),style:h(e.position,e.spans,r.colCount,n,r.gutter,r.rowMode,r.rowHeight)}})}}).reflow().performance();n.mdOnLayout({$event:{performance:d}}),A=r}}function p(e){return w+e+k}function h(e,t,n,o,i,r,a){var d=1/n*100,s=(n-1)/n,c=_({share:d,gutterShare:s,gutter:i}),l="rtl"!=document.dir&&"rtl"!=document.body.dir,m=l?{left:x({unit:c,offset:e.col,gutter:i}),width:N({unit:c,span:t.col,gutter:i}),paddingTop:"",marginTop:"",top:"",height:""}:{right:x({unit:c,offset:e.col,gutter:i}),width:N({unit:c,span:t.col,gutter:i}),paddingTop:"",marginTop:"",top:"",height:""};switch(r){case"fixed":m.top=x({unit:a,offset:e.row,gutter:i}),m.height=N({unit:a,span:t.row,gutter:i});break;case"ratio":var u=d/a,p=_({share:u,gutterShare:s,gutter:i});m.paddingTop=N({unit:p,span:t.row,gutter:i}),m.marginTop=x({unit:p,offset:e.row,gutter:i});break;case"fit":var h=(o-1)/o,u=1/o*100,p=_({share:u,gutterShare:h,gutter:i});m.top=x({unit:p,offset:e.row,gutter:i}),m.height=N({unit:p,span:t.row,gutter:i})}return m}function f(e,t,n,o,i){var r={};switch(o){case"fixed":r.height=N({unit:i,span:t,gutter:n}),r.paddingBottom="";break;case"ratio":var a=1===e?0:(e-1)/e,d=1/e*100,s=d*(1/i),c=_({share:s,gutterShare:a,gutter:n});r.height="",r.paddingBottom=N({unit:c,span:t,gutter:n});break;case"fit":}return r}function g(){return[].filter.call(a.children(),function(e){return"MD-GRID-TILE"==e.tagName&&!e.$$mdDestroyed})}function b(e){return[].map.call(e,function(e){var n=t.element(e).controller("mdGridTile");return{row:parseInt(r.getResponsiveAttribute(n.$attrs,"md-rowspan"),10)||1,col:parseInt(r.getResponsiveAttribute(n.$attrs,"md-colspan"),10)||1}})}function v(){var e=parseInt(r.getResponsiveAttribute(d,"md-cols"),10);if(isNaN(e))throw"md-grid-list: md-cols attribute was not found, or contained a non-numeric value";return e}function E(){return y(r.getResponsiveAttribute(d,"md-gutter")||1)}function $(){var e=r.getResponsiveAttribute(d,"md-row-height");if(!e)throw"md-grid-list: md-row-height attribute was not found";switch(C()){case"fixed":return y(e);case"ratio":var t=e.split(":");return parseFloat(t[0])/parseFloat(t[1]);case"fit":return 0}}function C(){var e=r.getResponsiveAttribute(d,"md-row-height");if(!e)throw"md-grid-list: md-row-height attribute was not found";return"fit"==e?"fit":e.indexOf(":")!==-1?"ratio":"fixed"}function y(e){return/\D$/.test(e)?e:e+"px"}a.addClass("_md"),a.attr("role","list"),s.layoutDelegate=u;var M=t.bind(s,s.invalidateLayout),T=c();n.$on("$destroy",l);var A,w=e.startSymbol(),k=e.endSymbol(),_=e(p("share")+"% - ("+p("gutter")+" * "+p("gutterShare")+")"),x=e("calc(("+p("unit")+" + "+p("gutter")+") * "+p("offset")+")"),N=e("calc(("+p("unit")+") * "+p("span")+" + ("+p("span")+" - 1) * "+p("gutter")+")")}return{restrict:"E",controller:n,scope:{mdOnLayout:"&"},link:a}}function n(e){this.layoutInvalidated=!1,this.tilesInvalidated=!1,this.$timeout_=e.nextTick,this.layoutDelegate=t.noop}function o(e){function n(t,n){var o,a,d,s,c,l;return s=e.time(function(){a=i(t,n)}),o={layoutInfo:function(){return a},map:function(t){return c=e.time(function(){var e=o.layoutInfo();d=t(e.positioning,e.rowCount)}),o},reflow:function(t){return l=e.time(function(){var e=t||r;e(d.grid,d.tiles)}),o},performance:function(){return{tileCount:n.length,layoutTime:s,mapTime:c,reflowTime:l,totalTime:s+c+l}}}}function o(e,t){e.element.css(e.style),t.forEach(function(e){e.element.css(e.style)})}function i(e,t){function n(t,n){if(t.col>e)throw"md-grid-list: Tile at position "+n+" has a colspan ("+t.col+") that exceeds the column count ("+e+")";for(var a=0,l=0;l-a=e?o():(a=c.indexOf(0,d),a!==-1&&(l=r(a+1))!==-1?d=l+1:(a=l=0,o()));return i(a,t.col,t.row),d=a+t.col,{col:a,row:s}}function o(){d=0,s++,i(0,e,-1)}function i(e,t,n){for(var o=e;o",transclude:!0,scope:{},controller:["$attrs",function(e){this.$attrs=e}],link:n}}function r(){return{template:"
",transclude:!0}}n.$inject=["$mdUtil"],o.$inject=["$mdUtil"],e.$inject=["$interpolate","$mdConstant","$mdGridLayout","$mdMedia"],i.$inject=["$mdMedia"],t.module("material.components.gridList",["material.core"]).directive("mdGridList",e).directive("mdGridTile",i).directive("mdGridTileFooter",r).directive("mdGridTileHeader",r).factory("$mdGridLayout",o),n.prototype={invalidateTiles:function(){this.tilesInvalidated=!0,this.invalidateLayout()},invalidateLayout:function(){this.layoutInvalidated||(this.layoutInvalidated=!0,this.$timeout_(t.bind(this,this.layout)))},layout:function(){try{this.layoutDelegate(this.tilesInvalidated)}finally{this.layoutInvalidated=!1,this.tilesInvalidated=!1}}}}(),function(){t.module("material.components.icon",["material.core"])}(),function(){function n(e,t){function n(t){var n=t[0].querySelector(r),o=t[0].querySelector(a);return n&&t.addClass("md-icon-left"),o&&t.addClass("md-icon-right"),function(t,n){e(n)}}function o(e,n,o,i){var r=this;r.isErrorGetter=o.mdIsError&&t(o.mdIsError),r.delegateClick=function(){r.input.focus()},r.element=n,r.setFocused=function(e){n.toggleClass("md-input-focused",!!e)},r.setHasValue=function(e){n.toggleClass("md-input-has-value",!!e)},r.setHasPlaceholder=function(e){n.toggleClass("md-input-has-placeholder",!!e)},r.setInvalid=function(e){e?i.addClass(n,"md-input-invalid"):i.removeClass(n,"md-input-invalid")},e.$watch(function(){return r.label&&r.input},function(e){e&&!r.label.attr("for")&&r.label.attr("for",r.input.attr("id"))})}o.$inject=["$scope","$element","$attrs","$animate"];var i=["INPUT","TEXTAREA","SELECT","MD-SELECT"],r=i.reduce(function(e,t){return e.concat(["md-icon ~ "+t,".md-icon ~ "+t])},[]).join(","),a=i.reduce(function(e,t){return e.concat([t+" ~ md-icon",t+" ~ .md-icon"])},[]).join(",");return{restrict:"E",compile:n,controller:o}}function o(){return{restrict:"E",require:"^?mdInputContainer",link:function(e,t,n,o){!o||n.mdNoFloat||t.hasClass("md-container-ignore")||(o.label=t,e.$on("$destroy",function(){o.label=null}))}}}function i(e,n,o,i,r){function a(a,d,s,c){function l(e){return h.setHasValue(!g.$isEmpty(e)),e}function m(){h.label&&s.$observe("required",function(e){h.label.toggleClass("md-required",e&&!E)})}function u(){h.setHasValue(d.val().length>0||(d[0].validity||{}).badInput)}function p(){function o(){d.attr("rows",1).css("height","auto").addClass("md-no-flex");var e=c();if(!$){var t=d[0].style.padding||"";$=d.css("padding",0).prop("offsetHeight"),d[0].style.padding=t}if(b&&$&&(e=Math.max(e,$*b)),v&&$){var n=$*v;n-1&&g.$formatters.splice(e,1)}}function u(){function e(e){e.preventDefault(),l=!0,u=e.clientY,p=parseFloat(d.css("height"))||d.prop("offsetHeight")}function n(e){l&&(e.preventDefault(),m(),f.addClass("md-input-resized"))}function o(e){l&&d.css("height",p+e.pointer.distanceY+"px")}function i(e){l&&(l=!1,f.removeClass("md-input-resized"))}if(!s.hasOwnProperty("mdNoResize")){var c=t.element('
'),l=!1,u=null,p=0,f=h.element,g=r.register(c,"drag",{horizontal:!1});d.wrap('
').after(c),c.on("mousedown",e),f.on("$md.dragstart",n).on("$md.drag",o).on("$md.dragend",i),a.$on("$destroy",function(){c.off("mousedown",e).remove(),f.off("$md.dragstart",n).off("$md.drag",o).off("$md.dragend",i),g(),c=null,f=null,g=null})}}var p=!s.hasOwnProperty("mdNoAutogrow");if(u(),p){var b=s.hasOwnProperty("rows")?parseInt(s.rows):NaN,v=s.hasOwnProperty("maxRows")?parseInt(s.maxRows):NaN,E=a.$on("md-resize-textarea",o),$=null,C=d[0];if(i(function(){e.nextTick(o)},10,!1),d.on("input",o),f&&g.$formatters.push(l),b||d.attr("rows",1),t.element(n).on("resize",o),a.$on("$destroy",m),s.hasOwnProperty("mdDetectHidden")){var y=function(){var e=!1;return function(){var t=0===C.offsetHeight;t===!1&&e===!0&&o(),e=t}}();a.$watch(function(){return e.nextTick(y,!1),!0})}}}var h=c[0],f=!!c[1],g=c[1]||e.fakeNgModel(),b=c[2],v=t.isDefined(s.readonly),E=e.parseAttributeBoolean(s.mdNoAsterisk),$=d[0].tagName.toLowerCase();if(h){if("hidden"===s.type)return void d.attr("aria-hidden","true");if(h.input){if(h.input[0].contains(d[0]))return;throw new Error(" can only have *one* , -
-``` - 3. Also inside the div, after the text field, code a `
-``` - 4. Add one or more MDL classes, separated by spaces, to the div container, text field, and field label using the `class` attribute. -```html -
- - -
-``` - -The multi-line text field component is ready for use. - -#### Examples - -Multi-line text field with one visible input line. -```html -
- - -
-``` - -Multi-line text field with one visible input line and floating label. -```html -
- - -
-``` - -Multi-line text field with multiple visible input lines and a maximum number of lines. -```html -
- - -
-``` - -### To include an *expandable* MDL **text field** component: - - 1. Code an "outer" `
` element to hold the expandable text field. -```html -
-... -
-``` - 2. Inside the div, code a `